Sending Mail via GMail with JavaMail

by
Tags: , ,

If you use JavaMail and/or the Spring MailSender bean to send mail through a GMail account with JavaMail these days, you get an authentication error saying GMail blocks less secure apps. At that point there are two options:

  1. Configure your GMail account to accept low-security connections (e.g. https://www.google.com/settings/security/lesssecureapps)
  2. Configure your JavaMail sender to use OAuth for authentication

This article will describe the second approach in detail.

OAuth Basics

Instead of usernames and passwords, OAuth deals with tokens.
First, you set up an application. Then you get an application ID and “secret” (a string of text), with which you can generate tokens.
Next, your application requests access to a specific GMail account (the one you plan to send mail from). Somebody with access to that account will need to visit a Web page and grant access to that application for the GMail account in question. That will generate another string of text which you can convert into tokens.
The first token is the “refresh token”. This is generally long-lived, expiring after six months of disuse or after too many other refresh tokens have been generated.
The second token is the “access token”. This generally lasts about an hour. This is the one you need in order to actually connect to GMail to send mail.
So then, you do all this setup, and your connection procedure looks like this:

  1. Check whether you have a valid (unexpired) access token
  2. If not, use your refresh token to generate a new access token
  3. Connect to GMail using the access token instead of a password, and send mail
  4. Profit!

Setting up for OAuth authentication

The specific steps for the first-time setup look like this:

  1. Go to https://console.developers.google.com/ and create a new “project”. You will need a Google account, which may or may not be the same as the one you intend to send mail from.
  2. When the project dashboard comes up for the new project, select “Enable and manage APIs”
  3. Select “Credentials” on the left
  4. Select “OAuth Consent Screen” on the right
  5. Enter a product name in “Product name shown to users”. When you need to approve access to the GMail account, you’ll see a message like “Do you want to allow [Product Name] to have full control over your GMail account?” so it should be a name someone will recognize.
  6. Hit “Save” and go to the “Credentials” tab
  7. Hit “Create Credentials” then “OAuth Client ID”
  8. Select the application type “Other” and then enter a name or description for the application (this one is not shown to users)
  9. This gives you a popup with your Client ID and Client Secret. Save both of those. You will need them for all the future steps.
  10. Generate a refresh key and your first access key. If you can run Python scripts, the easiest way is to run the oauth2.py file here (click it and then view Raw to download it). The top of the file has the parameters you need to pass when you run it, under “1. The first mode is used to generate and authorize an OAuth2 token.” You’ll need the GMail account you want to send mail from, and the Client ID and Client Secret generated above.
  11. When you run the Python script with that syntax, it will tell you to visit a Web URL. Copy and paste that into a browser. That’s where you’ll get the prompt for whether you want to allow the application [Product Name] to have full control over the GMail account in question. When that is approved, it will generate another text string for you.
  12. Copy that text string back into the waiting Python application. At that point it will emit the refresh token, your first access token, and the lifetime (in seconds) of the access token. Save at least the refresh token.

You should do all this only once. You will be left with a Client ID, Client Secret, and Refresh Token, all of which are strings of text. You can save the first Access Token if you like; if you do it’s worth noting the current time so you can calculate when it will expire.

Configuring a Spring MailSender for OAuth authentication

First, you must have the JavaMail reference implementation, version 1.5.5 or later. As of this writing, this is newer than what’s available in Maven or the JavaEE download package. You can get it from the JavaMail download site. The token response is formatted as JSON, so I’ve used Jackson to parse it in this example, though it’s simple enough that virtually anything would work.
The basic configuration for a MailSender that will connect to GMail looks like this:

<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
	<property name="host" value="smtp.gmail.com"/>
	<property name="port" value="587"/>
	<property name="username" value="username@gmail.com"/>
	<property name="javaMailProperties">
		<props>
			<!-- Use SMTP transport protocol -->
			<prop key="mail.transport.protocol">smtp</prop>
			<!-- Use SMTP-AUTH to authenticate to SMTP server -->
			<prop key="mail.smtp.auth">true</prop>
			<!-- GMail requires OAuth to not be considered "low-security" -->
			<prop key="mail.smtp.auth.mechanisms">XOAUTH2</prop>
			<!-- Use TLS to encrypt communication with SMTP server -->
			<prop key="mail.smtp.starttls.enable">true</prop>
			<prop key="mail.debug">false</prop>
		</props>
	</property>
</bean>

Then your code should have access to your Client ID, Client Secret, and Refresh Token. A simple example (without very intelligent handling if the operation fails) would look something like this:

private static String TOKEN_URL = "https://www.googleapis.com/oauth2/v4/token";
private JavaMailSender sender;
private String oauthClientId = "fixme.apps.googleusercontent.com";
private String oauthSecret = "fixme";
private String refreshToken = "fixme";
private String accessToken = "fixme";
private long tokenExpires = 1458168133864L;
public void sendMessage(...) {
    if(System.currentTimeMillis() > tokenExpires) {
        try {
            String request = "client_id="+URLEncoder.encode(oauthClientId, "UTF-8")
                    +"&client_secret="+URLEncoder.encode(oauthSecret, "UTF-8")
                    +"&refresh_token="+URLEncoder.encode(refreshToken, "UTF-8")
                    +"&grant_type=refresh_token";
            HttpURLConnection conn = (HttpURLConnection) new URL(TOKEN_URL).openConnection();
            conn.setDoOutput(true);
            conn.setRequestMethod("POST");
            PrintWriter out = new PrintWriter(conn.getOutputStream());
            out.print(request); // note: println causes error
            out.flush();
            out.close();
            conn.connect();
            try {
                HashMap<String,Object> result;
                result = new ObjectMapper().readValue(conn.getInputStream(), new TypeReference<HashMap<String,Object>>() {});
                accessToken = (String) result.get("access_token");
                tokenExpires = System.currentTimeMillis()+(((Number)result.get("expires_in")).intValue()*1000);
            } catch (IOException e) {
                String line;
                BufferedReader in = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
                while((line = in.readLine()) != null) {
                    System.out.println(line);
                }
                System.out.flush();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    ((JavaMailSenderImpl)this.sender).setPassword(accessToken);
    // Now send mail like normal
}

Note that it will take some time (on the order of a second) to generate the access token if the old one has expired, but then it can be reused until it expires (normally for an hour) before needing to regenerate it.
If you get an authentication failure, set mail.debug to true in the Spring configuration, and look at the start of the debug information for the JavaMail implementation version. Again, it should be 1.5.5 or greater.

References

Announcement of OAuth support in JavaMail with cursory details: https://java.net/projects/javamail/pages/OAuth2
Documentation for the request to generate a new access token from a refresh token: https://developers.google.com/identity/protocols/OAuth2InstalledApp#refresh