使用 Node.js、Nodemailer、SMTP、Gmail 和 OAuth2 安全地发送电子邮件
网上很多关于如何配置 Nodemailer 以使用 Gmail 的解决方案都需要您启用安全性较低的应用访问权限。如果您觉得这太过冒险,那么您来对地方了!本文将教您如何安全地配置 Nodemailer 和 Gmail。
我们先来了解一下Nodemailer是什么。
Nodemailer是一个模块,它使从 Node.js 应用程序发送电子邮件变得异常简单。
以下是发送电子邮件的主要步骤:
- 使用 SMTP 或其他传输机制创建传输器(用于发送电子邮件的对象)。
- 设置消息选项(谁向谁发送什么内容)
- 通过调用传输器上的 sendMail 方法发送电子邮件。
安全性较低的配置
在介绍配置 Nodemailer 和 Gmail 的安全解决方案之前,让我们先来看看不太安全的解决方案。
参考以上步骤,以下是相应的代码:
//Step 1: Creating the transporter
const transporter = nodemailer.createTransport({
service: "Gmail",
auth: {
user: "******@gmail.com",
pass: "gmail_password"
}
});
//Step 2: Setting up message options
const messageOptions = {
subject: "Test",
text: "I am sending an email from nodemailer!",
to: "put_email_of_the_recipient",
from: "put_email_of_sender"
};
//Step 3: Sending email
transporter.sendMail(messageOptions);
注意:上述解决方案需要您在 Google 帐户设置中启用安全性较低的应用访问权限才能生效。
现在,我们来看看更安全的解决方案。
步骤 1:创建 Google 项目
请访问Google 开发者控制台创建项目。我们需要创建一个项目,以便创建必要的 API 凭据。
进入控制台后,点击左上角的下拉菜单。
创建项目窗口加载完毕后,单击“新建项目”。
输入项目名称,然后点击创建。
步骤 2:创建 OAuth 2.0 API 凭证
要获取客户端密钥和客户端 ID,我们需要创建 OAuth 凭据。客户端 ID 用于向 Google 的 OAuth 服务器标识我们的应用,以便我们能够通过 Nodemailer 安全地发送电子邮件。
首先,请在左侧边栏中选择“凭据” 。选择后,应出现以下屏幕:
点击“创建凭据”后,将出现一个下拉菜单。在下拉菜单中,选择“OAuth 客户端 ID”。
在继续之前,我们需要配置同意屏幕。当应用程序提供 Google 登录功能时,同意屏幕的配置至关重要。此外,必须完成此配置才能创建客户端 ID 和密钥。
点击配置同意屏幕。
选择“外部用户”作为用户类型,然后单击“创建”。
多步骤表单出现后,请填写每个步骤的必填字段。
到达最后一步后,点击返回仪表盘。
返回“创建 OAuth 客户端 ID”屏幕(带有“配置同意屏幕”按钮的页面)。如果同意屏幕配置成功,则会显示一个应用程序类型下拉菜单。选择“Web 应用程序”并填写必填字段。
在“已授权重定向 URI”部分,请确保添加https://developers.google.com/oauthplayground。
现在点击创建!
复制屏幕上显示的客户端 ID 和客户端密钥并保存以备后用。
步骤 3:OAuth 2.0 测试平台
我们还需要一个刷新令牌和一个访问令牌,它们可以根据客户端 ID 和密钥生成。
首先访问https://developers.google.com/oauthplayground。
进入页面后,点击齿轮图标,勾选“使用您自己的 OAuth 凭据”复选框。然后粘贴之前获取的客户端 ID 和密钥。
在左侧“选择和授权 API”部分下方,找到 Gmail API v1 并选择https://mail.google.com/。或者,您也可以在“输入您自己的范围”字段中输入https://mail.google.com/。
现在点击“授权 API”。
如果出现以下页面,请点击允许,以便 Google OAuth 2.0 Playground 可以访问您的 Google 帐户。
重定向回 OAuth 2.0 Playground 后,
单击“交换授权码以获取令牌”部分下的“交换授权码以获取令牌”按钮。
刷新令牌和访问令牌生成后,复制刷新令牌并保存以备后用。
第四步:编写代码
现在我们有了客户端 ID、客户端密钥和刷新令牌,就可以用它们来发送电子邮件了!
首先为应用程序创建一个新文件夹,然后使用 cd 键进入该文件夹。
mkdir sendEmails
cd sendEmails
要将应用程序初始化为 node 项目,请运行npm init。
接下来,我们来安装 npm 包。
//Note: dotenv is a dev dependency
npm i nodemailer googleapis && npm i dotenv --save-dev
谷歌API
- 用于使用 Google API 的库
- 将用于动态生成访问令牌
dotenv
- 用于使用环境变量的库
- 我们将使用这种方法来避免在代码中使用 API 密钥。
与其他 NPM 包一样,我们首先需要引入这些包。因此,请创建一个index.js文件并添加以下内容:
const nodemailer = require("nodemailer");
const { google } = require("googleapis");
const OAuth2 = google.auth.OAuth2;
环境变量设置
通常情况下,在代码中使用敏感信息(例如 API 密钥)时,最佳实践是使用环境变量。
.env在项目根目录下创建一个文件,并添加以下内容:
EMAIL=YOUR_GOOGLE_EMAIL_HERE
REFRESH_TOKEN=PASTE_REFRESH_TOKEN_HERE
CLIENT_SECRET=PASTE_CLIENT_SECRET_HERE
CLIENT_ID=PASTE_CLIENT_ID_HERE
现在,我们需要在引入所有包之前,先引入并调用配置方法:
require('dotenv').config();
const nodemailer = require("nodemailer");
const { google } = require("googleapis");
const OAuth2 = google.auth.OAuth2;
process.env现在它拥有文件中定义的键和值.env。例如,我们可以通过以下方式访问客户端 ID:process.env.CLIENT_ID
制造传送器
我们首先需要使用之前的所有信息(客户端 ID、客户端密钥和 OAuth Playground URL)创建一个 OAuth 客户端。该 OAuth 客户端将允许我们从刷新令牌动态创建访问令牌。
“等等,为什么我们不能直接使用 OAuth Playground 中的访问令牌呢?或者,为什么要动态创建访问令牌呢?”
嗯,如果你之前注意到的话,会有一条消息表明访问令牌将在 3582 秒后过期。
以下代码创建 OAuth 客户端并向其提供刷新令牌:
const oauth2Client = new OAuth2(
process.env.CLIENT_ID,
process.env.CLIENT_SECRET,
"https://developers.google.com/oauthplayground"
);
oauth2Client.setCredentials({
refresh_token: process.env.REFRESH_TOKEN
});
由于通过 OAuth 客户端获取访问令牌是一个异步过程,我们需要将上述操作包装在一个异步函数中。
const createTransporter = async () => {
const oauth2Client = new OAuth2(
process.env.CLIENT_ID,
process.env.CLIENT_SECRET,
"https://developers.google.com/oauthplayground"
);
oauth2Client.setCredentials({
refresh_token: process.env.REFRESH_TOKEN
});
};
现在,我们可以通过调用 getAccessToken 方法来获取访问令牌。
const accessToken = await new Promise((resolve, reject) => {
oauth2Client.getAccessToken((err, token) => {
if (err) {
reject("Failed to create access token :(");
}
resolve(token);
});
});
你可能想知道,为什么要把 `getAccessToken` 方法调用包装在一个 Promise 中?这是因为 `getAccessToken` 需要一个回调函数,并且不支持使用 `async/await`。因此,我们可以将其包装在一个 Promise 中,或者在回调函数内部创建传输器。我更喜欢前者,因为它更易读。
现在进入主要部分,创建运输器对象本身。要创建它,我们需要将一些配置传递给 createTransport 方法。
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
type: "OAuth2",
user: process.env.EMAIL,
accessToken,
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
refreshToken: process.env.REFRESH_TOKEN
}
});
注意:如果您收到“未授权客户端”错误,请尝试将以下内容添加到上面的 JS 对象中。
tls: {
rejectUnauthorized: false
}
运输器创建完成后,完整的 createTransporter 函数应如下所示:
const createTransporter = async () => {
const oauth2Client = new OAuth2(
process.env.CLIENT_ID,
process.env.CLIENT_SECRET,
"https://developers.google.com/oauthplayground"
);
oauth2Client.setCredentials({
refresh_token: process.env.REFRESH_TOKEN
});
const accessToken = await new Promise((resolve, reject) => {
oauth2Client.getAccessToken((err, token) => {
if (err) {
reject();
}
resolve(token);
});
});
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
type: "OAuth2",
user: process.env.EMAIL,
accessToken,
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
refreshToken: process.env.REFRESH_TOKEN
}
});
return transporter;
};
请注意,我们返回的是传输器,而不是编写发送电子邮件的代码。为了提高代码可读性和分离关注点,我们将创建另一个函数来发送电子邮件。
现在我们来创建 sendEmail 函数。该函数会调用 createTransporter 函数,然后再调用传输器上存在的 sendMail 方法。
//emailOptions - who sends what to whom
const sendEmail = async (emailOptions) => {
let emailTransporter = await createTransporter();
await emailTransporter.sendMail(emailOptions);
};
现在剩下的就是调用 sendEmail 函数发送电子邮件了:
sendEmail({
subject: "Test",
text: "I am sending an email from nodemailer!",
to: "put_email_of_the_recipient",
from: process.env.EMAIL
});
完整的电子邮件选项列表可在https://nodemailer.com/message/找到。
node index.js从终端/命令行运行,瞧!这就是我们从应用程序发送的电子邮件!
供您参考,以下是已完成的index.js文件:
require("dotenv").config();
const nodemailer = require("nodemailer");
const { google } = require("googleapis");
const OAuth2 = google.auth.OAuth2;
const createTransporter = async () => {
const oauth2Client = new OAuth2(
process.env.CLIENT_ID,
process.env.CLIENT_SECRET,
"https://developers.google.com/oauthplayground"
);
oauth2Client.setCredentials({
refresh_token: process.env.REFRESH_TOKEN
});
const accessToken = await new Promise((resolve, reject) => {
oauth2Client.getAccessToken((err, token) => {
if (err) {
reject("Failed to create access token :(");
}
resolve(token);
});
});
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
type: "OAuth2",
user: process.env.EMAIL,
accessToken,
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
refreshToken: process.env.REFRESH_TOKEN
}
});
return transporter;
};
const sendEmail = async (emailOptions) => {
let emailTransporter = await createTransporter();
await emailTransporter.sendMail(emailOptions);
};
sendEmail({
subject: "Test",
text: "I am sending an email from nodemailer!",
to: "put_email_of_the_recipient",
from: process.env.EMAIL
});
















