All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.phloc.web.smtp.transport.MailAPI Maven / Gradle / Ivy

/**
 * Copyright (C) 2006-2015 phloc systems
 * http://www.phloc.com
 * office[at]phloc[dot]com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.phloc.web.smtp.transport;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.phloc.commons.GlobalDebug;
import com.phloc.commons.ValueEnforcer;
import com.phloc.commons.collections.ContainerHelper;
import com.phloc.commons.concurrent.ExtendedDefaultThreadFactory;
import com.phloc.commons.email.IEmailAddress;
import com.phloc.commons.state.EChange;
import com.phloc.commons.state.ESuccess;
import com.phloc.commons.stats.IStatisticsHandlerCounter;
import com.phloc.commons.stats.StatisticsManager;
import com.phloc.commons.string.StringHelper;
import com.phloc.commons.vendor.VendorInfo;
import com.phloc.datetime.PDTFactory;
import com.phloc.web.smtp.EmailGlobalSettings;
import com.phloc.web.smtp.IEmailData;
import com.phloc.web.smtp.ISMTPSettings;
import com.phloc.web.smtp.failed.FailedMailData;
import com.phloc.web.smtp.failed.FailedMailQueue;
import com.phloc.web.smtp.impl.ReadonlySMTPSettings;

/**
 * This class simplifies the task of sending an email.
* Note: do NOT use directly in pDAF3 - use the ScopedMailAPI instead. * * @author Philip Helger */ @ThreadSafe public final class MailAPI { /** The default prefix used in debug mode */ public static final String DEBUG_SUBJECT_PREFIX = "[DEBUG] "; /** The default subject to be used if none is specified */ public static final String DEFAULT_SUBJECT = ""; /** By default wait until all queued mails are send */ public static final boolean DEFAULT_STOP_IMMEDIATLY = false; private static final Logger s_aLogger = LoggerFactory.getLogger (MailAPI.class); private static final IStatisticsHandlerCounter s_aQueuedMailHdl = StatisticsManager.getCounterHandler (MailAPI.class.getName () + "$mails.queued"); private static final ReadWriteLock s_aRWLock = new ReentrantReadWriteLock (); private static final Map s_aQueueCache = new HashMap (); // Just to have custom named threads.... private static final ThreadFactory s_aThreadFactory = new ExtendedDefaultThreadFactory ("MailAPI"); private static final ExecutorService s_aSenderThreadPool = new ThreadPoolExecutor (0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue (), s_aThreadFactory); private static FailedMailQueue s_aFailedMailQueue = new FailedMailQueue (); private MailAPI () {} /** * @return The current failed mail queue. Never null. */ @Nonnull public static FailedMailQueue getFailedMailQueue () { s_aRWLock.readLock ().lock (); try { return s_aFailedMailQueue; } finally { s_aRWLock.readLock ().unlock (); } } /** * Set a new global failed mail queue. Updates all existing queues. * * @param aFailedMailQueue * The new failed mail queue to set. May not be null. */ public static void setFailedMailQueue (@Nonnull final FailedMailQueue aFailedMailQueue) { ValueEnforcer.notNull (aFailedMailQueue, "FailedMailQueue"); s_aRWLock.writeLock ().lock (); try { s_aFailedMailQueue = aFailedMailQueue; // Update all existing queues for (final MailQueuePerSMTP aMailQueue : s_aQueueCache.values ()) aMailQueue.setFailedMailQueue (aFailedMailQueue); } finally { s_aRWLock.writeLock ().unlock (); } s_aLogger.info ("Set FailedMailQueue to " + aFailedMailQueue); } @Nonnull private static MailQueuePerSMTP _getOrCreateMailQueuePerSMTP (@Nonnull final ISMTPSettings aSMTPSettings) { ValueEnforcer.notNull (aSMTPSettings, "SmtpSettings"); if (s_aSenderThreadPool.isShutdown ()) throw new IllegalStateException ("Cannot submit to mailqueues that are already stopped!"); // Ensure that always the same type is used! final ReadonlySMTPSettings aRealSMTPSettings = new ReadonlySMTPSettings (aSMTPSettings); // get queue per SMTP MailQueuePerSMTP aSMTPQueue = s_aQueueCache.get (aRealSMTPSettings); if (aSMTPQueue == null) { // create a new queue aSMTPQueue = new MailQueuePerSMTP (EmailGlobalSettings.getMaxMailQueueLength (), EmailGlobalSettings.getMaxMailSendCount (), aRealSMTPSettings, s_aFailedMailQueue); // put queue in cache s_aQueueCache.put (aRealSMTPSettings, aSMTPQueue); // and start running the queue s_aSenderThreadPool.submit (aSMTPQueue); } return aSMTPQueue; } public static boolean hasNonVendorEmailAddress (@Nullable final List aAddresses) { if (aAddresses != null) { final String sVendorEmailSuffix = VendorInfo.getVendorEmailSuffix (); for (final IEmailAddress aAddress : aAddresses) { if (!aAddress.getAddress ().endsWith (sVendorEmailSuffix)) { // check if the domain is explicitly allowed if (!EmailGlobalSettings.isAllowedNonVendorDomain (aAddress.getAddress ())) { return true; } } } } return false; } /** * Queue a single mail. * * @param aSMTPSettings * The SMTP settings to be used. * @param aMailData * The mail message to queue. May not be null. * @return {@link ESuccess}. */ @Nonnull public static ESuccess queueMail (@Nonnull final ISMTPSettings aSMTPSettings, @Nonnull final IEmailData aMailData) { final int nQueuedMails = queueMails (aSMTPSettings, ContainerHelper.newList (aMailData)); return ESuccess.valueOf (nQueuedMails == 1); } /** * Queue more than one mail. * * @param aSMTPSettings * The SMTP settings to be used. * @param aMailDataList * The mail messages to queue. May not be null. * @return The number of queued emails. Always ≥ 0. Maximum value is the * number of {@link IEmailData} objects in the argument. */ @Nonnegative public static int queueMails (@Nonnull final ISMTPSettings aSMTPSettings, @Nonnull final Collection aMailDataList) { ValueEnforcer.notNull (aSMTPSettings, "SmtpSettings"); ValueEnforcer.notNull (aMailDataList, "MailDataList"); if (aMailDataList.isEmpty ()) throw new IllegalArgumentException ("At least one message has to be supplied!"); MailQueuePerSMTP aSMTPQueue; s_aRWLock.writeLock ().lock (); try { // get queue per SMTP settings try { aSMTPQueue = _getOrCreateMailQueuePerSMTP (aSMTPSettings); } catch (final IllegalStateException ex) { // Happens if queue is already stopped // Put all errors in failed mail queue for (final IEmailData aEmailData : aMailDataList) getFailedMailQueue ().add (new FailedMailData (aSMTPSettings, aEmailData, ex)); return 0; } } finally { s_aRWLock.writeLock ().unlock (); } int nQueuedMails = 0; final boolean bSendVendorOnlyMails = GlobalDebug.isDebugMode (); // submit all messages for (final IEmailData aEmailData : aMailDataList) { if (aEmailData == null) { s_aLogger.error ("Mail data is null! Ignoring this item completely."); continue; } // queue the mail s_aQueuedMailHdl.increment (); // Do some consistency checks to ensure this particular email can be // send boolean bCanQueue = true; if (aEmailData.getFrom () == null) { s_aLogger.error ("Mail data has no sender address: " + aEmailData + " - not queuing!"); bCanQueue = false; } if (aEmailData.getToCount () == 0) { s_aLogger.error ("Mail data has no receiver address: " + aEmailData + " - not queuing!"); bCanQueue = false; } if (bSendVendorOnlyMails) { // In the debug version we can *only* send to vendor addresses! if (hasNonVendorEmailAddress (aEmailData.getTo ()) || hasNonVendorEmailAddress (aEmailData.getCc ()) || hasNonVendorEmailAddress (aEmailData.getBcc ())) { if (EmailGlobalSettings.isAllowNonVendorMailsInDebugOnLocalhost () && aSMTPSettings.getHostName ().equals ("localhost")) { s_aLogger.warn ("Debug mode: sending non-vendor mail TO '" + aEmailData.getTo () + "'" + (aEmailData.getCcCount () > 0 ? " and CC '" + aEmailData.getCc () + "'" : "") + (aEmailData.getBccCount () > 0 ? " and BCC '" + aEmailData.getBcc () + "'" : "") + " as it was allowed for localhost SMTP"); } else { s_aLogger.error ("Debug mode: ignoring mail TO '" + aEmailData.getTo () + "'" + (aEmailData.getCcCount () > 0 ? " and CC '" + aEmailData.getCc () + "'" : "") + (aEmailData.getBccCount () > 0 ? " and BCC '" + aEmailData.getBcc () + "'" : "") + " because at least one address is not targeted to the vendor domain"); bCanQueue = false; } } } // Check if queue is already stopped if (aSMTPQueue.isStopped ()) { s_aLogger.error ("Queue is already stopped - not queuing!"); bCanQueue = false; } boolean bWasQueued = false; Exception aException = null; if (bCanQueue) { // Check if a subject is present if (StringHelper.hasNoText (aEmailData.getSubject ())) { s_aLogger.warn ("Mail data has no subject: " + aEmailData + " - defaulting to " + DEFAULT_SUBJECT); aEmailData.setSubject (DEFAULT_SUBJECT); } // Check if a body is present if (StringHelper.hasNoText (aEmailData.getBody ())) s_aLogger.warn ("Mail data has no body: " + aEmailData); if (bSendVendorOnlyMails) { // Add special debug prefix if (!StringHelper.startsWith (aEmailData.getSubject (), DEBUG_SUBJECT_PREFIX)) aEmailData.setSubject (DEBUG_SUBJECT_PREFIX + aEmailData.getSubject ()); s_aLogger.info ("Sending only-to-vendor mail in debug version:\n" + aSMTPSettings + "\n" + aEmailData); } // Uses UTC timezone! aEmailData.setSentDate (PDTFactory.getCurrentDateTime ()); try { if (aSMTPQueue.queueObject (aEmailData).isSuccess ()) { bWasQueued = true; ++nQueuedMails; } // else an error message was already logged } catch (final Exception ex) { // Occurs if queue is already stopped aException = ex; } } if (!bWasQueued) { // Mail was not queued - put in failed mail queue aSMTPQueue.getFailedMailQueue ().add (new FailedMailData (aSMTPSettings, aEmailData, aException)); } } return nQueuedMails; } @Nonnegative private static int _getTotalQueueLength () { int ret = 0; // count over all queues for (final MailQueuePerSMTP aQueue : s_aQueueCache.values ()) ret += aQueue.getQueueLength (); return ret; } @Nonnegative public static int getTotalQueueLength () { s_aRWLock.readLock ().lock (); try { return _getTotalQueueLength (); } finally { s_aRWLock.readLock ().unlock (); } } /** * Stop taking new mails, and wait until all mails already in the queue are * delivered. * * @return {@link EChange} */ @Nonnull public static EChange stop () { return stop (DEFAULT_STOP_IMMEDIATLY); } /** * Stop taking new mails, and wait until all mails already in the queue are * delivered. * * @param bStopImmediately * true if all mails currently in the queue should be * removed and put in the failed mail queue. Only the emails currently * in sending are continued to be sent out. * @return {@link EChange} */ @Nonnull public static EChange stop (final boolean bStopImmediately) { s_aRWLock.writeLock ().lock (); try { // Check if the thread pool is already shut down if (s_aSenderThreadPool.isShutdown ()) return EChange.UNCHANGED; // don't take any more actions s_aSenderThreadPool.shutdown (); // stop all specific queues afterwards for (final MailQueuePerSMTP aQueue : s_aQueueCache.values ()) aQueue.stopQueuingNewObjects (bStopImmediately); final int nQueues = s_aQueueCache.size (); // Subtract 1 for the STOP_MESSAGE final int nQueueLength = _getTotalQueueLength () - 1; if (nQueues > 0 || nQueueLength > 0) s_aLogger.info ("Stopping central mail queues: " + nQueues + " queue" + (nQueues == 1 ? "" : "s") + " with " + nQueueLength + " mail" + (nQueueLength == 1 ? "" : "s")); } finally { s_aRWLock.writeLock ().unlock (); } // Don't wait in a writeLock! try { while (!s_aSenderThreadPool.awaitTermination (1, TimeUnit.SECONDS)) { // wait until we're done } } catch (final InterruptedException ex) { s_aLogger.error ("Error stopping mail queue", ex); } return EChange.CHANGED; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy