I was suprised WordPress is not able to send email using an SMTP server out-of-the-box. Not to mention using TLS transport for security. A quick Google search showed me multiple plugins to handle this. Hence, everything is handled through plugins in WordPress... Need to optimize your website? Use plugin x. Want a more secure WordPress? Use plugin y. I wanted to create something myself and here is how to override the wp-mail()
function and send email using authenticated SMTP and StartTLS from WordPress.
Authenticated SMTP and TLS in WordPress - secure email
I haven't checked how other plugins work, but I was sure that I wouldn't want my SMTP credentials to be stored in the MySQL database. My thought was that storing the SMTP credentials in the wp-config.php
file might be better.
I decided to try something, and it turns out to be pretty easy! Just follow the next few steps and you'll send emails from WordPress using authenticated SMTP (SMTP AUTH) over a StartTLS/TLS secured connection.
The mail sending function, called wp-mail, is defined in the file pluggable.php
. This file is located in the directory /wp-includes
. This means we can overrule it with our own function and plugin. To start, just copy that function into a new file. Now locate $phpmailer->IsMail();
and change that to $phpmailer->IsSMTP();
.
Later on you have to place your PHPMailer configuration directly below this line. If you'd like more information on some PHPMailer configuration settings, see https://github.com/PHPMailer/PHPMailer/blob/master/README.md, and/or the PHP code found in Part 2 below.
As shown below, you can define your SMTP configuration values in your wp-config.php
file. If placed in the plugin file, there is always that possibility of a registered user being able to view the contents of that file. We don't want that. Now, let's go.
WordPress SMTP configuration in wp-config.php
We don't want to store our SMTP credentials in either the MySQL database (wp_options
table) or wp-mail.php
plugin file. Therefor we need to define it in wp-config.php
using PHP's mail() function.
In /wp-config.php
, simply add:
define( 'SMTP_USER', 'user@example.com' );
define( 'SMTP_PASS', 'Your p4ssword' );
define( 'SMTP_PORT', '25' );
define( 'SMTP_SECURE', 'tls' );
define( 'SMTP_AUTH', true );
define( 'SMTP_HOST', 'smtp.example.com' );
define( 'SMTP_FROM', 'website@example.com' );
define( 'SMTP_FROM_NAME', 'e.g Website Name' );
define( 'SMTP_DEBUG', 0 ); // for debugging purposes only set to 1 or 2
WordPress wp-mail.php plugin file for SMTP
The second part is this wp-mail.php file.
File: /wp-content/plugins/wp-mail/wp-mail.php
Copy and paste the wp_mail()
function from /wp-includes/pluggable.php
, or copy and paste the code below and put it in a new file.
Save that new file as wp-mail.php
.
Now create a new folder called wp-mail in your /wp-content/plugins
directory, for instance using FTP, and upload your newly created wp-mail.php
file to that location.
<?php
/**
* Send authenticated SMTP email over TLS with WordPress
*
* @wordpress-plugin
* Plugin Name: Authenticated SMTP email over TLS with WordPress
* Plugin URI: https://www.saotn.org
* Donate Link: https://www.paypal.me/jreilink
* Description: Sends email using authenticated SMTP (SMTP AUTH), over an TLS encrypted connection in WordPress.
* Version: 1.0.0
* Author: Jan Reilink
* Author URI: https://www.saotn.org
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
*/
/**
* Plugin to override the wp_mail function in pluggable.php. See
* pluggable.php for details.
*/
if (!function_exists('wp_mail')) {
add_filter('plugin_row_meta', 'plugin_row_meta', 10, 2);
function plugin_row_meta($links, $file) {
if ( !preg_match('/wp-saotn-mail.php$/', $file ) ) {
return $links;
}
$links[] = sprintf(
'<a href="https://www.paypal.me/jreilink">%s</a>',
__( 'Donate' )
);
return $links;
}
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
// Compact the input, apply the filters, and extract them back out
/**
* Filter the wp_mail() arguments.
*
* @since 2.2.0
*
* @param array $args A compacted array of wp_mail() arguments, including the "to" email,
* subject, message, headers, and attachments values.
*/
extract( apply_filters( 'wp_mail', compact( 'to', 'subject', 'message', 'headers', 'attachments' ) ) );
if ( !is_array($attachments) )
$attachments = explode( "\n", str_replace( "\r\n", "\n", $attachments ) );
global $phpmailer;
// (Re)create it, if it's gone missing
if ( !is_object( $phpmailer ) || !is_a( $phpmailer, 'PHPMailer' ) ) {
require_once ABSPATH . WPINC . '/class-phpmailer.php';
require_once ABSPATH . WPINC . '/class-smtp.php';
$phpmailer = new PHPMailer( true );
}
// Headers
if ( empty( $headers ) ) {
$headers = array();
} else {
if ( !is_array( $headers ) ) {
// Explode the headers out, so this function can take both
// string headers and an array of headers.
$tempheaders = explode( "\n", str_replace( "\r\n", "\n", $headers ) );
} else {
$tempheaders = $headers;
}
$headers = array();
$cc = array();
$bcc = array();
// If it's actually got contents
if ( !empty( $tempheaders ) ) {
// Iterate through the raw headers
foreach ( (array) $tempheaders as $header ) {
if ( strpos($header, ':') === false ) {
if ( false !== stripos( $header, 'boundary=' ) ) {
$parts = preg_split('/boundary=/i', trim( $header ) );
$boundary = trim( str_replace( array( "'", '"' ), '', $parts[1] ) );
}
continue;
}
// Explode them out
list( $name, $content ) = explode( ':', trim( $header ), 2 );
// Cleanup crew
$name = trim( $name );
$content = trim( $content );
switch ( strtolower( $name ) ) {
// Mainly for legacy -- process a From: header if it's there
case 'from':
if ( strpos($content, '<' ) !== false ) {
// So... making my life hard again?
$from_name = substr( $content, 0, strpos( $content, '<' ) - 1 );
$from_name = str_replace( '"', '', $from_name );
$from_name = trim( $from_name );
$from_email = substr( $content, strpos( $content, '<' ) + 1 );
$from_email = str_replace( '>', '', $from_email );
$from_email = trim( $from_email );
} else {
$from_email = trim( $content );
}
break;
case 'content-type':
if ( strpos( $content, ';' ) !== false ) {
list( $type, $charset ) = explode( ';', $content );
$content_type = trim( $type );
if ( false !== stripos( $charset, 'charset=' ) ) {
$charset = trim( str_replace( array( 'charset=', '"' ), '', $charset ) );
} elseif ( false !== stripos( $charset, 'boundary=' ) ) {
$boundary = trim( str_replace( array( 'BOUNDARY=', 'boundary=', '"' ), '', $charset ) );
$charset = '';
}
} else {
$content_type = trim( $content );
}
break;
case 'cc':
$cc = array_merge( (array) $cc, explode( ',', $content ) );
break;
case 'bcc':
$bcc = array_merge( (array) $bcc, explode( ',', $content ) );
break;
default:
// Add it to our grand headers array
$headers[trim( $name )] = trim( $content );
break;
}
}
}
}
// Empty out the values that may be set
$phpmailer->ClearAllRecipients();
$phpmailer->ClearAttachments();
$phpmailer->ClearCustomHeaders();
$phpmailer->ClearReplyTos();
// From email and name
// If we don't have a name from the input headers
if ( !isset( $from_name ) )
$from_name = 'WordPress';
/* If we don't have an email from the input headers default to wordpress@$sitename
* Some hosts will block outgoing mail from this address if it doesn't exist but
* there's no easy alternative. Defaulting to admin_email might appear to be another
* option but some hosts may refuse to relay mail from an unknown domain. See
* http://trac.wordpress.org/ticket/5007.
*/
if ( !isset( $from_email ) ) {
// Get the site domain and get rid of www.
$sitename = strtolower( $_SERVER['SERVER_NAME'] );
if ( substr( $sitename, 0, 4 ) == 'www.' ) {
$sitename = substr( $sitename, 4 );
}
$from_email = 'wordpress@' . $sitename;
}
/**
* Filter the email address to send from.
*
* @since 2.2.0
*
* @param string $from_email Email address to send from.
*/
$phpmailer->From = apply_filters( 'wp_mail_from', $from_email );
/**
* Filter the name to associate with the "from" email address.
*
* @since 2.3.0
*
* @param string $from_name Name associated with the "from" email address.
*/
$phpmailer->FromName = apply_filters( 'wp_mail_from_name', $from_name );
// Set destination addresses
if ( !is_array( $to ) )
$to = explode( ',', $to );
foreach ( (array) $to as $recipient ) {
try {
// Break $recipient into name and address parts if in the format "Foo <bar@baz.com>"
$recipient_name = '';
if( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) {
if ( count( $matches ) == 3 ) {
$recipient_name = $matches[1];
$recipient = $matches[2];
}
}
$phpmailer->AddAddress( $recipient, $recipient_name);
} catch ( phpmailerException $e ) {
continue;
}
}
// Set mail's subject and body
$phpmailer->Subject = $subject;
$phpmailer->Body = $message;
// Add any CC and BCC recipients
if ( !empty( $cc ) ) {
foreach ( (array) $cc as $recipient ) {
try {
// Break $recipient into name and address parts if in the format "Foo <bar@baz.com>"
$recipient_name = '';
if( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) {
if ( count( $matches ) == 3 ) {
$recipient_name = $matches[1];
$recipient = $matches[2];
}
}
$phpmailer->AddCc( $recipient, $recipient_name );
} catch ( phpmailerException $e ) {
continue;
}
}
}
if ( !empty( $bcc ) ) {
foreach ( (array) $bcc as $recipient) {
try {
// Break $recipient into name and address parts if in the format "Foo <bar@baz.com>"
$recipient_name = '';
if( preg_match( '/(.*)<(.+)>/', $recipient, $matches ) ) {
if ( count( $matches ) == 3 ) {
$recipient_name = $matches[1];
$recipient = $matches[2];
}
}
$phpmailer->AddBcc( $recipient, $recipient_name );
} catch ( phpmailerException $e ) {
continue;
}
}
}
// Everything's defined in wp-config.php
// Remove what you don't need, see https://github.com/PHPMailer/PHPMailer/blob/master/README.md
$phpmailer->IsSMTP();
$phpmailer->SMTPSecure = SMTP_SECURE; // set security schema
$phpmailer->SMTPAuth = SMTP_AUTH; // enable SMTP authentication
$phpmailer->Port = SMTP_PORT; // set the SMTP server port
$phpmailer->Host = SMTP_HOST; // SMTP server
$phpmailer->Username = SMTP_USER; // SMTP server username
$phpmailer->Password = SMTP_PASS; // SMTP server password
$phpmailer->From = SMTP_FROM; // SMTP From email address
$phpmailer->FromName = SMTP_FROM_NAME; // SMTP From name
$phpmailer->SMTPDebug = SMTP_DEBUG;
// Set Content-Type and charset
// If we don't have a content-type from the input headers
if ( !isset( $content_type ) )
$content_type = 'text/plain';
/**
* Filter the wp_mail() content type.
*
* @since 2.3.0
*
* @param string $content_type Default wp_mail() content type.
*/
$content_type = apply_filters( 'wp_mail_content_type', $content_type );
$phpmailer->ContentType = $content_type;
// Set whether it's plaintext, depending on $content_type
if ( 'text/html' == $content_type )
$phpmailer->IsHTML( true );
// If we don't have a charset from the input headers
if ( !isset( $charset ) )
$charset = get_bloginfo( 'charset' );
// Set the content-type and charset
/**
* Filter the default wp_mail() charset.
*
* @since 2.3.0
*
* @param string $charset Default email charset.
*/
$phpmailer->CharSet = apply_filters( 'wp_mail_charset', $charset );
// Set custom headers
if ( !empty( $headers ) ) {
foreach( (array) $headers as $name => $content ) {
$phpmailer->AddCustomHeader( sprintf( '%1$s: %2$s', $name, $content ) );
}
if ( false !== stripos( $content_type, 'multipart' ) && ! empty($boundary) )
$phpmailer->AddCustomHeader( sprintf( "Content-Type: %s;\n\t boundary=\"%s\"", $content_type, $boundary ) );
}
if ( !empty( $attachments ) ) {
foreach ( $attachments as $attachment ) {
try {
$phpmailer->AddAttachment($attachment);
} catch ( phpmailerException $e ) {
continue;
}
}
}
/**
* Fires after PHPMailer is initialized.
*
* @since 2.2.0
*
* @param PHPMailer &$phpmailer The PHPMailer instance, passed by reference.
*/
do_action_ref_array( 'phpmailer_init', array( &$phpmailer ) );
// Send!
try {
return $phpmailer->Send();
} catch ( phpmailerException $e ) {
return false;
}
}
}
?>
Try for yourself now: activate the plugin, launch a new and different browser and register yourself as a new user on your blog. If you look at the headers of the confirmation email you received, you'll see the relevant SMTP and StartTLS lines.
For example:
[...]
Received: from www.saotn.org (unknown [IPv6:2a00:f60::2:153])
(using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits))
(No client certificate requested)
(Authenticated sender: user@example.com)
by some_host.example.org (Postfix) with ESMTPSA id E9E74F80034
for <some_user@example.org>; Tue, 29 Jul 2014 16:05:30 +0200 (CEST)
Date: Tue, 29 Jul 2014 14:05:30 +0000
To: some_user@example.org
From: Sysadmins of the North <website@saotn.org>
Reply-To: Jan Reilink <jan@saotn.nl>
Subject: [Sysadmins of the North] Your username and password
Message-ID: <a155ef9bd228bd89330da98f1e0391bd@www.saotn.org>
X-Priority: 3
X-Mailer: PHPMailer 5.2.7 (https://github.com/PHPMailer/PHPMailer/)
Send email in WordPress using Google Gmail SMTP servers
Relay WordPress email through Gmail SMTP
Want to send email in WordPress using Google Gmail SMTP servers? Here's how to send email in WordPress using Gmail. Use the following SMTP configuration in your wp-config.php
file. You have to use SSL as SMTP_SECURE option.
define( 'SMTP_USER', 'user@gmail.com' );
define( 'SMTP_PASS', 'p4ssword' );
define( 'SMTP_PORT', '465' );
define( 'SMTP_SECURE', 'ssl' );
define( 'SMTP_AUTH', true );
define( 'SMTP_HOST', 'smtp.gmail.com' );
define( 'SMTP_DEBUG', 0 ); // for debugging purposes only set to 1 or 2
Now WordPress sends email through smtp.gmail.com as a relay.
WordPress enhancement proposal
I decided to send my WordPress enhancement to the WordPress developers trough WordPress Trac. Maybe it's a fine addition to the WordPress core. You can find my enhancement proposal here: Proposal: wp-pluggable.php patch to send email through SMTP, not mail().
wp-includes/pluggable.php patch
--- wp-includes\pluggable.orig.php Wed Jul 30 11:52:55 2014
+++ wp-includes\pluggable.php Wed Jul 30 11:48:25 2014
@@ -433,7 +433,24 @@
}
// Set to use PHP's mail()
- $phpmailer->IsMail();
+ if ( ! USE_SMTP ) {
+ $phpmailer->IsMail();
+ }
+ else {
+ $phpmailer->IsSMTP();
+ $phpmailer->SMTPSecure = SMTP_SECURE; // set security schema, tls or ssl
+ $phpmailer->SMTPAuth = SMTP_AUTH; // enable SMTP authentication
+ $phpmailer->Port = SMTP_PORT; // set the SMTP server port, 25 or 587
+ $phpmailer->Host = SMTP_HOST; // SMTP server
+ $phpmailer->Username = SMTP_USER; // SMTP server username
+ $phpmailer->Password = SMTP_PASS; // SMTP server password
+ $phpmailer->From = SMTP_FROM; // SMTP From email address
+ $phpmailer->FromName = SMTP_FROM_NAME; // SMTP From name
+ if ( SMTP_ADD_REPLYTO_EMAIL !== '' && SMTP_ADD_REPLYTO_NAME !== '' ) {
+ $phpmailer->AddReplyTo(SMTP_ADD_REPLYTO_EMAIL, SMTP_ADD_REPLYTO_NAME);
+ }
+ $phpmailer->SMTPDebug = SMTP_DEBUG; // debug level, 1, 2 or 0
+ }
// Set Content-Type and charset
// If we don't have a content-type from the input headers
wp-config.php patch
--- wp-config.orig.php Wed Jul 30 12:05:57 2014
+++ wp-config.php Wed Jul 30 12:08:18 2014
@@ -58,9 +58,17 @@
/**#@-*/
-if(!defined('WP_WINCACHE_KEY_SALT')) {
- define('WP_WINCACHE_KEY_SALT', 'wp_2389x#s_');
-}
+define('SMTP_USER', 'user@example.com');
+define('SMTP_PASS', 'p4ssword');
+define('SMTP_PORT', '25');
+define('SMTP_SECURE', 'tls');
+define('SMTP_AUTH', true);
+define('SMTP_HOST', 'smtp.example.com');
+define('SMTP_FROM', 'website@example.com');
+define('SMTP_FROM_NAME', 'e.g Website Name');
+define('SMTP_ADD_REPLYTO_NAME', 'FirstName LastName');
+define('SMTP_ADD_REPLYTO_EMAIL', 'userName@example.org');
+define('SMTP_DEBUG', 0); // for debugging purposes only set to 1 or 2
/**
* WordPress Database Table prefix.
Note: the WP_WINCACHE_KEY_SALT
is specific to my set up.