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

C# 无法通过Azure web应用程序上的Mailkit发送电子邮件,c#,azure-web-app-service,mailkit,C#,Azure Web App Service,Mailkit,在我们的组织内,我们正在开发一个web应用程序(.NET Core 2.0),它托管在Azure应用程序服务中。对于我们的电子邮件基础架构,我们已经安装了最新版本的MailKit(撰写本文时的版本2.11.1) 在本地,发送电子邮件的过程正常工作,不会出现任何问题,但是,将应用程序部署到Azure环境后,连接时会引发SslHandshakeException MailKit.Security.SslHandshakeException: An error occurred while attem

在我们的组织内,我们正在开发一个web应用程序(
.NET Core 2.0
),它托管在Azure应用程序服务中。对于我们的电子邮件基础架构,我们已经安装了最新版本的
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.
我们正在使用以下配置(简化):

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

不过,似乎有效的方法是将
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上设置自己的:

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;
}
根据问题的性质,您的问题的一个可能解决方案可能是:

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