大多数人认为电子邮件是他们与之交互的程序——他们的邮件客户端,在技术上称为邮件用户代理(MUA)。但电子邮件的另一个重要部分是实际将邮件从发送方传输到接收方的软件——邮件传输代理(MTA)。互联网上的第一个 MTA,也是迄今为止最流行的,是 sendmail。
Sendmail 最初是在互联网正式出现之前创建的。它取得了非凡的成功,从 1981 年发展至今,当时互联网是否会成为一个仅有数百台主机参与的学术实验尚不确定,到今天,截至 2011 年 1 月,互联网主机数量已超过 8 亿1。Sendmail 仍然是互联网上使用最广泛的 SMTP 实现之一。
后来被称为 sendmail 的程序的第一个版本是在 1980 年编写的。它最初是一个快速修复程序,用于在不同的网络之间转发邮件。互联网当时正在开发中,但尚不完善。事实上,当时提出了许多不同的网络,并没有形成明显的共识。Arpanet 在美国使用,互联网被设计为一种升级方案,但欧洲将全部力量投入到 OSI(开放系统互连)项目中,并且有一段时间 OSI 看起来可能占据上风。这两者都使用了电话公司提供的租用线路;在美国,速度为 56 Kbps。
就连接的计算机和人员数量而言,当时可能最成功的网络是 UUCP 网络,它不同寻常之处在于它完全没有中央权威机构。从某种意义上说,它是最初的点对点网络,通过拨号电话线运行:9600 bps 在一段时间内是可用的最快速度。最快的网络(3 Mbps)基于 Xerox 的以太网,运行一种名为 XNS(Xerox 网络系统)的协议——但它无法在本地安装之外工作。
当时的运行环境与今天大不相同。计算机高度异构,甚至连使用 8 位字节都没有达成完全一致。例如,其他机器包括 PDP-10(36 位字,9 位字节)、PDP-11(16 位字,8 位字节)、CDC 6000 系列(60 位字,6 位字符)、IBM 360(32 位字,8 位字节)、XDS 940、ICL 470 和 Sigma 7。当时正在兴起的平台之一是 Unix,它来自贝尔实验室。大多数基于 Unix 的机器具有 16 位地址空间:当时 PDP-11 是主要的 Unix 机器,Data General 8/32 和 VAX-11/780 刚刚出现。线程不存在——事实上,动态进程的概念仍然相当新颖(Unix 有它们,但 IBM 的 OS/360 等“严肃”系统却没有)。Unix 内核不支持文件锁定(但可以使用文件系统链接实现一些技巧)。
网络(如果存在的话)通常速度很慢(许多基于 9600 波特的 TTY 线路;真正富有的用户可能可以使用以太网,但仅限于本地使用)。久负盛名的套接字接口还要很多年才会被发明出来。公钥加密也还没有发明出来,所以我们今天所知的多数网络安全措施在当时都是不可行的。
Unix 上已经存在网络电子邮件,但它是使用一些技巧创建的。当时的主要用户代理是 /bin/mail
命令(今天有时称为 binmail
或 v7mail
),但一些站点还有其他用户代理,例如来自伯克利的 Mail
,它实际上了解如何将邮件视为单个项目,而不是一个功能强大的 cat
程序。每个用户代理都直接读取(通常也写入!)/usr/spool/mail
;对于邮件的实际存储方式没有抽象概念。
将邮件路由到网络还是本地电子邮件的逻辑只不过是查看地址是否包含感叹号(UUCP)或冒号(BerkNET)。拥有 Arpanet 访问权限的用户必须使用一个完全独立的邮件程序,该程序无法与其他网络互操作,甚至将本地邮件存储在不同的位置和格式中。
为了使事情变得更有趣,邮件本身的格式几乎没有标准化。大家普遍同意,邮件顶部会有一块标题字段,每个标题字段都会在新的一行上,并且标题字段名称和值之间用冒号分隔。除此之外,无论是标题字段名称的选择还是各个字段的语法,标准化程度都很低。例如,某些系统使用 Subj:
而不是 Subject:
,Date:
字段的语法不同,并且某些系统不理解 From:
字段中的完整名称。最糟糕的是,文档中的内容往往模棱两可,或者与实际使用情况不符。特别是,RFC 733(声称描述了 Arpanet 邮件的格式)与实际使用情况存在细微但有时重要的差异,并且邮件的实际传输方法根本没有正式记录(尽管一些 RFC 提到了该机制,但没有一个定义它)。结果是,邮件系统周围出现了一些祭司。
1979 年,INGRES 关系数据库管理项目(也就是我的日常工作)获得了 DARPA 的资助,以及与之配套的 9600bps Arpanet 连接到我们的 PDP-11。当时它是计算机科学系唯一可用的 Arpanet 连接,所以每个人都想访问我们的机器以便访问 Arpanet。但是,那台机器已经达到最大负荷,因此我们只能为系里所有人员共享提供两个登录端口。这导致了大量的争夺和频繁的冲突。然而,我注意到人们最想要的东西不是远程登录或文件传输,而是电子邮件。
在这种情况下,sendmail(最初称为 delivermail)应运而生,试图将混乱统一到一个地方。每个 MUA(邮件用户代理或邮件客户端)都只需调用 delivermail 来传递邮件,而不是想方设法地以临时(且通常不兼容)的方式来完成。Delivermail/sendmail 并没有试图规定本地邮件的存储或传递方式;它除了在其他程序之间传递邮件之外什么也不做。(正如我们很快就会看到的,当添加 SMTP 时,这种情况发生了变化。)从某种意义上说,它只是将各种邮件系统粘合在一起的胶水,而不是一个独立的邮件系统。
在 sendmail 的开发过程中,Arpanet 发展成了互联网。从网络上的低级数据包到应用协议,变化非常广泛,并且并非一蹴而就。Sendmail 的开发实际上与标准的制定同时进行,并在某些情况下影响了标准的制定。同样值得注意的是,sendmail 在“网络”(我们今天所认为的网络)从几百台主机扩展到数亿台主机的过程中幸存下来,甚至蓬勃发展。
另一个网络
值得一提的是,当时还提出了另一个完全独立的邮件标准,称为 X.400,它是 ISO/OSI(国际标准化组织/开放系统互连)的一部分。X.400 是一种二进制协议,邮件使用 ASN.1(抽象语法表示法 1)进行编码,ASN.1 今天仍然用于一些互联网协议,例如 LDAP。LDAP 反过来是对 X.500 的简化,X.500 是 X.400 使用的目录服务。Sendmail 根本没有尝试直接兼容 X.400,尽管当时存在一些网关服务。尽管 X.400 最初被当时许多商业供应商采用,但互联网邮件和 SMTP 最终赢得了市场。
在开发 sendmail 的过程中,我坚持了几项设计原则。所有这些原则从某种意义上来说都归结为一件事:尽可能少做。这与当时其他一些具有更广泛目标且需要更大规模实现的项目形成了鲜明对比。
我将 sendmail 作为兼职、无偿项目编写。它旨在以一种快速的方式使加州大学伯克利分校的人员更容易访问 Arpanet 邮件。关键是在现有的网络之间转发邮件,所有这些网络都是作为独立程序实现的,并且不知道甚至存在多个网络。仅靠一个兼职程序员来修改现有软件的很大一部分是不切实际的。设计必须最大限度地减少需要修改的现有代码量以及需要编写的新的代码量。这一限制推动了大部分其他设计原则。事实证明,即使有更大的团队可用,在大多数情况下,这些原则也是正确的做法。
邮件用户代理(MUA)是大多数最终用户认为的“邮件系统”——它是他们用来阅读、编写和回复邮件的程序。它与邮件传输代理(MTA)截然不同,后者将邮件从发送方路由到接收方。在编写 sendmail 时,许多实现至少部分地结合了这两个功能,因此它们通常是同步开发的。试图同时处理这两者将过于困难,因此 Sendmail 完全放弃了用户界面问题:对 MUA 的唯一更改是让它们调用 sendmail 而不是执行自己的路由。特别是,当时已经存在几个用户代理,人们通常对他们与邮件的交互方式非常敏感。试图同时处理这两者将过于困难。现在,MUA 与 MTA 的分离已成为公认的智慧,但在当时远非标准做法。
本地邮件存储(将邮件保存到接收方来读取之前的位置)没有正式标准化。一些站点喜欢将它们存储在集中式位置,例如 /usr/mail
、/var/mail
或 /var/spool/mail
。其他站点喜欢将它们存储在接收方的主目录中(例如,作为名为 .mail
的文件)。大多数站点在每条邮件的开头都使用以“From”开头并后跟空格字符的行(这是一个非常糟糕的决定,但这是当时的惯例),但专注于 Arpanet 的站点通常使用包含四个控制-A 字符的行来分隔邮件。一些站点尝试锁定邮箱以防止冲突,但它们使用了不同的锁定约定(文件锁定原语尚未可用)。简而言之,唯一合理的方法是将本地邮件存储视为黑盒。
在几乎所有站点上,执行本地邮箱存储的实际机制都体现在 /bin/mail
程序中。它在一个程序中构建了(非常原始的)用户界面、路由和存储。为了整合 sendmail,路由部分被提取出来并替换为对 sendmail 的调用。添加了 -d
标志以强制最终传递,即它阻止 /bin/mail
调用 sendmail 来执行路由。在后来的几年里,用于将邮件传递到物理邮箱的代码被提取到另一个名为 mail.local
的程序中。/bin/mail
程序今天仅用于包含脚本发送邮件的最低公分母。
诸如 UUCP 和 BerkNET 之类的协议已经作为独立程序实现,它们拥有自己有时很古怪的命令行结构。在某些情况下,它们与 sendmail 同时被积极开发。很明显,重新实现它们(例如,将它们转换为标准调用约定)将会很痛苦。这直接导致了 sendmail 应该适应外部世界,而不是试图让外部世界适应 sendmail 的原则。
在 sendmail 的开发过程中,我尽可能地不去触碰任何我非触碰不可的东西。除了没有足够的时间去做之外,当时伯克利有一种文化,它避免大多数正式的代码所有权,转而采用“最后触碰代码的人是该程序的负责人”(或者更简单地说,“你触碰它,你就拥有它”)的策略。虽然按照大多数现代标准,这听起来很混乱,但在伯克利没有人被分配全职工作于 Unix 的世界里,它运行得相当好;个人从事他们感兴趣和致力于的系统部分的工作,并且除了在紧急情况下,不触碰代码库的其余部分。
在 sendmail 之前的邮件系统(包括大多数传输系统)并没有特别关注可靠性。例如,4.2BSD 之前的 Unix 版本没有本地文件锁定,尽管可以通过创建临时文件然后将其链接到锁定文件来模拟(如果锁定文件已存在,则链接调用将失败)。但是,有时不同的程序写入同一个数据文件不会就如何进行锁定达成一致(例如,它们可能使用不同的锁定文件名,甚至根本不尝试锁定),因此丢失邮件的情况并不少见。Sendmail 采取的方法是不允许丢失邮件(可能是由于我作为数据库人员的背景,在数据库中丢失数据是不可饶恕的罪过)。
早期版本中有很多事情没有做。我没有尝试重新设计邮件系统或构建一个完全通用的解决方案:功能可以根据需要添加。非常早期的版本甚至不打算在没有访问源代码和编译器的情况下进行完全配置(尽管这种情况在早期就发生了变化)。总的来说,sendmail 的工作方式是快速让某些东西工作起来,然后根据需要以及更好地理解问题来增强工作的代码。
像大多数长期存在的软件一样,sendmail 是分阶段开发的,每个阶段都有自己的基本主题和感觉。
sendmail 的第一个实例被称为 delivermail。它非常简单,甚至可以说是简陋。它的唯一工作是将邮件从一个程序转发到另一个程序;特别是,它没有 SMTP 支持,因此从未进行过任何直接的网络连接。不需要排队,因为每个网络都有自己的队列,所以该程序实际上只是一个交叉开关。由于 delivermail 没有直接的网络协议支持,因此它没有理由作为守护进程运行——它将被调用以在提交每条消息时路由它,将其传递给将实现下一跳的适当程序,然后终止。此外,没有尝试重写报头以匹配邮件正在发送到的网络。这通常会导致转发无法回复的消息。情况非常糟糕,以至于写了一整本书来讨论邮件寻址(恰如其分地称为!%@:: 电子邮件寻址和网络目录 [AF94])。
delivermail 中的所有配置都是编译进来的,并且仅基于每个地址中的特殊字符。这些字符具有优先级。例如,主机配置可能会搜索“@”符号,如果找到,则将整个地址发送到指定的 Arpanet 中继主机。否则,它可能会搜索冒号,如果找到冒号,则将消息发送到 BerkNET,并指定主机和用户,然后可以检查感叹号(“!”),表示消息应转发到指定的 UUCP 中继。否则,它将尝试本地传递。此配置可能会导致以下结果
输入 | 发送到 {网络,主机,用户} |
---|---|
foo@bar | {Arpanet, bar, foo} |
foo:bar | {Berknet, foo, bar} |
foo!bar!baz | {Uucp, foo, bar!baz} |
foo!bar@baz | {Arpanet, baz, foo!bar} |
请注意,地址分隔符在关联性方面有所不同,导致只能使用启发式方法解决的歧义。例如,在另一个站点上,最后一个示例可以合理地解析为 {Uucp, foo, bar@baz}。
出于几个原因,配置被编译到程序中:首先,在 16 位地址空间和有限内存的情况下,解析运行时配置过于昂贵。其次,当时的系统高度定制,重新编译是一个好主意,只是为了确保您拥有库的本地版本(Unix 第 6 版不存在共享库)。
Delivermail 与 4.0 和 4.1 BSD 一起分发,并且比预期更成功;伯克利远非唯一拥有混合网络架构的站点。很明显,需要做更多工作。
版本 1 和 2 以 delivermail 的名称分发。1981 年 3 月,版本 3 的工作开始了,该版本将以 sendmail 的名称分发。此时,16 位 PDP-11 仍在普遍使用,但 32 位 VAX-11 正在变得流行,因此与小型地址空间相关的大多数原始约束开始得到放松。
sendmail 的初始目标是转换为运行时配置,允许修改消息以提供跨网络转发邮件的兼容性,以及拥有一个更丰富的语言来做出路由决策。所使用的技术本质上是地址的文本重写(基于标记而不是字符字符串),这是当时一些专家系统中使用的一种机制。有一些特别的代码用于提取和保存任何注释字符串(在括号中),以及在程序化重写完成后重新插入它们。能够添加或增强报头字段也很重要(例如,添加 Date
报头字段或在 From
报头中包含发件人的全名,如果已知)。
SMTP 开发始于 1981 年 11 月。加州大学伯克利分校的计算机科学研究小组 (CSRG) 获得了 DARPA 合同,以生产一个基于 Unix 的平台来支持 DARPA 资助的研究,目的是使项目之间的共享更容易。当时 TCP/IP 协议栈的初步工作已经完成,尽管套接字接口的细节仍在变化。Telnet 和 FTP 等基本应用程序协议已经完成,但 SMTP 尚未实现。事实上,SMTP 协议当时甚至还没有最终确定;关于如何使用一个创造性地命名为邮件传输协议 (MTP) 的协议发送邮件,存在着巨大的争论。随着争论的加剧,MTP 变得越来越复杂,直到最终出于沮丧,SMTP(简单邮件传输协议)基本上是由命令制定的(但直到 1982 年 8 月才正式发布)。从官方上讲,我当时正在研究 INGRES 关系数据库管理系统,但由于我比当时伯克利周围的任何人都更了解邮件系统,所以我被说服去实现 SMTP。
我最初的想法是创建一个单独的 SMTP 邮件发送器,它有自己的排队和守护进程;该子系统将连接到 sendmail 以执行路由。但是,SMTP 的一些特性使得这变得很麻烦。例如,EXPN
和 VRFY
命令需要访问解析、别名和本地地址验证模块。此外,当时我认为重要的是,如果地址未知,则 RCPT
命令应立即返回,而不是接受邮件,然后以后发送邮件传递失败消息。事实证明这是一个有先见之明的决定。具有讽刺意味的是,后来的 MTA 通常会犯这个错误,从而加剧了垃圾邮件回弹问题。这些问题推动了将 SMTP 作为 sendmail 本身的一部分的决定。
Sendmail 3 与 4.1a 和 4.1c BSD(测试版)一起分发,sendmail 4 与 4.2 BSD 一起分发,sendmail 5 与 4.3 BSD 一起分发。
在我离开伯克利并加入一家创业公司后,我用于开发 sendmail 的时间迅速减少。但互联网开始真正爆炸,sendmail 正在各种新的(和更大的)环境中使用。大多数 Unix 系统供应商(特别是 Sun、DEC 和 IBM)都创建了自己的 sendmail 版本,所有这些版本彼此不兼容。也有人尝试构建开源版本,特别是 IDA sendmail 和 KJS。
IDA sendmail 来自于林雪平大学。IDA 包含扩展,使其更容易在更大的环境中安装和管理,以及一个全新的配置系统。一个主要的新功能是包含 dbm(3) 数据库映射以支持高度动态的站点。可以使用配置文件中的新语法访问这些映射,并用于许多功能,包括地址到外部语法的映射和从外部语法的映射(例如,以 john_doe@example.com 而不是 johnd@example.com 的形式发送邮件)以及路由。
King James Sendmail(KJS,由 Paul Vixie 制作)试图统一所有涌现的各种 sendmail 版本。不幸的是,它从未真正获得足够的吸引力以达到预期的效果。这个时代也受到大量反映在邮件系统中的新技术的推动。例如,Sun 创建的无盘群集添加了 YP(后来的 NIS)目录服务和 NFS,即网络文件系统。特别是,YP 必须对 sendmail 可见,因为别名存储在 YP 中而不是本地文件中。
几年后,我作为工作人员回到了伯克利。我的工作是管理一个团队,为计算机科学系周围的研究安装和支持共享基础设施。为了取得成功,各个研究小组的大量临时环境必须以某种合理的方式统一起来。就像互联网的早期一样,不同的研究小组在根本不同的平台上运行,其中一些平台非常老旧。总的来说,每个研究小组都运行自己的系统,虽然其中一些管理得很好,但大多数都存在“延迟维护”的问题。
在大多数情况下,电子邮件也同样支离破碎。每个人的电子邮件地址都是“person@host.berkeley.edu
”,其中 host
是他们办公室中工作站的名称或他们使用的共享服务器的名称(校园甚至没有内部子域名),除了少数拥有 @berkeley.edu
地址的特殊人员。目标是切换到内部子域名(因此所有单个主机都将位于 cs.berkeley.edu
子域名中)并拥有一个统一的邮件系统(因此每个人都将拥有 @cs.berkeley.edu
地址)。通过创建一个可在整个系中使用的 sendmail 新版本,最容易实现此目标。
我首先研究了许多已变得流行的 sendmail 变体。我的目的不是从不同的代码库开始,而是要理解其他人发现有用的功能。许多这些想法都以某种形式被整合进了 sendmail 8,通常会进行修改以合并相关想法或使它们更通用。例如,几个版本的 sendmail 都能够访问外部数据库,例如 dbm(3) 或 NIS;sendmail 8 将这些合并成一个“map”机制,该机制可以处理多种类型的数据库(甚至任意非数据库转换)。类似地,IDA sendmail 中的“generics”数据库(内部到外部名称映射)也被整合进来。
Sendmail 8 还包含一个使用 m4(1) 宏处理器的新配置包。这旨在比 sendmail 5 配置包更具声明性,sendmail 5 配置包在很大程度上是过程式的。也就是说,sendmail 5 配置包要求管理员基本上手动布置整个配置文件,实际上只使用 m4 中的“include”功能作为简写。sendmail 8 配置文件允许管理员仅声明需要哪些功能、邮件程序等,然后由 m4 布置最终的配置文件。
第 17.7 节 大部分内容讨论了 sendmail 8 中的增强功能。
随着互联网的发展和 sendmail 站点的数量增加,对不断增长的用户群的支持变得更加困难。有一段时间,我能够通过建立一组志愿者(非正式地称为“Sendmail 联盟”,即 sendmail.org)来继续提供支持,他们通过电子邮件和新闻组提供免费支持。但是到了 20 世纪 90 年代后期,安装基础已经发展到如此程度,以至于几乎不可能依靠志愿者来提供支持。我和一位更有商业头脑的朋友一起创立了 Sendmail, Inc.2,期望获得新的资源来投入代码开发。
尽管最初商业产品主要基于配置和管理工具,但为了满足商业世界的需求,在开源 MTA 中也添加了许多新功能。值得注意的是,该公司添加了对 TLS(连接加密)、SMTP 身份验证、站点安全增强功能(如拒绝服务保护)的支持,最重要的是邮件过滤插件(下面讨论的 Milter 接口)。
截至本文撰写之时,商业产品已扩展到包含一套庞大的基于电子邮件的应用程序,几乎所有这些应用程序都是基于公司最初几年添加到 sendmail 中的扩展构建的。
Sendmail 6 本质上是 sendmail 8 的测试版。它从未正式发布,但分发范围相当广泛。Sendmail 7 从未存在过;sendmail 直接跳到版本 8,因为当 1993 年 6 月发布 4.4 BSD 时,BSD 发行版的其他所有源文件都被提升到版本 8。
一些设计决策是正确的。一些最初是正确的,但随着世界的变化而变得错误。一些设计决策令人怀疑,而且并没有变得不那么令人怀疑。
配置文件的语法是由几个问题驱动的。首先,整个应用程序必须适合 16 位地址空间,因此解析器必须很小。其次,早期的配置非常短(不到一页),因此虽然语法晦涩,但文件仍然易于理解。然而,随着时间的推移,更多操作决策从 C 代码转移到配置文件中,文件开始增长。配置文件因其神秘而闻名。许多人感到特别沮丧的一点是选择制表符作为活动的语法项。这是一个从当时的其他系统(特别是 make
)复制的错误。随着窗口系统(以及因此而来的剪切和粘贴,通常不保留制表符)变得可用,这个问题变得更加严重。
回想起来,随着文件越来越大,32 位机器开始占据主导地位,重新考虑语法是有意义的。有一段时间我考虑过这样做,但最终放弃了,因为我不想破坏“庞大”的安装基础(当时可能只有几百台机器)。回想起来,这是一个错误;我根本没有意识到安装基础会发展到多大,如果我早点更改语法,可以节省我多少时间。此外,当标准稳定下来时,可以将相当一部分通用性推回到 C 代码库中,从而简化配置。
特别有趣的是,更多功能是如何被转移到配置文件中的。我在开发 sendmail 的同时,SMTP 标准也在不断发展。通过将操作决策转移到配置文件中,我能够快速响应设计更改——通常在 24 小时内。我相信这改善了 SMTP 标准,因为可以非常快速地获得提议的设计更改的操作经验,但这以使配置文件难以理解为代价。
编写 sendmail 时的一个困难决策是如何进行必要的重写,以便在网络之间转发邮件,而不会违反接收网络的标准。这些转换需要更改元字符(例如,BerkNET 使用冒号作为分隔符,这在 SMTP 地址中是不合法的),重新排列地址组件,添加或删除组件等。例如,在某些情况下需要以下重写
从 | 到 |
---|---|
a:foo | a.foo@berkeley.edu |
a!b!c | b!c@a.uucp |
<@a.net,@b.org:user@c.com> | <@b.org:user@c.com> |
正则表达式不是一个好的选择,因为它们对词边界、引号等没有良好的支持。很快就很明显,编写准确的正则表达式几乎是不可能的,更不用说易于理解的了。特别是,正则表达式保留了许多元字符,包括“.”、“*”、“+”、“{ [}”和“{ }]”,所有这些都可能出现在电子邮件地址中。这些可以在配置文件中转义,但我认为这很复杂、令人困惑,而且有点丑陋。(贝尔实验室的 UPAS(Unix 第八版中的邮件程序)尝试过这种方法,但从未流行起来3。)相反,需要一个扫描阶段来生成标记,然后可以像正则表达式中的字符一样操纵这些标记。一个描述“运算符字符”的单个参数就足够了,这些字符本身既是标记又是标记分隔符。空格分隔标记,但本身不是标记。重写规则只是模式匹配/替换对,组织成本质上是子程序的形式。
我没有使用大量需要转义才能失去其“神奇”属性的元字符(如正则表达式中使用的那样),而是使用了一个单个“转义”字符,该字符与普通字符组合以表示通配符模式(例如,匹配任意单词)。传统的 Unix 方法是使用反斜杠,但反斜杠在某些地址语法中已被用作引号字符。事实证明,“$”是为数不多的尚未用作某些电子邮件语法中标点符号的字符之一。
最初的错误决策之一,具有讽刺意味的是,仅仅是空格的使用方式问题。空格字符是一个分隔符,就像大多数扫描输入一样,因此可以在模式中的标记之间自由使用。但是,分发的原始配置文件不包含空格,导致模式比必要时更难以理解。请考虑以下两个(语义上相同)模式之间的区别
$+ + $* @ $+ . $={mydomain} $++$*@$+.$={mydomain}
有人建议 sendmail 应该使用传统的基于语法的解析技术来解析地址,而不是重写规则,并将重写规则留给地址修改。从表面上看,这似乎很有道理,因为标准使用语法定义地址。重新使用重写规则的主要原因是在某些情况下需要解析报头字段地址(例如,为了从报头中提取发件人信封,当从没有正式信封的网络接收邮件时)。使用(例如)YACC 和传统扫描器之类的 LALR(1) 解析器很难解析此类地址,因为需要大量的预读。例如,解析地址:allman@foo.bar.baz.com <eric@example.com>
需要由扫描器或解析器进行预读;在看到“<”之前,你无法知道初始“allman@…”是否为地址。由于 LALR(1) 解析器只有一个标记的预读,因此这必须在扫描器中完成,这会使扫描器变得非常复杂。由于重写规则已经具有任意回溯(即,它们可以任意远地预读),因此它们就足够了。
次要原因是相对容易使模式识别并修复损坏的输入。最后,重写功能足够强大,可以完成这项工作,并且重用任何代码都是明智的。
关于重写规则的一个不寻常之处:在进行模式匹配时,输入和模式都标记化是有用的。因此,相同的扫描器用于输入地址和模式本身。这要求扫描器使用不同的字符类型表来处理不同的输入。
实现传出(客户端)SMTP 的一种“显而易见”的方法是将其构建为一个外部邮件程序,类似于 UUCP。但这会引发许多其他问题。例如,排队是在 sendmail 中完成还是在 SMTP 客户端模块中完成?如果它在 sendmail 中完成,则必须将消息的单独副本发送到每个收件人(即,没有“搭载”,其中可以打开单个连接,然后发送多个 RCPT
命令),或者需要更丰富的通信回程路径来传递必要的每个收件人的状态,而这使用简单的 Unix 退出代码是不可能的。如果排队在客户端模块中完成,则可能导致大量复制;特别是,当时其他网络(如 XNS)仍然是可能的竞争者。此外,将队列包含到 sendmail 本身中提供了一种更优雅的方式来处理某些类型的故障,特别是诸如资源耗尽之类的瞬态问题。
传入(服务器)SMTP 涉及一组不同的决策。当时,我认为忠实地实现 VRFY
和 EXPN
SMTP 命令非常重要,这需要访问别名机制。这将再次需要在服务器 SMTP 模块和 sendmail 之间进行比使用命令行和退出代码可能的更丰富的协议交换——实际上,类似于 SMTP 本身的协议。
今天,我更有可能将排队保留在核心 sendmail 中,但将 SMTP 实现的两端都移动到其他进程中。原因之一是为了获得安全性:一旦服务器端打开了端口 25 的实例,它就不再需要访问 root 权限。诸如 TLS 和 DKIM 签名之类的现代扩展使客户端变得复杂(因为私钥不应可供非特权用户访问),但严格来说,仍然不需要 root 访问权限。尽管安全问题仍然存在,但如果客户端 SMTP 作为可以读取私钥的非 root 用户运行,那么该用户根据定义具有特殊权限,因此不应直接与其他站点通信。所有这些问题都可以通过一些工作来解决。
Sendmail 遵循当时存储队列文件的惯例。事实上,使用的格式与当时的 lpr 子系统非常相似。每个作业有两个文件,一个包含控制信息,另一个包含数据。控制文件是一个纯文本文件,每行的第一个字符表示该行的含义。
当 sendmail 需要处理队列时,它必须读取所有控制文件,将相关信息存储在内存中,然后对列表进行排序。对于队列中数量相对较少的邮件,这种方法运行良好,但在大约 10,000 条邮件时开始出现问题。具体来说,当目录变得足够大以至于需要文件系统中的间接块时,会出现严重的性能下降,性能下降幅度可能高达一个数量级。可以通过让 sendmail 理解多个队列目录来缓解这个问题,但这充其量只是一个权宜之计。
另一种实现方法可能是将所有控制文件存储在一个数据库文件中。之所以没有这样做,是因为在 sendmail 开始编码时,没有普遍可用的数据库包,而当 dbm(3) 可用时,它存在一些缺陷,包括无法回收空间,要求所有哈希在一起的键都适合一个 (512 字节) 页面,以及缺乏锁定机制。健壮的数据库包直到很多年后才出现。
另一种实现方法是使用一个单独的守护进程,该守护进程将队列的状态保存在内存中,可能写入日志以允许恢复。考虑到当时电子邮件流量相对较低,大多数机器上的内存不足,后台进程的成本相对较高,以及实现此类进程的复杂性,当时这似乎不是一个好的权衡。
另一个设计决策是将邮件头存储在队列控制文件中而不是数据文件中。其原因是,大多数邮件头需要大量的重写,这些重写因目标而异(并且由于邮件可能有多个目标,因此必须多次自定义它们),并且解析邮件头的成本似乎很高,因此将它们存储在预解析的格式中似乎可以节省成本。事后看来,这不是一个好的决定,就像将邮件正文存储在 Unix 标准格式(使用换行符结尾)中而不是接收到的格式(可以使用换行符、回车/换行符、裸回车或换行符/回车)中一样。随着电子邮件世界的不断发展和标准的采用,重写的需求减少了,即使看似无害的重写也存在出错的风险。
由于 sendmail 是在一个存在多种协议且令人不安地缺乏书面标准的世界中创建的,因此我决定尽可能地清理格式错误的邮件。这符合 RFC 7934 中阐述的“鲁棒性原则”(又名 Postel 定律)。其中一些更改是显而易见的,甚至也是必需的:在将 UUCP 邮件发送到 Arpanet 时,需要将 UUCP 地址转换为 Arpanet 地址,即使只是为了让“回复”命令能够正常工作,还需要在各种平台使用的约定之间转换行终止符,等等。有些则不太明显:如果收到一条邮件,其中不包含 Internet 规范中要求的 From:
邮件头字段,是否应该添加 From:
邮件头字段,在没有 From:
邮件头字段的情况下传递邮件,还是拒绝邮件?当时,我的首要考虑因素是互操作性,因此 sendmail 修复了邮件,例如,通过添加 From:
邮件头字段。然而,有人声称这导致其他有问题的邮件系统被延续了很长时间,而它们本应该在很久以前就被修复或淘汰了。
我相信我的决定在当时是正确的,但在今天却存在问题。高程度的互操作性对于让邮件畅通无阻非常重要。如果我拒绝格式错误的邮件,那么当时大多数邮件都会被拒绝。如果我未经修复地传递它们,则收件人会收到无法回复的邮件,在某些情况下甚至无法确定谁发送了邮件——或者邮件会被另一个邮件发送程序拒绝。
如今,标准已经编写出来,并且在大多数情况下,这些标准都是准确和完整的。大多数邮件不会被拒绝的情况不再存在,但仍然有一些邮件软件会发送损坏的邮件。这给 Internet 上的其他软件带来了不必要的许多问题。
有一段时间,我一直在对 sendmail 配置文件进行定期更改,并且亲自支持许多机器。由于不同机器之间的配置文件有很多相同的部分,因此使用工具来构建配置文件是可取的。m4 宏处理器包含在 Unix 中。它被设计为编程语言(尤其是 ratfor)的前端。最重要的是,它具有“包含”功能,就像 C 语言中的“#include”一样。最初的配置文件仅使用了此功能和一些小的宏扩展。
IDA sendmail 也使用了 m4,但方式却大不相同。事后看来,我可能应该更详细地研究这些原型。它们包含了许多巧妙的想法,特别是它们处理引号的方式。
从 sendmail 6 开始,m4 配置文件被完全重写为更声明性的风格,并且更小。这使用了 m4 处理器更多的功能,当 GNU m4 的引入以微妙的方式更改了一些语义时,这成为了一个问题。
最初的计划是 m4 配置文件将遵循 80/20 规则:它们将很简单(因此占 20% 的工作量),并且将涵盖 80% 的情况。由于两个原因,这种计划很快就被打破了。较不重要的原因是,事实证明处理绝大多数情况相对容易,至少在开始时是这样。随着 sendmail 和世界的不断发展,这变得更加困难,特别是随着 TLS 加密和 SMTP 身份验证等功能的加入,但这些功能直到很久以后才出现。
重要的原因是,很明显,原始的配置文件对于大多数人来说太难管理了。从本质上讲,.cf
(原始)格式已成为汇编代码——原则上可编辑,但实际上相当不透明。“源代码”是存储在 .mc
文件中的 m4 脚本。
另一个重要的区别是,原始格式的配置文件实际上是一种编程语言。它具有过程代码(规则集)、子程序调用、参数扩展和循环(但没有 goto)。语法晦涩难懂,但在很多方面类似于 sed
和 awk
命令,至少在概念上是这样。m4 格式是声明式的:尽管可以进入低级原始语言,但在实践中,这些细节对用户是隐藏的。
目前尚不清楚这个决定是正确还是错误。我当时(现在仍然)认为,对于复杂的系统,实现相当于用于构建系统某些部分的领域特定语言 (DSL) 可能是很有用的。但是,将该 DSL 作为配置方法公开给最终用户,实际上会将所有尝试配置系统的操作都转换为编程问题。这带来了强大的功能,但代价却不容小觑。
还有一些其他的架构和开发要点值得一提。
在大多数基于网络的系统中,客户端和服务器之间存在着一种张力。对客户端来说好的策略可能对服务器来说是错误的,反之亦然。例如,服务器在可能的情况下希望通过尽可能多地将处理成本推回给客户端来最小化其处理成本,当然客户端也希望这样做,但方向相反。例如,服务器可能希望在执行垃圾邮件处理时保持连接打开,因为这降低了拒绝邮件的成本(如今这是常见情况),但客户端希望尽快继续执行。从整个系统,即整个 Internet 的角度来看,最佳解决方案可能是平衡这两种需求。
有一些 MTA 使用了明确偏向客户端或服务器的策略。它们之所以能够这样做,仅仅是因为它们拥有相对较小的安装基础。当你的系统被 Internet 上很大一部分用户使用时,你必须设计它,以便在两端之间平衡负载,从而尝试优化整个 Internet。这因为始终会存在完全偏向一方或另一方的 MTA 而变得复杂——例如,批量邮件系统只关心优化发送端。
在设计一个包含连接两端的系统的过程中,避免偏袒任何一方非常重要。请注意,这与客户端和服务通常的不对称性形成鲜明对比——例如,Web 服务器和 Web 客户端通常不是由同一组开发人员开发的。
Sendmail 最重要的补充之一是 milter(mail filter)接口。Milter 允许使用外部插件(即,它们在单独的进程中运行)进行邮件处理。这些最初是为反垃圾邮件处理而设计的。milter 协议与服务器 SMTP 协议同步运行。当从客户端接收到每个新的 SMTP 命令时,sendmail 会使用该命令的信息调用 milter。milter 有机会接受命令或发送拒绝,这将拒绝适合 SMTP 命令的协议阶段。Milter 被建模为回调,因此当 SMTP 命令到来时,将调用相应的 milter 子例程。Milter 是带线程的,每个连接上下文指针都传递到每个例程中,以允许传递状态。
理论上,milter 可以作为 sendmail 地址空间中的可加载模块工作。我们拒绝这样做,原因有三个。首先,安全问题太严重了:即使 sendmail 作为唯一的非 root 用户 ID 运行,该用户也将能够访问其他所有邮件的状态。同样,一些 milter 作者不可避免地会尝试访问 sendmail 的内部状态。
其次,我们希望在 sendmail 和 milter 之间创建一个防火墙:如果 milter 崩溃,我们希望明确是谁的错,并且邮件(可能)继续流动。第三,对于 milter 作者来说,调试独立进程比调试整个 sendmail 要容易得多。
很快发现,milter 的用途不仅仅是反垃圾邮件处理。事实上,milter.org5 网站列出了用于反垃圾邮件、反病毒、存档、内容监控、日志记录、流量整形以及许多其他类别的 milter,这些 milter 由商业公司和开源项目提供。postfix 邮件发送程序6 已添加了对使用相同接口的 milter 的支持。Milter 已证明是 sendmail 的一项重大成功。
关于“尽早发布,频繁发布”和“发布稳定系统”这两种思想流派,一直存在着广泛的争论。Sendmail 在不同的时期都采用了这两种方法。在发生重大变化的时期,我有时一天会发布多个版本。我的总体理念是在每次更改后都发布一个版本。这类似于向公众开放源代码管理系统树。我个人更倾向于发布版本而不是提供公共源代码树,至少部分原因是我以现在被认为是不被认可的方式使用源代码管理:对于大型更改,我将在编写代码时检入非功能性快照。如果共享树,我会为这些快照使用分支,但在任何情况下,它们都可供全世界查看,并可能造成相当大的混乱。此外,创建版本意味着为其分配一个编号,这使得在浏览错误报告时更容易跟踪更改。当然,这要求版本易于生成,而这并非总是如此。
随着 Sendmail 在越来越多的关键生产环境中得到使用,这开始变得有问题。其他人并不总是很容易区分我想让人们测试的更改与真正打算在实际环境中使用的更改。将版本标记为“alpha”或“beta”可以缓解但无法解决此问题。结果是,随着 Sendmail 的成熟,它转向了更少但更大的版本发布。当 Sendmail 并入一家商业公司时,这种情况变得尤其尖锐,该公司拥有希望获得最新版本但也只需要稳定版本的客户,并且他们不会接受这两者是不兼容的。
开源开发者需求和商业产品需求之间的这种紧张关系永远不会消失。尽早发布和频繁发布有很多好处,最值得注意的是,潜在的大量勇敢(有时也很愚蠢)的测试人员可以以您几乎无法在标准开发系统中重现的方式对系统进行压力测试。但是,随着项目的成功,它往往会变成产品(即使该产品是开源且免费的),而产品与项目的需求不同。
从安全角度来看,Sendmail 经历了动荡的一生。其中一些是应该受到谴责的,但有些则不是,因为我们对“安全”的理解在我们不知不觉中发生了变化。互联网最初的用户群只有几千人,主要是在学术和研究领域。在许多方面,它比我们今天所知的互联网要友好得多。网络的设计是为了鼓励共享,而不是构建防火墙(另一个在早期不存在的概念)。网络现在是一个危险而充满敌意的场所,充满了垃圾邮件发送者和黑客。它越来越多地被描述为一个战争区,而在战争区,会有平民伤亡。
编写安全的网络服务器很难,尤其是在协议超出最简单的协议时。几乎所有程序都存在至少轻微的问题;甚至常见的 TCP/IP 实现也曾遭到成功攻击。更高级别的实现语言并没有成为灵丹妙药,甚至还产生了自身的安全漏洞。必要的警句是“不信任所有输入”,无论它来自何处。不信任输入包括辅助输入,例如来自 DNS 服务器和 Milter 的输入。与大多数早期的网络软件一样,Sendmail 在其早期版本中过于信任。
但 Sendmail 最大的问题是早期版本以 root 权限运行。为了打开 SMTP 监听套接字、读取各个用户的转发信息以及将邮件传递到各个用户的邮箱和主目录,需要 root 权限。但是,在当今大多数系统上,邮箱名称的概念已与系统用户的概念脱钩,这实际上消除了对 root 访问的需要,除非是为了打开 SMTP 监听套接字。如今,Sendmail 能够在处理连接之前放弃 root 权限,从而消除了对支持此功能的环境的担忧。值得注意的是,在那些不直接传递到用户邮箱的系统上,Sendmail 也可以在 chroot 环境中运行,从而进一步隔离权限。
不幸的是,随着 Sendmail 因安全性差而声名狼藉,它开始被归咎于与 Sendmail 无关的问题。例如,一位系统管理员将其 /etc
目录设置为世界可写,然后在有人替换 /etc/passwd
文件时责怪 Sendmail。正是类似这样的事件导致我们大幅加强了安全性,包括明确检查 Sendmail 访问的文件和目录的所有权和模式。这些措施过于严苛,以至于我们不得不包含 DontBlameSendmail
选项以(选择性地)关闭这些检查。
安全性还有其他方面与保护程序本身的地址空间无关。例如,垃圾邮件的兴起也导致了地址收集的兴起。SMTP 中的 VRFY
和 EXPN
命令分别专门设计用于验证单个地址和扩展邮件列表的内容。这些命令被垃圾邮件发送者滥用得如此严重,以至于现在大多数站点都完全关闭了它们。这令人遗憾,至少对于 VRFY
而言,因为此命令有时会被一些反垃圾邮件代理用来验证声称的发送地址。
同样,防病毒保护曾经被视为桌面问题,但其重要性日益提高,以至于任何商业级的 MTA 都必须提供防病毒检查功能。现代环境中的其他安全相关要求包括敏感数据的强制加密、数据丢失保护以及法规要求的执行,例如 HIPPA。
Sendmail 早期就牢记的原则之一是可靠性——每条邮件要么被投递,要么被报告回发件人。但是,joe-job(攻击者伪造邮件的回复地址,许多人将其视为安全问题)问题已导致许多站点关闭了退信的创建。如果在 SMTP 连接仍然打开时可以确定故障,则服务器可以通过使命令失败来报告问题,但 SMTP 连接关闭后,地址错误的邮件将静默消失。公平地说,当今大多数合法邮件都是单跳的,因此会报告问题,但至少从原则上讲,世界已经决定安全性胜过可靠性。
软件在快速变化的环境中无法生存,除非它能够进化以适应不断变化的环境。新的硬件技术不断出现,这会推动操作系统发生变化,而操作系统又会推动库和框架发生变化,最终又会推动应用程序发生变化。如果某个应用程序取得成功,它将在越来越复杂的环境中使用。变化是不可避免的;要取得成功,您必须接受并拥抱变化。本节描述了 Sendmail 演变过程中发生的一些更重要的变化。
Sendmail 的原始配置非常简洁。例如,选项和宏的名称都是单个字符。这样做的原因有三个。首先,它使解析非常简单(在 16 位环境中很重要)。其次,选项数量不多,因此不难想出助记符名称。第三,单个字符约定已在命令行标志中建立。
类似地,重写规则集最初是编号而不是命名。对于少量规则集来说,这也许可以忍受,但随着规则集数量的增长,为其提供更多助记符名称变得很重要。
随着 Sendmail 操作的环境变得越来越复杂,以及 16 位环境逐渐消失,对更丰富的配置语言的需求变得越来越明显。幸运的是,可以以向后兼容的方式进行这些更改。这些更改极大地提高了配置文件的可理解性。
编写 Sendmail 时,邮件系统在很大程度上与操作系统的其余部分隔离。有一些服务需要集成,例如 /etc/passwd
和 /etc/hosts
文件。服务切换尚未发明,目录服务不存在,配置规模很小且由人工维护。
这种情况很快发生了变化。第一个新增功能之一是 DNS。虽然系统主机查找抽象 (gethostbyname
) 可用于查找 IP 地址,但电子邮件必须使用其他查询,例如 MX。后来,IDA Sendmail 包含了使用 dbm(3) 文件的外部数据库查找功能。Sendmail 8 将其更新为一个通用映射服务,该服务允许使用其他数据库类型,包括外部数据库和无法使用重写完成的内部转换(例如,取消引用地址)。
如今,电子邮件系统依赖于许多外部服务,这些服务通常并非专门为电子邮件的独占使用而设计的。这使得 Sendmail 的代码走向了更多抽象。这也使得维护邮件系统变得更加困难,因为添加了更多“活动部件”。
Sendmail 是在一个如今看来完全陌生的世界中开发的。早期网络上的用户群体大多是研究人员,尽管存在有时恶劣的学术政治,但他们相对来说是良性的。Sendmail 反映了它创建的世界,非常重视尽可能可靠地传递邮件,即使面对用户错误也是如此。
今天的世界要敌对得多。绝大多数电子邮件都是恶意的。MTA 的目标已从传递邮件转变为阻止不良邮件。过滤可能是当今任何 MTA 的首要任务。这需要对 Sendmail 进行一些更改。
例如,添加了许多规则集以允许检查传入 SMTP 命令的参数,以便尽早发现问题。在读取信封时拒绝邮件比在承诺读取整封邮件后拒绝邮件要便宜得多,甚至比在接受邮件进行投递后拒绝邮件要便宜得多。在早期,过滤通常是通过接受邮件、将其传递给过滤器程序,然后如果邮件通过则将其发送到另一个 Sendmail 实例来完成(所谓的“三明治”配置)。这在当今世界中实在太昂贵了。
同样,Sendmail 也从一个相当普通的 TCP/IP 连接消费者变成了更加复杂的角色,例如“窥视”网络输入以查看发送方是否在确认上一个命令之前就发送了命令。这打破了一些以前旨在使 Sendmail 适应多种网络类型的抽象。例如,如今,将 Sendmail 连接到 XNS 或 DECnet 网络需要大量的工作,因为 TCP/IP 的知识已经内置到大量代码中。
添加了许多配置功能来解决充满敌意的世界,例如对访问表、实时黑洞列表、地址收集缓解、拒绝服务保护和垃圾邮件过滤的支持。这极大地复杂了配置邮件系统的任务,但适应当今世界绝对是必要的。
多年来出现了许多新的标准,这些标准需要对 Sendmail 进行重大更改。例如,添加 TLS(加密)需要对大量代码进行重大更改。SMTP 管道需要深入到低级 TCP/IP 流中以避免死锁。添加提交端口 (587) 需要能够监听多个传入端口,包括根据到达端口的不同而具有不同的行为。
其他压力是由情况而不是标准造成的。例如,添加 Milter 接口是对垃圾邮件的直接回应。虽然 Milter 不是已发布的标准,但它是一项主要的新技术。
在所有情况下,这些更改都以某种方式增强了邮件系统,无论是提高安全性、改善性能还是添加新功能。但是,它们都伴随着成本,在几乎所有情况下,都会使代码库和配置文件都变得更加复杂。
事后诸葛亮总是容易。今天看来,有很多事情我都会做得不同。有些当时是无法预见的(例如,垃圾邮件如何改变我们对电子邮件的认知,现代工具集会是什么样子等等),有些则是显而易见的。还有一些只是在编写 sendmail 的过程中,我学到了很多关于电子邮件、关于 TCP/IP 以及关于编程本身的知识——每个人在编写代码的过程中都会成长。
但也有很多事情我会保持不变,其中一些与标准智慧相矛盾。
也许我对 sendmail 最大的错误是在一开始没有认识到它将变得多么重要。我曾多次有机会将世界引导到正确的方向,但都没有抓住;事实上,在某些情况下我造成了损害,例如,当 sendmail 应该对错误输入进行更严格的检查时,我没有这样做。同样,我相当早地意识到配置文件语法需要改进,当时可能只有几百个 sendmail 实例部署,但我决定不进行更改,因为我不想给已安装的用户带来不必要的麻烦。回想起来,最好是尽早改进并造成暂时的麻烦,以便产生更好的长期结果。
一个例子就是 Version 7 邮箱如何分隔邮件。它们使用以“From␣”(其中“␣”表示 ASCII 空格字符,0x20)开头的行来分隔邮件。如果收到的邮件包含在行首出现“From␣”这个词,本地邮箱软件会将其转换为“>From␣”。某些(但不是所有)系统上的一个改进是要求前面有一空行,但这并不可靠。直到今天,“>From”仍然出现在极其意想不到的地方,这些地方显然与电子邮件无关(但显然曾经被电子邮件处理过)。回想起来,我可能应该将 BSD 邮件系统转换为使用新的语法。当时我肯定会遭到痛骂,但我可以为世界省去很多麻烦。
也许我在配置文件语法中犯的最大错误是在重写规则中使用制表符(HT,0x09)来分隔模式和替换内容。当时我是在模仿 make,只是几年后才了解到 make 的作者 Stuart Feldman 认为那是他犯过的最大错误之一。除了在屏幕上查看配置时不明显之外,制表符在大多数窗口系统中的剪切粘贴中都不会保留。
虽然我相信重写规则是正确的想法(见下文),但我将更改配置文件的总体结构。例如,我没有预料到配置中需要层次结构(例如,针对不同的 SMTP 监听端口设置不同的选项)。在设计配置文件时,没有“标准”格式。今天,我会倾向于使用 Apache 风格的配置——它简洁、整洁且具有足够的表达能力——或者甚至嵌入 Lua 之类的语言。
在开发 sendmail 时,地址空间很小,协议也还在不断变化。将尽可能多的内容放入配置文件似乎是个好主意。今天看来,这似乎是个错误:我们有足够的地址空间(对于 MTA)并且标准相当稳定。此外,“配置文件”的一部分实际上是需要在新的版本中更新的代码。.mc
配置文件解决了这个问题,但每次更新软件时都必须重建配置很麻烦。一个简单的解决方案就是让 sendmail 读取两个配置文件,一个隐藏的与每个新软件版本一起安装,另一个公开的用于本地配置。
今天有很多新的工具可用——例如,用于配置和构建软件。如果需要,工具可以成为很好的杠杆,但它们也可能过度使用,使系统难以理解。例如,当只需要 strtok(3) 时,你永远不应该使用 yacc(1) 语法。但重新发明轮子也不是一个好主意。特别是,尽管有一些保留意见,但我今天几乎肯定会使用 autoconf。
事后看来,并且知道 sendmail 变得多么普遍,我不会在开发的早期阶段过于担心破坏现有的安装。当现有实践严重错误时,应该修复它,而不是迁就它。也就是说,我仍然不会对所有邮件格式进行严格检查;一些问题可以轻松安全地忽略或修补。例如,我可能会在没有 Message-Id:
标头字段的邮件中插入一个,但我更有可能拒绝没有 From:
标头字段的邮件,而不是尝试从信封中的信息创建一个。
有一些内部抽象我不会再尝试,还有一些我会添加。例如,我不会使用以 null 结尾的字符串,而是选择使用长度/值对,尽管这意味着标准 C 库的许多部分将难以使用。仅此一项的安全隐患就使其值得这样做。相反,我不会尝试在 C 中构建异常处理,但我会创建一个一致的状态码系统,该系统将在整个代码中使用,而不是让例程返回 null
、false
或负数来表示错误。
我肯定会将邮箱名称的概念从 Unix 用户 ID 中抽象出来。在我编写 sendmail 时,模型是您只向 Unix 用户发送邮件。今天,这种情况几乎从未发生过;即使在使用该模型的系统上,也有一些系统帐户永远不应该收到电子邮件。
当然,有些事情确实运行良好……
sendmail 的一个成功的副项目是 syslog。在编写 sendmail 时,需要记录的程序会写入一个特定的文件。这些文件散布在整个文件系统中。Syslog 当时很难编写(UDP 当时还不存在,所以我使用了名为 mpx 文件的东西),但非常值得。但是,我会进行一项具体的更改:我会更加注意使记录消息的语法能够被机器解析——本质上,我没有预见到日志监控的存在。
重写规则一直备受争议,但我还会再次使用它们(尽管可能不会像现在这样用于很多事情)。使用制表符是一个明显的错误,但考虑到 ASCII 的限制和电子邮件地址的语法,可能需要一些转义字符7。总的来说,使用模式替换范例的概念运行良好且非常灵活。
尽管我在上面评论说我会使用更多现有的工具,但我还是不愿意使用今天可用的许多运行时库。在我看来,其中太多库都过于臃肿,以至于很危险。应该谨慎选择库,权衡重用的优点与使用过于强大的工具来解决简单问题的弊端。我将避免的一个特定工具是 XML,至少作为配置语言。我相信语法对于它所用于的大部分内容来说过于繁琐。XML 有其用途,但今天被过度使用了。
有些人建议更自然的实现语言是 Java 或 C++。尽管 C 存在众所周知的问题,但我仍然会将它用作我的实现语言。部分原因是个人喜好:我比 Java 或 C++ 更了解 C。但我也对大多数面向对象语言对内存分配采取的漫不经心的态度感到失望。分配内存存在许多难以描述的性能问题。Sendmail 在适当的地方内部使用面向对象的概念(例如,map 类别的实现),但在我看来,完全转向面向对象是浪费且限制过多的。
sendmail MTA 诞生在一个动荡不安的世界中,有点像“狂野西部”,当时电子邮件是临时性的,当前的邮件标准尚未制定。在随后的 31 年中,“电子邮件问题”已经从仅仅可靠地工作转变为处理大型邮件和繁重负载,再到保护站点免受垃圾邮件和病毒的侵害,最后到今天被用作大量基于电子邮件的应用程序的平台。Sendmail 已经发展成为一个工作马,即使电子邮件已经从纯文本的人际通信发展成为基础设施中基于多媒体的关键任务的一部分,它也受到即使是最厌恶风险的公司所接受。
成功的原因并不总是显而易见的。构建一个能够在快速变化的世界中生存甚至蓬勃发展,并且只有少数兼职开发人员的程序,是无法使用传统的软件开发方法完成的。我希望我已经提供了一些关于 sendmail 如何成功的见解。
http://ftp.isc.org/www/survey/reports/2011/01/
http://www.sendmail.com
http://doc.cat-v.org/bell_labs/upas_mail_system/upas.pdf
http://milter.org
http://postfix.org