configure the os x postfix mta  to successfully forward email on to your domain's smtp servers

By Steven Stromer, Apr 18, 2024

For programmers who prefer to develop on their local machines before deploying, it remains a challenge to keep the localhost environment functioning like a fully-managed hosting environment. I argue that maintaining the server services on our local machines helps to keep us aware of the latest features, capabilities, security issues, and bugs in the services that we are building our applications upon.

One of those core services is the MTA, or Mail Transfer Agent, an application responsible for transferring emails from one computer to another. MTAs handle the routing, relaying, and delivery of email messages between mail servers.

postfix, the mta included with os x

Two of the longest employed MTAs are sendmail and postfix. Mac OS X switched from using sendmail to postfix as its default MTA with the release of Mac OS X 10.4 Tiger in April 2005. Prior to Tiger, sendmail was the default MTA used in OS X. The decision to switch to postfix was likely influenced by various factors, including postfix's reputation for being more secure, easier to configure, and having a cleaner codebase compared to sendmail. Postfix also offered better performance and scalability, making it a preferred choice for many Unix-like operating systems, including OS X.

Unfortunately, Apple has not done a great job of keeping OS X users informed of how to configure postfix for even the most basic use cases. Further, postfix's maintainers do not provide OS X-specific documentation, leaving OS X users to muddle through Linux-focused documentation to configure the postfix instance provided with OS X.

Out of the box, postfix is configured to deliver mail created with the mail command, or from common language mailer functions, like PHP's mail(), directly to the localhost mailbox. In this article, we configure postfix to forward email generated from these sources to our preferred email hosting providers, via SMTP, over secure connections using TLS, or the older SSL, when TLS is unavailable.

It is necessary to note that use of PHP's mail() and similar functions is generally discouraged, due to the fact that they send mail directly from the web server, instead of from an SMTP mail server, and due to the increased exposure to header injection attacks. Tools, like PHPMailer, make this transition easy. However, for shell scripts, small applications, locations where installing classes like PHPMailer are not allowed, and for a multitude of other use cases, postfix configuration is the way to go.

conventions

brackets [ ]
In all of the following configuration snipets, brackets [ ] are intended to indicate that the enclosed content must be changed to match your own appropriate values. The brackets themselves are not to remain, especially in the relayhost directive, covered below. This specific directive has a valid format that does optionally use brackets. However, they are not to be used in our use case.

editor choice
Wherever vi is indicated in a command, feel free to use whichever text or code editor you prefer. Despite it's long history, my favorite editor is bbedit, which can be used in its extremely capable basic mode for free. It includes a set of command line tools that make it easy to invoke the editor directly from the command line.

step 1. enable sasl in the postfix configuration file

From the Terminal, open postfix's main configuration file, main.cf:

vi /private/etc/postfix/main.cf

Add the following lines to the end of the file:

# SPECIFY DOMAIN TO APPEND TO USERNAME
myorigin = [domain.tld]

# SPECIFY EXTERNAL SMTP HOST TO FORWARD EMAIL
relayhost = [smtp_host.domain.tld]:587

# CONFIGURE SASL
smtpd_sasl_path = smtpd										# this is the default value, so not actually needed
smtp_sasl_auth_enable = yes									# enable SASL authentication
smtp_sasl_security_options = noanonymous					# disallow anonymous authentication
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd		# path to sasl_passwd

# ENABLE STARTTLS ENCRYPTION FOR PORT 465
# smtp_use_tls = yes
# smtp_tls_wrappermode = yes
# smtp_tls_security_level = encrypt
# smtp_tls_CAfile = /etc/ssl/certs/ca_certificate.crt
# header_size_limit = 4096000

NOTE: These directives are primarily sourced from the postfix configuration page, located at: https://www.postfix.org/SOHO_README.html#fantasy

The myorigin directive should be set to the domain portion of the 'FROM' email address you intend to send email from. In most instances, remote SMTP servers will require that the FROM address be one that they are familiar with, i.e., one that they already manage.

(Additionally, note that the value of myorigin will be appended to the username of the current user in the terminal window from which the mail command that will later be used to send a test email. This can be an issue when the username and email (frequently) are not the same. We will address this later on.)

The relayhost = [smtp_host.domain.tld]:587 directive and value specify the mail host that postfix sould relay outgoing mail to. This should be set to your mail host's SMTP server, something similar to smtp.yourhost.com.

Appended to this is the port identifier :587. Port 587 is used exclusively for TLS-encrypted mail. Prior to TLS becoming the modern standard for mail encryption, SSL (Secure Socket Layer) was the standard, using port 465. Unlike port 465, port 587 routes to a backward-compatible encryption methodology, known as STARTTLS. With STARTTLS, computers negotiate the securest mutually compatible encryption standard over which they can successfully communicate.

so, what then are all of the SASL directives for?

SASL (Simple Authentication and Security Layer) is a framework used by network protocols such as SMTP to provide authentication and security services during communication between clients and servers. When a client connects to a server, they negotiate which authentication mechanisms they mutually support. The server typically advertises a list of supported mechanisms, and the client selects one from the list.

After selecting a SASL mechanism, the client and server perform the authentication process according to the chosen mechanism. This process may involve exchanging authentication credentials, such as usernames and passwords, as is true in our use case. For clarity, SASL determines the means of authentication, while STARTTLS negotiates whether the connection will be encrypted using SSL or TLS.

With our SASL directives, we tell postfix to use SASL to negotiate authentication, establish that, at minimum, the negotiated security mechanism must not allow anonymous authentication, and inform SASL where we've stored our encrypted username and password. More on this, next.

step 2. securely store authentication credentials

While our SMTP server authentication credentials (usernames and passwords) could simply be stored as readable plaintext in main.cf, this would be extremely poor practice. Instead, postfix provides for secure storage of credentials in an encrypted file, called sasl_passwd.

In the Terminal, enter the following commands, one at a time:

cd /etc/postfix					# move into the postfix directory
sudo touch sasl_passwd			# create the sasl_passwd file
vi sasl_passwd					# edit the file

Add the following line to the empty sasl_passwd file, swapping in your own SMTP hostname, email address and password. Note that the format of host and port must precisely match the previously configured value entered for relayhost in main.cf.

[smtp_host.domain.tld]:port [email@domain.tld]:[password]

Save and close the sasl_passwd file. Then, in the Terminal, enter the following commands, one at a time:

sudo postmap /etc/postfix/sasl_passwd			# copy into an enctrypted DB
sudo chmod 0600 /etc/postfix/sasl_passwd		# make the file only readable by its owner
sudo chmod 0600 /etc/postfix/sasl_passwd.db		# make the DB file only readable by its owner

step 3. testing our postfix configuration

It is now time to check whether postfix is running, and to fire it up, if necessary. Enter the following commands, one at a time:

sudo postfix status					# check whether postfix is running
sudo postfix reload					# if it is running, reload the config file
sudo postfix start					# otherwise, if it isn't, start it up

Next, make absolutely certain that the email account that will be receiving email has the sending email address whitelisted. Auto-generated emails tend to frequently be picked up by spam filters.

As we will next attempt to send a test email, we should watch our log files as we do so. Oddly, there is no dedicated log file for the postfix implementation on OS X. The quickest way to track postfix activity is:

log stream --predicate  '(process == "smtpd") || (process == "smtp")' --info

In a second terminal window, enter the following, swapping in your preferred destination email address. When you press enter, your email should be sent.

echo  "email body content here" | mail -s "This is a test email" [email@domain.tld]

The log file should indicate that your email was sent successfully, similar to the following:

2024-04-17 16:27:31.471064-0400 0x20fa882  Activity	0x3805dd0			65841  0	smtp: (libsystem_info.dylib) Retrieve User by Name
2024-04-17 16:27:31.472143-0400 0x20fa882  Activity	0x3805dd1			65841  0	smtp: (libsystem_info.dylib) Retrieve User by Name
2024-04-17 16:27:31.472745-0400 0x20fa882  Activity	0x3805dd2			65841  0	smtp: (libsystem_info.dylib) Retrieve Group by Name
2024-04-17 16:27:31.508750-0400 0x20fa882  Activity	0x3805dd3			65841  0	smtp: (libsystem_info.dylib) Resolve user group list
2024-04-17 16:27:47.764025-0400 0x20fa882  Info		0x0				  65841  0	smtp: 6BADD52518BC: to=, 
relay=smtp.dreamhost.com[64.90.62.162]:587, delay=16, delays=0.03/0.05/16/0.16, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as 4VKXXQ11rsz60)

A 'sent' message in your log does not guarantee that your email will successfully reach its destination. However, it does mean that you have successfully configured your postfix server to securely connect with your remote SMTP host and to forward email to that host.

From here, the SMTP server may or may not forward the email, based on criteria including whether it accepts the FROM address, whether it can resolve the TO address, whether there are other header details in the email that cause it to be flagged as potential spam, and so forth. If your SMTP host provides access to mail logs, you may be able to confirm, or otherwise debug, your emails progress on this next hop of its journey.

On the other hand, your email may just be waiting for you in your destination inbox.

step 4. optionally forcing postfix to run continuously

It is possible to force postfix to run continuously by setting the app to launch at login by changing the default content of com.apple.postfix.master.plist. However, postfix launches on it's own, as needed to deliver email, making this an unnecessary step. Further complicating things, Apple's System Integrity Protection (SIP) makes this an involved process, as it's not possible to edit protected .plist files without first disabling this security mechanism.

If you have a valid use case, here's how to proceed...

In the Terminal, enter the following command:

csrutil disable								# disable SIP

Next, reboot your saystem into Recovery Mode. Once rebooted, enter the following commands into Terminal, one at a time:

cd /System/Library/LaunchDaemons			# move into the Prefs folder
sudo vi com.apple.postfix.master.plist		# open the postfix preference file

Replace the entire <dict> section of the .plist file with the following. We add the keys 'KeepAlive' and 'RunAtLoad' and remove the default timer 'master -e 60', which together will keep postfix running continuously.

<dict>
	<key>KeepAlive</key>
	<true/>
	<key>Label</key>
	<string>com.apple.postfix.master</string>
	<key>Program</key>
	<string>/usr/libexec/postfix/master</string>
	<key>ProgramArguments</key>
	<key>QueueDirectories</key>
	<array>
		<string>/var/spool/postfix/maildrop</string>
	</array>
	<key>AbandonProcessGroup</key>
	<true/>
	<key>RunAtLoad</key>
	<true/>
</dict>

Save the file, and reboot a second time, to exit recovery mode. If you're the obsessive security type, you can use 'csrutil status' after rebooting to confirm SIP has been re-enabled.

WARNING It is highly recommended that you make copies of all files that are edited in this article, prior to editing. This is your choice, and therefore your responsibility!

DISCLAIMER This is to specifically emphasize that neither the author, nor any organization associated with the author, can or will be held responsible for data-loss or other issues caused by possible errors in this article. Use at your own risk.

Additional research sources:

Mac OS X 10.10 Yosemite Postfix SASL Authentication Failed
https://stackoverflow.com/questions/26447316/mac-os-x-10-10-yosemite-postfix-sasl-authentication-failed

Configure postfix as relay for macOS Sierra – Sonoma
https://gist.github.com/loziju/66d3f024e102704ff5222e54a4bfd50e

Postfix Recipient address rejected: Access denied Error
https://serverfault.com/questions/659135/postfix-recipient-address-rejected-access-denied-error