- CLI
【免费下载链接】himalaya
CLI to manage emails
本文围绕 himalaya v2 新增的message.send.save-copy配置项展开,讲解如何为每个账户配置"发送邮件时自动留存副本"的邮箱、如何使用--no-save跳过单次副本、以及底层"先发送、后保存"的执行顺序。读完本文,你将掌握该配置的全局/按账户写法、与--save/--no-save的优先级关系、v1 布尔值的兼容方式,以及背后的源码实现与测试验证依据。
背景:v2 为什么重新引入"保存副本"配置
在 himalaya v1 中,message.send.save-copy(当时名为email-sending-save-copy)用于控制发送邮件时是否把副本存入sent邮箱。v2 早期版本移除了这一配置,导致通过 SMTP 发送的邮件除非每次调用都显式传--save,否则不会在任何地方留存副本。这与邮件客户端的常规预期相悖:用户希望发出的信能在"已发送"目录里找到。
从 cairn/changes/message-send-save-copy/proposal.md 可以看到,v2 移除该配置后带来的问题是多重的:
- 每个前端(himalaya-emacs、himalaya-vim、himalaya-tui)都不得不各自发明一个"保存副本"的选项,而且它们并不知道某个账户实际通过哪个后端发送;
- 副本需求本身是按账户不同的:Gmail 和 Microsoft Graph 会在服务端自行归档已发送邮件,而 SMTP 不会;
- 更关键的是,旧版
--save在发送之前就先追加副本,一旦发送失败,就会留下一条"从未离开本机的邮件副本"。
本次变更正是针对这些痛点:让副本保存成为可配置的账户级默认行为,并修正执行顺序。
配置项:message.send.save-copy
取值形式与语义
message.send.save-copy可写在全局配置表或某个账户的[accounts.<name>]表下,作为发送命令未显式传--save时的默认"副本邮箱"。它接受三类值,见 src/config.rs 中的SaveCopyConfig枚举(serde(untagged)反序列化):
| 取值 | 含义 | 示例 |
|---|---|---|
true | 存到sent角色对应的邮箱(等价于写"sent") | message.send.save-copy = true |
false | 不保存副本 | message.send.save-copy = false |
| 字符串 | 一个邮箱名、别名或角色,解析方式与--save相同 | message.send.save-copy = "Sent Items" |
字符串形式遵循 v1 语义的延续:true代表sent邮箱,false代表不保存,因此旧版 v1 配置中的布尔值在 v2 中依然按原意读取。该兼容性由 src/account/context.rs 的测试save_copy_reads_a_v1_boolean_beside_v1_keys覆盖:全局配置写message.send.save-copy = true时,resolve_save解析为sent;账户级写false覆盖全局后,解析结果为None(不保存)。
全局与按账户的合并规则
配置结构上,message.send.save-copy位于MessageConfig→MessageSendConfig两级结构中(见 src/config.rs):
# 全局表 [message.send] save-copy = "sent" # 某个账户 [accounts.gmail] message.send.save-copy = falseMessageConfig特意未启用deny_unknown_fields(对比同文件中其他配置结构都启用了该属性),这样 v1 的[message]表里其余键(如read、write、delete等)在 v2 中依然能继续加载,不会因未知键报错。账户级配置通过Account::merge覆盖全局值(save_copy: other.save_copy.or(self.save_copy),见 src/account/context.rs)。
官方配置示例
config.sample.toml 给出了全局写法:
# Mailbox a sent message is copied to when `--save` is not passed, `--no-save` # skipping it for one send. A name, alias or role; `true` stands for `sent`. # Leave it unset on Gmail and Microsoft Graph, which file the sent message # themselves. #message.send.save-copy = "sent"账户级写法见 config.sample.toml:
[accounts.example] # Per-account overrides for the global options above. #message.send.save-copy = "sent"注意示例中的关键提示:Gmail 与 Microsoft Graph 账户应保持该项不设置,因为这两个后端在服务端会自行归档已发送邮件,不需要本地再存一份。
命令行为:--no-save与解析优先级
新增的--no-save参数
message send、message compose、message reply、message forward四个命令新增了--no-save参数,用于单次发送跳过配置的副本。它与--save通过 clap 的conflicts_with声明互斥(见 src/shared/message/send.rs):
/// Skip the copy `message.send.save-copy` configures. #[arg(long, conflicts_with = "save")] pub no_save: bool,compose/reply/forward三个编写命令中--no-save与--save、--send的组合语义相同,分别见 src/shared/message/compose.rs、src/shared/message/reply.rs、src/shared/message/forward.rs。
Account::resolve_save:统一的解析入口
所有发送/编写命令都调用同一个解析函数Account::resolve_save决定"最终保存到哪里",实现见 src/account/context.rs:
pub fn resolve_save<'a>( &'a self, over: Option<&'a str>, no_save: bool, send: bool, ) -> Option<&'a str> { if over.is_some() || no_save || !send { return over; } match self.save_copy.as_ref()? { SaveCopyConfig::Enabled(true) => Some(MailboxRole::Sent.as_str()), SaveCopyConfig::Enabled(false) => None, SaveCopyConfig::Mailbox(mailbox) => Some(mailbox), } }该函数的优先级规则可以总结为下表:
| 条件 | 结果 |
|---|---|
传了--save <MAILBOX>(over有值) | 使用命令行指定的邮箱,覆盖配置 |
传了--no-save | 返回None,不保存副本 |
未发送(send = false,如compose不配合--send) | 不自动保存,配置的副本仅针对已发送邮件 |
配置为true | 解析为sent角色邮箱 |
配置为false | 不保存 |
| 配置为字符串 | 返回该邮箱名/别名/角色,交给后续resolve_mailbox通过别名表解析 |
测试 resolve_save_falls_back_on_the_copy_only_when_sending 精确验证了这组行为:配置"Sent Items"时,发送场景解析为"Sent Items",非发送场景解析为None;--save Archive覆盖为"Archive";--no-save返回None。
以message send为例,命令执行时把三个参数原样传入(见 src/shared/message/send.rs):
let save = account.resolve_save(self.save.as_deref(), self.no_save, true); handler::route(printer, account, client, raw, save, true)命令行速查
# 发送并按账户配置保存副本(如 save-copy = "sent") himalaya message send < message.eml # 发送并保存到指定邮箱(覆盖配置) himalaya message send --save "Archive" < message.eml # 发送但本次不保存副本 himalaya message send --no-save < message.eml # 编写并发送,同时保存副本 himalaya message compose --send --save "Sent Items" # 配置了副本的账户,reply/forward 加 --send 也会自动存副本 himalaya message reply --send --no-save <reply-body.txt>执行顺序:先发送,后保存
本次变更的另一核心语义是**"先发送、后保存"**(send-before-save)。此前--save在发送之前就追加副本,发送失败时会在本地留下一条"从未真正离开"的幽灵副本。
统一处理逻辑集中在 src/shared/message/handler.rs 的handler::apply:
let queued = match send { true => { let sent = mailbox.or_else(|| account.mailbox_alias.get("sent").map(String::as_str)); client.send_message(sent, raw.clone())? } false => None, }; let saved_id = match mailbox { Some(mailbox) if send => Some( client .add_message(mailbox, flags, raw) .with_context(|| format!("Message sent, but saving a copy to {mailbox} failed"))?, ), Some(mailbox) => Some(client.add_message(mailbox, flags, raw)?), None => None, };关键点:
- 先调用
client.send_message真正把邮件投递出去,再调用client.add_message追加副本; - 若发送失败,
send_message返回错误并中断,不会留下副本; - 若发送成功但保存副本失败,命令同样以错误退出,但错误信息明确说明"邮件已发出",即
Message sent, but saving a copy to <mailbox> failed——用户不会误以为邮件没发出去而重复发送。
这一顺序对--save与--send组合、message send --save、以及message add --send(见 src/shared/message/add.rs)一视同仁,所有"既要发送又要保存"的路径都走同一个apply。
handler::route在此基础上打印结果行(见 src/shared/message/handler.rs):
- 保存且已发送:
Message successfully saved and sent; - 仅保存:
Message successfully saved; - 仅发送:
Message successfully sent; - 发送被推迟到 pimdir 队列时,输出会附带
queue_id。
保存的副本默认附带\Seen(已读)标志(Flag::from_iana(IanaFlag::Seen),见 src/shared/message/handler.rs),副本邮箱名则先经account.resolve_mailbox通过别名表做大小写不敏感解析(见 src/account/context.rs),所以配置里写别名、角色或后端原生 id 均可。
设计取舍与适用边界
本次变更的提案文档 proposal.md 与日志 cairn/log/2026-10-01-message-send-save-copy.md 明确记录了两处边界:
- 向导(wizard)不写入该选项。原因是 IMAP 后端要等到
imap-special-use-aliases变更落地后才能解析sent角色,而向导也不会写mailbox.alias.sent,若此时生成save-copy = "sent",每次 IMAP 发送都会失败。因此该配置适合用户在了解自己账户后端行为后手动填写。 - Gmail 与 Microsoft Graph 不需要设置:这两个后端服务端会自动归档已发送邮件,配置了反而可能产生重复副本。SMTP 账户才是本配置的主要受益者。
对应规范已固化到 cairn/spec/config.md:配置 SHALL 为发送命令提供默认副本邮箱、--no-saveSHALL 单次跳过且与--save冲突、message表 SHALL 接受未知键;同时 cairn/spec/commands.md 中固化了"保存跟随发送"的顺序要求(发送失败不留副本,保存失败须报错且说明邮件已发送)。
变更落地与验证
该变更已进入 CHANGELOG.md(Unreleased → Added):
Added
message.send.save-copy, the mailbox a sent message is copied to when--saveis not passed, and--no-saveonmessage send,compose,replyandforwardto skip it. It takes a mailbox name, alias or role, and the v1truestill reads as thesentmailbox.
任务清单 cairn/changes/message-send-save-copy/tasks.md 显示全部子任务均已完成:MessageConfig/MessageSendConfig与save-copy(字符串或布尔)、合并进Account并由Account::resolve_save解析、四个命令上的--no-save、handler::apply先发送后保存、测试与配置样例与 CHANGELOG、以及最终归入cairn/spec/config.md与cairn/spec/commands.md。
验证方式(见日志)为针对 Maildir 账户 + 脚本化 SMTP 服务器的端到端测试,覆盖了:配置副本生效、--no-save跳过、--save覆盖配置、发送被拒绝时不留下副本、保存副本失败时报出"邮件已发出"、以及compose --send组合场景。单元测试则固化在 src/account/context.rs,保证解析逻辑后续不会被无意破坏。
小结
message.send.save-copy把"发送邮件自动留副本"从每次调用都要显式传参,收敛为账户级配置 + 单次覆盖参数的组合:字符串或布尔取值兼容 v1 习惯,--no-save提供单次退出通道,resolve_save统一了优先级,而handler::apply的"先发送、后保存"顺序彻底消除了失败发送遗留幽灵副本的问题。对于 SMTP 账户,这几乎是开箱即用的最佳实践;对于 Gmail/Graph 账户,保持该项未设置即可让服务端接管归档。
- CLI
【免费下载链接】himalaya
CLI to manage emails
相关推荐
深入解析 TanStack Table 核心的 memo():依赖追踪记忆化函数及其在表格内部的应用
深入解析 TanStack Table 核心的 memo :依赖追踪记忆化函数及其在表格内部的应用 本文聚焦 TanStack Table(本仓库 ta/tab
CLIFrappe 邮件副本功能解析:send_me_a_copy 给自己发送出站邮件副本的实现与演进
Frappe 邮件副本功能解析:send_me_a_copy 给自己发送出站邮件副本的实现与演进 导读 本文基于 Frappe 仓库 v5 变更日志 https
后端Web框架低代码前端认证鉴权5 分钟快速上手 hoist-non-react-statics:提升组件静态属性的完整教程
5 分钟快速上手 hoist non react statics:提升组件静态属性的完整教程 在 React 高阶组件开发中,你是否遇到过静态属性丢失的困扰?h
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考