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.util.CastUtils; 036 import org.apache.camel.util.ObjectHelper; 037 import org.apache.commons.logging.Log; 038 import org.apache.commons.logging.LogFactory; 039 import org.springframework.mail.javamail.JavaMailSenderImpl; 040 041 /** 042 * A {@link org.apache.camel.Consumer Consumer} which consumes messages from JavaMail using a 043 * {@link javax.mail.Transport Transport} and dispatches them to the {@link Processor} 044 * 045 * @version $Revision: 897106 $ 046 */ 047 public class MailConsumer extends ScheduledPollConsumer implements BatchConsumer, ShutdownAware { 048 public static final long DEFAULT_CONSUMER_DELAY = 60 * 1000L; 049 private static final transient Log LOG = LogFactory.getLog(MailConsumer.class); 050 051 private final MailEndpoint endpoint; 052 private final JavaMailSenderImpl sender; 053 private Folder folder; 054 private Store store; 055 private int maxMessagesPerPoll; 056 private volatile ShutdownRunningTask shutdownRunningTask; 057 private volatile int pendingExchanges; 058 059 public MailConsumer(MailEndpoint endpoint, Processor processor, JavaMailSenderImpl sender) { 060 super(endpoint, processor); 061 this.endpoint = endpoint; 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 void poll() throws Exception { 083 // must reset for each poll 084 shutdownRunningTask = null; 085 pendingExchanges = 0; 086 087 ensureIsConnected(); 088 089 if (store == null || folder == null) { 090 throw new IllegalStateException("MailConsumer did not connect properly to the MailStore: " 091 + endpoint.getConfiguration().getMailStoreLogInformation()); 092 } 093 094 if (LOG.isDebugEnabled()) { 095 LOG.debug("Polling mailfolder: " + endpoint.getConfiguration().getMailStoreLogInformation()); 096 } 097 098 if (endpoint.getConfiguration().getFetchSize() == 0) { 099 LOG.warn("Fetch size is 0 meaning the configuration is set to poll no new messages at all. Camel will skip this poll."); 100 return; 101 } 102 103 // ensure folder is open 104 if (!folder.isOpen()) { 105 folder.open(Folder.READ_WRITE); 106 } 107 108 try { 109 int count = folder.getMessageCount(); 110 if (count > 0) { 111 Message[] messages; 112 113 // should we process all messages or only unseen messages 114 if (endpoint.getConfiguration().isUnseen()) { 115 messages = folder.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false)); 116 } else { 117 messages = folder.getMessages(); 118 } 119 120 processBatch(CastUtils.cast(createExchanges(messages))); 121 } else if (count == -1) { 122 throw new MessagingException("Folder: " + folder.getFullName() + " is closed"); 123 } 124 } catch (Exception e) { 125 handleException(e); 126 } finally { 127 // need to ensure we release resources 128 try { 129 if (folder.isOpen()) { 130 folder.close(true); 131 } 132 } catch (Exception e) { 133 // some mail servers will lock the folder so we ignore in this case (CAMEL-1263) 134 LOG.debug("Could not close mailbox folder: " + folder.getName(), e); 135 } 136 } 137 } 138 139 public void setMaxMessagesPerPoll(int maxMessagesPerPoll) { 140 this.maxMessagesPerPoll = maxMessagesPerPoll; 141 } 142 143 public void processBatch(Queue<Object> exchanges) throws Exception { 144 int total = exchanges.size(); 145 146 // limit if needed 147 if (maxMessagesPerPoll > 0 && total > maxMessagesPerPoll) { 148 if (LOG.isDebugEnabled()) { 149 LOG.debug("Limiting to maximum messages to poll " + maxMessagesPerPoll + " as there was " + total + " messages in this poll."); 150 } 151 total = maxMessagesPerPoll; 152 } 153 154 for (int index = 0; index < total && isBatchAllowed(); index++) { 155 // only loop if we are started (allowed to run) 156 Exchange exchange = ObjectHelper.cast(Exchange.class, exchanges.poll()); 157 // add current index and total as properties 158 exchange.setProperty(Exchange.BATCH_INDEX, index); 159 exchange.setProperty(Exchange.BATCH_SIZE, total); 160 exchange.setProperty(Exchange.BATCH_COMPLETE, index == total - 1); 161 162 // update pending number of exchanges 163 pendingExchanges = total - index - 1; 164 165 // process the current exchange 166 processExchange(exchange); 167 if (!exchange.isFailed()) { 168 processCommit(exchange); 169 } else { 170 processRollback(exchange); 171 } 172 } 173 } 174 175 public boolean deferShutdown(ShutdownRunningTask shutdownRunningTask) { 176 // store a reference what to do in case when shutting down and we have pending messages 177 this.shutdownRunningTask = shutdownRunningTask; 178 // do not defer shutdown 179 return false; 180 } 181 182 public int getPendingExchangesSize() { 183 // only return the real pending size in case we are configured to complete all tasks 184 if (ShutdownRunningTask.CompleteAllTasks == shutdownRunningTask) { 185 return pendingExchanges; 186 } else { 187 return 0; 188 } 189 } 190 191 public boolean isBatchAllowed() { 192 // stop if we are not running 193 boolean answer = isRunAllowed(); 194 if (!answer) { 195 return false; 196 } 197 198 if (shutdownRunningTask == null) { 199 // we are not shutting down so continue to run 200 return true; 201 } 202 203 // we are shutting down so only continue if we are configured to complete all tasks 204 return ShutdownRunningTask.CompleteAllTasks == shutdownRunningTask; 205 } 206 207 protected Queue<Exchange> createExchanges(Message[] messages) throws MessagingException { 208 Queue<Exchange> answer = new LinkedList<Exchange>(); 209 210 int fetchSize = endpoint.getConfiguration().getFetchSize(); 211 int count = fetchSize == -1 ? messages.length : Math.min(fetchSize, messages.length); 212 213 if (LOG.isDebugEnabled()) { 214 LOG.debug("Fetching " + count + " messages. Total " + messages.length + " messages."); 215 } 216 217 for (int i = 0; i < count; i++) { 218 Message message = messages[i]; 219 if (!message.getFlags().contains(Flags.Flag.DELETED)) { 220 Exchange exchange = endpoint.createExchange(message); 221 answer.add(exchange); 222 } else { 223 if (LOG.isDebugEnabled()) { 224 LOG.debug("Skipping message as it was flagged as deleted: " + MailUtils.dumpMessage(message)); 225 } 226 } 227 } 228 229 return answer; 230 } 231 232 /** 233 * Strategy to process the mail message. 234 */ 235 protected void processExchange(Exchange exchange) throws Exception { 236 if (LOG.isDebugEnabled()) { 237 MailMessage msg = (MailMessage) exchange.getIn(); 238 LOG.debug("Processing message: " + MailUtils.dumpMessage(msg.getMessage())); 239 } 240 getProcessor().process(exchange); 241 } 242 243 /** 244 * Strategy to flag the message after being processed. 245 */ 246 protected void processCommit(Exchange exchange) throws MessagingException { 247 MailMessage msg = (MailMessage) exchange.getIn(); 248 // Use the "original" Message, in case a copy ended up being made 249 Message message = msg.getOriginalMessage(); 250 251 if (endpoint.getConfiguration().isDelete()) { 252 LOG.debug("Exchange processed, so flagging message as DELETED"); 253 message.setFlag(Flags.Flag.DELETED, true); 254 } else { 255 LOG.debug("Exchange processed, so flagging message as SEEN"); 256 message.setFlag(Flags.Flag.SEEN, true); 257 } 258 } 259 260 /** 261 * Strategy when processing the exchange failed. 262 */ 263 protected void processRollback(Exchange exchange) throws MessagingException { 264 Exception cause = exchange.getException(); 265 if (cause != null) { 266 LOG.warn("Exchange failed, so rolling back message status: " + exchange, cause); 267 } else { 268 LOG.warn("Exchange failed, so rolling back message status: " + exchange); 269 } 270 } 271 272 private void ensureIsConnected() throws MessagingException { 273 MailConfiguration config = endpoint.getConfiguration(); 274 275 boolean connected = false; 276 try { 277 if (store != null && store.isConnected()) { 278 connected = true; 279 } 280 } catch (Exception e) { 281 LOG.debug("Exception while testing for is connected to MailStore: " 282 + endpoint.getConfiguration().getMailStoreLogInformation() 283 + ". Caused by: " + e.getMessage(), e); 284 } 285 286 if (!connected) { 287 // ensure resources get recreated on reconnection 288 store = null; 289 folder = null; 290 291 if (LOG.isDebugEnabled()) { 292 LOG.debug("Connecting to MailStore: " + endpoint.getConfiguration().getMailStoreLogInformation()); 293 } 294 store = sender.getSession().getStore(config.getProtocol()); 295 store.connect(config.getHost(), config.getPort(), config.getUsername(), config.getPassword()); 296 } 297 298 if (folder == null) { 299 if (LOG.isDebugEnabled()) { 300 LOG.debug("Getting folder " + config.getFolderName()); 301 } 302 folder = store.getFolder(config.getFolderName()); 303 if (folder == null || !folder.exists()) { 304 throw new FolderNotFoundException(folder, "Folder not found or invalid: " + config.getFolderName()); 305 } 306 } 307 } 308 309 }