/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.xwiki.mail.internal.thread; import java.util.Collections; import java.util.Iterator; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import javax.mail.Address; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import org.xwiki.component.annotation.Component; import org.xwiki.context.ExecutionContext; import org.xwiki.context.ExecutionContextException; import org.xwiki.mail.ExtendedMimeMessage; import org.xwiki.mail.MailContentStore; import org.xwiki.mail.MailListener; import org.xwiki.mail.MailStatusResult; import org.xwiki.mail.internal.UpdateableMailStatusResult; import com.xpn.xwiki.XWikiContext; /** * Runnable that regularly check for mail items on a Prepare Queue, and for each mail item there, generate the message * to send and persist it and put that reference on the Send Queue for sending. * * @version $Id: 599bef0367a742006f1766f93a1300bb44556288 $ * @since 6.4 */ @Component @Named("prepare") @Singleton public class PrepareMailRunnable extends AbstractMailRunnable { @Inject private MailQueueManager<PrepareMailQueueItem> prepareMailQueueManager; @Inject private MailQueueManager<SendMailQueueItem> sendMailQueueManager; @Inject @Named("filesystem") private MailContentStore mailContentStore; @Override public void run() { do { try { // Handle next message in the queue if (this.prepareMailQueueManager.hasMessage()) { // Important: only remove the mail item after the message has been created and put on the sender // queue. PrepareMailQueueItem mailItem = this.prepareMailQueueManager.peekMessage(); try { prepareMail(mailItem); } finally { this.prepareMailQueueManager.removeMessageFromQueue(mailItem); } } // Note: a short pause to catch thread interruptions and to be kind on CPU. Thread.sleep(100L); } catch (InterruptedException e) { // Thread has been stopped, exit this.logger.debug("Mail Prepare Thread was forcefully stopped", e); break; } catch (Exception e) { // There was an unexpected problem, we just log the problem but keep the thread alive! this.logger.error("Unexpected error in the Mail Prepare Thread", e); } } while (!this.shouldStop); } /** * Prepare the messages to send, persist them and put them on the Mail Sender Queue. * * @param item the queue item containing all the data for sending the mail * @throws org.xwiki.context.ExecutionContextException when the XWiki Context fails to be set up */ protected void prepareMail(PrepareMailQueueItem item) throws ExecutionContextException { Iterator<? extends MimeMessage> messageIterator = item.getMessages().iterator(); MailListener listener = item.getListener(); if (listener != null) { listener.onPrepareBegin(item.getBatchId(), Collections.<String, Object>emptyMap()); } // Count the total number of messages to process long messageCounter = 0; try { boolean shouldStop = false; while (!shouldStop) { // Note that we need to have the hasNext() call after the context is ready since the implementation can // need a valid XWiki Context. prepareContext(item.getContext()); try { if (messageIterator.hasNext()) { MimeMessage mimeMessage = messageIterator.next(); prepareSingleMail(mimeMessage, item); messageCounter++; } else { shouldStop = true; } } finally { removeContext(); } } } catch (Exception e) { if (listener != null) { listener.onPrepareFatalError(e, Collections.<String, Object>emptyMap()); } } finally { if (listener != null) { MailStatusResult result = listener.getMailStatusResult(); // Update the listener with the total number of messages prepared so that the user can known when // all the messages have been processed for the batch. We update here, even in case of failure // so that waiting process have a chance to see an end. if (result instanceof UpdateableMailStatusResult) { ((UpdateableMailStatusResult) result).setTotalSize(messageCounter); } listener.onPrepareEnd(Collections.<String, Object>emptyMap()); } } } protected void prepareContext(ExecutionContext executionContext) throws ExecutionContextException { try { this.execution.setContext(executionContext); } catch (Exception e) { // If inheritance fails, we will get an unchecked exception here. So we'll wrap it in an // ExecutionContextException. throw new ExecutionContextException("Failed to set the execution context.", e); } } private void prepareSingleMail(MimeMessage mimeMessage, PrepareMailQueueItem item) { MailListener listener = item.getListener(); // Step 1: Try to complete message with From and Bcc from configuration if needed completeMessage(mimeMessage); // Ensure mimeMessage to be extended ExtendedMimeMessage message = ExtendedMimeMessage.wrap(mimeMessage); // Step 2: Persist the MimeMessage // Note: Message identifier is stabilized at this step by the serialization process try { this.mailContentStore.save(item.getBatchId(), message); } catch (Exception e) { // An error occurred, notify the user if a listener has been provided if (listener != null) { listener.onPrepareMessageError(message, e, Collections.<String, Object>emptyMap()); } return; } // Step 3: Put the MimeMessage id on the Mail Send Queue for sending // Extract the wiki id from the context this.sendMailQueueManager.addToQueue(new SendMailQueueItem(message.getUniqueMessageId(), item.getSession(), listener, item.getBatchId(), extractWikiId(item))); // Step 4: Notify the user that the MimeMessage is prepared if (listener != null) { listener.onPrepareMessageSuccess(message, Collections.<String, Object>emptyMap()); } } private String extractWikiId(PrepareMailQueueItem item) { XWikiContext xcontext = (XWikiContext) item.getContext().getProperty(XWikiContext.EXECUTIONCONTEXT_KEY); return xcontext.getWikiId(); } private void completeMessage(MimeMessage mimeMessage) { // Note: We don't cache the default From and BCC addresses because they can be modified at runtime // (from the Admin UI for example) and we need to always get the latest configured values. // If the user has not set the From header then try to use the default value from configuration tryToEnsureFrom(mimeMessage); // Else JavaMail won't be able to send the mail but we'll get the error in the status. // If the user has not set the BCC header then use the default value from configuration tryToAddDefaultBccIfNeeded(mimeMessage); } private void tryToEnsureFrom(MimeMessage mimeMessage) { if (getFrom(mimeMessage) == null) { // Try using the From address in the Session String from = this.configuration.getFromAddress(); if (from != null) { try { mimeMessage.setFrom(new InternetAddress(from)); } catch (MessagingException e) { // ignored } } } } private void tryToAddDefaultBccIfNeeded(MimeMessage mimeMessage) { Address[] bccAddresses = getBccRecipients(mimeMessage); if (bccAddresses == null || bccAddresses.length == 0) { for (String address : this.configuration.getBCCAddresses()) { try { mimeMessage.addRecipient(Message.RecipientType.BCC, new InternetAddress(address)); } catch (MessagingException e) { // ignored } } } } private Address[] getFrom(MimeMessage mimeMessage) { try { return mimeMessage.getFrom(); } catch (MessagingException e) { return null; } } private Address[] getBccRecipients(MimeMessage mimeMessage) { try { return mimeMessage.getRecipients(Message.RecipientType.BCC); } catch (MessagingException e) { return null; } } }