ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RedwoodJS 邮件发送实战:基于 Nodemailer 与 SMTP 服务构建带审计记录的用户邮件系统

RedwoodJS 邮件发送实战:基于 Nodemailer 与 SMTP 服务构建带审计记录的用户邮件系统 RedwoodJS 邮件发送实战基于 Nodemailer 与 SMTP 服务构建带审计记录的用户邮件系统【免费下载链接】redwoodRedwoodGraphQL项目地址: https://gitcode.com/gh_mirrors/re/redwood本文以 RedwoodJS 官方文档《Sending Emails》为骨架完整演示如何在 Redwood 全栈应用中集成 SMTP 邮件能力从数据模型设计、Scaffold 脚手架、GraphQL Mutation到使用 nodemailer 对接 SendInBlue现 Brevo云邮件服务发送真实邮件最后利用服务调用服务这一特性实现邮件审计日志。读完本文你将掌握一套可复制、可运行的邮件发送方案并理解 Redwood 服务层之间的调用关系与权限边界还能了解到 Redwood 仓库中内置 Mailer 框架位于 packages/mailer/core的演进脉络。整体思路先建模再生成后接线RedwoodJS 的典型开发节奏是数据模型先行、代码生成跟上、业务逻辑收尾。本文要构建的示例应用包含两部分核心能力用户列表管理保存用户的姓名与邮箱提供一个可视化界面用来查看用户并触发给某用户发送测试邮件的动作邮件审计每当管理员向某用户发送一封邮件就在数据库中追加一条审计日志记录谁在什么时候给哪个用户发了邮件。审计功能将通过从一个 Service 内部调用另一个 Service 的导出函数来实现——这是 Redwood 服务层的一项强大特性能让业务逻辑保持高度复用。邮件本身由 npm 包nodemailer负责发送通过 SMTP 协议与云邮件服务商文中以 SendInBlue 为例通信。后续小节会看到Redwood 后续版本在仓库中内置了更完整的 Mailer 框架见 packages/mailer/core/src/mailer.ts但底层依然围绕 SMTP 与邮件处理器handler展开理解本节的手写方案有助于吃透整个邮件链路。环境准备创建项目并安装 nodemailer首先创建一个新的 RedwoodJS 项目TypeScript 版本yarn create redwood-app --typescript email项目创建完成后进入email目录将nodemailer安装到 api 工作区。Redwood 使用 Yarn workspaces 管理 api 与 web 两个子包因此需要显式指定安装目标yarn workspace api add nodemailer之所以装在api侧是因为邮件的 SMTP 通信属于服务端职责绝不能把 SMTP 凭据暴露给浏览器端。数据模型设计User 与 Audit打开api/db/schema.prisma删除示例模型替换为以下两个模型model User { id String id default(uuid()) createdAt DateTime default(now()) updatedAt DateTime default(now()) updatedAt email String unique name String? audits Audit[] } model Audit { id String id default(uuid()) createdAt DateTime default(now()) updatedAt DateTime default(now()) updatedAt userId String user User relation(fields: [userId], references: [id]) log String }设计要点主键与时间戳技术上 User 模型只需要email和关联字段但示例始终保留id、createdAt、updatedAt。这是良好的数据建模习惯——后期补充字段时不必回头做破坏性迁移。email唯一约束unique确保一个邮箱只对应一个用户这也是发送邮件的目标地址。一对多关系User通过audits Audit[]与Audit建立一对多关系Audit侧用userIdrelation指向User.id。这样可以通过关系字段轻松查出某个用户的所有审计记录。字段取舍示例中的审计模型刻意保持简单仅一个log字符串真实的生产审计追踪往往需要更多信息操作者、IP、请求上下文等此处仅作演示。模型就绪后执行迁移生成数据库表并同步 Prisma Clientyarn rw prisma migrate dev --name email用 Scaffold 快速生成 CRUD 界面Scaffold 是 Redwood 的招牌功能之一一条命令即可生成针对某个模型的 SDL、Service、路由与页面免去手工编写增删改查样板代码yarn rw g scaffold User yarn rw g scaffold Audit两条命令分别生成 User 与 Audit 的完整 CRUD。随后启动开发服务器yarn rw dev浏览器会自动打开默认首页。点击/users链接进入用户列表页创建几个用户用于测试。注意务必使用真实可接收邮件的邮箱地址这样后续才能验证邮件是否真正送达。文中示例使用 fakenamegenerator.com 生成的随机用户邮箱需要先在对应页面激活邮箱地址才能收到邮件。前端接线在用户详情页添加发送邮件按钮要让管理员一键给用户发邮件需要给 User 组件添加一个触发 GraphQL Mutation 的按钮。下面给出web/src/components/User/User.tsx的完整代码包含原有的删除逻辑与新增的邮件逻辑import { useMutation } from redwoodjs/web import { toast } from redwoodjs/web/toast import { Link, routes, navigate } from redwoodjs/router const DELETE_USER_MUTATION gql mutation DeleteUserMutation($id: String!) { deleteUser(id: $id) { id } } const EMAIL_USER_MUTATION gql mutation EmailUserMutation($id: String!) { emailUser(id: $id) { id } } const timeTag (datetime) { return ( time dateTime{datetime} title{datetime} {new Date(datetime).toUTCString()} /time ) } const User ({ user }) { const [deleteUser] useMutation(DELETE_USER_MUTATION, { onCompleted: () { toast.success(User deleted) navigate(routes.users()) }, onError: (error) { toast.error(error.message) }, }) const [emailUser] useMutation(EMAIL_USER_MUTATION, { onCompleted: () { toast.success(Email sent) }, onError: (error) { toast.error(error.message) }, }) const onDeleteClick (id) { if (confirm(Are you sure you want to delete user id ?)) { deleteUser({ variables: { id } }) } } const onEmailClick (user) { if (confirm(Are you sure you want to send an email to ${user.name}?)) { emailUser({ variables: { id: user.id } }) } } return ( div classNamerw-segment header classNamerw-segment-header h2 classNamerw-heading rw-heading-secondary User {user.id} Detail /h2 /header table classNamerw-table tbody tr thId/th td{user.id}/td /tr tr thCreated at/th td{timeTag(user.createdAt)}/td /tr tr thUpdated at/th td{timeTag(user.updatedAt)}/td /tr tr thEmail/th td{user.email}/td /tr tr thName/th td{user.name}/td /tr /tbody /table /div nav classNamerw-button-group Link to{routes.editUser({ id: user.id })} classNamerw-button rw-button-blue Edit /Link button typebutton classNamerw-button rw-button-red onClick{() onDeleteClick(user.id)} Delete /button button typebutton classNamerw-button rw-button-blue onClick{() onEmailClick(user)} Send email /button /nav / ) } export default User代码要点useMutation来自redwoodjs/web封装了 GraphQL 请求的发送与状态管理onCompleted/onError分别处理成功与失败回调配合redwoodjs/web/toast的toast.success/toast.error给出用户反馈EMAIL_USER_MUTATION声明式定义前端要调用的 Mutation参数为$id: String!返回User对象确认弹窗删除与发送邮件前都使用confirm二次确认避免误操作。定义 GraphQL SDL 与 Service 占位实现前端 Mutation 需要后端 SDL 中对应字段才能生效。在api/src/graphql/users.sdl.ts的Mutation类型中加入export const schema gql // ... type Mutation { // ... emailUser(id: String!): User! requireAuth } requireAuth指令表示该 Mutation 要求用户已登录属于受保护操作。从仓库源码看Redwood 的指令系统集中在 packages/graphql-server/src/directives负责把 SDL 中声明的requireAuth、skipAuth等指令转换为 GraphQL 执行时的鉴权校验逻辑这是服务端安全的最后一道闸门。随后在api/src/services/users/users.ts中先写一个占位实现验证链路是否打通// ... import type { Prisma } from prisma/client // ... export const emailUser async ({ id }: Prisma.UserWhereUniqueInput) { const user await db.user.findUnique({ where: { id }, }) console.log(Sending email to, user) return user } // ...此时点击Send email按钮服务端终端应打印Sending email to ...证明从前端 Mutation 到 Service 的完整链路已经打通。配置 SMTP 邮件服务商以 SendInBlue 为例要真正发出邮件需要一个可通过 SMTP 协议通信的邮件服务器。可选方案对比Etherealnodemailer 官方示例使用的测试服务邮件不会真正投递仅用于本地调试个人 Gmail可行但需要配置 OAuth2流程繁琐且不够可靠云邮件服务商推荐多数提供免费额度足以支撑小型生产应用。本文使用 SendInBlue现更名为 Brevo其免费套餐每天可发送 300 封邮件。SendInBlue 侧的操作步骤注册账号需要提供地址与电话号码用于防止垃圾邮件滥用点击右上角公司名称菜单选择SMTP API进入SMTP选项卡生成一个新的 SMTP key名称随意复制生成的密钥。回到代码编辑项目根目录的.env文件在末尾新增一行环境变量SEND_IN_BLUE_KEYxsmtpsib-7fa6eb37c244429933ea870185063c493ba1c820f826c5f620877dd815392602-rZgB6GUV1CF2NLAK上例仅为占位格式请替换为你自己生成的密钥。若开发服务器仍在运行需要重启才能加载新的环境变量。封装发送函数email.ts在 api 侧的lib目录即api/src/lib/新建email.ts封装基于 nodemailer 的邮件发送函数import * as nodemailer from nodemailer interface Options { to: string | string[] subject: string text: string html: string } export async function sendEmail({ to, subject, text, html }: Options) { console.log(Sending email to:, to) // create reusable transporter object using SendInBlue for SMTP const transporter nodemailer.createTransport({ host: smtp-relay.sendinblue.com, port: 587, secure: false, // true for 465, false for other ports auth: { user: youremail.com, pass: process.env.SEND_IN_BLUE_KEY, }, }) // send mail with defined transport object const info await transporter.sendMail({ from: Your Name youremail.com, to: Array.isArray(to) ? to : [to], // list of receivers subject, // Subject line text, // plain text body html, // html body }) return info }配置细节说明SMTP 端点host使用smtp-relay.sendinblue.com端口 587、secure: false对应 STARTTLS 加密若改用 465 端口则需secure: true认证auth.user填写你在 SendInBlue 注册时使用的邮箱注意区分大小写auth.pass使用环境变量SEND_IN_BLUE_KEY避免把密钥硬编码进源码收件人兼容性to参数设计为string | string[]发送前通过Array.isArray归一化为数组兼容单个与批量收件人双正文同时提供text纯文本与html富文本两种正文适配不同邮件客户端from地址代码中Your Name youremail.com的youremail.com需要替换为注册 SendInBlue 时使用的邮箱邮箱地址以 SendInBlue 网站显示为准大小写敏感。打通 Service将邮件发送接入业务逻辑回到api/src/services/users/users.ts完成三处接线1. 导入发送函数放在 db import 之后// ... import { sendEmail } from src/lib/email // ...2. 定义测试邮件辅助函数// ... function sendTestEmail(emailAddress: string) { const subject Test Email const text This is a manually triggered test email.\n\n It was sent from a RedwoodJS application. const html This is a manually triggered test email.brbr It was sent from a RedwoodJS application. return sendEmail({ to: emailAddress, subject, text, html }) } // ...3. 替换占位实现中的console.log// ... await sendTestEmail(user.email) // ...现在回到浏览器点击用户详情页的 Send email 按钮。终端会打印Sending email to: 邮箱地址稍等片刻邮件即出现在收件箱中若使用 fakenamegenerator 生成的邮箱投递可能有延迟需要耐心等待。服务调用服务实现邮件审计最后一个环节是审计。需求是每次通过emailUser发送邮件后自动调用审计服务的createAudit写入一条日志。Redwood 让这件事异常简单——直接在 Service 中 import 另一个 Service即可调用其所有导出函数// ... import { createAudit } from ../audits/audits // ... export const emailUser async ({ id }: Prisma.UserWhereUniqueInput) { // ... await sendTestEmail(user.email) await createAudit({ input: { userId: id, log: Admin sent test email to user }, }) // ... } // ...createAudit是 Scaffold 为 Audit 模型自动生成的 Service 函数其入参结构{ input: { userId, log } }可能初看不直观但 TypeScript 类型会给出完整提示。这里做的事情是把新审计记录与现有用户关联userId并写入日志消息Admin sent test email to user。审计记录会自动带上时间戳与生成的 id。查看审计日志同样简单Scaffold 已生成/audits页面访问http://localhost:8910/audits即可看到所有记录。安全边界服务间调用会绕过 GraphQL 指令文档在这里有一个重要的安全提示PSA服务间调用可能绕过你在 GraphQL 层设置的安全措施。原因在于从web 端调用 Service 时走的是 GraphQL请求会被requireAuth等指令拦截校验但Service 之间的直接函数调用不经过 GraphQL 层。如果一个对所有人开放的服务即 SDL 中使用skipAuth导入了另一个受保护的服务并调用其函数那么这些函数内部的逻辑会无条件执行无论目标函数在 GraphQL 侧声明了什么指令。在本文示例中emailUserMutation 声明了requireAuth因此不存在这个风险。但在设计自己的服务时务必把GraphQL 指令是传输层保护函数调用是业务层逻辑这一区别牢记在心必要时在 Service 函数内部自行做权限判断。进阶仓库内置 Mailer 框架的演进脉络当前仓库中Redwood 已演进出一套更完整的官方邮件方案。从源码结构看packages/mailer/core 提供了核心的Mailer类见 mailer.ts它把发送邮件抽象为两层组件Handler处理器负责实际投递抽象基类定义在 handler.ts其send方法接收渲染后的邮件内容、发送选项与处理器选项返回发送结果Renderer渲染器负责把模板渲染为邮件内容。Mailer类支持三种运行模式——test、development、production可在配置中分别指定不同模式下的 handler例如测试模式用内存 handler、开发模式用 Studio handler、生产模式才真正走 SMTP并根据NODE_ENV自动判定当前模式。这一设计与本文手写的email.ts方案一脉相承都是围绕 SMTP 投递链路做封装区别在于 Mailer 框架把 handler 可插拔化、把渲染与投递解耦、并把环境差异纳入配置管理。完整的 Mailer 使用说明可参考仓库文档 docs/docs/mailer.md。对于小型应用或快速原型本文基于 nodemailer 的手写方案足够直接有效随着项目规模增长可以平滑迁移到官方 Mailer 框架以获得更规范的开发/测试/生产隔离能力。小结本文完整走通了 RedwoodJS 邮件发送的实战链路建模与迁移Prisma 中定义 User 与 Audit 一对多模型yarn rw prisma migrate dev落地数据库Scaffold 生成yarn rw g scaffold User/Audit一键获得 CRUD 页面与服务前端触发User 组件中通过useMutation调用emailUserMutationSDL 保护Mutation 声明requireAuth由指令系统实施鉴权SMTP 集成api/src/lib/email.ts用 nodemailer 对接 SendInBlue凭据存放于.env服务编排emailUser内部调用sendTestEmail与createAudit实现发信即留痕的审计能力同时理解了服务间调用绕开 GraphQL 指令的权限边界。这套模式可以推广到任何需要邮件能力的 Redwood 应用——欢迎信、重置密码、通知提醒等只需替换邮件内容生成逻辑与触发时机即可复用。【免费下载链接】redwoodRedwoodGraphQL项目地址: https://gitcode.com/gh_mirrors/re/redwood创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表