ASP.NET MailKit SMTP 响应

ASP.NET MailKit SMTP response

我有这个代码,它已经成功地发送了电子邮件——除了前几天它没有成功。因此,我想检查 SMTP 响应,但不确定该怎么做。

这是我的代码:

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);
}

所以,我在 here 中读到可以使用 onMessageSent 或 MessageSent 函数来查看是否有响应 - 不过我真的很想看一个代码示例,这些函数是如何实现的在代码中用于确定消息是否真的收到?

我确实有包含异步发送函数的函数 public void,警告抑制平息了 VisualStudio 关于未等待调用的抱怨。

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
}

[新编辑]:那么,让我们想象一下我理顺了整个异步的事情,现在是时候真正捕捉到那些错误的消息了。在这里,我读到可以使用 MessageSent 和 OnMessageSent 函数来查看结果。当然,我想不通。 我正在寻找 here 一些可以使用 MailKit 进行镜像的示例。 在那里,行 client.SendCompleted += new SendCompletedEventHandler(SendCompletedCallback); 似乎是关键,我想知道在我的代码中,使用 client.MessageSent += ??? 是否是 MailKit 中的对应行。

@mason 似乎回答了关于异步滥用的原始问题。

现在回答你的新问题。

MessageSent 事件与 .NET 中的任何其他事件一样,可以通过以下方式收听:

client.MessageSent += OnMessageSent;

其中 OnMessageSent 方法是这样的:

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

但是,您想要收听此事件的原因似乎是误解了它真正为您提供的信息。

虽然 MessageSentEventArgs.Response 属性 包含服务器发送的实际响应,但它不太可能告诉您收件人电子邮件地址是否实际存在。

如果您向一个不存在的电子邮件地址发送邮件并且 SmtpClient.Send()SendAsync() 没有抛出异常,那么这意味着 SMTP 服务器可能没有验证电子邮件是否存在当它收到 MailKit 发送的 RCPT TO 命令时地址存在,并且会愉快地接受消息提交 w/o 错误,这意味着 MailKit 不会抛出异常。许多 SMTP 服务器这样做有两个原因:

  1. 保护他们用户的匿名性(这样垃圾邮件发送者就不能使用暴力破解技术来弄清楚他们的用户帐户名是什么——同样的原因人们禁用了 VRFYEXPN 命令)。
  2. 延迟查找电子邮件地址 - 即 SMTP 服务器在继续将邮件转发到适当的域之前实际上并不查找电子邮件地址的存在。

例如,如果您连接到 smtp.gmail.com 向另一个域中的用户发送消息,那么 smtp.gmail.com 就无法知道 user@another-domain.com直到它实际尝试将消息转发到例如smtp.another-domain.com.

如果您真的想获得有关电子邮件地址是否实际存在的反馈,这个过程需要您付出更多的努力和运气。

幸运。

首先,您需要希望并祈祷您的 SMTP 服务器支持 DSN(传递状态通知)扩展。

要检查你的服务器是否支持这个,你可以检查SmtpClient.Capabilities:

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

努力。

假设您的服务器支持 DSN 扩展,接下来您需要将 SmtpClient 子类化,以便您可以覆盖一些方法,以便为 MailKit 的 SmtpClient 提供一些需要的 information/options.

这些方法是:

  1. GetDeliveryStatusNotifications
  2. GetEnvelopeId

这两种方法的文档已经提供了以下代码片段,但我将其粘贴在这里以供后代使用:

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;
    }
}

好的,既然您已经完成了上述操作...这将请求 SMTP 服务器向您发送一封电子邮件 if/when 服务器无法将邮件传递给任何收件人。

现在 您可以处理接收上述电子邮件...

当您收到其中一条消息时,它将具有 multipart/report; report-type="delivery-status" 的顶级 Content-Type,将由 MultipartReport

表示

检测方法是:

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

然后您需要做的是找到 message/delivery-statusContent-Type 的 MIME 部分,它们是 multipart/report 的子部分(每个部分都将被表示通过 MessageDeliveryStatus):

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

然后您需要检查 StatusGroups 以提取您需要的信息。 StatusGroups 属性 是一个 HeaderListCollection 本质上是键值对列表的列表。

要弄清楚哪些键可用,您需要通读 Section 2.2 and Section 2.3 of rfc3464

至少,您需要检查第一个 StatusGroup 中的 "Original-Envelope-Id" 以确定报告针对的是哪封邮件(此信封 ID 字符串将与您在 GetEnvelopeId).

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

在以下每个 StatusGroup 中,您需要获取 "Original-Recipient" 的值(如果已设置,否则我想您可以检查 "Final-Recipient")。这将是 rfc822;user@domain.com 的形式 - 所以只需拆分 ';' 字符并使用第二个字符串。

最后,您需要检查 "Action" 值以确定所述收件人的状态。在您的情况下,如果值为 "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];

    ...
}

如果你把它们放在一起,你会得到这样的东西:

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;
            }
        }
    }
}