ERP, CRM, HR, notice, invoicing, and reporting apps frequently need to be able to send emails from an ASP.NET Core application. Applications used to authenticate SMTP using a Gmail username and password. This method of application authentication is no longer advised by Gmail. Alternatively, an application may be safely authenticated using OAuth 2.0 without requiring the Gmail account password to be stored.

We will use the following to develop Gmail email sending in C# in this article:

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
  • 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.

HostForLIFE.eu ASP.NET Core 10.0 Hosting
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.