ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于 RedwoodJS 发送电子邮件:从 nodemailer + SMTP 实战到 Service 间复用与审计

基于 RedwoodJS 发送电子邮件:从 nodemailer + SMTP 实战到 Service 间复用与审计 后端前端Web框架开发工具【免费下载链接】redwoodRedwoodGraphQL项目地址https://gitcode.com/gh_mirrors/re/redwood点击查看免费下载RedwoodJS 应用在业务成熟后几乎都会遇到发邮件的需求——无论是欢迎信、密码重置还是内部通知。本文将基于 RedwoodJS 7.x 官方教程docs/versioned_docs/version-7.x/how-to/sending-emails.md当前版本内容与 docs/docs/how-to/sending-emails.md 一致展开从一个可运行的完整示例出发逐步搭建用户列表 邮件发送按钮 发送审计三层功能并深入讲解nodemailer SMTP以 SendInBlue 为例的接入方式以及在一个 service 中调用另一个 service这一 Redwood 服务的核心特性与安全边界。读完后你将掌握从零实现、调试到审计邮件发送的完整技能链并了解 RedwoodJS 官方 Mailer 模块这一更现代的替代方案。方案选型为什么是 nodemailer SMTP 云服务要实现真实的邮件投递需要一个可通过 SMTP 协议通信的邮件服务器。nodemailer官方示例中使用的 Ethereal 只能生成测试消息邮件永远不会真正送达收件人使用个人 Gmail 账号则需要配置 OAuth2复杂度较高。因此更稳妥的做法是使用提供免费额度的云 SaaS 邮件服务。本教程选用 SendInBlue现 Brevo其免费档位允许每天发送 300 封邮件足以支撑一个小型生产应用。注意RedwoodJS 后续版本已内置更完整的 Mailer 模块见文末其 Nodemailer handler 正是封装了本节所用的同一套nodemailer机制因此本教程的底层原理依然适用。环境准备创建项目并安装依赖创建 RedwoodJS 项目yarn create redwood-app --typescript email这里使用--typescript生成 TypeScript 模板。email是项目目录名可自行替换。项目创建完成后进入目录并安装邮件发送所需的 npm 包cd email yarn workspace api add nodemailer使用yarn workspace api add是因为 RedwoodJS 采用 monorepo 结构nodemailer只在 api 侧Node 服务端使用不需要污染 web 侧的依赖。数据模型设计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和关系字段audits但保留id与createdAt/updatedAt两个时间戳几乎是 Redwood/Prisma 社区的通用习惯——事后补加字段的迁移成本远高于一开始就加上。name字段让示例更接近真实业务。Audit 模型为了演示刻意保持精简尤其是单个log字符串字段真实的审计追踪通常需要更多信息操作者、操作类型、请求上下文等。一对多关系User.audits与Audit.user通过userId外键关联后续可以通过该关系轻松查询某个用户的所有审计日志。模型就绪后执行数据库迁移yarn rw prisma migrate dev --name email用 Scaffold 快速搭建 UI 与 GraphQL 层Scaffold 是 RedwoodJS 的招牌能力之一会根据 Prisma 模型自动生成完整的 CRUD 页面、路由、SDLGraphQL Schema Definition Language与 serviceyarn rw g scaffold User yarn rw g scaffold Audit两条命令分别生成 User 与 Audit 的完整前后端脚手架。然后启动开发服务器yarn rw dev浏览器会自动打开 Redwood 默认首页。点击/users链接进入用户列表页创建几个用户。由于后续要真实发送邮件建议使用能实际收件的邮箱地址例如 fakenamegenerator.com 这类可激活的临时真实邮箱。创建几个用户后列表页与详情页的形态可以参考 scaffold 相关文档 中的说明。接下来我们要在详情页加入发送邮件按钮。前端改造为 User 详情页添加发送邮件按钮在 scaffold 生成的User.tsx组件中需要补充一个 GraphQL mutation、一个发送处理函数并在按钮组中新增按钮。完整代码如下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是 Redwood 对 Apollo Client 的封装通过onCompleted/onError处理成功与失败提示toast。EMAIL_USER_MUTATION调用服务端的emailUsermutation入参为id: String!。confirm()弹窗用于防止误触发送。在 SDL 中暴露 mutation要让这个 mutation 可被 GraphQL 调用需要在users.sdl.ts的 Mutation 类型中添加对应字段export const schema gql // ... type Mutation { // ... emailUser(id: String!): User! requireAuth } requireAuth是 RedwoodJS 的内置指令要求调用者必须通过认证这在后面讨论 service 复用的安全边界时至关重要。关于指令的更多细节可参考 directives 文档。先用占位实现打通链路先在 users service 中写一个只查询用户、不真正发信的占位方法验证整条链路通畅// ... 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按钮已经能触发 mutation 并打印日志。接下来配置真正的邮件服务。配置 SendInBlueSMTP 云服务SendInBlue 注册时需要提供地址和电话号码用于反垃圾注册。注册完成后点击右上角公司名称菜单选择 SMTP API再进入 SMTP 页签生成一个新的 SMTP key 并复制。在项目根目录的.env文件中新增环境变量SEND_IN_BLUE_KEYxsmtpsib-7fa6eb37c244429933ea870185063c493ba1c820f826c5f620877dd815392602-rZgB6GUV1CF2NLAK示例 key请替换为你的真实密钥。重要如果你的 dev server 仍在运行必须重启它才能让新环境变量生效。RedwoodJS 遵循 environment-variables 文档 中的约定开发环境会从.env加载变量而生产环境的密钥应配置在部署平台的环境中绝不应提交到代码仓库。实现邮件发送封装 email lib在 api 侧的lib目录下新建email.tsimport * 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 }需要替换的占位内容两处youremail.com替换为注册 SendInBlue 时的邮箱注意以 SendInBlue 网站上显示的为准区分大小写from中的显示名称Your Name可自定义。nodemailer.createTransport的配置说明配置项值说明hostsmtp-relay.sendinblue.comSendInBlue 的 SMTP 中继服务器地址port587SMTP 提交端口配合secure: false即 STARTTLS使用securefalsetrue对应 465 端口隐式 TLSfalse对应其他端口auth.user你的发件邮箱SendInBlue 账户邮箱大小写敏感auth.passprocess.env.SEND_IN_BLUE_KEYSMTP key从环境变量读取sendMail的to字段做了Array.isArray兼容处理支持单个与多个收件人。返回的info包含messageId等投递信息。在 users service 中接入在users.ts顶部、db 导入之后添加导入// ... import { sendEmail } from src/lib/email // ...添加一个测试邮件封装函数// ... 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 }) } // ...把之前占位实现中的console.log替换为// ... await sendTestEmail(user.email) // ...现在点击详情页的 Send email 按钮终端会打印类似Sending email to: horacebcarrierteleworm.us的日志几分钟后收件箱就能收到邮件使用 fakenamegenerator 生成的地址时投递可能较慢需要耐心等待。审计功能在 service 中调用另一个 service最后为邮件发送加上审计。当 users service 发送邮件后调用 audits service 的createAudit函数写入一条日志。RedwoodJS 让这一步变得异常简单——直接导入 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的参数结构{ input: { userId, log } }看起来不太直观但 TypeScript 类型scaffold 生成的Audit相关类型会给出明确提示。审计记录会自动获得createdAt时间戳和生成的id无需手动设置。查看审计日志访问http://localhost:8910/audits使用之前 scaffold 生成的页面即可浏览。安全边界service 间直接调用会绕过 GraphQL 指令这是本教程最重要的提醒。当从 web 侧调用 service 时请求经由 GraphQL 层会受到requireAuth/skipAuth等指令的保护但当 service A直接导入并调用service B 的函数时调用发生在 Node 进程内部完全绕过 GraphQL 层也就绕过了指令校验。具体风险场景如果一个使用skipAuth任何人均可访问的 service 导入了另一个受保护 service 的函数那么任何人都能通过暴露的入口间接触发受保护函数无论被调函数自身声明了何种指令。本示例中emailUser使用了requireAuth调用链整体是安全的但设计公共 service 时务必注意这一点可参考 services 文档 中关于服务组织与安全的最佳实践。从源码看 Mailer 的现代实现进阶延伸本教程基于nodemailer直连的方式v7.x 时代的主流做法。RedwoodJS 后续在 Mailer 模块 中对邮件发送做了体系化封装其核心架构与本文的实践一一对应值得延伸理解Handler发送端负责把渲染好的内容交给真实投递服务。官方提供redwoodjs/mailer-handler-nodemailer、redwoodjs/mailer-handler-in-memory测试用、redwoodjs/mailer-handler-studio开发用与redwoodjs/mailer-handler-resend。Nodemailer handler 实现 与本文手写的email.ts逻辑一致——createTransportsendMail并把to/cc/bcc/from/replyTo/subject/headers/attachments逐一映射为MailSendOptionsComplete最后返回{ messageID, handlerInformation }。Renderer渲染端把 React 组件如 React Email、MJML 模板渲染为{ html, text }字符串。模式路由Mailer 核心类 在构造时根据NODE_ENV判定test/development/production三种模式isTest/isDevelopment支持布尔值或函数形式的whensend()方法按当前模式选择 handler测试模式默认用 in-memory handler 拦截开发模式默认路由到 Studio 本地收件箱只有生产模式才真正投递给默认 handler。这一设计让开发/测试环境绝不误发真实邮件成为框架级保障相关行为在 mailer.test.ts 中有完整测试覆盖。快速上手yarn rw setup mailer会生成api/src/lib/mailer.ts配置与示例模板--force覆盖已有文件--skip-examples跳过示例内容详见 setup mailer 命令说明。对从零手写 nodemailer的开发者来说理解 Mailer 的 Handler/Renderer 分离、默认值合并utils.ts 中constructCompleteSendOptions对from/subject/to缺失会直接抛错等设计有助于写出更健壮的邮件模块。小结本文完整走通了一条RedwoodJS 应用发邮件的路径从 Prisma 数据模型、scaffold 生成到 GraphQL mutation、nodemailer SMTP 云服务接入再到 service 间复用实现审计闭环。核心收获有三点链路清晰web 端 mutation → SDL 暴露 → service 处理 → lib 封装nodemailer→ SMTP 投递每一层职责单一。安全边界service 间直接函数调用会绕过requireAuth等 GraphQL 指令设计公共 service 时须警惕被skipAuth入口间接调用。现代替代官方 Mailer 模块把 handler/renderer 分离、环境模式路由等最佳实践沉淀为框架能力可作为生产项目的首选方案。赞分享后端前端Web框架开发工具【免费下载链接】redwoodRedwoodGraphQL项目地址https://gitcode.com/gh_mirrors/re/redwood点击查看免费下载相关推荐RedwoodJS 实战用 Nodemailer SMTP 发送邮件并在 Service 之间实现审计日志RedwoodJS 实战用 Nodemailer SMTP 发送邮件并在 Service 之间实现审计日志 本教程基于 RedwoodJS 官方 how后端前端Web框架开发工具RedwoodJS 实战基于 Nodemailer 与 SendInBlue 实现邮件发送与审计日志RedwoodJS 实战基于 Nodemailer 与 SendInBlue 实现邮件发送与审计日志 本篇指南以 RedwoodJS v4.x 为背景完整演后端前端Web框架开发工具RedwoodJS 邮件发送实战基于 Nodemailer 与 SendInBlue 构建用户邮件与审计日志功能RedwoodJS 邮件发送实战基于 Nodemailer 与 SendInBlue 构建用户邮件与审计日志功能 本文以 RedwoodJS 官方 How T后端前端Web框架开发工具上一篇如何在3秒内预览Office文档QuickLook原生插件完整指南下一篇TopitmacOS窗口置顶工具终极指南 - 如何让任意窗口永远置顶创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表