How to send emails with wp_mail and HTML templates in PHP in WordPress

Contents

Introduction

This tutorial explains in full detail how to send HTML emails in WordPress using wp_mail, how to create and use HTML templates, how to handle headers, attachments, content types, and common debugging and deliverability considerations. You will find multiple complete, copy-paste-ready examples, a recommended pattern for template loading and safe placeholder replacement, how to set up SMTP via phpmailer_init for reliable delivery, and best practices to avoid common mistakes.

Quick summary

  • wp_mail is the WordPress wrapper around PHPMailer. It accepts arguments: to, subject, message, headers and attachments.
  • To send HTML email you can either add a Content-Type header or add a wp_mail_content_type filter that returns text/html.
  • Templates are best stored in a theme/plugin folder and loaded via a function that replaces placeholders with sanitized values.
  • For reliable delivery use an authenticated SMTP provider or a plugin such as WP Mail SMTP you can also configure PHPMailer directly using the phpmailer_init hook.

wp_mail basics

Signature of the function:

// wp_mail( stringarray to, string subject, string message, arraystring headers = , array attachments = array() )

Arguments:

  • to: email or array of emails.
  • subject: subject string.
  • message: message body (can be HTML when Content-Type set to text/html).
  • headers: string or array of headers (e.g., From: Name ltemail@domain.comgt or Content-Type: text/html charset=UTF-8).
  • attachments: array of file system paths to files to attach.

Sending a minimal HTML email

Two valid approaches to enable HTML content:

  1. Pass the Content-Type header in the headers argument.
  2. Temporarily add a filter with add_filter(wp_mail_content_type, …) and remove it after sending.

Example: add Content-Type in headers (recommended simple approach)


Example: use wp_mail_content_type filter (cleaner when sending multiple emails)

Hello from wp_mail

This message uses the wp_mail_content_type filter.

, array( From: Example ltno-reply@example.comgt ) ) // remove the filter so other mails are not forced to HTML remove_filter( wp_mail_content_type, set_html_mail_content_type ) ?>

Templates: structure and loading

Store email templates in a dedicated folder, for example: wp-content/themes/your-theme/emails/ or inside your plugins folder. Use a loader function that extracts variables and returns the final HTML. Always ensure user-supplied values are sanitized/escaped for safety.

Template file example (emails/welcome.php)

lt!-- save as emails/welcome.php --gt
lt!doctype htmlgt
lthtml lang=engt
ltheadgtltmeta charset=utf-8gtlt/headgt
ltbody style=font-family: Arial, sans-serif color: #333 margin: 0 padding: 20pxgt
  lttable width=100% cellpadding=0 cellspacing=0 role=presentationgt
    lttrgtlttdgt
      lth2 style=color:#2c3e50 margin-top:0gtWelcome, {{username}}!lt/h2gt
      ltpgtThanks for registering. Click the button to verify your email.lt/pgt
      ltpgtlta href={{verification_link}} style=display:inline-block padding:10px 18px background:#2ecc71 color:#fff text-decoration:none border-radius:4pxgtVerify Emaillt/agtlt/pgt
      ltp style=font-size:12px color:#999gtIf you didnt sign up, ignore this email.lt/pgt
    lt/tdgtlt/trgt
  lt/tablegt
lt/bodygt
lt/htmlgt

Loader and placeholder replacement (safe and flexible)

 value
  @return string Rendered HTML
 /
function mytheme_load_email_template( template_name, vars = array() ) {
    // Look in child theme then parent theme
    template_path = locate_template( emails/ . template_name . .php )

    // If not found in theme, check plugin path as fallback (example)
    if ( ! template_path ) {
        plugin_template = plugin_dir_path( __FILE__ ) . emails/ . template_name . .php
        if ( file_exists( plugin_template ) ) {
            template_path = plugin_template
        }
    }

    if ( ! template_path ) {
        return 
    }

    // Read file content (dont execute arbitrary PHP in templates unless intended)
    template = file_get_contents( template_path )

    // Replace placeholders like {{placeholder}} with escaped values
    foreach ( vars as key => value ) {
        // If value contains HTML you trust, use it cautiously. Otherwise escape:
        safe = wp_kses_post( value ) // allows common HTML tags adjust as needed
        template = str_replace( {{ . key . }}, safe, template )
    }

    return template
}
?>

Send using the template

 John Doe,
    verification_link => esc_url_raw( https://example.com/verify?token=abc123 )
) )

headers = array(
    From: My Site ltno-reply@example.comgt,
    Content-Type: text/html charset=UTF-8
)

wp_mail( john@example.com, Welcome to My Site, email_html, headers )
?>

Attachments

Attachments must be provided as absolute file paths on the server, not as URLs. If you have a media library attachment ID, use get_attached_file(attachment_id) to get the path.


Setting default From address and name

Rather than passing a From header every time, you can hook into wp_mail_from and wp_mail_from_name filters:


Configuring SMTP and PHPMailer

By default WordPress uses the PHP mail backend. For reliable delivery you should use authenticated SMTP or a transactional mail service (SendGrid, Mailgun, Amazon SES, etc.). Two approaches:

  • Install and configure a plugin such as WP Mail SMTP or similar.
  • Programmatically configure PHPMailer using the phpmailer_init action.

Configure PHPMailer via phpmailer_init

isSMTP()
    phpmailer->Host       = smtp.example.com
    phpmailer->SMTPAuth   = true
    phpmailer->Port       = 587
    phpmailer->Username   = smtp-username
    phpmailer->Password   = smtp-password
    phpmailer->SMTPSecure = tls // ssl or tls as required
    phpmailer->From       = no-reply@example.com
    phpmailer->FromName   = My Site
}
?>

Styling HTML emails: best practices

  • Keep layout simple many email clients have poor CSS support.
  • Prefer table-based layout for complex placements because of inconsistent CSS support.
  • Inline styles are the most reliable. Use a CSS inliner tool in your build process (e.g., Premailer, Juice) to convert ltstylegt blocks to inline styles.
  • Avoid external CSS files images should be absolute URLs (https) hosted on your server or CDN.
  • Test across clients (Gmail, Outlook, Apple Mail, mobile) and use tools like Litmus or Email on Acid for comprehensive testing, or Mailtrap/Mailhog locally.

Example of a compact, inlined HTML snippet for an email

lttable width=100% cellpadding=0 cellspacing=0 role=presentation style=background:#f6f6f6padding:20pxgt
  lttrgtlttd align=centergt
    lttable width=600 cellpadding=0 cellspacing=0 role=presentation style=background:#ffffffborder-radius:6pxoverflow:hiddengt
      lttrgtlttd style=padding:20pxgt
        lth2 style=color:#333margin:0 0 10pxgtHello {{username}}lt/h2gt
        ltp style=color:#666margin:0 0 15pxgtWelcome to our service.lt/pgt
        lta href={{action_link}} style=background:#0073aacolor:#fffpadding:10px 16pxtext-decoration:noneborder-radius:4pxdisplay:inline-blockgtGet startedlt/agt
      lt/tdgtlt/trgt
    lt/tablegt
  lt/tdgtlt/trgt
lt/tablegt

Security and data handling

  1. Sanitize recipient email addresses with sanitize_email().
  2. Escape or sanitize placeholder data before inserting into templates: esc_html() for plain text, esc_url() for links, and wp_kses_post() for safe HTML fragments.
  3. Never insert raw user-supplied HTML into templates without rigorous sanitization.
  4. Use nonces and capability checks on any actions that trigger emails from front-end forms.

Debugging delivery problems

  • Enable WP_DEBUG and check PHP error logs for fatal errors.
  • Inspect the return value of wp_mail. It returns true on success and false on failure (but success does not guarantee delivery).
  • Hook into phpmailer_init to inspect PHPMailer internals dump errors from phpmailer->ErrorInfo if sending fails.
  • Use Mailtrap or MailHog on dev machines to capture and review outbound messages without sending them to real addresses.
  • Check SPF, DKIM, and DMARC for your sending domain to avoid spam filtering.

Example: log failures and PHPMailer error info


Scaling and performance considerations

  • wp_mail is fine for transactional emails (password reset, notifications), but avoid using it for large bulk sends in a single request (time-outs, throttling). Use background queueing (WP Cron, action scheduler, or a queue worker) for bulk operations.
  • Use transactional email providers (Mailgun, SendGrid, Amazon SES) for high deliverability and analytics.
  • Consider batching and rate limits to avoid being blacklisted by recipients or SMTP providers.

Complete real-world example: send a templated HTML welcome email with attachment and SMTP configured

Below is an integrated example: template loader, set content type via headers, attach a file from the uploads folder, and configure PHPMailer via phpmailer_init for SMTP. Copy the parts you need into your plugin or theme (functions.php).

isSMTP()
    phpmailer->Host       = smtp.example.com
    phpmailer->SMTPAuth   = true
    phpmailer->Port       = 587
    phpmailer->Username   = smtp_user
    phpmailer->Password   = smtp_pass
    phpmailer->SMTPSecure = tls
    phpmailer->From       = no-reply@example.com
    phpmailer->FromName   = My Site
}

// 2) Template loader (as shown earlier)
function mytheme_load_email_template( template_name, vars = array() ) {
    template_path = locate_template( emails/ . template_name . .php )
    if ( ! template_path ) {
        template_path = plugin_dir_path( __FILE__ ) . emails/ . template_name . .php
    }
    if ( ! template_path  ! file_exists( template_path ) ) {
        return 
    }
    template = file_get_contents( template_path )
    foreach ( vars as key => value ) {
        safe = wp_kses_post( value )
        template = str_replace( {{ . key . }}, safe, template )
    }
    return template
}

// 3) Actual send
function my_send_welcome_email( user_email, username ) {
    to = sanitize_email( user_email )
    if ( ! is_email( to ) ) {
        return false
    }

    message = mytheme_load_email_template( welcome, array(
        username => username,
        verification_link => esc_url_raw( https://example.com/verify?token= . wp_hash( user_email ) )
    ) )

    headers = array(
        From: My Site ltno-reply@example.comgt,
        Content-Type: text/html charset=UTF-8
    )

    // Attach a welcome PDF from uploads if it exists
    upload_dir = wp_upload_dir()
    attachment_file = trailingslashit( upload_dir[basedir] ) . welcome-guide.pdf
    attachments = array()
    if ( file_exists( attachment_file ) ) {
        attachments[] = attachment_file
    }

    return wp_mail( to, Welcome to My Site, message, headers, attachments )
}
?>

Resources and references

Final checklist before going live

  1. Confirm Content-Type is set to text/html when sending HTML.
  2. Sanitize recipient addresses and user-supplied placeholders.
  3. Use inline CSS or an inliner tool for styles.
  4. Verify attachments use server file paths and exist.
  5. Configure SMTP or a transactional mail provider for production deliverability.
  6. Test on multiple clients and using an email testing tool.
  7. Monitor SPF/DKIM/DMARC and set them up for your sending domain.


Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *