使用集成发送邮件与验证码
连接 Resend 集成后在 Func 里用 ctx.email 发送事务邮件与邮箱验证码:密钥留在服务端,验证码的位数、有效期、一次性、常数时间比对、错误次数与限频由平台保证。
集成(Integrations)让你在编辑器里连接一次第三方服务,之后在 Func 里直接调用平台能力。密钥保存在服务端,不会出现在 Func 代码、日志和返回值里。当前已支持 Resend,用于发送事务邮件与邮箱验证码。
什么时候用集成,什么时候用环境变量
两者共存,判断标准很简单:
| 场景 | 用法 |
|---|---|
| 平台已适配的服务,标准用法(发信、验证码) | 连接集成,调用 ctx.email.* |
| 平台没适配的服务,或要用 provider 的高级接口 | 把密钥放进环境变量,用 fetch 自己调 |
集成的价值不是替你调 API,而是消灭「到底连通了吗」这个不确定性:保存密钥时就真实校验一次,发件域名从对方账号里已验证的列表中选,并且可以当场发一封测试邮件确认。
连接 Resend
- 在 Resend 控制台创建 API Key,并在 Resend 里完成发件域名验证。
- 打开编辑器的后端 → 集成,在 Resend 卡片里粘贴 API Key 保存。保存时平台会真实调用 Resend 校验这个 key,无效会当场报错,而不是等到线上发信时才失败。
- 从拉回的已验证域名里选择发件地址,例如
noreply@yourdomain.com;发件人名称与回复地址可选。 - 点发送测试邮件真发一封,确认整条链路通了。
发件地址必须属于 Resend 中已验证的域名。用未验证的域名发信会被 Resend 拒绝——这也是这里让你「从列表里选」而不是「手填」的原因。
发送邮件
连接完成后在 Func 里直接调用,代码里不需要出现任何密钥:
import type { TalizenFuncContext } from 'talizen/func-runtime'
export function notifyOwner(input, ctx: TalizenFuncContext) {
const { id } = ctx.email.send({
to: 'owner@example.com',
subject: '新的预约:' + input.name,
html: '<p>' + input.name + ' 预约了 ' + input.date + '</p>',
})
return { id }
}
to 可以是一个地址或地址数组,单次最多 50 个收件人;subject 必填,html 与 text 至少给一个。返回 { id, provider }。不传 from / replyTo 时使用集成里配置的默认值。
邮箱验证码
验证码不要自己写。sendCode 与 verifyCode 已经把这件事里容易写错的部分做完了:
import type { TalizenFuncContext } from 'talizen/func-runtime'
export function sendLoginCode(input, ctx: TalizenFuncContext) {
ctx.email.sendCode({ to: input.email, scene: 'login' })
return { ok: true }
}
export function verifyLoginCode(input, ctx: TalizenFuncContext) {
const ok = ctx.email.verifyCode({
to: input.email,
scene: 'login',
code: input.code,
})
if (!ok) throw new Error('验证码无效或已过期')
return { ok: true }
}
scene 按用途给验证码分命名空间:登录码和改密码的码互不干扰。省略时为 default,取值只能是 1–32 位的字母、数字、下划线或连字符。
| 平台已保证 | 默认值 |
|---|---|
| 随机数字验证码位数 | 6 位 |
| 有效期 | 10 分钟(最长可配 60 分钟) |
| 一次性:校验通过立即作废 | 是 |
| 常数时间比对,不泄漏前缀信息 | 是 |
| 同一个码的猜错上限,超过即作废 | 5 次 |
| 同一收件人同一 scene 的发送频率 | 10 分钟 5 封 |
| 单项目每天验证码发送上限 | 默认 500 封,可在集成里调整(上限 10000) |
不要重复实现:不要用
Math.random()生成验证码,不要把码存进ctx.db或ctx.cache自己管过期,也不要自己数错误次数。猜错后重试不会延长有效期,这一点自己实现时很容易写漏。
verifyCode 对「码不对、已过期、根本没有码」统一返回 false 而不抛异常,只有参数非法或服务异常才报错,所以业务里写 if (!ok) 就够了。
验证码通过只证明对方能收到这个邮箱的邮件,不等于已登录。登录态请走项目 Auth,见 Func 后端完整指南。
集成里的可配置项
| 配置 | 作用 |
|---|---|
| 发件地址 | 默认 from,必须来自已验证域名 |
| 发件人名称 | 收件人看到的显示名 |
| 回复地址 | 默认 replyTo |
| 验证码邮件标题 | 支持 {{code}} 占位,默认「你的验证码是 {{code}}」 |
| 验证码有效期 | 默认 10 分钟,上限 60 分钟 |
| 验证码位数 | 默认 6 位 |
| 每日上限 | 本项目每天最多发多少封验证码,默认 500,上限 10000 |
需要平台没封装的能力时
集成默认不把密钥交给 Func 沙箱。如果要用 provider 的其他接口(Resend 的联系人、批量发送、附件),或者干脆换一家自己发,打开集成里的 expose_to_func 开关:密钥会额外投影进 process.env(Resend 对应 RESEND_API_KEY),之后就能用 fetch 自己调。
开关默认关闭。打开后 Func 代码就能读到密钥,请只在确实需要时打开。
错误与边界
- 没连接、未通过校验或被停用时,
ctx.email.*会报错。这是编辑器里的配置动作,不要在代码里绕过去直连 provider 的 API。 - 集成里没有发件地址时报错,去后端 → 集成补上。
subject为空、html与text都为空、收件人超过 50 个,都会返回 400。- 触发发送频率限制返回 429,提示稍后再试。
- 三个方法都是同步调用(与
ctx.db、ctx.cache一致),写await也不会出错。 - 能力只挂在
ctx上。Func 没有模块加载器,import { email } from 'talizen/func-runtime'这类值导入一定失败;类型请用import type。
