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