Tuesday, 19 May 2009

How to send email via Gmail using Erlang

One of my pet projects, www.dayfindr.com, integrates with email to send notifications to users.

I use Google Apps for email infrastructure, so you need an SMTP client that supports TLS. At the time, I couldn't find a simple Erlang SMTP client that could handle TLS, so I used a command-line SMTP client.

For my new pet project, for want of a better name temporarily called The Isabelle Project, I need to add some email functionality. This time I would prefer to use an Erlang solution with proper error handling and logging.

I looked at the SMTP protocol on Wikipedia, and it didn't seem to difficult. Erlang's built-in ssl module also seemed to support TLS. So, with a bit of trial and error, here's the result:


connect() ->
{ok, Socket} = ssl:connect("smtp.gmail.com", 465, [{active, false}], 1000),
send(Socket, "HELO localhost"),
send(Socket, "AUTH LOGIN"),
send(Socket, binary_to_list(base64:encode("___@gmail.com"))),
send(Socket, binary_to_list(base64:encode("johngalt"))),
send(Socket, "MAIL FROM: <___@gmail.com>"),
send(Socket, "RCPT TO:<___@gmail.com>"),
send(Socket, "DATA"),
send_no_receive(Socket, "From: <___@gmail.com>"),
send_no_receive(Socket, "To: <___@gmail.com>"),
send_no_receive(Socket, "Date: Tue, 15 Jan 2008 16:02:43 +0000"),
send_no_receive(Socket, "Subject: Test message"),
send_no_receive(Socket, ""),
send_no_receive(Socket, "This is a test"),
send_no_receive(Socket, ""),
send(Socket, "."),
send(Socket, "QUIT"),

send_no_receive(Socket, Data) ->
ssl:send(Socket, Data ++ "\r\n").

send(Socket, Data) ->
ssl:send(Socket, Data ++ "\r\n"),

recv(Socket) ->
case ssl:recv(Socket, 0, 1000) of
{ok, Return} -> io:format("~p~n", [Return]);
{error, Reason} -> io:format("ERROR: ~p~n", [Reason])

And the output from the Erlang shell:

3> application:start(ssl).
4> smtp:connect().
"220 mx.google.com ESMTP y37sm613282mug.19\r\n"
"250 mx.google.com at your service\r\n"
"334 VXNlcm5hbWU6\r\n"
"334 UGFzc3dvcmQ6\r\n"
"235 2.7.0 Accepted\r\n"
"250 2.1.0 OK y37sm613282mug.19\r\n"
"250 2.1.5 OK y37sm613282mug.19\r\n"
"354 Go ahead y37sm613282mug.19\r\n"
"250 2.0.0 OK 1242683885 y37sm613282mug.19\r\n"
"221 2.0.0 closing connection y37sm613282mug.19\r\n"

The only tricky bit is that for the AUTO LOGIN, the received text and the username and password you send is base-64 encoded. By default the connect is active=false, which means the responses are send to the creating process directly. Using passive mode requires explicit receiving of the response using ssl:recv/2

You'll have to handle errors better if you use this in production, but the basic protocol is pretty straightforward...


Harish Mallipeddi said...

On a related note, I patched the epop package from jungerl to add SSL support. You can use it to fetch email from GMail.


Pichi said...

Just for curiosity, is there any reason to use binary_to_list/1? Also ssl:send(Socket, Data ++ "\r\n") should be simply ssl:send(Socket, [Data|"\r\n"]).

Benjamin Nortier said...

base64:encode/1 returns a binary, so the Data ++ "\r\n" won't work.

I could rewrite my send function to work like this:

send(Socket, Data) ->
ssl:send(Socket, Data),

which would support binaries and lists (but it's does two sends).

You're right about the [Data|"\r\n"], since ssl:send works with iolists, not just lists. But if Data is a binary, [Data|"\r\n"] is not legal since a binary is only allowed at the end of the list.

Perhaps this could be a better implementation:

send(Socket, Data) when is_binary(Data) ->
send(Socket, binary_to_list(Data));
send(Socket, Data) when is_list(Data) ->
ssl:send(Socket, [Data|"\r\n"]),

I can't think of a way to eliminate the binary_to_list for a single send. Can you?

Mamut said...

A million thanks! This has really helped me

Timothy Johansson said...

I'd recommend to try out AlphaMail which have a module for Erlang: http://comfirm.se/send-transactional-email-with-erlang/