news 2026/10/4 1:42:47

himalaya `message.send.save-copy` 配置详解:让已发送邮件自动归档到指定邮箱

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
himalaya `message.send.save-copy` 配置详解:让已发送邮件自动归档到指定邮箱
  • CLI

【免费下载链接】himalaya

CLI to manage emails

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

导读

本文围绕 himalaya 的message.send.save-copy配置项展开,讲解如何在发送命令(message send,以及带--send的message compose、message reply、message forward)未显式传入--save时,将已发送邮件的副本自动归档到指定邮箱,并深入剖析其底层实现(Account::resolve_save的解析优先级、handler::apply的“先发送后保存”顺序),帮助你按账号配置发送副本策略,避免 SMTP 发信后无处留存的问题。

背景:为什么需要“发送即保存副本”

himalaya v2 在重构时移除了 v1 中的message.send.save-copy配置。结果就是:通过 SMTP 发送的邮件,除非每次调用命令都显式传--save,否则发送成功后不会有任何副本留存。而不同的邮件服务商行为并不一致:

  • Gmail 和 Microsoft Graph:服务端会自动把已发送邮件归档进 Sent 文件夹,无需客户端保存副本;
  • SMTP(如自建服务器、Fastmail/Posteo 的 SMTP 通道):服务端不会保存副本,发送后本地无痕。

按账号来配置这个需求尤为关键:同一个 himalaya 实例可能同时管理 Gmail 与 SMTP 账号,前端(himalaya-emacs、himalaya-vim、himalaya-tui)无法预知每个账号走的是哪种后端。与其在每个前端各自添加保存选项,不如在核心配置层为每个账号指定“发件副本去哪”。

另外,旧行为还有一个隐患:--save是在发送之前追加副本的。如果发送失败,就会留下一个“从未真正发出”的邮件副本。新实现将保存顺序调整为“先发送、后保存”,彻底规避了这个问题。

配置项:message.send.save-copy

取值类型

message.send.save-copy支持两种取值形式,由 src/config.rs 中的SaveCopyConfig枚举通过#[serde(untagged)]反序列化解析:

写法含义示例
布尔值true表示“sent”角色邮箱(即 v1 的语义)message.send.save-copy = true
布尔值false不保存副本(v1 语义)message.send.save-copy = false
字符串邮箱名、别名(alias)或角色(role)message.send.save-copy = "Sent Items"、message.send.save-copy = "sent"

字符串解析与--save完全一致:可以是邮箱的实际名称(如"Sent Items")、配置在mailbox.alias中的别名,或者角色名(如sent、inbox、archive)。

全局与账号级覆盖

该配置可以写在全局[message.send]区域,也可以写在账号的[accounts.<name>]中,账号级覆盖全局。这一“全局 + 每账号覆盖”的合并逻辑由 Account::merge 实现:save_copy: other.save_copy.or(self.save_copy),即账号配置存在时优先采用账号值,否则回退到全局值。

参考 config.sample.toml 中的官方注释示例:

# 全局配置:所有账号默认将已发送副本存到 sent 邮箱 [message.send] save-copy = "sent" [accounts.example] email = "alice@example.org" # 账号级覆盖:该账号发送副本存入 "Sent Items" #message.send.save-copy = "Sent Items"

来自 config.sample.toml 与 config.sample.toml 的说明还特别提醒:Gmail 和 Microsoft Graph 账号应当保持该项不设置,因为这两个后端自己会归档已发送邮件,重复保存反而多此一举。

v1 兼容:message表开放未知键

himalaya v1 的[message]表包含read、write、delete、send.backend等大量键。为保证 v1 配置在 v2 中继续加载,MessageConfig 特意不再使用deny_unknown_fields:

#[serde(rename_all = "kebab-case")] pub struct MessageConfig { #[serde(default)] pub send: MessageSendConfig, }

也就是说,message表接受未知键,v1 的[message]表其余内容照常解析,不会因出现新版本不认识(或已移除)的键而整体报错。这一点由单元测试 save_copy_reads_a_v1_boolean_beside_v1_keys 直接验证:同时携带message.send.save-copy = true、message.send.backend = "smtp"、message.read.format = "plain"的 v1 风格配置可以完整反序列化,且save-copy = true被解析为sent角色。

命令行:--save与--no-save

新增--no-save

为了让用户在一次调用中跳过配置好的副本,四个发送命令都新增了--no-save标志:

  • message send
  • message compose(配合--send发送时)
  • message reply(配合--send发送时)
  • message forward(配合--send发送时)

以 send.rs 中的MessageSendCommand为例:

/// Append a copy of the sent message to this mailbox name, alias or /// role, overriding `message.send.save-copy`. #[arg(long, value_name = "MAILBOX")] pub save: Option<String>, /// Skip the copy `message.send.save-copy` configures. #[arg(long, conflicts_with = "save")] pub no_save: bool,

注意#[arg(long, conflicts_with = "save")]:--no-save与--save互相冲突,同时传入会直接报错退出,避免语义歧义。compose、reply、forward三个命令的 CLI 定义与此完全同构(见 compose.rs、reply.rs、forward.rs)。

优先级

实际生效的保存目标由 Account::resolve_save 统一裁决,优先级从高到低:

  1. --save <mailbox>显式指定:永远优先,覆盖一切配置;
  2. --no-save:本次发送跳过副本;
  3. message.send.save-copy配置:作为兜底;
  4. 两者皆无且未发送:不保存。

核心逻辑:

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-copy,如果只是执行不带--send的message compose(仅撰写不发送),resolve_save(..., send = false)也会返回None,不会把草稿写进副本邮箱。保存草稿仍需显式--save。这一点由测试 resolve_save_falls_back_on_the_copy_only_when_sending 锁定:

assert_eq!(account.resolve_save(None, false, true), Some("Sent Items")); // 发送时回退到配置 assert_eq!(account.resolve_save(None, false, false), None); // 不发送时忽略配置 assert_eq!(account.resolve_save(Some("Archive"), false, true), Some("Archive")); // --save 覆盖 assert_eq!(account.resolve_save(None, true, true), None); // --no-save 跳过

底层实现:先发送、后保存

handler::apply的执行顺序

无论是--save与发送命令组合,还是message add --send,保存与发送的统一执行入口都是 handler::apply。其顺序保证为:

  1. 先发送:client.send_message(sent, raw.clone())?,发送失败则整个命令以错误退出,不留下任何副本;
  2. 后保存:发送成功后再client.add_message(mailbox, flags, raw)追加副本;
  3. 保存失败:发送已成功、保存失败时,错误上下文明确告知“邮件已发送但副本保存失败”。

对应关键代码:

// NOTE: a deferred send is filed under the saved copy's mailbox, else // under the one the account names as sent. 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"))?, ), ... };

当发送失败时,send_message返回错误,后续的add_message根本不会执行——这正是“失败发送不产生副本”的保证来源。

相关细节

  • 副本附加\Seen标志:handler::route调用apply时传入&[Flag::from_iana(IanaFlag::Seen)],即保存的副本会被标记为已读(见 handler.rs)。
  • 副本邮箱经别名解析:apply中的let mailbox = save.map(|name| account.resolve_mailbox(name))会先经过mailbox.alias别名解析,再落到后端(见 handler.rs)。
  • 输出提示:发送并保存成功输出Message successfully saved and sent;仅保存输出Message successfully saved;仅发送输出相应提示(见 handler.rs)。
  • 发送回退邮箱:发送时若无副本邮箱,send_message会回退到mailbox.alias中名为sent的别名,保证 pimdir 等延迟发送(queued send)场景下发送记录能落到正确邮箱。

各命令用法速查

以下命令均适用于配置了save-copy的账号:

# 发送原始消息文件,并自动把副本存到 save-copy 配置的邮箱 himalaya message send --file ./message.eml # 发送并用 --save 临时覆盖配置,存到 Archive himalaya message send --file ./message.eml --save Archive # 本次发送不保存任何副本 himalaya message send --file ./message.eml --no-save # 撰写并发送,自动保存副本 himalaya message compose --send # 回复并发送,自动保存副本 himalaya message reply --send 42 # 转发并发送,自动保存副本 himalaya message forward --send 42

要点总结:

  • --no-save与--save冲突,不能同时使用;
  • --save优先级高于配置文件;
  • 副本只在“发送”动作发生时自动保存,纯撰写不会触发。

已知限制与后续规划

依据 proposal.md 的说明,本变更落地时明确了两个未覆盖的边界:

  1. 向导(wizard)不会写入该配置:himalaya wizard生成的配置不含save-copy,因为 IMAP 后端要等imap-special-use-aliases(LISTRETURN (SPECIAL-USE))能力落地后才能可靠解析sent角色;在此之前,向导生成一个save-copy = "sent"会在每次 IMAP 发送副本时失败。
  2. IMAP 的sent角色依赖:在 SPECIAL-USE 支持到位前,IMAP 账号需手动配置mailbox.alias.sent或用真实邮箱名作为save-copy值,副本保存才能指向正确位置。

验证记录与源码导读

该变更已随 himalaya 落地(cairn 状态status: landed,2026-10-01),并完成了完整的端到端验证(见 cairn/log/2026-10-01-message-send-save-copy.md):使用 Maildir 账号加脚本化 SMTP 服务器,覆盖了“配置副本生效”“--no-save跳过”“--save覆盖配置”“发送被拒不留副本”“副本保存失败时报告已发送”“compose --send组合”六类场景。

如需深入源码,建议按以下路径阅读:

  • 配置解析:src/config.rs(MessageConfig/MessageSendConfig/SaveCopyConfig)
  • 账号合并与解析:src/account/context.rs(Account::resolve_save)及同文件测试段 src/account/context.rs
  • 命令定义:src/shared/message/send.rs、compose.rs、reply.rs、forward.rs
  • 发送/保存统一入口:src/shared/message/handler.rs
  • 样例配置:config.sample.toml

通过message.send.save-copy,himalaya 把“发件副本去哪”的决定权收敛到了每个账号的配置文件里,配合--save/--no-save的临时覆盖,以及“先发送后保存”的顺序保证,为多后端、多账号场景下的发件留档提供了统一且可靠的方案。

  • CLI

【免费下载链接】himalaya

CLI to manage emails

项目地址:https://gitcode.com/gh_mirrors/hi/himalaya
点击查看免费下载
上一篇:还在为戴森球计划工厂布局抓狂?这份开源蓝图仓库让你3分钟变建造大师
下一篇:如何快速优化Pi-Hole体验:whitelist工具完整使用指南

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 1:39:35

PHP 核心机制解析:FastCGI 与 PHP-FPM

在深入探讨 FastCGI 与 PHP-FPM 之前&#xff0c;需要先梳理 PHP 的运行环境及其与 Web 服务器的交互原理, 本文参考了关于 mod_php、mod_fastcgi 与 php-fpm 的对比分析&#xff0c;以及 Nginx 实战配置等相关资料&#xff0c;旨在系统性地解析这些核心概念 1.Web 服务器与 PH…

作者头像 李华
网站建设 2026/10/4 1:39:01

AOCV签核实战:物理变异建模与三维时序修正技术

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:37:45

VC6.0迷宫小游戏开发实战:递归回溯算法与MFC界面实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:37:43

C#上位机TCP通讯库卡机器人:实时位置回传与运动控制实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华