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    }