We recently discovered that Gmail doesn’t play nicely with emails that are sent from our Azure instance (which is hosting our WordPress sites). More specifically, Gmail users don’t receive the emails sent to them. Because of this, we have developed an API that resides on a local, on-campus server, wsnet2 to send out WordPress emails. An alternative solution would be something like SendGrid but that would require each office to create their own account as well as potentially have to pay if they need to send more than 100 emails per day. This file/plugin will override WordPress’s “wp_mail()” function to call the API instead of using the built-in SMTP service.
Caller WordPress/PHP Code
<?php
/**
* @package dsawpmailoverride
*/
/*
Plugin Name: DSA Wp_mail Override
Plugin URI: https://studentaffairs.colostate.edu
Description: Have WordPress send e-mail to wsnet2 API
Version: 0.9
Author: Steve Garten & Shaun Geisert
Author URI: https://studentaffairs.colostate.edu
License: GPLv2 or later
Text Domain: dsa
*/
/*
* Description:
* Gmail doesn't play nicely with emails that are sent from our Azure instance (that is to say, Gmail
* users don't receive the emails sent to them). Because of this, we have developed an API that resides
* on wsnet2 (an on-campus server) to send out WordPress emails. An alternative solution would be
* something like SendGrid but that would require each office to create their own account as well as
* potentially have to pay if they need to send more than 100 emails per day. This file/plugin will
* override WordPress's "wp_mail()" function to call the API instead of using the built-in SMTP service.
*
* References:
* https://developer.wordpress.org/reference/functions/wp_mail/
* https://codex.wordpress.org/Pluggable_Functions
* https://wordpress.stackexchange.com/questions/328681/how-to-use-a-3rd-party-library-to-send-emails
* https://stackoverflow.com/questions/15944824/search-a-php-array-for-partial-string-match
*/
// No direct access
defined( 'ABSPATH' ) or die();
// Ensure another "wp_mail()" override doesn't exist
if ( !function_exists("wp_mail") ) {
// Define wp_mail()
function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) {
$log = true;
// Append auth token to $headers
$auth_token = 'AUTH_TOKEN_HERE';
$headers["Authorization"] = 'Bearer ' . $auth_token;
// Change content-type
$headers["Content-type"] = 'application/json';
// Massage "from" data
$from = $headers["From"];
// If the $headers provided to us was not an associative array, we'll have to find the "from" value from the normal array
if( $from == '' ) {
$from = array_filter($headers, function($value) { return strpos($value, 'From:') !== false; });
$from = $from[0];
}
$from = str_replace("From:", "", $from); // Remove "From: "
$fromName = ''; // Default to empty
$hasFromName = str_contains($from, "<"); // Boolean to determine if a from name was given
// Handle "from" data differently if a "from name" was given
if( $hasFromName ) {
$fromArray = explode("<", $from); // Split into an array based on "<" character
$fromName = $fromArray[0]; // Assign the first half to be the fromName
$from = $fromArray[1]; // Assign the second half to be the from (address)
$from = str_replace(">", "", $from); // Trim the close email address bracket
}
// Build body
$bodyArray = [];
$bodyArray["from"] = $from;
$bodyArray["fromName"] = $fromName;
$bodyArray["to"] = $to;
$bodyArray["subject"] = $subject;
$bodyArray["body"] = $message;
// Log
if( $log ) {
wp_mail_debug_logger("TO -> " . json_encode($to) . " | FROM -> " . json_encode($from) . " | FROMNAME -> " . $fromName . " | SUBJECT -> " . $subject . " | BODY -> " . $message );
}
// Encode as JSON
$bodyJson = json_encode($bodyArray);
// Organize data
$args = array(
'headers' => $headers,
'body' => $bodyJson,
'timeout' => 30 // Have to increase timeout as the API endpoint is occasionally idle after a period of time
);
// Submit POST to wsnet2 API
$mail_post = wp_remote_post( 'https://endpoint_here/emailer.asmx/Send', $args );
/**************************************************/
/* Alert Us if Attachment Was Included in Email */
/**************************************************/
if ( $attachments ) {
// Build body
$notify_bodyArray = [];
$notify_bodyArray["from"] = 'dsaweb@colostate.edu';
$notify_bodyArray["fromName"] = 'dsaweb';
$notify_bodyArray["to"] = 'dsaweb@colostate.edu';
$notify_bodyArray["subject"] = 'WARNING: WORDPRESS EMAIL ATTACHMENT SENT';
$notify_bodyArray["body"] = 'An email was sent from WordPress that contained an attachment. As of 10/6/22, support for attachments is not supported.';
// Encode as JSON
$notify_bodyJson = json_encode($notify_bodyArray);
// Organize data
$notify_args = array(
'headers' => $headers,
'body' => $notify_bodyJson,
'timeout' => 30
);
// Submit POST to wsnet2 API
wp_remote_post( 'https://endpoint_here/emailer.asmx/Send', $notify_args );
}
// Return status
if ( is_wp_error( $mail_post ) || wp_remote_retrieve_response_code( $mail_post ) != 200 ) {
if( $log ) {
wp_mail_debug_logger( "POST ERROR -> " . json_encode($mail_post) );
}
return false;
} else {
return true;
}
}
}
/************************************/
/* HELPER: LOGGING */
/************************************/
function wp_mail_debug_logger( $msg ) {
$log_file = plugin_dir_path( __FILE__ ) . 'debug.txt';
date_default_timezone_set('America/Denver');
$log = fopen( $log_file, 'a' ) or die ('fopen failed');
$log_string = "\n" . date("Y-m-d H:i:s") . " :: " . $msg;
fwrite( $log, $log_string ) or die ('fwrite failed');
fclose( $log );
}
Receiver C# Asmx Web Service
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Mail;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Caching;
using System.Web.Script.Services;
using System.Web.Services;
using DSAListServ.Entities;
using DSAListServ.Services;
using DSAListServ.Services.Helper;
/// <summary>
/// This is a bare bones implementation of a web service that calls our on-campus SMTP mail server (using very basic, static token security).
///
/// References:
/// https://wordpress.stackexchange.com/questions/328681/how-to-use-a-3rd-party-library-to-send-emails
/// https://social.msdn.microsoft.com/Forums/en-US/c7356008-d340-4c29-be14-341913f7b483/ajax-with-authentication-header-and-asmx-webservice?forum=aspdotnetjquery
/// </summary>
[WebService(Namespace = "DSAListServEmailer")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.
[ScriptService]
[GenerateScriptType(typeof(MailRecipient))]
public class EmailerApi : WebService
{
string validToken = "Bearer TOKEN_HERE";
string[] validCallerIPs = { "52.162.194.79", "20.88.13.205", "129.82.103.32", "::1" }; // not currently using, but may down the road
bool enableDBLogging = true;
bool preventDuplicates = true;
int hoursToExcludeDuplicates = 168;
public EmailerApi()
{
}
private bool IsTokenValid(string suppliedToken)
{
if (suppliedToken == validToken)
return true;
else
return false;
}
/// <summary>
/// Function which could be used to whitelist the calling server's IP
/// </summary>
/// <param name="callerIP"></param>
/// <returns></returns>
private bool IsCallerIPValid(string callerIP)
{
foreach (string ip in validCallerIPs)
{
if (callerIP == ip)
return true;
}
return false;
}
/// <summary>
/// Custom code to log a message to our database
/// </summary>
/// <param name="from"></param>
/// <param name="to"></param>
/// <param name="subject"></param>
/// <param name="body"></param>
/// <param name="emailResult"></param>
private void LogMessageToDB(string from, string to, string subject, string body, string emailResult)
{
// log to db
BulkEmailLog log = new BulkEmailLog();
log.PersonId = 1; // this is Shaun Geisert
log.FromEmail = from;
log.ToEmail = to;
log.Subject = subject;
var cleanedBody = body.StripHTML();
cleanedBody = Regex.Replace(cleanedBody, @"\s+", "");
log.Body = cleanedBody;
//log.Body = body;
log.EmailResult = emailResult;
log.SentDate = DateTime.Now;
BulkEmailLogService.CreateBulkEmailLog(log);
}
/// <summary>
/// Get all messages from the system, leveraging hourly cache
/// </summary>
/// <returns></returns>
private TList<BulkEmailLog> GetAllMessages()
{
HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.Server);
HttpContext.Current.Response.Cache.VaryByParams.IgnoreParams = true;
HttpContext.Current.Response.Cache.SetExpires(DateTime.Now.AddMinutes(5));
BulkEmailLogService s = new BulkEmailLogService();
// get all messages sent in system
TList<BulkEmailLog> messages;
// pull messages from cache, if possible
if (Context.Cache["AllBulkEmails"] == null)
{
messages = s.GetAll();
foreach (BulkEmailLog message in messages)
{
var cleanedBody = message.Body.StripHTML();
cleanedBody = Regex.Replace(cleanedBody, @"\s+", "");
message.Body = cleanedBody;
}
Context.Cache.Insert("AllBulkEmails", messages, null, DateTime.Now.AddMinutes(5), TimeSpan.Zero);
}
else
{
messages = (TList<BulkEmailLog>)Context.Cache["AllBulkEmails"];
}
return messages;
}
private bool WasMessageAlreadySent(string from, string to, string subject, string body, int hours)
{
hours = hours * -1;
var cleanedBody = body.StripHTML();
cleanedBody = Regex.Replace(cleanedBody, @"\s+", "");
// get all messages sent in system
TList<BulkEmailLog> messages = GetAllMessages();
// narrow it down to those sent in the last week
messages = messages.FindAll(delegate (BulkEmailLog r) { return r.SentDate > DateTime.Now.AddHours(hours); });
// further narrow it down where the from, to, subject, and body match
messages = messages.FindAll(delegate (BulkEmailLog r)
{
return
(r.FromEmail.ToUpper() == from.ToUpper())
&& (r.ToEmail.ToUpper() == to.ToUpper())
&& (r.Subject.ToUpper() == subject.ToUpper())
&& (r.Body == cleanedBody);
});
return (messages.Count > 0);
}
/// <summary>
/// Web method: Post to https://endpoint_here/emailer.asmx/Send
/// </summary>
/// <param name="from"></param>
/// <param name="fromName"></param>
/// <param name="to"></param>
/// <param name="subject"></param>
/// <param name="body"></param>
[WebMethod, ScriptMethod(UseHttpGet = false, ResponseFormat = ResponseFormat.Json)]
public void Send(string from, string fromName, string to, string subject, string body)
{
var token = HttpContext.Current.Request.Headers["Authorization"];
var host = HttpContext.Current.Request.UserHostAddress;
//if ((!IsTokenValid(token)) || (!IsCallerIPValid(host)))
if (!IsTokenValid(token))
{
HttpContext.Current.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
HttpContext.Current.Response.Write("Unauthorized");
if (enableDBLogging)
LogMessageToDB(from, to, subject, body, HttpContext.Current.Response.StatusCode.ToString());
return;
}
if (WasMessageAlreadySent(from, to, subject, body, hoursToExcludeDuplicates))
{
HttpContext.Current.Response.StatusCode = (int)HttpStatusCode.BadRequest;
HttpContext.Current.Response.Write("Duplicate Message Already Sent");
return;
}
try
{
List<MailRecipient> recipients = new List<MailRecipient>();
if (to.Contains(","))
{
List<string> emails = new List<string>(to.Split(','));
foreach (string email in emails)
{
MailRecipient recipient = new MailRecipient();
recipient.Email = email;
recipient.Message = body;
recipients.Add(recipient);
}
}
else
{
MailRecipient recipient = new MailRecipient();
recipient.Email = to;
recipient.Message = body;
recipients.Add(recipient);
}
MailAddress fromAddress = new MailAddress(from, fromName);
// have a placeholder to track success/failures
List<MailRecipient> mailedRecipients = new List<MailRecipient>();
EmailHelper mailer = new EmailHelper();
mailedRecipients = mailer.StartEmailRunApi(fromAddress, recipients, subject, true, MailPriority.Normal, false, false);
int currentCount = 0;
string mailResultsSuccess = string.Empty;
string mailResultsFailed = string.Empty;
int countRecipients = mailedRecipients.Count;
foreach (MailRecipient recipient in mailedRecipients)
{
currentCount++;
if (recipient.WasEmailed)
mailResultsSuccess += String.Format(@"<span style=""color:green"">{0}/{1} - Sent email to: {2}</span><br>", currentCount, countRecipients, recipient.Email);
else
mailResultsFailed += String.Format(@"<span style=""color:red"">{0}/{1} - Failed to send email to: {2} with exception: {3}</span><br>", currentCount, countRecipients, recipient.Email, recipient.Error);
}
mailResultsSuccess += "Finished<br />";
HttpContext.Current.Response.StatusCode = (int)HttpStatusCode.OK;
HttpContext.Current.Response.Write("OK");
if (enableDBLogging)
LogMessageToDB(from, to, subject, body, mailResultsSuccess + "<br>" + mailResultsFailed);
return;
}
catch (HttpException ex)
{
HttpContext.Current.Response.StatusCode = (int)ex.GetHttpCode();
HttpContext.Current.Response.Write("InternalServerError");
if (enableDBLogging)
LogMessageToDB(from, to, subject, body, HttpContext.Current.Response.StatusCode.ToString());
return;
}
}
}