2026年11月14日

2026年11月14日

How to Use wp_mail() in WordPress: HTML Email, CC/BCC, Attachments

This article is a localized edition of the Japanese original. Read the Japanese original

wp_mail() is WordPress's function for sending email. It sends through PHPMailer, the mail library bundled with WordPress, and lets you change what is sent and how through filters and actions. WordPress core's own password reset emails and comment notifications are sent with this function too.

The code in this article has been checked against the wp_mail() implementation in WordPress 7.1 as of November 2026.

Arguments and return value

// Definition in wp-includes/pluggable.php (body omitted)
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array(), $embeds = array() ) { /* ... */ }
Argument Description
$to Recipients. An array of email addresses or a comma-separated string
$subject Subject
$message Body
$headers Additional headers (optional). An array or a newline-separated string
$attachments Paths of files to attach (optional). An array or a string
$embeds Paths of images to embed in HTML email (optional, added in WordPress 6.9)

It returns true or false. true means "PHPMailer finished the sending process without errors"; it does not mean the message reached the recipient. It is also true when the message is treated as spam or rejected by the receiving server.

Where to call it

If you write wp_mail() directly in the theme's functions.php or similar, it runs on every page load and sends an email every time. Call it inside a hook that represents the trigger, such as "when a post is published" or "when a form is submitted".

This example notifies the administrator when a post is published.

<?php
// Notify the admin email address when a post is published for the first time
add_action( 'transition_post_status', function ( $new_status, $old_status, $post ) {
    if ( 'publish' !== $new_status || 'publish' === $old_status || 'post' !== $post->post_type ) {
        return;
    }
    wp_mail(
        get_option( 'admin_email' ),
        'Post published: ' . $post->post_title,
        get_permalink( $post )
    );
}, 10, 3 );

Send HTML email

By default the body is sent as plain text (text/plain). To send it as HTML, add a Content-Type header. Escape values you insert into the body with esc_html().

<?php
function kw_send_order_mail( $to, $order_no, $total ) {
    $subject = 'Order confirmation';
    $message = '<p>Thank you for your order.</p>'
        . '<p>Order number: <strong>' . esc_html( $order_no ) . '</strong></p>'
        . '<p>Total: ' . esc_html( number_format( $total ) ) . ' JPY</p>';
    $headers = array( 'Content-Type: text/html; charset=UTF-8' );

    return wp_mail( $to, $subject, $message, $headers );
}

There is also a wp_mail_content_type filter that makes every email HTML, but it also sends WordPress core's plain-text emails (such as password resets) as HTML, which breaks their layout, for example by losing line breaks. Set the header only on the emails that need it, as above.

Garbled characters in HTML email are covered in "Why HTML email from wp_mail() is garbled and how to fix it".

Multiple recipients, CC, BCC and Reply-To

<?php
$to      = array( 'user1@example.com', 'user2@example.com' );
$subject = 'Notice';
$message = 'A notice from the site.';
$headers = array(
    'From: Site Name <noreply@example.com>',
    'Cc: admin@example.com',
    'Bcc: log@example.com',
    'Reply-To: support@example.com',
);

wp_mail( $to, $subject, $message, $headers );
  • If you pass several addresses in $to, everyone appears in the To line of a single message. If recipients shouldn't see each other's addresses, call wp_mail() once per person or use BCC.
  • If you omit From:, the sender is wordpress@your-site-domain. Use a sender address on the site's own domain. An address on another domain (such as a free webmail service) fails sender checks such as SPF and is less likely to be delivered. Changing the sender is covered in "How to change the WordPress email From address and sender name".

Attach files

Pass server file paths in $attachments. URLs cannot be attached.

<?php
$attachments = array(
    get_attached_file( 123 ),                    // The file of Media Library ID 123
    WP_CONTENT_DIR . '/uploads/report.pdf',      // A path given directly
    'monthly-report.pdf' => '/path/to/report-2026-11.pdf', // A string key becomes the attachment name
);

wp_mail( 'user@example.com', 'Monthly report', 'Please see the attached report.', '', $attachments );

When an array key is a string, that key becomes the attachment file name the recipient sees (added in WordPress 6.2). Attachments whose files can't be found are skipped when sending.

Embed images in HTML email (WordPress 6.9 and later)

The sixth argument, $embeds, added in WordPress 6.9, embeds images in the message so the body's <img> can reference them as cid:key.

<?php
$message = '<p><img src="cid:logo" alt="Logo"></p><p>Thank you for using our service.</p>';
$headers = array( 'Content-Type: text/html; charset=UTF-8' );
$embeds  = array(
    'logo' => get_stylesheet_directory() . '/images/logo.png', // The key "logo" becomes the cid
);

wp_mail( 'user@example.com', 'Notice', $message, $headers, array(), $embeds );

On WordPress 6.8 and earlier, the sixth argument is ignored, so the image is not shown.

Log sending failures

When sending fails, a WP_Error is passed to the wp_mail_failed action. This example writes the error to the log.

<?php
add_action( 'wp_mail_failed', function ( $error ) {
    error_log( '[wp_mail failed] ' . $error->get_error_message() );
} );

$sent = wp_mail( 'user@example.com', 'Subject', 'Body' );
if ( ! $sent ) {
    // Handle the failure (show an error, retry later, etc.)
}

Where error_log() writes depends on the PHP configuration. If you set WP_DEBUG and WP_DEBUG_LOG to true in wp-config.php, it goes to wp-content/debug.log. Trying to send an email from here would fail for the same reason, so just log it.

On success, the wp_mail_succeeded action (added in WordPress 5.9) fires.

Don’t send email from a staging site

This example prevents a staging site copied from production data from emailing real users. It checks the environment with wp_get_environment_type(), added in WordPress 5.5. Set the environment in wp-config.php, for example define( 'WP_ENVIRONMENT_TYPE', 'staging' ); (it is production if not set).

<?php
// Outside production, don't send; just log the recipients
add_filter( 'pre_wp_mail', function ( $return, $atts ) {
    if ( 'production' !== wp_get_environment_type() ) {
        error_log( '[wp_mail skipped] to: ' . implode( ', ', (array) $atts['to'] ) );
        return true; // Report success without actually sending
    }
    return $return;
}, 10, 2 );

If pre_wp_mail (added in WordPress 5.7) returns anything other than null, wp_mail() returns that value without sending.

To send to your development team instead of the real recipients, rewrite the arguments with the wp_mail filter. Note, however, that any Cc: or Bcc: headers still go to the original addresses.

<?php
add_filter( 'wp_mail', function ( $args ) {
    if ( 'staging' === wp_get_environment_type() ) {
        $args['to']      = 'dev-team@example.com';
        $args['subject'] = '[STAGING] ' . $args['subject'];
    }
    return $args;
} );

Send through an SMTP server

By default, wp_mail() sends with the server's PHP mail() function. If messages aren't getting through, change PHPMailer's settings in the phpmailer_init action and send through your mail service's SMTP server.

<?php
// Send via SMTP using constants you define yourself in wp-config.php (SMTP_HOST, etc.)
add_action( 'phpmailer_init', function ( $phpmailer ) {
    $phpmailer->isSMTP();
    $phpmailer->Host       = SMTP_HOST;
    $phpmailer->Port       = SMTP_PORT;    // 587 (STARTTLS) or 465 (SSL/TLS)
    $phpmailer->SMTPAuth   = true;
    $phpmailer->Username   = SMTP_USER;
    $phpmailer->Password   = SMTP_PASS;
    $phpmailer->SMTPSecure = 'tls';        // 'tls' for 587, 'ssl' for 465
} );

SMTP_HOST and the others are not WordPress core constants; you define them yourself in wp-config.php so the password isn't written in the code (for example define( 'SMTP_HOST', 'smtp.example.com' );). Setup steps and the plugin-based approach are covered in "How to send WordPress email via SMTP".

Test sending

Where WP-CLI is available, this command runs wp_mail() once and shows the return value.

wp eval 'var_dump( wp_mail( "you@example.com", "Test", "wp_mail test" ) );'

If it prints bool(true) but nothing arrives, check the spam folder and the sender address's domain and SPF settings. How to investigate is covered in "What to do when WordPress confirmation emails don't arrive".

Things to watch out for

  • It is not suited to bulk sending such as newsletters. Each message runs a full send, which takes time and can hit the server's sending limits. Use an email delivery service for bulk mail.
  • When you put user input into the subject or body, escape it with esc_html() for HTML email. Don't put user input directly into headers.
  • wp_mail() is defined in pluggable.php, so a plugin may replace it entirely. If you use an SMTP plugin, how the hooks in this article behave can depend on that plugin's settings.

Summary

  • Send with wp_mail( $to, $subject, $message, $headers, $attachments, $embeds ); a true return value only means the sending process succeeded
  • Don't call it at the top level; call it inside a hook
  • For HTML email, set the Content-Type: text/html; charset=UTF-8 header per message
  • Log failures with wp_mail_failed, and stop sending on staging with pre_wp_mail
  • If messages aren't getting through, send via SMTP with phpmailer_init

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲