in the end i wrote a lightweight alternative approach using a thread in a while loop to monitor a folder for text files containing a serialized object with the email parameters. it has almost no overhead and complies with the office365 throttling policy. posting here in case it is of use to anyone else, built for asp.net and should be easy to adapt for other scenarios.
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Web.Hosting;
namespace Whatever
{
public class EmailerThread
{
public delegate void Worker();
private static Thread worker; // one worker thread for this application // https://stackoverflow.com/questions/1824933/right-way-to-create-thread-in-asp-net-web-application
public static string emailFolder;
public static int ScanIntervalMS = 2000; // office365 allows for 30 messages in a 60 second window, a 2 second delay plus the processing time required to connect & send each message should safely avoid the throttling restrictions.
/// <summary>
/// Must be invoked from Application_Start to ensure the thread is always running, if the applicationpool recycles etc
/// </summary>
public static void Init()
{
// create the folder used to store serialized files for each email to be sent
emailFolder = Path.Combine(HostingEnvironment.ApplicationPhysicalPath, "App_Data", "_EmailOutbox");
Directory.CreateDirectory(emailFolder);
worker = new Thread(new ThreadStart(new Worker(ScanForEmails)));
worker.Start();
}
/// <summary>
/// Serialize an object containing all the email parameters to a text file
/// Call this object
/// </summary>
/// <param name="e"></param>
public static void QueueEmail(EmailParametersContainer e)
{
string filename = Guid.NewGuid().ToString() + ".txt";
File.WriteAllText(Path.Combine(emailFolder, filename), JsonConvert.SerializeObject(e));
}
public static void ScanForEmails()
{
var client = new System.Net.Mail.SmtpClient(Settings.SmtpServer, 587);
client.EnableSsl = true;
client.UseDefaultCredentials = false;
client.DeliveryMethod = SmtpDeliveryMethod.Network;
client.Credentials = new System.Net.NetworkCredential(Settings.smtpUser, Settings.smtpPass);
client.Timeout = 5 * 60 * 1000; // 5 minutes
// infinite loop to keep scanning for files
while (true)
{
// take the oldest file in the folder and process it for sending
var nextFile = new DirectoryInfo(emailFolder).GetFiles("*.txt", SearchOption.TopDirectoryOnly).OrderBy(z => z.CreationTime).FirstOrDefault();
if (nextFile != null)
{
// deserialize the file
EmailParametersContainer e = JsonConvert.DeserializeObject<EmailParametersContainer>(File.ReadAllText(nextFile.FullName));
if (e != null)
{
try
{
MailMessage msg = new MailMessage();
AddEmailRecipients(msg, e.To, e.CC, e.BCC);
msg.From = new MailAddress(smtpUser);
msg.Subject = e.Subject;
msg.IsBodyHtml = e.HtmlFormat;
msg.Body = e.MessageText;
if (e.FilePaths != null && e.FilePaths.Count > 0)
foreach (string file in e.FilePaths)
if (!String.IsNullOrEmpty(file) && File.Exists(file))
msg.Attachments.Add(new Attachment(file));
client.Send(msg);
msg.Dispose();
// delete the text file now that the job has successfully completed
nextFile.Delete();
}
catch (Exception ex)
{
// Log the error however suits...
// rename the .txt file to a .fail file so that it stays in the folder but will not keep trying to send a problematic email (e.g. bad recipients or attachment size rejected)
nextFile.MoveTo(nextFile.FullName.Replace(".txt", ".fail"));
}
}
}
Thread.Sleep(ScanIntervalMS); // wait for the required time before looking for another job
}
}
/// <summary>
///
/// </summary>
/// <param name="msg"></param>
/// <param name="Recipients">Separated by ; or , or \n or space</param>
public static void AddEmailRecipients(MailMessage msg, string To, string CC, string BCC)
{
string[] list;
if (!String.IsNullOrEmpty(To))
{
list = To.Split(";, \n".ToCharArray());
foreach (string email in list)
if (email.Trim() != "" && ValidateEmail(email.Trim()))
msg.To.Add(new MailAddress(email.Trim()));
}
if (!String.IsNullOrEmpty(CC))
{
list = CC.Split(";, \n".ToCharArray());
foreach (string email in list)
if (email.Trim() != "" && ValidateEmail(email.Trim()))
msg.CC.Add(new MailAddress(email.Trim()));
}
if (!String.IsNullOrEmpty(BCC))
{
list = BCC.Split(";, \n".ToCharArray());
foreach (string email in list)
if (email.Trim() != "" && ValidateEmail(email.Trim()))
msg.Bcc.Add(new MailAddress(email.Trim()));
}
}
public static bool ValidateEmail(string email)
{
if (email.Contains(" ")) { return false; }
try
{
// rely on the .Net framework to validate the email address, rather than attempting some crazy regex
var m = new MailAddress(email);
return true;
}
catch
{
return false;
}
}
}
public class EmailParametersContainer
{
public string To { get; set; }
public string Cc { get; set; }
public string Bcc { get; set; }
public string Subject { get; set; }
public string MessageText { get; set; }
public List<string> FilePaths { get; set; }
public bool HtmlFormat { get; set; }
}
}