Your contact form notification and your WooCommerce order email are both sent by wp_mail(). If they land in spam, the mail content is usually not the first thing to fix. The first thing is authentication: the receiving mail server needs to know that the message really came from your domain.
The order below is about avoiding rework:
- Decide the sending domain and the SMTP relay.
- Route WordPress mail through that relay.
- Publish SPF for the relay.
- Publish DKIM for the domain in the From header.
- Add DMARC last, starting in monitoring mode.
- Test the real WordPress paths: form notification and order email.
SPF and DKIM can be done in either order once the relay is known. DMARC should come last because it reads the SPF and DKIM results.
1. Decide the sender identity first
You need three addresses before you touch DNS:
| Role | Example | Rule |
|---|---|---|
| Visible From | orders@example.com |
A domain you control. Not a free-mail address. |
| Return-Path / envelope sender | bounce@example.com |
The address that receives bounces. It can be on the same domain or a subdomain. |
| Reply-To | support@example.com |
A mailbox someone reads. For a contact form, this is often the visitor's address. |
Do not set the form notification From address to the visitor's email. That is spoofing your visitor's domain, and it will fail SPF or DMARC alignment on your domain. Use your domain in From and put the visitor in Reply-To.
The same applies to WooCommerce. The store sends the order email. Use a store address in From, and let replies go to the store.
At the DNS level, you need access to the zone for the domain in the From header. If you use a subdomain such as mail.example.com for the relay's envelope domain, keep the visible From on example.com only if your DMARC alignment mode can handle it. If you are not sure, keep the visible From, the Return-Path, and the DKIM d= on the same domain.
2. Configure SMTP in WordPress before adding SPF, DKIM, or DMARC
The SMTP relay determines which hosts and which DKIM selector you need to publish. It also gives you a log of what WordPress actually sent.
Use one SMTP path. If you use a plugin, use the plugin. If you use code, use code. Do not run two SMTP configurations at the same time.
The following must-use plugin routes wp_mail() through authenticated SMTP. Put it in wp-content/mu-plugins/smtp-mail.php.
<?php
/**
* Plugin Name: SMTP Mail Bootstrap
* Description: Send wp_mail() through an authenticated SMTP relay.
*/
defined('ABSPATH') || exit;
add_action('phpmailer_init', function ($phpmailer) {
if (!defined('SMTP_HOST') || !defined('SMTP_PORT') || !defined('SMTP_USER') || !defined('SMTP_PASS')) {
return;
}
$phpmailer->isSMTP();
$phpmailer->Host = SMTP_HOST;
$phpmailer->Port = (int) SMTP_PORT;
$phpmailer->SMTPAuth = true;
$phpmailer->Username = SMTP_USER;
$phpmailer->Password = SMTP_PASS;
$phpmailer->SMTPSecure = defined('SMTP_SECURE') ? SMTP_SECURE : 'tls';
$phpmailer->Timeout = 15;
if (defined('SMTP_FROM')) {
$phpmailer->From = SMTP_FROM;
}
if (defined('SMTP_FROM_NAME')) {
$phpmailer->FromName = SMTP_FROM_NAME;
}
if (defined('SMTP_BOUNCE')) {
$phpmailer->Sender = SMTP_BOUNCE;
}
});
add_action('wp_mail_failed', function ($error) {
error_log('wp_mail failed: ' . $error->get_error_message());
});
Add the constants to wp-config.php. Keep the password out of version control. On a managed host, set it as an environment variable.
define('SMTP_HOST', 'smtp.example.net');
define('SMTP_PORT', 587);
define('SMTP_USER', 'postmaster@example.com');
define('SMTP_PASS', getenv('SMTP_PASS') ?: '');
define('SMTP_SECURE', 'tls');
define('SMTP_FROM', 'orders@example.com');
define('SMTP_FROM_NAME', 'Example Store');
define('SMTP_BOUNCE', 'bounce@example.com');
Port 587 with tls is a common STARTTLS setup. Port 465 usually uses ssl. Use what your relay documents.
If WooCommerce overrides the From address, set the same address in WooCommerce > Settings > Emails, or use a filter. Do not set a different From in three places.
add_filter('woocommerce_email_from_address', function ($address) {
return defined('SMTP_FROM') ? SMTP_FROM : $address;
});
Test from the server with WP-CLI:
wp eval 'var_dump(wp_mail("inbox@example.com", "SMTP test " . time(), "Test body"));'
A true return means WordPress handed the message to the relay. It does not prove inbox placement. Check the relay log and the received message headers.
3. Add SPF for every host that can send as your envelope domain
SPF is a TXT record at the domain root. It lists the hosts that are allowed to send mail using that domain in the envelope. The receiver checks the Return-Path domain, not the visible From domain.
If you have no SPF record, a relay-specific record looks like this:
example.com. 300 IN TXT "v=spf1 include:_spf.example.net ~all"
If you already have an SPF record, merge the new include into it. Do not publish two v=spf1 records.
example.com. 300 IN TXT "v=spf1 include:_spf.example.net include:_spf.other.example ~all"
Check what is published:
dig +short TXT example.com | grep -i 'v=spf1'
Keep the DNS lookup count under 10. include, a, mx, ptr, exists, and redirect count toward the limit. If you are close to the limit, remove old senders you no longer use.
Use ~all while you are still discovering senders. Move to -all only after you are sure every legitimate sender is listed. A hard fail on a forgotten sender will block that mail.
SPF only authenticates the envelope domain. If your relay uses its own bounce domain in Return-Path, SPF can pass there and still not align with your visible From domain for DMARC. That is why DKIM is not optional.
4. Add DKIM with a selector on the From domain
DKIM signs the message. The receiving server checks a public key published at selector._domainkey.yourdomain.
Your relay generates the selector and the key pair. Publish the public part as the DNS record the relay gives you. It is often a CNAME:
provider-selector._domainkey.example.com. 300 IN CNAME provider-selector.dkim.example.net.
It can also be a TXT record:
provider-selector._domainkey.example.com. 300 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBg..."
Verify the record:
dig +short CNAME provider-selector._domainkey.example.com
dig +short TXT provider-selector._domainkey.example.com
Then check a real message header:
DKIM-Signature: v=1; a=rsa-sha256; d=example.com; s=provider-selector; ...
The d= value must match the domain in the From header, or be in the same organizational domain under relaxed DMARC alignment. If the relay signs with its own domain, ask for custom domain authentication. A valid DKIM signature from the relay's domain does not align with your From domain.
Do not paste a private DKIM key into WordPress, a ticket, or a repository. Only the public key goes into DNS. Rotate the key through the relay when needed.
5. Add DMARC last, and start with monitoring
DMARC tells the receiver what to do when a message fails both SPF and DKIM alignment. It also gives you reports.
Start with p=none so you can see what is happening without blocking mail:
_dmarc.example.com. 300 IN TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com; adkim=r; aspf=r; pct=100"
rua is where aggregate reports go. Use a mailbox you monitor. adkim=r and aspf=r mean relaxed alignment, which allows subdomains in the same organizational domain. If you need strict alignment, use s, but then keep the domains identical.
After the reports show that your legitimate sources pass and you have no unexpected failing source, move to quarantine:
_dmarc.example.com. 300 IN TXT "v=DMARC1; p=quarantine; pct=25; rua=mailto:dmarc@example.com; adkim=r; aspf=r"
Then increase pct, and eventually move to p=reject. Do not jump to p=reject on the first day. DMARC is an enforcement layer; it should describe a mail flow you already understand.
Check the record:
dig +short TXT _dmarc.example.com
6. Test the two paths that matter
A successful wp_mail() call is not enough. Test the real WordPress paths:
- Submit the contact form and confirm the notification arrives at the inbox you configured.
- Place a test order and confirm the customer and admin order emails arrive.
- Reply to the form notification and confirm it goes to the visitor, not to your own mailbox.
- Open the full headers and look for
spf=pass,dkim=pass, anddmarc=passin theAuthentication-Resultsheader. - Check the relay log for bounces, rejects, or rate limits.
If mail still lands in spam, authentication may be correct while content or reputation is the issue. The DNS records cannot guarantee inbox placement. They remove one class of failure: unauthenticated or misaligned mail.
Three parties, often not the same people
Before you start, find out who actually controls each piece. Most stuck setups are stuck here, not in the code.
| Piece | Who controls it |
|---|---|
| WordPress mail routing | Whoever has admin or file access to the site |
| SMTP relay account | Whoever owns the sending account and can read its logs |
| DNS zone for the From domain | Whoever runs DNS — sometimes the host, sometimes a separate DNS provider, sometimes a marketing team |
If you are commissioning the site rather than building it, add three lines to the handover checklist: SPF record published, DKIM selector published and signing with the From domain, DMARC record present in monitoring mode. Then ask for the Authentication-Results header from one real order email, not a screenshot of a test message.
AumCreate's Setup service is one option if you want a WordPress site deployed on your own host: https://aumcreate.com/setup?utm_source=devto&utm_medium=content&utm_campaign=devto-wp-transactional-email-deliverability-order — note that the relay is a third-party account you own and the DNS records still have to be published by whoever controls the zone, so keep that access in your own hands either way.
Common WordPress mistakes
| Symptom | Likely cause | Fix |
|---|---|---|
| Contact form notifications fail, but a test email works | The form uses the visitor's email as From | Use your domain in From; put the visitor in Reply-To |
| WooCommerce order emails fail | WooCommerce still uses a personal admin address | Set the WooCommerce From address to the same domain identity |
| SPF passes, DMARC fails | Return-Path is the relay's domain and DKIM is signed by the relay's domain | Enable custom DKIM on your domain, or use a custom Return-Path |
| Mail is intermittent | Two SMTP plugins or a plugin plus code are both configuring PHPMailer | Keep one SMTP path |
| Everything passes but mail still goes to spam | Content, reputation, or recipient engagement | Work on the message and sending history; DNS alone is not a guarantee |
If a form plugin sends through its own API instead of wp_mail(), the SMTP settings in this tutorial do not apply to that form. Check the plugin's mail settings separately.
The short version: configure SMTP first so you know the relay, publish SPF for that relay, publish DKIM on the From domain, and add DMARC last in monitoring mode. Then test the form and the order email, not just a generic test message.
Top comments (0)