﻿# 如何透過 .NET 操作 Google Workspace 的 Gmail API 發送郵件

Gmail API 是一個 RESTful API，主要用來管理 Gmail 信箱與發送郵件，而要通過認證的方法，就是使用 Google 的 OAuth 來進行驗證與授權，若你要用 .NET 來開發 Google API 應用，可以說幾乎找不到完整的文件與範例程式可供參考，所以上手確實有點難度。今天我就整理一下今天的研究心得，記錄一些常見的程式寫法。

![glowing-sky-sphere-orbits-starry-galaxy-generated-by-ai-2](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/ff4749d7d385e72d4482-7dc24fc5-617a-4466-9594-357cf040b62a.webp)

### 若為主控台應用程式該如何取得 OAuth 授權並發信

由於 OAuth 2.0 的 Authorization Code Flow 是一個需要人工介入的流程，例如你想用 `abc@gmail.com` 這個信箱發信，就要請 `abc@gmail.com` 這個帳號進行授權，讓你得到 Access Token 之後，才能代表 `abc@gmail.com` 這個帳號，拿著最終的 Access Token 來發信。但是 Console (主控台應用程式) 類型的應用程式並沒有瀏覽器可以跟使用者互動，所以解決方法就是另開瀏覽器來進行 OAuth 授權流程。

1.  先到 Google Cloud console 網站初始化一些設定
    
    主要是建立專案、啟用 Gmail API 服務、建立 OAuth 2.0 Client ID 憑證(請記得要選取 `Desktop` 或 `Installed application` 類型)、下載 `client_secret.json` 檔案 (裡面包含 [Client Secrets](https://developers.google.com/api-client-library/dotnet/guide/aaa_client_secrets?hl=en) 資訊)。
    
2.  安裝一些 NuGet 套件
    
    ```powershell
    Install-Package MailKit
    Install-Package Google.Apis.Auth
    Install-Package Google.Apis.Gmail.v1
    ```
    
3.  透過 [GoogleWebAuthorizationBroker](https://cloud.google.com/dotnet/docs/reference/Google.Apis/latest/Google.Apis.Auth.OAuth2.GoogleWebAuthorizationBroker) 進行驗證
    
    這個 `GoogleWebAuthorizationBroker.AuthorizeAsync()` 會協助使用者進行驗證，並回傳 [UserCredential](https://googleapis.dev/dotnet/Google.Apis.Auth/latest/api/Google.Apis.Auth.OAuth2.UserCredential.html) 物件，會在第一次執行時全自動替用戶端開啟一個瀏覽器，幫助使用者進行 OAuth 2.0 的 Authorization Code Flow！
    
    這個 `GoogleWebAuthorizationBroker.AuthorizeAsync()` 其實背後做了很多事，他會先在本機 Listen 一個 Port 專門用來當成 OAuth 的 Redirect URI 使用，好讓 OAuth 授權流程都完成後，自動取回 Access Token 與相關資訊，然後再關閉這個 Port 的使用。因此，請千萬不要在 Web 應用程式中使用這個 API 喔，你只能用在 Console 或 Windows Application 之類的應用上，也就是 Google 所稱呼的 **Installed application**！
    
    ```csharp
    using Google.Apis.Auth.OAuth2;
    using Google.Apis.Gmail.v1;
    using Google.Apis.Util.Store;
    using Google.Apis.Services;
    
    string[] scopes = { GmailService.Scope.GmailSend };
    
    UserCredential credential;
    
    using var stream = new FileStream("client_secret.json", FileMode.Open, FileAccess.Read);
    
    var credentialsPath = ".credentials";
    
    credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
        GoogleClientSecrets.FromStream(stream).Secrets,
        scopes,
        "user",
        CancellationToken.None,
        new FileDataStore(credentialsPath, true));
    
    if (credential.Token.IsExpired(Google.Apis.Util.SystemClock.Default))
    {
        if (!await credential.RefreshTokenAsync(CancellationToken.None))
        {
            throw new Exception("Access Token 已經過期，而且執行 Refresh Token 作業失敗！");
        }
    }
    
    Console.WriteLine("Credential file saved to: " + credentialsPath);
    ```
    
    上述程式碼的目的在於取得一個 [UserCredential](https://googleapis.dev/dotnet/Google.Apis.Auth/latest/api/Google.Apis.Auth.OAuth2.UserCredential.html) 型別的 `credential` 物件！
    
4.  準備郵件內容
    
    這裡我使用 [MailKit](https://github.com/jstedfast/MailKit) 套件來準備郵件內容，程式碼如下：
    
    ```csharp
    var fromName = "寄件者名稱";
    var fromEmail = "abc@gmail.com";
    var toName = "收件者名稱";
    var toEmail = "target@gmail.com";
    var subject = "這是一封測試郵件";
    var body = @"這是一封測試郵件，請勿回覆。";
    
    var message = new MimeMessage();
    message.From.Add(new MailboxAddress(fromName, fromEmail));
    message.To.Add(new MailboxAddress(toName, toEmail));
    message.Subject = subject;
    
    var builder = new BodyBuilder();
    builder.TextBody = body;
    message.Body = builder.ToMessageBody();
    ```
    
5.  準備 [RFC2822](https://www.rfc-editor.org/rfc/rfc2822) 格式的郵件內容
    
    我們可以透過 `MimeMessage` 型別下的 `WriteTo` 方法將內容先寫入到 [MemoryStream](https://learn.microsoft.com/zh-tw/dotnet/api/system.io.memorystream?view=net-7.0&WT.mc_id=DT-MVP-4015686) 之中，再透過 `Encoding.UTF8.GetString()` 取得完整的內容。
    
    ```csharp
    using var mem = new MemoryStream();
    message.WriteTo(mem);
    var rfc2822 = Encoding.UTF8.GetString(mem.ToArray());
    ```
    
6.  準備一個 `Google.Apis.Gmail.v1.Data.Message` 物件
    
    其實 Gmail API 有點難用，他必須要你自己準備好 RFC2822 格式的郵件內容，然後用 Base64 編碼後才能送出郵件！
    
    ```csharp
    string Base64UrlEncode(string input)
    {
        var inputBytes = System.Text.Encoding.UTF8.GetBytes(input);
        return Convert.ToBase64String(inputBytes).Replace("+", "-").Replace("/", "_").Replace("=", "");
    }
    
    var msg = new Google.Apis.Gmail.v1.Data.Message();
    msg.Raw = Base64UrlEncode(rfc2822); // 一定要用 Base64 編碼過
    ```
    
7.  建立一個 Gmail API 服務物件 (即 `GmailService` 實例)
    
    這裡要帶入 `credential` 物件到 `HttpClientInitializer` 屬性中，他會在呼叫 RESTful API 時自動加入必要的 `client_id` 或 `client_secret` 資訊：
    
    ```csharp
    var service = new GmailService(new BaseClientService.Initializer()
    {
        HttpClientInitializer = credential,
        ApplicationName = "Gmail Sender",
    });
    ```
    
8.  透過 Gmail API 發送郵件
    
    ```csharp
    // 這裡的 "me" 代表著通過 OAuth 驗證的那個使用者
    service.Users.Messages.Send(msg, "me").Execute();
    ```
    

### 若為 ASP.NET Core 應用程式該如何取得 OAuth 授權並發信

1.  先到 Google Cloud console 網站初始化一些設定
    
    主要是建立專案、啟用 Gmail API 服務、建立 OAuth 2.0 Client ID 憑證(請記得要選取 `Web application` 類型)、下載 `client_secret.json` 檔案 (裡面包含 [Client Secrets](https://developers.google.com/api-client-library/dotnet/guide/aaa_client_secrets?hl=en) 資訊)。
    
2.  透過標準的 OAuth 2.0 流程，在 `redirect_uri` 端點取得 `Access Token` 與 `Refresh Token`
    
3.  發信之前要先取得一個 [UserCredential](https://googleapis.dev/dotnet/Google.Apis.Auth/latest/api/Google.Apis.Auth.OAuth2.UserCredential.html) 物件
    
    ```csharp
    string[] scopes = { GmailService.Scope.GmailSend };
    
    var credentialsPath = ".credentials";
    var access_token = "...";
    var refresh_token = "...";
    
    var jsonCredentials = Path.Combine(Path.GetDirectoryName(Util.CurrentQueryPath), "client_secret.json");
    using var stream = new FileStream(jsonCredentials, FileMode.Open, FileAccess.Read);
    var flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
    {
        ClientSecrets = GoogleClientSecrets.FromStream(stream).Secrets,
        Scopes = scopes,
        DataStore = new FileDataStore(credentialsPath)
    });
    
    var myToken = new TokenResponse
    {
        AccessToken = access_token,
        RefreshToken = refresh_token
    };
    
    var credential = new UserCredential(flow, Environment.UserName, myToken);
    ```
    
4.  準備郵件內容
    
    這裡我使用 [MailKit](https://github.com/jstedfast/MailKit) 套件來準備郵件內容，程式碼如下：
    
    ```csharp
    var fromName = "寄件者名稱";
    var fromEmail = "abc@gmail.com";
    var toName = "收件者名稱";
    var toEmail = "target@gmail.com";
    var subject = "這是一封測試郵件";
    var body = @"這是一封測試郵件，請勿回覆。";
    
    var message = new MimeMessage();
    message.From.Add(new MailboxAddress(fromName, fromEmail));
    message.To.Add(new MailboxAddress(toName, toEmail));
    message.Subject = subject;
    
    var builder = new BodyBuilder();
    builder.TextBody = body;
    message.Body = builder.ToMessageBody();
    ```
    
5.  準備 [RFC2822](https://www.rfc-editor.org/rfc/rfc2822) 格式的郵件內容
    
    我們可以透過 `MimeMessage` 型別下的 `WriteTo` 方法將內容先寫入到 [MemoryStream](https://learn.microsoft.com/zh-tw/dotnet/api/system.io.memorystream?view=net-7.0&WT.mc_id=DT-MVP-4015686) 之中，再透過 `Encoding.UTF8.GetString()` 取得完整的內容。
    
    ```csharp
    using var mem = new MemoryStream();
    message.WriteTo(mem);
    var rfc2822 = Encoding.UTF8.GetString(mem.ToArray());
    ```
    
6.  準備一個 `Google.Apis.Gmail.v1.Data.Message` 物件
    
    其實 Gmail API 有點難用，他必須要你自己準備好 RFC2822 格式的郵件內容，然後用 Base64 編碼後才能送出郵件！
    
    ```csharp
    string Base64UrlEncode(string input)
    {
        var inputBytes = System.Text.Encoding.UTF8.GetBytes(input);
        return Convert.ToBase64String(inputBytes).Replace("+", "-").Replace("/", "_").Replace("=", "");
    }
    
    var msg = new Google.Apis.Gmail.v1.Data.Message();
    msg.Raw = Base64UrlEncode(rfc2822); // 一定要用 Base64 編碼過
    ```
    
7.  建立一個 Gmail API 服務物件 (即 `GmailService` 實例)
    
    這裡要帶入 `credential` 物件到 `HttpClientInitializer` 屬性中，他會在呼叫 RESTful API 時自動加入必要的 `client_id` 或 `client_secret` 資訊：
    
    ```csharp
    var service = new GmailService(new BaseClientService.Initializer()
    {
        HttpClientInitializer = credential,
        ApplicationName = "Gmail Sender",
    });
    ```
    
8.  建立一個 `GmailService` 實例
    
    ```csharp
    var service = new GmailService(new BaseClientService.Initializer()
    {
        HttpClientInitializer = credential,
        ApplicationName = "My App",
    });
    ```
    
9.  透過 Gmail API 發送郵件
    
    ```csharp
    // 這裡的 "me" 代表著通過 OAuth 驗證的那個使用者
    service.Users.Messages.Send(msg, "me").Execute();
    ```
    

### 相關連結

-   Google for Developers
    -   [OAuth 2.0  |  API Client Library for .NET](https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth?hl=en)
    -   [Gmail API Overview](https://developers.google.com/gmail/api/guides?hl=en)
    -   [Gmail API](https://developers.google.com/gmail/api/reference/rest?hl=en)
    -   [Google API Explorer](https://developers.google.com/apis-explorer?hl=zh-tw)
    -   [Choose Gmail API scopes](https://developers.google.com/gmail/api/auth/scopes?hl=en)
    -   [為網路應用程式應用程式使用 OAuth 2.0](https://developers.google.com/identity/protocols/oauth2/web-server?hl=zh-tw)
    -   [Using OAuth 2.0 for Web Server Applications](https://developers.google.com/identity/protocols/oauth2/web-server?hl=en)
    -   [googleapis/google-api-dotnet-client: Google APIs Client Library for .NET](https://github.com/googleapis/google-api-dotnet-client)
-   範例程式
    -   [googlearchive/google-api-dotnet-client-samples](https://github.com/googlearchive/google-api-dotnet-client-samples): Samples of using the Google APIs Client Library for .NET
    -   [googlesamples/oauth-apps-for-windows](https://github.com/googlesamples/oauth-apps-for-windows): OAuth for Apps: Samples for Windows
-   MailKit
    -   [jstedfast/MailKit](https://github.com/jstedfast/MailKit): A cross-platform .NET library for IMAP, POP3, and SMTP.
    -   [MailKit Documentation](http://www.mimekit.net/docs/html/Introduction.htm)
    -   [jstedfast/MimeKit](https://github.com/jstedfast/MimeKit): A .NET MIME creation and parser library with support for S/MIME, PGP, DKIM, TNEF and Unix mbox spools.
    -   [\[.Net Core\] 使用 MailKit 發送郵件：叡揚部落格](https://www.gss.com.tw/blog/net-core-send-mail-with-mailkit)
-   相關文章
    -   [在已安裝應用程式中取得 Google OAuth 2.0 授權](http://vito-note.blogspot.com/2015/04/invalid-credentials.html)
    -   [\[Csharp\]寄送Gmail使用OAuth2](https://blog.yslifes.com/archives/2445)
