75

To better protect your users, GMail and others mail providers recommends to upgrade all of our applications to OAuth 2.0.

Am I right that this means that System.Net.Mail don't work anymore and we need to use another library like MailKit?

In general I'm trying to understand how to send an email without allowing "Access for less secure apps"?

Because I have 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. When smtpClient.Send(message); executed.

If the only way to solve this problem is using MailKit, I think that this question will be a good practical step-by-step switching tutorial from System.Net.Mail to using MailKit and Google.Apis.Auth.OAuth2. I don't know maybe general solution will be using DotNetOpenAuth?

I have the following class in my application that correspond to send an email to any address(gmail, yandex and others):

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

It works fine until the new Google security policies.

I know that System.Net.Mail does not support OAuth2. I decided to use MailKit's SmtpClient to send messages.

After the investigation I understand that my initial code not change so much, because MailKit's API looks very similar(with System.Net.Mail).

Except one detail: I need to have the user's OAuth access token (MailKit does not have code that will fetch the OAuth token, but it can use it if I have it).

So in the future I will have the following line:

smtpClient.Authenticate (usersLoginName, usersOAuthToken);

I have an idea to add GoogleCredentials as new parameter to the SendEmail method:

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
}

How to get usersOAuthToken? Is it the best practice technique to use Google.Apis.Auth.OAuth2?

The code I posted above is for GMail ONLY and WILL NOT work for yandex.ru or other mail providers. To work with others, I will probably need to use another OAuth2 librarys. But I don't want to have many authentication mechanisms in my code for many possible mail providers. I would like to have ONE GENERAL SOLUTION for every mail providers. And one library that can send email (like .net smtpclient did)

Cœur
  • 37,241
  • 25
  • 195
  • 267
  • Comments are not for extended discussion; this conversation has been [moved to chat](http://chat.stackoverflow.com/rooms/101090/discussion-on-question-by-ivan-petrushenko-how-to-send-an-email-in-net-accordin). – George Stocker Jan 19 '16 at 14:25

4 Answers4

35

The general solution is https://galleryserverpro.com/use-gmail-as-your-smtp-server-even-when-using-2-factor-authentication-2-step-verification/

1) Use a browser to log in to your Google account and go to your Sign-in & security settings. Look for the 2-step verification setting.

2) If 2-step verification is off and you want to keep it that way that's mean that you will need to implement many auth mechanism as you said.

Solution: Turn it on and then generate and use google app password. It should work! You don't need to use other libraries like mailkit.

Michael Freidgeim
  • 26,542
  • 16
  • 152
  • 170
Anatoly
  • 1,908
  • 4
  • 25
  • 47
  • 1
    This will only work if you have control over the GMail account, otherwise you are stuck. If you are writing a program to work for GMail (and other) accounts that you do not own, you'll either need to use OAuth2 or else tell your users to log into their GMail account and change their settings (will that even work for servers like yamex.ru?) – jstedfast Jan 21 '16 at 13:07
  • I just checked that Google, Yahoo, Yandex etc have 2-step verification –  Jan 21 '16 at 13:27
  • And everything works fine. I think that my user have control over the GMail account(or other account), because they use it –  Jan 21 '16 at 13:30
  • App specific password is not working me. Still getting the same error – Tariq Feb 26 '16 at 07:49
  • @jstedfast I'm facing the same issue. I have a lot of users accounts, when my program spread one email message to them. How I would prevent this error happening in code? – Alaa' Jan 15 '18 at 07:34
  • See my solution below. – jstedfast Jan 15 '18 at 14:08
19

How to get usersOAuthToken?

The first thing you need to do is follow Google's instructions for obtaining OAuth 2.0 credentials for your application.

Once you've done that, the easiest way to obtain an access token is to use Google's Google.Apis.Auth library:

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

Is it the best practice technique to use Google.Apis.Auth.OAuth2?

Why wouldn't you use their API for getting an authentication token? It seems like the best way to get it to me...

Will I can send an emails to other not gmail accounts?

Yes, any email you send through GMail can be sent to any other email address - it doesn't have to only be to other GMail addresses.

jstedfast
  • 35,744
  • 5
  • 97
  • 110
  • Do you have some worked(real world example, maybe in some github repository) where I can see how people use MailKit and google api to send emails? –  Jan 19 '16 at 20:57
  • No because it would require that I publish private information (e.g. my developer key). – jstedfast Jan 19 '16 at 22:36
  • 3
    @jstedfast Where I can get X509Certificate2? And for example I need to send an email to yandex post, where I can get `credential.Token.AccessToken` ? – Anatoly Jan 20 '16 at 06:36
  • 1
    @jstedfast Do you think that I need to redesign my interface https://drive.google.com/file/d/0B_ikyn0YIY7KazYwcFN4SllFaWc/view Will I need to change the UI to add a new fields like in my GoogleCredentials data contract? –  Jan 20 '16 at 10:00
  • @Anatoly X509Certificate2 is in the `System.Security.Cryptography.X509Certificates` namespace. – jstedfast Jan 20 '16 at 12:02
  • @ivan_petrushenko your UI looks fine to me. You will not need to add new fields, because those values will need to be hard coded into your app. – jstedfast Jan 20 '16 at 12:05
  • 3
    @jstedfast Am I right that you provide the solution only for gmail mailserver? How do I know `credential` if my user will want to send email to `yandex.mail@yandex.ru` and fill-out all of the configuration fields correspond to yandex mailserver? –  Jan 20 '16 at 12:38
  • 1
    Yes, you are correct. The code I posted above is for GMail only and will not work for yandex.ru. To work with yandex.ru, you will probably need to use another OAuth2 library. There are a bunch of OAuth2 libraries on http://www.nuget.org – jstedfast Jan 20 '16 at 14:22
  • 2
    @jstedfast But this is a problem. For every possible mailserver (yandex.ru, mail.ru, gmail.com, end many others) I will need to have their own authenticator. Wtf? :) –  Jan 20 '16 at 15:45
  • If you use a general purpose OAuth2 library instead of Google's, you could potentially share code for different servers. However, you will need to know the different OAuth2 servers used by each mail service and you will likely also need to know what `Scope` values to use for each as well as register your app to be able to talk to each of these OAuth2 servers (e.g. app keys). – jstedfast Jan 20 '16 at 16:09
  • Use this link to create the credentials: https://console.cloud.google.com/iam-admin/serviceaccounts?_ga=1.177293292.814872674.1472736936 – user890332 Jun 30 '20 at 19:22
  • This is ingenious! Using a regular mail client with an oath token from google. I don't have to use googles gmail api. – user890332 Jun 30 '20 at 22:52
1

Authentication errors happen when using the Gmail smtp server from applications not implementing Google’s specific security requirements. In the Gmail account settings, turn on: "Sign-in & security" > "Connected apps & sites" > “Allow less secure apps” > On

user713836
  • 57
  • 2
0
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.");
    }
}