↓ Skip to main content

SPF, DKIM and DMARC for PHP apps in 2026: PHPMailer and Symfony Mailer

·5 mins
Hennadii Alforov
Author
Hennadii Alforov
Senior Backend Engineer with 15+ years in IT. I build and scale backend systems for high-traffic platforms - billing, payments, hosting and domain services.

If your PHP app sends email from your own domain, you need three DNS records. SPF lists the servers that may send mail for the domain. DKIM publishes the public key that receivers use to check the signature on each message. DMARC says what receivers should do when a message fails, and it checks that the domain in From is really yours. On top of that the app, or whatever sends mail for it, has to sign every message with the DKIM private key.

I wrote the first version of this post in Russian in 2015. PHPMailer didn't build a correct signature for me back then, so most of that post was a patched copy of its preSend(), an rsa-sha1 signature and a 1024-bit key generated on dkimcore.org. You don't need any of that today.

Why it isn't optional anymore
#

Since February 2024 Gmail and Yahoo check this for everyone. Every sender needs SPF or DKIM. Senders of more than 5,000 messages a day to Gmail need SPF, DKIM and DMARC, where p=none is enough, and the From domain has to match the SPF or the DKIM domain. Marketing mail needs one-click unsubscribe, and the spam rate has to stay below 0.3%.

Microsoft joined on May 5, 2025 for senders of more than 5,000 messages a day to Outlook.com, Hotmail and Live. Mail that doesn't pass isn't moved to junk, it's rejected with 550 5.7.15 Access denied.

An app that sends a few password resets a day sounds like someone else's problem. It isn't. Without SPF or DKIM you're outside Gmail's rules for all senders, small ones too.

Step 1: SPF
#

example.com.  TXT  "v=spf1 ip4:203.0.113.10 include:_spf.your-mail-provider.example -all"

List your own server's IP and add an include: for every service that sends as your domain. A domain gets one SPF record, so merge them instead of adding a second one. SPF also allows at most 10 DNS lookups, and every include: counts. ~all marks everything else as suspicious, -all as a fail. Start with ~all while you check, then switch.

One thing catches PHP apps. SPF checks the envelope sender, the address that ends up in Return-Path, not the From header. With mail() on shared hosting that's often the server's own address, so SPF passes for the host's domain and not for yours. In PHPMailer set it yourself:

$mail->Sender = 'bounces@example.com';

Step 2: DKIM keys and the DNS record
#

Generate a 2048-bit key pair:

openssl genrsa -out dkim_private.pem 2048
openssl rsa -in dkim_private.pem -pubout -outform der | openssl base64 -A

The second command prints the public key for DNS. The record goes under <selector>._domainkey:

s2026._domainkey.example.com.  TXT  "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEF..."

The selector is any name you like. I'd put the year in it: when it's time to rotate the key, you publish a new selector next to the old one and switch the app over.

With a 2048-bit key the record is about 410 characters long, and a single string in a DNS TXT record can't be longer than 255. So the value has to be split into several quoted strings. Most DNS panels do that for you, some don't.

Keep dkim_private.pem out of the web root and out of git.

Step 3: sign messages in PHPMailer 7
#

PHPMailer signs by itself now, with rsa-sha256:

use PHPMailer\PHPMailer\PHPMailer;

$mail = new PHPMailer(true);
$mail->setFrom('orders@example.com', 'Example Shop');
$mail->addAddress('customer@example.org');
$mail->Subject = 'Your order #1042';
$mail->Body = "Thanks for your order.\n";

$mail->DKIM_domain = 'example.com'; // the domain of the From address
$mail->DKIM_selector = 's2026';
$mail->DKIM_private = '/etc/myapp/dkim_private.pem';
$mail->DKIM_identity = $mail->From;
$mail->DKIM_copyHeaderFields = false;

$mail->send();

DKIM_copyHeaderFields is true by default and copies the signed headers into the signature for debugging. You don't need that in production.

Step 3 in Symfony Mailer 8
#

Either sign one message with DkimSigner:

use Symfony\Component\Mime\Crypto\DkimSigner;
use Symfony\Component\Mime\Email;

$email = (new Email())
    ->from('orders@example.com')
    ->to('customer@example.org')
    ->subject('Your order #1042')
    ->text("Thanks for your order.\n");

$signer = new DkimSigner('file:///etc/myapp/dkim_private.pem', 'example.com', 's2026');
$mailer->send($signer->sign($email));

Or sign everything the app sends, once, in the config:

# config/packages/mailer.yaml
framework:
    mailer:
        dkim_signer:
            key: 'file://%kernel.project_dir%/var/dkim/private.pem'
            domain: 'example.com'
            select: 's2026'

Yes, the key is select, not selector.

Before publishing I ran the PHPMailer and the DkimSigner examples through an independent DKIM verifier. Both signatures pass, and both fail as soon as the subject is changed. That second part is the whole point of DKIM.

Step 4: DMARC
#

_dmarc.example.com.  TXT  "v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com"

p=none changes nothing for delivery. It only asks receivers to send daily reports to the rua address, so you see who sends mail as your domain and whether it passes. Once the reports show only your own servers, move to p=quarantine and later to p=reject.

DMARC passes when SPF or DKIM passes and its domain matches the domain in From. That's why the envelope sender from step 1 and DKIM_domain from step 3 matter.

If an email API sends for you
#

If the app sends through an email API instead of its own server, you don't sign anything in PHP. The provider signs with its key, and you add the DNS records it gives you, usually DKIM and an SPF include. I built the official Mailtrap PHP SDK, so that's the one I know best, but every provider works this way. DMARC on your domain is still your job. And if your own product asks customers to add records like these, Domain Connect can set them up for the customer, without anyone typing a TXT record by hand.

How to check it
#

Look at the records first:

dig +short TXT example.com
dig +short TXT s2026._domainkey.example.com
dig +short TXT _dmarc.example.com

Then send a message to a Gmail address and open "Show original". SPF, DKIM and DMARC should all say PASS. Every other provider writes the same results into the Authentication-Results header of the received message.