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