无法通过 Azure Web 应用程序上的 Mailkit 发送电子邮件

Unable to send emails via Mailkit on an Azure web app

在我们的组织内,我们正在开发一个托管在 Azure 应用服务中的 Web 应用程序 (.NET Core 2.0)。对于我们的电子邮件基础设施,我们安装了最新版本的 MailKit(撰写本文时的版本 2.11.1)。

在本地,发送电子邮件的过程正常,没有出现任何问题,但是,将应用程序部署到我们的 Azure 环境后,连接时会抛出 SslHandshakeException

MailKit.Security.SslHandshakeException: An error occurred while attempting to establish an SSL or TLS connection.

This usually means that the SSL certificate presented by the server is not trusted by the system for one or more of
the following reasons:

1. The server is using a self-signed certificate which cannot be verified.
2. The local system is missing a Root or Intermediate certificate needed to verify the server's certificate.
3. A Certificate Authority CRL server for one or more of the certificates in the chain is temporarily unavailable.
4. The certificate presented by the server is expired or invalid.
5. The set of SSL/TLS protocols supported by the client and server do not match.

See https://github.com/jstedfast/MailKit/blob/master/FAQ.md#SslHandshakeException for possible solutions.
 ---> System.NotSupportedException: The requested security protocol is not supported.

我们使用以下配置(简化):

using (var client = new SmtpClient())
{
   client.Connect("smtp.office365.com", 587, SecureSocketOptions.StartTls);
   client.AuthenticationMechanisms.Remove("XOAUTH2");
   client.Authenticate("username", "password");
   client.Send(mimeMessage);
}

我们试过使用不同的配置值(例如其他端口)但没有成功。

不过,似乎有效的方法是将 MailKit 软件包降级到版本 2.3.1.6。没有任何配置更改,连接成功,我们能够发送电子邮件。

有人可以解释为什么这些版本的行为不同以及我们可能需要采取哪些步骤才能使我们的配置与最新版本的 MailKit 一起工作?

提前致谢!

MailKit 2.3.1.6 有一个默认的 SSL 证书验证回调,它接受的有效内容更加自由。

较新版本的 MailKit 没有(换句话说,较新版本的 MailKit 专注于安全性,而不是“只连接到该死的服务器,我不关心 SSL 证书是否有效”)。相反,MailKit 现在对一些更常见的邮件服务器(例如 GMail、Yahoo!Mail、Office365 和其他一些邮件服务器)的序列号和指纹进行硬编码,以使人们在大多数时间都能“神奇地”工作。但是,正如您所发现的,有时这些证书会更新,并且 MailKit 拥有的硬编码值不再是最新的(顺便说一句,刚刚发布的 2.12.0 更新了它们)。

解决此问题的最佳方法是在 SmtpClient 上设置您自己的 ServerCertificateValidationCallback

client.ServerCertificateValidationCallback = MySslCertificateValidationCallback;

为了帮助您调试问题,您的回调方法可能如下所示:

static bool MySslCertificateValidationCallback (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
    // If there are no errors, then everything went smoothly.
    if (sslPolicyErrors == SslPolicyErrors.None)
        return true;

    // Note: MailKit will always pass the host name string as the `sender` argument.
    var host = (string) sender;

    if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) != 0) {
        // This means that the remote certificate is unavailable. Notify the user and return false.
        Console.WriteLine ("The SSL certificate was not available for {0}", host);
        return false;
    }

    if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) != 0) {
        // This means that the server's SSL certificate did not match the host name that we are trying to connect to.
        var certificate2 = certificate as X509Certificate2;
        var cn = certificate2 != null ? certificate2.GetNameInfo (X509NameType.SimpleName, false) : certificate.Subject;

        Console.WriteLine ("The Common Name for the SSL certificate did not match {0}. Instead, it was {1}.", host, cn);
        return false;
    }

    // The only other errors left are chain errors.
    Console.WriteLine ("The SSL certificate for the server could not be validated for the following reasons:");

    // The first element's certificate will be the server's SSL certificate (and will match the `certificate` argument)
    // while the last element in the chain will typically either be the Root Certificate Authority's certificate -or- it
    // will be a non-authoritative self-signed certificate that the server admin created. 
    foreach (var element in chain.ChainElements) {
        // Each element in the chain will have its own status list. If the status list is empty, it means that the
        // certificate itself did not contain any errors.
        if (element.ChainElementStatus.Length == 0)
            continue;

        Console.WriteLine ("\u2022 {0}", element.Certificate.Subject);
        foreach (var error in element.ChainElementStatus) {
            // `error.StatusInformation` contains a human-readable error string while `error.Status` is the corresponding enum value.
            Console.WriteLine ("\t\u2022 {0}", error.StatusInformation);
        }
    }

    return false;
}

您的问题的一种可能解决方案,具体取决于问题所在,可能如下所示:

static bool MyServerCertificateValidationCallback (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
    if (sslPolicyErrors == SslPolicyErrors.None)
        return true;

    // Note: The following code casts to an X509Certificate2 because it's easier to get the
    // values for comparison, but it's possible to get them from an X509Certificate as well.
    if (certificate is X509Certificate2 certificate2) {
        var cn = certificate2.GetNameInfo (X509NameType.SimpleName, false);
        var fingerprint = certificate2.Thumbprint;
        var serial = certificate2.SerialNumber;
        var issuer = certificate2.Issuer;

        return cn == "outlook.com" &&
            issuer == "CN=DigiCert Cloud Services CA-1, O=DigiCert Inc, C=US" &&
            serial == "0CCAC32B0EF281026392B8852AB15642" &&
            fingerprint == "CBAA1582F1E49AD1D108193B5D38B966BE4993C6";
            // Expires 1/21/2022 6:59:59 PM
    }

    return false;
}