集成使用集成发送邮件与验证码

使用集成发送邮件与验证码

连接 Resend 集成后在 Func 里用 ctx.email 发送事务邮件与邮箱验证码:密钥留在服务端,验证码的位数、有效期、一次性、常数时间比对、错误次数与限频由平台保证。

集成(Integrations)让你在编辑器里连接一次第三方服务,之后在 Func 里直接调用平台能力。密钥保存在服务端,不会出现在 Func 代码、日志和返回值里。当前已支持 Resend,用于发送事务邮件与邮箱验证码。

什么时候用集成,什么时候用环境变量

两者共存,判断标准很简单:

场景用法
平台已适配的服务,标准用法(发信、验证码)连接集成,调用 ctx.email.*
平台没适配的服务,或要用 provider 的高级接口把密钥放进环境变量,用 fetch 自己调

集成的价值不是替你调 API,而是消灭「到底连通了吗」这个不确定性:保存密钥时就真实校验一次,发件域名从对方账号里已验证的列表中选,并且可以当场发一封测试邮件确认。

连接 Resend

  1. Resend 控制台创建 API Key,并在 Resend 里完成发件域名验证。
  2. 打开编辑器的后端 → 集成,在 Resend 卡片里粘贴 API Key 保存。保存时平台会真实调用 Resend 校验这个 key,无效会当场报错,而不是等到线上发信时才失败。
  3. 从拉回的已验证域名里选择发件地址,例如 noreply@yourdomain.com;发件人名称与回复地址可选。
  4. 发送测试邮件真发一封,确认整条链路通了。

发件地址必须属于 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 必填,htmltext 至少给一个。返回 { id, provider }。不传 from / replyTo 时使用集成里配置的默认值。

邮箱验证码

验证码不要自己写。sendCodeverifyCode 已经把这件事里容易写错的部分做完了:

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.dbctx.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 为空、htmltext 都为空、收件人超过 50 个,都会返回 400。
  • 触发发送频率限制返回 429,提示稍后再试。
  • 三个方法都是同步调用(与 ctx.dbctx.cache 一致),写 await 也不会出错。
  • 能力只挂在 ctx 上。Func 没有模块加载器,import { email } from 'talizen/func-runtime' 这类值导入一定失败;类型请用 import type

Render diagnostics