Introduction
Sending emails from an ASP.NET Core application is a common requirement for ERP, CRM, HR, notification, invoice, and reporting applications.
Traditionally, applications used a Gmail username and password for SMTP authentication. Gmail no longer recommends this approach for application authentication. Instead, OAuth 2.0 can be used to securely authenticate an application without storing the Gmail account password.
In this article, we will implement Gmail email sending in C# using:
Google OAuth 2.0
Gmail SMTP
Refresh Token
Access Token
MailKit
MimeKit
ASP.NET Core / C#
The implementation also supports:
HTML email body
To address
CC addresses
Dynamic subject
Multiple Gmail accounts
Refresh-token-based authentication
1. Architecture
The email-sending flow is:
ASP.NET Core Application
|
| ClientId
| ClientSecret
| RefreshToken
| Gmail Address
v
Google OAuth 2.0
|
| Access Token
v
MailKit SMTP Client
|
| smtp.gmail.com:587
| STARTTLS
| OAuth2
v
Gmail SMTP Server
|
v
Recipient
The important point is that the application does not use the Gmail password.
Instead, the application stores the OAuth refresh token and uses it to obtain an access token when required.
2. Required NuGet Packages
The following packages are required.
Google.Apis.Auth
MailKit
MimeKit
For example:
Install-Package Google.Apis.Auth
Install-Package MailKit
Install-Package MimeKit
3. Required Google OAuth Information
The application requires the following values:
Gmail Address
Client ID
Client Secret
Refresh Token
For example:
FromEmail = [email protected]
ClientId = xxxxxxxxxxxxx.apps.googleusercontent.com
ClientSecret = xxxxxxxxxxxxx
RefreshToken = xxxxxxxxxxxxx
The refresh token is particularly important because it allows the application to obtain a new access token when the current access token expires.
4. Email Model
The application uses an EmailSnd object to pass email information to the email sender.
A simplified model can look like this:
public class EmailSnd
{
public string FrmEmailId { get; set; }
public string ToEmailId { get; set; }
public string CC { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public string RefreshToken { get; set; }
public string AccessToken { get; set; }
public string ClientId { get; set; }
public string ClientSecret { get; set; }
public string SMTPAdrs { get; set; }
public int PortNo { get; set; }
public string DownloadUrl { get; set; }
}
Your actual EmailSnd class can contain additional ERP-specific properties.
5. Gmail Email Sender Class
The main implementation is the GmailEmailSender class.
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Responses;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Util;
using MimeKit;
using MailKit.Security;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace iSuiteCoreV1.Utilities.EmailSender
{
public class GmailEmailSender
{
public static bool IsValidEmail(string email)
{
try
{
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch
{
return false;
}
}
public async Task SendEmailAsync(EmailSnd emlSndngList)
{
try
{
var fromEmail = emlSndngList.FrmEmailId;
var toEmail = emlSndngList.ToEmailId;
var tokenResponse = new TokenResponse
{
RefreshToken = emlSndngList.RefreshToken
};
var flow =
new GoogleAuthorizationCodeFlow(
new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = emlSndngList.ClientId,
ClientSecret = emlSndngList.ClientSecret
},
Scopes = new[]
{
"https://mail.google.com/"
}
});
var credential =
new UserCredential(
flow,
fromEmail,
tokenResponse);
if (credential.Token.AccessToken == null ||
credential.Token.IsExpired(SystemClock.Default))
{
bool success =
await credential.RefreshTokenAsync(
CancellationToken.None);
if (!success ||
string.IsNullOrEmpty(
credential.Token.AccessToken))
{
throw new InvalidOperationException(
"Failed to acquire access token via refresh token.");
}
}
var message = new MimeMessage();
message.From.Add(
MailboxAddress.Parse(fromEmail));
message.To.Add(
MailboxAddress.Parse(toEmail));
message.Subject =
emlSndngList.Subject ?? "";
var bodyBuilder = new BodyBuilder
{
HtmlBody = emlSndngList.Body
};
if (!string.IsNullOrEmpty(emlSndngList.CC))
{
var ccEmails =
emlSndngList.CC
.Split(
new[] { ',', ':' },
StringSplitOptions.RemoveEmptyEntries)
.Select(email => email.Trim());
foreach (var email in ccEmails)
{
if (IsValidEmail(email))
{
message.Cc.Add(
MailboxAddress.Parse(email));
}
}
}
message.Body =
bodyBuilder.ToMessageBody();
using var smtpClient =
new MailKit.Net.Smtp.SmtpClient();
await smtpClient.ConnectAsync(
"smtp.gmail.com",
587,
SecureSocketOptions.StartTls);
var oauth2 =
new SaslMechanismOAuth2(
fromEmail,
credential.Token.AccessToken);
await smtpClient.AuthenticateAsync(oauth2);
await smtpClient.SendAsync(message);
await smtpClient.DisconnectAsync(true);
}
catch
{
throw;
}
}
}
}
6. Understanding the OAuth Flow
The most important part of the implementation is OAuth authentication.
Step 1: Create TokenResponse
var tokenResponse = new TokenResponse
{
RefreshToken = emlSndngList.RefreshToken
};
Here we provide the previously generated refresh token.
The refresh token is long-lived compared to an access token and is used to obtain new access tokens.
7. Create Google Authorization Flow
var flow =
new GoogleAuthorizationCodeFlow(
new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = emlSndngList.ClientId,
ClientSecret = emlSndngList.ClientSecret
},
Scopes = new[]
{
"https://mail.google.com/"
}
});
The important values are:
ClientId
ClientSecret
Scope
The scope:
https://mail.google.com/
provides Gmail access required for SMTP OAuth authentication.
8. Create UserCredential
var credential =
new UserCredential(
flow,
fromEmail,
tokenResponse);
The credential associates:
Gmail Account
+
OAuth Client
+
Refresh Token
9. Refresh the Access Token
An access token is temporary.
Therefore, before sending the email, we check whether an access token exists and whether it has expired.
if (credential.Token.AccessToken == null ||
credential.Token.IsExpired(SystemClock.Default))
{
bool success =
await credential.RefreshTokenAsync(
CancellationToken.None);
if (!success ||
string.IsNullOrEmpty(
credential.Token.AccessToken))
{
throw new InvalidOperationException(
"Failed to acquire access token via refresh token.");
}
}
This is an important part of the implementation.
Join the conversation! Your thoughts help the community grow.