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.spi.Synchronization;
036    import org.apache.camel.util.CastUtils;
037    import org.apache.camel.util.ObjectHelper;
038    import org.apache.commons.logging.Log;
039    import org.apache.commons.logging.LogFactory;
040    import org.springframework.mail.javamail.JavaMailSenderImpl;
041    
042    /**
043     * A {@link org.apache.camel.Consumer Consumer} which consumes messages from JavaMail using a
044     * {@link javax.mail.Transport Transport} and dispatches them to the {@link Processor}
045     *
046     * @version $Revision: 980707 $
047     */
048    public class MailConsumer extends ScheduledPollConsumer implements BatchConsumer, ShutdownAware {
049        public static final long DEFAULT_CONSUMER_DELAY = 60 * 1000L;
050        private static final transient Log LOG = LogFactory.getLog(MailConsumer.class);
051    
052        private final MailEndpoint endpoint;
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.endpoint = endpoint;
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 void poll() throws Exception {
084            // must reset for each poll
085            shutdownRunningTask = null;
086            pendingExchanges = 0;
087    
088            ensureIsConnected();
089    
090            if (store == null || folder == null) {
091                throw new IllegalStateException("MailConsumer did not connect properly to the MailStore: "
092                        + endpoint.getConfiguration().getMailStoreLogInformation());
093            }
094    
095            if (LOG.isDebugEnabled()) {
096                LOG.debug("Polling mailfolder: " + endpoint.getConfiguration().getMailStoreLogInformation());
097            }
098    
099            if (endpoint.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;
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 (endpoint.getConfiguration().isUnseen()) {
116                        messages = folder.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
117                    } else {
118                        messages = folder.getMessages();
119                    }
120    
121                    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    
140        public void setMaxMessagesPerPoll(int maxMessagesPerPoll) {
141            this.maxMessagesPerPoll = maxMessagesPerPoll;
142        }
143    
144        public void processBatch(Queue<Object> exchanges) throws Exception {
145            int total = exchanges.size();
146    
147            // limit if needed
148            if (maxMessagesPerPoll > 0 && total > maxMessagesPerPoll) {
149                if (LOG.isDebugEnabled()) {
150                    LOG.debug("Limiting to maximum messages to poll " + maxMessagesPerPoll + " as there was " + total + " messages in this poll.");
151                }
152                total = maxMessagesPerPoll;
153            }
154    
155            for (int index = 0; index < total && isBatchAllowed(); index++) {
156                // only loop if we are started (allowed to run)
157                Exchange exchange = ObjectHelper.cast(Exchange.class, exchanges.poll());
158                // add current index and total as properties
159                exchange.setProperty(Exchange.BATCH_INDEX, index);
160                exchange.setProperty(Exchange.BATCH_SIZE, total);
161                exchange.setProperty(Exchange.BATCH_COMPLETE, index == total - 1);
162    
163                // update pending number of exchanges
164                pendingExchanges = total - index - 1;
165    
166                // must use the original message in case we need to workaround a charset issue when extracting mail content
167                final Message mail = exchange.getIn(MailMessage.class).getOriginalMessage();
168    
169                // add on completion to handle after work when the exchange is done
170                exchange.addOnCompletion(new Synchronization() {
171                    public void onComplete(Exchange exchange) {
172                        processCommit(mail, exchange);
173                    }
174    
175                    public void onFailure(Exchange exchange) {
176                        processRollback(mail, exchange);
177                    }
178    
179                    @Override
180                    public String toString() {
181                        return "MailConsumerOnCompletion";
182                    }
183                });
184    
185                // process the exchange
186                processExchange(exchange);
187            }
188        }
189    
190        public boolean deferShutdown(ShutdownRunningTask shutdownRunningTask) {
191            // store a reference what to do in case when shutting down and we have pending messages
192            this.shutdownRunningTask = shutdownRunningTask;
193            // do not defer shutdown
194            return false;
195        }
196    
197        public int getPendingExchangesSize() {
198            // only return the real pending size in case we are configured to complete all tasks
199            if (ShutdownRunningTask.CompleteAllTasks == shutdownRunningTask) {
200                return pendingExchanges;
201            } else {
202                return 0;
203            }
204        }
205    
206        public boolean isBatchAllowed() {
207            // stop if we are not running
208            boolean answer = isRunAllowed();
209            if (!answer) {
210                return false;
211            }
212    
213            if (shutdownRunningTask == null) {
214                // we are not shutting down so continue to run
215                return true;
216            }
217    
218            // we are shutting down so only continue if we are configured to complete all tasks
219            return ShutdownRunningTask.CompleteAllTasks == shutdownRunningTask;
220        }
221    
222        protected Queue<Exchange> createExchanges(Message[] messages) throws MessagingException {
223            Queue<Exchange> answer = new LinkedList<Exchange>();
224    
225            int fetchSize = endpoint.getConfiguration().getFetchSize();
226            int count = fetchSize == -1 ? messages.length : Math.min(fetchSize, messages.length);
227    
228            if (LOG.isDebugEnabled()) {
229                LOG.debug("Fetching " + count + " messages. Total " + messages.length + " messages.");
230            }
231    
232            for (int i = 0; i < count; i++) {
233                Message message = messages[i];
234                if (!message.getFlags().contains(Flags.Flag.DELETED)) {
235                    Exchange exchange = endpoint.createExchange(message);
236                    answer.add(exchange);
237                } else {
238                    if (LOG.isDebugEnabled()) {
239                        LOG.debug("Skipping message as it was flagged as deleted: " + MailUtils.dumpMessage(message));
240                    }
241                }
242            }
243    
244            return answer;
245        }
246    
247        /**
248         * Strategy to process the mail message.
249         */
250        protected void processExchange(Exchange exchange) throws Exception {
251            if (LOG.isDebugEnabled()) {
252                MailMessage msg = (MailMessage) exchange.getIn();
253                LOG.debug("Processing message: " + MailUtils.dumpMessage(msg.getMessage()));
254            }
255            getProcessor().process(exchange);
256        }
257    
258        /**
259         * Strategy to flag the message after being processed.
260         *
261         * @param mail the mail message
262         * @param exchange the exchange
263         */
264        protected void processCommit(Message mail, Exchange exchange) {
265            try {
266                if (endpoint.getConfiguration().isDelete()) {
267                    LOG.debug("Exchange processed, so flagging message as DELETED");
268                    mail.setFlag(Flags.Flag.DELETED, true);
269                } else {
270                    LOG.debug("Exchange processed, so flagging message as SEEN");
271                    mail.setFlag(Flags.Flag.SEEN, true);
272                }
273            } catch (MessagingException e) {
274                LOG.warn("Error occurred during flagging message as DELETED/SEEN", e);
275                exchange.setException(e);
276            }
277        }
278    
279        /**
280         * Strategy when processing the exchange failed.
281         *
282         * @param mail the mail message
283         * @param exchange the exchange
284         */
285        protected void processRollback(Message mail, Exchange exchange) {
286            Exception cause = exchange.getException();
287            if (cause != null) {
288                LOG.warn("Exchange failed, so rolling back message status: " + exchange, cause);
289            } else {
290                LOG.warn("Exchange failed, so rolling back message status: " + exchange);
291            }
292        }
293    
294        private void ensureIsConnected() throws MessagingException {
295            MailConfiguration config = endpoint.getConfiguration();
296    
297            boolean connected = false;
298            try {
299                if (store != null && store.isConnected()) {
300                    connected = true;
301                }
302            } catch (Exception e) {
303                LOG.debug("Exception while testing for is connected to MailStore: "
304                        + endpoint.getConfiguration().getMailStoreLogInformation()
305                        + ". Caused by: " + e.getMessage(), e);
306            }
307    
308            if (!connected) {
309                // ensure resources get recreated on reconnection
310                store = null;
311                folder = null;
312    
313                if (LOG.isDebugEnabled()) {
314                    LOG.debug("Connecting to MailStore: " + endpoint.getConfiguration().getMailStoreLogInformation());
315                }
316                store = sender.getSession().getStore(config.getProtocol());
317                store.connect(config.getHost(), config.getPort(), config.getUsername(), config.getPassword());
318            }
319    
320            if (folder == null) {
321                if (LOG.isDebugEnabled()) {
322                    LOG.debug("Getting folder " + config.getFolderName());
323                }
324                folder = store.getFolder(config.getFolderName());
325                if (folder == null || !folder.exists()) {
326                    throw new FolderNotFoundException(folder, "Folder not found or invalid: " + config.getFolderName());
327                }
328            }
329        }
330    
331    }