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 mailfolder: " + 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 if (LOG.isDebugEnabled()) { 153 LOG.debug("Limiting to maximum messages to poll " + maxMessagesPerPoll + " as there was " + total + " messages in this poll."); 154 } 155 total = maxMessagesPerPoll; 156 } 157 158 for (int index = 0; index < total && isBatchAllowed(); index++) { 159 // only loop if we are started (allowed to run) 160 Exchange exchange = ObjectHelper.cast(Exchange.class, exchanges.poll()); 161 // add current index and total as properties 162 exchange.setProperty(Exchange.BATCH_INDEX, index); 163 exchange.setProperty(Exchange.BATCH_SIZE, total); 164 exchange.setProperty(Exchange.BATCH_COMPLETE, index == total - 1); 165 166 // update pending number of exchanges 167 pendingExchanges = total - index - 1; 168 169 // must use the original message in case we need to workaround a charset issue when extracting mail content 170 final Message mail = exchange.getIn(MailMessage.class).getOriginalMessage(); 171 172 // add on completion to handle after work when the exchange is done 173 exchange.addOnCompletion(new Synchronization() { 174 public void onComplete(Exchange exchange) { 175 processCommit(mail, exchange); 176 } 177 178 public void onFailure(Exchange exchange) { 179 processRollback(mail, exchange); 180 } 181 182 @Override 183 public String toString() { 184 return "MailConsumerOnCompletion"; 185 } 186 }); 187 188 // process the exchange 189 processExchange(exchange); 190 } 191 192 return total; 193 } 194 195 public boolean deferShutdown(ShutdownRunningTask shutdownRunningTask) { 196 // store a reference what to do in case when shutting down and we have pending messages 197 this.shutdownRunningTask = shutdownRunningTask; 198 // do not defer shutdown 199 return false; 200 } 201 202 public int getPendingExchangesSize() { 203 // only return the real pending size in case we are configured to complete all tasks 204 if (ShutdownRunningTask.CompleteAllTasks == shutdownRunningTask) { 205 return pendingExchanges; 206 } else { 207 return 0; 208 } 209 } 210 211 public void prepareShutdown() { 212 // noop 213 } 214 215 public boolean isBatchAllowed() { 216 // stop if we are not running 217 boolean answer = isRunAllowed(); 218 if (!answer) { 219 return false; 220 } 221 222 if (shutdownRunningTask == null) { 223 // we are not shutting down so continue to run 224 return true; 225 } 226 227 // we are shutting down so only continue if we are configured to complete all tasks 228 return ShutdownRunningTask.CompleteAllTasks == shutdownRunningTask; 229 } 230 231 protected Queue<Exchange> createExchanges(Message[] messages) throws MessagingException { 232 Queue<Exchange> answer = new LinkedList<Exchange>(); 233 234 int fetchSize = getEndpoint().getConfiguration().getFetchSize(); 235 int count = fetchSize == -1 ? messages.length : Math.min(fetchSize, messages.length); 236 237 if (LOG.isDebugEnabled()) { 238 LOG.debug("Fetching " + count + " messages. Total " + messages.length + " messages."); 239 } 240 241 for (int i = 0; i < count; i++) { 242 Message message = messages[i]; 243 if (!message.getFlags().contains(Flags.Flag.DELETED)) { 244 Exchange exchange = getEndpoint().createExchange(message); 245 246 // If the protocol is POP3 we need to remember the uid on the exchange 247 // so we can find the mail message again later to be able to delete it 248 if (getEndpoint().getConfiguration().getProtocol().startsWith("pop3")) { 249 String uid = generatePop3Uid(message); 250 if (uid != null) { 251 exchange.setProperty(POP3_UID, uid); 252 LOG.trace("POP3 mail message using uid {}", uid); 253 } 254 } 255 answer.add(exchange); 256 } else { 257 if (LOG.isDebugEnabled()) { 258 LOG.debug("Skipping message as it was flagged as deleted: " + MailUtils.dumpMessage(message)); 259 } 260 } 261 } 262 263 return answer; 264 } 265 266 /** 267 * Strategy to process the mail message. 268 */ 269 protected void processExchange(Exchange exchange) throws Exception { 270 if (LOG.isDebugEnabled()) { 271 MailMessage msg = (MailMessage) exchange.getIn(); 272 LOG.debug("Processing message: " + MailUtils.dumpMessage(msg.getMessage())); 273 } 274 getProcessor().process(exchange); 275 } 276 277 /** 278 * Strategy to flag the message after being processed. 279 * 280 * @param mail the mail message 281 * @param exchange the exchange 282 */ 283 protected void processCommit(Message mail, Exchange exchange) { 284 try { 285 // ensure folder is open 286 if (!folder.isOpen()) { 287 folder.open(Folder.READ_WRITE); 288 } 289 290 // If the protocol is POP3, the message needs to be synced with the folder via the UID. 291 // Otherwise setting the DELETE/SEEN flag won't delete the message. 292 String uid = (String) exchange.removeProperty(POP3_UID); 293 if (uid != null) { 294 int count = folder.getMessageCount(); 295 Message found = null; 296 LOG.trace("Looking for POP3Message with UID {} from folder with {} mails", uid, count); 297 for (int i = 1; i <= count; ++i) { 298 Message msg = folder.getMessage(i); 299 if (uid.equals(generatePop3Uid(msg))) { 300 LOG.debug("Found POP3Message with UID {} from folder with {} mails", uid, count); 301 found = msg; 302 break; 303 } 304 } 305 306 if (found == null) { 307 boolean delete = getEndpoint().getConfiguration().isDelete(); 308 LOG.warn("POP3message not found in folder. Message cannot be marked as " + (delete ? "DELETED" : "SEEN")); 309 } else { 310 mail = found; 311 } 312 } 313 314 if (getEndpoint().getConfiguration().isDelete()) { 315 LOG.trace("Exchange processed, so flagging message as DELETED"); 316 mail.setFlag(Flags.Flag.DELETED, true); 317 } else { 318 LOG.trace("Exchange processed, so flagging message as SEEN"); 319 mail.setFlag(Flags.Flag.SEEN, true); 320 } 321 } catch (MessagingException e) { 322 LOG.warn("Error occurred during flagging message as DELETED/SEEN", e); 323 exchange.setException(e); 324 } 325 } 326 327 /** 328 * Strategy when processing the exchange failed. 329 * 330 * @param mail the mail message 331 * @param exchange the exchange 332 */ 333 protected void processRollback(Message mail, Exchange exchange) { 334 Exception cause = exchange.getException(); 335 if (cause != null) { 336 LOG.warn("Exchange failed, so rolling back message status: " + exchange, cause); 337 } else { 338 LOG.warn("Exchange failed, so rolling back message status: " + exchange); 339 } 340 } 341 342 /** 343 * Generates an UID of the POP3Message 344 * 345 * @param message the POP3Message 346 * @return the generated uid 347 */ 348 protected String generatePop3Uid(Message message) { 349 String uid = null; 350 351 // create an UID based on message headers on the POP3Message, that ought to be unique 352 StringBuilder buffer = new StringBuilder(); 353 try { 354 Enumeration it = message.getAllHeaders(); 355 while (it.hasMoreElements()) { 356 Header header = (Header) it.nextElement(); 357 buffer.append(header.getName()) 358 .append("=") 359 .append(header.getValue()) 360 .append("\n"); 361 } 362 if (buffer.length() > 0) { 363 LOG.debug("Generating UID from the following:\n" + buffer); 364 uid = UUID.nameUUIDFromBytes(buffer.toString().getBytes()).toString(); 365 } 366 } catch (MessagingException e) { 367 LOG.warn("Cannot reader headers from mail message. This exception will be ignored.", e); 368 } 369 370 return uid; 371 } 372 373 private void ensureIsConnected() throws MessagingException { 374 MailConfiguration config = getEndpoint().getConfiguration(); 375 376 boolean connected = false; 377 try { 378 if (store != null && store.isConnected()) { 379 connected = true; 380 } 381 } catch (Exception e) { 382 LOG.debug("Exception while testing for is connected to MailStore: " 383 + getEndpoint().getConfiguration().getMailStoreLogInformation() 384 + ". Caused by: " + e.getMessage(), e); 385 } 386 387 if (!connected) { 388 // ensure resources get recreated on reconnection 389 store = null; 390 folder = null; 391 392 if (LOG.isDebugEnabled()) { 393 LOG.debug("Connecting to MailStore: " + getEndpoint().getConfiguration().getMailStoreLogInformation()); 394 } 395 store = sender.getSession().getStore(config.getProtocol()); 396 store.connect(config.getHost(), config.getPort(), config.getUsername(), config.getPassword()); 397 } 398 399 if (folder == null) { 400 if (LOG.isDebugEnabled()) { 401 LOG.debug("Getting folder " + config.getFolderName()); 402 } 403 folder = store.getFolder(config.getFolderName()); 404 if (folder == null || !folder.exists()) { 405 throw new FolderNotFoundException(folder, "Folder not found or invalid: " + config.getFolderName()); 406 } 407 } 408 } 409 410 @Override 411 public MailEndpoint getEndpoint() { 412 return (MailEndpoint) super.getEndpoint(); 413 } 414 415 }