這裡是我測試 Gmail API 和 Google API 憑証的一些記錄。
如果你的目的是要使用 Gmail Api 取代舊的 Gmail SMTP 來發送通知信,建議你先跳到最下方看一下結論。
如果你是想要看一下 Gmail API 和 Google API 憑証的使用方法,可以看一下這篇文章。
1. 在 google cloud platform 建立新的專案.
https://console.cloud.google.com/
啟用 Gmail API
因為我們要透過 OAuth 取得使用者授權,所以要設定使用 OAuth 的同意畫面。
指定授權的範圍
因為剛建立的專案,不會被公開,所以要指定測試使用者
如果要給任意使用者,必需經過發布的流程,但準備工作有點麻煩,所以這次就不發布了。
建立 OAuth 2.0 用戶端 ID 憑証
這裡除了名稱外,還有一個設定重導 Uri 的項目。現在不填寫,但稍後要回來補這個資料。
下載 json 之後,命名為 client_secret.json 保留後續使用。
再來就要建立專案了. 用 VS2022 建立一個新專案
記錄網址, 本測試專案是 https://localhost:44340/ ,請依實際網址為準。
回到 OAuth 2.0 用戶端 ID 的設定頁. 在已授權的重新導向 URI 中填入 https://localhost:44340/Home/AuthReturn (填入的網址依實際專案的狀況,可能會有變化)
在 VS2022 中,使用 Nuget 安裝套件: (有漏的再麻煩和我說)
Google.Apis.Gmail.v1
Google.Apis.Auth
MimeKit (發送 gmail 時使用)
建立認証用的網址:
建立一個 Action, 用來取得認証用的網址:
/// <summary>
/// 取得授權的項目
/// </summary>
static string[] Scopes = { GmailService.Scope.GmailSend };
// 和登入 google 的帳號無關
// 任意值,若未來有使用者認証,可使用使用者編號或登入帳號。
string Username = "ABC";
/// <summary>
/// 存放 client_secret 和 credential 的地方
/// </summary>
string SecretPath = @"D:\project\GmailTest\Data\Secrets";
/// <summary>
/// 認証完成後回傳的網址, 必需和 OAuth 2.0 Client Id 中填寫的 "已授權的重新導向 URI" 相同。
/// </summary>
string RedirectUri = $"https://localhost:44340/Home/AuthReturn";
/// <summary>
/// 取得認証用的網址
/// </summary>
/// <returns></returns>
public async Task<string> GetAuthUrl()
{
using (var stream = new FileStream(Path.Combine(SecretPath, "client_secret.json"), FileMode.Open, FileAccess.Read))
{
FileDataStore dataStore = null;
var credentialRoot = Path.Combine(SecretPath, "Credentials");
if (!Directory.Exists(credentialRoot))
{
Directory.CreateDirectory(credentialRoot);
}
//存放 credential 的地方,每個 username 會建立一個目錄。
string filePath = Path.Combine(credentialRoot, Username);
dataStore = new FileDataStore(filePath);
IAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = GoogleClientSecrets.Load(stream).Secrets,
Scopes = Scopes,
DataStore = dataStore
});
var authResult = await new AuthorizationCodeWebApp(flow, RedirectUri, Username)
.AuthorizeAsync(Username, CancellationToken.None);
return authResult.RedirectUri;
}
}
執行結果:
用 chrome 開啟產生的網址:
選取任一帳號,如果出現以下錯誤,請回到 "OAuth 同意畫面" 去新增測試使用者
因為應用程式尚未發布,所以會看到警告,勇敢的繼續下去
這裡會要求授權使用你的名義發送信件。(這是在程式中取得授權的項目 Scopes 中所指定的)
再繼續之後,會被重導至我們在 redirectUri 指定的網址。因為我們尚未完成,所以會看到錯誤,順便也可以看一下,會帶回哪一些參數。有 state, code, scope,共三個。
順便看一下,google 的套件會在 Credentials 的目錄下幫使用者建立一個目錄,在完成驗証前,會先放一個 System.String-oauth_XXX 的檔案,裡面的值和回傳的 state 是一樣的,這個應該是用來驗証回傳資料的。
接下來我們要新增 Action "AuthReturn" 如下:
public async Task<string> AuthReturn(AuthorizationCodeResponseUrl authorizationCode)
{
string[] scopes = new[] { GmailService.Scope.GmailSend };
using (var stream = new FileStream(Path.Combine(SecretPath, "client_secret.json"), FileMode.Open, FileAccess.Read))
{
//確認 credential 的目錄已建立.
var credentialRoot = Path.Combine(SecretPath, "Credentials");
if (!Directory.Exists(credentialRoot))
{
Directory.CreateDirectory(credentialRoot);
}
//暫存憑証用目錄
string tempPath = Path.Combine(credentialRoot, authorizationCode.State);
IAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow(
new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = GoogleClientSecrets.Load(stream).Secrets,
Scopes = scopes,
DataStore = new FileDataStore(tempPath)
});
//這個動作應該是要把 code 換成 token
await flow.ExchangeCodeForTokenAsync(Username, authorizationCode.Code, RedirectUri, CancellationToken.None).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(authorizationCode.State))
{
string newPath = Path.Combine(credentialRoot, Username);
if (tempPath.ToLower() != newPath.ToLower())
{
if (Directory.Exists(newPath))
Directory.Delete(newPath, true);
Directory.Move(tempPath, newPath);
}
}
return "OK";
}
}
再跑一次上面的流程,最後回到 AuthReturn
在 D:\project\GmailTest\Data\Secrets\Credentials\ABC 裡面會產生一個檔案: 這個就是我們的 token 了。
看一下裡面的內容, 有 access_token, refresh_token, scope 等等, 用途應該很好猜了.. 不知道各項目的目途也沒有關係。只要有這個 token 就可以了。
refresh_token 的效期請參考以下文件:
https://developers.google.com/identity/protocols/oauth2 。也可以參考下圖, 若是要用 gmail api 來發送通知信(例如連絡我們),紅色的地方是比較令人困擾的,例如 6 個月以上,沒有人留言,原來留下的 refresh_token 就失效了。使用者必需重新建立一個 refresh_token 。
最後來使用 gmail api 發送通知信, 直接看程式碼如下: 在這個過程中遇到最大的問題除了憑証問題之外,另一個問題是編碼。直到最後找到可以用 MimeKit 把 System.Net.Mail.MailMessage 編碼成 Gmail API 的格式才解決。程式碼如下:
public async Task<bool> SendTestMail()
{
var service = await GetGmailService();
GmailMessage message = new GmailMessage();
message.Subject = "標題";
message.Body = $"<h1>內容</h1>";
message.FromAddress = "bikehsu@gmail.com";
message.IsHtml = true;
message.ToRecipients = "bikehsu@gmail.com";
message.Attachments = new List<Attachment>();
string filePath = @"C:\Users\bike\Pictures\Vegetable_pumpkin.jpg"; //要附加的檔案
Attachment attachment1 = new Attachment(filePath);
message.Attachments.Add(attachment1);
SendEmail(message, service);
Console.WriteLine("OK");
return true;
}
async Task<GmailService> GetGmailService()
{
UserCredential credential = null;
var credentialRoot = Path.Combine(SecretPath, "Credentials");
if (!Directory.Exists(credentialRoot))
{
Directory.CreateDirectory(credentialRoot);
}
string filePath = Path.Combine(credentialRoot, Username);
using (var stream = new FileStream(Path.Combine(SecretPath, "client_secret.json"), FileMode.Open, FileAccess.Read))
{
credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.Load(stream).Secrets,
Scopes,
Username,
CancellationToken.None,
new FileDataStore(filePath));
}
var service = new GmailService(new BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = "Send Mail",
});
return service;
}
public class GmailMessage
{
public string FromAddress { get; set; }
public string ToRecipients { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
public bool IsHtml { get; set; }
public List<System.Net.Mail.Attachment> Attachments { get; set; }
}
public static void SendEmail(GmailMessage email, GmailService service)
{
var mailMessage = new System.Net.Mail.MailMessage();
mailMessage.From = new System.Net.Mail.MailAddress(email.FromAddress);
mailMessage.To.Add(email.ToRecipients);
mailMessage.ReplyToList.Add(email.FromAddress);
mailMessage.Subject = email.Subject;
mailMessage.Body = email.Body;
mailMessage.IsBodyHtml = email.IsHtml;
if (email.Attachments != null)
{
foreach (System.Net.Mail.Attachment attachment in email.Attachments)
{
mailMessage.Attachments.Add(attachment);
}
}
var mimeMessage = MimeKit.MimeMessage.CreateFromMailMessage(mailMessage);
var gmailMessage = new Google.Apis.Gmail.v1.Data.Message
{
Raw = Encode(mimeMessage)
};
Google.Apis.Gmail.v1.UsersResource.MessagesResource.SendRequest request = service.Users.Messages.Send(gmailMessage, "me");
request.Execute();
}
public static string Encode(MimeMessage mimeMessage)
{
using (MemoryStream ms = new MemoryStream())
{
mimeMessage.WriteTo(ms);
return Convert.ToBase64String(ms.GetBuffer())
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
}
收到的信件:
結論:
使用 Gmail API 最大的原因是要增加安全性,和舊的 smtp 不同的地方是,使用 gmail api 之後,客戶不需要提供 gmail 的帳號和密碼就可以讓系統使用 gmail 發送信件,不過由於 refresh_token 的效期問題,可能會造成無法發送通知信而沒有任何人發現的情況,整個實用性會變的很低。
另一個還沒有測試的部份是應用程的啟用。這個審核不知道會不很麻煩,不過可以而知的時,整個流程會花更多的時間。
取代的做法: 可能要改用 Amazon 的 SES 來寄信,而且為了避免每個小網站都要跑 SES 的建立流程,準備來寫一個 API 給各網站使用,可以發送簡單的通知信。
以上的程式碼可以在這裡下載: https://github.com/bikehsu/GmailTest
Bike, 2022/4/10 下午 09:31:15
寫一下, 以備不時之需, Codepage 和 Name 可以用來取得 encoding.
CodePage, DisplayName, Name
37, IBM EBCDIC (美國-加拿大), IBM037
437, OEM 美國, IBM437
500, IBM EBCDIC (國際), IBM500
708, 阿拉伯文 (ASMO 708), ASMO-708
720, 阿拉伯文 (DOS), DOS-720
737, 希臘文 (DOS), ibm737
775, 波羅的海文 (DOS), ibm775
850, 西歐語系 (DOS), ibm850
852, 中歐語系 (DOS), ibm852
855, OEM 斯拉夫文, IBM855
857, 土耳其文 (DOS), ibm857
858, OEM 多語系拉丁文 I, IBM00858
860, 葡萄牙文 (DOS), IBM860
861, 冰島文 (DOS), ibm861
862, 希伯來文 (DOS), DOS-862
863, 加拿大法文 (DOS), IBM863
864, 阿拉伯文 (864), IBM864
865, 北歐字母 (DOS), IBM865
866, 斯拉夫文 (DOS), cp866
869, 希臘文,現代 (DOS), ibm869
870, IBM EBCDIC (多語系拉丁文 2), IBM870
874, 泰文 (Windows), windows-874
875, IBM EBCDIC (希臘現代), cp875
932, 日文 (Shift-JIS), shift_jis
936, 簡體中文 (GB2312), gb2312
949, 韓文, ks_c_5601-1987
950, 繁體中文 (Big5), big5
1026, IBM EBCDIC (土耳其拉丁文 5), IBM1026
1047, IBM 拉丁文 1, IBM01047
1140, IBM EBCDIC (美國-加拿大-歐洲), IBM01140
1141, IBM EBCDIC (德國-歐洲), IBM01141
1142, IBM EBCDIC (丹麥-挪威-歐洲), IBM01142
1143, IBM EBCDIC (芬蘭-瑞典-歐洲), IBM01143
1144, IBM EBCDIC (義大利-歐洲), IBM01144
1145, IBM EBCDIC (西班牙-歐洲), IBM01145
1146, IBM EBCDIC (英國-歐洲), IBM01146
1147, IBM EBCDIC (法國-歐洲), IBM01147
1148, IBM EBCDIC (國際-歐洲), IBM01148
1149, IBM EBCDIC (冰島-歐洲), IBM01149
1200, Unicode, utf-16
1201, Unicode (位元組由大到小), utf-16BE
1250, 中歐語系 (Windows), windows-1250
1251, 斯拉夫文 (Windows), windows-1251
1252, 西歐語系 (Windows), Windows-1252
1253, 希臘文 (Windows), windows-1253
1254, 土耳其文 (Windows), windows-1254
1255, 希伯來文 (Windows), windows-1255
1256, 阿拉伯文 (Windows), windows-1256
1257, 波羅的海文 (Windows), windows-1257
1258, 越南文 (Windows), windows-1258
1361, 韓文 (Johab), Johab
10000, 西歐語系 (Mac), macintosh
10001, 日文 (Mac), x-mac-japanese
10002, 繁體中文 (Mac), x-mac-chinesetrad
10003, 韓文 (Mac), x-mac-korean
10004, 阿拉伯文 (Mac), x-mac-arabic
10005, 希伯來文 (Mac), x-mac-hebrew
10006, 希臘文 (Mac), x-mac-greek
10007, 斯拉夫文 (Mac), x-mac-cyrillic
10008, 簡體中文 (Mac), x-mac-chinesesimp
10010, 羅馬尼亞文 (Mac), x-mac-romanian
10017, 烏克蘭文 (Mac), x-mac-ukrainian
10021, 泰文 (Mac), x-mac-thai
10029, 中歐語系 (Mac), x-mac-ce
10079, 冰島文 (Mac), x-mac-icelandic
10081, 土耳其文 (Mac), x-mac-turkish
10082, 克羅埃西亞文 (Mac), x-mac-croatian
12000, Unicode (UTF-32), utf-32
12001, Unicode (UTF-32 位元組由大到小), utf-32BE
20000, 繁體中文 (CNS), x-Chinese-CNS
20001, TCA 台灣, x-cp20001
20002, 繁體中文 (Eten), x-Chinese-Eten
20003, IBM5550 台灣, x-cp20003
20004, TeleText 台灣, x-cp20004
20005, Wang 台灣, x-cp20005
20105, 西歐語系 (IA5), x-IA5
20106, 德文 (IA5), x-IA5-German
20107, 瑞典文 (IA5), x-IA5-Swedish
20108, 挪威文 (IA5), x-IA5-Norwegian
20127, US-ASCII, us-ascii
20261, T.61, x-cp20261
20269, ISO-6937, x-cp20269
20273, IBM EBCDIC (德國), IBM273
20277, IBM EBCDIC (丹麥-挪威), IBM277
20278, IBM EBCDIC (芬蘭-瑞典), IBM278
20280, IBM EBCDIC (義大利), IBM280
20284, IBM EBCDIC (西班牙), IBM284
20285, IBM EBCDIC (UK), IBM285
20290, IBM EBCDIC (日文片假名), IBM290
20297, IBM EBCDIC (法國), IBM297
20420, IBM EBCDIC (阿拉伯文), IBM420
20423, IBM EBCDIC (希臘文), IBM423
20424, IBM EBCDIC (希伯來文), IBM424
20833, IBM EBCDIC (韓文擴充), x-EBCDIC-KoreanExtended
20838, IBM EBCDIC (泰國), IBM-Thai
20866, 斯拉夫文 (KOI8-R), koi8-r
20871, IBM EBCDIC (冰島), IBM871
20880, IBM EBCDIC (斯拉夫俄文), IBM880
20905, IBM EBCDIC (土耳其), IBM905
20924, IBM 拉丁文 1, IBM00924
20932, 日文 (JIS 0208-1990 和 0212-1990), EUC-JP
20936, 簡體中文 (GB2312-80), x-cp20936
20949, 韓文 Wansung, x-cp20949
21025, IBM EBCDIC (斯拉夫塞爾維亞文-保加利亞文), cp1025
21866, 斯拉夫文 (KOI8-U), koi8-u
28591, 西歐語系 (ISO), iso-8859-1
28592, 中歐語系 (ISO), iso-8859-2
28593, 拉丁文 3 (ISO), iso-8859-3
28594, 波羅的海文 (ISO), iso-8859-4
28595, 斯拉夫文 (ISO), iso-8859-5
28596, 阿拉伯文 (ISO), iso-8859-6
28597, 希臘文 (ISO), iso-8859-7
28598, 希伯來文 (ISO-Visual), iso-8859-8
28599, 土耳其文 (ISO), iso-8859-9
28603, 愛沙尼亞文 (ISO), iso-8859-13
28605, 拉丁文 9 (ISO), iso-8859-15
29001, 歐洲, x-Europa
38598, 希伯來文 (ISO-Logical), iso-8859-8-i
50220, 日文 (JIS), iso-2022-jp
50221, 日文 (JIS-Allow 1 byte Kana), csISO2022JP
50222, 日文 (JIS-Allow 1 byte Kana - SO/SI), iso-2022-jp
50225, 韓文 (ISO), iso-2022-kr
50227, 簡體中文 (ISO-2022), x-cp50227
51932, 日文 (EUC), euc-jp
51936, 簡體中文 (EUC), EUC-CN
51949, 韓文 (EUC), euc-kr
52936, 簡體中文 (HZ), hz-gb-2312
54936, 簡體中文 (GB18030), GB18030
57002, ISCII 梵文語系, x-iscii-de
57003, ISCII 孟加拉文, x-iscii-be
57004, ISCII 坦米爾文, x-iscii-ta
57005, ISCII 特拉古文, x-iscii-te
57006, ISCII 阿薩姆文, x-iscii-as
57007, ISCII 歐利亞文, x-iscii-or
57008, ISCII 坎那達文, x-iscii-ka
57009, ISCII 馬來亞拉姆文, x-iscii-ma
57010, ISCII 古吉拉特文, x-iscii-gu
57011, ISCII 旁遮普語, x-iscii-pa
65000, Unicode (UTF-7), utf-7
65001, Unicode (UTF-8), utf-8
Bike, 2016/11/12 上午 10:12:03