asp.netmailkit

ASP.NET MailKit SMTP response


I have this code which has been successfully sending emails - except for the other day when it did not. Therefore, I'd like to check the SMTP response, but not sure what to do.

Here is my code now:

using (var client = new SmtpClient())
{
  client.LocalDomain = "xxxxxxxxxxxx";
  await client.ConnectAsync("xxxxxxxxxxxx", xx, SecureSocketOptions.StartTls).ConfigureAwait(false);
  await client.AuthenticateAsync( new System.Net.NetworkCredential("xxxxxxxxxxxx", "xxxxxxxxxxxx")).ConfigureAwait(false);
  await client.SendAsync(emailMessage).ConfigureAwait(false);
  await client.DisconnectAsync(true).ConfigureAwait(false);
}

So, I read in here the onMessageSent, or MessageSent functions can be used to see if there was a response - I'd really like to see an example of code though, how would those functions be used in code to determine if the message was really received?

I do have the function which contains the async sending function as a public void, and the warning suppression quells the VisualStudio complaints about the call not being awaited.

public void SendEmail(string HtmlEmailContents, string SubjectLine, string CustomFromAddress = null, string CustomEmailRecipients = null)
{
  string To = getRecipientString(mainRecipients);
  string Cc = getRecipientString(ccRecipients);
  string Bcc = getRecipientString(bccRecipients);
  if(CustomEmailRecipients != null && CustomEmailRecipients != "")
  {
    To = CustomEmailRecipients;
    Cc = "";
    Bcc = "";
  }
  string finalizedFromAddress;
  if(CustomFromAddress != null)
  {
    finalizedFromAddress = CustomFromAddress;
  }
  else
  {
    finalizedFromAddress = FromAddress;
  }
  #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
  MessageServices.SendEmailAsync(
    To,
    finalizedFromAddress,
    Cc,
    Bcc,
    SubjectLine,
    HtmlEmailContents
  );
  #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
}

[New Edit]: So, let's imagine I straightened out the whole async thing, and now it's time to really catch those faulty messages. Here, I have read that the MessageSent and OnMessageSent functions can be used to see the results. And of course, I can't figure this out. I am looking here for some examples which may be mirrored using the MailKit. In there, the line client.SendCompleted += new SendCompletedEventHandler(SendCompletedCallback); seems to hold the key, and I wonder if in my code, using client.MessageSent += ??? us the counterpart inside MailKit.


Solution

  • The original question seems to be answered by @mason regarding the misuse of async.

    So now to answer your new question.

    The MessageSent event is just like any other event in .NET and can be listened to in the following way:

    client.MessageSent += OnMessageSent;
    

    Where the OnMessageSent method is something like this:

    void OnMessageSent (object sender, MessageSentEventArgs e)
    {
        Console.WriteLine ("The message was sent!");
    }
    

    However, the reason you are wanting to listen for this event seems to be a misunderstanding of what information it really provides you with.

    While, yes, the MessageSentEventArgs.Response property contains the actual response sent by the server, it is unlikely to tell you whether or not the recipient email address(es) actually exist or not.

    If you are sending a message to a non-existent email address and SmtpClient.Send() or SendAsync() does not throw an exception, then it means that the SMTP server likely is not verifying whether the email addresses exist when it receives the RCPT TO commands sent by MailKit and will happily accept the message submission w/o error which means no exception will be thrown by MailKit. A lot of SMTP servers do this for 2 reasons:

    1. Protecting the anonymity of their users (so spammers can't use brute force techniques to figure out what their user's account names are - same reason man disable the VRFY and EXPN commands).
    2. Lazy lookups of email addresses - i.e. the SMTP server doesn't actually look up the existence of the email address until it proceeds to forward the message to the appropriate domain.

    For example, if you connect to smtp.gmail.com to send a message to a user on another domain, then there's no way for smtp.gmail.com to know that user@another-domain.com doesn't exist until it actually attempts to forward the message on to e.g. smtp.another-domain.com.

    If you actually want to get feedback as to whether an email address actually exists or not, the process will involve a bit more effort on your part and some luck.

    The Luck.

    First, you'll need to hope and pray that your SMTP server supports the DSN (Delivery Status Notification) extension.

    To check if your server supports this, you can check SmtpClient.Capabilities:

    if (client.Capabilities.HasFlag (SmtpCapability.Dsn)) {
        ...
    }
    

    The Effort.

    Assuming your server supports the DSN extension, next you'll need to subclass SmtpClient so that you can override some methods in order to provide MailKit's SmtpClient with some needed information/options.

    These methods are:

    1. GetDeliveryStatusNotifications
    2. GetEnvelopeId

    The documentation for both methods already provides the following code-snippet, but I'll paste it here for posterity:

    public class DSNSmtpClient : SmtpClient
    {
        public DSNSmtpClient ()
        {
        }
    
        /// <summary>
        /// Get the envelope identifier to be used with delivery status notifications.
        /// </summary>
        /// <remarks>
        /// <para>The envelope identifier, if non-empty, is useful in determining which message
        /// a delivery status notification was issued for.</para>
        /// <para>The envelope identifier should be unique and may be up to 100 characters in
        /// length, but must consist only of printable ASCII characters and no white space.</para>
        /// <para>For more information, see rfc3461, section 4.4.</para>
        /// </remarks>
        /// <returns>The envelope identifier.</returns>
        /// <param name="message">The message.</param>
        protected override string GetEnvelopeId (MimeMessage message)
        {
            // Since you will want to be able to map whatever identifier you return here to the
            // message, the obvious identifier to use is probably the Message-Id value.
            return message.MessageId;
        }
    
        /// <summary>
        /// Get the types of delivery status notification desired for the specified recipient mailbox.
        /// </summary>
        /// <remarks>
        /// Gets the types of delivery status notification desired for the specified recipient mailbox.
        /// </remarks>
        /// <returns>The desired delivery status notification type.</returns>
        /// <param name="message">The message being sent.</param>
        /// <param name="mailbox">The mailbox.</param>
        protected override DeliveryStatusNotification? GetDeliveryStatusNotifications (MimeMessage message, MailboxAddress mailbox)
        {
            // In this example, we only want to be notified of failures to deliver to a mailbox.
            // If you also want to be notified of delays or successful deliveries, simply bitwise-or
            // whatever combination of flags you want to be notified about.
            return DeliveryStatusNotification.Failure;
        }
    }
    

    Okay, now that you've done the above... this will request that the SMTP server sends you an email if/when the server fails to deliver the message to any of the recipients.

    Now you get to handle receiving said emails...

    When you get one of these messages, it will have a top-level Content-Type of multipart/report; report-type="delivery-status" which will be represented by a MultipartReport

    The way to detect this is:

    var report = message.Body as MultipartReport;
    if (report != null && report.ReportType != null && report.ReportType.Equals ("delivery-status", StringComparison.OrdinalIgnoreCase)) {
        ...
    }
    

    Then what you will need to do is locate the MIME part(s) with a Content-Type of message/delivery-status that are children of the multipart/report (each of which will be represented by MessageDeliveryStatus):

    foreach (var mds in report.OfType<MessageDeliveryStatus> ()) {
        ...
    }
    

    Then you'll need to check the StatusGroups in order to extract the information you need. The StatusGroups property is a HeaderListCollection which is essentially a list of a list of key-value pairs.

    To figure out what keys are available, you'll need to read over Section 2.2 and Section 2.3 of rfc3464.

    At a minimum, you'll need to check the "Original-Envelope-Id" in the first StatusGroup in order to figure out which message the report is for (this envelope id string will match the string you returned in GetEnvelopeId).

    var envelopeId = mds.StatusGroups[0]["Original-Envelope-Id"];
    

    In each of the following StatusGroups, you'll want to get the value for the "Original-Recipient" (if set, otherwise I guess you could check the "Final-Recipient"). This will be of the form rfc822;user@domain.com - so just split on the ';' character and use the second string.

    And finally you'll want to check the "Action" value to figure out what the status of said recipient is. In your case, if the value is "failed", then it means that delivery failed.

    for (int i = 1; i < mds.StatusGroups.Length; i++) {
        var recipient = mds.StatusGroups[i]["Original-Recipient"];
        var action = mds.StatusGroups[i]["Action"];
    
        if (recipient == null)
            recipient = mds.StatusGroups[i]["Final-Recipient"];
    
        var values = recipient.Split (';');
        var emailAddress = values[1];
    
        ...
    }
    

    If you put it all together, you get something like this:

    public void ProcessDeliveryStatusNotification (MimeMessage message)
    {
        var report = message.Body as MultipartReport;
    
        if (report == null || report.ReportType == null || !report.ReportType.Equals ("delivery-status", StringComparison.OrdinalIgnoreCase)) {
            // this is not a delivery status notification message...
            return;
        }
    
        // process the report
        foreach (var mds in report.OfType<MessageDeliveryStatus> ()) {
            // process the status groups - each status group represents a different recipient
    
            // The first status group contains information about the message
            var envelopeId = mds.StatusGroups[0]["Original-Envelope-Id"];
    
            // all of the other status groups contain per-recipient information
            for (int i = 1; i < mds.StatusGroups.Length; i++) {
                var recipient = mds.StatusGroups[i]["Original-Recipient"];
                var action = mds.StatusGroups[i]["Action"];
    
                if (recipient == null)
                    recipient = mds.StatusGroups[i]["Final-Recipient"];
                    
                // the recipient string should be in the form: "rfc822;user@domain.com"
                var index = recipient.IndexOf (';');
                var address = recipient.Substring (index + 1);
    
                switch (action) {
                case "failed":
                    Console.WriteLine ("Delivery of message {0} failed for {1}", envelopeId, address);
                    break;
                case "delayed":
                    Console.WriteLine ("Delivery of message {0} has been delayed for {1}", envelopeId, address);
                    break;
                case "delivered":
                    Console.WriteLine ("Delivery of message {0} has been delivered to {1}", envelopeId, address);
                    break;
                case "relayed":
                    Console.WriteLine ("Delivery of message {0} has been relayed for {1}", envelopeId, address);
                    break;
                case "expanded":
                    Console.WriteLine ("Delivery of message {0} has been delivered to {1} and relayed to the the expanded recipients", envelopeId, address);
                    break;
                }
            }
        }
    }