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 import javax.mail.Flags; 022 import javax.mail.Folder; 023 import javax.mail.FolderNotFoundException; 024 import javax.mail.Message; 025 import javax.mail.MessagingException; 026 import javax.mail.Store; 027 import javax.mail.search.FlagTerm; 028 029 import org.apache.camel.BatchConsumer; 030 import org.apache.camel.Exchange; 031 import org.apache.camel.Processor; 032 import org.apache.camel.ShutdownRunningTask; 033 import org.apache.camel.impl.ScheduledPollConsumer; 034 import org.apache.camel.spi.ShutdownAware; 035 import org.apache.camel.spi.Synchronization; 036 import org.apache.camel.util.CastUtils; 037 import org.apache.camel.util.ObjectHelper; 038 import org.apache.commons.logging.Log; 039 import org.apache.commons.logging.LogFactory; 040 import org.springframework.mail.javamail.JavaMailSenderImpl; 041 042 /** 043 * A {@link org.apache.camel.Consumer Consumer} which consumes messages from JavaMail using a 044 * {@link javax.mail.Transport Transport} and dispatches them to the {@link Processor} 045 * 046 * @version $Revision: 980707 $ 047 */ 048 public class MailConsumer extends ScheduledPollConsumer implements BatchConsumer, ShutdownAware { 049 public static final long DEFAULT_CONSUMER_DELAY = 60 * 1000L; 050 private static final transient Log LOG = LogFactory.getLog(MailConsumer.class); 051 052 private final MailEndpoint endpoint; 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.endpoint = endpoint; 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 void poll() throws Exception { 084 // must reset for each poll 085 shutdownRunningTask = null; 086 pendingExchanges = 0; 087 088 ensureIsConnected(); 089 090 if (store == null || folder == null) { 091 throw new IllegalStateException("MailConsumer did not connect properly to the MailStore: " 092 + endpoint.getConfiguration().getMailStoreLogInformation()); 093 } 094 095 if (LOG.isDebugEnabled()) { 096 LOG.debug("Polling mailfolder: " + endpoint.getConfiguration().getMailStoreLogInformation()); 097 } 098 099 if (endpoint.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; 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 (endpoint.getConfiguration().isUnseen()) { 116 messages = folder.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false)); 117 } else { 118 messages = folder.getMessages(); 119 } 120 121 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 140 public void setMaxMessagesPerPoll(int maxMessagesPerPoll) { 141 this.maxMessagesPerPoll = maxMessagesPerPoll; 142 } 143 144 public void processBatch(Queue<Object> exchanges) throws Exception { 145 int total = exchanges.size(); 146 147 // limit if needed 148 if (maxMessagesPerPoll > 0 && total > maxMessagesPerPoll) { 149 if (LOG.isDebugEnabled()) { 150 LOG.debug("Limiting to maximum messages to poll " + maxMessagesPerPoll + " as there was " + total + " messages in this poll."); 151 } 152 total = maxMessagesPerPoll; 153 } 154 155 for (int index = 0; index < total && isBatchAllowed(); index++) { 156 // only loop if we are started (allowed to run) 157 Exchange exchange = ObjectHelper.cast(Exchange.class, exchanges.poll()); 158 // add current index and total as properties 159 exchange.setProperty(Exchange.BATCH_INDEX, index); 160 exchange.setProperty(Exchange.BATCH_SIZE, total); 161 exchange.setProperty(Exchange.BATCH_COMPLETE, index == total - 1); 162 163 // update pending number of exchanges 164 pendingExchanges = total - index - 1; 165 166 // must use the original message in case we need to workaround a charset issue when extracting mail content 167 final Message mail = exchange.getIn(MailMessage.class).getOriginalMessage(); 168 169 // add on completion to handle after work when the exchange is done 170 exchange.addOnCompletion(new Synchronization() { 171 public void onComplete(Exchange exchange) { 172 processCommit(mail, exchange); 173 } 174 175 public void onFailure(Exchange exchange) { 176 processRollback(mail, exchange); 177 } 178 179 @Override 180 public String toString() { 181 return "MailConsumerOnCompletion"; 182 } 183 }); 184 185 // process the exchange 186 processExchange(exchange); 187 } 188 } 189 190 public boolean deferShutdown(ShutdownRunningTask shutdownRunningTask) { 191 // store a reference what to do in case when shutting down and we have pending messages 192 this.shutdownRunningTask = shutdownRunningTask; 193 // do not defer shutdown 194 return false; 195 } 196 197 public int getPendingExchangesSize() { 198 // only return the real pending size in case we are configured to complete all tasks 199 if (ShutdownRunningTask.CompleteAllTasks == shutdownRunningTask) { 200 return pendingExchanges; 201 } else { 202 return 0; 203 } 204 } 205 206 public boolean isBatchAllowed() { 207 // stop if we are not running 208 boolean answer = isRunAllowed(); 209 if (!answer) { 210 return false; 211 } 212 213 if (shutdownRunningTask == null) { 214 // we are not shutting down so continue to run 215 return true; 216 } 217 218 // we are shutting down so only continue if we are configured to complete all tasks 219 return ShutdownRunningTask.CompleteAllTasks == shutdownRunningTask; 220 } 221 222 protected Queue<Exchange> createExchanges(Message[] messages) throws MessagingException { 223 Queue<Exchange> answer = new LinkedList<Exchange>(); 224 225 int fetchSize = endpoint.getConfiguration().getFetchSize(); 226 int count = fetchSize == -1 ? messages.length : Math.min(fetchSize, messages.length); 227 228 if (LOG.isDebugEnabled()) { 229 LOG.debug("Fetching " + count + " messages. Total " + messages.length + " messages."); 230 } 231 232 for (int i = 0; i < count; i++) { 233 Message message = messages[i]; 234 if (!message.getFlags().contains(Flags.Flag.DELETED)) { 235 Exchange exchange = endpoint.createExchange(message); 236 answer.add(exchange); 237 } else { 238 if (LOG.isDebugEnabled()) { 239 LOG.debug("Skipping message as it was flagged as deleted: " + MailUtils.dumpMessage(message)); 240 } 241 } 242 } 243 244 return answer; 245 } 246 247 /** 248 * Strategy to process the mail message. 249 */ 250 protected void processExchange(Exchange exchange) throws Exception { 251 if (LOG.isDebugEnabled()) { 252 MailMessage msg = (MailMessage) exchange.getIn(); 253 LOG.debug("Processing message: " + MailUtils.dumpMessage(msg.getMessage())); 254 } 255 getProcessor().process(exchange); 256 } 257 258 /** 259 * Strategy to flag the message after being processed. 260 * 261 * @param mail the mail message 262 * @param exchange the exchange 263 */ 264 protected void processCommit(Message mail, Exchange exchange) { 265 try { 266 if (endpoint.getConfiguration().isDelete()) { 267 LOG.debug("Exchange processed, so flagging message as DELETED"); 268 mail.setFlag(Flags.Flag.DELETED, true); 269 } else { 270 LOG.debug("Exchange processed, so flagging message as SEEN"); 271 mail.setFlag(Flags.Flag.SEEN, true); 272 } 273 } catch (MessagingException e) { 274 LOG.warn("Error occurred during flagging message as DELETED/SEEN", e); 275 exchange.setException(e); 276 } 277 } 278 279 /** 280 * Strategy when processing the exchange failed. 281 * 282 * @param mail the mail message 283 * @param exchange the exchange 284 */ 285 protected void processRollback(Message mail, Exchange exchange) { 286 Exception cause = exchange.getException(); 287 if (cause != null) { 288 LOG.warn("Exchange failed, so rolling back message status: " + exchange, cause); 289 } else { 290 LOG.warn("Exchange failed, so rolling back message status: " + exchange); 291 } 292 } 293 294 private void ensureIsConnected() throws MessagingException { 295 MailConfiguration config = endpoint.getConfiguration(); 296 297 boolean connected = false; 298 try { 299 if (store != null && store.isConnected()) { 300 connected = true; 301 } 302 } catch (Exception e) { 303 LOG.debug("Exception while testing for is connected to MailStore: " 304 + endpoint.getConfiguration().getMailStoreLogInformation() 305 + ". Caused by: " + e.getMessage(), e); 306 } 307 308 if (!connected) { 309 // ensure resources get recreated on reconnection 310 store = null; 311 folder = null; 312 313 if (LOG.isDebugEnabled()) { 314 LOG.debug("Connecting to MailStore: " + endpoint.getConfiguration().getMailStoreLogInformation()); 315 } 316 store = sender.getSession().getStore(config.getProtocol()); 317 store.connect(config.getHost(), config.getPort(), config.getUsername(), config.getPassword()); 318 } 319 320 if (folder == null) { 321 if (LOG.isDebugEnabled()) { 322 LOG.debug("Getting folder " + config.getFolderName()); 323 } 324 folder = store.getFolder(config.getFolderName()); 325 if (folder == null || !folder.exists()) { 326 throw new FolderNotFoundException(folder, "Folder not found or invalid: " + config.getFolderName()); 327 } 328 } 329 } 330 331 }