How to send WordPress default emails as HTML

View as Markdown

WordPress sends its own emails as plain text. Why, which 25 emails that means, why the one-line content-type fix breaks them, and how to give them a layout.

Open the password reset email WordPress sends and it looks like something from 2005: a wall of plain text, a raw URL, no site name at the top, no link home. The same goes for the new-user welcome, the “your site has updated” report and the comment notification. Most other email in your users’ inboxes, from the bank to the newsletter, has a layout. WordPress’s own have none.

This guide covers why those emails are plain text, exactly which emails are involved, why the popular one-line fix does more harm than good, and how to give them a simple HTML layout without touching the emails from WooCommerce and your other plugins.

Why WordPress emails are plain text

wp_mail(), the function every WordPress email goes through, sends text/plain unless a caller says otherwise. Plugins that care about how their email looks, such as WooCommerce, set the content type to HTML and build a template. WordPress core does not. It writes each of its emails as a string with line breaks in it and hands it over as is.

Plain text is a safe default for a site whose theme, language and mail setup core knows nothing about. It just means the emails core writes are the plainest ones your site sends.

Which emails this means

Across WordPress core there are 25 such emails: 19 on a single site, and 6 more that exist only on a multisite network.

If a plugin sends it, it is not on this list. Form notifications, order emails and newsletters are that plugin’s business, and each has its own template settings.

The fix that breaks things

Search this problem and the first answer is a snippet:

add_filter( 'wp_mail_content_type', function () {
    return 'text/html';
} );

It does make WordPress declare its emails as HTML. It also does three things you did not ask for.

The snippet changes the label without changing the email. What you actually want is a template around core’s emails, applied only to core’s emails, with the text inside left as core wrote it.

What a good fix looks like

Three rules keep this safe:

  1. Only wrap the emails WordPress core writes. Identify each one as core builds it, not by guessing from a subject line that is translated and filterable.
  2. Leave every other email alone. Anything from a plugin, and anything already sent as HTML, goes out untouched.
  3. Keep the text. Escape it, keep the line breaks, turn the web addresses into links so the reset link still works, and put a header and footer around it. Do not rewrite what core said.

A layout built for mail clients also means no images, no web fonts and every style inline, since many clients block remote images, ignore web fonts, and strip or limit stylesheets.

Set it up with Adminkeep

Adminkeep is a free plugin made of small features that each have one switch. Its Nice Default Emails feature does the above and nothing more: it recognises each of the 25 core emails as WordPress builds it, and sends it with your site’s name at the top, the text in a card, and the site’s name and address at the bottom. If you don’t have Adminkeep yet, installation takes a minute.

  1. Go to Settings → Adminkeep and switch on Nice Default Emails in the Email group.
  2. That is the whole setup. There are no options: one layout, named and linked from your site’s own settings.
  3. Send yourself a password reset from the login page to see it.

What you get:

It works with or without an SMTP setup. The layout is applied just before the email reaches the mailer, so it goes out the same way through Adminkeep’s own SMTP feature, another SMTP plugin, or PHP mail. With Email Log on, the log keeps the HTML version and previews it the way a mail client would.

The Nice Default Emails docs list every email by id, with what the layout holds and what is left alone.

See them in your own inbox

Most of these emails are awkward to trigger on purpose. You would need a new account, a comment held for moderation, an automatic update, a personal data request. Adminkeep can send each one for you, through the WordPress function that sends it on a live site, to one address:

wp adminkeep feature enable nice_emails
wp adminkeep emails list
wp adminkeep emails send password_reset [email protected]
wp adminkeep emails send --all [email protected]

On a single site that is 19 emails; the six multisite ones are listed and sent only on a network. Every email goes to the address you give and to nobody else. What an email needs is created for the run and deleted after it: a throwaway account, a private post with two comments, a personal data request. Your own accounts are not touched, and settings WordPress writes while sending are put back, so the links in those emails do nothing. The WP-CLI docs cover the flags.

Keep one email plain, or wrap one of your own

For developers, one filter decides. It is asked before each plain-text email goes out, with true for one of core’s emails and false for anything else, the email’s id, and the wp_mail() arguments:

add_filter( 'adminkeep_nice_emails_wrap', function ( $wrap, $email_id, $atts ) {
    if ( 'comment_moderation' === $email_id ) {
        return false; // keep this one plain
    }
    return $wrap;
}, 10, 3 );

Return true for a plain-text email of your own and it gets the same layout. Emails that already declare HTML are not asked about.

Switch on what you need. Forget the rest.

A free WordPress plugin — GPL, instantly reversible, and updates come to you.

Type to search the whole site.