📧 Outlook 里明明有这封邮件,为什么就是搜不到

从一个漏掉的工单号开始,记一次 PST 分卷、索引对账、邮件修复和插件落地的完整排查

Posted by Se7enMuting on · Updated on

11 月 3 日,我在 Outlook 里搜一个工单号。

网页版能搜到一串来往邮件,本地只能找到其中几封。最奇怪的是,漏掉的那封邮件没有丢:退出搜索,按日期翻到 10 月 30 日,它就在那里,标题里也明明白白写着那个工单号。

重新输入单号,还是没有。

这件事如果只是慢一点,其实很好处理,等就行了。真正麻烦的是:它很快给出一部分结果,然后告诉你“搜索完成”。如果我事先不知道那封邮件存在,根本不会意识到结果少了。

接下来的几天,我重建过索引,拆过文件夹,迁移过 PST,追过搜索进程,还给上千封邮件做过副本修复。期间几次以为已经结束,隔一会儿又被下一封搜不到的邮件拉回来。

最后留下来的日常方案倒很朴素:历史邮件分到独立的 PST,主力库保持在可接受的规模,再让本地搜索绕开 Windows Search。 后续又把邮件检查与修复整理成了插件和批量工具。

这篇把整个过程留下来。哪些实验确实有效,哪些解释后来站不住,都会一起写。否则只抄最后一条注册表命令,过半年再碰到类似问题,还是不知道该从哪里查。

本文讨论 Windows 上的经典版 Outlook 与本地 PST。在线 Exchange 邮箱、新版 Outlook、Mac 版的搜索路径和扩展方式并不相同。

公司、人员、工单号、业务主题及目录名称已替换;技术统计保留当时的记录口径。工具源码和独立 Skill 已整理到 outlook-mail-cleaner。

全文比较长。只想先试配置,可以跳到最后采用的搜索设置;复现诊断时看核验备忘。

先把最后的选择放在这里

如果你只是想知道我现在怎么用,可以先看这三件事:

项目 最后采用的方式
历史数据 2020及以前.pst、2021-2024.pst、2025-现在.pst 分开存放
日常搜索 设置 PreventIndexingOutlook=1,让经典 Outlook 使用内置搜索
邮件检查 按需使用批量工具;增量插件作为可单独启停的辅助功能

这里没有“所有人都该关闭索引”的意思。索引正常时,搜索速度和附件内容检索都有价值。我的取舍是:在这批长回复链、多附件、本地长期归档的邮件里,反复维护索引的成本已经超过收益;分卷后,直接搜索的等待又已经可以接受。

这次选择解决的是我的日常可用性,不是一张“任何条件都绝不漏搜”的保证书。 搜索范围选错、邮件没有完整下载、查询条件不匹配,换哪个引擎都可能找不到。

下面从那封邮件说起。

1. 邮件没有丢,少的是另一份记录

我的在线邮箱空间只有约 1 GB,需要定期清理。长期资料主要靠客户端规则复制到本地 PST,再按项目、人员或事项归档。这样的用法很常见:在线邮箱负责收发,本地文件负责把几年甚至十年的资料留住。

当时的主 PST 约 24.6 GB,仅收件箱就有 2.45 万封邮件。它放在普通本地目录里,目录名字中的 Box 只是“邮箱”的意思,并不是网盘同步目录。

最开始的现象也很有迷惑性:

  • 搜工单号,返回的经常是我自己发出去的邮件;
  • 某些手动复制到项目小文件夹的副本能搜到,收件箱原件却没有;
  • 换一个主题搜索,结果又似乎是全的;
  • 右键“查找此对话中的邮件”,也会缺几封;
  • “高级查找”并没有稳定地补齐结果。

很容易因此怀疑:是不是复制归档把搜索搞乱了?是不是某个视图只显示自己发的?是不是没勾选子文件夹?

这些都值得查,但不能只在搜索框里反复换姿势。

这次最有用的第一步,是另外建立一条不依赖搜索结果的核验路径:通过 Outlook 对象模型访问本地存储,按日期缩小范围,再枚举实际邮件,直接比较主题里的工单号。

它确实找到了那封漏搜邮件,也找到了同一工单的其他往来记录。后来一次核验能列出 14 封相关邮件,而界面中的搜索结果仍少于这个数量。

这才把问题分开:邮件实体在,搜索返回的集合不完整。

Outlook 的“能找到”,其实有几层含义

在这个案例里,至少要分清四件事:

核验层次 实际要回答的问题
本地存储 PST 里是否有这封邮件,能否直接打开?
索引记录 Windows Search 是否有它的邮件主记录?
查询结果 当前搜索范围和条件能否把它返回?
会话归组 “此对话中的邮件”能否把它和其他回复放在一起?

后面踩过的几个大坑,基本都来自把这四件事混在一起。例如,邮件搜到了,不代表会话已经归组;附件有索引,也不代表父邮件的标题能搜到。

对于本地索引这一路,可以先记住这个简化过程:

1
2
3
4
5
6
7
PST 中的邮件
    ↓ Outlook / MAPI 协议处理
需要抓取的项目 URL 与通知队列
    ↓ 属性、正文和受支持附件的提取
Windows Search 索引目录
    ↓ 当前范围、查询条件
Outlook 搜索结果

排队、抓取和最终写入索引,是不同阶段。微软的 Windows Search 索引流程说明也明确作了这样的区分。某一阶段结束,不能替另外几个阶段宣布成功。

这张图只描述本文的本地路径。连接中的企业邮箱还可能使用服务端搜索,不能把所有 Outlook 搜索一概理解成查询本机索引。

2. 重建了,也分了文件夹,为什么还是没有

第一轮处理当然是重建索引。

入口是“文件 → 选项 → 搜索 → 编制索引选项 → 高级 → 重建”。这一步确实让部分近期邮件重新出现在结果里,但 10 月 30 日那封仍然没有。

于是开始给收件箱减负。

先在原来的同一个 PST 内建立年份文件夹,把旧收件和旧发件分开移动。完成后的一次统计是:

位置 收件 已发送 合计
主目录保留的近期邮件 9,851 463 10,314
2021—2023 年归档 6,083 380 6,463
2020 年及以前归档 8,637 649 9,286

邮件列表明显好整理了,可搜索问题并没有就此消失。

当时有个判断说得太满:每个文件夹降到一万封以内,索引器就能顺利扫完,因此不会再漏。这是把一个合理的性能优化,直接说成了故障根治。

同一个 PST 里分文件夹,确实减少了单个文件夹的项目数;但文件还是原来的文件,存储提供程序、索引登记和邮件正文也还是原来的那一套。 它没有自动修好这些环节。

另外,“一万封邮件是 Outlook 的统一硬上限”也不准确。微软针对相关版本的性能说明,区分了邮件项目、日历项目和文件夹数量,并没有给出“超过一万封普通邮件就必然漏索引”的规则。把活跃文件夹控制到几千封,是这次为了管理和直接搜索作出的选择。

这轮尝试仍然有价值:它证明了只分文件夹不够,也把后面按年份迁移的数据范围整理好了。

真正拆成独立 PST

下一步不是拿工具把 24 GB 的文件从中间切开,而是新建 PST,再通过 Outlook 把相应邮件复制或移动进去。

先试最老的一段,建立 2020及以前.pst。确认后,再建立 2021-2024.pst,最后整理 2025-现在.pst。最初两个历史文件的体积都在 6.4~6.5 GB 左右;主力文件则保留近期邮件和需要继续使用的目录结构。

这里有两种完全不同的动作:

1
2
同库整理:一个 PST → 多个年份文件夹
独立分卷:一个旧 PST → 多个独立 PST 存储

后者让历史文件可以单独挂载、卸载、备份,也缩小了日常主要搜索的数据范围。但它仍然不保证每封邮件都会被索引——很快就有新问题出现。

微软关于大数据文件造成暂停的说明,支持通过移动或归档来减少活跃数据文件的负担。它提供的是性能背景,不是“24.6 GB 已损坏”或“拆开后索引一定完整”的证明。

3. 迁移邮件,比复制文件多了不少事情

原本以为迁移只是等进度条。真正做起来,最费时间的是卡住以后判断:它是在慢慢写,还是停在某一封消息上。

S/MIME 邮件卡住了批量操作

在迁移近期收件时,进度几次长时间不动。检查当时未完成的一批项目,记录中出现了:

1
2
IPM.Note.SMIME.MultipartSigned    带 S/MIME 数字签名的邮件
IPM.Outlook.Recall                撤回通知

其中一次尾批统计里,有 100 封签名邮件和 34 封撤回通知。另一次集中整理的公司公告则有约 1,400 封。这些是不同范围的计数,不能因为都是公告,就推导出所有公告都属于同一种消息类。

这件事提醒我:Outlook 文件夹中的项目不是清一色的普通 MailItem。签名、加密、会议和撤回通知,都可能有自己的处理路径。

当时沿着证书链、联网吊销检查以及安全提示等待的方向查过,也尝试过 UseCRLChasing 设置。这个方向有解释力:涉及证书的操作,确实可能受到网络环境与证书检查影响。但历史材料没有为每一次停顿保留下完整调用栈,不能据此宣布“所有卡顿都已经证明是 CRL 死锁”。

这里不把关闭证书检查列成通用修复步骤。 那是另一个有安全含义的设置,与“让本地资料更好搜”不是同一件事。

实际处理时,把特殊消息先分出来,避免在一段普通邮件复制循环里反复撞同一个卡点;部分公告改用 Outlook 自身的规则执行机制 Rule.Execute 批量归类,才把任务推进下去。

它在这次任务里有效,不代表规则引擎从此不会卡,也不代表签名邮件只要查看就一定不会触发校验。

漏掉的已发送邮件,来自日期字段

迁移收件之外,还要核对已发送。

中途发现,有一段近期发件没有进入新库。追查后,最初筛选依赖的 PR_CLIENT_SUBMIT_TIME 属性,在部分由客户端规则生成的本地副本上并不齐全,导致 71 封邮件没有被初始条件选中。

后来改用实际可读取的发送时间逐项对比,才补齐这一批。

“有发送日期”与“某个预期的底层属性在每封副本上都存在”,并不是一回事。迁移脚本不能只看执行成功,还要拿源端和目标端对账。

文件夹搬过去了,规则和分类不一定跟着过去

迁移完成,还得检查这些东西:

对象 这次遇到的情况
收发件规则 仍指向旧 PST,必须重新绑定目标文件夹
项目子文件夹 需要确认内容与层级都在,不能只检查收件箱
颜色分类 邮件上的分类文字与分类名称/颜色清单没有一起完整迁移
收藏夹、聚合视图 原入口可能仍引用旧目录
存储名称 磁盘文件名、数据文件显示名、根文件夹名称不是同一个属性

颜色分类尤其容易漏:邮件上保留了类别,并不等于新存储已经拥有原来的完整类别清单。当时还检查了隐藏的 IPM.Configuration.CategoryList 配置消息,才让名称和颜色显示恢复一致。

规则也出现过另一个问题:只按主题包含“任务发起通知”等词归类,会把正常业务回复一起选进去。后来补上系统发件人的条件,再把误归类的业务邮件移回。

移动和复制的善后还不同:误移动要恢复原位置;原件仍在、只是多复制了一份时,才是处理多出来的副本。

其中一次界面批量操作还因焦点变化,把 32 封近期业务邮件误移到了已删除邮件。最后通过在线邮箱与本地逐项对比恢复。这个小事故让我彻底放弃了“批量操作完成、数量大致对上就算完”的想法。

删除以后,PST 为什么还那么大

旧文件里移走了大量历史邮件,磁盘上的体积却几乎没变。这不等于移动失败。

PST 删除项目后会留下可重用空间,文件不一定立即缩小;压缩数据文件与删除邮件是两个动作。微软的数据文件瘦身说明也解释了后台压缩与手动压缩。

因此,“还有 24 GB”不能拿来判断邮件是否仍在旧库;同样,文件变小了也不能替代内容对账。

4. 改完名字,新库竟然一封都搜不到

迁移过程中改了几次名字,最后统一成 2025-现在。

随后遇到一个比漏几封更吓人的现象:新库里的邮件可以打开,规则也在往里面放副本,但搜什么都没有。

Windows 的索引总数明明有几万项,看起来还很正常。

直接查 SystemIndex 才发现:那些数字主要属于另外两个历史存储和其他索引内容,主力新库在当时的目标范围内是 0 项。

全机总数很大,对当前 PST 是否正常,没有多少说服力。

一个大小写差异,把存储身份问题引了出来

排查对比了磁盘路径、Outlook Profile、存储标识与索引 URL。路径中出现了一组看起来不起眼的区别。用脱敏后的例子表示:

1
2
磁盘与一部分登记:D:\Mailbox\2025-现在.pst
另一部分配置记录:D:\MailBox\2025-现在.pst

在通常的 Windows 文件访问中,这两种写法往往指向同一个文件。但在当时的配置里,与新库有关的二进制存储标识和 mapi16:// 登记前缀并没有保持一致。

相关记录还包括:

1
2
Event 38: Added: 8843, Results: 0
Event 36: error=0x80940d23

处理后,对应库的索引量开始从 0 → 54 → 302 → 390 多项增加。

这是一个很有价值的现场线索。不过现在复盘,要把话说准确:

这次统一路径、修正配置与存储登记,确实伴随着索引恢复;但同一轮还进行了重启、清理旧搜索状态和规则重存,不能把结果简化成“Windows Search 所有路径都区分大小写”。

更不能只凭两组不同的后缀,就宣布已经反推出微软 StoreID 的完整哈希算法。StoreID 本来就是存储提供程序使用的不透明标识。

可重复使用的经验是:改名或重新挂载以后,同时核对文件路径、存储身份、根目录身份、规则目标与索引前缀。 能打开文件,只能证明文件访问通了;不能证明索引链路里所有对象都认得它。

这些 Profile 和二进制属性不适合照着文章直接改。我把它记在这里,是为了下次知道该往哪里查。常规迁移仍应优先使用 Outlook 支持的数据文件管理方式。

5. 索引只加了四项,是在偷懒,还是已经漏掉了

新库开始入库后,又进入了漫长的等待。

某次观察中,界面上的索引总数几小时只从 52,359 增加到 52,363。直觉当然是:又卡了。

可另一组记录里,通知队列从 20,573 降到 11,874。后台并没有只干四件事。

原因是这两个计数不在回答同一个问题:

  • 已索引总数,近似反映目录里当前已有多少项目;
  • 待处理队列,反映还有多少通知或抓取工作没有消化;
  • 已有项目被重新检查或更新,不一定让总数增加;
  • 邮件、附件、文件夹和其他文件,也不能直接混算成“邮件封数”。

所以,总数增长慢,不足以证明进程停住。反过来,队列消化得快,也不足以证明每封邮件都成功入库。

真正有转折意义的是:后来队列降到了 0,目录状态变为 IDLE,那封目标邮件却仍然搜不到。

这时再说“它还在排队,继续等”,就没有多少依据了。

1,622 封究竟是怎么数出来的

当时一轮对账采用了这组数字:

1
2
3
本地收件箱邮件:4,946
索引中对应邮件:3,324
差值:          1,622

1,622 首先是一组集合对比的差值,不是一份微软逐封出具的 1,622 行失败报告。

这个区别,是后来追问“你真的看到每封的失败记录了吗”才澄清的。

历史中还出现过 4,947 这个数,之后实际提取出来的待处理清单则是 1,626 封。这些数字来自不同轮次;期间有新邮件、测试副本和筛选口径变化,记录没有保留足够一致的快照,可以把那四封的来源完整还原。因此本文也不强行把 4946 - 3324 写成 1,626。

尤其不能靠“根文件夹算一项”去给 Items.Count 凑数。文件夹内的项目数、目录中的文件夹记录和普通邮件计数,需要各自确认。

如果重做这一步,我会先固定四个条件:同一个存储、同一个文件夹范围、同一种消息类型、同一轮快照。 然后输出逐封缺失清单,再分析原因,而不是先拿两个总数宣判。

日志和二进制分析,给了什么证据

那段时间的事件日志反复出现 Event 10024,内容是过滤宿主无响应后被强制终止。

1
2
The filter host process (SearchFilterHost.exe) did not respond
and is being forcibly terminated.

这支持“提取链路存在异常或超时”这个判断,但不能把同一时间窗口里的每封缺失邮件,都自动归因到同一次宿主终止。

为了继续往下找,还分析过 SearchProtocolHost.exe 等模块的字符串和代码引用。记录中确实找到了这些线索:

1
2
3
4
searchprotocolhost\fltrdaemon.cxx
[SrchFilterDaemon] Cannot add incomplete embedding
SemanticSearchHardwareRequirementsSlim.h
CopilotPlusPCDetection.h

查到这里很容易兴奋:好像终于能把所有问题都归到某次系统版本引入的 AI 搜索上了。

但二进制里有 embedding 代码,和某封邮件在本机一定走了这条路径,是两回事。fltrdaemon 的名字也不能直接当作“AI 生成器”的定义。缺少与样本对应的完整栈、转储和更严格的复现,就还不能宣布“已确认某个 Windows 版本的已知 AI 缺陷”。

同样,0x80070057 是通用的参数错误;一次高优先级通知被消化,也不等于这个通知之后的内容处理必然成功。那次副本实验中,优先级推送本身还记录过异常。

所以我保留这段分析,但把它放回应有的位置:它帮助缩小过方向,没有完成全部根因证明。 后面更有用的,是直接改一个样本,再看结果是否改变。

6. 不再盯进度条,拿一封副本做对照

这轮实验的思路很直接:

  1. 找到一封确定存在、确定搜不到的邮件;
  2. 原件保持不动,建立几个副本;
  3. 每个副本改变不同因素;
  4. 再查索引记录与 Outlook 结果。

选的就是最初那封 10 月 30 日的邮件。下面是当时的实验记录:

副本 处理方式 当次观察
TEST1_RAW 保留原 HTML,原样复制 没有进入目标邮件索引结果
TEST3_NO_ATT 保留 HTML,移除附件 仍未进入
TEST2_PLAIN 改成纯文本正文 约 3.6 秒后查到邮件记录
TEST5_CLEAN_HTML 改写 HTML 结构 约 4 秒后查到邮件记录

这比“可能是文件太大”前进了一步:同一批实验里,改变正文表示以后,结果确实改变了。 附件不是这个样本的唯一阻碍。

不过也要记住,改变正文并保存,会同时带来内容变化和变更通知。这种对照能支持正文处理方向,尚不足以把某个标签单独定为唯一原因。

从怀疑嵌套标签,到缩小清洗范围

最早注意到的是,多轮回复混合了 Word 生成的 HTML、第三方客户端签名和很深的 div 嵌套。当时还提到过连续约 17 层的结构。

但“17 层”不是一个 HTML 非法阈值,嵌套深也不自动等于解析器必崩。真正有操作价值的线索,后来集中到两类 Word 历史结构:

1
2
3
4
5
6
7
<style>
v\:* { behavior:url(#default#VML); }
</style>

<!--[if gte mso 9]><xml>
  <o:shapedefaults v:ext="edit" spidmax="1026" />
</xml><![endif]-->

它们是后续兼容处理的候选特征。这里有意不用“凡是出现就有毒”这种说法:正常 Word 邮件里也可能带这些结构,而当时有不少带旧标记的邮件本来就能被索引。

能搜到了,为什么看起来不对

最早的纯文本副本当然丢了格式。另一版虽然还是 HTML,却重建了正文,表格、字体和图片表现也有变化。

随后尝试保留 <body>、去掉整个 <head>,看起来接近了,但仍被发现:行间距变大了,空行变多了。 某些测试副本还出现了“在 Web 浏览器中查看”的提示。

这回问题很具体。Word 邮件常见的段落依赖这类样式:

1
2
3
4
p.MsoNormal {
    margin: 0cm;
    margin-bottom: .0001pt;
}

如果把头部样式整个扔掉,正文即使一字没少,段落的显示方式也可能变。把邮件包装成一张“标准网页”,同样不等于保持了原来的 Outlook 显示。

最后才收敛到更小的改动:保留原来的文档结构、样式、正文和内嵌附件引用,只针对选中的 XML 条件块和 VML 行为做处理,不再整段重建 HTML。

这一次,“能搜到”和“看起来像原件”,才在选定样本上同时过关。

它也留下一个很实在的限制:微软明确说明,Outlook 使用 Word 处理邮件 HTML,通过 HTMLBody 编辑可能发生格式变化。所以不能仅凭清洗前后可见文字相同,就写下“无损”二字。

从 20 封样本扩大到 1,626 封

后面的处理没有直接覆盖全部原件,而是先把待处理项目复制到“待修复邮件”文件夹,原样保留,再从中挑 20 封不同日期、不同来源、不同附件情况的样本,把处理后的副本放入“已修复邮件”。

样本覆盖了长回复链、表格、内嵌图片、系统通知和多附件邮件。中途发现行距差异后,重新处理了这 20 封,再继续剩下的 1,606 封。

最终的副本集合是 1,626 封。这里说的是工作清单的数量,并不表示每一封都改了 HTML;其中没有命中特征的项目,存在原样复制通过的情况。

替换回主收件箱以前,又先核对每个原件在待修复目录中有备份,再找到对应的处理后副本。早期的 TEST2_PLAIN、TEST5_CLEAN_HTML 等测试件另行清理,避免把测试成功误当作正式原件已修好。

随后还检查了其他范围:

  • 2025-现在 的备份归档有 437 封当时未匹配的项目,其中 169 封命中特定 HTML 结构;169 是 437 的子集,不能再相加。
  • 这批归档中的 430 封能在主收件箱找到对应副本,优先利用已经处理过的副本核对替换,少量孤立或特殊消息单独处理。
  • 2021-2024 当次列出 311 封待处理项目;2020及以前 当次列出 56 封。

这些数字的意义是记住当时处理了哪些范围,不能把几个批次简单相加,作为最终“坏邮件总数”。重复归档、特殊消息和不同时间的清单,会让这种相加失去意义。

做到这里,已经花了不少力气。我以为最难的一段终于过去了。

7. “100% 完成”之后,又一封普通邮件搜不到

新的问题来得很快。

一封 11 月 6 日收到的技术回复,列表里就在眼前,按它的主题搜,却没有。这次无法再用“历史巨库太大”解释了:主收件箱已经只剩五千封左右,也处理过那批 HTML 候选。

重新对账,出现了这组数据:

同一轮核验 数量
收件箱实际项目 4,949
当时匹配到的邮件主记录 4,148
覆盖率 83.81%

前一次“100%”错在哪里?

一部分原因是看了索引状态和大范围数量,另一部分是路径查询把子文件夹、附件或其他项目带了进来。分子与分母根本不是同一组东西。数字看起来够大,并不能证明每个邮件对象都在里面。

这一次改成了更直接的做法:尽可能按单封定位信息,把 Outlook 项目与索引中的邮件主记录对应起来,而不是只比较路径下的总行数。

对那批没有明显 HTML 特征、但仍未匹配的邮件,尝试调用 mail.Save(),让 Outlook 发出项目变更通知,再观察索引是否补录。记录中处理了 799 封,脚本执行约 38.16 秒,调用层面没有报告失败。

但结果并没有变成 100%。

核验指标 触碰前 触碰后
收件箱项目数 4,949 4,949
匹配到的邮件主记录 4,148 4,843
覆盖率 83.81% 97.86%
触碰后仍未匹配 — 106

799 次保存成功,与邮件主记录增加 695 条,是两件不同的事。 通知被接受,不代表正文和属性随后一定成功进入可查询的索引。

而且 Save() 也不该被称作完全不改动数据:即使不主动修改可见正文,它仍可能更新修改状态或时间。对只读取证、签名邮件或其他敏感数据,不能随意批量“摸一下”。

剩下的 106 项,不是一种问题

那轮清单把 106 项分成:

1
2
22 项会议相关消息
84 封普通邮件

会议消息与普通邮件在对象类型、索引类别和筛选方式上可能不同,需要独立核对。不能因为它不在某个 email 查询集合里,就宣布它被系统丢弃;也不能反过来说会议类项目天生都不需要验证。

更值得关注的是剩余 84 封普通邮件。当时进一步统计发现:

  • 74 封有附件;
  • 39 封能找到相关附件的索引项;
  • 平均大小约 2.81 MB,部分超过 10 MB;
  • 有长回复链、多张内嵌图和较大的压缩包或技术附件。

其中一个用于验证的固件类邮件约 13.3 MB,主题搜索仍不能可靠找到邮件本体。

索引 URL 里有 /at=... 的附件项,是排查中的一条重要线索:附件进入了目录,并不保证其父邮件已经作为同样可查询的邮件主记录出现。 搜附件名能否在 Outlook 中带出原邮件,也仍需实际验证。

当时用“大邮件触发提取限额或超时”解释过这批项目。这是合理方向,但手头的材料不足以给每封都锁定相同原因,更不足以写出一个适用于所有 Outlook 的“固定 30 秒、固定大小上限”。

后面队列空了,这批普通邮件仍没有全部补齐。继续等待,并没有给我想要的确定性。

8. 搜得到,和“此对话中的邮件”能找到,不是同一回事

那封 11 月 6 日的邮件恢复搜索以后,我又右键点了“查找此对话中的邮件”。

结果只出现它自己。

从其他相关邮件发起同样的查找,能看到其他回复,唯独没有这封。这已经不是“索引里有没有它”的问题,而是会话归组问题。

检查主题和相关 MAPI 属性,发现多种中英文回复前缀叠在一起,例如:

1
2
Re: 回复: 参数导入与版本确认
回复:回复: Re: 参数导入与版本确认

涉及的属性包括 ConversationTopic、PR_NORMALIZED_SUBJECT 和 PR_CONVERSATION_INDEX 等。它们用于不同目的,不能只看界面标题,把所有属性都当成同一个“去掉 RE 的主题”。

那轮扫描曾标出 648 封有前缀残留等特征的候选,约占当时收件箱的 13.1%。这只是候选数量,不能直接宣称 648 封都已经证实会话断裂,更不能说所有归组异常只有这一种原因。

曾经对一封测试邮件调整过标题和会话相关属性,确实改变了查找结果。但这相当于修改邮件内容与元数据,已经超过 HTML 兼容处理的范围。最后把它还原,没有继续批量规范标题。

这件事很容易被一个“修复成功”掩盖:如果只检查主搜索框,根本看不到另一个问题还在。

同一阶段还查过几对外观相同的重复邮件,比较了 Message-ID、传输头和时间。有的更像不同投递,有的更像本地多了一份副本。但相同 Message-ID 本身不足以证明是哪条规则或哪次重连导致重复,仍需服务器或规则执行证据。那轮限定日期的检查没有再找到同类剩余项,也只是那一轮的结果。

9. 最后没有再重建一次,而是换了取舍

到了这里,问题已经不是能不能把覆盖率再抬高一点。

我日常最怕的是:查一个旧问题,只看到一半往来,却不知道漏了哪一封。97.86% 这个数字看上去不错,但漏掉的那几封里,可能恰好就有当年确认参数或交付附件的邮件。

为了补这部分结果,已经投入了远超过日常检索本身的时间。

所以最后保留分卷后的结构,把本地 Outlook 切换到内置搜索。

我使用的注册表策略

先记录原有值。以下路径是系统级策略位置,需要管理员权限;如果由单位策略管理,应遵循现有管理方式。

reg query "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v PreventIndexingOutlook

启用这次采用的设置:

reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v PreventIndexingOutlook /t REG_DWORD /d 1 /f

然后关闭并重新打开 Outlook。

微软在一个历史搜索故障的临时绕过说明中,明确给出该设置,使 Outlook 使用内置搜索。该文档对应的问题已经标为修复;这里引用它,是说明策略的用途,不是拿它证明本次遇到同一个系统缺陷。

切换后可能出现“组策略关闭了 Windows Search,因此搜索性能会受影响”一类提示。这正是使用后备搜索路径时可能看到的信息。

恢复时,应恢复你修改前的状态。如果原来没有这个值,可以只删除这一项:

reg delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v PreventIndexingOutlook /f

如果原来明确是 0,就恢复为 0,不要把已有管理配置当作临时新建项处理。恢复后同样重启 Outlook。

这里没有停掉整台电脑的 Windows Search 服务,也没有保证重新启用后一定不用补索引。索引内容和状态是否还在,要另行确认。

为什么前面要先分卷

假如仍让 Outlook 在一个长期累积的大库里扫描全部历史邮件,内置搜索的等待可能明显增加。先把活跃数据缩小,再明确搜索当前文件夹或主力库,才让这条路适合日常使用。

我的实际结论是“等待可以接受”,不把早期估计的“一到三秒”写成统一性能指标。硬件、PST 体积、查询字段、结果量和跨库范围,都会影响时间。

更不能承诺:关闭索引后,压缩包内部、所有附件正文和所有复杂查询还能保持原来的搜索能力。索引和非索引路径支持的内容与语法需要分别验证。

“高级查找”没有替我完成这个切换

前面曾反复得到一个建议:按 Ctrl+Shift+F,用高级查找就能绕开 Windows Search。

可这次实测中,索引开启时,高级查找同样没有可靠返回那封已知存在的邮件。仅换窗口并没有成为可用的兜底方案。

因此我最后用了明确的策略开关,再拿已知漏搜的样本验证。这里也不推导“所有版本、所有条件的高级查找都只走某个引擎”;窗口叫高级,不是它一定使用另一条检索路径的证据。

10. 搜索先稳定下来,工具继续留下

事情原本可以在这里结束。但还有一个现实需求:既然已经找到一类可以用固定代码识别、并在样本上改善过兼容性的结构,之后每来一封邮件,总不能再开一次人工排查。

于是把工具拆成了两个入口。

A:只处理本地新副本的 COM 加载项

初版设计考虑过 NewMailEx。后来把边界收紧为:只监听本地 PST 文件夹的 Items.ItemAdd,等规则已经把副本放到本地,再检查这个本地对象。

原因不是接口名字不同,而是处理时机与对象位置不同。微软的 NewMailEx 说明指出,它发生在客户端规则处理之前。直接在这里处理主邮箱新邮件,容易触及本来不想修改的云端原件。

最后的思路是:

1
2
3
4
5
6
7
企业邮箱收到邮件
    ↓ Outlook 规则按原有方式复制
允许处理的本地 PST 文件夹加入副本
    ↓ ItemAdd
确认本地存储、普通邮件、正文已下载
    ↓
检查候选结构 → 先备份 → 局部处理 → 保存 → 记录

不在这里重扫多年历史,不处理签名、加密、会议或撤回消息,也不调整标题和会话属性。

还要考虑内网或 VPN 断开时的情况。处理本地副本、跳过仅标头项目、捕获 COM 错误,可以减少额外访问和异常叠加;但不能因此保证 Outlook 自身永远不弹凭据提示,也不能保证磁盘与 COM 调用永不阻塞。

B:独立进程里的按需批量工具

B 负责存量,按 PST、文件夹和时间范围只读扫描;需要修改时再进入修复模式,也负责查看记录和恢复 HTML。

“独立进程”方便控制任务,不代表它不依赖 Outlook。它仍通过 COM 访问客户端,读取和保存仍可能让 Outlook 等待。

这次没有把它设成常驻任务。平时有需要再运行,避免把一个排障工具变成另一个必须长期维护的后台服务。

插件勾上又掉,最后查到了接口和编码问题

落地过程也没有一次通过。

先是加载项名称出现乱码。调查到脚本编码与 PowerShell 入口的差异:机器上有 PowerShell 7,并不意味着运行 powershell 就会调用它;通常要使用 pwsh。Windows PowerShell 5.1 对无 BOM 的 UTF-8 脚本读取,也可能与预期不同。

接着是 COM 加载项勾选后再打开仍未勾选。仅看注册表已经有项,不能证明插件加载成功。那轮检查最终纠正了接口声明,改用正式的 Extensibility 接口库,并检查依赖程序集与加载情况。

用于核对的 IDTExtensibility2 接口 GUID 是:

1
B65AD801-ABAF-11D0-BB8B-00A0C90F2744

它有微软接口定义可对照。加载项自身的 CLSID 是另外一个东西,不能混用。

最后,状态查看也要分三层:注册有没有、启动加载如何配置、运行中 COMAddIn.Connect 是否真的连接。Outlook 进程存在,不能替插件宣布“正在保护”。

11. 为什么“修过了”,隔天又报出 2,721 封

工具刚落地,又碰到一个让人很不放心的问题:换个范围检查,结果居然又有 2,721 封“缺陷邮件”。

第一反应当然是:昨天不是修过了吗,难道又坏回去了?

追过代码和范围以后发现,这个数字首先反映的是检测口径变化。

早先的主要筛选是:先拿邮件与索引比,找未匹配项,再在这一小批里检查结构。后来的批量工具不再依赖索引状态,而是遍历更多本地文件夹,直接查看 HTML。

这会把“原来已入库、因此没有进入那轮待处理清单”的邮件也找出来。

第二个变化更大:判断从具体结构,放宽成了子串。

1
2
3
4
5
bool hasVml = html.IndexOf("behavior:url(#default#VML)",
    StringComparison.OrdinalIgnoreCase) >= 0;

bool hasMso = html.IndexOf("mso 9",
    StringComparison.OrdinalIgnoreCase) >= 0;

mso 9 是常见的 Office 历史标记,不是一份错误报告。正文里讨论这个字符串,或者正常模板中出现相关条件块,都可能命中。

因此,新旧两轮的“缺陷数”,不能放在一条曲线上理解成故障复发。

还有一些更小、但同样关键的范围差异:

当时的检查口径 检查项目数 旧规则命中数
主力库收件箱,近期一个窗口 59 9
主力库收件箱,当月 121 29
主力库全部当月文件夹 215 58
更大范围,包含在线邮箱 不与上面直接比较 145,其中本地 58、在线 87

这几组结果并不矛盾:前两组时间不同,中间两组文件夹范围不同,最后一组连存储范围都变了。

这也促成一个明确要求:默认只检查通过规则复制到本地的 PST 副本,不查也不改企业在线邮箱。 这条要求既要写在 Skill 里,也要落到代码的存储类型检查上,不能只靠默认参数刚好为空。

顺便纠正一个容易出现的复盘说法:后续归档文件夹并不是“以前完全没检查过”。前面处理过 437 封归档项目。真实情况是几个阶段的覆盖范围有交集,也有遗漏,不能再用一句“其他目录从未处理”概括。

58 封的标签统计,与像素验证的边界

对当月那 58 封备份,历史中进一步做过分类:

1
2
3
45 封:同时匹配 VML 与 Word XML
12 封:匹配 Word XML
 1 封:只在宽松 mso 9 匹配中出现,另检查到转义结构

还统计过被删除的条件块,并对两份 HTML 做了 Edge 无头截图。日志里,两组清洗前后的 PNG 哈希分别相同。

这些记录可以说明:所选输入、那两次 Edge 渲染和截图范围内,没有观察到图像差异。

它证明不了“58 封全部在经典 Outlook 中像素一致”,更证明不了以后所有邮件都没有风险。经典 Outlook 使用 Word 的 HTML 处理路径,不能把 Edge 截图当成同一个渲染器;某些 MSO 或 VML 条件结构本来就可能影响 Outlook 的显示。

回头看,“非致死的标记清掉也绝对没风险、只会让数据更健康”这句话,应该删掉。

如果邮件本来能正常显示和检索,只因为带一个旧标签就改写它,收益未必存在,修改本身却是真实发生的。

12. 公开分享时,我没有直接上传那份原始 Skill

整理源码时,顺手把这一步也做了:将检查与修复工具发布为独立项目。

项目地址:se7enmuting/outlook-mail-cleaner。

里面包含方案 A、方案 B、构建脚本、合成测试和独立的 SKILL.md。Skill 的规范目录名为 00-personal-13-outlook-mail-cleaner;另一台电脑只要具备所需运行环境,就可以按项目内说明使用,不依赖这次对话、原用户名或原项目路径。

这次公开整理还改了几处实际代码问题:

原实现中发现的问题 公开整理版的处理
传入 --store 后,可能绕过默认 PST 过滤 所有入口先做本地 PST 检查,精确匹配目标名称
备份失败仍可能继续回写 原文、manifest 和预写审计成功后才修改
同月重复处理同一邮件可能覆盖旧备份 每次操作独立命名,旧版本保留
任意 mso 9 子串被称为缺陷 单独统计旧标记,只针对受限结构生成可修改候选
删除整段 MSO 或 !mso 条件内容 保留非 XML 条件内容与图片回退,只做较小范围处理
回滚只凭 EntryID 或日志子串定位 同时记录 StoreID、EntryID、原文与保存后哈希
无法查询插件连接,也报告已运行 注册配置、实时连接和目录挂载分别报告

公开版仍然是实验性的兼容处理工具。它不查询 SystemIndex,因此不会因为发现某个 HTML 结构,就声称已经找到了索引失败邮件。

默认只读扫描,插件注册后也默认停用。启用增量修改还需要明确配置允许处理的 PST 路径。签名、加密等特殊消息不进入修复;没有云端修复参数。

微软还明确指出,大量项目同时加入时 ItemAdd 可能不触发。所以事件插件不能代替批量补查。新建目录、停用期间或大量导入后的邮件,也需要重新核对。

截至本次公开整理,已完成 27 项离线行为断言、完整 x64 源码编译,以及将 Skill 移到独立目录后的验证。这不是一次新的 Outlook 真机验收:COM 加载、事件触发、实际邮件外观和索引结果,仍需在目标环境的测试 PST 中分别验证。

仓库里没有实际邮件、截图、历史对话、原始备份、私人运行日志,也不重新分发微软 Office 程序集。运行依赖在目标电脑解析,私人状态另存用户数据目录。HTML 恢复功能只恢复正文,不能替代完整 PST 或原始 MSG 备份。

附:下次再遇到,我会先做的几项核验

这一段是给以后自己留的。正文中的路已经走过一遍,下次不必从重建索引开始重跑。

A. 先留一封能直接打开、但搜索遗漏的样本

记录它的存储、实际文件夹、日期、主题关键词、消息类,以及当时的 StoreID 和 EntryID。先在文件夹里打开确认,再用实际使用的查询条件搜索。

跨 PST 复制或移动后,EntryID 可能变化。需要追踪对应关系,不能拿迁移前的 ID 硬套到迁移后。Message-ID 可以帮助关联副本,但它也不是在本地所有位置中唯一的一行主键。

B. 查索引前,先识别当前存储前缀

诊断 Windows Search 时,可以通过 OLE DB 的 Search.CollatorDSO 提供程序只读查询 SystemIndex。例如,先观察一小批 MAPI 路径:

1
2
3
4
5
6
SELECT TOP 30
    System.ItemUrl,
    System.ItemName,
    System.Kind
FROM SystemIndex
WHERE System.ItemUrl LIKE 'mapi16://%'

这只是识别本机实际 URL 结构的起点,不是一条“统计全部 Outlook 邮件”的查询。不要照抄别人的 SID、存储后缀或汉字路径作为自己的范围。

随后固定目标 PST 和目录,分别核对邮件主记录、附件项、文件夹项及特殊消息。需要查全时,输出逐封未匹配清单,并以已知样本验证对应关系。

不直接打开或写入 Windows 搜索数据库文件,也不手工往目录中伪造一条“已索引”记录。

C. 状态、数量和样本,要同时看

指标 能说明什么 不能替你证明什么
“索引完成”或队列为零 当前没有相同意义下的待处理工作 所有邮件均成功收录
某 PST 的索引总行数 目标范围有一批索引记录 行数等于普通邮件封数
一封邮件调用 Save 成功 对象保存调用完成 之后索引一定成功
某 HTML 特征命中 输入满足检测规则 邮件已损坏或必然崩溃
浏览器截图一致 该渲染环境下未观察到差异 经典 Outlook 与全部场景都一致
COM 加载项已注册 注册信息存在 插件已连接并监听正确目录

D. 做修改时,别让恢复依赖记忆

至少保留修改前 HTML、完整邮件或 PST 备份,以及对应关系和修改日志。批量任务先小样本,再扩大范围;发生不确定的保存错误,留下现场,不盲目重试。

单独记录“待处理数量”“实际改写数量”“仅触发通知数量”“成功保存数量”“实际搜索验收数量”。这几个数可以不同,不必为了让报告漂亮把它们写成一个数。


这次排查最后没有得到一个能解释一切的单一根因。

迁移有迁移的卡点,存储登记有登记的问题,正文处理会影响索引,也可能影响排版;索引恢复以后,会话归组还可能是另一回事。把它们全写成“PST 太大”或“某个系统版本有 bug”,都省事,但下次帮不上忙。

我现在至少知道,搜索框说“没有结果”时,先不要急着相信它。找一封确定存在的邮件,拿它把存储、索引和查询一层层对上。那封最开始漏掉的邮件,比后来任何一条“已经 100% 完成”的提示,都更适合作为验收标准。