如今,能够通过网页应用程序发送电子邮件来进行沟通是非常重要的。这有助于企业与潜在客户保持联系,安全地验证用户身份,并发送诸如密码重置等重要通知。
但是,有时候,将运行良好的电子邮件功能部署到云端后,却会出现一些意外且令人沮丧的错误。你在本地构建并测试了后端代码,发现它运行得非常顺利;但当将其部署到云端后,应用程序突然就无法发送电子邮件了。
在本文中,你将了解到为什么在Render或Heroku这样的云平台上,你的电子邮件设置会失败,导致这一问题的根本原因是什么,以及如何利用Brevo的HTTP API巧妙地绕过这些限制。
让我们马上开始吧。
目录
先决条件
要想充分利用本教程的内容,你需要具备以下一些基础知识:
-
JavaScript与Node.js:如果对JavaScript在服务器端的工作原理有基本的了解,那么学习这个项目会更容易。
-
REST API:你应该了解如何使用Node.js中的`fetch()`方法来发送HTTP请求(如POST和GET请求)。
-
Express.js:了解如何创建基本的服务器路由会对学习有所帮助,因为我们会构建一个实际的控制器。
-
需要了解Nodemailer是什么,以及云托管平台(如Render或Heroku)的运作原理。
我们将使用的工具
在我最近的一个项目中,我设计了一个复杂的认证流程,用户需要接收发送到他们邮箱的一次性密码才能完成注册。我配置了Nodemailer,关联了我的Gmail账户,并在`localhost`上进行了测试,结果电子邮件很快就成功发送到了用户的邮箱里。
但是当我将后端服务部署到Render平台上时,整个注册流程都出现了问题。经过深入排查,我找到了问题出现的原因,也知道了如何彻底解决它。现在既然我已经了解了其中的原理,就想要和大家分享这些知识。
问题所在:Nodemailer与SMTP被阻断
那么,到底是什么导致了这个问题呢?
Nodemailer是一个非常受欢迎的Node.js模块,它可以帮助我们高效地发送电子邮件。通常,开发人员会使用它通过SMTP(简单邮件传输协议)连接到Gmail或Mailtrap等服务。当你的代码尝试发送邮件时,Nodemailer会通过端口587(用于STARTTLS连接)或端口465(用于SSL连接)来建立与邮件服务器的连接。
然而,像Render、Heroku、DigitalOcean和AWS这样的云服务提供商每天都要面对大量的自动发送垃圾邮件的行为。恶意用户经常会创建数千台免费级别的服务器,用来发送数以百万计的垃圾邮件。如果云服务提供商允许这种行为发生,他们的整个网络IP地址块就会被Gmail、Outlook和Yahoo列入黑名单。
为了保护自己的网络声誉,这些云服务提供商制定了一条严格的规定:在免费及基础套餐中,所有通过端口25、465和587发出的出站流量都会被严格阻断。
这意味着你的服务器实际上被防火墙隔在了外面。如果你查看服务器日志,你不会看到“密码无效”这样的错误信息,而是会看到类似以下的超时错误:
Error: connect ETIMEDOUT 142.250.102.108:587
at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1494:16)
你的代码并没有出现问题,只是因为在网络层被阻断了而已!
“现代”的陷阱:域名验证
当开发人员遇到这种障碍时,他们往往会尝试使用像Resend或SendGrid这样的基于API的邮件服务。这些工具确实非常有用,但它们也给初学者带来了一个新的问题:严格的域名认证要求。
要想在正式环境中使用Resend,你必须拥有自己的自定义域名(比如yourname.com),并且还需要配置DNS记录(SPF、DKIM和DMARC)。如果你没有自己的域名,Resend的沙箱模式会严格限制你只能给自己发送邮件,而无法向你的真实用户发送邮件。
对于那些刚刚打算启动一个个人项目的人来说,仅仅为了测试邮件的发送而购买域名,无疑是一个巨大的障碍。
终极解决方案:Brevo与HTTP API
我们需要一个能够满足以下两个条件的解决方案:
-
它必须能够绕过端口
587的防火墙限制。 -
它必须允许我们向任何人发送邮件,而无需购买自定义域名。
在这里,SMTP与REST API在架构上的区别就发挥了作用。SMTP是一种专门用于路由邮件的协议,而REST API则是通过标准的Web流量进行通信的,它使用的是HTTPS(端口443)。因此,云服务提供商无法阻断端口443,因为这样做会导致你的服务器无法从数据库中获取数据,也无法正常作为Web服务器运行。
请使用Brevo(原名Sendinblue)。Brevo是一个功能强大的电子邮件平台,您可以通过标准的REST API发送邮件。最棒的是,他们的免费套餐(每天300封邮件)支持单发者验证功能。您只需验证自己的Gmail地址,就可以向任何收件人发送邮件了!
通过使用HTTPS将JSON数据发送到Brevo的API,您的服务器会通过不受限制的443端口发送请求,从而完全绕过防火墙的限制。
现在您已经了解了我们将要使用的这些工具背后的原理,接下来我们就开始编写代码吧。
后端配置
首先,您需要设置开发环境。如果您的电脑上还没有安装Node.js,请访问他们的官网进行下载和安装。
在终端中运行npm init -y,这样就会生成用于管理项目并存储所有依赖关系的package.json文件。
接下来,运行npm install express dotenv。
您可能已经习惯使用nodemailer>来处理电子邮件相关任务。但由于我们会直接使用Node.js内置的fetch() API与Brevo的API进行交互,因此实际上完全不需要安装任何复杂的邮件处理库!我们希望让后端代码尽可能简洁。
Brevo配置设置
在编写发送邮件的函数之前,您首先需要配置Brevo,以便能够获取到您的API密钥。
-
访问Brevo官网并创建一个免费账户。
-
在设置过程中,系统会要求您输入发件人邮箱地址。请确保输入的是您的Gmail地址。系统会发送一封邮件,其中包含用于验证该地址所属权的链接。
-
验证通过后,进入控制面板,在右上角点击您的个人资料名称,然后从下拉菜单中选择SMTP & API选项。
-
进入API密钥页面,点击生成新的API密钥。给这个密钥起个名字,比如“MyWebApp”。
将生成的API密钥复制出来,并安全地保存在项目根目录下的.env文件中:
# .env文件
EMAIL_USER = yourverifiedemail@gmail.com
BREVO_API_KEY = xkeysib-your-generated-api-key-goes-here
创建发送邮件的函数
现在您已经获得了API密钥,并设置了环境变量,接下来就要开始编写后端代码了。
创建一个名为utils/email.js的文件。
首先,确保能够读取.env文件,这样就能轻松访问之前生成的配置信息:
require("dotenv").config();
// 我们将定义这个函数,让它能够接受动态参数
const sendEmail = async (options) => {
const brevoApiKey = process.env.BREVO_API_KEY;
const senderEmail = process.env.EMAIL_USER;
// 确保这些密钥确实存在
if (!brevoApiKey || !senderEmail) {
throw new Error("环境变量中缺少Brevo所需的配置信息。");
}
接下来,你需要组织好要发送的数据结构。这个JSON对象会明确告诉Brevo是谁发送了这封邮件、谁收到了邮件,以及邮件的内容是什么。具体操作方法如下:
const payload = {
sender: {
name: "我的超棒网页应用",
email: senderEmail, // 必须与你在Brevo平台上验证过的电子邮件地址一致
},
to: [
{
email: options.email, // 收件人的动态电子邮件地址
},
],
subject: options.subject,
htmlContent: options.html,
};
在上面的代码中,payload对象将你的信息安全地封装起来。我们使用了options.email、options.subject和options(html)这些参数,这样就可以用同一个函数来处理欢迎邮件、密码重置通知等各种场景了。
现在,我们需要创建实际的网络请求,将数据发送到Brevo的后端服务器。我们将使用POST方法进行发送,而且在发送之前,数据必须被转换成JSON格式。
try {
const response = await fetch("https://api.brevo.com/v3/smtp/email", {
method: "POST",
headers: {
"Content-Type": "application/json",
"api-key": brevoApiKey,
},
body: JSON.stringify(payload),
});
const result = await response.json();
if (!response.ok) {
throw new Error(`Brevo API错误:${JSON.stringify(result)}`);
}
console.log(`通过Brevo HTTP API成功将邮件发送给${options.email}!`);
} catch (error) {
console.error("错误详情:", error.message);
}
};
module.exports = sendEmail;
在上面的代码中,如果数据发送成功,终端会显示相应的成功日志;但如果由于API密钥输入错误等原因导致发送失败,系统会抛出错误信息,帮助你找出问题所在。
将此功能集成到Express路由中
到目前为止,你已经成功构建了一个功能完善的邮件发送模块。接下来,让我们看看如何在实际的Express应用中使用它。
创建一个index.js文件,并设置一个简单的Express服务器路由:
const express = require("express");
const sendEmail = require("./utils/email");
const app = express();
app.use(express.json()); // 用于解析JSON格式的请求体
app.post("/api/signup", async (req, res) => {
const { username, email } = req.body;
// 1. 将用户信息保存到数据库中(为简洁起见,这里省略了具体实现步骤)
// 2. 生成一个随机的OTP验证码
const otp = Math.floor(100000 + Math.random() * 900000);
// 3. 使用我们新开发的Brevo功能发送邮件
try {
await sendEmail({
email: email,
subject: "欢迎使用!这是您的验证码",
html: `
欢迎来到我的超棒网页应用,${username}!
请使用以下验证码完成注册:
${otp}
此验证码将在10分钟后失效。
},
});
res.status(201).json({ message: "用户信息已成功保存,邮件也已发送!" });
} catch (error) {
res.status(500).json({ error: "发送邮件失败。" });
}
});
app.listen(8000, () => {
console.log("服务器正在8000端口运行");
});
就这样!现在你可以从你的 React 或 Vue 前端代码中调用这个 /api/signup 端点,Brevo 的 REST API 会立即发送一封格式精美的电子邮件。
结论
作为开发者,遇到在本地可以正常运行、但在生产环境中会出现问题的漏洞是再平常不过的事情。但“邮件发送失败”这种错误却具有特殊的意义——它提醒我们:软件工程并不仅仅意味着编写规范的代码,更重要的是要理解底层基础设施、网络架构以及代码运行环境中的安全因素。
通过将 SMTP 协议替换为 REST API over HTTPS 这种架构模式,你不仅修复了一个漏洞,还成功实现了一种安全、免费且可靠的方案,使得你的应用程序能够绕过云服务提供的防火墙,而无需依赖 Nodemailer 这类复杂的第三方库。
如果你们读到了这里,我希望我已经成功地向你们说明了理解网络架构的重要性,以及如何利用 HTTP API 来安全地从网页应用中发送电子邮件。
感谢大家的阅读!



