- 后端
- 前端
- Web框架
- 开发工具
【免费下载链接】redwood
RedwoodGraphQL
RedwoodJS 内置的 Mailer 是一个端到端的邮件设计与交付框架:它通过Renderer(渲染器)把 React 组件转成 HTML/纯文本,再通过Handler(处理器)交给 Nodemailer、Resend、SES 等具体服务发出,并针对开发、测试、生产三种环境自动切换发送通道,防止邮件泄漏。读完本文,你将掌握yarn rw setup mailer的完整初始化流程、api/src/lib/mailer.ts的每一项配置、如何在 Service 中发送模板邮件、如何用 InMemory 处理器做快照级单元测试,以及如何接入 Redwood Studio 的模板预览与本地收件箱。
本文以仓库中的 version-6.x/mailer.md 文档为主线,并结合packages/mailer下的核心源码、四个官方处理器、两个官方渲染器及 CLI 生成模板进行源码级印证。
设计目标:为什么 Mailer 不只是"发一封邮件"
RedwoodJS Mailer 的设计出发点,是把"发送邮件"这个动作拆成一条可组合、可测试、可切换的流水线。官方文档明确列出了它的设计约束,这些约束直接决定了下面要讲的架构形态:
- 可对接主流第三方服务:如 Resend、SendGrid、Postmark、Amazon SES 等;
- 可自托管:通过 Nodemailer 作为开源自建方案;
- 按场景选通道:例如事务邮件走 Resend、摘要邮件走 SES,调用方可以在发信时按需指定 handler;
- 开发/测试环境安全隔离:在 "sandbox" 中发送,绝不意外泄露真实邮件;
- 支持 React 模板体系:基于 React Email 或 MJML 编写 HTML/纯文本邮件,并预留更多模板方案;
- 可单元测试:断言 to、from、cc、subject、正文等字段;
- 与 RedwoodJS Studio 深度集成:用于模板设计与预览。
如文档所说,RedwoodJS Mailer 是"一整套端到端的邮件设计、开发与测试工具包",其核心由handlers与renderers两类抽象组成,配合api/src/lib/mailer.ts配置文件完成装配。
总体架构:Handler × Renderer × 三种运行模式
官方文档给出了一张 Mailer Flow 示意图(见 docs/static/img/mailer/flow.svg),完整描绘了邮件从模板组件到最终投递的路径:
整条链路在源码 packages/mailer/core/src/mailer.ts 的Mailer类中实现。Mailer在构造时做三件事:
- 确定运行模式(
this.mode = this.isTest() ? 'test' : this.isDevelopment() ? 'development' : 'production',见 mailer.ts#L52-L56); - 校验并装载 handlers / renderers,同时为测试与开发模式准备"回退处理器";
- 抽取默认发送参数(
extractDefaults)。
调用mailer.send(...)时,send方法根据当前 mode 决定实际使用哪个 handler:test 模式用测试 handler,development 模式用开发 handler,production 模式才使用发送选项中指定的(或默认的)生产 handler(见 mailer.ts#L164-L180)。这种模式驱动的路由机制,正是"开发/测试不泄露真实邮件"这一设计目标的技术基础。
两种核心抽象:Renderer 与 Handler
Renderer(渲染器)负责把 React 组件渲染成可供邮件客户端使用的字符串(HTML 与纯文本)。抽象基类AbstractMailRenderer定义在 packages/mailer/core/src/renderer.ts,其契约非常简单:
abstract render( template: unknown, options: MailRendererOptions<unknown>, utilities?: MailUtilities, ): MailRenderedContent abstract internal(): Record<string, unknown>Handler(处理器)负责把渲染后的内容交给真实投递服务。抽象基类AbstractMailHandler定义在 packages/mailer/core/src/handler.ts:
abstract send( renderedContent: MailRenderedContent, sendOptions: MailSendOptionsComplete, handlerOptions?: Record<string | number | symbol, unknown>, utilities?: MailUtilities, ): Promise<MailResult> | MailResult abstract internal(): Record<string, unknown>注意MailRenderedContent同时包含html与text两个字段(见 packages/mailer/core/src/types.ts#L9-L12),因此任何官方渲染器都会尽量同时产出两种格式,任何官方 handler 也会把两种格式一并投递。
官方 Renderer 一览
Mailer 目前官方提供两个渲染器,目录均在 packages/mailer/renderers:
| Renderer | 依赖技术 | 源码位置 |
|---|---|---|
@redwoodjs/mailer-renderer-react-email | React Email | |
@redwoodjs/mailer-renderer-mjml-react | MJML(Faire/mjml-react |
以 React Email 渲染器为例,其实现位于 packages/mailer/renderers/react-email/src/index.ts:它支持outputFormat选项,取值'both' | 'html' | 'text',默认'both';html分支使用reactEmailRender(template, { pretty: true, plainText: false }),text分支使用plainText: true,从而基于同一份组件同时产出两种正文格式。
官方文档特别强调:邮件客户端在 HTML 渲染上出了名的不一致,因此强烈建议使用 React Email、MJML 这类健壮的组件库来编写邮件模板,以保证跨客户端的视觉效果一致性。
官方 Handler 一览
Mailer 目前官方提供四个处理器,目录在 packages/mailer/handlers:
| Handler | 用途 | 源码位置 |
|---|---|---|
@redwoodjs/mailer-handler-in-memory | 内存收件箱,典型用于测试 | packages/mailer/handlers/in-memory |
@redwoodjs/mailer-handler-nodemailer | 基于 Nodemailer 的通用 SMTP 发送 | packages/mailer/handlers/nodemailer |
@redwoodjs/mailer-handler-studio | 把邮件送入 Redwood Studio(内部仍走 Nodemailer) | packages/mailer/handlers/studio |
@redwoodjs/mailer-handler-resend | 对接 Resend 云服务 | packages/mailer/handlers/resend |
以 Nodemailer handler 为例(见 packages/mailer/handlers/nodemailer/src/index.ts),它的配置HandlerConfig接受transport(可以是SMTPTransport、SMTPTransport.Options或连接字符串)与可选的defaults,构造时即调用nodemailer.createTransport(config.transport, config.defaults)。send方法把MailSendOptionsComplete中的to/cc/bcc/from/replyTo/subject/headers/attachments与渲染出的text/html一起传给transporter.sendMail,并返回{ messageID: result.messageId, handlerInformation: result }。
Resend handler(见 packages/mailer/handlers/resend/src/index.ts)则略有不同:它通过new Resend(apiToken)创建云客户端,发送时把replyTo映射为 Resend API 的reply_to,并把字符串形式的附件内容转成 UTF-8Buffer后随请求发出;同时支持 Resend 特有的tags选项用于邮件打标。
InMemoryMailHandler(见 packages/mailer/handlers/in-memory/src/index.ts)最为简单:它维护一个inbox数组,send时把完整发送参数、textContent、htmlContent以及utilities中携带的handler、renderer信息一并压入 inbox,并返回形如in-memory-1的messageID;同时提供clearInbox()方法便于测试间重置。这正是文档中测试断言的落点。
如果你的目标服务不在官方列表里,也可以参考以上实现自行编写 handler/renderer,接口契约就在@redwoodjs/mailer-core(packages/mailer/core)中,完成后可以开源回馈社区。
关键文件与目录约定
Mailer 的核心配置文件是api/src/lib/mailer.ts。文档给出了初始化后的默认形态,这与 CLI 生成的模板 packages/cli/src/commands/setup/mailer/templates/mailer.ts.template 完全一致:
import { Mailer } from '@redwoodjs/mailer-core' import { NodemailerMailHandler } from '@redwoodjs/mailer-handler-nodemailer' import { ReactEmailRenderer } from '@redwoodjs/mailer-renderer-react-email' import { logger } from 'src/lib/logger' export const mailer = new Mailer({ handling: { handlers: { // TODO: Update this handler config or switch it out for a different handler completely nodemailer: new NodemailerMailHandler({ transport: { host: 'localhost', port: 4319, secure: false, }, }), }, default: 'nodemailer', }, rendering: { renderers: { reactEmail: new ReactEmailRenderer(), }, default: 'reactEmail', }, logger, })配置结构说明(类型定义见 packages/mailer/core/src/types.ts#L65-L85):
handling.handlers:一个以任意字符串为 key、handler 实例为 value 的映射表;handling.default:生产模式下默认使用的 handler key,必须提供,否则构造时会抛出No default handler configured;handling.options:可选,为每个 handler 提供默认的 handlerOptions(发送时会与调用方的 handlerOptions 浅合并,见 mailer.ts#L218-L224);rendering.renderers与rendering.default:同理,渲染器的注册表与默认项;defaults:可选,全局默认发送参数(to与subject除外,见下文"默认发送参数"小节);development/test:可选,覆盖两种非生产模式的行为;logger:可选,缺省时回退为console,且 Mailer 会为 logger 挂一个{ module: 'mailer' }的子上下文(见 mailer.ts#L47-L49)。
Mailer 还约定你的邮件模板组件放在api/src/mail目录下。例如欢迎邮件应位于api/src/mail/Welcome/Welcome.tsx。这保证了"配置在lib、模板在mail"的目录约定清晰可循。
初始化:yarn rw setup mailer
新建的 RedwoodJS 应用默认不包含 Mailer,但初始化非常简单,只需运行:
yarn rw setup mailer该命令会完成两件事(对应 packages/cli/src/commands/setup/mailer/mailer.js 及其 handler mailerHandler.js):
- 安装必要依赖:包括
@redwoodjs/mailer-core、@redwoodjs/mailer-handler-nodemailer、@redwoodjs/mailer-renderer-react-email,并且会把@redwoodjs/mailer-handler-in-memory作为devDependency自动加入,以便测试模式默认可用; - 生成初始配置:即上文
api/src/lib/mailer.ts模板。
该命令还支持两个可选参数:
--force(别名-f):覆盖已存在的配置文件;--skip-examples:只生成必需文件,跳过示例模板。
初始化之后,邮件模板组件(如Welcome.tsx)需要你自己在api/src/mail下创建,命令本身不会替你生成业务模板。
发送邮件:一个完整的 Contact Us 实战
官方文档用一个博客站点的"联系我们"功能作为示例:表单提交的 name、email、message 落库后,同时向内部邮箱发送一封通知邮件。改造后的 Service 如下:
import { mailer } from 'src/lib/mailer' import { ContactUsEmail } from 'src/mail/Example/Example' // ... export const createContact: MutationResolvers['createContact'] = async ({ input, }) => { const contact = await db.contact.create({ data: input, }) // Send email await mailer.send( ContactUsEmail({ name: input.name, email: input.email, // Note the date is hardcoded here for the sake of test snapshot consistency when: new Date(0).toLocaleString(), }), { to: 'inbox@example.com', subject: 'New Contact Us Submission', replyTo: input.email, from: 'contact-us@example.com', } ) return contact }这段代码做了三件事:
- 导入Mailer 单例与邮件模板组件;
- 调用
mailer.send,第一个参数是模板组件(可传入基于用户输入的 props),第二个参数是发送选项; - 发送选项中的
to、subject、replyTo、from决定邮件收件人、主题、回复地址与发件人。
发送选项的类型契约
send的第二个参数类型是MailSendOptions(见 packages/mailer/core/src/types.ts#L111-L118),它继承MailSendWithoutRenderingOptions,在 types.ts#L89-L102 中定义了完整的字段集:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
to | MailAddress \| MailAddress[] | 是 | 收件人,MailAddress可以是纯字符串或{ name?, address }对象 |
cc | MailAddress \| MailAddress[] | 否 | 抄送 |
bcc | MailAddress \| MailAddress[] | 否 | 密送 |
from | MailAddress | 否(可由 defaults 提供) | 发件人,若最终缺失会抛Missing from address |
replyTo | MailAddress | 否 | 回复地址 |
subject | string | 是 | 主题,缺失会抛Missing subject |
headers | Record<string, string> | 否 | 自定义邮件头 |
attachments | MailAttachment[] | 否 | 附件,MailAttachment支持filename、path、content(字符串或 Buffer) |
handler | handler key | 否 | 覆盖生产模式下的 handler |
renderer | renderer key | 否 | 覆盖默认渲染器 |
默认发送参数 defaults
示例里显式写了replyTo,但如果大量邮件都想默认使用replyTo: 'no-reply@example.com',不必每处重复。可以在api/src/lib/mailer.ts中通过defaults统一设置:
defaults: { replyTo: 'no-reply@example.com', },源码对defaults的处理在 packages/mailer/core/src/utils.ts 的extractDefaults中:cc/bcc/replyTo/from会被预先通过convertAddress转换成标准字符串(形如Name <address>或裸地址),attachments与headers缺省为空数组/空对象。注意defaults的类型是Partial<Omit<MailBasicSendOptions, 'to' | 'subject'>>(types.ts#L78),即不能把to、subject放进 defaults——这两项必须每次发送时显式指定。
发送时,constructCompleteSendOptions(utils.ts#L61-L130)会把"本次发送选项"与"defaults"合并:本次显式给出的字段优先,未给出的字段回退到 defaults,最终形成一份完整的MailSendOptionsComplete。若合并后仍缺少from、subject或to,会分别抛出对应的错误。
三种运行模式的自动分流
Mailer 会根据NODE_ENV自动选择模式(各模式的判定与 handler 路由逻辑见 mailer.ts#L319-L345):
| 模式 | 触发条件 | 邮件去向 |
|---|---|---|
test | 默认NODE_ENV === 'test' | 测试 handler(默认 in-memory) |
development | 默认NODE_ENV !== 'production' | 开发 handler(默认 Studio) |
production | 上述均不满足 | 发送选项指定的 handler,未指定则用handling.default |
每个模式的when都可以是一个布尔值或返回布尔的函数,handler则指定该模式下使用的 handler key。send/sendWithoutRendering内部都通过switch (this.mode)选取 handler;如果选出的 handler 为null,则直接返回空结果{},即"no-op 不发信"(mailer.ts#L182-L185)。
测试模式:InMemory 收件箱与快照断言
当NODE_ENV为test时,Mailer 进入测试模式,所有邮件(无论调用时指定了哪个 handler)都会改走测试 handler。默认行为是:
- 创建 Mailer 时检查
@redwoodjs/mailer-handler-in-memory是否可用(mailer.ts#L66-L90); - 可用则自动装载
InMemoryMailHandler作为测试 handler,并打印一条 warn 日志; - 不可用则测试 handler 退化为 no-op,邮件不投递、不落库;
- 由于
yarn rw setup mailer已把 in-memory 包加入 devDependencies,正常项目默认即有可用的测试收件箱。
如果需要显式控制测试模式,可在api/src/lib/mailer.ts中加入:
test: { when: process.env.NODE_ENV === 'test', handler: 'someOtherHandler', }when:布尔值或返回布尔值的函数,决定 Mailer 创建时是否进入测试模式;handler:测试模式下实际使用的 handler key;若设为null则测试模式下完全不发信。
在测试代码中,可以通过mailer.getTestHandler()拿到测试 handler(mailer.ts#L347-L356),进而读取inbox做断言。文档给出的完整测试示例:
describe('contacts', () => { scenario('creates a contact', async () => { const result = await createContact({ input: { name: 'String', email: 'String', message: 'String' }, }) expect(result.name).toEqual('String') expect(result.email).toEqual('String') expect(result.message).toEqual('String') // Mail const testHandler = mailer.getTestHandler() as InMemoryMailHandler expect(testHandler.inbox.length).toBe(1) const sentMail = testHandler.inbox[0] expect({ ...sentMail, htmlContent: undefined, textContent: undefined, }).toMatchInlineSnapshot(` { "attachments": [], "bcc": [], "cc": [], "from": "contact-us@example.com", "handler": "nodemailer", "handlerOptions": undefined, "headers": {}, "htmlContent": undefined, "renderer": "reactEmail", "rendererOptions": {}, "replyTo": "String", "subject": "New Contact Us Submission", "textContent": undefined, "to": [ "inbox@example.com", ], } `) expect(sentMail.htmlContent).toMatchSnapshot() expect(sentMail.textContent).toMatchSnapshot() }) })这个测试覆盖了三层断言:
- 发送数量:
inbox.length为 1,确认恰好发了一封; - 发送选项:通过内联快照断言
to、from、replyTo、subject、cc/bcc/headers/attachments以及handler、renderer等元信息与期望一致; - 渲染结果:
htmlContent与textContent分别用toMatchSnapshot()锁定,保证模板改动不会悄悄破坏产出。
值得注意的是,InMemoryMailHandler会把utilities中的handler、rendererkey 一并记录到 inbox 条目里(in-memory/src/index.ts#L32-L40),因此快照里能看到"handler": "nodemailer"、"renderer": "reactEmail"——即使在测试模式下,也保留了"生产环境会走哪个通道"的可观测信息。官方核心测试 packages/mailer/core/src/tests/mailer.test.ts 也用 Mock handler/renderer 验证了模式切换、默认 handler 校验等行为,可作为深入阅读的起点。
开发模式:Studio 本地收件箱与实时预览
与测试模式类似,Mailer 还提供开发模式。当NODE_ENV不是production时自动进入;默认尝试加载@redwoodjs/mailer-handler-studio作为开发 handler(mailer.ts#L91-L115),把邮件送入 Redwood Studio 内置的本地 SMTP 收件箱。
可通过如下配置自定义:
development: { when: process.env.NODE_ENV !== 'production', handler: 'someOtherHandler', },配置语义与test完全一致:when决定是否进入开发模式,handler指定该模式使用的 handler(null表示不发信)。
Template Previews 模板预览
开发模式下,Studio 可以提供邮件模板的实时预览:你更新模板代码时预览会自动重新渲染;还可以提供一个 JSON payload 作为模板组件的 props。官方文档明确指出预览是"近似"效果,但对大多数场景足以覆盖 90% 的观感判断。
Local Inbox 本地收件箱
使用默认的 Studio 开发 handler 时,你应用里发出的每封邮件都会进入 Studio 内置的本地 SMTP 收件箱。这样你可以在本地完整跑通"发送 -> 收件 -> 查看"流程,无需自建本地邮箱,也不必借助在线临时邮箱服务。
:::warning Redwood Studio 目前仍是实验性功能,处于持续开发中。上述 UI 可能与最新版本略有差异,功能细节也可能会随时间调整。 :::
生产模式:直达默认通道
当NODE_ENV既不是test也不是development时,Mailer 进入生产模式。此模式下不会重定向任何邮件:邮件直接交给发送选项中指定的 handler;未指定则使用handling.default配置的默认 handler。这也是配置文件中default字段为什么是必填项的原因——生产模式的兜底通道完全由它决定。
另外,Mailer还暴露了sendWithoutRendering方法(mailer.ts#L252-L317):当你已经有现成的 HTML/文本内容、不需要渲染器时,可以直接把内容交给 handler 发送,同样支持按模式分流与 defaults 合并。此外还有一组 getter 可用于编程式访问当前环境下的实际通道:getTestHandler()、getDevelopmentHandler()、getDefaultProductionHandler()、getDefaultHandler()与getDefaultRenderer()。
自定义 Handler 与 Renderer:扩展生态
如果官方 handler/renderer 无法满足你的技术栈,Mailer 并不阻止你自建。做法是:
- 阅读现有实现作为参照:
- handlers:packages/mailer/handlers
- renderers:packages/mailer/renderers
- 实现
AbstractMailHandler/AbstractMailRenderer定义的接口(位于 packages/mailer/core 的@redwoodjs/mailer-core包中),并实现internal()方法暴露实例内部状态; - 在你的
api/src/lib/mailer.ts中注册并设为默认(或按模式指定); - 可以把自己的实现开源分享给 RedwoodJS 社区,也欢迎在社区论坛中交流。
一个自定义 handler 的最小形态可以参考 mailer.test.ts 里的MockMailHandler:继承AbstractMailHandler,实现send与internal即可。Mailer构造时会自动校验你注册的默认 handler/renderer 是否真实存在于映射表中,不存在会直接抛错,从而避免生产环境出现"配置了却发不出"的隐性故障。
小结
RedwoodJS Mailer 的价值在于把"邮件能力"做成了与框架一体的工程化组件:api/src/lib/mailer.ts一处装配,api/src/mail目录统一管理模板,send一条 API 覆盖渲染与投递,而 test/development/production 三态路由保证了从开发、测试到上线的全链路安全。配合 InMemory 收件箱做快照级测试、Studio 做模板预览与本地收件,它确实是官方文档所说的"端到端设计、开发与测试的完整邮件工具包"。若需深入实现细节,可从 packages/mailer/core 的Mailer类及其单元测试读起,再逐层查看你所用 handler 与 renderer 的具体实现。
- 后端
- 前端
- Web框架
- 开发工具
【免费下载链接】redwood
RedwoodGraphQL
相关推荐
RedwoodJS Mailer 完全指南:端到端的邮件发送、渲染、测试与 Studio 集成
RedwoodJS Mailer 完全指南:端到端的邮件发送、渲染、测试与 Studio 集成 导读 RedwoodJS Mailer 是 RedwoodJS
后端前端Web框架开发工具RedwoodJS Mailer 完全指南:从模板渲染到多环境投递的端到端邮件方案
RedwoodJS Mailer 完全指南:从模板渲染到多环境投递的端到端邮件方案 RedwoodJS Mailer 是 RedwoodJS 内置的端到端邮件发
后端前端Web框架开发工具RedwoodJS Mailer 全指南:从模板渲染、多 Provider 投递到测试与开发沙箱
RedwoodJS Mailer 全指南:从模板渲染、多 Provider 投递到测试与开发沙箱 导读 RedwoodJS Mailer 是 RedwoodJS
后端前端Web框架开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考