wp_mail() is NOT broken

Hey there! I'm currently working on a CLI tool to deploy WordPress apps to DigitalOcean. Check it out! It's free and open source.

So why is everybody trying to fix it? I’ll try to explain.

A few of months ago I started working on a new open source project called Sail, which is a CLI tool to provision WordPress servers on DigitalOcean. I wanted the best possible e-mail configuration I could have, fully transparent to the WordPress user, so I did some research. A lot of research actually.

The first thing I noticed, is that Google is just filled with search results about how broken the wp_mail() function is in WordPress, and how you need a ton of plugins and paid services to fix it. And yet, when I looked deeper into that “fix” that was offered, I was quite surprised, and also a bit disappointed with some of our beloved hosting providers.

But what do hosting providers have to do with e-mail? Well, nothing. But also everything.

What is SMTP?

Before talking about what exactly is broken, and why the commonly suggested fix is not really a fix, you’ll need to know a bit more about how e-mail works. No in-depth stuff, just a quick overview of what SMTP is and how e-mail travels around the Internet, in a very simplified explanation.

When Bob wants to send Alice an e-mail, he uses a Mail User Agent or MUA, such as Outlook or Gmail to write and send that message. The MUA will send that mail to the configured Mail Transfer Agent, or MTA. This MTA will then, through a series of other MTAs, find its way to Alice’s MUA.

SMTP is really just the language, or protocol that these MTAs and MUAs usually speak between one another.

What about websites?

Maybe Bob has a WordPress site, and Alice forgot her password, so she goes for that recovery e-mail, which we know happens through the wp_mail() function. What happens then? Well, nothing special really, but this is where it often times gets messy.

Internally wp_mail() is just a wrapper around an open source PHP library called phpMailer. By default, this library uses PHP’s internal function called mail(). And that, by default, will invoke whatever is in the sendmail_path php.ini configuration directive, which defaults to the executable /usr/sbin/sendmail. And this is our default MTA.

Sendmail used to be the largest mail server software in use on the Internet, but today there are plenty of other options, including Postfix, Exim, Qmail and others, which happen to also have MTA executables compatible with Sendmail, which means they can read a message from stdin, and send it wherever it needs to go, based on the destination address. Postfix even ships with a /usr/sbin/sendmail executable, so it’s a full drop-in replacement for Sendmail.

Going back to wp_mail(), what exactly does it do then? Nothing extra-ordinary: it creates an e-mail, which is just a string of headers and some content, and pipes it to an executable on the system. If you had to do this in an SSH shell, here’s what you would do:

$ echo "To: alice@somewhere.org
From: bob@elsewhere.org
Subject: Hey

Guess you forgot your password, eh?" | /usr/sbin/sendmail

Yes. It’s that simple.

And it’s very efficient too. Your PHP script doesn’t need to connect to any servers, it doesn’t need to figure out where the mail is headed or how. It doesn’t even need to speak SMTP. It just needs to pipe a string into a binary. Your PHP script is the MUA., not the MTA.

So why is it broken?

Let’s go to some managed hosts and see what they have to say about e-mail not working:

Here’s Kinsta’s various reasoning:

  • It is not a problem on the server, but rather email is set up incorrectly on the WordPress installation
  • Some e-mail clients identify WordPress e-mails as spam, because they’re automated
  • Your server isn’t configured to send them

Not a problem on the server, eh? When you run out of things to bash, why not bash WordPress. Also, e-mail clients identify WordPress e-mails as spam? This sounds ridiculous to me. Why would a spam filter classify an e-mail as spam if it contains password reset instructions, which is very likely exactly what the recipient was looking for? I surely hope most modern spam filters are smarter than that.

Now their last point, about the server being misconfigured, that sounds more like the truth.

WP Engine claims:

  • Default e-mail functionality on WP Engine is limited
  • We impose a hard limit on the amount of emails that can be sent in an hour
  • For robust email functionality, monitoring and scalability, we highly suggest customers utilize a 3rd party email host

Okay, that sounds reasonable. They don’t want their IP addresses flagged for spam, so they filter things that don’t look like default WordPress e-mails (user registration, password reset, etc.) and for anything more than that, they want us to use a third-party service. Sounds fair, at least they’re not saying our WordPress install is broken.

Let’s see what SiteGround has to say:

  • WordPress uses the PHP mail() function to send emails … Many email providers will mark messages sent this way as spam
  • Misconfiguration between SMTP settings in Site Tools and your WordPress
  • You should configure your WordPress to use SMTP instead of the PHP mail() function

Ugh. The mail providers I know usually rely on things like SPF records, DKIM, and also the e-mail contents. Not the name of the PHP function that was used to send the e-mail. That would be weird.

I looked at a few other hosts as well, and they mostly all tell the same story. For those that do support sending e-mail without any additional configuration, it is almost always limited to a couple hundred e-mails per hour, which I think is fair.

So is it really broken?

Not really I guess. Look, they can send transactional mail without any additional configuration. This means that they do have local MTAs responding to whatever is in sendmail_path. They’re rate limiting and filtering, either directly in that binary, or at some other MTA, probably before the mail leaves their network.

This makes total sense.

What doesn’t make sense to me, however, is that with the vast majority of hosts, the recommended way of “solving” this problem, is with an SMTP plugin for WordPress, and they go through great lengths, to teach us how to install and configure all these different SMTP/Transactional mail plugins.

What SMTP plugins do to wp_mail()

Honestly, there’s nothing wrong with using a plugin like that. It allows the user to specify SMTP settings and credentials of a third-party SMTP relay service, and causes wp_mail() to use phpMailer’s SMTP capabilities, instead of using PHP’s internal mail().

Remember I said how sending a string to the sendmail binary is so clean and efficient. Compare that to sending something over SMTP via PHP, which looks roughly like this:

  • Create a PHP socket with fsockopen() or stream_context_create()
  • Establish a connection to the third party relay’s SMTP server
  • Secure that connection with TLS
  • Authenticate with this server using your SMTP credentials
  • Send the e-mail contents
  • Wait for the SMTP server to acknowledge/accept the message for delivery
  • Close the connection

If this third-party relay is an HTTP API service, then it’s probably slightly simpler than this, but still very complex compared to sending a string to a system executable. And imagine what happens if this SMTP/API service is offline or overloaded, or if the network conditions aren’t great.

Best case scenario, the plugin will detect that, and attempt to do all that work again later. Worst case, it will fail silently the message will never leave your PHP script.

In other words, with a plugin like that, your wp_mail() function stops being just an MUA, and becomes an MTA.

Can WordPress be a reliable MTA?

No.

Just like WP Engine, Kinsta and SiteGround claim they’re great at web hosting, but want nothing to do with e-mail, your WordPress site is great at … being a site, but not a Mail Transfer Agent.

Sure, one could argue they can schedule e-mail delivery with the Action Scheduler library, and maintain a fully functional e-mail queue in the WordPress database, then use the WordPress cron to execute scheduled actions and attempt to deliver mail through SMTP or an API, and then defer to a different queue for retries later.

Yes, in theory that might work. But will it be great? I don’t think so.

Sendmail was written in the 1980s, Exim and Qmail were written in 1995, Postfix was released in 1998, and there are plenty of other options. These are widely adopted MTAs with decades worth of coding and testing in some of the largest and busiest networks in the world. Why reinvent the wheel in PHP and WordPress, instead of using something that’s proven to work so reliably over the decades.

Don’t let WordPress be your MTA.

Can WordPress be a performant MTA?

No.

I got myself a Mailgun account to run some simple mail performance tests. I provisioned a brand new vanilla WordPress install on DigitalOcean, and used Sail’s mailgun.yaml blueprint to quickly install the Postfix MTA, and configure it to relay mail to Mailgun’s SMTP servers.

I then wrote some sample code to send 20 e-mails to myself, nothing fancy:

And I fired it. I measured the results with Query Monitor and profiling in Sail.

Postfix results

I then tried a couple of SMTP plugins for WordPress, configured to the exact same Mailgun SMTP servers, and ran the exact same test:

SMTP plugin results

Ahem. 30 seconds! Against 0.35. That’s like 90x slower. And this is only 20 e-mails.

I ran the same request through an actual profiler to try and learn more about what’s happening, as expected 20 new instances of phpMailer, 20 new SMTP connections and so on and so forth:

Next, I tried some of the same SMTP plugins, but with a Mailgun configuration, which uses their HTTP API, instead of SMTP. The results were slightly better, as I expected:

Mailgun via HTTP API

Still, that’s 13 seconds, against the original 0.35, so “only” about 40 times slower. How about a fancy chart to put that into perspective?

And again, this is just 20 e-mails. No wonder why everyone’s recommending not to use wp_mail() for newsletters, or anything other than password reset communication. You just can’t afford to spend that amount of time in PHP, waiting for an e-mail to leave your script.

Don’t let WordPress be your MTA.

What’s the alternative?

There’s some good news, and there’s some bad news.

The good news is that if you happen to be a DIY-type of person, like to tinker with servers and wouldn’t mind getting your hands a little dirty with the command line, you can have Postfix (or any other great MTA) relay your mail to an external SMTP service.

Sail CLI allows you to do this on DigitalOcean with a couple of simple commands for Mailgun, or any other SMTP service through the Postfix blueprint. If you’d like to do it from scratch, perhaps on a different cloud provider, DigitalOcean has a great guide on setting that up in Ubuntu Linux.

The bad news is that if you’re on a managed WordPress host, you’re probably out of luck, and will have to continue using an SMTP plugin for all your outgoing wp_mail() needs. If you do use one, and it happens to support your mail service provider via an HTTP API, chances are that’s going to be more efficient than actual SMTP.

There’s hope for a better wp_mail()

Sendmail is not all that complicated. It’s not rocket science. Nor is it string theory, although it does parse some strings from stdin. And hey, we already know that most WordPress hosts have MTAs up and running, so one piece of the puzzle is already solved.

The missing piece is the ability to configure an SMTP relay in your hosting control panel. Four input fields – SMTP host, SMTP port, SMTP username and SMTP password. Here, I’ll provide a free design for you, so you don’t have to spend $30k and eighteen months on your first iteration:

Bake these values into a Postfix (or whatever MTA software you’re comfortable with) SMTP relay configuration, add per-user authentication if you’ve got a shared environment, might need to add some isolation in sendmail for PHP, to make sure users are who they say they are, and you’re golden.

Let your MTA be the MTA, so that WordPress can be WordPress.

P.S. If you work at a hosting company that’s already doing this, please let me know so I can recommend you folks like crazy.

2 thoughts on “wp_mail() is NOT broken

  1. I built WPMUDEV hosting and we configure postfix to send transactional email via mailgun. Partly for this, partly so we can log and rate limit email to try and limit abuse.

    A more applicable post is how freaking hard it is to do email for WordPress and keep it deliverable. It’s impossible to find clean IPs for mailservers. Common WP email usage due to poorly thought out plugins and themes is highly likely to get you banned by any transactional email provider (comment notifications, contact form loop backs, signup activation emails, plugin notices, etc).

    • Thanks for your comment Aaron. Why wouldn’t you let users configure their own Mailgun accounts with your Postfix instance? Wouldn’t that solve your abuse and rate limiting problem, and risk of having your account banned by Mailgun?

Comments are closed.