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

NestJS用Nodemailer发验证OTP时遇ESOCKET-connect错误

NestJS使用Nodemailer+Gmail SMTP发送OTP时出现ESOCKET-connect错误

配置与代码详情

邮件器配置(app.module)

MailerModule.forRootAsync({
  useFactory: (configService: ConfigService) => configService.get('mailer'),
  inject: [ConfigService],
}),

mailer.config

export default () => ({
  mailer: {
    transport: {
      host: process.env.MAIL_HOST,
      port: parseInt(process.env.MAIL_PORT),
      auth: {
        user: process.env.MAIL_USER,
        pass: process.env.MAIL_PASSWORD,
      },
    },
    defaults: {
      from: process.env.MAIL_FROM,
    },
  },
});

register-email.provider

import { ConflictException, Injectable } from '@nestjs/common';
import { FindOneByEmailProvider } from 'src/users/providers/find-one-by-email.provider';
// import { v4 as uuidv4 } from 'uuid';
import { MailerService } from '@nestjs-modules/mailer';
import { OtpProvider } from './otp.provider';
import { EmailDto } from '../dto/email.dto';

@Injectable()
export class RegisterEmailProvider {
    constructor(
        private readonly findOneByEmailProvider: FindOneByEmailProvider,
        private readonly otpProvider: OtpProvider,
        private readonly mailerService: MailerService,
    ) {
        // 定时清理过期OTP
        setInterval(() => this.otpProvider.cleanUpExpiredOtps(), 60 * 1000); // 每分钟执行一次
    }

    public async registerEmail(registerEmailDto: EmailDto) {
        const { email } = registerEmailDto;

        const existingUser = await this.findOneByEmailProvider.findOneByEmailProvider(email);
        if (existingUser && existingUser.isActive === true) {
            throw new ConflictException('Email already exists', {
                description: 'Email already exists',
            });
        }

        // 生成6位数字OTP
        const otp = Math.floor(100000 + Math.random() * 900000).toString();
        const expiresAt = Date.now() + 15 * 60 * 1000; // 15分钟后过期

        this.otpProvider.setOtp(email, otp, expiresAt);

        console.log('OTP:', otp);

        // 发送OTP邮件
        try {
            await this.mailerService.sendMail({
                to: email,
                subject: 'Email Verification OTP',
                text: `Your OTP for email verification is: ${otp}`,
            });

            console.log('OTP sent to email');

            return { message: 'OTP sent to email' };
        } catch (error) {
            throw new ConflictException(error, 'Failed to send OTP email');
        }
    }
}

错误信息

{
    "errno": -4039,
    "code": "ESOCKET",
    "syscall": "connect",
    "address": "209.85.202.108",
    "port": 587,
    "command": "CONN"
}

环境变量配置

# mailer configuration
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USER=my_email_account  
MAIL_PASSWORD='xxxx xxxx xxxx xxxx'            # Gmail应用专用密码
MAIL_FROM='"Sender" <no-reply@sender.com>'

原因分析与解决方法

1. 网络连接受阻

ESOCKET错误核心是无法建立到Gmail SMTP服务器的TCP连接,排查方向:

  • 用telnet smtp.gmail.com 587测试连通性,若无法连接,说明防火墙、代理或网络策略拦截了587端口的出站流量。
  • 云服务器环境需检查安全组/防火墙规则,确保允许587端口的出站请求;本地开发则检查杀毒软件、系统防火墙是否拦截了Nodemailer的连接。

2. 缺失TLS加密配置

Gmail 587端口要求使用STARTTLS加密,当前配置未明确开启,需修改mailer.config的transport配置:

transport: {
  host: process.env.MAIL_HOST,
  port: parseInt(process.env.MAIL_PORT),
  secure: false, // 587端口设为false,465端口设为true
  requireTLS: true, // 强制启用STARTTLS加密
  auth: {
    user: process.env.MAIL_USER,
    pass: process.env.MAIL_PASSWORD,
  },
},

3. 应用专用密码无效

确认Gmail账号已开启2步验证,且应用专用密码正确:

  • 登录Google账号「安全」设置,确认2步验证处于启用状态。
  • 重新生成应用专用密码,环境变量中不要给密码加多余引号(改为MAIL_PASSWORD=xxxx xxxx xxxx xxxx)。

4. DNS解析异常

若域名解析失败,可临时用Gmail SMTP的IP地址替换域名测试:

host: '209.85.202.108', // 替换原smtp.gmail.com

内容的提问来源于stack exchange,提问作者Ahmed Mahmud

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 18:43:13