Wednesday, August 22, 2012

javax.mail and plain, STARTTLS, SSL/TSL connection for POP3 and SMTP without keystore

Today my target was to implement various connections for POP3/SMTP email accounts. incuding:
  • plain connection with no encryption
  • STARTTLS
  • SSL/TSL
The key requirement here was to have it without java keystore file configured for storing and accepting server certificates. This should work transparently for the user.

Having dug through tons of not working examples on the net, and after trying figure out some solutions by trial and error, I decided to take my Plain Old Eclipse Debugger and dive into the javax.mail classes. Here are quick results of HOWTO.

The environment you need is Java Mail API 1.4.5 (1.4 doesn't work properly, due to inability to set custom socket factory for STARTTLS). On the other hand, I don't use plain SMTP connection, but Spring JavaMailSender, but it has exactly the same config (with properties).

AlwaysTrustSSLContextFactory

First thing to have for encrypted connection is to have SSLContextFactory, that trusts all certificates, to skip the verification stage (this is not required for my application). Here is the code:

package com.blogspot.lifeinide;

import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.SocketFactory;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

/**
 * SSL socket factory accepting any certificate.
 */
public class AlwaysTrustSSLContextFactory extends SSLSocketFactory {

    private SSLSocketFactory factory;
        
    public static class DefaultTrustManager implements X509TrustManager {

        @Override
        public void checkClientTrusted(X509Certificate[] arg0, String arg1) 
        throws CertificateException {}

        @Override
        public void checkServerTrusted(X509Certificate[] arg0, String arg1)
        throws CertificateException {}

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    }

    public AlwaysTrustSSLContextFactory() 
    throws NoSuchAlgorithmException, KeyManagementException {
        super();

        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(new KeyManager[0], new TrustManager[] {
            new DefaultTrustManager()}, new SecureRandom());
            factory = (SSLSocketFactory) ctx.getSocketFactory();
    }

    public static SocketFactory getDefault() {
        try {
            return new AlwaysTrustSSLContextFactory();
        } catch (Exception e) {
            throw new RuntimeException("Cannot instantiate default AlwaysTrustSSLContextFactory", e);
        }
    }

    public Socket createSocket() throws IOException {
        return factory.createSocket();
    }

    public Socket createSocket(InetAddress address, int port, 
    InetAddress localAddress, int localPort)
    throws IOException {
        return factory.createSocket(address, port, localAddress, localPort);
    }

    public Socket createSocket(InetAddress host, int port) throws IOException {
        return factory.createSocket(host, port);
    }

    public Socket createSocket(Socket s, String host, int port, 
    boolean autoClose) throws IOException {
        return factory.createSocket(s, host, port, autoClose);
    }

    public Socket createSocket(String host, int port, InetAddress localHost, 
    int localPort) throws IOException, UnknownHostException {
        return factory.createSocket(host, port, localHost, localPort);
    }

    public Socket createSocket(String host, int port) throws IOException, 
    UnknownHostException {
        return factory.createSocket(host, port);
    }

    public boolean equals(Object obj) {
        return factory.equals(obj);
    }

    public String[] getDefaultCipherSuites() {
        return factory.getDefaultCipherSuites();
    }

    public String[] getSupportedCipherSuites() {
        return factory.getSupportedCipherSuites();
    }

}

The funny part is that the getDefault() static method is crucial here, because javax.mail classes get the SSLContextFactory from this static method, and not by creating the object using constructor. This is a big trap here that can consume a lot of time to conceive. This also can reveal how the rest of sources are written, what is pretty bad for me (strange initializations by static methods and poorly described string properties, with which you can build a lot of combinations to try make it work).

Now, without a lot of complaining more, the revealed solutions. The thing we want to achieve is to get the connected javax.mail.Store from javax.mail.Session to work with POP3, and configured JavaMailSender (with javax.mail properties) for SMTP.

Plain connection

The easiest one. Implementation for POP3:

Session session = Session.getDefaultInstance(new Properties(), null);
Store store = session.getStore("pop3");
store.connect(host, port, username, password);

And SMTP:

JavaMailSenderImpl sender = new JavaMailSenderImpl(); 
sender.setHost(host);
sender.setUsername(username);
sender.setPassword(password);
sender.setPort(port);
                
Properties props = new Properties();
props.setProperty("mail.smtp.auth", "true");

sender.setJavaMailProperties(props);

SSL/TLS

Now the version working from the beginning through secure channel. POP3:

Properties props = new Properties();

props.setProperty("mail.pop3s.socketFactory.class", "com.blogspot.lifeinide.AlwaysTrustSSLContextFactory");
props.setProperty("mail.pop3s.socketFactory.port", port);
props.setProperty("mail.pop3.ssl.enable", "true"); 

URLName url = new URLName("pop3s", host, port, "", username, password);
Session session = Session.getInstance(props, null);
Store store = session.getStore(url);
store.connect();

And the SMTP likewise:

JavaMailSenderImpl sender = new JavaMailSenderImpl(); 
sender.setHost(host);
sender.setUsername(username);
sender.setPassword(password);
sender.setPort(port);

Properties props = new Properties();
sender.setProtocol("smtps");
props.setProperty("mail.smtps.auth", "true");
props.setProperty("mail.smtps.socketFactory.class", "com.blogspot.lifeinide.AlwaysTrustSSLContextFactory");
props.setProperty("mail.smtps.socketFactory.port", port);
props.setProperty("mail.smtp.ssl.enable", "true"); 

sender.setJavaMailProperties(props);

STARTTLS

The last one. POP3 version:

Properties props = new Properties();

props.setProperty("mail.pop3.ssl.socketFactory.class", "com.blogspot.lifeinide.AlwaysTrustSSLContextFactory");
props.setProperty("mail.pop3.ssl.socketFactory.port", port);
props.setProperty("mail.pop3.starttls.enable", "true");

URLName url = new URLName("pop3", host, port, "", username, password);
Session session = Session.getInstance(props, null);
Store store = session.getStore(url);
store.connect();

And SMTP:

JavaMailSenderImpl sender = new JavaMailSenderImpl(); 
sender.setHost(host);
sender.setUsername(username);
sender.setPassword(password);
sender.setPort(port);
                
Properties props = new Properties();
props.setProperty("mail.smtp.auth", "true");
props.setProperty("mail.smtp.ssl.socketFactory.class", "com.blogspot.lifeinide.AlwaysTrustSSLContextFactory");
props.setProperty("mail.smtp.ssl.socketFactory.port", port);
props.setProperty("mail.smtp.starttls.enable", "true");

sender.setJavaMailProperties(props);

Looking for amount of questions and not working examples on the net, and amount of time I needed to solve it, the guys implemented it this way should get the prize for one of the most obscure solutions in java I was working with.