How to make Postfix and SpamAssassin play nicely when using SPF

I happened to be looking at the raw headers of an email today and noticed something weird. The message had been sent to me from a local user on my server, so it had never actually left the server; however, SpamAssassin had given the message a couple of spam points because it said the SPF verification had failed. I double-checked the relevant SPF records and there was nothing wrong there, so I began to suspect there might be a bug in SpamAssassin's SPF verification routine.

I tried sending an email from my server to a Gmail address (this is a good way to test SPF records, since Gmail adds headers to messages indicating whether the SPF verification succeeded). Sure enough, Gmail's SPF verification succeeded. I also sent messages to a few standard SPF test services, and they all indicated success as well. Furthermore, mail sent from Gmail to my server didn't result in an SPF failure, so SpamAssassin was verifying Gmail's SPF record correctly, but not mine.

Finally, after staring at the email headers for a few more minutes, I realized what was happening. SpamAssassin wasn't buggy; it just didn't have all the information it needed.

When a local user on my server sends mail to another local user, the mail server only adds a single "Received" header to the message, and the "from" IP in this header is the user's IP (in this case, a dynamic Comcast IP). If the email had originated from an outside mail server like Gmail, it would have had at least two "Received" headers—one for Gmail and one for my server—and SpamAssassin would verify the SPF record by checking that the handoff from Gmail to my server came from a valid IP. But since this message appeared to be originating from a dynamic IP, SpamAssassin had no way of knowing that the message actually originated on the local server, because it had no way of knowing that the user had been authenticated via Postfix SMTP auth.

As it turns out, all I had to do was tell Postfix to add a header indicating that the sender was authenticated, and then SpamAssassin was happy. Putting the following line in Postfix's main.cf file did the trick:

smtpd_sasl_authenticated_header = yes