Java Tip 115: Secure JavaMail with JSSE

Add secure, SSL-based connections to JavaMail

The Java Secure Socket Extension (JSSE) API provides SSL functionality that you can add to any JavaMail storage provider. Before we get started, you must download and install JavaMail, JSSE, and all the required packages as described in the JSSE and JavaMail documentation on Sun's Website (most required downloads are included in Java 2 Standard Edition 1.4), as well as JavaMail and JSSE providers of your choice.

Before the JSSE framework can recognize any JSSE providers, you must register them permanently in the <java home>\jre\lib\security\java.security properties file or register them dynamically by calling the Security.addProvider() method in your code:

  Security.addProvider( new com.sun.net.ssl.internal.ssl.Provider());

Then you need to replace JavaMail's default socket factory with JSSE's SSL socket factory. This approach is similar to the one used in "Java Tip 96: Use HTTPS in Your Java Client Code"; however, in our case, we have no control over socket creation since sockets are created inside the JavaMail framework, not in the application code or storage provider. Fortunately, JavaMail accepts several undocumented properties, allowing you to set up a custom socket factory class and some other parameters. Those properties are:

  mail.<protocol>.socketFactory.class
  mail.<protocol>.socketFactory.fallback 
  mail.<protocol>.socketFactory.port
  mail.<protocol>.timeout

Now you can use the following code to replace a socket factory:

  final String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory";
  Properties props = System.getProperties();
  // IMAP provider
  props.setProperty( "mail.imap.socketFactory.class", SSL_FACTORY);
  // POP3 provider
  props.setProperty( "mail.pop3.socketFactory.class", SSL_FACTORY);
  // NNTP provider (if any)
  // props.setProperty( "mail.nntp.socketFactory.class", SSL_FACTORY);

If you want JavaMail to use only secure connections, you configure the appropriate properties so JavaMail doesn't fall back to an unsecure connection when a secure one fails:

  // IMAP provider
  props.setProperty( "mail.imap.socketFactory.fallback", "false");
  // POP3 provider
  props.setProperty( "mail.pop3.socketFactory.fallback", "false");
  // NNTP provider (if any)
  // props.setProperty( "mail.nntp.socketFactory.fallback", "false");

You then change the default port number to the corresponding port that your protocol's secure version uses; otherwise, you must use a fully qualified address (that includes a port number) in the URL passed to JavaMail (for example, imap://id:password@your.imap.server.com:993/folder/), or else you get an "unrecognized SSL handshake" exception. You specify these properties like so:

  // IMAP provider
  props.setProperty( "mail.imap.port", "993");
  props.setProperty( "mail.imap.socketFactory.port", "993");
  // POP3 provider
  props.setProperty( "mail.pop3.port", "995");
  props.setProperty( "mail.pop3.socketFactory.port", "995");
  // NNTP provider (if any)
  // props.setProperty( "mail.pop3.port", "563");
  // props.setProperty( "mail.pop3.socketFactory.port", "563");

After setting up all the properties, you can open a secure JavaMail session:

  Session session = Session.getInstance(props);

Certificates

Unfortunately, you may realize that the code above throws an SSLException "untrusted server cert chain" if the mail server certificate is not installed locally. In that case, you should obtain a correct, valid certificate for the server and use the keytool utility to add it to a local key storage at <javahome>\jre\lib\security\cacerts.

Alternatively, you can replace the default TrustManager. However, since you can't control socket creation and you want your TrustManager to work, you must use your own socket factory with SSLSocketFactory, which creates secure sockets.

Our simple example TrustManager implementation, DummyTrustManager.java, accepts all certificates without validation, even if the certificate name differs from the server name. This could be a security issue you may want to address in your application. For example, you can show a nice GUI dialog asking if the user trusts the certificate, and then store the certificate locally (you've probably seen these dialogs in Netscape Navigator or MS Internet Explorer).

Here is the code:

  import com.sun.net.ssl.X509TrustManager;
  import java.security.cert.X509Certificate;
  public class DummyTrustManager implements X509TrustManager {
    public boolean isClientTrusted( X509Certificate[] cert) {
      return true;
    }
    public boolean isServerTrusted( X509Certificate[] cert) {
      return true;
    }
    public X509Certificate[] getAcceptedIssuers() {
      return new X509Certificate[ 0];
    }
  }

You also need to implement a custom SSLSocketFactory, like DummySSLSocketFactor.java. We will use the Proxy pattern to change the TrustManager of a standard SSLSocketFactory:

  import com.sun.net.ssl.*;
  import java.io.IOException;
  import java.net.InetAddress;
  import java.net.Socket;
  import javax.net.SocketFactory;
  import javax.net.ssl.SSLSocketFactory;
  public class DummySSLSocketFactory extends SSLSocketFactory {
    private SSLSocketFactory factory;
    public DummySSLSocketFactory() {
      System.out.println( "DummySocketFactory instantiated");
      try {
        SSLContext sslcontext = SSLContext.getInstance( "TLS");
        sslcontext.init( null, // No KeyManager required
            new TrustManager[] { new DummyTrustManager()},
            new java.security.SecureRandom());
        factory = ( SSLSocketFactory) sslcontext.getSocketFactory();
      } catch( Exception ex) {
        ex.printStackTrace();
      }
    }
    public static SocketFactory getDefault() {
      return new DummySSLSocketFactory();
    }
    public Socket createSocket( Socket socket, String s, int i, boolean 
flag)
        throws IOException {
      return factory.createSocket( socket, s, i, flag);
    }
    public Socket createSocket( InetAddress inaddr, int i,
        InetAddress inaddr1, int j) throws IOException {
      return factory.createSocket( inaddr, i, inaddr1, j);
    }
    public Socket createSocket( InetAddress inaddr, int i) throws 
IOException {
      return factory.createSocket( inaddr, i);
    }
    public Socket createSocket( String s, int i, InetAddress inaddr, int j)
        throws IOException {
      return factory.createSocket( s, i, inaddr, j);
    }
    public Socket createSocket( String s, int i) throws IOException {
      return factory.createSocket( s, i);
    }
    public String[] getDefaultCipherSuites() {
      return factory.getSupportedCipherSuites();
    }
    public String[] getSupportedCipherSuites() {
      return factory.getSupportedCipherSuites();
    }
  }

Finally you should install the SocketFactory provider like this:

  Security.setProperty( "ssl.SocketFactory.provider", 
"DummySSLSocketFactory");

You should execute this code before creating any JavaMail sessions. Also, don't forget to make the DummySSLSocketFactory class visible to your code.

Joining technologies

We've created a sample application, called FolderList.java, based on the JavaMail mail folder listing demo, which uses the above code. It shows that you can securely connect to the mail server using JSSE by configuring the JavaMail framework properly. With this approach, you're ready to make your JavaMail-based application support secure communication without any significant changes in your own code.

Eugen Kuleshov is a software engineer at Canada-based Peregrine Systems, where he develops Java adapters/connectors for middleware platforms. During his free time, Eugen develops Java applications using OOAD practices and does research in security and cryptography for the Java platform and XML. Dmitry I. Platonoff is a senior Java developer with Canada-based ThinkDynamics, developing enterprise applications with Java. His interests include server-side development and Web technologies.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies