@nestos/mail
v0.5.1
Published
Portable indexed VFS mail archive with paginated threads and a host-injected reader.
Readme
@nestos/mail
独立安装与开发
npm install @nestos/mail @nestos/vfs-core
# 在本仓库开发
pnpm install --frozen-lockfile
pnpm typecheck
pnpm test
pnpm build
pnpm pack根入口与 /contracts 入口不依赖 NestOS 服务;可选 /ui 入口使用原生 DOM,并通过宿主传入文案、图标及导航。按需引入 @nestos/mail/ui.css。Node 或只使用领域功能的消费者无需导入 UI。
独立、平台中立的 VFS 邮件归档模块;不依赖 NestOS、Session、Flow、Node 或数据库私有接口。
- 根入口:
MailArchive、SeqMailIndex、邮件解析。宿主注入已授权的邮件/索引文件视图、索引位置、操作 ID 与诊断函数。 @nestos/mail/contracts:MailClient、分页摘要与主题正文 DTO,不加载 I/O 或 UI。@nestos/mail/ui:MailWorkbench,复用 mdxeditor 的 ContentView / ContentListView / ContentReader,宿主注入文案、图标和导航,可选 createReader 扩展正文阅读器。@nestos/mail/ui.css:可选阅读器样式。
查询与搜索
queryMail({page, sourceId, search, searchMode}) 固定每页 20 个主题。先在所选邮箱的完整索引上匹配,再按主题分组和分页;搜索不受当前页限制。headers 默认匹配主题、发件人、收件人、服务商与邮箱,content 另外匹配正文。空格分隔的关键词取交集,匹配大小写与全角半角归一化后的子串,不支持正则或 Gmail 查询语法。
同一来源目录内的主题按 NFKC、大小写与空白归一化,并去掉重复的 Re/Fw/Fwd/回复/答复/转发前缀后合并。不同邮箱不合并。相同主题但无直接回复关系的邮件也会合并;当前格式没有可靠的 Message-ID/References。主题身份由来源目录与归一化主题确定,新增回复不改变身份。历史 .email 路径仍可打开对应主题。
readThread({id, offset, revision}) 按需读取正文,每批至多 20 封、约 1 MiB,单封至多 512 KiB。继续读取携带 revision,索引变化返回 MAIL_THREAD_CHANGED,避免翻页时遗漏或重复。UI 每封邮件挂载独立的 ContentView 与 ContentReader,卡片显示发件人、主题、日期与正文摘要;默认展开最新一封,其余折叠,支持单封和全部折叠/展开。展开后邮件头与正文分开呈现,正文按阅读排版保留原始文本。复制整个主题已加载的邮件;仅当 nextOffset 存在且小于总数时显示加载更多,追加邮件保留已有正文实例和折叠状态,并显示已加载/总数。默认正文使用纯文本和 literal HTML 策略,邮件内容不能触发脚本、外部图片或插件执行。
存储与更新
主题摘要的 unread 与 readSnapshot 只由索引和阅读状态元数据派生。readThread 返回同一主题的成员/源版本快照;成功显示当前代次的正文后,UI 才调用 confirmThreadRead({id, readSnapshot}),读取失败、切换后的迟到结果与关闭不会提交确认。主题以成功显示第一页作为已读边界,不要求展开或加载全部历史邮件。
确认在原索引 SeqFile 的独立 read/<encoded-thread-id> 项保存精确成员/版本快照,不修改源邮件或远程邮箱。索引重建不覆盖阅读记录;缺少状态的旧归档默认未读。新增或变化的成员使快照不匹配而恢复未读;旧确认先检查当前成员快照,再以索引修订事务提交,竞争时返回 MAIL_READ_STALE 并刷新侧栏,不自动重放写入。跨实例确认不会以旧状态覆盖新索引。摘要只补读当前页最多 20 个状态项,不为已读状态读取正文。
侧栏以中英文已读/未读文字、无障碍标签和字重区分,选中态保持独立;保存成功立即更新匹配快照的条目并使在途查询失效。保存失败保留未读并提示重新打开重试及具体安全错误码;mail.read-state 的 state.load / state.confirm 日志包含关联操作 ID、结果、错误分类和耗时,不记录主题、路径、快照或正文。
原始 .email 和 .mail-source.json 保留为事实来源。独立 SeqFile 保存版本 1 的 manifest 及逐邮件摘要、源文件版本戳、归一化正文搜索文本;索引包含私密正文的搜索副本,必须与原归档使用相同用户隔离和访问权限,不向浏览器暴露索引文件。原始邮件不重写、不迁移。
首次查询扫描归档并建立索引。后续查询复用内存索引;文件事件使其失效,显式刷新或距上次检查 30 秒后的查询重新核对目录元数据,只读新增/变化邮件的正文。重新创建服务会从持久索引恢复,再核对源文件;不逐封重读未变化的正文。上限为 20,000 个目录项、邮件总计 32 MiB,失败保留明确错误分类。当前搜索扫描有界内存索引,不是无限规模的数据库全文检索。
重建与并发查询共用同一在途任务;文件读取使用最多八个连续工作者,避免固定批次屏障。SeqFile 事务原子更新变化行、删除失效行及 manifest,并检查索引修订,竞争失败重新读取后重试。宿主应在同一用户归档的多个视图间共享一个实例,最后一个租约关闭时释放监听。
验证:pnpm --filter @nestos/mail typecheck、test、build。
选区 AI 通过可选 onContentSelection(selection, signal) 注入;只取可见正文中实际选择的文字,跨多封邮件保留有序不透明来源 ID。选中文字只显示入口,点击才调用宿主创建新会话。失败重试保留同一选区对象,隐藏/销毁终止观察,不撤销已接纳的执行;分页和邮件业务不进入 mdxeditor。
邮件正文窗口目录通过 ContentListView.outlineName 配置“时间 + 发件人”,复用 ui-common 的公共目录视图与窗口注册表。重复发件人不影响按邮件 ID 定位;选择目录项展开原内容框,保留正文 reader。宿主文案新增 outline、outlineEmpty 和 outlineError,目录诊断不包含名称、发件人或正文。
The optional UI mountTitleBar(container, parts) hook lets hosts adopt the original thread title and actions into their shared navigation header. The returned cleanup runs when the thread is replaced or destroyed, including late reader preparation. Navigation remains host-owned. Mobile hosts should determine list/detail visibility from the active resource rather than treating the initial empty-reader hint as opened content.
