WordPress wp_mail Example: Send Transactional Email Safely
Use wp_mail safely for transactional email with validation, HTML content rules, failure logging, duplicate-send prevention, and deliverability checks.
Published
April 20, 2026
Reading Time
3 min read
Updated
April 20, 2026

Hardening Notes
Baselines, access reduction, and default settings that stand up in production.
Best For
Teams preparing, launching, or maintaining WordPress as a backend service in a production stack.
Primary Topics
Editorial Focus
Control Ledger: Baselines, access reduction, and default settings that stand up in production. Updated on April 20, 2026.
Full Report
Last reviewed: April 20, 2026
Email looks simple in WordPress until a production plugin depends on it. Password resets, order confirmations, security alerts, form notifications, and editorial workflow messages all need predictable delivery. The mistake is treating wp_mail() as proof that the recipient received the message. It is not.
This guide shows a practical wp_mail() example for sending transactional email safely from a plugin. It covers input validation, headers, HTML content, logging failures, and the operational limits of WordPress email delivery.
Who this guide is for
This article is for WordPress plugin developers, WooCommerce-adjacent implementers, agencies, and site operators who need email features that behave reliably in production.
What wp_mail() can and cannot promise
wp_mail() returns whether WordPress and PHPMailer were able to process the send attempt. A true return value does not guarantee inbox delivery. DNS configuration, SPF, DKIM, DMARC, SMTP provider reputation, recipient filtering, and bounce handling all happen outside the function.
Use wp_mail() as the application send interface, but treat deliverability as an operational system.
Basic transactional email example
<?php
function vulnwp_send_security_alert_email( $recipient_email, $post_id ) {
$recipient_email = sanitize_email( $recipient_email );
$post_id = absint( $post_id );
if ( ! is_email( $recipient_email ) || ! $post_id ) {
return new WP_Error( 'vulnwp_invalid_email_request', 'Invalid email request.' );
}
$post = get_post( $post_id );
if ( ! $post ) {
return new WP_Error( 'vulnwp_missing_post', 'Post not found.' );
}
$subject = sprintf(
'Security review needed: %s',
wp_strip_all_tags( get_the_title( $post ) )
);
$message = sprintf(
'The post "%1$s" needs a security review. Edit it here: %2$s',
wp_strip_all_tags( get_the_title( $post ) ),
esc_url_raw( get_edit_post_link( $post_id, '' ) )
);
$headers = array(
'Content-Type: text/plain; charset=UTF-8',
);
$sent = wp_mail( $recipient_email, $subject, $message, $headers );
if ( ! $sent ) {
return new WP_Error( 'vulnwp_mail_failed', 'WordPress could not process the email send.' );
}
return true;
}
The example validates the recipient, checks that the post exists, strips HTML from the subject, and sends a plain text message. Plain text is often the safest default for operational alerts.
HTML email example
If the message needs links or formatting, use an explicit content type header and escape dynamic values before composing the HTML.
$subject = 'Deployment checklist ready';
$message = sprintf(
'<p>%s</p><p><a href="%s">%s</a></p>',
esc_html__( 'A new deployment checklist is ready for review.', 'vulnwp-mail' ),
esc_url( admin_url( 'edit.php' ) ),
esc_html__( 'Open WordPress admin', 'vulnwp-mail' )
);
$headers = array(
'Content-Type: text/html; charset=UTF-8',
);
wp_mail( '[email protected]', $subject, $message, $headers );
Do not build HTML email from unsanitized form values. Email clients are inconsistent, and unsafe HTML can still create trust and phishing problems even if it does not execute JavaScript.
Log send failures without exposing secrets
Use the wp_mail_failed hook to capture failures. Avoid logging full message bodies if they may contain personal data, tokens, reset URLs, or private content.
add_action(
'wp_mail_failed',
function ( WP_Error $error ) {
error_log( 'VulnWP mail failed: ' . $error->get_error_message() );
}
);
From address and SMTP notes
Production sites should send from a domain they control and authenticate that domain with the mail provider. Many hosts block or restrict raw PHP mail. For reliable delivery, use a real SMTP or transactional email provider and configure DNS records correctly.
If changing sender details globally, use WordPress mail filters carefully and test core emails after the change. A broken global mail configuration can affect password resets, admin notifications, ecommerce emails, and plugin alerts.
Rate limiting and duplicate sends
Transactional email should not be triggered repeatedly by refreshes, retries, or repeated webhook delivery. If an email belongs to a specific event, store an event marker before or immediately after sending so the same message is not sent many times.
$already_sent = get_post_meta( $post_id, 'vulnwp_security_alert_sent', true );
if ( $already_sent ) {
return true;
}
$result = vulnwp_send_security_alert_email( $recipient_email, $post_id );
if ( true === $result ) {
update_post_meta( $post_id, 'vulnwp_security_alert_sent', current_time( 'mysql', true ) );
}
This is especially important for scheduled tasks, import tools, status transitions, and payment-related workflows. Duplicate email is a trust problem even when the message content is correct.
Testing workflow
- Test on staging with the same SMTP provider or a mail capture tool.
- Verify plain text and HTML rendering in at least two email clients.
- Test invalid recipient addresses and missing content.
- Trigger a failure and confirm
wp_mail_failedlogging works. - Confirm password reset emails still work after mail configuration changes.
Production checklist
- Validate recipient addresses with
sanitize_email()andis_email(). - Keep subjects plain and strip unexpected HTML.
- Escape dynamic values before composing HTML email.
- Use a real mail provider for production delivery.
- Monitor
wp_mail_failedwithout logging secrets. - Test password reset and admin emails after SMTP changes.
- Document which plugin actions trigger email.
Common mistakes
- Assuming
truemeans delivered. It only means WordPress processed the send attempt. - Sending user input directly. Validate and escape values before including them in subject or body.
- Forgetting content type. HTML messages need an HTML content type.
- Logging complete messages. Email bodies can contain private data.
- No deliverability testing. DNS and provider configuration matter as much as code.
Related reading
Email features should be reviewed with the same discipline as other plugin surfaces. Pair this with our HTTP API example for external integrations and the debug log exposure checklist.


