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: 17297 $
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 }