The problem with letting emails disappear
Your WordPress site sends a password reset email. The user never receives it. A critical order confirmation lands in spam. A client's invoice vanishes before they even see it. These aren't rare edge cases—they're the default outcome when you ignore email deliverability. Email deliverability for WordPress: a step-by-step guide exists because the platform's default mail handling is fundamentally broken, and fixing it requires deliberate action at every layer: the sending mechanism, DNS records, and logging infrastructure.
Do not rely on PHP mail()
WordPress uses PHP's mail() function by default. This function hands your message to the server's local MTA (usually Postfix or Sendmail), which then attempts delivery. Three problems emerge immediately.
First, the local MTA has no reputation. ISPs don't know your server and treat mail from unknown IPs with suspicion. Second, PHP mail sends synchronously—if the MTA is slow or down, your site response time suffers. Third, you have zero visibility. No logs, no bounce handling, no retry logic. The email might fail silently.
We've seen production sites where mail() appeared to work because no errors were raised, yet 60% of transactional emails never arrived. The sender trusted WordPress's default and moved on. Months later, customers complained they'd never received password resets.
If you inherit a site using mail(), your first move is to replace it. Don't configure Postfix. Don't tune the local MTA. Replace the entire mechanism.
Use SMTP or an email API
You have two reliable paths: SMTP relays and HTTP-based email APIs. Each has trade-offs.
SMTP relays
Services like SendGrid, Mailgun, Amazon SES, and Brevo accept SMTP connections and forward mail to recipients. They handle reputation, bounce processing, and retries. You authenticate with credentials, send via their infrastructure, and benefit from their sender reputation.
Install WP Mail SMTP or Flexible Sendgrid Mailer. Both replace WordPress's default mailer with a configurable SMTP client. Here's the typical configuration flow:
- Create a SendGrid account and generate an API key
- Install WP Mail SMTP plugin
- Navigate to WP Mail SMTP → Settings
- Select SendGrid as the mailer
- Paste your API key
- Set the "From" email address (must match a verified sender domain)
- Send a test email
The plugin modifies the WordPress wp_mail() function to use SMTP instead of mail(). All transactional emails—password resets, notifications, orders—route through SendGrid. Deliverability improves because SendGrid maintains reputation with ISPs. You get bounce webhooks and delivery logs.
HTTP-based APIs
Services like SendGrid's API endpoint, Mailgun's API, and Postmark provide REST endpoints. You POST JSON, they handle delivery. Advantages: asynchronous (faster response), detailed event webhooks, and language-agnostic.
If you're building a custom plugin or high-volume scenario, HTTP APIs shine. A simple example using Mailgun:
function send_email_via_mailgun( $to, $subject, $message ) {
$domain = 'mg.yourdomain.com';
$key = 'key-xxxxxxx';
$response = wp_remote_post(
"https://api.mailgun.net/v3/{$domain}/messages",
array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Basic ' . base64_encode( "api:{$key}" ),
),
'body' => array(
'from' => 'noreply@yourdomain.com',
'to' => $to,
'subject' => $subject,
'text' => wp_strip_all_tags( $message ),
'html' => $message,
),
)
);
if ( is_wp_error( $response ) ) {
error_log( 'Mailgun error: ' . $response->get_error_message() );
return false;
}
$body = json_decode( wp_remote_retrieve_body( $response ), true );
error_log( 'Mailgun response: ' . wp_remote_retrieve_response_code( $response ) );
return isset( $body['id'] );
}
This sends synchronously via HTTP. For high-volume sites, queue these calls with a job runner (WP-Cron isn't suitable; use Action Scheduler or Laravel Horizon if you're running a custom stack).
Recommendation: Start with SMTP via WP Mail SMTP for most WordPress sites. It requires minimal code changes. Switch to HTTP APIs if you need advanced features or custom integration.
Set SPF, DKIM, and DMARC records
DNS records prove to recipient servers that you own the domain and authorize sending. Without them, ISPs classify your email as suspicious.
SPF (Sender Policy Framework)
SPF records tell ISPs: "I allow these servers to send mail from my domain." Add this TXT record to your domain's DNS zone:
v=spf1 include:sendgrid.net ~all
If using Mailgun:
v=spf1 include:mailgun.org ~all
The include: directive trusts the provider's infrastructure. The ~all means "softfail" on other sources (strict shops use -all). Propagation takes a few hours.
DKIM (DomainKeys Identified Mail)
DKIM signs emails with a private key you control. Recipients verify the signature with your public key in DNS. This proves you sent the message and it wasn't modified in transit.
Your email provider generates DKIM records. For SendGrid, you'll receive CNAME records like:
s1._domainkey.yourdomain.com CNAME s1.domainkey.sendgrid.net.
s2._domainkey.yourdomain.com CNAME s2.domainkey.sendgrid.net.
Add both to your DNS zone. Verify in SendGrid's dashboard. This usually takes 24-48 hours for ISPs to trust.
DMARC (Domain-based Message Authentication, Reporting and Conformance)
DMARC builds on SPF and DKIM, setting policy for what happens when SPF or DKIM fail. A basic policy:
v=DMARC1; p=quarantine; rua=mailto:security@yourdomain.com
This TXT record lives at _dmarc.yourdomain.com. It says: "If authentication fails, quarantine (move to spam). Send me daily aggregate reports." Over time, you collect data on who's impersonating your domain and refine the policy to p=reject.
Setup checklist:
- Log into your email provider and request SPF, DKIM, and DMARC records
- Add them to your domain's DNS zone
- Wait 24-48 hours for propagation
- Use MXToolbox or similar tools to verify records are live
- Check your provider's dashboard—most show a green checkmark once verified
Without these records, your email sits in spam regardless of SMTP quality.
Log sent emails
When an email fails to deliver, you need evidence. WordPress logs nothing by default. Add structured logging to catch failures early.
Enable WordPress debug logging
Add to wp-config.php:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
This writes errors to /wp-content/debug.log. WP Mail SMTP and similar plugins log here by default.
Custom email logging
For deeper insight, hook into WordPress's mail filter:
add_filter( 'wp_mail', function( $args ) {
$log_entry = array(
'timestamp' => current_time( 'mysql' ),
'to' => $args['to'],
'subject' => $args['subject'],
'headers' => $args['headers'] ?? array(),
);
error_log( 'Email attempt: ' . wp_json_encode( $log_entry ) );
return $args;
} );
add_action( 'wp_mail_failed', function( $error ) {
error_log( 'Email failed: ' . $error->get_error_message() );
} );
This logs every mail attempt and captures failures. Over time, you spot patterns: certain email types fail consistently, or failures spike after deployments.
Email provider webhooks
Your SMTP or API provider sends webhooks for delivery events: delivered, bounced, complained, opened. Connect these to your WordPress database for full visibility.
Example: SendGrid webhook. Configure at Settings → Mail Send → Event Webhook, pointing to https://yourdomain.com/wp-json/emails/webhook. Build a custom endpoint:
add_action( 'rest_api_init', function() {
register_rest_route(
'emails',
'/webhook',
array(
'methods' => 'POST',
'callback' => 'handle_sendgrid_webhook',
'permission_callback' => '__return_true',
)
);
} );
function handle_sendgrid_webhook( $request ) {
$events = $request->get_json_params();
foreach ( $events as $event ) {
$log = array(
'message_id' => $event['sg_message_id'] ?? '',
'email' => $event['email'] ?? '',
'event' => $event['event'] ?? '',
'timestamp' => current_time( 'mysql' ),
);
// Store in custom table
global $wpdb;
$wpdb->insert( $wpdb->prefix . 'email_logs', $log );
}
return array( 'success' => true );
}
Now you have a full audit trail. You can run reports: "How many emails bounced last week?" "Which customers aren't receiving transactional mail?" This data drives operational decisions.
Common mistakes
- Mixing senders: Some emails via SMTP, others via
mail(). This fragments your reputation and confuses ISPs. Route all transactional mail through one channel. - Skipping authentication records: SPF, DKIM, DMARC are not optional. Emails land in spam without them, no matter how good your SMTP provider is.
- Using a noreply address without verification: Your email provider requires verified senders. Use your branded domain (not Gmail or Outlook). Verify each sender address in your provider's dashboard.
- Ignoring bounce emails: If a recipient's mailbox is full or doesn't exist, your provider sends a bounce. Ignore these and your reputation declines. Subscribe to bounce webhooks and remove dead addresses.
- Sending too fast: Hammering an ISP with 1,000 emails in one second looks like a spam bot. Space sends over minutes or hours, or use your provider's burst limits.
- Not monitoring logs: Emails fail silently. Without logs, you won't notice for weeks. Set up alerts: if more than 5% of emails fail in an hour, page the team.
Key takeaways
- Replace PHP
mail()with SMTP (WP Mail SMTP plugin) or an HTTP API. Local MTAs have no reputation. - Add SPF, DKIM, and DMARC DNS records. These authenticate your domain and dramatically improve inbox placement.
- Log all email events—attempts, failures, and provider webhooks. Build a queryable audit trail so you can diagnose delivery issues.
- Verify your sender domain and each sender email address in your provider's dashboard. ISPs trust verified senders.
- Monitor bounce rates and unsubscribe webhooks. Maintain a clean recipient list to protect your reputation.
- Test end-to-end before going live: send a test email from your WordPress site and confirm it arrives in the recipient's inbox, not spam.






Comments
Loading comments…