Domain Specific Smarthost SMTP Relay With cPanel

Photo of author

Rob Woodgate

Published:

Updated:

I moved my Trust Vega SaaS to a new web host recently, which meant the dedicated server IP address I’d spent the last 6 years nurturing to ensure our excellent email delivery had to change.

Until that point, I had never really had to think much about the wisdom of keeping web and email services separated, or the benefits of using a transactional email service like Amazon SES, MailGun or Sendgrid.

But faced with a new IP with, at best, no reputation, and at worst a shady past, I knew the time had come to bite the bullet and send emails via a reputable transactional email provider.

After a bit of research, I decided to create an account with MailGun.

Why Use A Smarthost SMTP Relay?

MailGun offer two main ways of sending emails – via their SMTP gateway and via their API.

Trust Vega, like most of my SaaS services, is built on top of WordPress, and it uses the wp_mail() function to pass the emails it creates to the email server using the default exim/sendmail server.

On a server that hosts its own email, that’s incredibly efficient, because WordPress can simply pass the email to the email server for processing… it doesn’t have to actually send it itself.

I chose to use a Smarthost SMTP relay rather than adding SMTP to WordPress directly in order to keep this efficient handoff as well as avoid rewriting Trust Vega’s emailer to use MailGun’s API.

The other reason for not sending via SMTP directly in WordPress is to avoid having to compromise the security of the new server by allowing scripts to make outgoing SMTP connections.

The other advantage is that once installed, this solution is easy to expand to other domains on the server that need to pass transactional emails to a third party service.

Adding A Smarthost Per-Domain

Rather than talk about the basics of smarthosts, I’m going to get straight to the meat and show you a flexible configuration that lets you specify a different transactional SMTP service (smarthost) for each domain on your cPanel server.

I can’t claim to have invented this method – in fact, it’s HEAVILY based on this cPanel forum post – which has been regurgitated verbatim in many places.

I have, however, made some tweaks / improvements / fixes to my version, which I’ll highlight as we go through this article.

Note: You will need root access to your server.

Step 1 – Backup your Exim Configuration

Login to WHM, go to Exim Configuration Manager, select the Backup tab and make a backup.

Don’t skip this step, seriously.

Step 2 – Add The Smarthost Config to Exim

Staying in Exim Configuration Manager, click the Advanced Tab

Copy in the contents of the box below into the box called Section: AUTH:

#Smart Host Sending
sendbysmarthosts:
    driver = plaintext
    public_name = LOGIN
    hide client_send = : ${extract{user}{${lookup{$sender_address_domain}partial()lsearch{/etc/exim_smarthosts}}}}: ${extract{pass}{${lookup{$sender_address_domain}partial()lsearch{/etc/exim_smarthosts}}}}

Note: The original forum post did a regular lsearch for user and password here. We are doing a partial()lsearch. This gives us flexibility when creating smarthosts by allowing subdomains to use the root domain smarthost if they don’t have one of their own.

Copy in the contents of the box below into the box called Section: PREROUTERS:

#Smart Host Sending
sendbysmarthostsrouter:
    driver = manualroute
    domains = ! +local_domains
    condition =  "${if eq{${lookup{$sender_address_domain}partial()lsearch{/etc/exim_smarthosts}{$value}}}{}{false}{true}}"
    condition = ${if eq{${lookup{$sender_address_domain}partial()lsearch{/etc/userdomains}}}{$sender_ident}}
    ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8
    headers_add = "${perl{mailtrapheaders}}"
    headers_remove = "x-php-script:x-php-originating-script"
    transport = sendbysmarthoststransport
    route_list = * ${extract{smtp}{${lookup{$sender_address_domain}partial()lsearch{/etc/exim_smarthosts}}}} byname

Note: This section differs to the original forum post in the following ways.

Firstly, the original forum post attempted to add some ‘wildcard’ flexibility with the partial-lsearch condition, but this was effectively useless as all the sub-lookups were strict lsearch.

I fixed this by changing the regular lsearch and partial-lsearch commands to partial()lsearch, which as mentioned above allows a subdomain to use the root domain smarthost if they don’t have one of their own.

Secondly, I’ve added a new condition that checks the sending cPanel user actually has access to the root domain the email is being sent out on!

This fixes a security hole in the original code, which would allow a user on the cPanel server to send out email via another user’s smarthost, just by setting their domain in the From address! (aka spoofing).

Finally, SpamAssassin scores any email with the X-PHP header(s) an extra 2.5 points by default as PHP scripts can be a common source of spam, so we remove these headers for emails going out via our smarthost router.

Copy in the contents of the box below into the box called Section: TRANSPORTSTART:

#Smart Host Sending
sendbysmarthoststransport:
    driver = smtp
    port = ${extract{port}{${lookup{$sender_address_domain}partial()lsearch{/etc/exim_smarthosts}}}}
    hosts_require_auth = <; $host_address
    hosts_require_tls = <; $host_address

Note: This section differs to the original forum post in the following ways.

Firstly, IPv6 addresses contain colons, so the <; is used to change the list separator to a semi-colon to avoid issues.

Secondly, as above, the regular lsearch is replaced with a partial()lsearch.

That’s all you need for this step, so save the configuration. This will also restart Exim.

Step 3 – Add A Smarthost Config File

If you’ve had a go at reading the snippets we added to Exim in step 2, you’ll notice that Exim customizes the smarthost based on the domain it finds in a file called /etc/exim_smarthosts.

We’ll add that file in this step, so fire up a root SSH session or use the WHM > Terminal.

The file should be owned by root and have 0644 permissions.

[root@host ~]# touch /etc/exim_smarthosts
[root@host ~]# chmod 0644 /etc/exim_smarthosts 
[root@host ~]# ls -al /etc/exim_smarthosts 
-rw-r--r-- 1 root root 321 Nov 26 19:43 /etc/exim_smarthosts

This file should contain your per-domain smarthost configs in the following format:

# sending by MailGun
mg.trustvega.com: [email protected] pass=***redacted*** smtp=smtp.mailgun.org port=587
domain2.com: user=MAILGUN_smtp_user pass=MAILGUN_smtp_password smtp=smtp.mailgun.org port=587

# sending by Sendgrid
domain3.com: user=SENDGRID_smtp_user pass=SENDGRID_api_password smtp=smtp.sendgrid.net port=587
sg.domain4.com: user=SENDGRID_smtp_user pass=SENDGRID_api_password smtp=smtp.sendgrid.net port=587

# sending by Mailjet
domain4.com: user=MAILJET_smtp_user pass=MAILJET_smtp_password smtp=in-v3.mailjet.com port=587

# sending by Sendpulse
domain5.com: user=SENDPULSE_smtp_user pass=SENDPULSE_smtp_password smtp=smtp-pulse.com port=587

The basic format is one smarthost configuration per line – blank lines and lines starting with a hash (#) are ignored. The order of the smarthosts doesn’t matter, you can group them by the transactional service they relate to as above, or in any other way that makes sense to you.

Each line starts with an index, which is the name of the sending (sub)domain followed by a colon. After the colon, each of the smarthost key parameters (user, pass, smtp, port) is specified as a key=value.

(Note: In the original forum post, the domain was specified twice: once as the index, then as a parameter. But the domain parameter was not used, so I removed it).

Because we’ve used partial()lsearch for smarthost lookups, you can set the smarthost indexes at the subdomain or root domain level, and the most specific one will be used.

So, in the example file above, the sg.domain4.com smarthost will route emails via SendGrid for any of the following senders:

  • <email>@sg.domain4.com
  • <email>@baz.sg.domain4.com

And the less specific domain4.com smarthost will route emails via MailJet for the following:

  • <email>@domain4.com
  • <email>@foo.domain4.com
  • <email>@bar.foo.domain4.com

So, set your smarthost domains at the lowest (sub)domain level you want them to apply at.

I’ve added the trustvega,com entry to the highlighted line above to demonstrate.

I’ve got MailGun set up on its own subdomain (the recommended config), so I’m only smarthost routing emails from <email>@mg.trustvega.com, not the whole domain. This allows me to send regular (main domain) addressed emails via the server in the normal way.

You only need to add the domains you want relayed via a smarthost to this file. Any domain not included in this file will continue to send email out directly via the server as before.

Step 4 – Test!

After installing, send some test emails to check:

  • The domains you added actually relay to your smarthost. In MailGun, for example, you can look in the Sending > Logs menu for your sending domain.
  • Sending still works as before for domains NOT included (you only need to test one domain)

Changes to the /etc/exim_smarthosts file should be picked up by Exim right away (at least that’s true on my servers), but depending on your environment, you may need to restart Exim for it to notice any changes. You can do this in WHM > Restart Services > Mail Server (Exim), or if you are still in terminal, you can run:

[root@host ~]# /scripts/restartsrv_exim

If you have any problems, you can reverse the changes by restoring the exim backup you made in step 1… or by removing the snippets you added in step #2.

Troubleshooting

The only downside I’ve found with this approach is if you have multiple smarthosts with the same smtp parameter (eg smtp.mailgun.org) and there is an authentication problem… such as getting one of the smarthost passwords wrong!

In this case, you will eventually see emails being queued for all hosts sharing that smtp server, as exim starts falling back to its retry schedules.

You can tell which smarthost is the issue by examining the exim retry database:

[root@host ~]# strings /var/spool/exim/db/retry

Once fixed, you can either wait for the retry period to sort itself out, or delete the retry database and restart Exim.

Leave a Comment