3

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.

Vasily Hall
  • 891
  • 10
  • 22
  • I'm not familiar with MailKit, but I would expect that if sending fails, SendAsync would throw an exception. There's not actually a need to check the response. You would assume it was a successful response, and if not the library would throw an exception. That's how System.Net.Mail.SmtpClient works anyways. – mason Jan 05 '18 at 17:57
  • I wonder if since it's async, the web response went normally even though the send failed ? – Vasily Hall Jan 05 '18 at 17:59
  • You are awaiting the response. This isn't fire and forget. If an exception is thrown by SendAsync it will bubble up. If you were in the context of an HTTP request/response, and didn't catch the error somewhere, then it would result in an HTTP 500 or whatever your handling process for uncaught exceptions looks like. – mason Jan 05 '18 at 18:01
  • As long as you don't have an `async void` method somewhere in the call stack (unless it's an event handler for an ASP.NET lifecycle event), the exception would bubble up. – mason Jan 05 '18 at 18:03
  • Hmm , I do have this block wrapped in a warning suppression which may be doing something. I'll check it again and maybe edit my question to show more info . – Vasily Hall Jan 05 '18 at 18:04
  • Okay, I am interested in your thoughts on the new code I have pasted here, if you care to be obliged.. – Vasily Hall Jan 05 '18 at 19:36
  • 1
    Yep, there's your problem. You didn't await the results of the `MessageServices.SendEmailAsync` call. You need to await that. And the method should be marked as async and return a Task instead of void. And so forth all the way back up the call stack. Compiler warnings are there for a reason. Don't suppress them unless you absolutely know what you're doing. – mason Jan 05 '18 at 20:38
  • I'm changing the SendMail to `public async void SendEmail` and the part with MessageServices.SendEmailAsync to `await MessageServices.SendEmailAsync` - now I hope it does work. – Vasily Hall Jan 05 '18 at 21:26
  • No, like I said earlier, *do not do async void*. There is only one reason to do `async avoid` and that's if you're doing an event handler. In most cases, if you mark a method as `async` then you should return a `Task` or `Task` and `await` the result. Read over [Async/Await - Best Practices in Asynchronous Programming](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx). – mason Jan 05 '18 at 21:32
  • I just do not understand then: my goal is to have a void function sending an email from inside the controller, but I want to throw the 500 error if it's not sent properly. But when I change the `public async void SendEmail` to `public void SendEmail` , then I have the 'await' causing problems being in front of `MessageServices.SendEmailAsync()` - but if I remove it then the green lines show up - the reason I chose to suppress the warning in the first place. Basically I want the SendEmail to be where the error would bubble up into and keep my async stuff to the MessageServices.SendEmailAsync . – Vasily Hall Jan 05 '18 at 21:42
  • You haven't read over the link I just provided you. You *should not do async void* unless you're writing an event handler. You should be doing `async Task` instead. And you should await the result of `MessageServices.SendEmailAsync`. If you're unwilling to follow the proper guidelines for asynchronous programming, then don't do asynchronous programming. Just use the synchronous versions of the methods and it will simplify your code. – mason Jan 05 '18 at 21:44
  • My bad, I did not notice that link – Vasily Hall Jan 05 '18 at 21:45
  • Thank you for helping, you will be horrified to know that the reason I was using Async in the first place was due to code examples I've picked up from someplace which seemed to 'just work'. But, with your guidance I was able to get started on my journey to ascend the async. – Vasily Hall Jan 05 '18 at 21:59
  • Like I said earlier, you likely don't need to "see the results". If SendCompleted or SendCompletedAsync runs without throwing an exception, then you can be confident you've successfully handed your email off to the server. – mason Jan 05 '18 at 23:57
  • Yes thank you, but I did this test where I sent a mail to a nonexistent mail address and it went without any errors- this time I made sure that the code was synchronous so that it would be debugged all the way through completing the send. So this is why I'd like to register a response from the message to check whether it was received. Although this may not have an error effect, I'd like to throw one when something happens that we don't like. – Vasily Hall Jan 06 '18 at 00:13
  • See [this question](https://stackoverflow.com/questions/565504/how-to-check-if-an-email-address-exists-without-sending-an-email). You're barking up the wrong tree. If you don't get an exception, then you can be sure you've handed the email off to the STMP server successfully. If not, then it didn't. There's no need to check anything because the framework checks for you and throws an exception if something went wrong with handing the email off. – mason Jan 06 '18 at 00:31
  • Okay, I have no reason to think this isn't the case, but if it's possible to use the MessageSent event to get information about the email transaction, I'm still curious as to how it's done and what kind of information people get using this code. Now it's in more of the realm of personal curiosity I suppose. – Vasily Hall Jan 06 '18 at 00:38
  • I guess you're exactly right on barking up trees, because according to the writer (https://github.com/jstedfast/MailKit/issues/126), there actually is supposed to be an exception. So I still have code adjusting to do since I'm not seeing this exception come through for some reason. – Vasily Hall Jan 06 '18 at 02:15
  • Your async void is likely preventing you from seeing the exceptions. – mason Jan 06 '18 at 03:26
  • 1
    Some SMTP servers (to protect user anonymity) will not reply to the RCPT TO command with an error when the address does not exist, so in those cases, MailKit will not throw an exception because there is no way for it to know that the address does not exist. – jstedfast Jan 06 '18 at 22:35
  • The MessageSent event is not likely to help you in this case, either. – jstedfast Jan 06 '18 at 22:36
  • 1
    I just wanted to point out that the System.Net.Mail.SmtpClient library does not, in fact, throw an exception if an email fails to send unless the issue is between you and the SMTP server. You have to hook up the SendCompleted event listener, and when the email either succeeds or fails, you can check the event arguments in the callback to know what the status is. – Lucas Leblanc Jan 18 '19 at 16:39

1 Answers1

11

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;
            }
        }
    }
}
Community
  • 1
  • 1
jstedfast
  • 35,744
  • 5
  • 97
  • 110
  • 1
    You're welcome. FWIW, my original answer was incorrect wrt checking the Original-Envelope-Id - apparently the Original-Envelope-Id is only in the very first StatusGroup and the following StatusGroups contain the per-recipient info. I've updated my answer with that info plus an example code-snippet putting it all together. – jstedfast Jan 07 '18 at 00:10
  • I'm working more on this today and have confirmed several tests - among which I am able to see the response and how it's *2.0.0 Ok: queued as...* for various emails, even ones which do not exist. The Capabilities Dsn check does fail though. Perhaps the work continues, depending on what the IT say. But - so far I'm glad to have learned more about async, the events in ASP as well as "The Effort" which relies on the use of DSN extension. Hopefully I may get to use it still. I'm going to give it a day or so and see if I'll get to use "The Effort" - but otherwise this is correct answer + mason cmts – Vasily Hall Jan 08 '18 at 17:14
  • Is there a way to find out if the SMTP server rejected my email because of some issue, how can I collect the SMTP errors ? – Krishnan Venkiteswaran Feb 17 '20 at 11:44
  • Read the part of my answer that explains how to request Delivery Status Notifications. – jstedfast Feb 17 '20 at 13:10