CLI【免费下载链接】himalayaCLI to manage emails项目地址https://gitcode.com/gh_mirrors/hi/himalaya点击查看免费下载本文围绕 himalaya 仓库中已落地的 Cairn 变更提案 pimdir-public-id/proposal.md 展开讲解 pimdir离线缓存后端如何把Envelope.id从冗长的内部link_id形如mid:……的长字符串改为由 io-pimdir 存储分配的短公开seq小整数语义类似 IMAP UID并完整走读读路径、写路径与add_message三条调用链的解析细节与失败处理。读完你将掌握pimdir 后端的消息寻址模型、seq与link_id的解析调用链以及如何按短 ID 执行读信、打标、移动、删除等真实操作。背景为什么 pimdir 需要一个公开 IDpimdir 是 himalaya 的一个本地离线后端它不是一个真正的邮件服务器而是由同步引擎Neverest / io-pimdir填充的本地缓存底层是 SQLite 索引pimdir.db加内容寻址的 blob 存储objects/。在本次变更之前pimdir 后端把Envelope.id直接设为内部键link_id——那是一个很长的mid:……字符串。它的后果在提案中写得很直白长字符串会填满信封表格的 ID 列排版被撑坏用户每次按 ID 操作读、打标、移动、删除都被迫输入或粘贴这串内部键而link_id本质上是内部键不是给人看的。相比之下其他后端都有短 IDIMAP 展示 UIDMaildir/mbox 展示短文件名。因此本次变更的目标是让 pimdir 也向用户展示并接受一个短公开 ID。变更方案三层改动提案proposal.md 的 What 一节给出的方案是由 io-pimdir 为每个条目item分配一个按集合per-collection的公开seq小整数IMAP-UID 风格后端在所有位置展示并接受它路径变更内容读路径list_envelopes/search_envelopes设置Envelope.id seq读信 写操作get_message与store_flags、copy/move、delete把 id 解析为seq再解析回内部link_id经由get_item/synced_placement后才操作非数字 id 必须清晰报错新增消息add_message返回新条目的seq经由seq_for_link配套的 Delta 需求delta.md把它提炼为一条规范陈述pimdir 后端应把每条消息的公开 iditems.seq小整数作为其Envelope.id展示与接受而不是内部link_id在读取正文或暂存编辑之前应通过 store 的get_item/seq_for_link把 id 解析回link_id并对非数字 id 清晰报错。源码走读读路径如何展示短 seq读路径的核心实现位于 src/pimdir/backend.rs。list_envelopesbackend.rs和search_envelopesbackend.rs先把整个集合的条目拉出来scan_items按sort_key seq做 keyset 分页每页SCAN_BATCH 500见 backend.rs然后逐个调用envelope_from_item构建信封/// Builds a shared [Envelope] from a stored item (no body read): the /// flags from the item, the display fields from its mail summary. fn envelope_from_item(item: PimdirItem) - Envelope { // NOTE: the public id is a short store-global integer rather than the // long link id. envelope( item.seq.to_string(), item.flags, mail_summary(item.summary.as_ref()), ) }backend.rs关键点在于id直接取自item.seq.to_string()且源码注释明确标注了这是短 store 全局整数而非长 link id。信封其余字段message_id、subject、from/to、date、size、has_attachment、flags全部来自存储的类型化邮件摘要PimdirMailSummary不读取正文——所以正文尚未本地化的条目照样能列出、能读只是读正文时会提示尚未抓取而不是报数据丢失见下节。对应单元测试 envelope_is_built_from_the_summary_without_a_body 断言envelope.id 42即 seq并逐一校验摘要字段到信封字段的映射。源码走读公开 ID 的解析与写操作写路径和读正文都需要先把公开 ID 解析成内部地址。三个辅助函数承担这件事/// Parses a message id, the public seq, off the command line, with a clear /// error for a non-numeric one. fn parse_id(id: str) - Resulti64 { id.parse::i64() .map_err(|_| anyhow!(Invalid message id {id} (expected a number))) } /// The item behind a public id, or None when the collection holds none. fn get(self, collection: str, id: str) - ResultOptionPimdirItem { self.store .get_item(collection, parse_id(id)?) .map_err(|err| anyhow!(Read {id} in {collection}: {err})) } /// The public id an action addresses, checked against the collection so a /// stale id is refused here rather than parked by the owner much later. fn seq(self, collection: str, id: str) - Resulti64 { match self.get(collection, id)? { Some(item) Ok(item.seq), None bail!(Message {id} not found in {}, collection), } }parse_id、get、seq对应提案中经由get_item/synced_placement解析的描述synced_placement是提案与落地记录2026-08-02-pimdir-public-id.md所称的写操作解析器当前 backend.rs 中承担这一职责的正是get()与seq()二者最终都通过store.get_item(collection, seq)完成解析。解析的失败语义分两种非数字 idparse_id直接报Invalid message id \{id} (expected a number)数字但不存在seq()报Message \{id} not found in {collection}即先按集合校验再操作让过期 id 在客户端侧就被拒绝而不是很久以后被 owner 搁置。各写操作如何使用解析结果get_messagebackend.rsget()拿到 item → 取item.object的 hash → 从 blob 读正文若条目没有本地正文object为 None报not downloaded yet (body not fetched); run a sync to hydrate it作为提示同步的信号而非数据丢失。seen参数为 true 时顺带暂存\Seen标记且失败只warn不打断读取。store_flagsbackend.rs对每个 id 调seq()结合当前标记集用apply_flag_op计算替换集暂存一个携带全量替换集的SetFlags动作——owner 重复应用两次会落到同一状态天然幂等。delete_messagesbackend.rs解析出seq后暂存PimdirAction::Remove { seq }。copy/moverefilebackend.rs解析源邮箱的seq构造携带seq与目标集合 id 的动作。这些写操作全部是暂存staginghimalaya 只是生产者producer从不修改索引动作入队后由 store 的 owner 在下次同步时应用。PimdirClient以无锁的PimdirReader角色读取读不阻塞同步、也不被同步阻塞以PimdirProducer角色生产者名himalaya暂存写操作见 src/pimdir/client.rs。add_message 的返回演进从 seq 到 link_id这是本次变更中值得注意的一处设计细化。提案、Delta 与最初落地记录都写明add_message应通过store.seq_for_link返回新条目的 seqadd_messagereturns the new itemsseq。但最终落地的规范cairn/spec/backends.md与当前源码把这一点修正为add_messageSHALL return the link id it staged: a queued create has noseqyet, the store assigning one when its owner applies the action.原因很实际队列中的创建queued create在 owner 应用之前还没有 seq——seq 是 store 应用动作时才分配的。当前 add_message 的实现正是如此先通过mail::derive从正文推导 link_id裸Message-ID把正文写入 blob store再入队Add动作最后返回Ok(link_id.0)。这个 link_id 在暂存到同步的时间窗口内唯一标识这次创建。与之呼应队列渲染也遵守无 seq 即无 id的约定queued_from_actionbackend.rs构造的信封故意保留空 idenvelope(String::new(), ...)测试 a_queued_creation_renders_as_mail_with_no_id 明确断言queued.envelope.id.is_empty()——因为该消息还没有公开 id而队列行 id 属于另一个空间不能塞进每条命令都会读回的那个字段。这也解释了为什么add_message的返回值最终落在 link_id 上。边界情况重复 Message-ID 不再互相隐藏本次变更的深层动机之一是重复Message-ID。规范 backends.md 中的 A Message-ID is not an address 明确pimdir 后端不得假定 item 的 link_id 就是正文携带的Message-ID也不得假定一个Message-ID在一个邮箱里至多对应一条消息。一次双重投递、一次重试的 append、一次恢复或一封已发送邮件的副本都会让同一邮箱出现两条共享Message-ID的消息——store 会为第二条铸造mint一个区分用的键形如dup:twicehost#1174。如果地址仍然从正文反推即用 link_id/Message-ID 寻址这样的寻址就指向数量未知的消息。而 seq 天然不同两条消息各有各的 seq所以都能列出、读取、操作且谁也不会被另一条隐藏铸造的键也永远不会暴露给用户。这正是展示短公开 id与重复 Message-ID两项变更的协同点相关变更见 duplicate-link-id-mints-an-item/proposal.md。单元测试 two_items_sharing_a_message_id_project_two_public_ids 验证了这一点两个共享message_id的条目分别投影为id 11与id 12且断言!minted.id.contains(dup:)。配套约定邮箱寻址与配置短 ID 只解决消息寻址邮箱寻址由另一条配套约定解决backends.mdpimdir 邮箱就是集合 id 本身逐字使用。同步引擎把来源集合绑定在命名空间下所以服务器叫INBOX的邮箱在这里是imap/INBOX-m参数接受的就是这个全名后端不推导、不截断、不接受缩写id 对 store 是不透明字符串缩短等于猜测生产者的约定。嫌长就配置别名。pimdir 相关配置集中在 config.sample.toml# A local pimdir store, a SQLite index beside content-addressed blobs, that the # Neverest sync engine populates. Read mail offline and stage the edits the next # sync pushes. # The store directory Neverest writes, holding pimdir.db and objects/. #pimdir.root ~/.local/state/neverest/example # The account whose collections this client reads, the name Neverest syncs # under. Leave it unset: a store synced by one account is read as that one. #pimdir.account posteo # A mailbox is its collection id, verbatim: ... Alias it to avoid typing it: #mailbox.alias.inbox imap/INBOX # A sent message is queued for the stores owner to send, filed under the # --save mailbox or else this one; the smtp section is not used. #mailbox.alias.sent imap/Sent要点pimdir.root指向 Neverest 写入的存储目录默认按账号落在 XDG state 目录下或跟随 Neverest 的store.root客户端打开时要求存储已存在client.rs 检查pimdir.db不存在则报No pimdir store at …; run a sync to create one避免把拼错的路径答成一个空邮箱pimdir.account决定读哪个账号的集合多账号共享同一 store 时必填单账号可留空自动推导。此外src/pimdir/cli.rs 定义了pimdir子命令pimdir queue list/pimdir queue cancel用于查看和撤回尚未被同步应用的动作——队列中的创建/发送没有公开 id因此不占信封表格的 ID 列而是作为排队中的消息单独渲染。验证与测试本次变更的核对清单记录在 tasks.md全部勾选完成并且经过实机验证落地日志envelope list显示 id1..N恰好适配表格message read 3按短 id 读到正文flag add 2 --flag flagged能解析 id 并暂存标记变更非数字 id 得到Invalid message id … (expected a number)的清晰报错。在仓库内可直接复现的单元测试还包括envelope_is_built_from_the_summary_without_a_bodyseq 进入Envelope.id摘要字段完整投影全程不读正文two_items_sharing_a_message_id_project_two_public_ids重复 Message-ID 下两条消息各有独立公开 idan_unread_flag_set_renders_as_no_flags_rather_than_panicking未知标记集PimdirFlags::Unknown不 panic且 id 仍为1a_queued_creation_renders_as_mail_with_no_id排队中的创建以无 id 信封渲染。总结本变更以pimdir 展示短公开 idbackends.md为落点核心成果可以概括为四点展示list/search路径把Envelope.id设为 store 分配的短seqbackend.rs表格从此告别长mid:……串接受读信与所有写操作先parse_id解析为seq再经store.get_item解析回内部link_id后操作非数字 id 报Invalid message id … (expected a number)未知 id 报 not foundbackend.rs新增add_message在落地时细化——排队中的创建尚无 seq因此返回暂存的 link_id 以标识本次创建等待窗口内可被queue list追踪一致性短 id 与邮箱 集合 id 逐字使用imap/INBOX共同构成 pimdir 后端的寻址约定配合mailbox.alias别名可完全避免键入长名。对使用 himalaya pimdir 离线缓存的用户和自动化脚本而言这套机制意味着列表里的 ID 可以直接复制、输入和记忆重复投递导致的重复消息不会互相吞没且所有按 ID 的操作在等待同步期间都能被清晰地暂存与追踪。赞分享CLI【免费下载链接】himalayaCLI to manage emails项目地址https://gitcode.com/gh_mirrors/hi/himalaya点击查看免费下载相关推荐himalaya pimdir 后端消息公开标识public seq机制解析从长 link_id 到短整型 ID 的完整改造himalaya pimdir 后端消息公开标识public seq机制解析从长 link_id 到短整型 ID 的完整改造 本文基于 himalayaCLIHimalaya pimdir 后端重构邮箱即集合 IDA pimdir mailbox is its collection id完整解析Himalaya pimdir 后端重构邮箱即集合 IDA pimdir mailbox is its collection id完整解析 本文围绕 HiCLIhimalaya pimdir 后端自动探测写入源pimdir-auto-source实现解析himalaya pimdir 后端自动探测写入源pimdir auto source实现解析 本篇技术指南以 himalaya 仓库中已落地的 pimdiCLI上一篇akshare数据开源开放协作的金融数据解决方案下一篇chrome-extensions-samples 之 Telnet 示例解析用 Chrome Apps Socket API 构建 ANSI 彩色 TCP 客户端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考