You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NodeMailer跨模块发邮件异常:PayAsYouLike请求遭连接拒绝

问题描述

我的网站需发送两类邮件:

  • PayAsYouLike模块:向申请用户发送随机生成的密码
  • GetInTouch模块:将联系表单内容发送至我的个人邮箱

两类邮件共用email.js路由文件。使用Thunder Client测试时,两类请求均能正常发送邮件;但前端发起调用时,GetInTouch模块请求100%成功,PayAsYouLike模块请求却100%失败,控制台报ERR_CONNECTION_REFUSED错误。服务器日志仅能打印到sendMail函数内的Inside sendMail, but before transporter.sendMail call!,说明错误出在transporter.sendMail(mailOptions)调用环节。尝试过拆分路由、复用GetInTouch的fetch逻辑等方法,问题依旧。

附上email.js路由代码:

const express = require("express");
const router = express.Router();
require("dotenv").config();
const nodeMailer = require("nodemailer");

router.route("/").post((req, res) => {
  const form = req.body;
  let mailOptions = {};

  console.log(form);

  const transporter = nodeMailer.createTransport({
    service: "gmail",
    host: "smtp.gmail.com",
    port: 587,
    secure: false,
    auth: {
      user: process.env.USER,
      pass: process.env.APP_PASSWORD,
    },
  });

  if ("password" in form) {
    const passwordDate = new Date(form.date);
    const passwordValidity = new Date(passwordDate.getTime() + 172800000);
    const options = { 
      year: 'numeric', 
      month: 'long', 
      day: 'numeric', 
      hour: 'numeric', 
      minute: 'numeric', 
      second: 'numeric',
      hour12: true 
    };

    mailOptions = {
      from: process.env.USER,
      to: form.email,
      subject: "Your PASSWORD is here!",
      html: `Dear ${ form.name || form.email }, <br><br>Your PASSWORD: <h3 style="display:inline;">${ form.password }</h3><br><i>Expiry:</i> <h4 style="display:inline;">${passwordValidity.toLocaleDateString('en-US', options)}</h4>`,
    };
  } else {
    mailOptions = {
      from: process.env.USER,
      to: "karangour@gmail.com",
      subject: "GetInTouch form",
      html: `FROM: ${form.name}<br>EMAIL: ${form.email}<br>SUBJECT: ${form.subject}<br>MESSAGE: ${form.message}`,
    };
  }
  console.log("Mail options prepared:", mailOptions);

  const sendMail = async (transporter, mailOptions) => {
    try {
      console.log('Inside sendMail, but before transporter.sendMail call!')
      await transporter.sendMail(mailOptions);
      console.log("Email has been sent successfully!!");
      res.json({
        status: "Success",
        message: "Email sent successfully",
      });
    } catch (error) {
      console.log(error);
      res.json({
        status: "Error",
        message: "Failed to send email",
        error: error.message,
      });
    }
  };

  sendMail(transporter, mailOptions);
});

module.exports = router;
排查与解决方案

1. 核对前端请求数据一致性

对比前端PayAsYouLike模块发送的form数据与Thunder Client的测试数据,重点检查:

  • date字段的格式:前端可能传入了非标准日期字符串(如非ISO格式),导致后端new Date(form.date)生成无效日期,进而破坏邮件内容格式,触发SMTP服务器拒绝连接
  • email字段的合法性:确认前端传入的收件人邮箱格式符合规范,避免因无效邮箱触发SMTP校验失败
  • 请求头配置:确保前端请求的Content-Type为application/json,与Thunder Client测试配置一致

2. 增强后端错误捕获与日志

在transporter.sendMail调用前添加参数校验,同时打印完整错误信息定位问题:

// 在sendMail调用前插入
// 校验收件人邮箱格式
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(mailOptions.to)) {
  console.error("无效收件人邮箱:", mailOptions.to);
  return res.status(400).json({ status: "Error", message: "无效邮箱地址" });
}
// 校验date字段有效性
if ("password" in form && isNaN(new Date(form.date).getTime())) {
  console.error("无效日期格式:", form.date);
  return res.status(400).json({ status: "Error", message: "无效日期格式" });
}

// 修改catch块日志
catch (error) {
  console.log("完整错误详情:", error);
  res.json({
    status: "Error",
    message: "邮件发送失败",
    error: error.message,
    fullError: JSON.stringify(error)
  });
}

3. 优化Transporter创建逻辑

将Transporter的创建移至路由外部,全局复用连接,避免每次请求创建新SMTP连接导致的资源耗尽:

const express = require("express");
const router = express.Router();
require("dotenv").config();
const nodeMailer = require("nodemailer");

// 全局创建并复用Transporter
const transporter = nodeMailer.createTransport({
  service: "gmail",
  host: "smtp.gmail.com",
  port: 587,
  secure: false,
  auth: {
    user: process.env.USER,
    pass: process.env.APP_PASSWORD,
  },
});

// 提前校验Transporter配置
transporter.verify((error, success) => {
  if (error) console.error("Transporter校验失败:", error);
  else console.log("Transporter已就绪");
});

router.route("/").post(async (req, res) => {
  const form = req.body;
  let mailOptions = {};

  console.log(form);

  // ... 原有mailOptions生成逻辑不变 ...

  console.log("邮件配置已准备:", mailOptions);

  try {
    console.log('进入sendMail,即将调用transporter.sendMail!')
    await transporter.sendMail(mailOptions);
    console.log("邮件发送成功!!");
    res.json({
      status: "Success",
      message: "邮件发送成功",
    });
  } catch (error) {
    console.log("完整错误详情:", error);
    res.json({
      status: "Error",
      message: "邮件发送失败",
      error: error.message,
      fullError: JSON.stringify(error),
    });
  }
});

module.exports = router;

4. 检查Gmail SMTP配置

  • 确认Gmail账号已开启2FA,且使用的APP_PASSWORD正确有效
  • 检查Gmail发送配额,避免因请求频率过高触发限制

内容的提问来源于stack exchange,提问作者Karan Gour

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 15:53:18