Every WordPress website has to send emails. I’m not talking about marketing or newsletter emails. I’m talking about useful emails, like resetting a password, a registration notification, or a WooCommerce order confirmation. When emails sent by your applications don’t get delivered, you can safely consider it broken. Yet, many WordPress users have a really hard time with email deliverability, and there are good reasons for that.
Let’s take a step back. There’s one central function in WordPress that lets you send emails – wp_mail(). This function expects 5 arguments:
- $to – the recipient’s email.
- $subject
- $message
- $headers – email headers, like “From”, “BCC”, “CC”, etc.
- $attachments – paths to files on the disk that are to be included as attachments.
Internally, this function uses the PHPMailer library to format and send the email. By default, the email is sent with the PHP mail() function. This function passes this email to the Mail Transfer Agent (MTA) installed and configured on your server. An MTA is just software that manages email transfer, kind of like a digital postman. Some examples include Postfix, Exim, Sendmail, or Microsoft Exchange. What MTA you use is fully dependent on your server’s configuration – it doesn’t have anything to do with WordPress.
The Global Mail System
How the email system works is a little beyond the scope of this guide, but unfortunately it’s critical to understanding this topic, so I’ll give you a brief overview. As I already said, emails are sent by MTAs. To do that, they use the Simple Mail Transfer Protocol (SMTP). The way this works is your MTA connects to your recipient’s MTA and transfers the message.
The problem is – anybody can send an email saying that they are someone else. The recipient MTA has no way of knowing that the email was actually sent by johndoe@example.com. This problem is solved by two DNS records – SPF and DKIM. SPF is basically just a list of IP addresses that are allowed to send an email in the name of your domain (example.com). DKIM is a cryptographic scheme allowing the receiver to verify that the message was signed with your private key (which only you, that is – your MTA, has access to).
What the receiving MTA does with this information is completely up to its configuration (actually, it also depends on DMARC, but that’s mostly irrelevant here). It can be lenient and allow all messages in, even if the sender’s IP isn’t present in SPF and DKIM isn’t used. That would, however, be very insecure. Most MTAs will either send mail like that to spam or completely reject it. That’s where I’m going to stop my explanation of the mailing system. If you still don’t understand it – go ask ChatGPT about it.
Why mail() Often Fails
As already noted, the wp_mail() function uses the native PHP mail() function to send the message, which relays it to the local MTA. There are multiple reasons why this might fail. The most trivial one is that the web server doesn’t permit mail(). Many shared hosting environments disable this function completely in order to prevent bad actors from sending spam and getting their IP blacklisted. In that case, the email will never even reach an MTA.
But let’s say that your hosting environment allows mail() and has an MTA configured. Even if your website’s domain name is example.com and you send an email as johndoe@example.com, there’s a good chance that your mail server is different from your web server. If that’s the case, your web server’s IP will most likely not be present in your SPF (unless you added it), and your web server’s MTA will definitely not have the private key required for DKIM verification (unless you configured it, which isn’t usually possible on hosting environments prepared for websites). If the recipient’s server requires those, your message will be rejected.
The mailing system is complex and there are probably many more possible causes for mail() not to work. One of them is the fact that some sending MTAs don’t allow sending emails if the actual email matching the “From” header doesn’t exist. Here’s the catch – if you don’t supply this header yourself, WordPress will default to wordpress@example.com (assuming example.com is your website’s domain). If this email doesn’t exist, an MTA with those limitations will refuse to send the message.
Making wp_mail() Reliable
I think you should understand by now why using the default mail() is likely a bad idea. I mean, you could add your web server’s IP address to your SPF record and configure DKIM on your MTA, but that’s making your web server be your mail server – you already have a mail server for mail. That’s of course unless your web server already is your mail server. In that case, you don’t really have to configure anything more.
On most websites, you’ll have to set up an SMTP connection. The usual way of doing that is with a plugin. WP Mail SMTP is the most popular one but there are quite a few choices. What these plugins do is either hook into wp_mail() or override it completely (wp_mail() is pluggable). This way, they can modify the behavior of PHPMailer so that it doesn’t try to send the email directly to the receiver but instead relay it through your chosen SMTP server.
Your job would then be to configure the plugin with all the details needed to make an SMTP connection to your actual mail server, i.e., host, port (usually 465 or 587), username, and password. When using SMTP, instead of WordPress trying to send your email directly, your web server will create an SMTP connection to your mail server. Your web server will authenticate with your details (like you just logged into your email account) and will pass the message to your mail server. Your mail server will then send it to the receiver.
This way, the email is actually being sent by the MTA on your mail server, not your web server – exactly as if you logged into your email account and sent it manually. Assuming you’ve correctly configured your mail-related DNS settings, the message should have far fewer deliverability problems. You’re also not using mail() anymore, which means that you can send emails even on servers that disable it.
Some of these plugins also include built-in support for popular transactional email providers, such as SendGrid, Postmark, Mailgun, Amazon SES, and more. These are external email providers that you can configure and use (for a fee). They ensure superior deliverability compared to most self-hosted mail servers. Look into them if email is an important part of your website (e.g., on a WooCommerce store).
Modifying wp_mail() With Hooks
There are a few useful filters inside wp_mail() that allow you to modify its behavior. Search for hooks with “wp_mail” or just look at the function’s source code. Some of the interesting ones include wp_mail_from, wp_mail_from_name, wp_mail_charset, and wp_mail_content_type. For example, you could change the content type of your email from text/plain to text/html like this:
function pgn_set_html_content_type() {
return 'text/html';
}
function send_my_html_email() {
// Add the filter right before calling wp_mail()
add_filter( 'wp_mail_content_type', 'pgn_set_html_content_type' );
$to = 'user@example.com';
$subject = 'This is an HTML email';
$message = '<html><body><h1>Hello, World!</h1><p>This is a paragraph.</p></body></html>';
wp_mail( $to, $subject, $message );
// IMPORTANT: Remove the filter immediately after to avoid affecting other emails.
remove_filter( 'wp_mail_content_type', 'pgn_set_html_content_type' );
}