Monday, August 13, 2012

Android app to send emails from GMail using OAuth for authorisation

UPDATE: This no longer works, since OAuth 1 is no longer supported by Google.
 
It is appalling to see how few documents and sample codes exist for something this simple (check this): I want an app to authorise me to GMail through OAuth and let me send emails.

I am going to release parts of my project that deals with this as a complete working application, sounds good ha? The bad news is that I'm using OAuth 1 which is officially deprecated by Google, yet they don't advise on how to do the same with OAuth 2 as many things are different in the new protocol.

Feel free to download my code here or check it out from SVN:
svn checkout http://tinywebgears-samples.googlecode.com/svn/trunk/email-oauth/ tinywebgears-samples-read-only
You can import it in Eclipse once you have checked it out, enjoy.


While you are here, it is worthwhile to take you through some important parts of my code. Everything mentioned here is already put in the SVN module, so you don't need to copy/paste from here.

As you can see I'm using a patched version of commons-net library, to be able to pass "AUTH XOAUTH BASE64_TOKEN" to the SMTP server (update: this feature is available in commons-net, from this revision onwards):
diff -ru commons-net-3.1-src/src/main/java/org/apache/commons/net/smtp/AuthenticatingSMTPClient.java commons-net-3.1-src-patched/src/main/java/org/apache/commons/net/smtp/AuthenticatingSMTPClient.java
--- commons-net-3.1-src/src/main/java/org/apache/commons/net/smtp/AuthenticatingSMTPClient.java 2012-02-15 03:11:49.000000000 +1100
+++ commons-net-3.1-src-patched/src/main/java/org/apache/commons/net/smtp/AuthenticatingSMTPClient.java 2012-07-25 00:36:30.809267711 +1000
@@ -208,6 +208,10 @@
             }
             return SMTPReply.isPositiveCompletion(sendCommand(
                 new String(Base64.encodeBase64(password.getBytes()))));
+        }
+        else if (method.equals(AUTH_METHOD.XOAUTH))
+        {
+            return SMTPReply.isPositiveIntermediate(sendCommand(username));
         } else {
             return false; // safety check
         }
@@ -243,7 +247,9 @@
         /** The standarised (RFC2195) CRAM-MD5 method, which doesn't send the password (secure). */
         CRAM_MD5,
         /** The unstandarised Microsoft LOGIN method, which sends the password unencrypted (insecure). */
-        LOGIN;
+        LOGIN,
+        /** XOAuth method which accepts a signed and base64ed OAuth URL. */
+        XOAUTH;

         /**
          * Gets the name of the given authentication method suitable for the server.
@@ -258,6 +264,8 @@
                 return "CRAM-MD5";
             } else if (method.equals(AUTH_METHOD.LOGIN)) {
                 return "LOGIN";
+            } else if (method.equals(AUTH_METHOD.XOAUTH)) {
+                return "XOAUTH";
             } else {
                 return null;
             }
The main OAuth functions are performed by a helper class, which comprise, by their appearing order:
  • creating a request token,
  • making an authorization url,
  • getting an access token using the verifier code passed back by Google,
  • making a secure request to the user details API to get user's email address,
  • and finally making an XOAUTH string every time an email is due to be sent.
One thing that took me a while to figure out was that the access token is long-lived and almost never expires (unless the user revokes it), but the XOAUTH string has a very short life-time and hence must be generated quite frequently.

Let's have a look at important parts of the code. Using Scribe library my code is much less than when I used Signpost:

package com.tinywebgears.gmailoauth.util;

import org.scribe.builder.ServiceBuilder;
import org.scribe.builder.api.GoogleApi;
import org.scribe.model.OAuthRequest;
import org.scribe.model.Response;
import org.scribe.model.Token;
import org.scribe.model.Verb;
import org.scribe.model.Verifier;
import org.scribe.oauth.OAuthService;

public class OAuthHelperV1 implements OAuthHelper
{
    public static final String OAUTH_APP_KEY = "anonymous";
    public static final String OAUTH_APP_SECRET = "anonymous";
    public static final String URL_OAUTH_AUTHORIZE = "https://www.google.com/accounts/OAuthAuthorizeToken?oauth_token=";
    public static final String OAUTH_PARAM_VERIFIER = "oauth_verifier";

    private OAuthService service;
    private Token requestToken;

    private static OAuthHelperV1 oauthHelper;

    static
    {
        if (oauthHelper == null)
        {
            String scope = OAUTH_SCOPE_EMAIL + " " + OAUTH_SCOPE_GMAIL;
            oauthHelper = new OAuthHelperV1(OAUTH_APP_KEY, OAUTH_APP_SECRET, scope, OAUTH_CALLBACK_URL, OAUTH_APP_NAME);
        }
    }

    public static OAuthHelperV1 get()
    {
        return oauthHelper;
    }

    public OAuthHelperV1(String consumerKey, String consumerSecret, String scope, String callbackUrl, String appname)
    {
        service = new ServiceBuilder().provider(GoogleApi.class).apiKey(consumerKey).apiSecret(consumerSecret)
                .scope(scope).callback(callbackUrl).build();
    }

    @Override
    public String getAuthorizationUrl()
    {
        requestToken = service.getRequestToken();
        return URL_OAUTH_AUTHORIZE + requestToken.getToken();
    }

    @Override
    public String extractVerifier(String uri)
    {
        if (uri.indexOf(OAUTH_PARAM_VERIFIER + "=") != -1)
            return UriUtil.getParam(uri, OAUTH_PARAM_VERIFIER);
        return null;
    }

    @Override
    public Token getAccessToken(String verifier)
    {
        Token accessToken = service.getAccessToken(requestToken, new Verifier(verifier));
        return accessToken;
    }

    @Override
    public String makeSecuredRequest(Token accessToken, String url)
    {
        OAuthRequest request = new OAuthRequest(Verb.GET, url);
        service.signRequest(accessToken, request);
        request.addHeader("GData-Version", "3.0");
        Response response = request.send();
        System.out.println("Got it! Lets see what we found...");
        System.out.println();
        System.out.println(response.getCode());
        System.out.println(response.getBody());
        return response.getBody();
    }

    @Override
    public void signRequest(Token accessToken, OAuthRequest request)
    {
        service.signRequest(accessToken, request);
    }
}

Here is also the code that sends emails:

package com.tinywebgears.gmailoauth.mail;

import java.io.PrintWriter;
import java.io.Writer;
import java.security.AccessController;
import java.security.Provider;
import java.security.Security;

import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.smtp.AuthenticatingSMTPClient;
import org.apache.commons.net.smtp.SMTPReply;
import org.apache.commons.net.smtp.SimpleSMTPHeader;

import android.util.AndroidRuntimeException;
import android.util.Log;

public class OAuthGMailSender
{
    private static final String TAG = "OAuthGMailSender";

    private String mailhost = "smtp.gmail.com";
    private Integer mailport = 587;
    private String xoauthToken;

    static
    {
        Security.addProvider(new JSSEProvider());
    }

    public OAuthGMailSender(String xoauthToken)
    {
        this.xoauthToken = xoauthToken;

    }

    public synchronized void sendMail(String subject, String body, String sender, String recipient) throws Exception
    {
        Log.d(TAG, "Sending email...");
        SimpleSMTPHeader header = new SimpleSMTPHeader(sender, recipient, subject);

        AuthenticatingSMTPClient client = new AuthenticatingSMTPClient();
        client.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
        client.connect(mailhost, mailport);
        client.login();
        client.execTLS();
        client.auth(AuthenticatingSMTPClient.AUTH_METHOD.XOAUTH, xoauthToken, null);

        if (!SMTPReply.isPositiveCompletion(client.getReplyCode()))
        {
            Log.e(TAG, "SMTP Authentication failed: " + client.getReplyCode() + ":" + client.getReplyString());
            client.disconnect();
            throw new AndroidRuntimeException("SMTP Authentication failed.");
        }

        client.setSender(sender);
        client.addRecipient(recipient);

        Writer writer = client.sendMessageData();
        if (writer != null)
        {
            writer.write(header.toString());
            writer.write(body);
            writer.write("\n.\n");
            writer.close();
            client.completePendingCommand();
        }

        client.logout();

        client.disconnect();
    }

    @SuppressWarnings("serial")
    public static final class JSSEProvider extends Provider
    {
        public JSSEProvider()
        {
            super("HarmonyJSSE", 1.0, "Harmony JSSE Provider");
            AccessController.doPrivileged(new java.security.PrivilegedAction<Void>()
            {
                public Void run()
                {
                    put("SSLContext.TLS", "org.apache.harmony.xnet.provider.jsse.SSLContextImpl");
                    put("Alg.Alias.SSLContext.TLSv1", "TLS");
                    put("KeyManagerFactory.X509", "org.apache.harmony.xnet.provider.jsse.KeyManagerFactoryImpl");
                    put("TrustManagerFactory.X509", "org.apache.harmony.xnet.provider.jsse.TrustManagerFactoryImpl");
                    return null;
                }
            });
        }
    }
}


Read less interesting stuff yourselves!