[C#] #12 使用 Google Gmail API 發送電子郵件 - antqtech/KM GitHub Wiki

IGmail.cs

namespace SeeGo_API.Services.Interfaces
{
    public interface IGmail
    {
        string SendMail(string toEmail, string subject, string htmlBodyContent);
    }
}

GmailService.cs

using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Responses;
using Google.Apis.Gmail.v1;
using Google.Apis.Gmail.v1.Data;
using Google.Apis.Services;
using MimeKit;
using System;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Text;
using Newtonsoft.Json.Linq;
using SeeGo_API.Services.Interfaces;

namespace SeeGo_API.Services
{
    public class GmailServices : IGmail
    {
        private string ClientId = "ClientId ";
        private string ClientSecret = "ClientSecret";

        public string SendMail(string toEmail, string subject, string htmlBodyContent)
        {
            string fromAddress = "[email protected]";
            string fromDisplayName = "戶外運動整合平台";

            var mailMessage = new MimeMessage();
            mailMessage.From.Add(new MailboxAddress(fromDisplayName, fromAddress));
            mailMessage.To.Add(new MailboxAddress("", toEmail));
            mailMessage.Subject = subject;

            var textPart = new TextPart("html")
            {
                Text = htmlBodyContent
            };
            textPart.ContentTransferEncoding = ContentEncoding.Base64;

            mailMessage.Body = textPart;

            var rawMessage = Convert.ToBase64String(Encoding.UTF8.GetBytes(mailMessage.ToString()))
                .Replace('+', '-')
                .Replace('/', '_')
                .Replace("=", "");

            var OAuth_Scopes = new string[] { GmailService.Scope.GmailCompose };

            var flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
            {
                ClientSecrets = new ClientSecrets
                {
                    ClientId = ClientId,
                    ClientSecret = ClientSecret
                },
                Scopes = OAuth_Scopes
            });

            var token = new TokenResponse
            {
                AccessToken = GetAccessToken("XXXXXXXXXXXXXXXXXXXXXX"),
                RefreshToken = "RRRRRRRRRRRRRRRRRRRRRRRRRRR"
            };

            var gmail = new Google.Apis.Gmail.v1.GmailService(new BaseClientService.Initializer
            {
                ApplicationName = "SeeGo",
                HttpClientInitializer = new UserCredential(flow, fromAddress, token)
            });

            var result = gmail.Users.Messages.Send(new Message
            {
                Raw = rawMessage
            }, "me").Execute();

            return result.Id;
        }

        private string GetAccessToken(string refreshToken)
        {
            using (var wb = new WebClient())
            {
                var data = new NameValueCollection
                {
                    ["refresh_token"] = refreshToken,
                    ["client_id"] = ClientId,
                    ["client_secret"] = ClientSecret,
                    ["grant_type"] = "refresh_token"
                };

                var responseBytes = wb.UploadValues("https://accounts.google.com/o/oauth2/token", "POST", data);
                var tokens = Encoding.UTF8.GetString(responseBytes);

                var json = JObject.Parse(tokens);

                return json["access_token"].ToString();
            }
        }
    }
}

GmailService解釋

程式碼定義了一個名為 GmailServices 的服務類別,用於通過 Google Gmail API 發送電子郵件。這個類別實現了 IGmail 介面,並通過 OAuth 2.0 認證流程進行 Google 帳戶授權以發送郵件。

會取名為 GmailServices 而不是 GmailService ,是因為Gmail函式庫裡面有同名的Class。

1. 成員變數

private string ClientId = "123456-XXXX.apps.googleusercontent.com"; 
private string ClientSecret = "Gsssssssss";

這些變數是 Google API 的客戶端 ID (ClientId) 和客戶端密鑰 (ClientSecret),用於 OAuth 認證流程中授權此應用程式進行 API 請求。

2. SendMail 方法

SendMail 方法是該類別的主要方法,用於發送電子郵件,並接受三個參數:

  • toEmail:收件人的電子郵件地址。
  • subject:郵件主題。
  • htmlBodyContent:郵件的 HTML 內容。

方法步驟

  1. 組建郵件訊息
  • 創建 MimeMessage 對象,並設置發件人和收件人信息。
  • 使用 TextPart 設置郵件的 HTML 主體內容,並將其編碼為 Base64 以符合 Gmail API 要求。
  1. OAuth 認證設置
  • 設置 Gmail API 的所需範圍(GmailService.Scope.GmailCompose)。
  • 初始化 GoogleAuthorizationCodeFlow,用於管理 OAuth 認證流程,並通過 ClientSecrets 包含的 ClientIdClientSecret 授權應用。
  1. 獲取 OAuth Token
  • 使用 TokenResponse 包含的 AccessTokenRefreshToken 進行 API 認證。這些 token 通常來自 OAuth 認證流程中的預先授權,用於維持授權連接。
  • 調用自定義的 GetAccessToken 方法來刷新 token,確保授權有效性。
  1. 發送郵件
  • 使用 GmailService 初始化 API 客戶端服務,並通過 Users.Messages.Send 方法將構建的郵件 rawMessage 送出,這段程式碼設置了 me 表示發件人為已授權的用戶。

3. GetAccessToken 方法

GetAccessToken 方法負責使用 refresh_token 向 Google OAuth API 請求新的 access_token,以保證授權持續有效。該方法的流程如下:

  • 使用 WebClient 進行 HTTP POST 請求,傳送 refresh token 和其他必需的 OAuth 參數至 https://accounts.google.com/o/oauth2/token
  • 解析響應 JSON,提取並返回 access_token

IEmailTemplate.cs

namespace SeeGo_API.Services.Interfaces
{
    public interface IEmailTemplate
    {
        string GetTestOneParamEmailBody(string code);

        string GetTestManyParamEmailBody(string code, string phone, string email);
    }
}

EmailTemplateService.cs

using SeeGo_API.Services.Interfaces;
using System.Numerics;

namespace SeeGo_API.Services
{
    public class EmailTemplateService : IEmailTemplate
    {
        private readonly IWebHostEnvironment _env;

        public EmailTemplateService(IWebHostEnvironment env)
        {
            _env = env;
        }

        public string GetTestOneParamEmailBody(string code)
        {
            string projectRoot = _env.ContentRootPath;
            string filePath = Path.Combine(projectRoot, "EmailTemplate", "Register.html");
            string htmlTemplate = File.ReadAllText(filePath);
            return htmlTemplate.Replace("{{code}}", code);
        }

        public string GetTestManyParamEmailBody(string code, string phone, string email)
        {
            string projectRoot = _env.ContentRootPath;
            string filePath = Path.Combine(projectRoot, "EmailTemplate", "test.html");
            string htmlTemplate = File.ReadAllText(filePath);
            htmlTemplate = htmlTemplate.Replace("{{code}}", code);
            htmlTemplate = htmlTemplate.Replace("{{phone}}", phone);
            htmlTemplate = htmlTemplate.Replace("{{email}}", email);
            return htmlTemplate;
        }
    }
}

EmailTemplateService解釋

程式碼定義了一個名為 EmailTemplateService 的類別,這個類別的作用是讀取 HTML 模板並插入動態數據,用於生成包含使用者資訊的電子郵件內容。該類別實現了 IEmailTemplate 介面,並使用依賴注入獲取專案的根目錄。

1. 成員變數

private readonly IWebHostEnvironment _env;
  • _envIWebHostEnvironment 類型的變數,表示當前 Web 應用的主機環境。通過依賴注入(Dependency Injection)傳入該環境變數,程式可以獲取應用程式的根目錄路徑,以便定位並讀取 HTML 模板文件。如果沒有這個就沒辦法找到模板。

2. GetTestOneParamEmailBody 方法

public string GetTestOneParamEmailBody(string code)
{
    string projectRoot = _env.ContentRootPath;
    string filePath = Path.Combine(projectRoot, "EmailTemplate", "Register.html");
    string htmlTemplate = File.ReadAllText(filePath);
    return htmlTemplate.Replace("{{code}}", code);
}
  • 這個方法用於生成單個參數的電子郵件模板,接受一個 code 字串參數。
  • 運作步驟:
    1. 使用 _env.ContentRootPath 獲取專案的根目錄路徑。
    2. 使用 Path.Combine 方法生成 Register.html 的完整路徑。
    3. 使用 File.ReadAllText 方法讀取該 HTML 模板文件。
    4. 使用 Replace("{{code}}", code) 將模板中的 {{code}} 標記替換為實際的 code 值,並返回最終的 HTML 字串。

3. GetTestManyParamEmailBody 方法

public string GetTestManyParamEmailBody(string code, string phone, string email)
{
    string projectRoot = _env.ContentRootPath;
    string filePath = Path.Combine(projectRoot, "EmailTemplate", "test.html");
    string htmlTemplate = File.ReadAllText(filePath);
    htmlTemplate = htmlTemplate.Replace("{{code}}", code);
    htmlTemplate = htmlTemplate.Replace("{{phone}}", phone);
    htmlTemplate = htmlTemplate.Replace("{{email}}", email);
    return htmlTemplate;
}
  • 這個方法用於生成多個參數的電子郵件模板,接受三個參數:codephoneemail
  • 運作步驟與 GetTestOneParamEmailBody 類似,但在讀取 test.html 模板文件後,它依次使用 Replace 方法將 {{code}}{{phone}}{{email}} 標記替換為相應的參數值,最終返回完整的 HTML 字串。

實際運用

Program.cs 註冊

builder.Services.AddScoped<IEmailTemplate, EmailTemplateService>();
builder.Services.AddScoped<IGmail, GmailServices>(); // Gmail庫有GmailService這個class 所以多一個s

Controller 依賴注入

namespace SeeGo_API.Controllers
{
    [Route("Account")]
    [ApiController]
    public class AccountController : Controller
    {
        // ClassBaseDB
        private readonly SeeGoDbConext _dbcontext;
        private readonly IWebHostEnvironment _env;
        private readonly IEmailTemplate _emailTemplate;
        private readonly IGmail _gmail;

        public AccountController(SeeGoDbConext dbcontext, IWebHostEnvironment env, IEmailTemplate emailTemplate, IGmail gmail)
        {
            _dbcontext = dbcontext;
            _env = env;
            _emailTemplate = emailTemplate;
            _gmail = gmail;

        }
    }

實際使用

/// <include file='Docs/AccountAPIDocs.xml' path='doc/members/member[@name="電子郵件發送測試"]/*'/>
[HttpPost("TestEmail")]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult TestEmail([FromBody] ReqVerifyEmail ReqVerifyEmail)
{
    int VType = ReqVerifyEmail.VType;
    string MID = ReqVerifyEmail.Email;

    string GenerateRandomNumber()
    {
        Random random = new Random();
        return string.Concat(Enumerable.Range(0, 6).Select(_ => random.Next(0, 9).ToString()));
    }

    string ranNum = GenerateRandomNumber();
    int timeStamp = Convert.ToInt32(DateTime.Now.AddMinutes(10).Subtract(new DateTime(1970, 1, 1)).TotalSeconds);
    string encryptedMsg = _aesCrypto.Encrypt($"{ReqVerifyEmail.Email};{ranNum};{timeStamp}");

    void SendVerificationEmail(string subject, string htmlBodyContent)
    {
        _gmail.SendMail(ReqVerifyEmail.Email, subject, htmlBodyContent);
    }

    if(VType == 1)
    {
        SendVerificationEmail("【SeeGo測試單參數郵件】郵件測試", _emailTemplate.GetTestOneParamEmailBody());
    }

    if(VType == 2)
    {
        string code = "948794", phone = "(02) 2597-9100", email = "[email protected]";

        SendVerificationEmail("【SeeGo測試多參數郵件】郵件測試", _emailTemplate.GetTestManyParamEmailBody(code, phone, email));
        return _response.GenerateOkResponse(encryptedMsg);
    }

    return _response.GenerateOkResponse("測試郵件已發送!");
}