Written by 9:47 pm Blog Views: 9

WordPress Email Deliverability: SMTP Setup Without Plugins

Set up reliable WordPress email delivery using SMTP via wp-config.php constants and an mu-plugin. Configure Postmark, Amazon SES, or Mailgun with SPF, DKIM, and DMARC for inbox delivery.

WordPress email deliverability SMTP configuration

WordPress ships with wp_mail(), a wrapper around PHPMailer that routes every notification, password reset, WooCommerce order email, and contact-form message your site produces. On most shared hosts, that function hands off to PHP’s built-in mail(), which sends from the server’s local MTA – and lands straight in spam, or gets silently dropped. This guide covers how wp_mail() works under the hood, how to route it through a real SMTP service using only wp-config.php constants (no bloat plugin required), how to configure Postmark, Amazon SES, and Mailgun, and how to debug every failure mode you are likely to encounter in production.


How wp_mail() Actually Works

Every outgoing email in WordPress goes through a single function: wp_mail( $to, $subject, $message, $headers, $attachments ). Before it fires, WordPress applies the wp_mail filter – which lets any plugin or mu-plugin intercept and modify the arguments. Then it initialises a PHPMailer instance and fires the phpmailer_init action, passing the mailer object by reference. Any code hooked into phpmailer_init can switch the transport to SMTP, set credentials, and change the From address.

After sending, WordPress fires either wp_mail_succeeded (on success) or wp_mail_failed (with a WP_Error object). These two hooks are the correct place to build email logging – not a filter on wp_mail itself, which runs before the attempt.

The critical insight: wp_mail() returns true or false, but that only reflects whether PHPMailer accepted the message. It does not guarantee delivery. A message can return true from SMTP submission and still bounce or get filtered at the receiving mail server. That is why SMTP logging at the provider level matters more than checking the return value.

The Default Transport Problem

When no SMTP configuration is applied, PHPMailer falls back to the system’s sendmail binary or PHP’s mail() function. On most shared hosting environments this means:

  • No authentication – the receiving server cannot verify you are who you say you are
  • The envelope sender is often [email protected] rather than your domain
  • No DKIM signing – messages fail DMARC alignment checks
  • No delivery visibility – you have no dashboard to see what was accepted or rejected

On VPS and dedicated servers the situation is slightly better, but you still need SPF, DKIM, and PTR records set up correctly before the server’s own MTA will be trusted by Gmail, Outlook, and Yahoo. Routing through a dedicated SMTP relay bypasses all of that complexity in exchange for a few constants in wp-config.php.


Step 1 – Enable wp_mail Debugging Before You Touch SMTP

Before changing anything, get a baseline. Drop this mu-plugin in wp-content/mu-plugins/wp-mail-debug.php. It hooks into phpmailer_init to enable PHPMailer’s built-in SMTP transcript logging, and into wp_mail, wp_mail_succeeded, and wp_mail_failed to write every attempt and result to a log file.

Once active, trigger a test email (use the WP-CLI commands in a later section, or just do a password reset). Then check wp-content/wp-mail-debug.log. The PHPMailer SMTP transcript shows you the full SMTP conversation – EHLO, AUTH LOGIN, MAIL FROM, RCPT TO, DATA, and the server’s response code. A 250 OK at the end means accepted. A 550 or 535 points directly at authentication or policy rejection.

Remove or comment out this mu-plugin once debugging is done. Leaving SMTPDebug = 2 active in production writes sensitive SMTP credentials to your log file.


Step 2 – Configure SMTP via wp-config.php (No Plugin Required)

The plugin-free approach stores SMTP credentials as PHP constants and hooks into phpmailer_init from an mu-plugin. This keeps credentials out of the database (where they are visible to anyone with database access), avoids plugin update cycles, and loads on every request with zero overhead.

Add Constants to wp-config.php

Add these constants above the /* That's all, stop editing! */ line in wp-config.php. The example below shows Mailgun values – swap in your provider’s details:

Drop the mu-plugin that Reads Those Constants

Create wp-content/mu-plugins/smtp-init.php. This file reads the constants and passes them to PHPMailer via phpmailer_init:

With these two files in place, every call to wp_mail() – including WooCommerce orders, BuddyPress notifications, and user registrations – routes through your configured SMTP server. No plugin, no admin UI, no database query to fetch credentials.

PortProtocolNotes
25SMTP (plain)Blocked on most hosting providers to prevent spam
465SMTPS (SSL)Deprecated standard but still widely supported
587Submission (TLS/STARTTLS)Recommended – negotiates TLS after connection
2525Alternate submissionUsed by some providers when 587 is blocked

Provider Setup: Postmark

Postmark is optimised for transactional email. Its shared IP pools maintain some of the highest deliverability rates in the industry because Postmark enforces strict acceptable-use policies – no marketing blasts allowed on transactional streams. It is the right choice when you need password resets and order confirmations to arrive reliably.

Getting Postmark SMTP Credentials

  1. Create a Postmark account and add a Server at postmarkapp.com
  2. Under that Server, go to API Tokens and copy the Server API Token
  3. Verify your sending domain under Sender Signatures – Postmark will give you DKIM and Return-Path DNS records to add
  4. For SMTP auth, both the username and password are your Server API Token

The X-PM-Message-Stream header tells Postmark which stream to route the message through. Use outbound for transactional email. If you later add a broadcast stream for newsletters, you pass that stream’s ID instead. Postmark keeps separate reputation pools and analytics per stream.

Postmark’s “Bounces” dashboard shows every rejection with a reason code and the raw SMTP response from the receiving server – the fastest way to diagnose hard delivery failures without SSH access.


Provider Setup: Amazon SES

SES is the lowest cost option at scale – fractions of a cent per email. The trade-off is setup complexity: you need to navigate IAM, SMTP credential generation, sandbox mode approval, and potentially VPC configuration if your EC2 instances route through a private subnet. For high-volume WordPress sites (WooCommerce stores processing thousands of orders, multisite networks), SES is worth every minute of that setup time.

SES-Specific Setup Steps

  1. Go to AWS Console > SES > Verified Identities and verify your sending domain (not just an email address – domain verification enables DKIM signing)
  2. Add the three CNAME records SES provides for DKIM to your DNS
  3. Go to SES > SMTP Settings > Create SMTP Credentials – this creates an IAM user and generates SMTP-specific credentials. These are NOT your AWS Access Key/Secret
  4. Request production access (removes sandbox restrictions) via SES > Account Dashboard > Request Production Access. Approval typically takes 24 hours

Common SES gotchas: In sandbox mode, both sender and recipient addresses must be verified. Sending to an unverified address returns a 554 Message rejected. Once you exit sandbox this restriction is lifted. Also, SES enforces a sending rate limit per second – burst traffic from high-volume WooCommerce sales events can hit this. Use SES > Account Dashboard to monitor sending limits and request increases proactively.


Provider Setup: Mailgun

Mailgun sits between Postmark and SES in the pricing curve. It has a generous free tier (5,000 emails/month for 3 months as of writing), strong EU data residency options, and good webhook support for building custom bounce handling. The Mailgun API is more flexible than Postmark’s stream model, making it a solid choice for sites that mix transactional and notification email.

Getting Mailgun SMTP Credentials

  1. Add your domain under Mailgun > Sending > Domains
  2. Add the SPF, DKIM, and CNAME records Mailgun provides to your DNS
  3. Under your domain’s settings, find the SMTP credentials section – the password here is different from your Mailgun account password and your API key
  4. EU customers: use smtp.eu.mailgun.org as the host to keep data in Europe

Mailgun’s sandbox domain is useful for testing before DNS verification completes. The catch is that sandbox only delivers to addresses you whitelist under Sending > Sandbox. Trying to send to a non-whitelisted address from sandbox returns a generic 550 that looks like a real delivery failure – check the Mailgun logs dashboard first before assuming your SMTP configuration is broken.


Build a Persistent Email Log

File-based debug logs are fine for a debugging session. Production sites need a proper email log in the database: one row per attempt, a status column, and an error field for failures. This lets you answer questions like “did WooCommerce send the shipping confirmation for order #1042?” without enabling debug mode and reproducing the event.

The logger below creates a {prefix}email_log table on first run using dbDelta(), logs every wp_mail call with a pending status, then updates to sent or failed via the succeeded/failed hooks:

To query the log directly, use WP-CLI:

The WP-CLI eval commands let you test email delivery, inspect the active mailer class, and check SMTP connectivity without any browser session. The connectivity test at the end – fsockopen() to your SMTP host and port – is particularly useful on managed hosts where outbound port 587 may be blocked by firewall rules. If fsockopen fails, your SMTP credentials are irrelevant: the packet never reaches the server.


DNS Records: SPF, DKIM, and DMARC

SMTP configuration handles the transport layer. DNS records handle the authentication layer. Both are required. Getting email delivered to the inbox depends on your domain passing authentication checks at the receiving server – and those checks are all DNS lookups.

SPF (Sender Policy Framework)

SPF is a TXT record on your domain that lists which servers are allowed to send email on your behalf. The receiving server checks the envelope sender’s domain against your SPF record. If the sending server is not listed, the check fails. A typical SPF record for a domain using Mailgun looks like this:

v=spf1 include:mailgun.org ~all

The ~all (softfail) means unlisted senders get a warning flag but are not rejected outright. Use -all (hardfail) once you are confident your SPF record covers all your legitimate sending sources. Watch out for the 10-DNS-lookup limit in SPF – each include: directive can recursively expand into more lookups, and exceeding 10 causes SPF to return permerror, which some receivers treat as a failure.

DKIM (DomainKeys Identified Mail)

DKIM adds a cryptographic signature to each outgoing message. The public key lives in a DNS TXT record at selector._domainkey.yourdomain.com. The receiving server fetches the public key, verifies the signature in the DKIM-Signature header, and confirms the message was not tampered with in transit. Every major SMTP provider generates a DKIM key pair for you – you just need to add the TXT record they provide.

DMARC (Domain-based Message Authentication, Reporting and Conformance)

DMARC ties SPF and DKIM together and tells receiving servers what to do when a message fails both checks. It also enables aggregate reporting (rua) so you receive daily XML reports showing authentication pass/fail rates across your domain. Start with p=none to monitor without blocking anything, then move to p=quarantine and finally p=reject once your reports show consistent passes:

v=DMARC1; p=none; rua=mailto:[email protected]; adkim=r; aspf=r

Use this shell script to check all three records for your domain from the command line:

The PTR check at the end matters if you are sending from your own server’s MTA in addition to, or instead of, a relay. A PTR record (reverse DNS) maps your server’s IP to a hostname. Many spam filters do a forward-confirmed reverse DNS check: they look up your IP’s PTR record, then verify the resulting hostname resolves back to that IP. Mismatched or missing PTR records are a common cause of deliverability problems on VPS environments.


Debugging Deliverability Failures – A Systematic Approach

Email debugging is most efficient when you work from the outside in: check what the receiving server actually saw before digging into your WordPress configuration. The tool for that is MXToolbox at mxtoolbox.com – paste your domain and it checks SPF, DKIM, DMARC, blacklists, and MX records in one pass. If you see a blacklist hit, that is your priority: no amount of SMTP configuration fixes a domain on Spamhaus.

Common Failure Modes and Fixes

SymptomMost Likely CauseWhere to Check
wp_mail returns falsePHPMailer could not connect to SMTP hostRun fsockopen test via WP-CLI; check firewall rules
535 Authentication failedWrong SMTP username or passwordDouble-check constants in wp-config.php; regenerate credentials
550 Message rejectedSender not verified (SES sandbox), or domain on blocklistSES: verify sender in console. Otherwise: MXToolbox blacklist check
Email sent but lands in spamMissing or failing SPF/DKIM/DMARCRun 09-dns-check.sh; use mail-tester.com for full score
Email sent but never arrivesSilent bounce, ISP-level block, or wrong recipient addressCheck provider bounce dashboard; confirm recipient address is correct
Connection timeoutOutbound port blocked by firewallTry port 2525 as fallback; contact host about port 587
wp_mail returns true but provider logs nothingA plugin is swapping the mailer before your hook firesCheck plugin list for SMTP plugins; deactivate and test

Checking for Plugin Conflicts

If you have an SMTP plugin active (WP Mail SMTP, Post SMTP, Easy WP SMTP), it hooks into phpmailer_init and may override your mu-plugin. Check for conflicts by running this WP-CLI eval from the debugging section above – it shows the active transport class. If you see a class name from a plugin rather than the base PHPMailer, that plugin has taken over. Either configure through the plugin’s UI or deactivate it and use the mu-plugin approach exclusively.

Testing with mail-tester.com

mail-tester.com gives you a unique email address. Send a test email to that address from WordPress (use the WP-CLI eval in the debugging section), then check your score at the site. It tests SPF alignment, DKIM validity, DMARC pass/fail, content spam score, blacklists, and header structure. A score of 9/10 or higher means your setup is solid. Under 7/10 means something significant is broken – the report identifies exactly what.


WordPress-Specific Deliverability Considerations

The From Address Problem

WordPress defaults to sending from [email protected] with the name “WordPress”. Many SMTP providers and spam filters check whether the From address is an authenticated sender on your account. If you configure SES but send from an address that is not verified in SES, the email will be rejected with a 554 error. Always align your SMTP_FROM constant with a verified sender address on your provider.

Some plugins override the From address using the wp_mail_from and wp_mail_from_name filters. If your provider rejects messages from those plugin-set addresses, hook into those filters with a higher priority to force your verified address:

add_filter( 'wp_mail_from', fn() => SMTP_FROM, 99 );
add_filter( 'wp_mail_from_name', fn() => SMTP_FROM_NAME, 99 );

WooCommerce and High-Volume Sending

WooCommerce sends multiple emails per order: new order (admin), order confirmation (customer), and potentially shipping confirmation, download links, and review requests depending on your setup. On high-traffic stores this creates bursts of SMTP connections. Providers like SES enforce a per-second rate limit. If you see occasional 454 Throttling failure errors in your logs during order spikes, you need to either increase your SES sending quota or implement a queue.

The correct approach for high-volume WordPress email is to add the WP Cron based email queue pattern: instead of sending immediately via wp_mail(), push to a custom database queue and process in batches via a scheduled event. This is outside the scope of this guide but worth knowing if SES rate limits become a problem.

Multisite Considerations

On WordPress multisite, wp-config.php constants are global – the SMTP configuration in mu-plugins/smtp-init.php applies to every subsite automatically. This is usually what you want. If individual subsites need different From addresses (for white-label setups), use the wp_mail_from filter inside a site-specific mu-plugin that checks the current blog ID before overriding.


Choosing the Right Provider

ProviderBest ForFree TierPricing SignalComplexity
PostmarkTransactional email, strict deliverability requirements100 emails/monthHigher per-email costLow – clean UI, fast setup
Amazon SESHigh volume, AWS infrastructure already in use62,000/month (EC2-only)Lowest per-email cost at scaleHigh – IAM, sandbox, DNS setup
MailgunMixed transactional + notification, EU data residency5,000/month (3 months)Mid-rangeMedium – good docs, EU region available
Brevo (Sendinblue)Smaller sites, combined email + SMS300/dayCompetitiveLow
SendGridDeveloper-first, good API, large scale100/dayMid-rangeMedium

For most WordPress sites processing fewer than 10,000 emails/month, Postmark is the pragmatic choice: fast to set up, consistently high deliverability, and the bounce management dashboard saves significant debugging time. Move to SES when volume makes the cost difference meaningful or when you are already running infrastructure on AWS.


Quick Reference Checklist

  • Add SMTP constants to wp-config.php (host, port, user, pass, from address)
  • Drop smtp-init.php in wp-content/mu-plugins/ to apply constants to PHPMailer
  • Verify SPF record exists for your sending domain and includes your provider
  • Add provider-supplied DKIM TXT record to DNS
  • Add DMARC TXT record at _dmarc.yourdomain.com starting with p=none
  • Run 09-dns-check.sh to confirm all three DNS records resolve correctly
  • Check PTR record if sending from your own server IP
  • Send a test email via WP-CLI and confirm it lands in inbox (not spam)
  • Test at mail-tester.com and aim for 9/10 or higher
  • Enable 07-email-logger.php for persistent delivery visibility
  • Remove 01-wpmailcheck.php once debugging is complete
  • For SES: request production access before going live
  • For Postmark: confirm your domain is verified under Sender Signatures
  • For high-volume WooCommerce: monitor provider rate limits and request increases proactively

Take Email Off Your Troubleshooting List

WordPress email reliability is one of those things that works perfectly until it does not, and then it is very hard to diagnose without visibility into what is happening at each layer. The mu-plugin approach gives you SMTP without plugin overhead. The logging setup gives you delivery visibility without a third-party service. And the DNS checklist eliminates the authentication failures that drop messages before your SMTP credentials even matter.

If you are running a more advanced server configuration – custom WP-Cron intervals, server-level caching, or nginx-based rate limiting – check the WordPress performance tweaks category for patterns that complement this email setup.

All code snippets in this guide are available as a single GitHub Gist: gist.github.com/vapvarun/12e991ba5acf25c2d42b01ac4e1afe84 – nine files covering the debug mu-plugin, SMTP init, provider-specific setup for Postmark, SES, and Mailgun, the email logger, WP-CLI test commands, and the DNS check script.

Visited 9 times, 1 visit(s) today

Last modified: March 26, 2026