首页
/ 在 Supabase Edge Functions 中通过 SMTP 发送邮件:send-email-smtp 实战指南

在 Supabase Edge Functions 中通过 SMTP 发送邮件:send-email-smtp 实战指南

2026-09-06 18:27:01作者:温艾琴Wonderful

本文围绕 Supabase 开源仓库中 examples/edge-functions/supabase/functions/send-email-smtp 这一官方示例展开,讲解如何编写一个由 Deno 运行时托管、使用 nodemailer 直连任意 SMTP 服务(如 AWS SES)的 Edge Function,并完整覆盖环境变量配置、本地调试与云端部署三个环节。读完本文,你将掌握 SMTP 类边缘函数的鉴权模型(verify_jwt)、端口限制等部署陷阱,以及一套可直接复制的发信代码骨架。

示例背景:为什么需要 SMTP 版发信函数

在 Supabase Edge Functions 生态中,发信类函数通常有两条路线:一是调用第三方邮件 API(仓库中对应的对照示例是 send-email-resend,基于 Resend HTTP API);二是本示例采用的 SMTP 直连方案——使用 Node 生态中最常用的邮件库 nodemailer,通过标准 SMTP 协议连接你自己的邮件服务商。后者不依赖特定 SaaS,凡支持 SMTP 的服务(AWS SES、Mailgun、自建邮件网关等)都可接入,适合已有 SMTP 账号或需要统一邮件中继的场景。

完整示例仅由两个文件构成:函数主体 index.ts 与部署说明 README.md

函数实现:nodemailer + @supabase/server 鉴权封装

先看核心实现 index.ts 的完整结构,它是理解整篇配置的基础:

import { withSupabase } from 'npm:@supabase/server@^1'
import nodemailer from 'npm:nodemailer@^9'

const transport = nodemailer.createTransport({
  host: Deno.env.get('SMTP_HOSTNAME')!,
  port: Number(Deno.env.get('SMTP_PORT')!),
  secure: Boolean(Deno.env.get('SMTP_SECURE')!),
  auth: {
    user: Deno.env.get('SMTP_USERNAME')!,
    pass: Deno.env.get('SMTP_PASSWORD')!,
  },
})

console.log(`Function "send-email-smtp" up and running!`)

// Authenticated endpoint, so deploy with verify_jwt = true.
export default {
  fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
    try {
      await new Promise<void>((resolve, reject) => {
        transport.sendMail(
          {
            from: Deno.env.get('SMTP_FROM')!,
            to: 'testr@test.de',
            subject: `Hello from Supabase Edge Functions`,
            text: `Hello Functions \\o/`,
          },
          (error) => {
            if (error) {
              return reject(error)
            }
            resolve()
          }
        )
      })
    } catch (error) {
      return Response.json({ error: error.message }, { status: 500 })
    }

    return Response.json({ done: true })
  }),
}

逐段拆解其关键设计:

1. npm 包直接导入。Supabase Edge Functions 运行在 Deno 之上,但通过 npm: 前缀可以无缝使用 npm 生态包。这里用 npm:nodemailer@^9 引入 nodemailer,用 npm:@supabase/server@^1 引入服务端辅助库。若希望编辑器提供自动补全与跳转定义,可参考源码文件头部注释,在编辑器中接入 Deno language server。

2. SMTP 传输层在模块顶层创建nodemailer.createTransport 在函数模块加载时执行一次,凭据全部来自 Deno.env.get(...) 读取的环境变量,冷启动后连接可复用,避免每次请求都重新建连。

3. secure 标志的处理值得注意。代码使用 Boolean(Deno.env.get('SMTP_SECURE')!)——该写法只有在变量为字符串 "true" 时才为 true,其余情况均为 false。这意味着若你的 SMTP 服务走 TLS 加密端口(如 465),需要显式设置 SMTP_SECURE="true";而 AWS SES 的 2587 端口属于 STARTTLS 场景,通常保持该变量为空即可。

4. 鉴权模型决定部署配置。函数通过 withSupabase({ auth: 'user' }, ...) 封装,只接受已登录用户的 JWT,因此源码明确注释:"Authenticated endpoint, so deploy with verify_jwt = true"。这与仓库中对照示例 send-email-resend/index.tsauth: ['user', 'secret'],允许密钥调用,部署时 verify_jwt = false)形成鲜明对比——鉴权范围越窄,越需要用 verify_jwt = true 强制校验,防止未授权方滥用你的发信额度。

5. 错误处理与响应约定transport.sendMail 以回调方式报告错误,示例将其包装进 Promise,出错时返回 { error } 与 HTTP 500;成功则返回 { done: true }。这个约定让调用方(服务端触发器、supabase-js 的 invoke 或 curl)都能获得明确的成败信号。

环境变量:SMTP 凭据的五要素

函数在顶层一次性读取 5 个环境变量,缺失任何一个都会因 ! 非空断言在模块加载期直接抛错。仓库在 .env.local.example 中给出了本地调试用的模板值:

# send-email-smtp
SMTP_HOSTNAME="your.hostname.com"
SMTP_PORT="2587"
SMTP_USERNAME="your_username"
SMTP_PASSWORD="your_password"
SMTP_FROM="no-reply@example.com"

各变量的作用与在代码中的消费位置如下表:

变量 示例值 对应代码位置 说明
SMTP_HOSTNAME email-smtp.us-east-1.amazonaws.com transport.host SMTP 服务器主机名
SMTP_PORT 2587 transport.port(经 Number() 转换) 端口,必须避开 25/587,见下文部署陷阱
SMTP_USERNAME SMTP IAM 用户名 auth.user 登录账号
SMTP_PASSWORD 对应密钥 auth.pass 登录密码
SMTP_FROM no-reply@example.com sendMail({ from }) 发件人地址,须与 SMTP 服务允许的发信域一致

本地调试时,按示例根目录 README.md 的通用流程执行:

supabase start          # 确保 Docker 守护进程已运行
cp ./supabase/.env.local.example ./supabase/.env.local

然后编辑 .env.local,把上面 5 个变量替换成真实凭据。

本地运行与调用验证

创建 .env.local 后,用 --env-file 让本地函数服务加载该环境文件:

supabase functions serve --env-file ./supabase/.env.local

由于示例函数设计为 JWT 鉴权(verify_jwt = true),源码注释给出了携带用户 Access Token 的 curl 调用方式:

curl -i --location --request POST 'http://localhost:54321/functions/v1/send-email-smtp' \
  --header 'Authorization: Bearer <USER_ACCESS_TOKEN>' \
  --header 'Content-Type: application/json' \
  --data '{"name":"Functions"}'

请求成功时函数会返回 {"done":true} 与 HTTP 200;若 SMTP 发送失败(如凭据错误、端口不通、发件域未验证),则返回 {"error":"..."} 与 HTTP 500,便于在终端直接看到 nodemailer 抛出的底层错误信息。

云端部署:三步走与 verify_jwt 配置

部署流程在原 README 中为三条命令,顺序上存在强依赖,说明如下:

# 1. 将本地项目关联到云端 Supabase 项目
supabase link --project-ref your-project-ref

# 2. 一次性写入全部 SMTP 环境变量(注意各变量值要用引号包裹)
supabase secrets set SMTP_HOSTNAME="your.hostname.com" SMTP_PORT="2587" \
  SMTP_USERNAME="your_username" SMTP_PASSWORD="your_password" \
  SMTP_FROM="no-reply@example.com"

# 3. 部署函数
supabase functions deploy send-email-smtp
  • supabase link--project-ref 需替换为云端项目引用(可在 Supabase 控制台的项目设置中找到)。
  • supabase secrets set 写入的是 Edge Runtime 环境变量,属于部署期注入,与本地 .env.local 相互独立;可用 supabase secrets list 复核是否写入成功。
  • supabase functions deploy send-email-smtp 中函数名必须与函数目录名一致。

verify_jwt 的云端一致性。仓库为该项目维护的本地配置 config.toml 中写明了鉴权开关:

[functions.send-email-smtp]
verify_jwt = true

也就是说,示例在本地与云端都强制 JWT 校验。如果你改用 withSupabase({ auth: ['user', 'secret'] }) 之类的混合鉴权,就应把此处改为 verify_jwt = false,否则携带 service role 密钥的调用会被拒绝。修改配置后可通过 supabase functions deploy 同步到云端。

部署陷阱:为何端口必须避开 25 与 587

原 README 的 Note 是全篇最容易踩坑的一条约束,值得展开解释:

SMTP_PORT 必须选择 25 和 587 之外的端口,因为 Deno Deploy 不支持对这两个端口发起出站连接。推荐使用 AWS SES(端口 2587)。

其背后的原因是:Supabase Edge Functions 构建在 Deno Deploy 的边缘网络之上,运行时的出站网络策略对 25(传统明文 SMTP)与 587(标准提交端口)作了封禁,属于平台侧限制而非示例代码缺陷。因此选择 SMTP 服务时,请优先确认其是否提供非常用端口:

  • AWS SES:官方推荐 2587(STARTTLS),这也是 README 推荐的原因;
  • 部分服务商把 2525 作为 587 的替代端口,同样可用;
  • 若你的服务商只开放 587,则需要更换为支持 2587/2525 的中继服务,或在服务商侧开通替代端口。

结合代码中的 secure: Boolean(Deno.env.get('SMTP_SECURE')!) 再强调一次:AWS SES 2587 走 STARTTLS,不需要设 SMTP_SECURE;只有连接 465 等隐式 TLS 端口时才需要把该变量设为字符串 "true"

发送结果与可观测性

函数启动时会在控制台打印 Function "send-email-smtp" up and running!,云端可通过函数日志面板查看。每次调用结束返回 JSON 响应:

  • 成功:{"done":true}
  • 失败:{"error":"<nodemailer 返回的具体错误>","status":500}

实际使用时,建议把硬编码的收件人 testr@test.de 与正文内容替换为请求参数驱动(例如从 await req.json() 读取 tosubjecttext 字段),即可从"测试发信"升级为通用的邮件发送服务。

小结

本文围绕 send-email-smtp 示例目录 梳理了一条完整的 SMTP 发信链路:在 index.ts 中用 nodemailer 建连、以 withSupabase({ auth: 'user' }) 限定调用方身份,凭据经 .env.local.example 定义并由 CLI 注入,最终以 verify_jwt = true 部署上线。核心经验可浓缩为三点:端口务必选 2587 或 2525 等非 25/587 端口;发件人与 SMTP 账号/域要保持一致;鉴权方式要与代码中的 auth 配置严格对应。以此为基础,你可以快速改造出带动态收件人、模板正文乃至附件支持的通用边缘邮件服务。

登录后查看全文
热门项目推荐
相关项目推荐