001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.camel.component.mail;
018
019 import java.util.LinkedList;
020 import java.util.Queue;
021
022 import javax.mail.Flags;
023 import javax.mail.Folder;
024 import javax.mail.FolderNotFoundException;
025 import javax.mail.Message;
026 import javax.mail.MessagingException;
027 import javax.mail.Store;
028 import javax.mail.search.FlagTerm;
029
030 import org.apache.camel.BatchConsumer;
031 import org.apache.camel.Exchange;
032 import org.apache.camel.Processor;
033 import org.apache.camel.ShutdownRunningTask;
034 import org.apache.camel.impl.ScheduledPollConsumer;
035 import org.apache.camel.spi.ShutdownAware;
036 import org.apache.camel.spi.Synchronization;
037 import org.apache.camel.util.CastUtils;
038 import org.apache.camel.util.ObjectHelper;
039 import org.apache.commons.logging.Log;
040 import org.apache.commons.logging.LogFactory;
041 import org.springframework.mail.javamail.JavaMailSenderImpl;
042
043 /**
044 * A {@link org.apache.camel.Consumer Consumer} which consumes messages from JavaMail using a
045 * {@link javax.mail.Transport Transport} and dispatches them to the {@link Processor}
046 *
047 * @version $Revision: 22874 $
048 */
049 public class MailConsumer extends ScheduledPollConsumer implements BatchConsumer, ShutdownAware {
050 public static final long DEFAULT_CONSUMER_DELAY = 60 * 1000L;
051 private static final transient Log LOG = LogFactory.getLog(MailConsumer.class);
052
053 private final JavaMailSenderImpl sender;
054 private Folder folder;
055 private Store store;
056 private int maxMessagesPerPoll;
057 private volatile ShutdownRunningTask shutdownRunningTask;
058 private volatile int pendingExchanges;
059
060 public MailConsumer(MailEndpoint endpoint, Processor processor, JavaMailSenderImpl sender) {
061 super(endpoint, processor);
062 this.sender = sender;
063 }
064
065 @Override
066 protected void doStart() throws Exception {
067 super.doStart();
068 }
069
070 @Override
071 protected void doStop() throws Exception {
072 if (folder != null && folder.isOpen()) {
073 folder.close(true);
074 }
075 if (store != null && store.isConnected()) {
076 store.close();
077 }
078
079 super.doStop();
080 }
081
082 protected int poll() throws Exception {
083 // must reset for each poll
084 shutdownRunningTask = null;
085 pendingExchanges = 0;
086 int polledMessages = 0;
087
088 ensureIsConnected();
089
090 if (store == null || folder == null) {
091 throw new IllegalStateException("MailConsumer did not connect properly to the MailStore: "
092 + getEndpoint().getConfiguration().getMailStoreLogInformation());
093 }
094
095 if (LOG.isDebugEnabled()) {
096 LOG.debug("Polling mailfolder: " + getEndpoint().getConfiguration().getMailStoreLogInformation());
097 }
098
099 if (getEndpoint().getConfiguration().getFetchSize() == 0) {
100 LOG.warn("Fetch size is 0 meaning the configuration is set to poll no new messages at all. Camel will skip this poll.");
101 return 0;
102 }
103
104 // ensure folder is open
105 if (!folder.isOpen()) {
106 folder.open(Folder.READ_WRITE);
107 }
108
109 try {
110 int count = folder.getMessageCount();
111 if (count > 0) {
112 Message[] messages;
113
114 // should we process all messages or only unseen messages
115 if (getEndpoint().getConfiguration().isUnseen()) {
116 messages = folder.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
117 } else {
118 messages = folder.getMessages();
119 }
120
121 polledMessages = processBatch(CastUtils.cast(createExchanges(messages)));
122 } else if (count == -1) {
123 throw new MessagingException("Folder: " + folder.getFullName() + " is closed");
124 }
125 } catch (Exception e) {
126 handleException(e);
127 } finally {
128 // need to ensure we release resources
129 try {
130 if (folder.isOpen()) {
131 folder.close(true);
132 }
133 } catch (Exception e) {
134 // some mail servers will lock the folder so we ignore in this case (CAMEL-1263)
135 LOG.debug("Could not close mailbox folder: " + folder.getName(), e);
136 }
137 }
138
139 return polledMessages;
140 }
141
142 public void setMaxMessagesPerPoll(int maxMessagesPerPoll) {
143 this.maxMessagesPerPoll = maxMessagesPerPoll;
144 }
145
146 public int processBatch(Queue<Object> exchanges) throws Exception {
147 int total = exchanges.size();
148
149 // limit if needed
150 if (maxMessagesPerPoll > 0 && total > maxMessagesPerPoll) {
151 if (LOG.isDebugEnabled()) {
152 LOG.debug("Limiting to maximum messages to poll " + maxMessagesPerPoll + " as there was " + total + " messages in this poll.");
153 }
154 total = maxMessagesPerPoll;
155 }
156
157 for (int index = 0; index < total && isBatchAllowed(); index++) {
158 // only loop if we are started (allowed to run)
159 Exchange exchange = ObjectHelper.cast(Exchange.class, exchanges.poll());
160 // add current index and total as properties
161 exchange.setProperty(Exchange.BATCH_INDEX, index);
162 exchange.setProperty(Exchange.BATCH_SIZE, total);
163 exchange.setProperty(Exchange.BATCH_COMPLETE, index == total - 1);
164
165 // update pending number of exchanges
166 pendingExchanges = total - index - 1;
167
168 // must use the original message in case we need to workaround a charset issue when extracting mail content
169 final Message mail = exchange.getIn(MailMessage.class).getOriginalMessage();
170
171 // add on completion to handle after work when the exchange is done
172 exchange.addOnCompletion(new Synchronization() {
173 public void onComplete(Exchange exchange) {
174 processCommit(mail, exchange);
175 }
176
177 public void onFailure(Exchange exchange) {
178 processRollback(mail, exchange);
179 }
180
181 @Override
182 public String toString() {
183 return "MailConsumerOnCompletion";
184 }
185 });
186
187 // process the exchange
188 processExchange(exchange);
189 }
190
191 return total;
192 }
193
194 public boolean deferShutdown(ShutdownRunningTask shutdownRunningTask) {
195 // store a reference what to do in case when shutting down and we have pending messages
196 this.shutdownRunningTask = shutdownRunningTask;
197 // do not defer shutdown
198 return false;
199 }
200
201 public int getPendingExchangesSize() {
202 // only return the real pending size in case we are configured to complete all tasks
203 if (ShutdownRunningTask.CompleteAllTasks == shutdownRunningTask) {
204 return pendingExchanges;
205 } else {
206 return 0;
207 }
208 }
209
210 public void prepareShutdown() {
211 // noop
212 }
213
214 public boolean isBatchAllowed() {
215 // stop if we are not running
216 boolean answer = isRunAllowed();
217 if (!answer) {
218 return false;
219 }
220
221 if (shutdownRunningTask == null) {
222 // we are not shutting down so continue to run
223 return true;
224 }
225
226 // we are shutting down so only continue if we are configured to complete all tasks
227 return ShutdownRunningTask.CompleteAllTasks == shutdownRunningTask;
228 }
229
230 protected Queue<Exchange> createExchanges(Message[] messages) throws MessagingException {
231 Queue<Exchange> answer = new LinkedList<Exchange>();
232
233 int fetchSize = getEndpoint().getConfiguration().getFetchSize();
234 int count = fetchSize == -1 ? messages.length : Math.min(fetchSize, messages.length);
235
236 if (LOG.isDebugEnabled()) {
237 LOG.debug("Fetching " + count + " messages. Total " + messages.length + " messages.");
238 }
239
240 for (int i = 0; i < count; i++) {
241 Message message = messages[i];
242 if (!message.getFlags().contains(Flags.Flag.DELETED)) {
243 Exchange exchange = getEndpoint().createExchange(message);
244 answer.add(exchange);
245 } else {
246 if (LOG.isDebugEnabled()) {
247 LOG.debug("Skipping message as it was flagged as deleted: " + MailUtils.dumpMessage(message));
248 }
249 }
250 }
251
252 return answer;
253 }
254
255 /**
256 * Strategy to process the mail message.
257 */
258 protected void processExchange(Exchange exchange) throws Exception {
259 if (LOG.isDebugEnabled()) {
260 MailMessage msg = (MailMessage) exchange.getIn();
261 LOG.debug("Processing message: " + MailUtils.dumpMessage(msg.getMessage()));
262 }
263 getProcessor().process(exchange);
264 }
265
266 /**
267 * Strategy to flag the message after being processed.
268 *
269 * @param mail the mail message
270 * @param exchange the exchange
271 */
272 protected void processCommit(Message mail, Exchange exchange) {
273 try {
274 if (getEndpoint().getConfiguration().isDelete()) {
275 LOG.debug("Exchange processed, so flagging message as DELETED");
276 mail.setFlag(Flags.Flag.DELETED, true);
277 } else {
278 LOG.debug("Exchange processed, so flagging message as SEEN");
279 mail.setFlag(Flags.Flag.SEEN, true);
280 }
281 } catch (MessagingException e) {
282 LOG.warn("Error occurred during flagging message as DELETED/SEEN", e);
283 exchange.setException(e);
284 }
285 }
286
287 /**
288 * Strategy when processing the exchange failed.
289 *
290 * @param mail the mail message
291 * @param exchange the exchange
292 */
293 protected void processRollback(Message mail, Exchange exchange) {
294 Exception cause = exchange.getException();
295 if (cause != null) {
296 LOG.warn("Exchange failed, so rolling back message status: " + exchange, cause);
297 } else {
298 LOG.warn("Exchange failed, so rolling back message status: " + exchange);
299 }
300 }
301
302 private void ensureIsConnected() throws MessagingException {
303 MailConfiguration config = getEndpoint().getConfiguration();
304
305 boolean connected = false;
306 try {
307 if (store != null && store.isConnected()) {
308 connected = true;
309 }
310 } catch (Exception e) {
311 LOG.debug("Exception while testing for is connected to MailStore: "
312 + getEndpoint().getConfiguration().getMailStoreLogInformation()
313 + ". Caused by: " + e.getMessage(), e);
314 }
315
316 if (!connected) {
317 // ensure resources get recreated on reconnection
318 store = null;
319 folder = null;
320
321 if (LOG.isDebugEnabled()) {
322 LOG.debug("Connecting to MailStore: " + getEndpoint().getConfiguration().getMailStoreLogInformation());
323 }
324 store = sender.getSession().getStore(config.getProtocol());
325 store.connect(config.getHost(), config.getPort(), config.getUsername(), config.getPassword());
326 }
327
328 if (folder == null) {
329 if (LOG.isDebugEnabled()) {
330 LOG.debug("Getting folder " + config.getFolderName());
331 }
332 folder = store.getFolder(config.getFolderName());
333 if (folder == null || !folder.exists()) {
334 throw new FolderNotFoundException(folder, "Folder not found or invalid: " + config.getFolderName());
335 }
336 }
337 }
338
339 @Override
340 public MailEndpoint getEndpoint() {
341 return (MailEndpoint) super.getEndpoint();
342 }
343 }