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
-
SmtpFailedRecipientException
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!
Thank you for submitting this cool story - Trackback from DotNetShoutout
Pingback from Dew Drop – June 24, 2009 | Alvin Ashcraft's Morning Dew
Late last month I made a post about testing E-Mail functionality with Donovan Brown’s Neptune . Several
DotNetBurner - burning hot .net content
Pingback from If At First You Don’t Succeed – Retrying Mail Operations in .NET « Ramani Sandeep
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