Baserow 邮件系统实战:MJML 模板编译、BaseEmailMessage 发送与 MailHog 测试全流程

📅 发布时间:2026/9/17 22:09:12
Baserow 邮件系统实战:MJML 模板编译、BaseEmailMessage 发送与 MailHog 测试全流程
Baserow 邮件系统实战MJML 模板编译、BaseEmailMessage 发送与 MailHog 测试全流程【免费下载链接】baserowBuild databases, automations, apps agents with AI — no code. Open source platform available on cloud and self-hosted. GDPR, HIPAA, SOC 2 compliant. Best Airtable alternative.项目地址: https://gitcode.com/GitHub_Trending/ba/baserowBaserow 的所有用户通知邮件注册验证、工作区邀请、密码重置、通知摘要等都遵循一套统一的工程模式用 MJML Eta 定义响应式邮件模板构建时编译为 Django HTML 模板运行时通过继承BaseEmailMessage的子类发送。读完本文你将掌握如何在 Baserow 中新增一个可翻译的邮件模板、如何定义并发送带 HTML/纯文本双版本的邮件消息以及如何在开发环境中用 MailHog 验证邮件效果并对模板编译器eta → mjml 两步构建的底层实现有源码级的理解。一、邮件模板体系总览Baserow 使用 MJML 框架来定义邮件模板。MJML 是一种基于 XML 的标记语言专门用于编写响应式邮件但 Django 无法直接消费 MJML因此模板必须先编译为 HTML 版本才能被 Django 使用。模板源码文件*.mjml.eta与编译产物*.html并存于同一个目录中。以核心模块为例backend/src/baserow/core/templates/baserow目录下可以看到成对出现的文件core/user/reset_password.mjml.eta源模板→core/user/reset_password.html编译产物core/workspace_invitation.mjml.eta→core/workspace_invitation.htmlcore/notifications_summary.mjml.eta→core/notifications_summary.html顶层的base.layout.eta则是所有邮件共用的 Eta 基础布局二、创建一个新的邮件模板2.1 文件位置与继承关系新建模板文件*.mjml.eta应放在以下两个位置之一核心邮件模板目录backend/src/baserow/core/templates/baserow该模板所属 contrib 模块自己的模板目录。模板必须满足三个要求使用MJML 格式书写通常通过 Eta 的layout()语句继承所有邮件共用的基础布局base.layout.eta除格式之外它是一个普通的 Django 模板——使用 Django 模板语言并采用标准的 Django 翻译语法。一个模板的典型写法如下% layout(../base.layout.eta) % mj-section mj-column mj-text mj-classtitle{% trans Title to be translated %}/mj-text mj-text mj-classtext {% blocktrans trimmed %} Text to be translated {% endblocktrans %} /mj-text mj-button mj-classbutton href{{ some_link_passed_as_context_variable }} {% trans Link text to be translated %} /mj-button /mj-column /mj-section仓库中 reset_password.mjml.eta 就是一个完整的真实示例。注意它有三个值得学习的细节首行% layout(../../base.layout.eta) %通过 Eta 继承基础布局路径相对于模板自身目录{% blocktrans trimmed with expire_hours|floatformat:0 as hours %}展示了如何在翻译块中内联 Django 过滤器、绑定变量实现带占位符的可翻译文案按钮下方额外放置了一个button-url文本块直接显示{{ reset_url }}并用!-- htmlmin:ignore --注释包裹条件块防止 HTML 压缩器吞掉 Django 的{% if %}/{% endif %}标签。2.2 基础布局 base.layout.eta 的约定base.layout.eta 是所有邮件的公共骨架文件头部的注释块解释了整套构建思路为什么不能只跑 Django 模板Django 在运行时求值而邮件构建不应在运行时执行 mjml也不能让 mjml 直接处理含 Djangoblock/extend标签的模板mjml 无法解析分散在 Django 块中的 MJML 片段所以才引入 Eta 作为构建时的第一步模板处理最终在构建期生成 HTML 版 Django 模板运行时不再需要打包、运行 mjml。该布局定义了若干可复用的mj-class新模板应直接引用而不是自己内联样式mj-class 名称用途关键样式title邮件大标题22px600 字重Inter 字体text正文13px行高 170%button主按钮白字、#5190ef背景、4px 圆角、12px 30px内边距button-url按钮下方的链接提示文本12px灰色notification-title/notification-description通知摘要邮件的标题与描述14px / 12pxmb-20/mt-20上下外边距工具类20px布局还负责页眉左侧渲染{{ logo_url }}图片并链接到{{ baserow_embedded_share_url }}右侧渲染{{ logo_additional_text }}子模板的正文通过%~ it.body %插值注入。这些上下文变量由后端统一注入见下文email_context_registry无需每个邮件自行传递。2.3 编译模板使用 Baserow Docker Compose 开发环境just dc-dev up -d时新模板会被自动编译为 HTML 版本并生成同名.html文件。若不是这种情况例如本地开发按 backend/email_compiler 的说明手动编译# 在 backend/email_compiler 目录下 yarn install # 安装 eta、mjml、chokidar 等依赖 yarn run watch # 监听模式监听 *.mjml.eta / *.layout.eta 变化并重新编译 yarn run compile # 编译一次后退出规则Do确保收到的邮件同时具有 HTML 和纯文本两个版本确保邮件模板的全部内容都可以翻译确保链接正确指向 Baserow 实例不要硬编码 URL。规则Dont不要直接编辑 HTML 邮件模板而是编辑对应的 MJML 模板后重新编译。2.4 编译器源码剖析两步构建编译入口是 baserowEmailCompiler.js其核心逻辑印证了上述文档描述定位源文件默认以backend/src为搜索根可用环境变量MJML_FILE_SEARCH_ROOT覆盖用 glob 匹配**/*.mjml.eta与**/*.layout.eta两种模式——因此 contrib 模块下的模板同样会被自动发现并编译Eta 先跑compileEtaAndMjml函数将 Eta 的views配置为模板所在目录使layout(path)语句可以相对自身目录解析然后Eta.render得到一段完整的 MJML 文本MJML 再跑mjml2html(mjmlText, { validationLevel: strict, beautify: true })注意strict校验级别——模板中写错的 MJML 属性会直接报错而不是被静默忽略落盘将.mjml.eta后缀替换为.html后写出编译产物监听模式watch参数启用时用 chokidar 监听新增/修改的*.mjml.eta并单个重编译一旦*.layout.eta基础布局变化则全量重编译所有模板recompileAllEtaAndMjmlFilesAfterLayoutFileChanges因为每个模板都依赖布局。package.json 显示核心依赖为eta ^2.0.0、mjml ^4.15.0、chokidar ^3.5.3、glob ^9.0.0watch/compile两个 script 分别对应上述两种运行方式。三、发送邮件继承 BaseEmailMessage文档要求通过继承BaseEmailMessage来定义一个携带正确模板和全部所需参数的邮件消息类。其实现位于 emails.py阅读源码可以补充文档未展开的关键机制class BaseEmailMessage(EmailMultiAlternatives): subject None template_name None def __init__(self, to, from_emailNone): if not from_email: from_email self.get_from_email() subject self.get_subject() template_name self.get_template_name() context self.get_context() html_content render_to_string(template_name, context) text_content self._get_plain_text_from_html(html_content) super().__init__( subjectsubject, bodytext_content, from_emailfrom_email, toto ) self.attach_alternative(html_content, text/html)由此可以看出邮件必须同时有 HTML 和纯文本版本这条 Do 规则的实现方式子类只需维护 HTML 模板纯文本正文由_get_plain_text_from_html自动从 HTML 转换截取body段、strip_tags去标签、正则折叠多余空格与连续换行。send()方法还包了transaction.on_commit——事务未提交时邮件不会发出避免回滚后出现幽灵邮件。子类约定三个可覆盖的钩子get_subject()返回主题行未实现时抛NotImplementedErrorget_template_name()返回编译好的 HTML 模板路径*.html不是*.mjml.etaget_context()基础实现返回email_context_registry.get_context()注入logo_url、baserow_embedded_share_url等全局上下文变量子类应调用super().get_context()后再context.update(...)追加自己的变量——这正是模板示例中{{ some_link_passed_as_context_variable }}的由来。仓库中的标准用法示例来自 user/emails.pyclass ResetPasswordEmail(BaseEmailMessage): template_name baserow/core/user/reset_password.html # ... 在 __init__ 中保存 reset_url / expire_hours # 在 get_context() 中 context.update(reset_url..., expire_hours...)emails.py 本身还包含三个内置邮件各自示范了不同的工程约束EmailPendingVerificationEmail最简单的传 URL 覆写主题模式WorkspaceInvitationEmail主题行刻意不含任何用户可控内容如邀请人姓名、工作区名因为工作区邀请可被滥用来向任意人发送钓鱼邮件用户可控字段在get_context中经truncatechars(..., 60)截断并经过prevent_autolink处理——在每个.后插入零宽空格阻止邮件客户端把域名样式的用户内容自动渲染成可点击链接NotificationsSummaryEmail循环渲染通知列表时会检查notification_type.include_in_notifications_email标志不满足则记录 error 日志并跳过同时计算unlisted_notifications_count用于在邮件中提示另有 N 条未列出的通知。发送邮件的 Do 规则源码佐证在后台 Celery 任务中发送邮件避免把 SMTP 网络 IO 放在请求主流程中使用with translation.override(user.profile.language)以用户所选语言发送邮件。仓库中这一模式被广泛使用例如 core/handler.py 在发送工作区邀请邮件前切换到邀请人的语言环境database/table/handler.py 在表格相关通知中同样如此。由于模板中的{% trans %}/{% blocktrans %}在render_to_string时求值语言环境切换必须在渲染之前生效。四、测试用 MailHog 验证邮件Baserow 开发环境会自动启动一个 MailHog 实例地址为http://localhost:8025/可以直接在那里查看邮件的 HTML 渲染效果和发送情况验证模板格式是否正确Docker 开发环境just dc-dev up -d # MailHog 已包含在栈中本地开发just dev up # MailHog 通过 Docker 运行一个完整的新增邮件工作流因此是在templates目录新增*.mjml.eta→ 由编译器自动或手动yarn run compile产出.html→ 在 user/emails.py 这类模块中新增BaseEmailMessage子类并在后台任务中发送 → 打开localhost:8025核对双语版本、翻译与链接。由于编译是严格校验的validationLevel: strict且测试环境对模板渲染失败会直接抛错模板层面的问题通常能在这条链路上尽早暴露。五、小结环节关键点仓库依据模板定义*.mjml.eta 继承base.layout.eta Django 翻译标签base.layout.eta模板编译Eta 先展开布局 → mjml 严格校验 → 写出.htmlwatch 模式自动重编译布局变更触发全量重编译baserowEmailCompiler.js消息发送继承BaseEmailMessage纯文本自动由 HTML 派生transaction.on_commit延迟投递emails.py语言与并发Celery 后台任务 translation.override(user.profile.language)core/handler.py验证MailHoglocalhost:8025just dc-dev up -d/just dev upemail_compiler/README.md遵循编辑 MJML 而非 HTML、全内容可翻译、链接不硬编码、后台发送这几条规则即可在 Baserow 中安全地扩展出符合现有工程体系的新邮件类型。【免费下载链接】baserowBuild databases, automations, apps agents with AI — no code. Open source platform available on cloud and self-hosted. GDPR, HIPAA, SOC 2 compliant. Best Airtable alternative.项目地址: https://gitcode.com/GitHub_Trending/ba/baserow创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考