Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2.7 Popclient 蜕变为 Fetchmail

这个项目真正的转折点,是哈里·霍赫海瑟(Harry Hochheiser)给我发了一段把邮件转发到客户端主机的 SMTP 端口的 demo 代码。我瞬间就反应过来:只要把这个功能稳定实现好,现有的其他所有邮件投递方式基本都可以淘汰了。

在过去的好几周,我一直在小幅迭代优化 fetchmail,虽然它的交互界面勉强能用,但设计却很粗糙:设计一点都不优雅,大量可有可无的零散参数到处堆砌。其中两项配置最让我心里别扭——一是将拉取到的邮件存入本地邮箱文件,二是直接输出到标准输出流,但我却说不上来问题到底出在哪。

(如果你对互联网邮件的技术细节不感兴趣,可以放心跳过下面两段。)

仔细琢磨 SMTP 转发这套方案后我才发现,popclient 之前承担的职责实在太多了。它最初的设计一身二用,同时充当邮件传输代理(MTA)与本地投递代理(MDA)。有了 SMTP 转发功能,它就能彻底和 MDA 相关逻辑解耦,只做纯粹的 MTA,像 sendmail 那样把本地投递工作转交其他程序处理。

几乎所有支持 TCP/IP 的系统都会默认开放 25 端口,既然如此,何必费心折腾复杂的 MDA 配置,还要给邮箱文件做锁机制、处理追加写入逻辑?更重要的是,采用这种转发方式后,拉取到的邮件格式会和普通发件方主动推送的 SMTP 邮件完全一致,这也正是我们想要的效果。

(拔高一层视角来看……)

就算前面一堆技术术语你没完全看懂,这里也有几点重要心得。先说第一点:主动借鉴林纳斯的开发思路,给我带来的最大收获就是这套 SMTP 转发方案。有位用户给了我这个很棒的点子,我只需要理清这套方案背后的深层逻辑就行。

11.自己想出好点子固然厉害,但发掘用户提出的好思路同样重要。很多时候后者反而更可贵。

你很快会发现一件有意思的事:只要你坦荡承认成果离不开他人帮助,他们反倒会认为所有的创新全是你自己摸索出来的,只会觉得你天资出众,只是分寸得当、低调谦逊。林纳斯就是最好的例子,这套做法在他身上非常有效!

(1997 年 8 月,我在第一届 Perl 大会做分享,大牛黑客拉里・沃尔就坐在第一排。等我讲到上面那段收尾的话时,他学着布道大会那种激昂的腔调高声喊:“说得好,就该这么讲,兄弟!” 台下所有人哄堂大笑,大家都清楚,这套处世之道在 Perl 之父身上同样行得通。)

我抱着同样的心态维护这个项目几周后,就收到了源源不断的夸赞,不光来自项目用户,不少听说过这件事的圈外人也纷纷发来好评。我留了一部分这类邮件;要是哪天我怀疑自己做的一切是否有意义,就翻出来看一看 :-)。

但这里还有两条更核心、无关派系立场的经验,适用于所有类型的设计工作。

12.很多时候,最亮眼、最有突破性的方案,都源于意识到自己最初对问题的认知从根上就错了。

我之前一直搞错了解决方向,持续迭代 popclient,让它同时做 MTA 和 MDA,还堆了一堆别扭的本地投递逻辑。Fetchmail 的设计必须彻底推倒重来,只做纯粹的 MTA,融入互联网标准 SMTP 邮件流转链路。

如果你在开发过程中陷入瓶颈,怎么都想不出下一版补丁方案,不必纠结解法对错,而要反思自己提出的问题本身对不对,你或许需要重新定义问题。

好吧,我重新定义了我的问题。显然,正确的方案是:(1) 把 SMTP 转发支持塞到通用驱动里;(2) 将这个模式设置成默认模式;(3) 最终移除其它所有的投递模式,包括投递到文件、投递到标准输出两类选项。

我对第三步纠结了很久,生怕得罪那些依赖原有投递方式的老 popclient 用户。理论上,他们完全可以用 .forward 文件或是其他 sendmail 同类工具实现同样的效果,但在实际的切换过程中大概率会出各种状况。

可真正删掉这些逻辑后,收益远超预期。驱动层里那些最陈旧、最杂乱的祖传代码直接消失了,配置也大幅简化——再也不用窝囊地去翻找系统 MDA、定位用户邮箱,也不必操心底层系统是否支持文件锁。

除此之外,唯一会导致邮件丢失的场景也彻底消失了。原来如果指定投递至本地文件,一旦磁盘写满,邮件就会直接丢失;改用 SMTP 转发后就不会出现这种问题:SMTP 接收端只有确认邮件可成功投递、或至少能存入队列暂存待发的时候,才会返回 OK。

除此之外程序性能也有所提升,只是单次执行很难感知到差别。这次改造还有一个不小的好处:工具手册页精简了非常多。

后来为了适配动态 SLIP 这类少见的特殊场景,我不得不重新加回“由用户指定本地 MDA 投递”的功能,不过这次我找到了简洁得多的实现方案。

这件事给我们什么启示?只要不会削弱程序功能,该舍弃过时的功能时就别犹豫。安托万・德・圣-埃克苏佩里(Antoine de SaintExupéry,这位除了写下经典儿童文学,同时也是飞行员、飞机设计师)曾说过:

13.“(设计的)完美,不在于无物可增,而在于无可再减。”

当代码质量稳步提升、同时逻辑也更加简洁时,就说明你的设计走对路了。也正是在这样一番改造后,fetchmail 形成了独属于自己的设计定位,和前身 popclient 彻底区分开来。

是时候更换项目名称了。全新架构相比旧版 popclient,和 sendmail 更像是一对互补工具:二者同属邮件传输代理 MTA,sendmail 向外推送邮件再完成投递,而重构后的工具则先拉取邮件再投递。项目启动 2 个月后,我就把它重命名为 fetchmail。

从 fetchmail 新增 SMTP 投递功能这件事里,还能总结出一条普适经验:不光程序调试可以并行,开发工作,甚至各类方案探索也都可以并行推进。如果你的开发模式以快速迭代为主,新增功能、优化拓展都可以当作调试的特殊形式:修补软件最初的功能规划、核心思路里存在的 “设计疏漏 bug”。

即便上升到顶层设计层面,多名协作开发者围绕产品,在设计空间里自由发散尝试,价值也格外突出。不妨观察积水自动流向排水口,或是蚂蚁觅食的过程:先依靠扩散式的方式全域探索,再依托一套可扩展的信息传递机制,集中利用已经发现的有效路径。这套模式非常有用,就像当初哈里·霍赫海瑟给我提供思路那样,团队里的前哨成员很可能发现一处价值巨大的优化方向,而你因为过度聚焦手头工作反倒视而不见。