☰
himalaya 邮件发送副本保存:`message.send.save-copy` 配置解析与“先发送后保存“语义
2026/10/4 1:52:20 网站建设 项目流程
  • CLI

【免费下载链接】himalaya

CLI to manage emails

项目地址:https://gitcode.com/gh_mirrors/hi/himalaya
点击查看免费下载

本文围绕 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 = false

MessageConfig特意未启用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, };

关键点:

  1. 先调用client.send_message真正把邮件投递出去,再调用client.add_message追加副本;
  2. 若发送失败,send_message返回错误并中断,不会留下副本;
  3. 若发送成功但保存副本失败,命令同样以错误退出,但错误信息明确说明"邮件已发出",即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):

Addedmessage.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

项目地址:https://gitcode.com/gh_mirrors/hi/himalaya
点击查看免费下载

相关推荐

上一篇:Android权限请求状态保存:PermissionsDispatcher与SavedStateHandle
下一篇:大模型微调全攻略:LLMs千面郎君中的SFT与PEFT技术详解

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询