Avoid spam/junk folders when sending mail from web apps
If you’re running a local mail client to send mail from your web application, you’ve probably already spent hours upon hours wondering why mail always ends up in the recipients spam/junk folder. No matter what combination of custom headers you pass to PHP’s mail function- yahoo, hotmail and maybe even gmail still marks it as spam. Have no fear. The solution is quite simple.
There are two options that are very similar. They are DKIM and DomainKeys. When I was first experimenting, I had installed DomainKeys only to realize it worked with gmail and yahoo, but not hotmail. After some reading I found that DKIMProxy implemented both DKIM and DomainKeys, so I ditched DomainKeys and went with DKIMProxy. This covers all the major email services and ISPs. Switching was easy, because they work exactly the same with only a few minor differences in syntax.
Read more about how those protocols work here:
Essentially, DKIM is a solution for email providers to verify that mail being sent from a particular domain is authorized. The way it works is that the domain owner inserts a TXT record into DNS for that domain. This record contains a public key. Each e-mail that gets sent must be signed using the private key, the signature gets placed into the header of the email. Email providers can then verify the authenticity of each email that is sent using classic pgp-like signature techniques. DKIMProxy is a service that runs on your server, works with your already existing mail server (sendmail, postfix, qmail, etc..) and can automatically sign your mail pieces.
Below I highlight the steps for setting up PostFix + DKIMProxy on FreeBSD to send mail from the local machine ONLY. This will be setup on an application server whose sole purpose is sending mail through the local relay, not receiving or relaying email from other machines. If you’ve already setup PostFix for wider purposes, you’re probably already pretty comfortable with it; just use the same techniques to specify the content_filter for any other listening service and skip my PostFix write-up. There will be some differences if you’re running this instance of FreeBSD in a jail (as I do), however I will document those differences and the security precautions you’ll need to take as well.
If you’re not using FreeBSD, you’ll have to substitute my use of ports with your distribution’s package management command. The location of your config files may also vary, as well as the start-up scripts; otherwise, it’s all the same.
Installing / Configuring Postfix:
cd /usr/ports/mail/postfix make & make install
Choose yes to modify mailer.conf
Stop sendmail if it’s running:
cd /etc/mail make stop
Edit your rc.conf file to turn off sendmail and enable postfix. Add the following:
If we only want to send local mail we can comment out the following line in /usr/local/etc/postfix/master.cf :
#smtp inet n - n - - smtpd
This is beneficial if we are running postfix in a jail that doesn’t have access to a loopback device. If you were running in a jail, but wanted to run an SMTP server as well, you could specify an IP address to listen on by prepending the line with the ip address as follows:
10.0.0.10:smtp inet n - n - - smtpd
It’s fine to comment out though.
Edit your /usr/local/etc/main.cf file according to your needs. Since I am just using this to send mail from my domain, but am using Google Apps to receive mail I do the following in main.cf :
myhostname = mydomain.com inet_interfaces = loopback-only mydestination = localhost local_recipient_maps=
The default main.cf file has those specific parameters documented for your reference (RTFM).
Now start PostFix :
Check /var/log/maillog for errors. If there is an error about reading /etc/aliases.db, you probably just need to generate it. Run the `newaliases` command and it should solve your problems.
Test sending mail from the local machine to make sure PostFix works. You can either use a PHP script or run sendmail manually. If it doesn’t show in your inbox, check the spam folder and /var/log/maillog .
You need to install the following Perl modules… CPAN doesn’t work well from a jail console, and if you’re like me and don’t want to install SSH or Tmux to fix your tty issues in jails, the following commands will install the needed Perl modules from the command line and works no matter what distribution you use:
perl -MCPAN -e'CPAN::Shell->install("Crypt::OpenSSL::RSA")' perl -MCPAN -e'CPAN::Shell->install("Digest::SHA")' perl -MCPAN -e'CPAN::Shell->install("Digest::SHA1")' perl -MCPAN -e'CPAN::Shell->install("Mail::Address")' perl -MCPAN -e'CPAN::Shell->install("MIME::Base64")' perl -MCPAN -e'CPAN::Shell->install("Net::DNS")' perl -MCPAN -e'CPAN::Shell->install("Net::Server")' perl -MCPAN -e'CPAN::Shell->install("Test::More")' perl -MCPAN -e'CPAN::Shell->install("Error")' perl -MCPAN -e'CPAN::Shell->install("Text::Wrap")' perl -MCPAN -e'CPAN::Shell->install("Mail::Address")' perl -MCPAN -e'CPAN::Shell->install("Mail::DomainKeys")' perl -MCPAN -e'CPAN::Shell->install("Mail::DKIM")'
cd /usr/ports/mail/dkimproxy make && make install
Add the following to your rc.conf :
We won’t configure “in” because we are only interested in sending mail out right now.
Generate some keys:
cd /usr/local/etc/ openssl genrsa -out privatedkim.key 1024 openssl rsa -in privatedkim.key -pubout -out publicdkim.key
My /usr/local/etc/dkimproxy_out.conf file looks like this:
# specify what address/port DKIMproxy should listen on listen 10.0.0.10:10027 # specify what address/port DKIMproxy forwards mail to relay 10.0.0.10:10028 # specify what domains DKIMproxy can sign for (comma-separated, no spaces) domain mydomain.com # specify what signatures to add signature dkim(c=relaxed) signature domainkeys(c=nofws) # specify location of the private key keyfile /usr/local/etc/privatedkim.key # specify the selector (i.e. the name of the key record put in DNS) selector selector1
You can just copy /usr/local/etc/dkimproxy_out.conf.example to /usr/local/etc/dkimproxy_out.conf and make the appropriate changes. The selector parameter is what we’ll specify later in DNS.
dkimproxy is going to listen on the ip address / port (10027) we’ve specified for mail and it’s going to proxy smtp traffic to the ip address / port (10028) we’ve specified, with the appropriate headers attached.
Security concern: If you’re using a jail and/or specified a public facing IP address to listen on, you’ve just opened up relaying for the world even if relaying is turned off in postfix. The reason is because you’ve opened a proxy that will listen publicly and forward internally to postfix. SOOO… If you’ve specified a public facing IP address, either change it to an internal-only or firewall it off.
Back to PostFix Configuration:
Now we need to configure postfix to use the DKIM proxy as a content_filter (sending mail out to it on port 10027) and then receiving it back on 10028.
We’ll be editing /usr/local/etc/postfix/master.cf and since we are only concerned with sending local mail, you can edit the pickup line to reflect the following:
pickup fifo n - n 60 1 pickup -o content_filter=dksign:[10.0.0.10]:10027
and add the following somewhere below:
dksign unix - - n - 10 smtp -o smtp_send_xforward_command=yes -o smtp_discard_ehlo_keywords=8bitmime 10.0.0.10:10028 inet n - n - 10 smtpd -o content_filter= -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks -o smtpd_helo_restrictions= -o smtpd_client_restrictions= -o smtpd_sender_restrictions= -o smtpd_recipient_restrictions=permit_mynetworks,reject -o mynetworks=10.0.0.10 -o smtpd_authorized_xforward_hosts=10.0.0.10,mydomain.com,localhost
Now restart postfix and start dkimproxy:
/usr/local/etc/rc.d/postfix reload /usr/local/etc/rc.d/dkimproxy_out start
Check netstat -na to make sure you’ve got services listening on both 10027 and 10028 . If not, check /var/log/maillog for errors.
Your DNS Records:
You’ll need to add three DNS records:
@ 3600 IN TXT "v=spf1 a mx ~all" _domainkey 3600 IN TXT "t=y; o=~;" selector1._domainkey 3600 IN TXT "g=*; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDhrfYr2KLFiU7Zo6H06LlhFBEpif/Tb7oBJvKdEIm1uED9FqJump/q6RSt3Yw1iuM3iBaQcPohGbdoGiuaJGOUWMOblsSXkAOWxl4lbI5UQ6zCTBpVdLVDVWJ0E3UW1YJs1crSBdmG9G3WghrvIRkHzxfDMqndIV5gliYt+nmqXQIDAQAB;"
Once your tests are complete you can remove t=y; More info
Where you see p=MIGfMA…, you need to concatenate the publicdkim.key file into one line without the header/footer. So for example my publicdkim.key file originally looks like this:
-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDhrfYr2KLFiU7Zo6H06LlhFBEp if/Tb7oBJvKdEIm1uED9FqJump/q6RSt3Yw1iuM3iBaQcPohGbdoGiuaJGOUWMOb lsSXkAOWxl4lbI5UQ6zCTBpVdLVDVWJ0E3UW1YJs1crSBdmG9G3WghrvIRkHzxfD MqndIV5gliYt+nmqXQIDAQAB -----END PUBLIC KEY-----
You can see how my p=… is related.
If you are using a GUI DNS editor tool, you’ll have to figure out how to place these into DNS using that tool.
Now to test it out:
Assuming DNS has propagated, you can try sending a couple e-mails. tail -f /var/log/maillog while you do and you should be able to see the proxy in action. If you send mail to hotmail or google, you can view the message source and see the DKIM and DomainKeys header information as well.
You can also use the following tools which have been extraordinarily helpful to me when setting this up:
- Send an e-mail from your server to: firstname.lastname@example.org
It will respond with some helpful diagnostic information, but make sure you can receive an e-mail response to the reply-to field.
Submit your SPF record to evil Microsoft
If everything passes, you should submit your spf record to Microsoft. They are sort of like the really strong dumb kid on the block who can steal your candy, but can’t perform a DNS lookup on their own. Without submitting to Microsoft your mail will end up in the hotmail / live.com SPAM folder with a senderid temperror. You can sign up for a hotmail account and view full message source to see where it fails, then burn your monitor, keyboard and clothes for having signed up for a Microsoft product.
DomainKeys DNS Testing Tools:
That should get you on your way. I’ve pretty much covered every hurdle I’ve come across when setting this up. You’ll find plenty others who have similar, but slightly different setups by doing a cursory Google search.