package org.simplejavamail.mailer.internal.mailsender;
import org.simplejavamail.MailException;
import org.simplejavamail.converter.internal.mimemessage.MimeMessageHelper;
import org.simplejavamail.email.Email;
import org.simplejavamail.mailer.config.ProxyConfig;
import org.simplejavamail.mailer.config.TransportStrategy;
import org.simplejavamail.mailer.internal.socks.AuthenticatingSocks5Bridge;
import org.simplejavamail.mailer.internal.socks.socks5server.AnonymousSocks5Server;
import org.simplejavamail.util.ConfigLoader;
import org.simplejavamail.util.ConfigLoader.Property;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.mail.*;
import javax.mail.internet.MimeMessage;
import java.io.UnsupportedEncodingException;
import java.util.Properties;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Phaser;
import static org.simplejavamail.converter.EmailConverter.mimeMessageToEML;
/**
* Class that performs the actual javax.mail SMTP integration.
* <p>
* Refer to {@link #send(Email, boolean)} for details.
* <p>
* <hr/>
* <p>
* On a technical note, this is the most complex class in the library (aside from the SOCKS5 bridging server), because it deals with optional
* asynchronous mailing requests and an optional proxy server that needs to be started and stopped on the fly depending on how many emails are (still)
* being sent. Especially the combination of asynchronous emails and synchronous emails needs to be managed properly.
*/
public class MailSender {
private static final Logger LOGGER = LoggerFactory.getLogger(MailSender.class);
/**
* For multi-threaded scenario's where a batch of emails sent asynchronously, the default maximum number of threads is {@value
* #DEFAULT_POOL_SIZE}. Can be overridden from a config file or through System variable.
*
* @see Property#DEFAULT_POOL_SIZE
*/
@SuppressWarnings("JavaDoc")
private static final int DEFAULT_POOL_SIZE = 10;
/**
* Used to actually send the email. This session can come from being passed in the default constructor, or made by <code>Mailer</code> directly.
*/
private final Session session;
/**
* Intermediary SOCKS5 relay server that acts as bridge between JavaMail and remote proxy (since JavaMail only supports anonymous SOCKS proxies).
* Only set when {@link ProxyConfig} is provided with authentication details.
*/
private AnonymousSocks5Server proxyServer = null;
/**
* Allows us to manage how many thread we run at the same time using a thread pool.
* <p>
* Can't be initialized in the field, because we need to reinitialize it whenever the threadpool was closed after a batch of emails and this
* MailSender instance is again engaged.
*/
private ExecutorService executor;
/**
* Used to keep track of running SMTP requests, so that we know when to close down the proxy bridging server (if used).
* <p>
* Can't be initialized in the field, because we need to reinitialize if the phaser was terminated after a batch of emails and this MailSender
* instance is again engaged.
*/
private Phaser smtpRequestsPhaser;
private int threadPoolSize;
/**
* Determines whether at the very last moment an email is sent out using JavaMail's native API or whether the email is simply only logged.
*/
private boolean transportModeLoggingOnly;
/**
* @see #configureSessionWithProxy(ProxyConfig, Session, TransportStrategy)
*/
public MailSender(final Session session, final ProxyConfig proxyConfig, final TransportStrategy transportStrategy) {
this.session = session;
this.proxyServer = configureSessionWithProxy(proxyConfig, session, transportStrategy);
this.threadPoolSize = ConfigLoader.valueOrProperty(null, Property.DEFAULT_POOL_SIZE, DEFAULT_POOL_SIZE);
this.transportModeLoggingOnly = ConfigLoader.valueOrProperty(null, Property.TRANSPORT_MODE_LOGGING_ONLY, false);
}
/**
* If a {@link ProxyConfig} was provided with a host address, then the appropriate properties are set on the {@link Session}, overriding any SOCKS
* properties already there.
* <p>
* These properties are <em>"mail.smtp.socks.host"</em> and <em>"mail.smtp.socks.port"</em>, which are set to "localhost" and {@link
* ProxyConfig#getProxyBridgePort()}.
*
* @param proxyConfig Proxy server details, optionally with username / password.
* @param session The session with properties to add the new configuration to.
* @param transportStrategy Used to verify if the current combination with proxy is allowed (SMTP with SSL trategy doesn't support any proxy,
* virtue of the underlying JavaMail framework).
* @return null in case of no proxy or anonymous proxy, or a AnonymousSocks5Server proxy bridging server instance in case of authenticated proxy.
*/
private static AnonymousSocks5Server configureSessionWithProxy(final ProxyConfig proxyConfig, final Session session,
final TransportStrategy transportStrategy) {
final ProxyConfig effectiveProxyConfig = (proxyConfig != null) ? proxyConfig : new ProxyConfig();
if (!effectiveProxyConfig.requiresProxy()) {
LOGGER.debug("No proxy set, skipping proxy.");
} else {
if (transportStrategy == TransportStrategy.SMTP_SSL) {
throw new MailSenderException(MailSenderException.INVALID_PROXY_SLL_COMBINATION);
}
final Properties sessionProperties = session.getProperties();
sessionProperties.put("mail.smtp.socks.host", effectiveProxyConfig.getRemoteProxyHost());
sessionProperties.put("mail.smtp.socks.port", String.valueOf(effectiveProxyConfig.getRemoteProxyPort()));
if (effectiveProxyConfig.requiresAuthentication()) {
sessionProperties.put("mail.smtp.socks.host", "localhost");
sessionProperties.put("mail.smtp.socks.port", String.valueOf(effectiveProxyConfig.getProxyBridgePort()));
return new AnonymousSocks5Server(new AuthenticatingSocks5Bridge(effectiveProxyConfig),
effectiveProxyConfig.getProxyBridgePort());
}
}
return null;
}
/**
* Processes an {@link Email} instance into a completely configured {@link Message}.
* <p/>
* Sends the Sun JavaMail {@link Message} object using {@link Session#getTransport()}. It will call {@link Transport#connect()} assuming all
* connection details have been configured in the provided {@link Session} instance and finally {@link Transport#sendMessage(Message,
* Address[])}.
* <p/>
* Performs a call to {@link Message#saveChanges()} as the Sun JavaMail API indicates it is needed to configure the message headers and providing
* a message id.
* <p>
* If the email should be sent asynchrounously - perhaps as part of a batch, then a new thread is started using the {@link #executor} for
* threadpooling.
* <p>
* If the email should go through an authenticated proxy server, then the SOCKS proxy bridge is started if not already running. When the last
* email in a batch has finished, the proxy bridging server is shut down.
*
* @param email The information for the email to be sent.
* @param async If false, this method blocks until the mail has been processed completely by the SMTP server. If true, a new thread is started to
* send the email and this method returns immediately.
* @throws MailException Can be thrown if an email isn't validating correctly, or some other problem occurs during connection, sending etc.
* @see Executors#newFixedThreadPool(int)
*/
public final synchronized void send(final Email email, final boolean async) {
/*
we need to track even non-async emails to prevent async emails from shutting down
the proxy bridge server (or connection pool in async mode) while a non-async email is still being processed
*/
// phaser auto-terminates each time the all parties have arrived, so re-initialize when needed
if (smtpRequestsPhaser == null || smtpRequestsPhaser.isTerminated()) {
smtpRequestsPhaser = new Phaser();
}
smtpRequestsPhaser.register();
if (async) {
// start up threadpool pool if necessary
if (executor == null || executor.isTerminated()) {
executor = Executors.newFixedThreadPool(threadPoolSize);
}
executor.execute(new Thread("sendMail process") {
@Override
public void run() {
sendMailClosure(session, email);
}
});
} else {
sendMailClosure(session, email);
}
}
/**
* Separate closure that can be executed directly or from a thread. Refer to {@link #send(Email, boolean)} for details.
*
* @param session The session with which to produce the {@link MimeMessage} aquire the {@link Transport} for connections.
* @param email The email that will be converted into a {@link MimeMessage}.
*/
private void sendMailClosure(final Session session, final Email email) {
LOGGER.trace("sending email...");
try {
// fill and send wrapped mime message parts
final MimeMessage message = MimeMessageHelper.produceMimeMessage(email, session);
logSession(session);
message.saveChanges(); // some headers and id's will be set for this specific message
final Transport transport = session.getTransport();
try {
synchronized (this) {
// proxy server is null when not needed
if (proxyServer != null && !proxyServer.isRunning()) {
LOGGER.trace("starting proxy bridge");
proxyServer.start();
}
}
if (!transportModeLoggingOnly) {
LOGGER.trace("\t\nEmail: {}", email);
LOGGER.trace("\t\nMimeMessage: {}\n", mimeMessageToEML(message));
try {
transport.connect();
transport.sendMessage(message, message.getAllRecipients());
} finally {
LOGGER.trace("closing transport");
//noinspection ThrowFromFinallyBlock
transport.close();
}
} else {
LOGGER.info("TRANSPORT_MODE_LOGGING_ONLY: skipping actual sending...");
LOGGER.info("\n\nEmail: {}\n", email);
LOGGER.info("\n\nMimeMessage: {}\n", mimeMessageToEML(message));
}
} finally {
checkShutDownRunningProcesses();
}
} catch (final UnsupportedEncodingException e) {
throw new MailSenderException(MailSenderException.INVALID_ENCODING, e);
} catch (final MessagingException e) {
throw new MailSenderException(MailSenderException.GENERIC_ERROR, e);
}
}
/**
* We need to keep a count of running threads in case a proxyserver is running or a connection pool needs to be shut down.
*/
private synchronized void checkShutDownRunningProcesses() {
smtpRequestsPhaser.arriveAndDeregister();
LOGGER.trace("SMTP request threads left: {}", smtpRequestsPhaser.getUnarrivedParties());
// if this thread is the last one finishing
if (smtpRequestsPhaser.getUnarrivedParties() == 0) {
LOGGER.trace("all threads have finished processing");
if (proxyServer != null && proxyServer.isRunning() && !proxyServer.isStopping()) {
LOGGER.trace("stopping proxy bridge...");
proxyServer.stop();
}
// shutdown the threadpool, or else the Mailer will keep any JVM alive forever
// executor is only available in async mode
if (executor != null) {
executor.shutdown();
}
}
}
/**
* Simply logs host details, credentials used and whether authentication will take place and finally the transport protocol used.
*/
private static void logSession(final Session session) {
final TransportStrategy transportStrategy = TransportStrategy.findStrategyForSession(session);
final Properties properties = session.getProperties();
final String sessionDetails = (transportStrategy != null) ? transportStrategy.toString(properties) : properties.toString();
LOGGER.debug("starting mail with " + sessionDetails);
}
/**
* Refer to Session{@link Session#setDebug(boolean)}
*/
public void setDebug(final boolean debug) {
session.setDebug(debug);
}
/**
* Configures the current session to trust all hosts and don't validate any SSL keys. The property "mail.smtp.ssl.trust" is set to "*".
* <p>
* Refer to https://javamail.java.net/nonav/docs/api/com/sun/mail/smtp/package-summary.html#mail.smtp.ssl.trust
*/
public void trustAllHosts(final boolean trustAllHosts) {
session.getProperties().remove("mail.smtp.ssl.trust");
if (trustAllHosts) {
session.getProperties().setProperty("mail.smtp.ssl.trust", "*");
}
}
/**
* Configures the current session to white list all provided hosts and don't validate SSL keys for them. The property "mail.smtp.ssl.trust" is set
* to a comma separated list.
* <p>
* Refer to https://javamail.java.net/nonav/docs/api/com/sun/mail/smtp/package-summary.html#mail.smtp.ssl.trust
*/
public void trustHosts(final String... hosts) {
trustAllHosts(false);
if (hosts.length > 0) {
final StringBuilder builder = new StringBuilder(hosts[0]);
for (int i = 1; i < hosts.length; i++) {
builder.append(",").append(hosts[i]);
}
session.getProperties().setProperty("mail.smtp.ssl.trust", builder.toString());
}
}
/**
* @param properties Properties which will be added to the current {@link Session} instance.
*/
public void applyProperties(final Properties properties) {
session.getProperties().putAll(properties);
}
/**
* For emergencies, when a client really wants access to the internally created {@link Session} instance.
*/
public Session getSession() {
return session;
}
/**
* @param threadPoolSize The maximum number of threads when sending emails in async fashion.
* @see Property#DEFAULT_POOL_SIZE
*/
public synchronized void setThreadPoolSize(final int threadPoolSize) {
this.threadPoolSize = threadPoolSize;
}
/**
* Sets the transport mode for this mail sender to logging only, which means no mail will be actually sent out.
*/
public synchronized void setTransportModeLoggingOnly(final boolean transportModeLoggingOnly) {
this.transportModeLoggingOnly = transportModeLoggingOnly;
}
/**
* @return {@link #transportModeLoggingOnly}
*/
public boolean isTransportModeLoggingOnly() {
return transportModeLoggingOnly;
}
}