ASP.NET Core  

Send Gmail Emails from ASP.NET Core Using OAuth 2.0, Refresh Token and MailKit

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.

The application does not need to manually request a new access token every time.

Instead:

  
    Refresh Token
      |
      v
Google OAuth Server
      |
      v
New Access Token
      |
      v
Gmail SMTP
  

10. Create the Email Message

MailKit uses MimeMessage to construct the email.

  
    var message = new MimeMessage();

message.From.Add(
    MailboxAddress.Parse(fromEmail));

message.To.Add(
    MailboxAddress.Parse(toEmail));

message.Subject =
    emlSndngList.Subject ?? "";
  

This defines:

  
    From
To
Subject
  

11. Send HTML Email

The application uses BodyBuilder .

  
    var bodyBuilder = new BodyBuilder
{
    HtmlBody = emlSndngList.Body
};

message.Body =
    bodyBuilder.ToMessageBody();
  

This allows the application to send HTML content.

For example:

  
    <h2>Purchase Order Notification</h2>

<p>Your purchase order has been approved.</p>

<table>
    <tr>
        <td>PO Number</td>
        <td>PO-10001</td>
    </tr>
</table>
  

This is useful for ERP applications where notification emails need formatting.

12. Adding CC Recipients

The implementation accepts multiple CC addresses.

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

For example:

  
    [email protected],[email protected]
  

The application splits the addresses and adds them individually.

13. Connect to Gmail SMTP

The Gmail SMTP server is:

  
    smtp.gmail.com
  

The implementation uses port:

  
    587
  

with STARTTLS.

  
    await smtpClient.ConnectAsync(
    "smtp.gmail.com",
    587,
    SecureSocketOptions.StartTls);
  

The connection flow is:

  
    Application
   |
   | STARTTLS
   v
smtp.gmail.com:587
  

14. OAuth2 SMTP Authentication

Instead of username/password authentication, the application uses OAuth 2.0.

  
    var oauth2 =
    new SaslMechanismOAuth2(
        fromEmail,
        credential.Token.AccessToken);

await smtpClient.AuthenticateAsync(oauth2);
  

The important point is:

  
    Gmail Address
+
OAuth Access Token
=
SMTP Authentication
  

No Gmail password is passed to MailKit.

15. Send the Email

Once authentication succeeds:

  
    await smtpClient.SendAsync(message);
  

Then disconnect:

  
    await smtpClient.DisconnectAsync(true);
  

The complete sequence is:

  
    Create Email
     |
     v
Get Refresh Token
     |
     v
Generate Access Token
     |
     v
Connect Gmail SMTP
     |
     v
OAuth2 Authentication
     |
     v
Send Email
     |
     v
Disconnect
  

16. Calling the Email Sender from the Application

Your ERP application creates the EmailSnd object.

  
    EmailSnd emailSending = new EmailSnd();

emailSending.FrmEmailId =
    emailSetups.FromEmail;

emailSending.SMTPAdrs =
    emailSetups.SmtpServer;

emailSending.PortNo =
    emailSetups.PortNo;

emailSending.RefreshToken =
    emailSetups.RefreshToken;

emailSending.AccessToken =
    emailSetups.AccessToken;

emailSending.ClientSecret =
    emailSetups.ClientSecret;

emailSending.ClientId =
    emailSetups.ClientId;

emailSending.DownloadUrl =
    text;

emailSending.Body =
    text;

emailSending.Subject =
    notificationMessage;

emailSending.ToEmailId =
    toEmail;

await _emailSender.SendEmailAsync(emailSending);
  

Important

In your original code you have:

  
    _emailSender.SendEmailAsync(emailSending);
  

Because SendEmailAsync() is asynchronous, it is better to use:

  
    await _emailSender.SendEmailAsync(emailSending);
  

Otherwise, the caller does not wait for the email operation to finish.

17. Dependency Injection

In an ASP.NET Core application, the sender can be registered with dependency injection.

For example:

  
    builder.Services.AddScoped<GmailEmailSender>();
  

Then inject it into your service/controller:

  
    private readonly GmailEmailSender _emailSender;

public NotificationService(
    GmailEmailSender emailSender)
{
    _emailSender = emailSender;
}
  

Then:

  
    await _emailSender.SendEmailAsync(emailSending);
  

18. Recommended Database Configuration

For an ERP application, Gmail configuration can be stored in an email setup table.

For example:

  
    EmailSetup
--------------------------------
Id
FromEmail
SmtpServer
PortNo
ClientId
ClientSecret
RefreshToken
AccessToken
IsActive
  

Example:

  
    FromEmail     : [email protected]
SmtpServer    : smtp.gmail.com
PortNo        : 587
ClientId      : Google Client ID
ClientSecret  : Google Client Secret
RefreshToken  : OAuth Refresh Token
  

This allows the ERP application to load the email configuration dynamically.

19. Multiple Gmail Accounts

This architecture can also support multiple Gmail accounts.

For example:

  
    Company A
    |
    +-- gmail account
    +-- Client ID
    +-- Client Secret
    +-- Refresh Token

Company B
    |
    +-- gmail account
    +-- Client ID
    +-- Client Secret
    +-- Refresh Token
  

When sending an email, the application selects the appropriate email configuration.

This is useful for multi-company ERP applications.

20. Security Considerations

OAuth credentials are sensitive.

The following values should NOT be exposed in Angular, JavaScript, browser local storage, or source control:

  
    ClientSecret
RefreshToken
AccessToken
  

They should remain on the server.

For production applications, consider storing sensitive credentials using:

  • Environment variables

  • Azure Key Vault

  • AWS Secrets Manager

  • Secure database encryption

  • ASP.NET Core Secret Manager for development

Do not commit OAuth credentials to GitHub.

21. Error Handling

Your original code contains:

  
    catch (Exception ex)
{
    throw ex;
}
  

This should be changed to:

  
    catch
{
    throw;
}
  

or, if logging is required:

  
    catch (Exception ex)
{
    // Log exception
    throw;
}
  

throw; preserves the original stack trace, while throw ex; can reset the stack-trace information.

22. Common Errors

Invalid Grant

  
    invalid_grant
  

Possible reasons include:

  • Refresh token revoked

  • OAuth consent changed

  • Google account security settings changed

  • Incorrect Client ID / Client Secret

  • Refresh token belongs to a different OAuth client

A new OAuth authorization may be required.

Failed to Acquire Access Token

If this code fails:

  
    await credential.RefreshTokenAsync(
    CancellationToken.None);
  

verify:

  
    ClientId
ClientSecret
RefreshToken
  

Authentication Failed

If MailKit reports SMTP authentication failure, check:

  
    smtp.gmail.com
Port 587
STARTTLS
OAuth2 access token
  

Also verify that the Gmail account and OAuth configuration are valid.

23. Recommended Improvement

Your model currently contains both:

  
    AccessToken
RefreshToken
  

In a refresh-token-based architecture, the refresh token is the important persistent credential.

The access token is temporary and can be regenerated.

Therefore, the application can generally follow:

  
    Database
   |
   +-- ClientId
   +-- ClientSecret
   +-- RefreshToken
   |
   v
Google OAuth
   |
   v
Access Token
   |
   v
MailKit
   |
   v
Gmail
  

The access token does not necessarily need to be permanently stored unless your application has a specific reason to cache it.

24. Complete Email Sending Flow

The complete implementation can be summarized as:

  
    User / ERP Transaction
        |
        v
Notification Service
        |
        v
EmailSnd Object
        |
        v
GmailEmailSender
        |
        +---- Client ID
        |
        +---- Client Secret
        |
        +---- Refresh Token
        |
        v
Google OAuth 2.0
        |
        v
Access Token
        |
        v
MailKit
        |
        v
smtp.gmail.com:587
        |
        | OAuth2 + STARTTLS
        v
Gmail
        |
        v
Recipient
  

25. Advantages of This Approach

1. No Gmail Password

The application does not need the Gmail account password.

2. OAuth 2.0

Authentication is performed using Google's OAuth mechanism.

3. Refresh Token

The application can obtain new access tokens when required.

4. HTML Email

MailKit/MimeKit supports rich HTML email content.

5. CC Support

Multiple CC recipients can be supported.

6. ERP Friendly

Email configuration can be maintained per company, branch, or email account.

7. Server-Side Security

OAuth credentials remain inside the ASP.NET Core application.

26. Final Code Recommendation

One small change I strongly recommend in your calling code is:

  
    await _emailSender.SendEmailAsync(emailSending);
  

instead of:

  
    _emailSender.SendEmailAsync(emailSending);
  

Also change:

  
    catch (Exception ex)
{
    throw ex;
}
  

to:

  
    catch
{
    throw;
}
  

This gives you a cleaner asynchronous implementation and preserves the original exception stack trace.

Conclusion

Gmail SMTP can be integrated into an ASP.NET Core ERP application without storing the Gmail password.

The application uses:

  
    Google OAuth 2.0
        +
Refresh Token
        ↓
Access Token
        ↓
MailKit
        ↓
Gmail SMTP
        ↓
Email
  

This approach is particularly useful for ERP systems that need to send purchase-order notifications, invoice emails, approval notifications, reports, alerts, and other transactional emails.

The main credentials that need to be protected are:

  
    Client ID
Client Secret
Refresh Token
  

The refresh token should be treated as a sensitive credential and should never be exposed to the browser or committed to source control.