Async Your SMTP

While working on an ASP.NET MVC site, I had a scenario where I needed to send an e-mail using smtp. In my case, it was to confirm an e-mail address in the usual registration fashion. It was at this time that I noticed the SendAsync method of the SmtpClient, which sounded like a great fit. I added the code only to get the below nastiness for my troubles.

System.InvalidOperationException: Asynchronous operations are not allowed in this context. Page starting an asynchronous operation has to have the Async attribute set to true and an asynchronous operation can only be started on a page prior to PreRenderComplete event.

at System.Web.LegacyAspNetSynchronizationContext.OperationStarted()
at System.ComponentModel.AsyncOperation.CreateOperation(Object userSuppliedState, SynchronizationContext syncContext)
at System.Net.Mail.SmtpClient.SendAsync(MailMessage message, Object userToken)

The issue here has to do with the methodology of ASP.NET. Each request returns a response, period. To do anything asynchronously, you will need to use a different thread or process from the one currently servicing the request (or you could do it synchronously, but who wants that?). I developed the below code which draws heavily from information in this Stack Overflow question.

Note the below example uses log4net for logging.

        public static void Send(string recipients, string subject, string body)
        {
            Task.Factory.StartNew(() =>
                {
                    var from = ConfigurationManager.AppSettings["noreplyemail"].ToString();
                    Log.DebugFormat("Sending an e-mail to [{0}] from [{1}] with the subject [{2}] and body [{3}].", recipients, from, subject, body);
                    Send(new MailMessage(from, recipients, subject, body));
                });
        }

        public static void Send(MailMessage message)
        {
            var port = int.Parse(ConfigurationManager.AppSettings["SMTPPort"].ToString());
            var host = ConfigurationManager.AppSettings["SMTPserver"].ToString();
            var username = ConfigurationManager.AppSettings["SMTPusername"].ToString();
            var password = ConfigurationManager.AppSettings["SMTPpassword"].ToString();
            var smtpClient = new SmtpClient(host, port);
            smtpClient.UseDefaultCredentials = string.IsNullOrWhiteSpace(username) && string.IsNullOrWhiteSpace(password);
            smtpClient.Credentials = new NetworkCredential(username, password);

            smtpClient.SendCompleted += new SendCompletedEventHandler(SendCompletedCallback);
            smtpClient.SendCompleted += (s, e) =>
            {
                smtpClient.Dispose();
                message.Dispose();
            };

            smtpClient.SendAsync(message, null);
        }

        private static void SendCompletedCallback(object sender, AsyncCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                Log.Error(e.Error);
            }
            else
            {
                Log.InfoFormat("The email message was successfully sent.");
            }
        }

There are some gotcha’s to keep in mind with disposing. Don’t dispose too early or else your message won’t be sent. I learned this the hard way after wrapping the SmtpClient and the MailMessage in using statements which did what they were supposed to (i.e. cleaning up any resources), just before I wanted them to.

You will notice that I chose to use a Task here. I chose to do this mainly for the sake of simplicity. One option to consider is using the WebBackgrounder for these kinds of tasks. In my case, this was overkill since I was only sending e-mail and occasionally at that. In other cases, it may very well be a good fit.

I hope this provides a more complete example to this problem than I could find myself.


 

Warning: Unsolicited Advice

If you are about to embark upon sending e-mail with smtp, I highly recommend that you take the time to learn how to debug it at a low level. You should be able to telnet into your smtp server and send e-mails that way. You should be able to sniff your network traffic with Wireshark, which allows you to even filter your traffic for the smtp protocol. Without this knowledge, you are guaranteed to have a difficult time troubleshooting any issues you encounter.