Sendmail 翻译

原文地址:http://www.aosabook.org/en/sendmail.html

译者邮箱:wdyxhk@hotmail.com

序言

大多数人认为电子邮件是他们与之互动的的邮件客户端程序,学术上被称为邮件用户代理(MUA),但是电子邮件系统还有另外一个重要组成部分负责将邮件从发送者手中发送到收件人手中,称之为邮件传输代理(MTA)。在互联网上最先被使用的MTA,也是至今为止使用最为广泛的MTA就是sendmail。

sendmail在因特网存在之前就已经被首先创造出来了。在一开始互联网只是有几百台主机的学术实验衍生物的时候,sendmail的杰出并不显著,到了今天,在互联网拥有8亿台主机的2011年1月,sendmail的成功非比寻常。Senamail至今仍然是互联网上使用最为广泛的网络协议。

17.1 很久很久之前

sendmail的初版于1980年写成。它最早用来快速截取在不同网络间传递的信息。在那时,互联网正处于发展时期而功能尚不完善。事实上,许多网络在计划发展时并没有统一的特征。阿帕网首先在美国使用,而互联网被设计为阿帕网的某种形式的升级。但是当时欧洲政府认为OSI的发展更为重要,在当时短时间内OSI看起来确实更加引人关注。OSI和sendmail都依靠从电话公司租赁线路来实现。当时的美国电话线路的速度为56kbps。

如果依据连接起来的电脑和用户的数目来说,UUCP网络是当时最为成功的网络类型。UUCP不同寻常之处在于在UUCP网络中绝对不存在中央主机这样的概念。从某种意义上来说,UUCP是一种通过电话线路传输的原始的点到点网络。一般来说9600bps大概已经是最快的传输速度了。最快的网络是由施乐公司开发的运行XNS协议的以太网(3 Mbps),但是该以太网缺少了本地客户端就无法运行。

当时的网络环境与现在的网络环境天差地别。电脑之间的区别度很大,甚至到了不是所有的电脑都支持8-bit字节的程度。这其中包括 PDP-10 (36 bit words, 9 bit bytes), PDP-11 (16 bit words, 8 bit bytes),CDC 6000 系列 (60 bit words, 6 bit characters),IBM 360 (32 bit words, 8 bit bytes),XDS 940, ICL 470 和Sigma 7。当时贝尔实验室开发的Unix是一个非常具有发展潜力的平台。大多数基于Unix的机器都拥有16位的地址空间,当时PDP-11是最主要的Unix机,而Data General 8/32和VAX-11/780才刚刚出现。线程的概念还不存在,事实上,当时动态过程的概念也才刚刚出现(Unix使用了动态过程的思想,但是一些例如IBM’s OS/360的“古典”的系统还未使用)。Unix内核并不支持文件锁(但当时的陷阱触发机制使用了文件链接)。

从出现开始,网络大多处于低速(许多基于9600波特的TTY线路,真正富有的人才用得起以太网,而且也只能本地使用),令人敬佩的发明套接字技术当时还没有发明出来,公共密钥加密技术同样如此,所以大多数我们今天已知网络安全技术在当时都不适用。

当时Unix上已经网络邮件已经出现,但是只是被创造用来截取报文。当时最初的用户代理只是形如 /bin/mail的命令行(到今天也有可能是binmail或者v7nail),但是也有些网站使用一些其他的代理,比如Berkeley制作的Mail,你呢个够真正把报文看作是独立的个体而非一整块程序.每种用户代理都会直接读取(常常也会写入) /user/spool/mail,没有怎样将信件真正存储起来的概念。

当时区别于本地邮件而发送邮件仅仅依靠判断地址是否包含一个感叹号或者冒号。使用阿帕网的用户不得不使用一个完全自成一体,与网络毫无交互的邮件程序,甚至还会将邮件以不同形式储存在不同位置。

更有趣的是,邮件本身事实上对数据格式并没有统一的标准。只有一个大致的约定,在邮件开头应该有一块标题字段,每一块标题字段应该另起一行,其中标题和内容应该用冒号分割开。不止如此,对于标题字段名字的选择或者是各个子块的语法都几乎没有固定的标准。比如说,有一些邮件系统Subj:取代Subject:,Date:域使用不同的语法规则,还有一些邮件系统并不结合搜From:域内的全名。更夸张的是,什么是documented这条定义总是模糊不清,在实际使用中也常常没有按照概念。尤其是RFC 733(声称定义了阿帕网报文的格式)在实际使用中在一些细节处与它的定义并不相符。造成的结果就是在邮件系统使用过程中遵照的规则有几分玄学的感觉。

在1979年, the INGRES Relational Database Management Project 计划获得了美国国防部的资助,让我们的PDP-11获得了一条带宽9600dps的阿帕网连线。在当时这是在计算机科学领域唯一一条可用的阿帕网接入口。所以每个人都想获取连接我们主机的权限来连接到阿帕网。然而,这台机器早就满载了,所以我们只能提供2个登录端口供整个机构的人分享,这造成网络环境中出现了大量的争夺和冲突。然而,我注意到大家普遍需要的不是远程接入或者文件传输,而是电子邮件。

介于这个发现,sendmail(起初叫做delivermail)作为解决网络冲突和混乱的措施出现了。每一个MUA只会调用delivermail程序来发送邮件而不是自己特地去解决如何发送邮件的问题。delivermail/sendmail并没有解决本地如何存储或者发送邮件的问题,它只是从别的程序中将发送邮件的任务提取出来。(当SMTP出现的时候这些都发生了改变,我们很快会谈及这个)。某种意义上来说它只是将不同的邮件系统整合起来而不是本身成为一个邮件系统。

在sendmail发展过程中,阿帕网也发展为互联网。变化跨度如此之大,从底层传输的数据包到应用协议。sendmail随着网络发展而发展,并且从某些意义上也影响了网络的发展。值得一提的是,sendmail存活了下来并且和网络一起从几百台主机发展到几亿台主机。

17.2 设计原则

在开发sendmail的时候,我坚持了一些设计原则。所有设计原则在某些程度上而言可以归纳为一句话:做的越少,做的越好。这与当时致力于更加广泛的目标用途和对更加宏大的计划的需求形成了鲜明的对比。

17.2.1 接收一个程序员精力终归有限的事实

我编写sendmail的工作是作为一种无偿的副业,目的是为了是使阿帕网得邮件系统能够更快让加州大学伯克利分校的师生正常使用。这其中的关键就在于让邮件在当前存在的网络之中传播,而这些网络本身并不能识别到有其他的网络存在。改进这样一个不只是并不算小规模的软件仅仅把它当做一种副业是不可行的。设计过程中必须最小化需要改进的已有代码和需要添加的新代码。这个原则驱动形成了余下的设计原则。自从这样的原则被制定之后,在大多数情况下即使有一个更大的工作团队,做的越少,做的越好的原则依旧是正确的。

17.2.2 不要重新设计用户代理

一个MUA(邮件用户代理)是用户所能感知到的最终端的“邮件系统”,他们认为这是一个用来读写和回复邮件的程序。这与用来将邮件按照路径从发送方发送到接收方的邮件传输代理(MTA)截然不同。在编写sendmail的时候,许多实现方式是至少部分地将这两个功能连接起来,所以他们通常是一前一后发展的。试图同时开发这两个功能会使工作量过大,所以sendmail完全舍弃了优化用户端口对的问题,对于MUA的唯一改变在于让他们发送邮件不再自己规划路径而是调用sendmail来完成。尤其是当时已经几种MUA并且人们对于他们与邮件系统的交互往往存在不确定性。试图致力于解决两个方面的问题任务量过大。这种将MUA与MTA分隔开考虑的思路在现在已经被大多数人接受,而在当时却是突破常规的。

17.2.3 不要重新设计本地邮件的存储方式

本地邮件存储(用来存放接收到的邮件直到收件人来阅读邮件为止)并没有被系统规范过形式。一些网站喜欢将这些邮件存储在计算机的核心位置,比如/usr/mail, /var/mail或者 /var/spool/mail。还有一些网站喜欢将邮件储存在收件人的根目录(比如新建一个名为mail的文件夹)。大多数网站将邮件用“From”后加一串字符作为邮件的起始行(非常差劲的决定,不过在当时成为了一种约定俗成的规则)。但是专注于阿帕网的网站用4个control-A字符作为第一行的开头。还有一些网站致力于将收件箱锁起来来避免冲突,但他们往往使用的是不同的规则约束(当时文件锁定原语还不可用)。简而言之,当时唯一合理的方案是将本地邮件存储结构作为黑盒子。

几乎所有网站都会把实现本体邮件存储的实际原理呈现在 /bin/mail 程序中。在该程序中整合了一个(相当原始的)用户接口,路由功能和存储功能。为了和sendmail结合,路由功能被去除,取而代之的是对sendmail的调用。加入了一个 -d 标识强制进行最后的发件,等等之类的措施来防止 /bin/mail 直接进行路由。近几年这段代码被提取出来加入到 mail.local 程序中来
将邮件发送到物理邮箱之中。而 /bin/mail 程序保存到今天仅仅用来作为最底层的结构来发送邮件。

17.2.4 让sendmail适应这个世界,而不是反过来

UUCP和BerkNET一类的协议已经被当做单独的程序运行,有专属的略显古怪的命令行结构。在某些情况下他们与sendmail共同得到发展。重现这些协议功能(比如将他们转变为标准的调用公约)将是令人痛苦的。这直接导致设计sendmail的原则在于将sendmail适应这个世界而不是反其道而行。

17.2.5 尽可能不做改变

在研发sendmail的过程中我尽最大的可能尽可能少碰我不需要的东西。另外我也没时间去做额外的事,在当时伯克利分校有这样一种校园文化,反对固有的代码所有权定义从而支持这样一种论调“谁最后一个接触代码谁就对这段代码负责”(简而言之,谁碰了它,谁就拥有它)。尽管这听起来和现代标准格格不入,在当时没有人全心全意研发Unix的伯克利确着实奏效了,每个人只负责他感兴趣的部分并且通常情况下不去碰别的地方。

17.2.6 提前考虑可靠性

在sendmail之前的邮件系统(包括大部分邮件传输系统)并没有着重关注可靠性问题。举个例子,4.2BSD版本之前Unix系统没有文件锁机制,尽管这可以通过创建一个临时文件并链接到一个上锁文件模拟(如果上锁文件已存在就会失败)。然而,有时不同的程序构造出的相同数据文件在上锁方式上并未达成一致(他们可能给上锁文件不同的命名,也可能根本不上锁),所以丢失文件不足为奇。sendmail则采取措施防止邮件丢失(可能由于我的专业背景与丢失数据就是大错特错的数据库有关)。

17.2.7 考虑哪些内容可以暂时抛弃

在早期版本中很多内容没有实现。我没有试图去重构整个邮件系统或者建立一个通用的解决方案,功能可以再需求出现时再添加。非常早期的版本离开了源代码和编译器甚至都不是可配置的(虽然这很早就修正了)。总的来说,sendmail的运作原理是尽可能快速地运行功能然后再根据需要添加工作代码来让问题更好地得到解决。

17.3 发展历程

和很多历时长久的软件一样,sendmail是一步一步发展的,每个发展阶段都有自己的基本主题和用意。

17.3.1 第一阶段:delivermail

sendmail的第一个实例被称作delivermail。它简约而不简单。它的基本工作是将意见从一个程序运送到另一个程序。值得一提的是,deliverman不支持SMTP,所以它从不进行任何直接的网络连接。由于每一个网络本身已经拥有队列,所以它并不需要队列系统,这个程序更像是一个纵横开关。既然delivermail不直接支持网络协议,那就不需要将它作为后台程序运行,它只需要在每条报文提交的时候调用,将报文发送到能够进行下一步传递的程序就可以结束使命。同理,delivermail也不需要为了匹配发送的目标网络而重写报文头部。但这通常会导致发送的邮件没有得到回应。这样的情况甚至严重到有人专门写了一本书来讨论电子邮件(叫做《 fittingly, !%@:: A Directory of Electronic Mail Addressing & Networks [AF94]》)。
所有dilivermail中的配置是内置的,只对应每个地址中特定的字符。这些字符具有优先级。比如说,搜寻主机配置时可能会直接1搜索“@”符号,一旦找到,就将整条主机地址发送到指定的阿帕网中继主机。不然的话,程序会继续搜索冒号,一旦找到就将报文发送到指定主机指定用户的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位地址空间和有限的内存空间中使用运行时配置太奢侈,其次,当时的系统高度客制化以至于重新编译更好,只要你本地有函数库(Unix6代时共享库还不存在)。

Divermail分布在BSD4.0和BSD4.1(加州大学伯克利分校软件)之中,并且超出意料的成功,伯克利分校早就不是仅有的混合网络架构的使用者。显然,还有很多工作等着去做。

17.3.2 第二阶段:sendmail3,4和5

sendmail第一版和第二版都以delivermail的名字传播使用,而从1981年3月研发的第三版开始,程序将会使用sendmail的名字。在当时16位机PDP-11仍被广泛使用,但是32位机VAX-11正在越来越受欢迎,因此许多原本地址空间过小形成的限制正在越来越变得宽松。

sendmail的最初的目的是将程序转变为运行时可配置的程序,允许报文修正来为邮件在不同网络之间传播提供兼容性,对于决定路由也可以有更丰富的语言约束。当时许多专业系统使用的技术本质上是对地址的重写(基于符号而非字符串)。不但有专案法去提取和保存任何在括号里的评论字符串,同时在地址重写完成之后将这些字符串重新插入。能够增加或者增大头区域(例如,添加一块Date头区域或者在From头区域中的发件人的全名包括进来,如果已知的话)。

SMTP于1981年开始发展。在伯克利大学的计算机科学研究组(CSRG)获得了美国国防部高级研究计划局(DAPRA)研发一个用于支持DAPRA资金调研的基于Unix的平台的合同,意图让工程之间的分享更加容易。对于TCP/IP栈的工作那时刚刚完成,尽管套接字技术当时还在不断变化。像Telnet和FTP这样的底层应用协议已经形成,但是SMTP尚未完全成型。事实上,SMTP协议在当时还未成定案,当时关于如何发送邮件的创造性协议MTP还存在巨大争议。随着争议的持续,MTP变得越来越复杂,直到SMTP或多或少的开始被许可设计(但是知道1982年才被正式出版)。我正式为安格尔关系数据库管理系统工作,但是因为当时在伯克利我比任何人都熟悉邮件系统,我只得说道完善SMTP的是。

我最初的想法是创建一个独立的拥有自己队列系统和后台程序的SMTP邮件程序,子程序会依附于sendmail来获得路由。然而,SMTP的几个特征使得这个想法存在一些问题。举个例子,EXPN和VRFY命令需要解析,混叠和本地地址验证的权限。而且,当时我认为重要的一点是,RCFT命令如果地址未知应该立即return而不是接收报文然后返回一个发送错误。这在之后被证明是有先见之明的。讽刺的是,之后服务器常常因此出错,使得垃圾邮件的背向散射问题更加恶化。这个问题促使我下决心将SMTP囊括进sendmail本身。

Sendmail3版本分布在由4.1a到4.1cBSD之间(beta版本),sendmail内嵌在4.2BSD,sendmail则5内嵌在4.3BSD。

17.3.3 第三阶段:混乱的那几年

在我离开伯克利到一家新创立的公司之后,我可供自由支配的时间显著的变少了。但是因特网却开始真正的爆炸式发展,sendmail在一个复杂和崭新的环境中被使用。大多数Unix系统供应商(尤其是Sun,DEC和IBM)长砸出了他们自己版本的sendmail并且彼此之间互不相容。当时也有人致力于开发出开源版本,尤其是IDA sendmail和KJS。

IDA sendmail由林雪平大学创建,IDA包括拓展来使之更加容易在更大的环境和完全不同的系统中安装和工作。一个最主要的特征是IDA包含dbm(3)数据结构图用来支持高度动态的网站。在配置文件中使用一种新的语法是可行的,并且在外部语法中加入包括发送和接受地址结构图的函数功能(例如用地址john_doe@example.com取代johnd@example)并路由。

King James Sendmail(KJS,由PaulVixie创建)是一次对统一所有已经出现的sendmail版本的尝试。不幸的是,并没有获得足够的支持来取得预期效果。这个时代也被大量的新技术驱动发展,这一切在邮件系统的发展过程都有所体现中。例如,Sun创建无盘集群时增加了YP(黄页,后来的NIS,即网络信息服务)目录服务和NFS,即网络文件系统(the Network File System)。特别是,YP不得不对sendmail来说可见,因为邮件别名存储在YP而不是本地文件。

17.3.4 第四阶段:sendmail 8

几年之后,我作为一名工作人员回到了伯克利。我的工作是为计算机科学系的研究管理一组安装和支持共享的基础设施。为了成功完成任务,个人研究小组的主要临时环境需要专案法。就像早期的互联网,不同的研究组在完全不同的平台上工作,其中一些平台很老旧。总的来说,每个研究小组运行自己的系统,尽管有些平台仍然管理完善,他们中的大多数还是因为“拖延维护”而深受困扰。

在大多数情况下,电子邮件有相似的分段。每个人的邮件地址都是“person@host.berkeley.ed”,这个地址的host是他们办公室中或者他们使用的共享服务器上的工作站的名字(校园网甚至没有内部子域),除了一些特殊的以@berkeley.edu结尾的地址。其目标是交换至内部的子域(因此所有个人主机地址会在cs.berkeley.edu子域中)和形成一个统一的邮件系统(所以每个人都会有一个@cs.berkeley.edu结尾的地址)。通过创造一个全新版本的能在全系使用的sendmail使得这个目标轻松地实现。

我开始研究许多已经变得流行的变种sendmail。我的意图不是从不同的代码库开始写起,而是了解其他变异版所发现的有用的功能。许多这些想法能够在sendmail 8中找到自己的影子,通常情况下这些点子被进行了修饰来使得相似的想法合并或者让它们变得更通用。例如,几个版本的sendmail都能够获取内部数据库的权限,如 dbm(3)或NIS;sendmail 8那他们合并为一个能够处理多类型数据库(或者任意的无数据库转换)的“映射图”机制。类似的也包含了来自IDA sendmail的“泛型”数据库(内部到外部的名称映射)。

sendmail 8还包括一个使用m4(1)宏处理器的新的配置文件包。目的是比sendmail 5的已经大部分嵌入程序的配置文件包更加的陈述化。也就是说,sendmail 5配置文件包本质上需要管理员手动配置整个配置文件,这真的仅仅将m4的“include”结构作为一种速记。sendmail 8配置文件允许管理员来声明需要哪些特性,邮件相关地址等,然后m4就会展开最终的配置文件。

17.7节的大部分内容论述了sendmail 8中的增强性功能。

17.3.5 第五阶段:投入商业化的那几年

随着互联网的发展,使用sendmail的网站数量激增,支持前所未有的用户量成为了大问题。一段时间内,我可以设置一组通过电子邮件和新闻组提供无偿帮助的志愿者(非正式地讲叫做“Sendmail联盟”,又名sendmail.org)来继续维护sendmail系统。但在上世纪90年代末,已经安装sendmail的基础设施已经发展到几乎不可能基于志愿者帮助解决的程度。为了得到新的资源来维护这些代码,我和一个更有商业头脑的朋友一起创建了sendmail,Inc.。

虽然最开始商业产品大部分基于配置和管理工具,但是许多新的功能被添加到开源MTA来支持商业发展的需要。值得一提的是,公司新增了对TLS(连接加密)、SMTP认证、网站安全增强的支持——例如拒绝服务保护,最重要的是邮件过滤插件(Milter接口的问题接下来会讨论)。

在写这篇文章时,sendmail商业产品已拓展到包括一整套基于电子邮件的应用,几乎所有应用都是建立在公司的开始几年对sendmail的功能扩展的基础上。

17.3.6 到底sendmail 6和sendmail 7发生了什么

sendmail 6本质上是sendmail 8的beta版本,从未正式发布过但确实被广泛对的传播和应用。Sendmail 7从未存在过,sendmail直接跳到了版本8是因为所有其他BSD的源文件在BSD4.4于1993年6月发布的时候都碰巧是版本8。

17.4 设计决策

sendmail有正确的设计决策,也有开始是正确的,却因为世界的不断变化而成为错误的设计决策,有些至今不能判断是对是错。

17.4.1 配置文件的语法

配置文件的语法由两个因素决定。首先整个应用需要适配于16位的地址空间。所以分析程序需要足够小。除此之外,早期的配置应该要尽量少(长度在一页之内)。这样,即使语法是模糊的,文件仍然是可理解的。然而,随着时间流逝,越来越多的操作设定从C语言代码中转移到配置文件中,使得文件开始不断变大。因此,配置文件被认为是晦涩难懂的。对于很多人来说,选择Tab字符作为一个活跃的语法条目就已经是一不能理解的了。当时如果从别的系统中拷贝内容就会发生错误,尤其是make文件,当窗口系统投入使用时,这个问题变得更加严峻(因为剪切粘贴命令通常不会保存Tab)。

回顾到文件开始变大,32位机开始流行的时候,讲道理是时候开始重新考虑语法问题了。曾经有段时间我心里是这样想的,但是却以为内不想破坏“庞大”的安装基础(当时可能只有几百台机器)而决定不这样做。现在看来这是个错误,我当时并没有意识到安装基础会发展到这么大,也没有意识到如果早些改变语法会让我省下多大的力。并且,当标准固定下来之后,不少普遍设置可以重新放回C语言源代码中,从而简化配置文件。

特别要注意的是,怎么把更多功能搬进配置文件里。在我改进sendmail的同时,SMTP协议也在不断发展。通过将操作决策搬进配置文件,我能够在24小时之内马上设计出相应改变。我认为这改进了SMTP的标准,因为这让通过制定的设计改变快速获取操作经验成为了可能,代价仅仅是是的配置问价更加难以理解。

17.4.2 重写规则

编写sendmail的困难之一在于怎样为了在不违背接受网络的标准的情况下允许报文在不同网络间传递而进行必要的重写。转换过程中需要改变通配符(比如BerkNET使用冒号作为分割,然而在SMTP网络中这是不合法的),重排地址组成,加入或者删除组成部分等等、比如,下面的重写在某些环境下是必要的

Form To
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尝试过了,应用在UNIX8的邮件系统上,但并没有流行起来。)取而代之地,我认为一个扫描的过程是必要的,可以产生类似正则表达式中的字符以供操作。使用一个单独的参数来描述操作符,既作为标记也作为分隔符姐足够了。空格用来分隔符号但本身并不是语法单元。重写规则只是符号的替换与匹配来适应子程序。

比起许多需要避开才能不影响的通配符(比如在正则表达式内的影响),我选择使用一个“escape”符号来联系原始符号和通配符(比如匹配任意一个单词)。原始的Unix路径使用的是反斜杠,但是反斜杠在一些地址语法中已经被应用为引用符号。结果证明,“$”是为数不多的尚未被其他语法使用的字符之一。

讽刺的是,一开始的几个错误的决策之一,就是怎样使用空格。就像大部分扫描输入一样,一个空格符是一个分隔符,所以在标识符之间被无限制地使用。然而,初始的分散的配置文件并没有使用空格,导致模式变得不必要的复杂难懂。考虑一下两个予以相同的模式之间的区别:

‘’$+ + $* @ $+ . $={mydomain}’’

‘‘$++$*@$+.$={mydomain}’’

17.4.3 为语法分析重写

有些人认为sendmail应该使用常规的基于语法的分析技术来分析地址而不是重写规则并且根据规则来修改地址。假如标准是用语法来定义地址,那表面上这种说法看起来有几分道理。重复使用规则重写的主要原因是在许多情况下有必要对标题区域的地址进行语法分析(举个例子,为了在来源网络并没有使用标准的封装的情况下提取出发送者的发送信息)。使用例如YACC的LALR(1)语法分析器和一个传统的扫描器是很难分析这样的地址的,原因在于先行逻辑的具体细节不明确。比如说,扫描这样的地址:allman@foo.bar.baz.comeric@example.com 需要扫描器或者分析器中使用先行逻辑,你不知道一开始的“allman@…”是地址,直到你看见了“<”。出于LALR(1)分析器只能向前分析一步的原因,所以你真能在扫描器中使用先行逻辑,而这本质上是问题复杂化了。既然重写规则已经能够任意步数回溯,就足够了。

第二个原因在于相比而言重写规则更能识别和修复损坏的输入。除此之外重写足以完成整个工作,而复用代码总是理智的。

关于重写规则,有一点与众不同:当在进行符号匹配时,标记化输入和模式是有用的。因此,同样的扫描器可以用于扫描输入和模式本身。这需要为不同的输入调用使用不同的符号类型表的扫描器。

17.4.4 在sendmail中加入SMTP和排队模块

实现外向SMTP(客户端)的一个“显而易见”方法就是像UUCP一样将它构建成一个外部邮箱收发器。但是事实已经证明了许多问题,例如排队功序列应该在设计在sendmail中还是SMTP模块中?如果把它设计在sendmail中,要么每份报文不得不被发送到各自的容器中(比如说,没有了“背负式装运”,那其中的的一条连结可能保持打开,使得多条’’RCRT’’指令被发出),要么需要一个远比可能使用的Unix退出代码长的回溯路径来传达必须的每个接收器的状态。如果排队序列实在客户端模块中完成,那么接下来就有大量内容需要复制,特别是在当时许多例如XNS的网络仍然具有竞争力。另外,将排队序列加入到sendmail模块中能提供一种更简洁的方式解决某些显著而暂时的问题,比方说资源枯竭。

传入SMTP(服务器)涉及到很多不同的决策。当时,我觉得原原本本地实现’’VRFY’’和’’EXPN’’指令很重要,这需要使用到别名机制。这会又一次的对比可能使用到的命令行和退出代码更多的用于SMTP服务器和sendmail之间的交换协议提出需求,事实上,就缺一个跟SMTP本身类似的协议。

时至今日,我会更倾向于选择把排队序列交给sendmail实现,但是把两端SMTP的实现放在其他过程中。一个理由是为了安全性:一旦服务器端获取了开放端口25的实例,那么就已经不再需要获取根权限。现代的拓展,例如TLS和DKIM,签订了复杂的客户端(自从非特权用户无法获取私人秘钥之后),但是严格意义上讲根权限获取仍然是不必要的。虽然安全问题仍然存在,但是如果SMTP客户端作为非根用户权限运行,没人可以访问私人秘钥,只有定义用户拥有特权,因此不应该直接与其他网站通信。所有这些问题都可以巧妙处理。

17.4.5 队列功能的实现

Sendmail遵循当时的惯例采用存储队列文件。事实上,采用的格式非常近似于当时的LPR子系统。每个需求都有两个文件,一个用来存储控制信息,另一个用来存放数据。控制文件是一个每一行首字符就能表示每一行含义的纯文本文件。

在sendmail需要处理队列时就需要阅读所有的控制文件,将相关信息存放在内存中然后对队列进行排序。在队列中只有有限数量的报文时,这样的机制确实有效,但是如果用这样的机制在面对上万个队列报文时就会失效。具体来说,如果目录索引大到可以直接指向文件系统中的的某一个文件时,会有一个与节约的时间处于同一数量级的瓶颈导致最终表现并没有得到优化。改善这个问题的一个可能的路径是使sendmail能偶处理复合队列,但那对黑客来说是最好的消息。

一个备用方案是将所有控制文件存储在一个数据库文件中。可是最终这个方案并没有成行,原因在于当sendmail开始代码编写的时候,市面上还没有普遍的数据库包可供使用,而当dbm(3)面世时又存在诸多瑕疵,包括无法开辟空间,需要所有的秘钥一起哈希后能存储在一个512字节大小的页面内和缺少锁机制。很长一段时间之内可靠的数据库包都没有出现。

另一方备用方案是维护一个守护进程来将队列状态保存在内存中,可能还需要记录日志以便恢复。考虑到当时相比而言较低的邮件收发量,大部分终端的内存缺乏,相对较高的后台维护需求和试试这样一个计划的复杂度,看起来在当时也不是一个好的折中计划。

顶一个涉及决策是讲报文头部存储在队列控制文件中而不是数据文件。基本原理在于大部分报文头部从发出网络到目的网络需要相当大的重写量(报文有时能够拥有多个目的地址,所以需要多次自定义),并且解析报文头的开销很高,所以将它们以预编译格式存储看起来是一条出路。现在回过头来想想这并不是一个好主意,就像以Unix标准格式(以换行符结尾)存储报文主体而不是以接受格式(使用换行符,回车/换行,空回车或者换行/回车)存储。随着email世界的不断发展和标准的不断采纳,重写的需求越来越小,甚至看起来无害的重写规则变得存在风险。

17.4.6 接受和修复虚假输入

自从sendmail在多重协议和不断变化的书写标准的环境中被创造出来之后,我决定去清理所有可能的畸形报文。这也符合于RFC 794.中明确提到的“Robustness原则”(也叫做Postel原则)。其中有一些改变是显然的,甚至是必须的:当一封UUCP报文被发送到阿帕网时,UUCP地址需要转化成阿帕网地址,要是允许“reply”命令正确运行,行终结符需要在不同平台使用的结束符之间转换。有一些不是那么显而易见的:如果一封接收到的报文不含有互联网规格要求的’’From:’’报文头,那么是选择添加’’From:’’报文头,还是通过这封没有’’From:’’报文头的报文,又或是拒收这封报文呢?在当时,我主要考虑的是回馈和互动,所以sendmail会修正报文,比如加上’’From:’’报文头。然而,这就要求其他破损的邮件系统在被修复过之后能够长时间的保持不变。

我确信我的决策在当时是正确的,但时至今日是存在问题的。高度的互动性对于让邮件畅通无阻的传播是非常重要的。如果当时我拒收非标准格式的报文,,那当时大部分邮件都会被拒收。如果我当时让他们通过检测,邮件容器将会接受到他们无法响应的邮件,甚至有时候都不能分辨出发件人——这样的邮件会被其他邮件系统拒收。

现如今标准已经被制定,对于其中的绝大多数模块来说这样的标准是精确而完整的。不再有大部分邮件被拒收的情况了,即使仍然有邮件软件发送算坏的邮件。这样的软件为互联网上的其他软件制造了不必要的麻烦。

17.4.7 配置和使用M4

在一段时间之内我既为sendmail配置文做常规的改进,又以个人名义为许多设备作支持。既然大量不同的终端之间的配置文件是一样的,使用一个工具来构建配置文件可取的。m4宏处理器包含在Unix之中。这是作为编程语言(特别是前端设计)。最重要的是,它有“包括”功能, 就像C语言中的“#include”。使用的原始配置文件拓展了此功能和一些小宏扩展。

IDA sendmail也使用m4,但使用的是截然不同的方式。现在回想起来,我可能应该更加细致地研究这些原型。他们包含许多好点子,尤其是在他们对引用处理的方式上。

从sendmail6开始,完全重写m4配置文件使得配置文件声明样式更多,体积更小。对于M4处理器的重度使用,使得GNU M4在引入一些微妙的语义变化时会产生问题。

原计划是M4配置将遵循80/20规则:他们会简单(大约20%的工作量),并将覆盖80%的病例。这个计划很快就瓦解了,主要有两个原因。较小的一个原因是它确实容易处理绝大多数的情况,至少在刚开始的时候。但随着sendmail和世界的发展,特别是包容的特性如TLS加密和SMTP认证的发展,M4配置越来越力不从心,但这些情况短时间之内并不会到来。

最重要的原因在于有一个问题愈加明显,原始配置文件对大多数人来说太难了处理。在本质上, ‘’.cf’’ (原始)格式已经成为组成代码的一部分——原则上可编辑,但实际上很不透明。“源代码”是一个脚本存储在m4上的’’.mc’’文件。

另一个重要的区别是,原始格式配置文件是一个编程语言,包含有过程代码 (规则集)、子程序调用、参数扩展和循环(但没有goto)。 语法是模糊的,但在许多方面很像’’sed’’和’’awk’’命令,在 至少在概念上。m4格式如下声明:尽管它可能下降到低层次的原始语言,但是在实践中这些细节对用户隐藏。

目前还不清楚,这个决定是正确的或不正确的。 我觉得当时(现在仍然觉得),复杂的系统可以有助于实现相当于一个领域特定语言(DSL),用于构建系统的某些部分。然而,让终端用户接触到DSL的配置方法作为解决方法基本上把所有配置一个系统的需要的努力汇集成一个编程问题。从中我们最终获得了强大的驱动力,但又维持在一个相对低的消耗。

17.5 其他注意事项

最后还有一些建设和发展sendmail的要点值得一提。

17.5.1 优化互联网规模

在大多数基于网络的系统中,客户端和服务器之间往往存在一种矛盾。一个对客户端来说有力的策略,对服务器端可能就是个错误,反之亦然。 例如,如果可能的话服务器会最小化其处理的过程,把尽可能多的操作推回到客户端,当然反过来客户端也是一样。 例如,一个服务器可能希望保持一个开放接口在做垃圾邮件处理拒绝消息时降低成本(最近很常见的一种情况),但是客户想尽可能加快这一过程。遍观整个系统,整体最优解可能就是平衡这两种需求。

有大量使用MTAs的实例明确支持客户端或服务器。 他们有能力这样做只是因为他们有一个相对较小的安装基础。当你的软件系统被用于互联网上重要的一部分时,你的设计必须平衡双方的之间的负载,所做的努力都是为了保持互联网这个整体。而由于总会有MTAs被曲解为另一个意思,例如,邮件系统只关心优化出口端。

当设计一个连接双方的系统,重要的是要避免有所偏颇。值得注意的是,这与其他互相对立客户端与服务器端的简历形成了鲜明对比,例如,web服务器和web客户端一般由不同的小组开发。

17.5.2 Milters

最重要的一个sendmail的附件是Milter界面(邮件过滤器)。Milter允许使用offboard插件(即单独运行的插件)用来处理邮件。这原本是为反垃圾邮件处理而设计。Milter协议与服务器同步运行SMTP协议。随着每一个新的SMTP命令从客户机接收到,sendmail调用Milter与以及相关指令信息。Milter有机会接受命令或发送一个拒绝指令, 拒绝该阶段的SMTP协议命令。Milter使用回调模型,所以当一个SMTP命令到达时会调用相应的Milter子例程。Milter是螺旋上升的,对每种情况都接入不同的环境来传递状态。

理论上milters可以在sendmail地址空间中作为可加载模块运行。我们拒绝这样做有三个原因。首先, 安全问题太重要:即使sendmail作为一个独一无二的的非根用户id运行,用户仍然拥有访问其他消息所有状态的权限。同样,不可避免会有一些Milter作者试图访问内部sendmail状态。

第二,我们想创建一个sendmail和Milter之间的防火墙:如果Milter停止工作,我们希望清楚弄清楚错误源头并且使邮件(仍有可能的情况下)继续传递。 第三,比起吧sendmail作为一个整体,Milter作者更容易调试一个独立的过程。

显然,这些Milter比反垃圾邮件处理有用得多。事实上,milter.org网站列出了反垃圾邮件、病毒、归档、内容监控、日志记录、交通影响,和许多其他类别,由商业生产公司和开放源码项目的Milter。后加的Mail6增加了使用相同的接口支持Milter。Milter已被证明是sendmail的一个伟大的成功。

17.5.3 发布时间表

有一个流行的“release early and often”和“release stable systems”学派之间的争论。sendmail在不同时期分别使用了这两种思想。在软件有极大变化时,我有时一天不止发布一个版本。 我的总体思想是每个变更后都会发布。这类似于提供公共访问源代码管理系统树。我个人喜欢发布版本时提供公共源代码树,至少在某种程度上是因为我使用一种现在看来被认为不能通过的方式管理源程序:对于大的改动,在我编写代码时,我会检查确认无功能快照。如果这棵树是共享的,我会对这些快照使用分支,但在任何情况下,他们是可用的,让世界看到并能创造相当大的混乱。同时,创建一个发布意味着绑定一个版本号,这使得它更容易追踪变化并生成一个错误报告。 当然,这要求发布的版本容易生成, 这并不总是现实的。

随着sendmail在更加关键的生产环境中被使用,问题开始产生。它并不总是那么容易区分变化,我想要的是让人们在现实状况下对我做的改动进行真正的测试。给版本添加“alpha”或“beta”标签缓解但不解决这个问题。 结果是,sendmail逐渐成熟,走向不那么频繁,但区别更大的版本发布。这成为当务之急,因为当sendmail成为一个商业产品,顾客想要的是最新和最伟大的而且稳定的版本,不会接受这两个不相容的版本。

开源开发人员需求和商品需求之间的紧张关系永远不会消失。尽早并且频繁发布版本有诸多好处,特别是潜在的巨大的观众中有不少勇敢的(有时是愚蠢的)测试人员对系统进行压力测试的方式,你几乎不可能指望从标准发展进程中有这样的体验。但它往往作为一个项目成功地变成一个产品(即使那个产品是开源和免费的), 比项目比起来,产品有不同的需求。

17.6 安全性

Sendmail发展历程和应用环境多变,但在安全性方面一直是保有理智的。其中一些措施的值得的,但有些不是,原因在于我们的“安全”的概念发生了变化。互联网开始的用户基础只有几千人,主要分布在学术和研究领域。当时的互联网在许多方面比我们今天知道的更友善、更温和。网络旨在鼓励分享,而不是构建防火墙(另一个在早期都未出现的概念)。现在的网络是一个危险的,充满敌意的地方,充满了垃圾邮件发送者和破快者。被越来越多的人描述为一个战区,在战区就有平民伤亡。

我们很难编写网络服务器时保证其安全,特别是当协议并不简单时。几乎所有的项目或多或少有一些小问题,即使常见的TCP/IP也被成功攻破。更高级别的实现语言证明防止攻击并没有灵丹妙药,甚至他们本身都创造出了一些弱点。必要的观察短语是“不信任所有输入”,不管它是从哪里来的,不信任的输入包括二次输入,例如DNS的输入和Milter的输入。像大多数早期的网络软件一样,sendmail的早期版本对输入显得太过信任。

但sendmail的最大问题是,早期版本运行时sendmail获取了根权限。为了打开SMTP监听套接字,读个别用户的转发信息,并提供个人用户的邮箱和家庭目录,根权限是必须的。然而,对今天的大多数系统,邮箱的概念名字已经脱离系统用户的概念,有效地消除了对根权限的需求,除了打开SMTP监听套接字仍然需要根权限之外。今天sendmail能够在过程连接之前放弃根权限,这么做消除了支持sendnail环境的担忧。值得注意的是,在这些不提供直接提供给用户信息的邮箱系统中,sendmial仍然可以在改变根目录的环境中运行,以便进一步与根权限隔离。

不幸的是,随着sendmail的安全性低已经广为人知的时候,它开始因为一些无关的问题受到指责。例如,一个系统管理员保持’’etc’’目录可写,然后当有人替换了’’/etc/passwd’’ 文件就开始职责sendmail。这样的事件让我们着手大幅加强安全性,包括明确检查sendmail访问的文件和目录的所有权和模式。甚至由于这么做太过苛刻,我们被迫在设计中包含一个’’DontBlameSendmail’’的可选项来控制这些检查是否开启。

还有其他方面的安全问题与保护程序的地址空间本身并不相关。例如,垃圾邮件的泛滥推动了收集地址的发展。SMTP中的’’VRFY’’和’’EXPN’’指令是专门设计来验证个人地址和扩大邮件列表的指令。这些都被垃圾邮件发送者严重滥用的命令,导致现在大多数网站禁止这样的指令。这是不幸的,至少对类似’’VRFY’’这样的命令而言,因为这些指令会被一些反垃圾邮件代理用于验证发送地址。

同样,反病毒保护一度被视为摆在台面上的一个问题,但是其重要性上升到任何商业级MTA都不得不开启防病毒检查的地步。其他现代设置与安全相关的需求包括强制加密敏感数据, 数据丢失保护和执法监管要求,例如,对于健康保险流通与责任法案(HIPPA)。

早期sendmail另一个着重注意的原则就是可靠性——每条消息要么被交付收件人要么回到发送方。但joe-jobs(攻击者伪造返回地址信息的邮件的手段,被许多人视为安全问题)导致很多网站不再创建回退邮件。如果可以确定问题所在,SMTP连接仍然是开放的,服务器可以以命令失败来报告这个问题,但SMTP连接关闭后,错误消息将解决默默地消失。公平地说, 今天的大多数合法的邮件都是单跳,所以问题信息仍会得到报告,但至少在原则上世界主流认了安全胜过可靠性。

17.7 Sendmail的演变

软件如果不能适应这个飞速变化的大环境,那一定是无法存活下来的。新的硬件技术的出现,推动了操作系统、资源库、软件框架和应用程序的发展变化。如果一个软件应用获得成功,它就被用于问题越来越多元化的环境。改变是不可避免的;你必须接受并拥抱变化。本节介绍了一些随着sendmail的演变而产生的重大变化。

17.7.1 配置变得更加详细精准

最初的sendmail配置很简洁。例如,选项和宏的名称都是单个字符。这主要出于三个原因。首先,它使解析变得非常简单(处在一个16位的环境之中,这一点很重要)。第二,除此之外选择也不多,所以不是很难想出助记名字。第三,单个字符公约已经确立为命令行标记。

同样,重写规则集最初用编号代替命名。最初也许容许少量的规则集,但是随着规则集规模扩大,规则集能有更多的助记符也变的更加重要。

随着sendmail运营的环境变得更加复杂,和16位环境消退,显然需要一种更丰富的配置语言。幸运的是,用向后兼容的方式实现这些变化让这种语言的出现变得可能。这些变化显著提高了配置文件的可理解性。

17.7.2 与其他子系统的连接更紧密:更广义的整合

在编写sendmail的时候,邮件时系统在很大程度上是孤立于操作系统的其余部分之外的。然而有一些服务需要集成环境才能运行就比如’’/etc/passwd’’和’’/etc/hosts’’文件。可开关服务还没有被发明出来,目录服务是不存在的,配置文件规模很小并且手动维护。

因此sendmail需要迅速改变。第一个改变的就是添加DNS的服务。虽然系统主机查找抽象(‘’gethostbyname’’)致力于查找IP地址,邮件必须使用例如MX的其他查询服务。之后,IDA sendmail使用dbm(3)文件来实现外部数据库查询功能。Sendmail 8更新加入了通用映射服务,允许其他数据库类型,包括外部数据库和仅靠重写无法做到的内部转换(如消除引用地址)。

现在电子邮件系统普遍依赖于许多并非电子邮件专用的外部服务。这使得sendmail在代码层面上更加抽象。而随着“活动部件”越来越多,维护电子邮件系统也变得更加举步维艰。

17.7.3 去适应一个带有敌意的世界

Sendmail在一个按现在标准看来似乎完全陌生的世界完成开发。早期网络上尽管有时有恶性的学术政治出现,但用户群体主要是相对良性的研究人员,。Sendmail反映了当时创建它的世界的特点,把重点放在邮件传递的可靠性,哪怕是需要用户错误。

当今世界更加充满敌意,绝大多数的电子邮件是恶意的。MTA的目标已从邮件保持递送变为去除恶意邮件。过滤邮件可能是现在任何MTA的优先事项,这就要求sendmail做出对应的变化。例如,许多规则集已经被添加用于允许检查对传入的SMTP命令行参数以尽早发现问题。阅读一条消息的信封时拒收邮件要比阅读整个邮件再拒收要大大节约开销,更不用说在发送回应报文时才拒收。在早期通常是过滤的过程是先接受信息,然后通过一个过滤程序,如果通过过滤再发送给另一个sendmail实例( 所谓的“三明治”结构)。这对当今世界的网络环境而言开销太大了。

同样,sendmail已经从一个相当普通TCP/IP消费者变为更加复杂的成分,通过类似于“窥视”网络输入发件人的行为来确认在之前的命令得到确认之前,当前命令是否已经被发出。这推翻了一些以前旨在令sendmail适应多种网络类型的设计。今天,它将分配相当大的工作量用于连接sendmail到XNS或数据网络,因为TCP/IP已建成的知识涉及到到那么多的代码。

许多配置功能被添加用于应对这个带有敌意的外部环境,如支持访问表、实时黑名单、地址获取缓解、拒绝服务保护和垃圾邮件过滤。这使得配置一个邮件系统的任务变得大大复杂,但是为了适应当今世界,这些都是必须的。

17.7.4 加入新技术

这些年出现了许多新的标准,使得sendmail需要做出显著的变化。例如,为了添加TLS (加密)服务需要做出的修改贯穿整个工程代码。SMTP流水线需要监视底层TCP/IP流以避免死锁。提交的端口(587) 需要监听多个输入端口的能力,包括取决于到达港口做出不同应对的能力。

其他压力被迫由环境而不是标准。例如,Milter界面是用于直接应对垃圾邮件的。尽管Milter不是出版的标准,但也是一个重要的新技术。

在不同情况下,这些变化从不同的方面为邮件系统增加了安全,更好的性能或新功能。然而,他们都需要开销,在几乎所有的情况下都使得代码和配置文件更加复杂。

17.8 如果放到现在我会怎么做?

不谈事后诸葛亮,如果放到现在编写sendmail,许多事情的处理方式都会有所不同。有些当时无法预知(比如说,垃圾邮件如何改变我们对电子邮件的观感,现代工具集将是什么样子等等),有些完全是可预测的。有些是在编写sendmail的过程我学到的很多关于电子邮件,关于TCP/IP, 和编程本身——每个人都随着自己写的代码而成长。

但也有很多方面我仍会做出一样的决定,有些甚至矛与所谓标准的智慧相悖。

17.8.1 我想以不同方式实现的部分

也许我对sendmail犯的最大的错误就是没有认识到sendmail在将来会变得这是多么重要。我有几次机会推动世界向正确方向发展,但没有把握住它们; 事实上,在某些情况下我做了负面的影响,例如: 没有使sendmail在合适的时候更严格地应对不当输入。同样,在相当早的时候,我就认识到需要改进配置文件的语法,当时可能只有几百个sendmail实例部署,但我最终决定不做改变,因为我不想引起安装用户不必要的痛苦。现在想来,早点改进造成的痛苦是短暂的,产生的好的结果才是长久的。

第七版Mailbox语法

这方面的一个例子是第七版Mailbox分割邮件的做法。他们用一个行开始符“From_”( “From_”代表了ASCII空格字符,编码0x20)分离消息。如果接收到消息在一行开始的地方本身包含单词“From_”,本地邮箱软件就会转换为“From_”。对一些但并不是所有的系统而言有一个细化要求,邮件之前需要一个空行,但这不能作为公约被信赖。直到今天,“>From”仍然会出现在非常意想不到的地方,但显然与邮件本身无关(但很明显,电子邮件系统昨晚会处理到)。回想起来我可能已经将BSD邮件系统变为使用新的语法。我本该在当时收到用户抱怨,但我一定替世界解决了一大堆麻烦。

语法和配置文件的内容

也许我配置文件的语法犯的最大的错误是在重写规则中使用制表符(HT,0x09)进行模式转换。当时我只是效仿make,在晚些时候得知斯图尔特·费尔德曼,make的作者,也认为是他最大的错误之一。除了在屏幕上观察配置不够明显之外,制表符也存在对于大多数窗口剪切和粘贴都不能保留制表符的缺陷。

虽然我相信重写规则是正确的(如下文所叙),但我将改变配置文件的总体结构。例如,我没有预料到需要在配置中加入层次结构(比如说,设置不同的选项应对不同的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用户消息的模型。今天,这样的模型不再适用;即使在使用这种模型的系统中,也有系统用户从来没有收到电子邮件。

17.8.2 我将坚持的设计

当然,也有一些设计至今运作良好···

系统日志

sendmail中一个成功的副产物就是系统日志。在sendmail中需要日志文件去记录一个特定的程序。这些都分散在文件系统中。系统日志在当时很难写(UDP还不存在,所以我使用一个叫做mpx的文件),但这一切都值得。然而,我会做出一个特定的改进:我会花费更多精力注意语法能被及其解析——本质上,我没有能够预测日志监控的产生。

重写规则

重写规则一直争议颇多,但我将莹然使用它(尽管可能不是像现在用的这么频繁)。使用制表符是一个明显的错误,但考虑到限制ASCII和电子邮件地址的语法,一些转义字符可能仍然需要。一般来说,使用模式替换概念的范式运行良好并且非常灵活。

避免不必要的工具

尽管我上面的发言表明我将使用更多现有的工具,但是我是不愿使用今天的许多可用的运行时库。在我看来,这些运行时库大多是臃肿而危险的。库文件应该小心选择,权衡利弊使用,不能使用过度强大的工具来解决简单的问题。我会避免使用的一个特定的工具是XML,至少 不会用它作为一种配置语言。我认为XML对于当前很多使用它的环境来说语法过于复杂了。XML有它的用武之地,但是现在它是被滥用了。

用C来编写

有些人建议一个更自然的实现语言,比如Java或c++。尽管C有一些众所周知的问题,我还是会用它作为我的实现语言。在一定程度上,这是个人因素: 比起Java或c++,我更了解C。但我也对大多数面向对象语言对内存分配的忽视感到失望。分配内存可能产生很多难以定性的性能问题。sendmail内部在适当的地方使用了面向对象的概念(例如,图类的实现),但在我看来,完全面向对象是浪费的,也是过度限制的。

17.9 总结

sendmail MTA出生于一个天翻地覆的世界,一种类似于“西大荒”的时间段,电子邮件是新的一种概念,当前的邮件标准都尚未制定。在过去31年“电子邮件问题”也已经从只需要在大数据高负载下可靠地工作转变为保护从垃圾邮件和病毒的攻击下保护站点,直到今天终于被用作一个基于大量电子邮件的应用程序的平台。Sendmail已经演变成一个大量希望避免风险的企业依靠的平台,即便电子邮件已经从纯文本人际传播发展为多媒体任务关键基础设施的一部分。

成功的原因并非总是显而易见的。在一个快速变化的世界,只有少数兼职开发人员要想编写一个软件程序就不能依赖传统的软件开发方法。我希望我能提供一些sendmail成功的深层原因和经验。


(全篇完)