net.metanotion.emailqueue.SmtpSender.java Source code

Java tutorial

Introduction

Here is the source code for net.metanotion.emailqueue.SmtpSender.java

Source

/***************************************************************************
   Copyright 2012 Emily Estes
    
   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 net.metanotion.emailqueue;

import java.io.IOException;
import java.io.Writer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import javax.net.ssl.SSLException;

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

import org.apache.commons.net.ProtocolCommandEvent;
import org.apache.commons.net.ProtocolCommandListener;
import org.apache.commons.net.smtp.AuthenticatingSMTPClient;
import org.apache.commons.net.smtp.SMTPReply;
import org.apache.commons.net.smtp.SimpleSMTPHeader;

/** An implementation of the MailSender interface to batch send emails via SMTP. This class
will authenticate to the server and attempt to set up a TLS connection, if this fails, it will
not work. So this should be a fairly secure approach to SMTP. This class relies on the
<a href="http://commons.apache.org/proper/commons-net/">Apache Commons Net</a> package
for the SMTP client. This is (currently) the only concrete protocol dependency in the whole
email stack, as the rest of the web framework email components use database queues to
communicate. Security note: I am always interested in ways to improve the operational
security of my code, and so any suggestions for ways to improve this code are more than
welcome. */
public final class SmtpSender implements MailSender<SmtpSender.Connection> {
    /** This enum is used to specify the security level required for SMTP connections. */
    public enum Security {
        /** Only connect if TLS is enabled on the server. */
        TLS_ONLY,
        /** Try to start TLS, but allow the connection to continue if it's unavailable. */
        TRY_TLS,
        /** Do not use TLS. */
        NONE
    };

    public static final class Connection implements AutoCloseable {
        public final AuthenticatingSMTPClient client;

        public Connection(final AuthenticatingSMTPClient client) {
            this.client = client;
        }

        @Override
        public void close() {
            try {
                this.client.logout();
            } catch (final IOException e) {
            }
            try {
                this.client.disconnect();
            } catch (final IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private static final Logger logger = LoggerFactory.getLogger(SmtpSender.class);

    /** The address of the SMTP server we're going to connect to. */
    private final String server;
    /** The port of the SMTP sever we're going to connect to. */
    private final int port;
    /** The username to use when authenticating with the SMTP server. */
    private final String username;
    /** The password to use when authenticating with the SMTP server. */
    private final String password;
    /** The hostname to use when authenticating with the SMTP server. */
    private final String host;
    /** TLS requirements. */
    private final Security tls;

    /** Create a MailSender instance that uses SMTP as the transport.
       @param server The address of the SMTP server we're going to connect to.
       @param port The port of the SMTP sever we're going to connect to.
       @param username The username to use when authenticating with the SMTP server.
       @param password The password to use when authenticating with the SMTP server.
       @param host The hostname to use when authenticating with the SMTP server.
    */
    public SmtpSender(final String server, final int port, final String username, final String password,
            final String host) {
        this(server, port, username, password, host, Security.TLS_ONLY);
    }

    /** Create a MailSender instance that uses SMTP as the transport.
       @param server The address of the SMTP server we're going to connect to.
       @param port The port of the SMTP sever we're going to connect to.
       @param username The username to use when authenticating with the SMTP server.
       @param password The password to use when authenticating with the SMTP server.
       @param host The hostname to use when authenticating with the SMTP server.
       @param tls The level of TLS to be enforced on connection attempts.
    */
    public SmtpSender(final String server, final int port, final String username, final String password,
            final String host, final Security tls) {
        this.server = server;
        this.port = port;
        this.username = username;
        this.password = password;
        this.host = host;
        this.tls = tls;
    }

    private static final String COMMAND_LOG_FORMAT = "command {} message {}";

    private void startTls(final AuthenticatingSMTPClient client) throws SSLException, IOException {
        if (this.tls != Security.NONE) {
            logger.trace("Starting TLS");
            if (!client.execTLS()) {
                logger.error("Could not Start TLS.");
                if (this.tls == Security.TLS_ONLY) {
                    try {
                        client.disconnect();
                    } catch (final IOException e) {
                    }
                    throw new RuntimeException("TLS was required for connection but could not be established.");
                }
            }
        }
    }

    @Override
    public Connection getConnection() {
        AuthenticatingSMTPClient client = null;
        try {
            client = new AuthenticatingSMTPClient();
            client.addProtocolCommandListener(new ProtocolCommandListener() {
                @Override
                public void protocolCommandSent(final ProtocolCommandEvent event) {
                    logger.trace(COMMAND_LOG_FORMAT, event.getCommand(), event.getMessage());
                }

                @Override
                public void protocolReplyReceived(final ProtocolCommandEvent event) {
                    logger.trace(COMMAND_LOG_FORMAT, event.getCommand(), event.getMessage());
                }
            });
            logger.trace("Connecting");
            client.connect(this.server, this.port);
            if (!SMTPReply.isPositiveCompletion(client.getReplyCode())) {
                logger.error("Did not get positive completion.");
                throw new IOException("Could not complete SMTP connection.");
            }
            startTls(client);
            logger.trace("Authenticating");
            client.auth(AuthenticatingSMTPClient.AUTH_METHOD.PLAIN, username, password);
            logger.trace("Login");
            client.login(host);
            return new Connection(client);
        } catch (final IOException | NoSuchAlgorithmException | InvalidKeyException | InvalidKeySpecException ex) {
            logger.debug("Error", ex);
            try {
                if (client != null) {
                    client.disconnect();
                }
            } catch (final IOException ex2) {
            }
            throw new RuntimeException(ex);
        }
    }

    @Override
    public boolean send(final Connection conn, final MessageStruct msg) throws IOException {
        logger.trace("Set Sender " + msg.Sender);
        conn.client.setSender(msg.Sender);
        logger.trace("Set Recipient " + msg.Recipient);
        conn.client.addRecipient(msg.Recipient);
        final Writer writer = conn.client.sendMessageData();
        final SimpleSMTPHeader header = new SimpleSMTPHeader(msg.Sender, msg.Recipient, msg.Subject);
        logger.trace("Write header");
        writer.write(header.toString());
        logger.trace("Write message");
        writer.write(msg.Message);
        logger.trace("Close");
        writer.close();
        logger.trace("Complete command");
        return conn.client.completePendingCommand();
    }
}