
inboxzero/email-editor 源码解析Inbox Zero 可移植邮件 HTML 契约与 Tiptap 富文本编辑器【免费下载链接】inbox-zeroThe worlds best AI personal assistant for email. Open source app to help you reach inbox zero fast.项目地址: https://gitcode.com/GitHub_Trending/in/inbox-zero导读inboxzero/email-editor是 Inbox Zero 项目中负责“邮件撰写”的独立包它把邮箱提供商Gmail / Outlook的草稿 HTML 归一化为一份可移植的邮件 HTML 契约并在此之上提供一套基于 React Tiptap 的富文本编辑器。本文以 packages/email-editor/README.md 为主线结合包内源码与测试完整讲解该包的安装方式、三大导出面、核心 API 的底层实现HTML 清洗、引文/签名保护、cid:内联图片重写、附件校验、Web 编辑器的工作模式以及发布流程。读完你可以直接复用这套“纯 HTML 契约 可插拔编辑器”的架构也能精确掌握每个 API 的输入输出与安全边界。一、包定位为什么把“邮件正确性规则”从编辑器中拆出来电子邮件正文天然是 HTML但不同提供商Gmail、Outlook生成的草稿 HTML 千差万别且往往夹杂表格布局、gmail_signature、gmail_quote等专有结构。如果编辑器直接把这些 HTML 交给 Tiptap 解析会遇到两类问题格式化丢失表格、内联样式、gmail_attr等标记无法被通用富文本模型忠实表达安全风险草稿可能携带script、iframe、追踪像素等不应在编辑器中激活的内容。inboxzero/email-editor的解法是 README 中强调的设计决策把提供商交换格式保持在 HTML 层面同时将“可移植的邮件正确性规则”与“React/Tiptap 编辑界面”彻底分离。也就是说一套与 UI 无关的核心契约core负责 HTML 归一化、安全清洗、引文/签名拆分、附件校验一套可选的Web 编辑层web只负责把契约产出的 HTML 呈现为可编辑的富文本原生端mobile或后端发送流水线可以只依赖 core完全不带 React/Tiptap。这一分层使 Inbox Zero 的桌面端、移动端与后端发送链路能够共享同一套 HTML 语义而不会各自重复实现“如何判断一段 HTML 能不能安全编辑”。二、安装与依赖管理包已发布到 npmREADME 给出的安装命令为pnpm add inboxzero/email-editor2.1 两套入口两种依赖要求包在 package.json 中通过exports暴露三个子路径本地开发直接指向源码便于 monorepo 热更新exports: { ./core: ./src/core/email-html.ts, ./fixtures: ./src/fixtures/email-html.ts, ./web: ./src/web/EmailEditor.tsx }inboxzero/email-editor/core纯 JavaScript 构建产物 TypeScript 声明不加载 React 与 Tiptap适合任意运行时inboxzero/email-editor/webReact/Tiptap 编辑器组件必须同时安装 peer 依赖inboxzero/email-editor/fixtures匿名的 Gmail / Outlook 风格 HTML 夹具供提供商往返测试使用。core 的用法如下摘自 READMEimport { prepareEmailDraft, sanitizeEditableEmailHtml, validateEmailAttachments, } from inboxzero/email-editor/core;2.2 可选 peer 依赖原生端与后端免装 Web 栈inboxzero/email-editor/web的 peer 依赖在 package.json 中全部标记为peerDependenciesMeta.*.optional true包括tiptap/core、tiptap/pm、tiptap/react、tiptap/starter-kit、tiptap/extension-image、tiptap/extension-placeholder版本均锁定3.30.2react、react-dom19因此仅使用 core 的消费者不会引入任何 Web 编辑栈只有渲染编辑器时才需要补齐这些 peer 依赖。这正对应 README 中“native and backend consumers can install the core without bringing in a web editor stack”的表述。三、core可移植邮件 HTML 契约core 的全部实现集中在 src/core/email-html.ts约 1200 行零第三方依赖仅使用parse5做 HTML 解析与序列化。它回答一个核心问题“一段邮箱草稿 HTML能否被安全、无损地编辑”3.1 支持的编辑 profileREADME 明确列出了 core 支持的编辑格式段落与硬换行、加粗、斜体、下划线、删除线、链接、有序/无序列表、引用块、内联图片与块级方向LTR/RTL。对应到源码中的SUPPORTED_TAGSemail-html.ts类别标签块级p、div、blockquote、ol、ul、li行内样式strong、b、em、i、u、s、strike、del、span其它a、br、img需要强调的是span只允许携带font-style / font-weight / text-decoration三类样式声明isSupportedStyle见 email-html.tsdiv/p只允许direction: ltr|rtl。颜色、字号等样式不在可编辑 profile 内会被视为“不支持标记”。3.2 不支持标记的 fallback 检测与“无损优先”prepareEmailDraftemail-html.ts是 core 的入口函数它接收可编辑正文、已知引文与已知签名返回type PreparedEmailDraft { editableHtml: string; // 归一化后的可编辑正文 mode: rich | fallback; // 编辑模式 quotedHtml: string; // 受保护的引文 HTML signatureHtml: string; // 受保护的签名 HTML unsupported: string[]; // 不支持标记清单 };其处理顺序为先拆分引文或使用调用方显式传入的quotedHtml再拆分签名然后调用findUnsupportedEditableMarkup扫描可编辑区中所有不在SUPPORTED_TAGS内、或带有非法属性/非法样式/不安全 URL 的节点。若unsupported.length 0返回mode: fallback且editableHtml是原始 HTML 原样保留否则返回mode: richeditableHtml是经过normalizeEditableEmailHtml归一化的标准 profile。这正是 README 所述“未触碰的草稿保持逐字节不变byte-for-byte intact”的机制prepareEmailDraft只是检查不做破坏性清洗真正的清洗只发生在用户编辑其 sanitized 视图之后。测试 email-html.test.ts 验证了这一点——对于含table的UNSUPPORTED_EDITABLE_FIXTURE结果mode为fallback且editableHtml与输入完全相等。3.3 归一化把提供商等价标记折叠为最小 profile当可编辑区“干净”时normalizeEditableEmailHtml会通过renderFlow/renderBlock/renderInlineemail-html.ts把 HTML 折叠为统一形态文本与行内内容被包进pb折叠为strongi/em折叠为emstrike/del/s折叠为s可表达的span样式如font-weight:700转换为语义标签div内若包含块级子节点则递归展开否则渲染为段落链接统一追加target_blank relnoopener noreferrerdir属性在段落、列表、引用块间正确继承getDirection。测试 email-html.test.ts 展示了归一化结果RTL_EDITABLE_FIXTUREdiv dirrtl中嵌套b、i被折叠为三个带dirrtl的p段落。3.4 双重清洗预览 sanitize 与编辑 sanitizecore 提供两个不同强度的清洗函数sanitizePreservedEmailHtmlForPreviewemail-html.ts用于引文/签名的只读预览。它保留复杂布局表格、样式但移除DANGEROUS_PREVIEW_TAGSscript、iframe、form、object、style、svg、link、meta、base等 15 个标签只保留白名单属性与白名单 CSS 属性并强制href必须为安全 URL、src只能是cid:或受限data:image。测试 email-html.test.ts 证明表格结构被保留、onclick/onerror/script/追踪像素全部被剔除而width:100%这类安全样式得以保留。sanitizeEditableEmailHtmlemail-html.ts用于可编辑区。它的策略更激进DANGEROUS_EDITABLE_TAGS整标签删除不支持的容器标签如section、main、mark被“解包”子节点提升到父级剩余属性经getAllowedEditableAttributes白名单过滤。测试 email-html.test.ts 表明script、onclick、javascript:链接被删除file://本地图片预览得以保留无意义的mark[data-token]容器被解包为纯文本。四、引文与签名的“保护块”机制复杂的历史引文quote与签名signature不能进入编辑模型——否则 Tiptap 无法表达其表格布局。core 的答案是检测 → 拆分 → 保护。4.1 引文容器识别isQuoteContaineremail-html.ts识别以下特征任意命中即视为引文起点class 含gmail_quote/gmail_quote_containerid 为divrplyfwdmsg/x_divrplyfwdmsg/appendonsendblockquote typecite带border-top样式的divOutlook 回复分隔线。splitQuotedHtml利用parse5的sourceCodeLocationInfo找到引文容器的源码偏移量把偏移量之前的正文作为editableHtml并剥离尾部br偏移量之后全部作为quotedHtml。测试 email-html.test.ts 甚至覆盖了 2000 个连续br分隔符的极端情况。4.2 签名容器识别与剥离isSignatureContaineremail-html.ts识别gmail_signature、ms-outlook-signature、idsignature、带data-smartmail属性的节点splitSignatureHtml还支持调用方传入已知签名 HTMLknownSignatureHtml做精确匹配剥离并兼容 Gmail 的gmail_signature_prefix--前缀。4.3 发送时才合并引文与签名在整个编辑过程中保持为受保护 HTML只有真正发送时才通过combineEmailHtmlemail-html.ts按可编辑正文 → 签名 → 引文的顺序用br拼接。测试 email-html.test.ts 验证了合并顺序与空白保留行为。这正是 README 中“protected HTML combined with the canonical editable reply only when sending”的源码实现。五、内联图片与cid:重写管线内联图片的完整生命周期是 README 中描述的重点之一对应源码有三段式管线编辑期图片使用临时本地预览 URLblob:或file:显示并在img上携带data-content-id属性。Web 端通过EmailImage扩展email-extensions.tsx读写该属性core 的isSafeEditableImageSource允许file:/content:/blob:作为可编辑图片来源。发送前finalizeEditableEmailHtmlemail-html.ts遍历所有img若data-content-id能匹配调用方传入的 inline 附件集合 → 把src重写为cid:contentId并移除data-content-id否则若src仍为blob:/data:→直接删除该节点未被匹配的预览图不允许外发。发送期提供商适配器必须把对应 MIME 附件Gmail 的 MIME part / Microsoft Graph attachment与相同的 Content-ID一起发送。测试 email-html.test.ts 验证了blob:预览被改写为srccid:inline-1example、而残留data:URL 被移除。配套工具函数还包括createInlineContentId(domain)email-html.ts生成uuiddomain格式的 Content-IDdomain 必须匹配[a-z\d.-]且长度 ≤ 200否则抛错无crypto.randomUUID环境使用时间戳随机数降级方案detectInlineImageMimeTypeemail-html.ts通过 Base64 前缀解码出的魔数PNG/JPEG/GIF/WebP识别真实 MIME 类型用于校验附件内容与声明类型一致防止伪装。六、附件校验一份完整的参数契约validateEmailAttachments与validateEmailAttachmentMetadataemail-html.ts在发送前对附件做全量校验约束全部来自常量EMAIL_ATTACHMENT_LIMITSemail-html.ts常量值含义maxFiles10单封邮件最多 10 个附件maxInlineFiles5最多 5 个内联图片maxFileBytes10 MB单个附件上限maxInlineBytes3 MB单个内联图片上限maxTotalBytes15 MB附件总大小上限同时校验项还包括附件id唯一、filename非空、size为非负安全整数、Base64 内容合法且解码后大小与声明的size一致、inline 附件 MIME 必须在EMAIL_INLINE_IMAGE_MIME_TYPESgif/jpeg/png/webp内、inline 附件必须携带合法且唯一的contentId。每条失败路径都返回人类可读的error字符串见 email-html.test.ts 的失败用例覆盖。七、web无受控uncontrolledReact/Tiptap 编辑器7.1 双模式渲染EmailEditor组件src/web/EmailEditor.tsx根据prepareEmailDraft产出的mode自动选择两种渲染RichEmailEditorTiptap 富文本编辑器支持加粗/斜体/下划线/删除线/链接/有序无序列表/引用块/LTR/RTL 方向附带 BubbleMenu 浮动工具栏与链接编辑面板支持⌘/CtrlK打开。FallbackEmailEditorcontentEditable的纯 HTML 编辑器顶部显示警告条提示“草稿含提供商特有的无法安全表达为富文本的格式原样发送可保留原 HTML编辑可能简化格式”并把清洗后的 HTML 通过dangerouslySetInnerHTML渲染。其insertInlineImage/removeInlineImage恒返回false即 fallback 模式下不支持内联图片操作。组件的核心 propsEmailEditor.tsx包括initialHtml、mode、preservedBlocks、unsupported、placeholder、autofocus、onStateChange、onImageFiles以及appearance: contained | seamless。7.2 命令式句柄imperative handle通过forwardRef暴露EmailEditorHandleEmailEditor.tsxtype EmailEditorHandle { focus: () void; getValue: () EmailEditorValue; // { editableHtml, inlineContentIds, mode, preservedBlockIds } insertInlineImage: (image: { alt: string; contentId: string; previewUrl: string }) boolean; removeInlineImage: (contentId: string) boolean; };getValue在富文本模式下通过DOMSerializer把 Tiptap 文档序列化为editableHtml并收集inlineContentIds与preservedBlockIdsinsertInlineImage向文档插入emailImage节点src为 previewUrl、data-content-id为 contentIdremoveInlineImage遍历文档删除所有匹配指定contentId的图片节点。7.3 四个自研扩展email-extensions.tsx 中的createEmailEditorExtensions组装了 Tiptap 扩展集StarterKit 裁剪关闭code、codeBlock、heading、horizontalRule、dropcursor、gapcursor、trailingNode与“仅邮件子集”的 profile 保持一致link配置autolink、linkOnPaste、defaultProtocol: https并通过isAllowedUri叠加isSafeEmailUrl白名单。EmailImage基于tiptap/extension-image扩展inline: true新增contentId属性读写data-content-id并禁用 Base64 直接嵌入allowBase64: false。EmailDirection为 paragraph / blockquote / bulletList / orderedList 提供dir全局属性ltr/rtl/auto。UrlHighlighturl-highlight.ts用linkifyjs扫描纯文本中的裸 URL添加data-email-url-highlight行内装饰实现“未成链接的 URL 也能高亮”。PreservedEmailBlockNode一个atom: true、selectable: false的块级节点承载引文/签名的预览配合PreservedBlockViewpreserved-block.tsx以“⋯”展开按钮呈现——签名以内联 HTML 展示、引文以sandbox空白的iframe srcDoc展示两者在编辑器中都是contentEditable{false}的保护块。八、fixtures提供商往返测试的匿名夹具src/fixtures/email-html.ts 提供四类匿名化夹具被 email-html.test.ts 直接引用GMAIL_DRAFT_FIXTURE带gmail_signature、gmail_signature_prefix、gmail_quote_container的 Gmail 回复草稿OUTLOOK_DRAFT_FIXTURE带idSignature、iddivRplyFwdMsg与border-top样式的 Outlook 草稿含 RTL 希伯来文RTL_EDITABLE_FIXTURE纯 RTL 可编辑正文UNSUPPORTED_EDITABLE_FIXTURE含table布局的不可编辑样例。这些夹具“匿名”不含真实用户数据可安全用于提供商往返测试provider round-trip tests例如验证 Gmail 草稿解析后signatureHtml与quotedHtml被正确拆分email-html.test.ts。九、构建与发布流程包的构建脚本package.json为scripts: { build: node scripts/clean-package.mjs tsc -p tsconfig.build.json node scripts/prepare-package.mjs, pack:check: pnpm run build cd dist pnpm pack --dry-run, test: vitest run, typecheck: tsc --noEmit }工作区清单在本地开发时直接指向源码exports指向./src/*.ts保证 monorepo 内无需构建即可引用scripts/prepare-package.mjs负责生成可发布的dist把 exports 重写为{ types, import, default }三字段、为相对导入补全.js扩展名Node ESM 要求、拷贝 CSS、README 与根 LICENSEREADME 给出的发布命令cd dist pnpm publish --access public --otp one-time-password即通过pnpm pack:check先构建到dist并验证 tarball 内容测试与类型检查通过后由授权维护者执行上述发布命令需提供 npm 一次性 OTP。十、总结可以复用的“契约 编辑器”范式从 README 到源码inboxzero/email-editor呈现了一个值得借鉴的架构范式先用一套无 UI 依赖的核心把“正确性”钉死在 HTML 契约上无损优先、fallback 兜底、白名单清洗再把“编辑体验”交给可选的 React/Tiptap 层。其关键产出物——PreparedEmailDraft的 rich/fallback 双模式、cid:内联图片重写管线、引文/签名保护块、附件校验参数表——都可以直接复用到任何需要“安全编辑外部 HTML 邮件”的产品中。想深入验证这些行为可以直接阅读 src/core/email-html.test.ts 中 30 余个用例它们本身就是一份可执行的行为规格说明书。【免费下载链接】inbox-zeroThe worlds best AI personal assistant for email. Open source app to help you reach inbox zero fast.项目地址: https://gitcode.com/GitHub_Trending/in/inbox-zero创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考