Saturday, February 28, 2009

Making Exim4 send outgoing mail through Gmail

Say you have a personal server on the Internet, and you want it to send e-mail.

In the good old days, that meant nothing more than ensuring you could make outbound connections on tcp port 25. In our world of pervasive junk e-mail, however, it can take a great deal of work to get your sent mail to comply with all of the guidelines that major e-mail providers recommend to avoid having your mail classified as spam (if it is, it will likely never be seen). You'll get your own domain in DNS so you can set up DKIM milters and SPF records, make sure your MTA has appropriate retry rules as expected by greylisting hosts and that it responds properly to callback verifications, get a static IP or two in a reputable neighborhood - oh, and make sure your ISP allows you to control the PTR record. You might even throw in some Hashcash headers for good measure. And even after all that, you might have a hard time getting your mail through to your recipients until you've been sending from that IP for long enough to build up a digital reputation.

Hardly seems worth it anymore, does it?

One potential solution (although one I haven't seen discussed or used much) is to teach your server how to authenticate to an existing mail provider so it can send mail through it. Your provider does all the work to ensure availability and deliverability, so you don't have to worry about it. And there are some good free e-mail providers.

Here is the solution I rolled for a particular server. I configured exim4 to send e-mail using an account on a Google Apps domain (so, essentially, it's going through Gmail). Gmail provides a free SMTP sending interface, and one thing Exim is good at is speaking SMTP, but it takes a bit of configuration to express what we need.

The centerpiece of my approach is a file called passwd.gmail in the Exim config directory. It maps addresses of Gmail/Google Apps accounts to passwords. When Exim gets an outgoing mail with a sender address in the map, it uses the password to authenticate to Gmail's SMTP server so Gmail can resend it.

A sample passwd.gmail:

someaddress@gmail.com:         S00perSekritP@ssw0rd
daemon@mygoogleappsdomain.com: j2K&.sh4J
foo@bar.com: em8baiXu


First, I defined a router called use_gmail:


use_gmail:
debug_print = "R: use_gmail for $domain (sender $sender_address)"
driver = manualroute
require_files = CONFDIR/passwd.gmail
senders = lsearch;CONFDIR/passwd.gmail
route_list = * smtp.gmail.com::587
transport = smtp_through_gmail


The manualroute router type is for manually routing (that is, determining the next-hop SMTP server) based on certain tests. Normally you make the decision based on the destination address, but here we check the sender address. And the manual destination we set for those addresses is always smtp.gmail.com, port 587.

It instructs that matching mail be sent to its destination using the smtp_through_gmail transport:


smtp_through_gmail:
debug_print = "T: smtp_through_gmail for $local_part@$domain"
driver = smtp
connection_max_messages = 1
hosts_require_auth = *
hosts_require_tls = *
tls_certificate = CONFDIR/exim.crt
tls_privatekey = CONFDIR/exim.key
tls_verify_certificates = /etc/ssl/certs/ca-certificates.crt


Our transport always uses SMTP, and the remote end always requires TLS and authentication. We verify the certificate presented by the remote end (smtp.gmail.com) to make sure the certificate is signed by an authority in our local list, and that it actually is a certificate for the right address. The exim.crt and exim.key files are for our client-side certificate. Creating such a key/cert pair is beyond the scope here, but there is good info all over the net.

One other parameter there which bears explanation is connection_max_messages = 1. It says that only one message will ever be sent per connection. I'm not completely sure whether that's necessary, but I suspect that if it were not there, Exim might try to send multiple messages from different senders after authenticating as only one. That would be bad- you'd get messages coming from the wrong sender all over the place.

The last step is to tell the authenticators how to get the appropriate passwords. Since I didn't need to use the default login or plain authenticators, I just tweaked them for my needs. If you have a separate need for them (for example, if you need to want to allow incoming mail also authenticated with username/password pairs) then you'll need to work out how to have those authenticators get the right passwords from the right place for each situation.

Here are my authenticators, though:


PASSWDLINE=${sg{\
${lookup{$sender_address}\
nwildlsearch\
{CONFDIR/passwd.gmail}\
{$value}\
fail}\
}\
{\\N[\\^]\\N}\
{^^}\
}

plain:
driver = plaintext
public_name = PLAIN
client_send = "<; ${if !eq{$tls_cipher}{}\
{^$sender_address^PASSWDLINE}\
fail}"

login:
driver = plaintext
public_name = LOGIN

# Return empty string if not non-TLS AND looking up
# $host in passwd-file yields a non-empty string;
# fail otherwise.
client_send = "<; ${if and{\
{!eq{$tls_cipher}{}}\
{!eq{PASSWDLINE}{}}\
}\
{}fail}\
; ${extract{1}{::}{PASSWDLINE}}\
; ${sg{PASSWDLINE}{\\N([^:]+:)(.*)\\N}{\\$2}}"


I would go through an explanation of all that but it makes my head hurt just to look at it. It just says only to use passwords when the connection is encrypted (a tls_cipher is in use) and to get them from the passwd.gmail line. It also tells Exim the details of how both authentication mechanisms (PLAIN and LOGIN, they're pretty standard) work. But ugh. Even Perl would be easier to read than Exim's funkotron string expansion language. If you need to find out more, check the Exim docs here and here.

That's all the necessary configuration. You'll probably want to make a few extra tweaks, though. For example, standard Exim installations don't allow normal users to set the envelope (sender) address to something besides user@mailname. On my system, all the user accounts are trusted to send mail from any of the configured addresses, so I just add them all to Exim's trusted_users list. There are other ways to provide that permission in a more fine-grained way, but again, beyond the scope here.

To set the sender address explicitly for a mail, pipe the mail (including all headers) into


/usr/sbin/sendmail -oi -f $SENDER_ADDRESS <list of recipient addresses>


(Change the path to your sendmail binary as necessary.)

And that's it. Outgoing mail from this server having one of the supported sender addresses doesn't ever get lost in spam folders anymore.

I am no kind of Exim expert, so I might have missed some ways to make this easier, more general, or more robust (although I haven't had any problems yet). Comments and improvements are welcome.

No comments:

Post a Comment