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    }