如今,能够通过网页应用程序发送电子邮件来进行沟通是非常重要的。这有助于企业与潜在客户保持联系,安全地验证用户身份,并发送诸如密码重置等重要通知。

但是,有时候,将运行良好的电子邮件功能部署到云端后,却会出现一些意外且令人沮丧的错误。你在本地构建并测试了后端代码,发现它运行得非常顺利;但当将其部署到云端后,应用程序突然就无法发送电子邮件了。

在本文中,你将了解到为什么在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

我们需要一个能够满足以下两个条件的解决方案:

  1. 它必须能够绕过端口587的防火墙限制。

  2. 它必须允许我们向任何人发送邮件,而无需购买自定义域名。

在这里,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密钥。

  1. 访问Brevo官网并创建一个免费账户。

  2. 在设置过程中,系统会要求您输入发件人邮箱地址。请确保输入的是您的Gmail地址。系统会发送一封邮件,其中包含用于验证该地址所属权的链接。

  3. 验证通过后,进入控制面板,在右上角点击您的个人资料名称,然后从下拉菜单中选择SMTP & API选项。

  4. 进入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.emailoptions.subjectoptions(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 来安全地从网页应用中发送电子邮件。

感谢大家的阅读!

Comments are closed.