Node.js 應用篇 - 使用 Nodemailer 來發送 Email

Web

前言

這一篇我將會記錄一下如何用 Nodemailer 來發送 Email,而這一篇將會記錄使用 Gmail 以及 OAuth2 跟帳號密碼(應用程式密碼)的兩種方式來發送 Email。

事前準備

首先這邊我會先提供事前的準備環境,你可以透過以下連結取用

這個環境相對單純一點,我只有安裝 nodemailergoogleapisdotenv 套件,其餘都是 Express 產生器的東西。

如果你不想使用這個初始範本,你可以自己建立一個 Express 專案,然後安裝 nodemailergoogleapisdotenv 套件即可。

1
npm install nodemailer googleapis dotenv

Nodemailer

雖然早期我有寫過關於 全端勇士之路 Node.js-OAuth 2.0 & nodemailer & Gmail,但我認為這一篇有點過於老舊,所以我決定重新寫一篇。

那麼 Nodemailer 是什麼神奇的東西呢?簡單來講它是一個 Node.js 的套件,可以讓我們在 Node.js 環境下發送 Email,而這個功能對於一個網站來說是非常重要的,因為我們可以透過 Email 來發送驗證碼、通知信、訂閱信等等,所以在實戰上可以說是非常常見的。

以官方文件來講底下就有提供一個算是滿完整的 Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
"use strict";
const nodemailer = require("nodemailer");

const transporter = nodemailer.createTransport({
host: "smtp.forwardemail.net",
port: 465,
secure: true,
auth: {
// TODO: replace `user` and `pass` values from <https://forwardemail.net>
user: '[email protected]',
pass: 'REPLACE-WITH-YOUR-GENERATED-PASSWORD'
}
});

// async..await is not allowed in global scope, must use a wrapper
async function main() {
// send mail with defined transport object
const info = await transporter.sendMail({
from: '"Fred Foo 👻" <[email protected]>', // sender address
to: "[email protected], [email protected]", // list of receivers
subject: "Hello ✔", // Subject line
text: "Hello world?", // plain text body
html: "<b>Hello world?</b>", // html body
});

console.log("Message sent: %s", info.messageId);
// Message sent: <[email protected]>

//
// NOTE: You can go to https://forwardemail.net/my-account/emails to see your email delivery status and preview
// Or you can use the "preview-email" npm package to preview emails locally in browsers and iOS Simulator
// <https://github.com/forwardemail/preview-email>
//
}

main().catch(console.error);

但是今天我們的目標是要使用 Gmail 來發送 Email,並且是搭配 OAuth2 的方式來發送,所以我們就來看看怎麼做吧。

所以我們必須要先了解 Gmail 的限制,那麼有哪些限制呢?這邊我就只列出比較需要注意的限制:

  • 每日發送限制:500 封
  • 單一電子郵件收件者超過 500 封
  • 每封電子郵件大小上限:25 MB

其他部分舉凡…濫發垃圾郵件、濫發廣告郵件、濫發病毒郵件等等,都是不被允許的,如果被 Google 發現,你的帳號可能會被鎖定,所以請務必注意哩。

使用 OAuth 2.0 寄信

早期許多 Nodemailer 文章教學都是介紹使用帳號密碼的方式來介紹,但是後來 Google 開始限制使用這種方式,因為使用傳統帳號密碼來發送信件的方式並不是很安全,所以 Google 開始推出 OAuth2 的方式來取代傳統帳號密碼的方式,但這種方式其實比較複雜,因為你必須要先取得授權,所以前面一開始我也會先介紹比較複雜的部分。

建立專案

首先這邊我們要先到 「Google Cloud Platform」建立一個新專案,當然你也可以使用舊專案,但是我建議你還是建立一個新專案,因為這樣比較不會搞混。

建立專案

專案名稱你可以自己取,這邊我取名為「nodemailer-example」

新增專案

建立過程中稍微等它一下下…

建立中...

建立成功後,你可以在通知或是剛剛選取專案的地方找到剛剛建立的專案,然後點選進去

專案

專案

啟用 API 和服務

接著請你點一下左邊漢堡選單,然後點選「已啟用的 API 和服務」

已啟用的 API 和服務

接著點一下上方「啟用 API 和服務」

啟用 API 和服務

接著搜尋「Gmail API」,然後點選「Gmail API」

Gmail API

接著點一下「啟用」

Gmail API

設定 OAuth 同意畫面

接著回到「API 和服務」,然後點選「憑證」

憑證

要「建立憑證」之前,如果你跟我一樣是第一次設定的話,你會看到一個提示,提示你要先設定 OAuth 同意畫面,所以請你點選「設定同意畫面」

設定同意畫面

接著請你選擇「外部」,然後點選「建立」

同意畫面

接下來會進入「編輯應用程式註冊申請」畫面,這邊要填寫一些資料,除了底下這幾個欄位是必填的,其餘都是選填的:

  • 應用程式名稱:你可以自己取名,這邊我取名為「nodemailer-example」
  • 使用者支援電子郵件:選你自己 Email
  • 開發人員聯絡資訊:填寫你自己 Email

如果不確定,你可以依照我圖片這樣填寫就好

編輯應用程式註冊申請

接下來會到達「範圍」的設定頁面,這邊直接繼續就好

範圍

接下來要設定「測試使用者」,基本上如果你這個應用程式發布狀態是「測試中」的話,你就必須設定一些可以使用這個應用程式的測試使用者,這邊你可以點一下 「Add Users」並輸入自己 Google 帳號,如果是正式環境的話則不用,那我這邊就先不設定直接點選「儲存並繼續」。

基本上你到達「摘要」時,就完成了「OAuth 同意畫面」的設定囉

摘要

建立憑證

接下來你就可以回到「憑證」的頁面,然後點選「建立憑證」,接著點選「OAuth 用戶端 ID」

OAuth 用戶端 ID

接著在應用程式類型欄位選擇「網路應用程式」,然後輸入以下欄位:

  • 名稱:你可以自己取名,這邊我取名為「nodemailer-example-web」
  • 已授權的重新導向 URI:請輸入 http://localhost:3000/auth/google/callback

憑證

Note
在我先前的文章中,我有提到過這個欄位要填寫「https://developers.google.com/oauthplayground/」的網址,但這邊我們要換另一種方式。

接下來按下「建立」後就可以取得 Client ID 與 Client Secret,這兩個請不要外流,因為這兩個是用來驗證你的身份的,如果被人拿去亂用,你的帳號可能會被 Google 鎖定(你也可以直接下載 JSON 檔案,裡面就有這兩個資訊)

到這邊為止,我們的 Gmail OAuth 相關前置動作就終於準備好了,接下來就可以開始撰寫程式碼囉~

撰寫 Gmail OAuth 程式碼

那麼前面我已經有先提供基本的環境,所以我們就可以直接開始撰寫程式碼了。

Note
請務必確定你的專案內有無安裝 nodemailergoogleapisdotenv 這幾個套件,否則稍後可能會出現錯誤無法往下練習。

建立 Env

首先這邊請你在專案底下建立一個 .env 檔案,內容如下:

1
2
3
CLIENT_ID=你的 Client ID
CLIENT_SECRET=你的 Client Secret
REDIRECT_URI=http://localhost:3000/auth/google/callback

這幾個資訊都可以在剛剛建立憑證時取得,如果你有下載 JSON 檔案的話,你可以直接打開 JSON 檔案,裡面就有這些資訊

JSON

建立 Google OAuth2 Client

接下來請你在根目錄下建立一個資料夾,名稱為 config,然後在裡面建立一個檔案,名稱為 googleOAuth2Client.js,內容如下:

1
2
3
4
5
6
7
8
9
10
11
require('dotenv').config();

const { google } = require('googleapis');

const googleOAuth2Client = new google.auth.OAuth2(
process.env.CLIENT_ID,
process.env.CLIENT_SECRET,
process.env.REDIRECT_URI
);

module.exports = googleOAuth2Client;

這邊我們使用 googleapis 套件來建立 Google OAuth2 Client,這邊我們使用 google.auth.OAuth2 來建立,並且傳入三個參數:

  • CLIENT_ID:你的 Client ID
  • CLIENT_SECRET:你的 Client Secret
  • REDIRECT_URI:你的 Redirect URI

建立 Google OAuth2 URL

接下來請你在根目錄下建立一個資料夾,名稱為 routes,然後在裡面建立一個檔案,名稱為 auth.js,內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var express = require('express');
var router = express.Router();

const googleOAuth2Client = require('../config/googleOAuth2Client');

const SCOPES = [
'https://mail.google.com/',
];

router.get('/login', (req, res) => {
const authUrl = googleOAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
res.redirect(authUrl);
});

router.get('/google/callback', async (req, res) => {
const code = req.query.code;
try {
const { tokens } = await googleOAuth2Client.getToken(code)

googleOAuth2Client.setCredentials(tokens);
req.session.tokens = tokens;

res.redirect('/email/user');
} catch (err) {
console.error('Error authenticating with Google:', err);
res.status(500).send('Error authenticating with Google');
}
});

module.exports = router;

接著到 app.js 加入以下

1
2
3
4
5
var authRouter = require('./routes/auth');

// ... 略過大量程式碼

app.use('/auth', authRouter);

接下來你就可以透過網址 http://localhost:3000/auth/login 來取得 Google OAuth2 URL,這時候你會被導向到 Google 登入頁面

Google 登入頁面

點選你要登入的帳號,接下來會出現一個警告畫面,這邊不用太擔心畢竟是你的應用程式,只需要點開「進階」,然後點選「前往 nodemailer-example(不安全)」即可

不安全的應用程式

接下來這邊按下「允許」

允許

完成後就會重新導向回 http://localhost:3000/auth/google/callback 這個網址,並且取得 code,這個 code 就是用來取得 tokens 的,而 tokens 就是用來驗證你的身份的。

而這邊我們也將 tokens 儲存在 session 中,這樣我們就可以在後續的程式碼中使用 tokens 來驗證身份。

但是這邊你應該會發生一個錯,也就是以下

1
Error authenticating with Google: TypeError: Cannot set properties of undefined (setting 'tokens')

這個原因是預設 Express 產生器並沒有啟用 session 功能,所以我們必須要自己啟用 session 功能,這邊我們使用 express-session 來啟用 session 功能,所以請你在根目錄下執行以下指令:

1
npm install express-session

接著到 app.js 加入以下

1
2
3
4
5
6
7
8
9
10
var session = require('express-session');

// ... 略過大量程式碼

app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
cookie: { secure: false }
}))

這樣你才不會發生「TypeError: Cannot set properties of undefined」的錯誤唷。

Note
express-session 有一個小雷點要注意一下,官方所提供的範例 secure 屬性是 true,這就代表著你本地開發時,你的 session 會沒有辦法儲存下來,只要一換頁面就會被清除,所以這邊我將 secure 屬性改為 false,這樣你就可以在本地開發時使用 session 功能了。

建立 Nodemailer OAuth2

接下來請你到 routes 資料夾的 index.js,我們要在這邊建立 Nodemailer,內容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
router.post('/user/send', (req, res) => {
const {
refresh_token,
access_token,
} = req.session.tokens;


const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
type: 'OAuth2',
user: '你要用來發送信件的 Gmail',
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
refreshToken: refresh_token,
accessToken: access_token,
},
});

const mailOptions = {
from: '你要用來發送信件的 Gmail',
to: '你要發送的對象',
subject: '這是信件的主旨',
text: '‘這是信件的內容',
};

transporter.sendMail(mailOptions, (err, info) => {
if (err) {
console.error(err);
res.status(500).send('Error sending email');
} else {
console.log(info);
res.send('Email sent');
}
});
});

module.exports = router;

基本上到這邊為止,你就完成了 Nodemailer 的設定,接下來你可以透過網頁來發送出信件囉~

使用帳號密碼(應用程式密碼)來發送 Email

接下來我想介紹另一種方式發送信箱,為什麼呢?

因為前面的做法是使用了 OAuth 的方式來發送信件,那麼使用 OAuth 的問題在於,如果我今天登入的使用者不是 A 而是 B 的話,那麼就無法寄出,因為我們使用的 Token 與 Refresh Token 都是 A 的,所以這邊我想介紹另一種方式,那就是使用帳號密碼直接寫死並登入,這樣就不會有上述的問題了。

那麼這邊有些人應該有嘗試過使用帳號密碼方式寄送 Gmail,應該大多都有遇過以下錯誤訊息

1
Error: Invalid login: 535-5.7.8 Username and Password not accepted. Learn more at 535 5.7.8  https://support.google.com/mail/?p=BadCredentials k17-20020aa78211000000b00682562bf477sm3695625pfi.82 - gsmtp

這個錯誤訊息是因為 Google 開始限制使用帳號密碼的方式來寄送信件,早期我們可以到 Google 設定中開啟「低安全性應用程式存取權」,這樣就可以使用帳號密碼的方式來寄送信件,但是後來 Google 也開始限制這個功能,所以我們就無法使用這個方式來寄送信件了。

所以這邊我要教另一種方式哩~

撰寫 Env

接下來請你在原本的 .env 補充以下:

1
2
GMAIL_USER=你要用來發送信件的 Gmail
GMAIL_PASS=你要用來發送信件的 Gmail 密碼

接下來準備來也是要去設定一下 Google 的部分,但不會像剛剛那樣複雜。

啟用 Google 設定

首先請你到 Google 設定中心,然後點選「安全性」

安全性

如果你沒有啟用二次驗證的話,這邊會建議你啟用後再繼續,避免你無法往後操作唷

二次驗證

Note
後來我建立新帳號實測,Google 是強制你一定要開啟二次驗證的唷。

如果你本身已經有啟用的話,那麼你就可以點一下兩步驟驗證,然後輸入密碼後滾到最下方找到「應用程式密碼」

應用程式密碼

點進去後,你選擇一下應用程式和裝置

  • 應用程式:選擇「郵件」
  • 裝置:選擇「其他(自訂名稱)」

應用程式

接著會跳出要你命名,你可以依照自己需求命名,所以你可以命名為「nodemailer-example」,然後點選「產生」

應用程式命名

點下產生後,你就會取得一組 16 字元的密碼,請好好保存不要外流,這會是我們後續使用的密碼

系統產生的應用程式密碼

Note
Google 幾乎時常在更新這個介面,所以我往往文章會來不及更新,若又更新的話再麻煩告知我一聲,我會再更新文章內容。

底下是 2023 年 10 月 20 日補上的畫面,如果你發現你找不到「應用程式密碼」的話也不用擔心,你可以在上方搜尋搜尋「應用程式密碼」

應用程式密碼

接著你就可以在下方 App Name 輸入「nodemail」,然後點選「建立」

App name

建立後,你就會跳出一組類似亂碼的密碼,這就是我們要使用的密碼,請好好保存不要外流唷~

應用程式密碼

撰寫程式碼

接下來就是撰寫程式碼了,你只需要將你要用來發信的 Email 帳號跟剛剛生成的應用程式密碼填入到 Env

1
2
[email protected] # 不用擔心這是假的
GMAIL_PASS=d81ydvsldqiwdhnc # 不用擔心這是假的

Note
系統產生的應用程式密碼看起來會有空格,請去除不要複製空格;GMAIL_USER 就是你的 Gmail 帳號,而 GMAIL_PASS 就是你的應用程式密碼。

接下來請你到 routes 資料夾的 email.js,貼入以下內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
router.post('/server/send', async (req, res, next)=> {
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.GMAIL_USER,
pass: process.env.GMAIL_PASS,
},
});

await transporter.verify();

const mailOptions = {
from: process.env.GMAIL_USER,
to: '你要發送的對象信箱',
subject: '這是信件的主旨',
text: '這是信件的內容',
};

transporter.sendMail(mailOptions, (err, info) => {
if (err) {
console.error(err);
res.status(500).send('Error sending email');
} else {
console.log(info);
res.send('Email sent');
}
});
});

恭喜你,到這邊為止你就可以正常發信了,而且不是透過 OAuth2 的方式,而是透過帳號密碼的方式,這樣就不會有上述的問題了~

完整範例程式碼

最後這邊我也附上完整的程式碼,你可以參考一下

這一份程式碼你可以直接在畫面上點擊按鈕觸發發送信件的,當然,如果你要作為實戰使用還是需要做調整一下哩。

結語

不得不說這一塊真的是有點複雜且混亂,而且目前網路上的資源滿零散的,自己 Try 了不少次才弄出來,所以這邊我就整理一下,希望對你有幫助。

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ