My musings about .NET and what not

If At First You Don’t Succeed - Retrying Mail Operations in .NET

Mail sent from your application didn’t go through? Don’t give up so easily! The majority of mail server interruptions are very temporary in nature, lasting only a few seconds. Instead of failing right away, why not give your SMTP client another shot?


As I’m sure you’re very aware, sending email from applications is an extremely common task. For example, just about every web site has a “Contact Us’ page of some sort. Yours are probably much prettier that the screenshot below, but the idea is the same.

If you only need to send mail from this one place, you might stick the required code directly in the code-behind of your contact page. However, most applications need to send mail for all kinds of reasons - to confirm new member signups, subscription requests, password changes and resets, and various other notifications based on your business rules. Therefore, it’s much more convenient to encapsulate the mail-sending logic and just call it from wherever you need to, as below.

protected void btnSend_Click(object sender, EventArgs e)
{
   if (IsValid)
   {
      try
      {
         Email.Send(txtName.Text, txtEmail.Text, txtSubject.Text, txtBody.Text);         
         lblResult.Text = "Your mail was sent.";         
      }
      catch (SmtpException)
      {
         lblResult.Text = "An error was encountered while sending your mail.";
      }
   }
}

You’ll note that the page method above handles SmtpException, not Exception. Regular readers of my blog know full well that I’m not a fan of general catch blocks. In this case, if you properly validate your input, the possibility of every exception that can be thrown by the SmptClient.Send method can be eliminated except this one. Therefore, there is no reason to handle any other exception type.

The typical Send method is usually nothing very fancy. Below, we present a common implementation. The address to which the message is to be sent is encapsulated in the web.config file.


   

 

/// 
/// Provides a method for sending email.
/// 
public static class Email
{
   /// 
   /// Constructs and sends an email message.
   /// 
   /// The display name of the person the email is from.
   /// The email address of the person the email is from.
   /// The subject of the email.
   /// The body of the email.
   public static void Send(string fromName, string fromEmail, string subject, string body)
   {
      MailMessage message = new MailMessage
                               {
                                  IsBodyHtml = false,
                                  From = new MailAddress(fromEmail, fromName),
                                  Subject = subject,
                                  Body = body
                               };
      message.To.Add(WebConfigurationManager.AppSettings["mailToAddress"]);

      new SmtpClient().Send(message);
   }
}

Here, if there is a problem of any kind at the SMTP server end, the server reports an error, which the runtime wraps in a SmtpException. While this code works perfectly well, its functionality is limited in that it makes no effort to determine exactly how the send failed.

Why is that important? Well, sometimes the mail server really is experiencing a serious issue, and of course, there’s nothing you can do about that. But very often, when a SMTP server reports an error, the actual problem is not with the server itself but with an individual mailbox, and mailbox problems are usually quite fleeting. The most common cause of mailbox issues are as follows:

  • The destination mailbox is currently in use. This condition, if it occurs, usually lasts a very short time.
  • The mailbox is unavailable. This may mean that there is actually no such address on the server, but it may also mean that the search for the mailbox simply timed out.
  • The transaction with the mailbox failed. The reasons for this can be somewhat mysterious; but like a stubborn web page, sometimes a second nudge is all it takes to do the trick.

This tells us that we could gracefully recover from the majority of email send failures by detecting these particular conditions and attempting the send a second time.

Of course, if the second send also happens to fail, then it’s very likely that the problem is not temporary or recoverable, in which case there isn’t any point in continuing to try. When that happens, we can allow the exception to bubble up to the calling page as it normally would.

Retrying Mail Operations

Problems with a mailbox are not easily discovered by catching SmtpException. Fortunately, there is a more derived type called SmtpFailedRecipientException that the .NET Framework uses to wrap errors reported from an individual mailbox. This exception contains a StatusCode property of type enum that will tell us the exact cause of the error.

using System.Net.Mail;
using System.Threading;
using System.Web.Configuration;

/// 
/// Provides a method for sending email.
/// 
public static class Email
{
   /// 
   /// Constructs and sends an email message.
   /// 
   /// The display name of the person the email is from.
   /// The email address of the person the email is from.
   /// The subject of the email.
   /// The body of the email.
   public static void Send(string fromName, string fromEmail, string subject, string body)
   {
      MailMessage message = new MailMessage
                               {
                                  IsBodyHtml = false,
                                  From = new MailAddress(fromEmail, fromName),
                                  Subject = subject,
                                  Body = body
                               };
      message.To.Add(WebConfigurationManager.AppSettings["mailToAddress"]);

      Send(message);
   }

   private static void Send(MailMessage message)
   {
      SmtpClient client = new SmtpClient();

      try
      {
         client.Send(message);
      }
      catch (SmtpFailedRecipientException ex)
      {
         SmtpStatusCode statusCode = ex.StatusCode;

         if (statusCode == SmtpStatusCode.MailboxBusy ||
             statusCode == SmtpStatusCode.MailboxUnavailable ||
             statusCode == SmtpStatusCode.TransactionFailed)
         {              
            // wait 5 seconds, try a second time
            Thread.Sleep(5000);
            client.Send(message);
         }
         else
         {
            throw;
         }
      }
      finally
      {
         message.Dispose();
      }
   }
}

With this code, we’re catching the SmtpFailedRecipientException that is thrown as a result of the mailbox error, and examining its StatusCode before actually handling the exception. If the reported error is due to the mailbox being busy or unavailable, or the transaction fails, we wait five seconds before trying the send one more time. If the error is due to any other reason, the exception is passed unhandled back to the caller, and subsequently back to the page, where it will be handled as its base type. A failure on the second send will also cause the exception to propagate back to the page.

Retrying Mail Operations with Multiple Recipients

The code example I just presented works beautifully, as long as the mail is intended for a single destination address. However, when there are multiple recipients involved, there are problems with this implementation.

When you’re dealing with multiple addresses, the number of failure scenarios increases. It is possible that all of the mailboxes return errors, and those errors may be different for each mailbox. You may have a situation where the mail is sent successfully to some mailboxes and not to others. You may also have a situation where only a single mailbox out of the group fails.

One problem with our current Send method is that the error handling kicks in if one or more mailboxes fail, but the message is resent to everyone – even to those who successfully received it the first time around. That means all the “good” address are going to get the message twice – definitely not cool. Therefore, when working with multiple recipients, we need a way to determine which specific mailboxes failed and why, and then attempt to resend to only those mailboxes.

Furthermore, we need to consider the semantic meaning of the term “failure” from the client standpoint. If a contact form is configured to be sent to three mailboxes and all three mailboxes fail, then obviously the form should indicate to the user that the send was not successful. But what should happen if two mailboxes succeed, and only one does not? Is that still considered a “fail?” Should we fail on any mailbox error, or should the send be considered successful if the message makes it through to at least one mailbox? This is something only you can determine, based on your own requirements and the business rules. Either way though, we need to be sure to incorporate the desired logic into the Send method.

Here is how we’ll do that.

    
   
   

 

using System;
using System.Net.Mail;
using System.Threading;
using System.Web.Configuration;

/// 
/// Provides a method for sending email.
/// 
public static class Email
{
   /// 
   /// Constructs and sends an email message.
   /// 
   /// The display name of the person the email is from.
   /// The email address of the person the email is from.
   /// The subject of the email.
   /// The body of the email.
   public static void Send(string fromName, string fromEmail, string subject, string body)
   {
      MailMessage message = new MailMessage
                               {
                                  IsBodyHtml = false,
                                  From = new MailAddress(fromEmail, fromName),
                                  Subject = subject,
                                  Body = body
                               };
      message.To.Add(WebConfigurationManager.AppSettings["mailToAddresses"]);

      int originalRecipientCount = message.To.Count;
      bool failOnAnyAddress = Convert.ToBoolean(WebConfigurationManager.AppSettings["failOnAnyAddress"]);

      try
      {
         Send(message);
      }
      catch (SmtpFailedRecipientException)
      {
         if (message.To.Count == originalRecipientCount)
         {
            // all recipients failed
            throw;
         }

         if (failOnAnyAddress)
         {
            // some (not ALL) recipients failed
            throw;
         }
      }
   }

   private static void Send(MailMessage message)
   {
      SmtpClient client = new SmtpClient();

      try
      {
         client.Send(message);
      }
      catch (SmtpFailedRecipientsException ex)
      {
         // multiple fail
         message.To.Clear();

         foreach (SmtpFailedRecipientException sfrEx in ex.InnerExceptions)
         {
            CheckStatusAndReaddress(message, sfrEx);
         }

         if (message.To.Count > 0)
         {
            // wait 5 seconds, try a second time
            Thread.Sleep(5000);
            client.Send(message);
         }
         else
         {
            throw;
         }
      }
      catch (SmtpFailedRecipientException ex)
      {
         // single fail
         message.To.Clear();

         CheckStatusAndReaddress(message, ex);
         
         if (message.To.Count > 0)
         {
            // wait 5 seconds, try a second time
            Thread.Sleep(5000);
            client.Send(message);
         }
         else
         {
            throw;
         }
      }
      finally
      {
         message.Dispose();
      }
   }

   private static void CheckStatusAndReaddress(
      MailMessage message,
      SmtpFailedRecipientException exception)
   {
      SmtpStatusCode statusCode = exception.StatusCode;

      if (statusCode == SmtpStatusCode.MailboxBusy ||
          statusCode == SmtpStatusCode.MailboxUnavailable ||
          statusCode == SmtpStatusCode.TransactionFailed)
      {
         message.To.Add(exception.FailedRecipient);
      }
   }
}

A limitation of the SmtpFailedRecipientException is that even if there are multiple failed recipient mailboxes in a single message, only the first one in the Message.To property (of type MailAddressCollection) is reported. To work with multiple mailbox errors, we need to work with the SmtpFailedRecipientsException, which is an even more derived type .NET uses to collect errors returned by more than one mailbox in a single message. (Note the extra ‘S’ in there: SmtpFailedRecipientsException.)

The inheritance hierarchy goes like this:

  • SmtpException
    • SmtpFailedRecipientException 
      • SmtpFailedRecipientsException

Our revised method now uses two exception handlers. The first (most specific) one catches SmtpFailedRecipientsException, which is thrown when two or more bad recipients are encountered. First, all of the original recipients are cleared from the outgoing message. Then, we loop through the InnerExceptions of the SmtpFailedRecipientsException. The InnerExceptions property holds an array of SmtpFailedRecipientException[], each one representing one of the mailboxes that failed.

The StatusCode of each failed recipient is checked in a separate private method called CheckStatusAndReaddress. If the StatusCode matches one of the defined failure criteria, that address is added back to the To property of the message. Then, if at least one “retryable” mailbox was added, the send is retried. Note that by using this logic, no “good” addresses are being pinged twice.

The second exception handler catches SmtpFailedRecipientException, which is thrown when one and only one bad mailbox exists. The logic here is very much the same as in the first catch block, except this time there is no need to loop through an array of failed mailboxes, since there is only one.

Finally, if there were failures on the second send, the public Send method “pre-screens” the resulting exception before letting it bubble back up to the client UI. In the case where all the mailboxes failed, the exception is rethrown. In the case where some, but not all, of the mailboxes failed, the exception is rethrown only if our failOnAnyAddress configuration attribute is true. If it is set to false, the exception is swallowed, and the contact form reports the send operation as successful to the user.

Summary

In this blog post, you learned about some of the causes of SMTP mailbox failures, and the importance of checking to determine if it is worth our while to attempt to resend the message under certain failure conditions.

You learned how to implement logic that allows you to retry the mail operation when there is a single recipient. You then refined this logic to cover situations in which multiple recipients are involved. This improved method covers all possible outcomes: a single recipient failure, the failure of all recipients, or a “mix” of failed and successful recipients. You also learned how to incorporate logic to indicate under which conditions a batch send should be reported as “successful” by the client application.

One important note: if you use error logging in your application, you’ll immediately see the added benefit of this implementation. Instead of merely logging that some mysterious mail server error occurred, you can trap and log exceptions at the mailbox level, and even report which specific addresses are causing trouble, and why. It would be easy to modify the implementation above to incorporate such enhanced logging functionality.

Subscribe to this blog for more cool content like this!

kick it on DotNetKicks.com

shout it on DotNetShoutOut.com

vote it on WebDevVote.com

Bookmark / Share

    » Similar Posts

    1. Integrating Exception Handling Into the Development Cycle
    2. Defensive Programming, or Why Exception Handling Is Like Car Insurance
    3. Should We Return Null From Our Methods?

    » Trackbacks & Pingbacks

    1. Thank you for submitting this cool story - Trackback from DotNetShoutout

      If At First You Don’t Succeed - Retrying Mail Operations in .NET — June 23, 2009 12:10 PM
    2. Pingback from Dew Drop – June 24, 2009 | Alvin Ashcraft's Morning Dew

      Dew Drop – June 24, 2009 | Alvin Ashcraft's Morning Dew — June 24, 2009 7:39 AM
    3. Late last month I made a post about testing E-Mail functionality with Donovan Brown’s Neptune . Several

      Automated Testing E-Mail with Neptune — July 30, 2009 9:20 PM
    4. DotNetBurner - burning hot .net content

      If At First You Don’t Succeed - Retrying Mail Operations in .NET : LeeDumond.com — September 7, 2009 8:21 AM
    5. Pingback from If At First You Don’t Succeed – Retrying Mail Operations in .NET « Ramani Sandeep

      If At First You Don’t Succeed – Retrying Mail Operations in .NET « Ramani Sandeep — September 21, 2009 5:18 AM
    6. The time to start thinking about exception handling is right after you click File > New Project. Exception handling shouldn't be something you "tack-on" to an application after the fact. Here, I discuss a practical approach to incorporating

    Trackback link for this post:
    http://leedumond.com/trackback.ashx?id=67

    » Comments

    1. Jef Claes avatar

      Quality post!

      Jef Claes — June 24, 2009 6:16 AM
    2. Lee Dumond avatar

      Thank you Jeff!

      Lee Dumond — June 24, 2009 8:36 AM
    3. anon avatar

      Hi,

      I can't receive failed messages with ms-exchange server.

      What was your mail server?

      anon — July 1, 2009 5:47 AM
    4. travispuk avatar

      Lee,

      I think that there is an issue in this approach in that the ReAddress function will essentially break a conversation. What I mean by that is that if you have sent a message to three recips, one or two fail, so this function readdresses the failed recips and sends/retries directly to those failed recips. However in order to not double send to the successful recips, it strips them out (nice method though, hope I have understood that correctly). The issue that creates I think is that the new readdressed recip can no longer 'reply all' to an email as it would technically not include the other recips.

      Hope that makes sense.

      Travis

      travispuk — July 1, 2009 6:22 AM
    5. Lee Dumond avatar

      @anon - I have used this successfully with mail servers in IIS 5.1, 6, and 7.

      @Travis - You have a point there. But if those addresses are genuinely "bad", what would be the purpose of including them in a reply-all?

      Here at least, the "good" recipients will be able to readily see that there are "missing" addresses, and can include them manually if desired.

      Lee Dumond — July 1, 2009 9:45 AM
    6. frinkfr avatar

      Awesome detail on the recovery attempts for mail message send failure! The ability to pinpoint bad recipient addresses will be greatly appreciated by my users. Thanks

      frinkfr — August 13, 2009 4:52 PM
    7. Sayitfast avatar

      Great info!!! Very nice approach.

      Sayitfast — September 6, 2009 10:44 PM
    8. Satheesh avatar

      Good post!! On "Article of the day" in www.asp.net today!!

      Satheesh — September 7, 2009 12:26 AM
    9. Rajeev avatar

      Good Post. SmtpClinet is nicely explored here.

      Rajeev — September 7, 2009 2:17 AM
    10. rob avatar

      Great article! I read it on: thatstoday.com/.../if-at-first-you

      rob — September 7, 2009 4:12 AM
    11. Riza Marhaban avatar

      Hi Mr. Lee, nice post. I'm new in learning sending email from web forms. Is this how we should send an email from a forms? especially if failed on first attempt sending.

      Currently, this is what I do, I make a separate class which is always monitor the email outbox database in the backgound every seconds after the FlushEmail() was called. The class is using simple threading, a collection object and so it will send on the fly if I add (inject) some list of messages to the collection. It will loop sending inside the threading several times until a sending is success. Than I remove from the collection and update the database status as success. The other email in the collection is queue. Well it depends on how much recipients.

      While trying to send, actually the forms that I made is monitoring the email database for a success sending. I use AJAX progress to wait for success notification, checking the database with the emailID and sucess status every 2 seconds. Its just a simple getProgress() functions. I can also do monitoring using triggers in SQl Server to FlushEmail() when its filled. But I don't like it.

      My question is:

      1. Is the 5 seconds wait must be use if sending is failed?

      2. Should I better use static functions for sending email from a forms?

      3. Is using threading 'okay' for sending email from forms?

      4. Should I better use threading or OneWay web service, you know the fire-and-forget web service?

      Thanks.

      Riza Marhaban — September 8, 2009 3:07 AM
    12. Lee Dumond avatar

      Riza, it would be interesting to see what you did. Maybe you could do a blog post somewhere that shows it in detail.

      The reason for using the 5 seconds is that should be plenty of time for a mailbox to recover if it is busy or unavailable. And because this takes place in the main thread, anything longer than 5 seconds would require too long a wait for the thread to return and show the user the success/failure message. One could adapt this code to use asychronous sending, but for a five second pause in the rare case of a mailbox fail, it probably isn't worth it.

      Also, when encapsulating email functionality in a separate class, using static functions is pretty common.

      Lee Dumond — September 8, 2009 12:30 PM
    13. Riza Marhaban avatar

      ah... I see. No wonder sometimes even I used threading, it would still wait too long for the loop to finish. And sometimes there is no respond from the server at all. I don't understand why. That is why I want to find out, how the big website process sending emails. Threading is sometimes fails, weather my logic coding is wrong or my method was wrong. Some say to use a OneWay webservice instead of threading. That is why propably the 5 seconds delay will correct the errors. I will try it your way than.

      Thanks Mr. Lee. Nice!

      Riza Marhaban — September 9, 2009 4:31 AM
    14. Julio Izquierdo avatar

      Hi Lee,

      Thanks for the article, it's very informative and nicely done.

      How would you implement this functionality using SmtpClient.SendAsync.

      Thanks!

      Julio Izquierdo — September 10, 2009 3:04 PM
    15. Lee Dumond avatar

      Julio, good question. I might be a good idea to look into that and post it as a "Part Two" sometime in the near future.

      Lee Dumond — September 10, 2009 6:49 PM
    16. Ramani Sandeep avatar

      Really Great Article

      Very Useful

      Ramani Sandeep — September 22, 2009 12:26 AM
    17. Hot Information avatar

      Yeah... that great. i'll try to my other site. Thanks

      Hot Information — March 26, 2010 8:05 AM
    18. Michael avatar

      Hi,

      What if you are using a Exchange Server and you didn't get any error, but the email never is delivered?

      Thanks

      Michael — May 20, 2010 11:18 AM
    19. Radu avatar

      Very nice way to handle retries on error. Thanks very much.

      Really good programming.

      Radu — June 23, 2010 12:41 PM
    20. eddie avatar

      I've implemented my own thing based on your very nice code of course, to deal with CC and Bcc fields of recipients. I did it by holding coppies of the original recipients lists of the MailMessage and then within the CheckStatusAndReaddress method i do something like that in order to find out the list that the failedRecipient originally belonged to so that I add them there and not to the To list.

      if (mailCC.Any(x => x.Address == exception.FailedRecipient))

      {

      message.CC.Add(exception.FailedRecipient);

      return;

      }

      Note that mailCC is an MailAddress[ ] which the copy i am making of the original CC list and i pass into the method here as a parameter... I do the same with 'To' and for 'Bcc' lists.

      But I was wondering, is there any better way so that no copies and no searches (.Any) are required? Maybe the exception object holds info that allow us to know the list (To, CC, Bcc) where the failedRecipient belonged to?

      eddie — July 9, 2010 3:11 AM
    21. ezmoz blog avatar

      Hopefully, I'll be able to send the mail next.

      ezmoz blog — August 15, 2010 2:42 AM
    22. tzpswk avatar

      Hi,

      SmtpClient.Send methis does not seem to throw an exception "SmtpFailedRecipientException" for the incorrect email id. Any idea?

      Thanks!

      tzpswk — November 10, 2010 1:05 AM

    » Leave a Comment