I like to have a working mail setup on all machines as this allows to be notified about cronjobs that failed and also to be able to send other notifications that would otherwise be lost. It is also especially useful for things like git send-email or automatically sending GPG signatures with caff to others.
However, mails cannot just be sent from any device and mail servers on the internet usually reject mails from dial-up IPs or public WiFi networks. To fight spam, techniques like SPF have been developed that restrict the mail servers that are allowed to send mails for the domain name used in the
From: field. Therefore the best way is to relay all outgoing mail through the mail server that is responsible for your domains.
While most tools also allow you to configure an external SMTP server, it is on one hand tedious to configure it everywhere and on the other hand also insecure if you have to write the username and password for authentication to many user-readable configuration files on your system. Therefore I am running a local MTA on all the computers I administrate to relay mails to a central mail server.
macOS already includes Postfix in the base installation. Luckily, this is also the mail daemon I am most experienced with, although other options such as msmtp or nullmailer exist. However, due to the System Integrity Protection (SIP) on recent macOS versions, there is no way to disable the builtin Postfix. Therefore the best option is to embrace that as a feature and use it.
On macOS, services can automatically be started by triggers configured in the launchd init system. This is also the case for Postfix, which is way there is no process running unless a mail needs to be send. As soon a mail is written to the mail spool directory by sendmail, the Postfix master will be started to pickup and process the mail as usual. This is already configured out of the box on macOS and no special configuration is needed.
Generic relayhost configuration
The following describes the necessary configuration to relay mail over another host with authentication. Note this will also work on other non-macOS machines that have a regular Postfix daemon that runs all the time.
The following additions to
/etc/postfix/main.cf are required. First, tell Postfix with which name it should identify itself to other mail servers and which domain name should be appended when sending mail from local users. If your local username is
foo, the mail will appear to be from
firstname.lastname@example.org. The relay host is the machine that will accept mails after authentication and send them to the and For the authentication to the SMTP server, SASL has to be enabled with an additional configuration file which is the password storage. The last option ensures that Postfix will only use TLS encrypted connections. Note that this setup assumes you are using port 587 with
STARTTLS. Refer to the Postfix manual if you need to use SMTPS over port 465.
# /etc/postfix/main.cf # Relay mails over mail.example.org myhostname = foo.example.org myorigin = example.org relayhost = [mail.example.org]:587 smtp_sasl_auth_enable = yes smtp_sasl_password_maps = hash:/etc/postfix/saslpasswd smtp_tls_security_level = encrypt
The password storage should look like the following example. The password is stored as plaintext, so make sure you set the permissions of the file in a way that only root can access it. It might be a good idea to create a separate user per host on your relay host for this, in order to be able to change or revoke passwords without having to reconfigure all machines.
# /etc/postfix/saslpasswd [mail.example.org]:587 user:password
Every time you make changes to this file, you have to remember to execute the
postmap command to update the
.db file right next to it.
$ sudo postmap /etc/postfix/saslpasswd
And that’s it already. Now you are able to send mails via the local sendmail interface. Use the following command with your own email address to test the setup.
$ echo "Foo Bar Baz" | mail -s Test email@example.com
Broken Postfix on macOS 10.12.6 Sierra
This setup worked fine for years until macOS 10.12.6 Sierra or the following security update arrived, with which Apple broke the TLS client of Postfix. Apparently this was fixed on macOS 10.13 High Sierra, so the following section will not apply to you if you have already upgraded.
As there are no traditional log files on macOS anymore, it took me quite a while to figure out how to debug it as the various Postfix processes all have different names. Use the following command to see everything related to Postfix:
$ log stream --predicate '(process == "master" OR process == "qmgr" OR process == "pickup" OR process == "cleanup" OR process == "smtp")' --level debug Filtering the log data using "process == "master" OR process == "qmgr" OR process == "pickup" OR process == "cleanup" OR process == "smtp"" Timestamp Thread Type Activity PID ... 2018-03-21 23:41:28.629231+0100 0x158e938 Default 0x0 82375 smtp: warning: Digest algorithm "md5" not found 2018-03-21 23:41:28.629310+0100 0x158e938 Default 0x0 82375 smtp: warning: disabling TLS support ...
It looks like that
EVP_get_digestbyname(const char *name) from libcrypto will always return
NULL and cannot find any digests. I guess Apple patched something and it is now missing a call to
OpenSSL_add_all_algorithms(); or similar. One of those classic mistakes when writing your first program against the OpenSSL API.
Using stunnel as an on demand TLS wrapper for Postfix
As the TLS client in Postfix is not usable, I had to find a new solution as I really do not want to send mails over an unencrypted connection. Therefore I chose stunnel as a TLS wrapper acting as a client and relaying connections from a local listening port to a remote host.
$ sudo port install stunnel certsync # or curl-ca-bundle
Once again, launchd can be used with a new LaunchDaemon in inetd mode to only start the process when it is needed. Choose a free port to run this on, in this example I am using port 555. I recommend to use a port <1024 to ensure it cannot be bound by any other user except root on the same machine. This is using the stunnel3 launch script, as stunnel 5.x only works with configuration files, but I really do not want to keep a file lying around.
<!-- /Library/LaunchDaemons/org.example.mail.plist --> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>org.example.mail</string> <key>ProgramArguments</key> <array> <string>/opt/local/bin/stunnel3</string> <string>-c</string> <string>-r</string> <string>mail.example.org:587</string> <string>-n</string> <string>smtp</string> <string>-v</string> <string>2</string> <string>-A</string> <string>/opt/local/etc/openssl/cert.pem</string> </array> <key>UserName</key> <string>nobody</string> <key>Sockets</key> <dict> <key>Listeners</key> <dict> <key>SockNodeName</key> <string>localhost</string> <key>SockServiceName</key> <string>555</string> <key>SockType</key> <string>stream</string> </dict> </dict> <key>inetdCompatibility</key> <dict> <key>Wait</key> <false/> </dict> </dict> </plist>
After creating the file, load the new LaunchDaemon and mark it to be loaded automatically each time the system boots. In case you need it, you can permanently disable it again with
unload -w. Afterwards test the connection with netcat and see how stunnel connects to the remote host and handles the STARTTLS sequence to transparently encrypt this connection with TLS.
$ sudo launchctl load -w /Library/LaunchDaemons/org.example.mail.plist $ nc localhost 555 220 mail.example.org ESMTP Postfix
Now all that is left is to edit the Postfix configuration in
/etc/postfix/main.cf to connect to
localhost:555 instead of the original mail host. As TLS is broken, the security level needs to be reduced to allow unencrypted connections. You will also have to adapt the SASL password storage accordingly and remember to run
# /etc/postfix/main.cf ... relayhost = [localhost]:555 smtp_tls_security_level = may ...
Now test it again by sending yourself an email with the