如何根据新的安全策略在 .Net 中发送电子邮件?

Posted

技术标签:

【中文标题】如何根据新的安全策略在 .Net 中发送电子邮件?【英文标题】:How to send an email in .Net according to new security policies? 【发布时间】:2016-04-23 10:22:56 【问题描述】:

为了更好地保护您的用户,GMail 和其他邮件提供商建议将我们所有的应用程序升级到 OAuth 2.0。

这是否意味着 System.Net.Mail 不再工作,我们需要使用另一个库,例如 MailKit

一般来说,我想了解如何在不允许“访问不太安全的应用程序”的情况下发送电子邮件?

因为我有System.Net.Mail.SmtpException: The SMTP server requires a secure connection or the client was not authenticated. The server response was: 5.5.1 Authentication Required.smtpClient.Send(message); 被执行。

如果解决这个问题的唯一方法是使用MailKit,我认为这个问题将是一个很好的实用逐步切换教程,从System.Net.Mail 到使用MailKitGoogle.Apis.Auth.OAuth2。我不知道一般解决方案是否会使用DotNetOpenAuth

我的应用程序中有以下类,对应于向任何地址(gmail、yandex 和其他)发送电子邮件:

public class EmailSender

    public void SendEmail(SmtpServerSettings serverSettings, SendEmailRequest emailRequest)
    
        // Usually I have 587 port, SmtpServerName = smtp.gmail.com 
        _logger.Trace("Sending message with subject '0' using SMTP server 1:2",
                      emailRequest.Subject,
                      serverSettings.SmtpServerName,
                      serverSettings.SmtpPort);

        try
        
            using (var smtpClient = new SmtpClient(serverSettings.SmtpServerName, (int)serverSettings.SmtpPort))
            
                smtpClient.EnableSsl = serverSettings.SmtpUseSsl; // true
                if (!string.IsNullOrEmpty(serverSettings.UserName) || !string.IsNullOrEmpty(serverSettings.EncryptedPassword))
                
                    smtpClient.Credentials = new NetworkCredential(serverSettings.UserName, serverSettings.EncryptedPassword);
                

                smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network;
                smtpClient.Timeout = (int)serverSettings.SmtpTimeout.TotalMilliseconds;

                using (var message = new MailMessage())
                
                    message.From = new MailAddress(serverSettings.FromAddress);

                    emailRequest.To.ForEach(message.To.Add);
                    emailRequest.CC.ForEach(message.CC.Add);
                    emailRequest.Bcc.ForEach(message.Bcc.Add);

                    message.Subject = emailRequest.Subject.Replace('\r', ' ').Replace('\n', ' ');
                    message.Body = emailRequest.Body;
                    message.BodyEncoding = Encoding.UTF8;
                    message.IsBodyhtml = false;

                    smtpClient.Send(message);
                
            

            _logger.Trace("Sent message with subject '0' using SMTP server 1:2",
                          emailRequest.Subject,
                          serverSettings.SmtpServerName,
                          serverSettings.SmtpPort);
        
        catch (SmtpFailedRecipientsException e)
        
            var failedRecipients = e.InnerExceptions.Select(x => x.FailedRecipient);
            LogAndReThrowWithValidMessage(e, EmailsLocalization.EmailDeliveryFailed, failedRecipients);
        
   

在new Google security policies 之前它工作正常。

我知道System.Net.Mail 不支持OAuth2。我决定使用MailKit's SmtpClient 发送消息。

经过调查,我了解到我的初始代码没有太大变化,因为MailKit's API 看起来非常相似(与System.Net.Mail)。

除了一个细节:我需要用户的 OAuth 访问令牌(MailKit 没有可以获取 OAuth 令牌的代码,但如果我有它,它可以使用它)。

所以以后我会有下面这行:

smtpClient.Authenticate (usersLoginName, usersOAuthToken);

我有一个想法,将GoogleCredentials 作为新参数添加到SendEmail 方法中:

public void SendEmail(SmtpServerSettings serverSettings, SendEmailRequest emailRequest, 
                      GoogleCredentials credentials)

    var certificate = new X509Certificate2(credentials.CertificateFilePath,
                                           credentials.PrivateKey,
                                           X509KeyStorageFlags.Exportable);

     var credential = new ServiceAccountCredential(
                      new ServiceAccountCredential.Initializer(credentials.ServiceAccountEmail)
                             
                                 Scopes = new[]  "https://mail.google.com/" ,
                                 User = serverSettings.UserName
                             .FromCertificate(certificate));

    ....
    //my previous code but with MailKit API

如何获得usersOAuthToken? 使用Google.Apis.Auth.OAuth2 是最佳实践技术吗?

我在上面发布的代码仅适用于 GMail,不适用于 yandex.ru 或其他邮件提供商。为了与他人合作,我可能需要使用另一个 OAuth2 库。但是我不想在我的代码中为许多可能的邮件提供商提供许多身份验证机制。我想为每个邮件提供商提供一个通用解决方案。还有一个可以发送电子邮件的库(就像 .net smtpclient 所做的那样)

【问题讨论】:

评论不用于扩展讨论;这个对话是moved to chat。 【参考方案1】:

如何获取usersOAuthToken?

您需要做的第一件事是关注Google's instructions 为您的应用程序获取 OAuth 2.0 凭据。

完成此操作后,获取访问令牌的最简单方法是使用 Google 的 Google.Apis.Auth 库:

using System;
using System.Threading;
using System.Security.Cryptography.X509Certificates;

using Google.Apis.Auth.OAuth2;

using MimeKit;
using MailKit.Net.Smtp;
using MailKit.Security;

namespace Example 
    class Program
    
        public static async void Main (string[] args)
        
            var certificate = new X509Certificate2 (@"C:\path\to\certificate.p12", "password", X509KeyStorageFlags.Exportable);
            var credential = new ServiceAccountCredential (new ServiceAccountCredential
                .Initializer ("your-developer-id@developer.gserviceaccount.com") 
                    // Note: other scopes can be found here: https://developers.google.com/gmail/api/auth/scopes
                    Scopes = new[]  "https://mail.google.com/" ,
                    User = "username@gmail.com"
                .FromCertificate (certificate));

            // Note: result will be true if the access token was received successfully
            bool result = await credential.RequestAccessTokenAsync (CancellationToken.None);

            if (!result) 
                Console.WriteLine ("Error fetching access token!");
                return;
            

            var message = new MimeMessage ();
            message.From.Add (new MailboxAddress ("Your Name", "username@gmail.com"));
            message.To.Add (new MailboxAddress ("Recipient's Name", "recipient@yahoo.com"));
            message.Subject = "This is a test message";

            var builder = new BodyBuilder ();
            builder.TextBody = "This is the body of the message.";
            builder.Attachments.Add (@"C:\path\to\attachment");

            message.Body = builder.ToMessageBody ();

            using (var client = new SmtpClient ()) 
                client.Connect ("smtp.gmail.com", 587, SecureSocketOptions.StartTls);

                // use the access token as the password string
                client.Authenticate ("username@gmail.com", credential.Token.AccessToken);

                client.Send (message);

                client.Disconnect (true);
            
        
    

使用 Google.Apis.Auth.OAuth2 是最佳实践技术吗?

您为什么不使用他们的 API 来获取身份验证令牌?这似乎是把它交给我的最好方法......

我可以向其他非 gmail 帐户发送电子邮件吗?

是的,您通过 GMail 发送的任何电子邮件都可以发送到任何其他电子邮件地址 - 它不必只是其他 GMail 地址。

【讨论】:

@jstedfast 我在哪里可以获得 X509Certificate2?例如,我需要向 yandex post 发送电子邮件,在那里我可以得到credential.Token.AccessToken @jstedfast 你认为我需要重新设计我的界面吗drive.google.com/file/d/0B_ikyn0YIY7KazYwcFN4SllFaWc/view 我是否需要更改 UI 以在我的 GoogleCredentials 数据合同中添加新字段? @jstedfast 您是否只为 gmail 邮件服务器提供解决方案?我如何知道credential 我的用户是否要向yandex.mail@yandex.ru 发送电子邮件并填写与yandex 邮件服务器对应的所有配置字段? 是的,你是对的。我上面发布的代码仅适用于 GMail,不适用于 yandex.ru。要使用 yandex.ru,您可能需要使用另一个 OAuth2 库。 nuget.org上有一堆OAuth2库 @jstedfast 但这是个问题。对于每一个可能的邮件服务器(yandex.ru、mail.ru、gmail.com,以及许多其他邮件服务器),我都需要拥有自己的身份验证器。什么? :)【参考方案2】:

一般的解决方案是https://galleryserverpro.com/use-gmail-as-your-smtp-server-even-when-using-2-factor-authentication-2-step-verification/

1) 使用浏览器登录您的 Google 帐户并转到您的登录和安全设置。查找两步验证设置。

2) 如果两步验证已关闭并且您希望保持这种状态,这意味着您将需要按照您所说的实现许多身份验证机制。

解决方法:开启后生成并使用google app密码。它应该工作!您不需要使用其他库,例如 mailkit

【讨论】:

这只有在您可以控制 GMail 帐户的情况下才有效,否则您会被卡住。如果您正在编写一个程序来为您不拥有的 GMail(和其他)帐户工作,您将需要使用 OAuth2 或告诉您的用户登录到他们的 GMail 帐户并更改他们的设置(这甚至适用于像 yamex.ru 这样的服务器?) 我刚刚检查了 Google、Yahoo、Yandex 等是否有两步验证 一切正常。我认为我的用户可以控制 GMail 帐户(或其他帐户),因为他们使用它 应用专用密码对我不起作用。仍然遇到同样的错误 @jstedfast 我面临同样的问题。当我的程序向他们发送一封电子邮件时,我有很多用户帐户。如何防止代码中发生此错误?【参考方案3】:

在未实现 Google 特定安全要求的应用程序中使用 Gmail smtp 服务器时会发生身份验证错误。在 Gmail 帐户设置中,打开: “登录和安全” > “连接的应用程序和网站” > “允许不太安全的应用程序”> 开

【讨论】:

“允许不太安全的应用” 这是天才怎么没人想到这个:-D 所有这些其他答案,但我在其他任何地方都没有遇到过这个明显的解决方案。【参考方案4】:
using System;
using System.Net;
using System.Net.Mail;
using System.Net.Mime;
using System.Threading;
using System.ComponentModel;
namespace Examples.SmtpExamples.Async

public class SimpleAsynchronousExample

    static bool mailSent = false;
    private static void SendCompletedCallback(object sender, AsyncCompletedEventArgs e)
    
        // Get the unique identifier for this asynchronous operation.
         String token = (string) e.UserState;

        if (e.Cancelled)
        
             Console.WriteLine("[0] Send canceled.", token);
        
        if (e.Error != null)
        
             Console.WriteLine("[0] 1", token, e.Error.ToString());
         else
        
            Console.WriteLine("Message sent.");
        
        mailSent = true;
    
    public static void Main(string[] args)
    
        // Command-line argument must be the SMTP host.
        SmtpClient client = new SmtpClient(args[0]);
        // Specify the email sender.
        // Create a mailing address that includes a UTF8 character
        // in the display name.
        MailAddress from = new MailAddress("jane@contoso.com",
           "Jane " + (char)0xD8+ " Clayton",
        System.Text.Encoding.UTF8);
        // Set destinations for the email message.
        MailAddress to = new MailAddress("ben@contoso.com");
        // Specify the message content.
        MailMessage message = new MailMessage(from, to);
        message.Body = "This is a test email message sent by an application. ";
        // Include some non-ASCII characters in body and subject.
        string someArrows = new string(new char[] '\u2190', '\u2191', '\u2192', '\u2193');
        message.Body += Environment.NewLine + someArrows;
        message.BodyEncoding =  System.Text.Encoding.UTF8;
        message.Subject = "test message 1" + someArrows;
        message.SubjectEncoding = System.Text.Encoding.UTF8;
        // Set the method that is called back when the send operation ends.
        client.SendCompleted += new
        SendCompletedEventHandler(SendCompletedCallback);
        // The userState can be any object that allows your callback
        // method to identify this send operation.
        // For this example, the userToken is a string constant.
        string userState = "test message1";
        client.SendAsync(message, userState);
        Console.WriteLine("Sending message... press c to cancel mail. Press any other key to exit.");
        string answer = Console.ReadLine();
        // If the user canceled the send, and mail hasn't been sent yet,
        // then cancel the pending operation.
        if (answer.StartsWith("c") && mailSent == false)
        
            client.SendAsyncCancel();
        
        // Clean up.
        message.Dispose();
        Console.WriteLine("Goodbye.");
    

【讨论】:

以上是关于如何根据新的安全策略在 .Net 中发送电子邮件?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 ASP.NET C# 中发送电子邮件

如何在 Java 中发送电子邮件?

如何单击按钮并打开窗口以在c#中发送电子邮件

无法在 asp.net vb 网站中发送电子邮件

在asp.net mvc 3中的后台作业/计划任务中发送电子邮件

如何在android中发送电子邮件[重复]