在 Supabase Edge Functions 中通过 SMTP 发送邮件:send-email-smtp 实战指南
本文围绕 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.ts(auth: ['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() 读取 to、subject、text 字段),即可从"测试发信"升级为通用的邮件发送服务。
小结
本文围绕 send-email-smtp 示例目录 梳理了一条完整的 SMTP 发信链路:在 index.ts 中用 nodemailer 建连、以 withSupabase({ auth: 'user' }) 限定调用方身份,凭据经 .env.local.example 定义并由 CLI 注入,最终以 verify_jwt = true 部署上线。核心经验可浓缩为三点:端口务必选 2587 或 2525 等非 25/587 端口;发件人与 SMTP 账号/域要保持一致;鉴权方式要与代码中的 auth 配置严格对应。以此为基础,你可以快速改造出带动态收件人、模板正文乃至附件支持的通用边缘邮件服务。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00