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

> 在 Creght 编辑器连接 Resend 集成，然后用 ctx.email.send / sendCode / verifyCode 在 Func 里发信和校验邮箱验证码，密钥不进代码。

[![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)API for AI](/)

[查看 llms.txt](/llms.txt)

概览

- [Creght AI 编程指南](/api.md)

AI 可发现性

- [如何优化 llms.txt](/api/optimize-llms-txt.md)

站点配置

- [实现基于域名的多语言路由](/api/domain-locale-routing.md)

后端

- [使用 Func 构建站点后端能力](/api/func-backend.md)
- [使用 Func 接入支付宝电脑网站支付](/api/func-alipay-payment.md)

集成

- [使用集成发送邮件与验证码](/api/func-email-integration.md)

本页目录

- [什么时候用集成，什么时候用环境变量](#when)
- [连接 Resend](#connect)
- [发送邮件](#send)
- [邮箱验证码](#code)
- [集成里的可配置项](#config)
- [需要平台没封装的能力时](#escape)
- [错误与边界](#errors)

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

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

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

复制 Markdown 链接

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

**本文范围**

覆盖邮件能力： `ctx.email.send`、 `ctx.email.sendCode`、 `ctx.email.verifyCode`。支付没有托管集成，也没有 `ctx.payment`，需要在 Func 里自己实现，见 [使用 Func 接入支付宝电脑网站支付](/api/func-alipay-payment.md)。

## 什么时候用集成，什么时候用环境变量

两者共存，判断标准很简单：

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

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

## 连接 Resend

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

> 发件地址必须属于 Resend 中 **已验证** 的域名。用未验证的域名发信会被 Resend 拒绝——这也是这里让你「从列表里选」而不是「手填」的原因。

## 发送邮件

连接完成后在 Func 里直接调用，代码里不需要出现任何密钥：

```typescript
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` 已经把这件事里容易写错的部分做完了：

```typescript
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 后端完整指南](/api/func-backend.md)。

## 集成里的可配置项

| 配置 | 作用 |
| --- | --- |
| 发件地址 | 默认 `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`。

**Func 基础能力**

文件、调用、数据表、鉴权、资源上传、超时和 SSE 见 [Func 后端完整指南](/api/func-backend.md)。

![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)

此网站使用 [Creght](/) 创建

![微信客服](https://fsu.creght.com/site/2066727200882692096/1785119134612__image.png)

微信客服

## 链接

- [价格](/price.md)
- [解决方案](/solution.md)
- [客户案例](/customers.md)
- [帮助中心](/help.md)
- [联系我们](/contact.md)
- [更新记录 & 博客](/blogs.md)
- [退款说明](/tuikuan.md)

## 资源

- [全部资源](/resources.md)
- [模板](/templates.md)
- [组件库](https://creghtlib.site.creght.com)
- [动效库](/effects.md)
- [Figma to Creght](/figma2creght.md)
- [API](/api.md)

## 产品对比

- [对比上线了](/creght-vs-sxl.md)
- [对比凡科建站](/creght-vs-fkw.md)
- [自己写代码 vs Creght](/compare/self-coding.md)
- [外包 vs 自己做](/compare/outsourcing.md)

## 协议

- [用户协议](/legal/terms.md)
- [隐私政策](/legal/privacy.md)
- [可接受使用政策](/legal/acceptable-use.md)

## 社交媒体

- [小红书](https://www.xiaohongshu.com/user/profile/5a38606811be10715f4895b6)
- [哔哩哔哩](https://space.bilibili.com/513308095)

[蜀ICP备2023038192号-2](https://beian.miit.gov.cn)
