Transactional Email Delivery in  WordPress

Configure transactional email for WordPress that's reliable using an SMTP Relay or an HTTP API.

WordPress provides a built-in function for delivering programmatic emails: wp_mail. Without any modification this function will send emails directly from your web-server, which will arrive directly into your recipient’s spam folder (most likely). Email spam is vicious, and spam filtering technology has to be strong and strict to protect your inbox. Any email delivered right from a web server is extremely likely to be flagged as spam. There are ways to avoid this issue, and they can be used to turn wp_mail() into a reliable, robust transactional email delivery system.

Reliably Email Delivery

To deliver reliable emails from WordPress that aren’t flagged as spam you’ll need to send email from a real mail server, not your web server. This may sound complex, but it’s not. There are plenty of transactional email services out there, many of which provide a large number of free email sends each month. Signup with a transactional email delivery service like Mailjet or SendGrid and you’ll have what you need to configure WordPress with an email sending domain.

Once you have a service that will deliver the emails you’ll need to configure WordPress to use it. This can be done in one of two ways, both of which are outlined below.

Simple Configuration with SMTP Relay

SMTP stands for Simple Mail Transfer Protocol, which provides a way to send and receive email on a web server, using a remote mailbox server to relay the messages back and forth. If your transactional email provider has given you SMTP credentials to work with, then the following bit of code is all you need to send all email delivered by WordPress using it.

<?php
/**
 * Transactional WordPress Emails
 */
class Transactional_Emails
{
  public $from_email = '[email protected]';
  public $from_name = 'Kevinleary.net';

  /**
   * Instantiate.
   */
  public function __construct()
  {
    add_filter('wp_mail_content_type', [$this, 'wp_mail_content_type'], 10);
    add_filter('wp_mail_from', [$this, 'wp_mail_from'], 99);
    add_action('wp_mail_failed', [$this, 'wp_mail_failed'], 10, 1);
    add_filter('wp_mail', [$this, 'wp_mail'], 10, 1);
    add_action('phpmailer_init', [$this, 'phpmailer_init'], 10, 1);
    add_action('init', [$this, 'test_send']);
  }

  /**
   * Mailer Config.
   *
   * @param mixed $phpmailer
   */
  public function phpmailer_init(&$phpmailer)
  {
    $phpmailer->CharSet = 'utf-8';
    $phpmailer->FromName = $this->from_name;
    $phpmailer->From = $this->from_email;
    $phpmailer->Sender = $this->from_email;
    $phpmailer->addReplyTo($this->from_email, 'SBLI');
  }

  /**
   * Test Email Delivery
   *
   * Add [email protected] onto a URL in your site
   * to send out a test email.
   */
  public function test_send()
  {
    if (! isset($_GET['test-email'])) {
      return;
    }

    $email = esc_attr($_GET['test-email']);
    $valid = filter_var($email, FILTER_VALIDATE_EMAIL);
    if (! $valid) {
      wp_die('Email provided is not a valid email address.');
    }

    $mail = wp_mail($email, 'Test Send', 'Email test send.');
    wp_die("Test email sent to {$email}.".PHP_EOL.var_dump($mail));
  }

  /**
   * Log Mail Delivery Errors
   *
   * @param mixed $error
   */
  public function wp_mail_failed($error)
  {
    $message = $error->get_error_message();
    trigger_error("wp_mail() triggered the following error and failed to delivery email: $message", E_USER_WARNING);
  }

  /**
   * Transactional HTML Email Template
   *
   * HTML template wrapper for wp_mail().
   *
   * @param array $args
   */
  public function wp_mail($args)
  {
    ob_start();
    $body = isset($args['message']) ? $args['message'] : '';
    include get_stylesheet_directory().'/templates/email.php';
    $args['message'] = ob_get_clean();

    return $args;
  }

  /**
   * From Email
   */
  public function wp_mail_from()
  {
    return $this->from_email;
  }

  /**
   * HTML Emails
   *
   * Send HTML emails with wp_mail()
   */
  public function wp_mail_content_type()
  {
    return 'text/html';
  }
}

new Transactional_Emails();

Sending Email with an HTTP API

Many transactional email providers have an HTTP API alternative option for delivering mail. This can provide better analytics tracking, improved logging and error reporting, among a few other benefits. If you’d like your WordPress website to send all emails using a transactional email servers HTTP API you can replace the core functionality of wp_mail() function entirely using the pre_wp_mail filter.

This allows us to handle all uses of the wp_mail() function with a transactional email API by short-circuiting WordPress’s phpmailer built-in functionality. In the example below I’m handling wp_mail() with the SendGrid Mail Send API. I’ve included two built-in actions hooks for handling errors and successful requests, but this excludes a few others. You may need to expand on what is below to support all plugins and use cases.

/**
 * API Email Delivery
 *
 * @param null|bool $return Short-circuit return value.
 * @param array $atts Array of the `wp_mail()` arguments.
 * @type string|string[] $to  Array or comma-separated list of email addresses to send message.
 * @type string $subject  Email subject.
 * @type string $message  Message contents.
 * @type string|string[] $headers  Additional headers.
 * @type string|string[] $attachments  Paths to files to attach.
 */
function kevinlearynet_wp_mail($return, $atts)
{
  extract($atts);

  $from_email = get_option('admin_email');
  $from_name = get_bloginfo('sitename');

  // SendGrid Send API
  $payload = [
    'personalizations' => [[
      'to' => [[
        'email' => $to,
      ]],
      'subject' => $subject,
    ]],
    'from' => [
      'email' => $from_email,
      'name' => $from_name,
    ],
    'content' => [[
      'type' => 'text/html',
      'value' => $message,
    ]],
  ];
  $send = wp_remote_post('https://api.sendgrid.com/v3/mail/send', [
    'headers' => [
      'Authorization' => 'Bearer AbCdEf123456',
      'Content-Type' => 'application/json',
    ],
    'body' => json_encode($payload),
  ]);
  $body = wp_remote_retrieve_body($send);
  $status = wp_remote_retrieve_response_code($send);
  $message = wp_remote_retrieve_response_message($send);

  // Errors
  if ('20' !== substr("{$status}", 0, 2)) {
    $error_data = [
      'status' => $status,
      'response' => json_decode($body),
      'request' => $args,
    ];
    $wp_error = new WP_Error('wp_mail_failed', "SendGrid API Error: {$message}", $error_data);
    do_action('wp_mail_failed', $wp_error);

    return false;
  }

  // Success
  $json = json_decode($body, true);
  $data = $atts;
  $data['api_response'] = $json;
  do_action('wp_mail_succeeded', $data);

  return true;
}
add_filter('pre_wp_mail', 'kevinlearynet_wp_mail', 99, 2);

With this function in place you’re able to use a transactional email providers HTTP API to send all email within WordPress. You can also do this with the help of plugins, but this approach provides more control, and far less code and overhead than any plugin option. It’s the way I typically handle email delivery on a minimal WordPress site.

Related Articles

Meet the Author

Kevin Leary, WordPress Consultant

I'm a custom WordPress web developer and analytics consultant in Boston, MA with 16 years of experience building websites and applications. View a portfolio of my work or request an estimate for your next project.