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.slf4j.Logger; 040 import org.slf4j.LoggerFactory; 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 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 Logger LOG = LoggerFactory.getLogger(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 }