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.Enumeration;
020 import java.util.LinkedList;
021 import java.util.Queue;
022 import java.util.UUID;
023 import javax.mail.Flags;
024 import javax.mail.Folder;
025 import javax.mail.FolderNotFoundException;
026 import javax.mail.Header;
027 import javax.mail.Message;
028 import javax.mail.MessagingException;
029 import javax.mail.Store;
030 import javax.mail.search.FlagTerm;
031
032 import org.apache.camel.BatchConsumer;
033 import org.apache.camel.Exchange;
034 import org.apache.camel.Processor;
035 import org.apache.camel.ShutdownRunningTask;
036 import org.apache.camel.impl.ScheduledPollConsumer;
037 import org.apache.camel.spi.ShutdownAware;
038 import org.apache.camel.spi.Synchronization;
039 import org.apache.camel.util.CastUtils;
040 import org.apache.camel.util.ObjectHelper;
041 import org.slf4j.Logger;
042 import org.slf4j.LoggerFactory;
043 import org.springframework.mail.javamail.JavaMailSenderImpl;
044
045 /**
046 * A {@link org.apache.camel.Consumer Consumer} which consumes messages from JavaMail using a
047 * {@link javax.mail.Transport Transport} and dispatches them to the {@link Processor}
048 */
049 public class MailConsumer extends ScheduledPollConsumer implements BatchConsumer, ShutdownAware {
050 public static final String POP3_UID = "CamelPop3Uid";
051 public static final long DEFAULT_CONSUMER_DELAY = 60 * 1000L;
052 private static final transient Logger LOG = LoggerFactory.getLogger(MailConsumer.class);
053
054 private final JavaMailSenderImpl sender;
055 private Folder folder;
056 private Store store;
057 private int maxMessagesPerPoll;
058 private volatile ShutdownRunningTask shutdownRunningTask;
059 private volatile int pendingExchanges;
060
061 public MailConsumer(MailEndpoint endpoint, Processor processor, JavaMailSenderImpl sender) {
062 super(endpoint, processor);
063 this.sender = sender;
064 }
065
066 @Override
067 protected void doStart() throws Exception {
068 super.doStart();
069 }
070
071 @Override
072 protected void doStop() throws Exception {
073 if (folder != null && folder.isOpen()) {
074 folder.close(true);
075 }
076 if (store != null && store.isConnected()) {
077 store.close();
078 }
079
080 super.doStop();
081 }
082
083 protected int poll() throws Exception {
084 // must reset for each poll
085 shutdownRunningTask = null;
086 pendingExchanges = 0;
087 int polledMessages = 0;
088
089 ensureIsConnected();
090
091 if (store == null || folder == null) {
092 throw new IllegalStateException("MailConsumer did not connect properly to the MailStore: "
093 + getEndpoint().getConfiguration().getMailStoreLogInformation());
094 }
095
096 if (LOG.isDebugEnabled()) {
097 LOG.debug("Polling mailbox folder: " + getEndpoint().getConfiguration().getMailStoreLogInformation());
098 }
099
100 if (getEndpoint().getConfiguration().getFetchSize() == 0) {
101 LOG.warn("Fetch size is 0 meaning the configuration is set to poll no new messages at all. Camel will skip this poll.");
102 return 0;
103 }
104
105 // ensure folder is open
106 if (!folder.isOpen()) {
107 folder.open(Folder.READ_WRITE);
108 }
109
110 try {
111 int count = folder.getMessageCount();
112 if (count > 0) {
113 Message[] messages;
114
115 // should we process all messages or only unseen messages
116 if (getEndpoint().getConfiguration().isUnseen()) {
117 messages = folder.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
118 } else {
119 messages = folder.getMessages();
120 }
121
122 polledMessages = processBatch(CastUtils.cast(createExchanges(messages)));
123 } else if (count == -1) {
124 throw new MessagingException("Folder: " + folder.getFullName() + " is closed");
125 }
126 } catch (Exception e) {
127 handleException(e);
128 } finally {
129 // need to ensure we release resources
130 try {
131 if (folder.isOpen()) {
132 folder.close(true);
133 }
134 } catch (Exception e) {
135 // some mail servers will lock the folder so we ignore in this case (CAMEL-1263)
136 LOG.debug("Could not close mailbox folder: " + folder.getName(), e);
137 }
138 }
139
140 return polledMessages;
141 }
142
143 public void setMaxMessagesPerPoll(int maxMessagesPerPoll) {
144 this.maxMessagesPerPoll = maxMessagesPerPoll;
145 }
146
147 public int processBatch(Queue<Object> exchanges) throws Exception {
148 int total = exchanges.size();
149
150 // limit if needed
151 if (maxMessagesPerPoll > 0 && total > maxMessagesPerPoll) {
152 LOG.debug("Limiting to maximum messages to poll {} as there was {} messages in this poll.", maxMessagesPerPoll, total);
153 total = maxMessagesPerPoll;
154 }
155
156 for (int index = 0; index < total && isBatchAllowed(); index++) {
157 // only loop if we are started (allowed to run)
158 Exchange exchange = ObjectHelper.cast(Exchange.class, exchanges.poll());
159 // add current index and total as properties
160 exchange.setProperty(Exchange.BATCH_INDEX, index);
161 exchange.setProperty(Exchange.BATCH_SIZE, total);
162 exchange.setProperty(Exchange.BATCH_COMPLETE, index == total - 1);
163
164 // update pending number of exchanges
165 pendingExchanges = total - index - 1;
166
167 // must use the original message in case we need to workaround a charset issue when extracting mail content
168 final Message mail = exchange.getIn(MailMessage.class).getOriginalMessage();
169
170 // add on completion to handle after work when the exchange is done
171 exchange.addOnCompletion(new Synchronization() {
172 public void onComplete(Exchange exchange) {
173 processCommit(mail, exchange);
174 }
175
176 public void onFailure(Exchange exchange) {
177 processRollback(mail, exchange);
178 }
179
180 @Override
181 public String toString() {
182 return "MailConsumerOnCompletion";
183 }
184 });
185
186 // process the exchange
187 processExchange(exchange);
188 }
189
190 return total;
191 }
192
193 public boolean deferShutdown(ShutdownRunningTask shutdownRunningTask) {
194 // store a reference what to do in case when shutting down and we have pending messages
195 this.shutdownRunningTask = shutdownRunningTask;
196 // do not defer shutdown
197 return false;
198 }
199
200 public int getPendingExchangesSize() {
201 int answer;
202 // only return the real pending size in case we are configured to complete all tasks
203 if (ShutdownRunningTask.CompleteAllTasks == shutdownRunningTask) {
204 answer = pendingExchanges;
205 } else {
206 answer = 0;
207 }
208
209 if (answer == 0 && isPolling()) {
210 // force at least one pending exchange if we are polling as there is a little gap
211 // in the processBatch method and until an exchange gets enlisted as in-flight
212 // which happens later, so we need to signal back to the shutdown strategy that
213 // there is a pending exchange. When we are no longer polling, then we will return 0
214 log.trace("Currently polling so returning 1 as pending exchanges");
215 answer = 1;
216 }
217
218 return answer;
219 }
220
221 public void prepareShutdown() {
222 // noop
223 }
224
225 public boolean isBatchAllowed() {
226 // stop if we are not running
227 boolean answer = isRunAllowed();
228 if (!answer) {
229 return false;
230 }
231
232 if (shutdownRunningTask == null) {
233 // we are not shutting down so continue to run
234 return true;
235 }
236
237 // we are shutting down so only continue if we are configured to complete all tasks
238 return ShutdownRunningTask.CompleteAllTasks == shutdownRunningTask;
239 }
240
241 protected Queue<Exchange> createExchanges(Message[] messages) throws MessagingException {
242 Queue<Exchange> answer = new LinkedList<Exchange>();
243
244 int fetchSize = getEndpoint().getConfiguration().getFetchSize();
245 int count = fetchSize == -1 ? messages.length : Math.min(fetchSize, messages.length);
246
247 if (LOG.isDebugEnabled()) {
248 LOG.debug("Fetching {} messages. Total {} messages.", count, messages.length);
249 }
250
251 for (int i = 0; i < count; i++) {
252 Message message = messages[i];
253 if (!message.getFlags().contains(Flags.Flag.DELETED)) {
254 Exchange exchange = getEndpoint().createExchange(message);
255
256 // If the protocol is POP3 we need to remember the uid on the exchange
257 // so we can find the mail message again later to be able to delete it
258 if (getEndpoint().getConfiguration().getProtocol().startsWith("pop3")) {
259 String uid = generatePop3Uid(message);
260 if (uid != null) {
261 exchange.setProperty(POP3_UID, uid);
262 LOG.trace("POP3 mail message using uid {}", uid);
263 }
264 }
265 answer.add(exchange);
266 } else {
267 if (LOG.isDebugEnabled()) {
268 LOG.debug("Skipping message as it was flagged as deleted: {}", MailUtils.dumpMessage(message));
269 }
270 }
271 }
272
273 return answer;
274 }
275
276 /**
277 * Strategy to process the mail message.
278 */
279 protected void processExchange(Exchange exchange) throws Exception {
280 if (LOG.isDebugEnabled()) {
281 MailMessage msg = (MailMessage) exchange.getIn();
282 LOG.debug("Processing message: {}", MailUtils.dumpMessage(msg.getMessage()));
283 }
284 getProcessor().process(exchange);
285 }
286
287 /**
288 * Strategy to flag the message after being processed.
289 *
290 * @param mail the mail message
291 * @param exchange the exchange
292 */
293 protected void processCommit(Message mail, Exchange exchange) {
294 try {
295 // ensure folder is open
296 if (!folder.isOpen()) {
297 folder.open(Folder.READ_WRITE);
298 }
299
300 // If the protocol is POP3, the message needs to be synced with the folder via the UID.
301 // Otherwise setting the DELETE/SEEN flag won't delete the message.
302 String uid = (String) exchange.removeProperty(POP3_UID);
303 if (uid != null) {
304 int count = folder.getMessageCount();
305 Message found = null;
306 LOG.trace("Looking for POP3Message with UID {} from folder with {} mails", uid, count);
307 for (int i = 1; i <= count; ++i) {
308 Message msg = folder.getMessage(i);
309 if (uid.equals(generatePop3Uid(msg))) {
310 LOG.debug("Found POP3Message with UID {} from folder with {} mails", uid, count);
311 found = msg;
312 break;
313 }
314 }
315
316 if (found == null) {
317 boolean delete = getEndpoint().getConfiguration().isDelete();
318 LOG.warn("POP3message not found in folder. Message cannot be marked as " + (delete ? "DELETED" : "SEEN"));
319 } else {
320 mail = found;
321 }
322 }
323
324 if (getEndpoint().getConfiguration().isDelete()) {
325 LOG.trace("Exchange processed, so flagging message as DELETED");
326 mail.setFlag(Flags.Flag.DELETED, true);
327 } else {
328 LOG.trace("Exchange processed, so flagging message as SEEN");
329 mail.setFlag(Flags.Flag.SEEN, true);
330 }
331 } catch (MessagingException e) {
332 LOG.warn("Error occurred during flagging message as DELETED/SEEN", e);
333 exchange.setException(e);
334 }
335 }
336
337 /**
338 * Strategy when processing the exchange failed.
339 *
340 * @param mail the mail message
341 * @param exchange the exchange
342 */
343 protected void processRollback(Message mail, Exchange exchange) {
344 Exception cause = exchange.getException();
345 if (cause != null) {
346 LOG.warn("Exchange failed, so rolling back message status: " + exchange, cause);
347 } else {
348 LOG.warn("Exchange failed, so rolling back message status: " + exchange);
349 }
350 }
351
352 /**
353 * Generates an UID of the POP3Message
354 *
355 * @param message the POP3Message
356 * @return the generated uid
357 */
358 protected String generatePop3Uid(Message message) {
359 String uid = null;
360
361 // create an UID based on message headers on the POP3Message, that ought
362 // to be unique
363 StringBuilder buffer = new StringBuilder();
364 try {
365 Enumeration it = message.getAllHeaders();
366 while (it.hasMoreElements()) {
367 Header header = (Header)it.nextElement();
368 buffer.append(header.getName()).append("=").append(header.getValue()).append("\n");
369 }
370 if (buffer.length() > 0) {
371 LOG.debug("Generating UID from the following:\n" + buffer);
372 uid = UUID.nameUUIDFromBytes(buffer.toString().getBytes()).toString();
373 }
374 } catch (MessagingException e) {
375 LOG.warn("Cannot reader headers from mail message. This exception will be ignored.", e);
376 }
377
378 return uid;
379 }
380
381 private void ensureIsConnected() throws MessagingException {
382 MailConfiguration config = getEndpoint().getConfiguration();
383
384 boolean connected = false;
385 try {
386 if (store != null && store.isConnected()) {
387 connected = true;
388 }
389 } catch (Exception e) {
390 LOG.debug("Exception while testing for is connected to MailStore: "
391 + getEndpoint().getConfiguration().getMailStoreLogInformation()
392 + ". Caused by: " + e.getMessage(), e);
393 }
394
395 if (!connected) {
396 // ensure resources get recreated on reconnection
397 store = null;
398 folder = null;
399
400 if (LOG.isDebugEnabled()) {
401 LOG.debug("Connecting to MailStore: {}", getEndpoint().getConfiguration().getMailStoreLogInformation());
402 }
403 store = sender.getSession().getStore(config.getProtocol());
404 store.connect(config.getHost(), config.getPort(), config.getUsername(), config.getPassword());
405 }
406
407 if (folder == null) {
408 if (LOG.isDebugEnabled()) {
409 LOG.debug("Getting folder {}", config.getFolderName());
410 }
411 folder = store.getFolder(config.getFolderName());
412 if (folder == null || !folder.exists()) {
413 throw new FolderNotFoundException(folder, "Folder not found or invalid: " + config.getFolderName());
414 }
415 }
416 }
417
418 @Override
419 public MailEndpoint getEndpoint() {
420 return (MailEndpoint) super.getEndpoint();
421 }
422
423 }