此章节的 http://www.aosabook.org/en/sendmail.html|英文原文http://www.ituring.com.cn/minibook/19|图灵社区 中没有中文翻译,所以会先行翻译,再发布调研报告。

Sendmail 中文翻译

原作者:Eric Allman

译者:徐有健 131220113(翻译比较渣,欢迎大家勘误)

大多数人把电子邮件这个看作是他们所交互的程序,他们的邮件客户端——在技术上被称为邮件用户代理(MUA)。但是电子邮件另一重要的部分是把邮件从发件人传送到收件人的软件,即邮件传输代理(MTA)。第一个出现在互联网上的MTA是 sendmail,这目前也依旧是最流行的MTA。

sendmail在互联网正式出现之前就被初次创造出来了。他从1981年——那时因特网还不是明显地要成为不仅仅是拥有几百台主机的学术实验,一直成长到今天——在2011年一月就有了超过8亿台主机,而且成长得格外成功。sendmail一直存在于因特网上简单邮件传输协议(SMTP)最常用的安装启用中。

17.1.很久很久从前……

成为后来的sendmail的程序的第一版本写于1980年。起初他只是为了在网络之间传送消息而草草写下的程序。在那时,因特网正处于发展中但没有多少功能。事实上,许多不同种类的网络被提议但是没有一个能够明显脱颖而出被大多数人认可。Arpanet(阿帕网络,互联网前身)使用于美国,因特网作为他的一个升级版本被设计出来。但是,欧洲国家却支持OSI(Open Systems Interconnect开放式系统互联网),而且在一段时间内,OSI显现出将要成功的势头。这些网络都是用来自电话公司的专线网络。在美国,这种专线网络的速度为56Kbps。

按照使用的电脑数目和连接的人数来看,当时最成功的网络或许是UUCP网络(Unix to Unix Copy Protocol Network复制协议网络)。这是非同寻常的因为他没有集中的中心。在某种意义上,他是一种原始的对等网络(peer-to-peer network)。在当时,对等网络用尽了拨号电话线,有时可用地最快网速大约是9600bps。最快的网络(速度在3Mbps这个级别)是基于施乐公司的以太网(Ethernet),它使用一个叫做XNS(Xerox Network Systems施乐网络系统)的协议,这种协议必须要进行本地安装。

当时的环境和现在所有的完全不同。电脑的种类多种多样,甚至于到了连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’s OS/360)。Unix内核不支持文件上锁(技巧可能是使用文件系统链接)。

从某种程度上说,网络普遍是低速的(许多网络基于9600波特的电传线,有钱人或许能使用以太网络,但也只是在本地使用)。伟大的套接字接口还要许多年才能被发明出来。公共秘钥加密也还没有被发明。我们现在所熟知的网络安全在当时是完全不可行的。

网络邮件已经存在于Unix中了,但也只是草创。当时主要的用户代理是’’/bin/mail’’指令(现在有时指’’binmail’’或者’’v7mail’’),但是有些站点使用其他用户代理,比如伯克利(Berkeley)的’’Mail’’——他事实上明白如何把消息当做个体来对待而不是美其名曰作为’’cat’’程序。所有的用户代理直接读(通常也会写)’’/usr/spool/mail’’。对于消息如何被存储完全没有抽象的概念。

对本地电子邮件来说,把消息按线路传送至网络的逻辑就是看地址是否包含感叹号(UUCP)或者冒号(BerkNET)。使用阿帕网络的人必须使用一个完全独立的邮件程序。这样的程序不会和其他网络交互,甚至将本地邮件以不同的格式存储在了不同的地方。

更加有趣的是,事实上这些信息本身也没有标准的格式。获得普遍认同的是在消息开头会有一段标题字段,每个标题字段占一行且标题字段的名字和值会用冒号加以分隔。除此之外,在标题字段的名字和个别字段的格式方面几乎没有标准。比如有些系统使用’’Subj:’’而不是’’Subject:’’,’’Date:’’字段格式不同,有些系统不识别在’’From:’’字段中的全名。除了这些外,被记录在案的往往是模棱两可的或者不完全是在实际使用中的。尤其要指出的是,RFC 733(它意图去描述阿帕网络消息的格式)和那些被实际上用微妙但有时重要的方法去使用的机器不相同,而且它实际上传递消息的方法没有被官方地记录在案(尽管有些RFC机器引用了一些机制,但没有一个做出了定义)。最后结果是,消息传输系统这方面多少感觉像是圣职。

在1979年,安格尔关系数据库管理项目(也是我的日常工作)获得了美国国防部高级研究计划局(DARPA)的拨款,用这笔拨款,我们连接了一条9600bps的阿帕网络到我们的PDP-11机器上。在当时,这是计算机科学部门唯一的阿帕网络连接,所以每个人都想使用我们的机器来连接上阿帕网络。但是,机器已经负担很重了,所以我们只能开放两个登录接口让部门里的每一个人分享使用。这导致了大量的争论和频繁的冲突。然而我注意到大家最想要的并不是远程登录或者文件传输,而是电子邮件。

因此,sendmail(最初叫做delivermail)作为使混乱变成一致的一次尝试而诞生了。每一个MUA(邮件用户代理,或邮件客户端)会给delivermail发送命令来发送邮件,而不是搞明白应该如何在需要时发送,这又是也是相矛盾的。delivermail/sendmail 不尝试去描述本地的邮件应该如何被存储或者被发送。他除了在其他程序之间传送邮件之外什么也没干。(但是在我们可以预见的不久后,当SMTP被添加时,这种情况发生了改变。)在某种意义上,它相当是一种将各种各样的邮件系统连接在一起的胶水,而不是独立地成为一个邮件系统。

在sendmail的发展过程中,阿帕网络转变成了因特网。从线路上低级的数据包到贯穿整个应用协议,这种变化很广泛但是没有立即就发生。sendmail 伴随着标准的变化稳步地进步着,甚至在某些情况下影响着标准。值得注意的是,sendmail存活了下来,甚至随着网络(和如今我们所想的概念相同)规模从几百台主机到成千上万台主机,sendmail变得兴盛繁荣。

另一种网络
值得一提另一种完全不同的邮件标准,在当时被提议时被称为X.400,它是ISO/OSI(International Standards Organization/Open Systems Interconnect,国际标准化组织/开放系统互联)的一部分。X.400是一种二进制协议,用ASN.1(Abstract Syntax Notation 1,抽象描述文法)将消息编码,如今仍然使用于一些互联网协议中,例如LDAP(Lightweight Directory Access Protocol,轻量级目录访问协议)。LDAP反而是X.500的简化版本。X.500是X.400使用的目录服务。sendmail没有做任何尝试去直接与X.400兼容,尽管在当时X.400有一些显著有效的网关服务。当时虽然一开始X.400被许多商业合作商采用,但是互联网邮件和SMTP赢得了最终的市场地位。

17.2.设计原则

当发展sendmail时,我坚持了一些设计原则。所有这些在某种程度上归结为一件事:做的越少越好。这和当时的其他一些拥有更广大的目标和需要更大实现的努力形成了鲜明对比。

17.2.1.接受一个程序员是有限的

我把sendmail当做是一个兼职无回报的项目来完成。它被意图使在加州大学伯克利分校的人们更多的可以连接上阿帕网络邮件。关键是在已存在的网络之间传送邮件。这些网络是被当做单机程序而设置的,人们并没有意识到甚至有不止一个网络存在。然而一个兼职程序员修改程序以适应超过一个少量的现存网络是不可实行的。设计需要最小化需要修改的现存代码和需要编写的新代码的数量。这样的约束推动了大部分剩余设计原则的形成。结果是,在大部分情况下,这些设计原则显得十分正确,即使有一个更大的团队。

17.2.2.不要重新设计用户代理

邮件用户代理(MUA)是大多数最终用户所认为的“邮件系统”。实际上,它是用来读,写和回复邮件的程序。它和把邮件从发件人传送到收件人的邮件传输代理(MTA)非常不同。当时sendmail被编写时,许多的实现至少部分的结合了这两个功能,所以他们常常一前一后的发展。尝试同时在两方面工作工作量实在太大了,所以sendmail完全去除了用户界面问题:MUA唯一需要修改的是调用sendmail而不是让他们自己寻找路径传输邮件。尤其是已经有了几个用户代理,而且人们对他们如何和邮件交互非常情绪化。尝试同时在两方面工作工作量实在太大了。区别于MTA的MUA已经做得足够聪明了,但是在当时还远远没有行规。

17.2.3.不要重新设计本地邮件存储库

本地邮件存储库(信息被存储在此处知道收件人出现阅读了它)还没有被正式标准化。有些站点喜欢把它们存储在一个集中地地方,例如’’/usr/mail’’,’’/var/mail’’,’’/var/spool/mial’’。其他站点喜欢把它们存储在收件人的根目录(举例来说,比如叫做’’.mail’’的文件)。大多数站点让每个信息以一行后面跟了一个空格的“From”来开头(这是一个极其坏的决定,但却是当时的惯例),但是专注于阿帕网络的站点通常把消息以包含了4个control-A字符的一行来区别开来存储。一些站点尝试着把邮箱上锁来避免碰撞冲突,但他们使用不同的上锁惯例(文件上锁的基本功能还没有实现)。简而言之,唯一合理的做法是把本地邮件存储库当做黑盒子来对待。

在几乎所有的站点,实现本地邮箱的实际原理具体呈现在’’/bin/mail’’中的程序。这个程序把一个相当原始的用户界面,路由选择和存储建设在一个程序中。为了合并sendmail,路由选择的部分被去除并被代替为调用sendmail。一个’’-d’’标记被添加进去来推动最终的邮件传送,也就是它阻止了’’/bin/mail’’调用sendmail去做路由选择。在之后的几年,被用来把消息传送到物理的邮箱的代码被提取出来变成了另一个叫做’’mail.loacl’’的程序。现存的’’/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。如果不是简单得过分的话,它已经是极其的简单了。它唯一的功能就是把邮件从一个程序传送到另一个程序,尤其是在没有SMTP支持的情况下,所以它从来没有做任何的直接网络连接。排队是不必要的,因为每个网络已经有各自的排队机制了,所以这个程序真的只是个横纵开关。既然delivermail没有直接的网络协议的支持,那么也就么有理由让它作为后台程序来运行——当任意一段消息被提交时,它(delivermail)会被唤醒去按线路发送消息,把它传送到能够实现下一跳转的程序,然后就终止运行。相同的也不用尝试去重新写首部去适应消息要被传送到达的网络。这一般会导致的结果是被传送的消息不能被回复。这种情况非常糟糕,以至于有关于处理邮件地址的一整本书被写出来(这本书被非常适合地叫做!%@:: A Directory of Electronic Mail Addressing & Networks [AF94])

在delivermail中所有的配置都被编译在且仅基于每个地址的特殊字符。字符拥有优先权。例如,一个主机的配置可能会寻找一个“@”的标志,如果找到了一个“@”,它会把整个地址发送给一个指定的阿帕网中继主机。否则,它可能会寻找一个冒号,如果找到了一个,它会把消息、指定的主机和用户一起发送给BerNET,然后检查一个表明消息应该转发给指定的UUCP中继的感叹号(“!”)。否则,它会尝试本地传递。这种配置可能最终形如以下:

Input Send To {net,host,user}
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中)。

delivermail随着4.0和4.1版本的BSD传播开来,而且成功得超过了预期;伯克利分校还远不是唯一的有混合网络架构的站点。显然,这需要更多的工作。

17.3.2. 第二阶段:sendmail 3,4和5

版本1和2是以delivermail的名字分布的。在1981年三月,版本3的工作开始了,这将是以sendmail名字发布。在这个时间点上,16位的PDP-11仍处于普遍使用中,但是32位的VAX-11越来越受欢迎,所以很多原来和小地址空间相关的约束条件开始变得宽松起来。

sendmail的最初目标转换至运行环境配置,允许修改消息来为在不同网络之间转发邮件而提供兼容性,并有进行路由决策的更丰富的语言。所使用的技术本质上是基于原文重写地址(基于令牌而不是字符串),这是当时在一些专家系统中所使用的机制。有临时代码提取和保存任何注释字符串(在括号中)以及在程序改写完成后重新插入它们。同样重要的是能够添加或增大首部区域(例如,添加一个’’Date’’首部字段,或者在已知的情况下把发送者的全名包含在’’From’’首部中)。

SMTP的发展始于1981年十一月。加州大学伯克利分校的计算机科学研究组(CSRG)得到了DARPA(美国国防部高级研究计划局)的合同去提供一个基于Unix的平台来支持DARPA资助的研究,目的是使项目之间的共享更加方便。TCP/IP协议栈的初始工作是在当时完成的,虽然套接字接口的细节还在不断变化。基本的应用协议,例如Telnet和FTP都完成了,但SMTP尚未实施。事实上,SMTP协议当时还没有完全编写结束;关于邮件应该如何使用一个被创造性地命名为Mail Transfer Protocol(邮件传输协议,MTP)的协议来被发送仍然有巨大的争论议。随着争论愈演愈烈,MTP变得越来越复杂,直到SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)在挫折中被设计出来并或多或少的得到了许可(但直到1982年八月才正式发表)。我正式地工作在安格尔关系数据库管理系统,但因为我对邮件系统的了解比当时伯克利分校的任何人都要深入,所以我被找去加入实现SMTP的项目。

我最初的想法是创建一个单独的有自己的队列和后台进程的SMTP邮件程序;子系统将依靠sendmail做路由。然而,SMTP的几个特征使得这一想法有些问题。例如,在’’EXPN’’和’’VRFY’’命令需要解析、别名分析和本地地址验证模块的权限。而且,当时我认为如果地址未知那么’’RCPT’’命令立即返回是很重要的,而不是先接受消息,然后之后不得不发送一个发送失败信息。这后来被认为是一个有先见之明的决定。具有讽刺意味的是,后来MTA经常犯这样的错,恶化了垃圾邮件后向散射问题。这些问题促成了将SMTP包含为sendmail本身一部分的决定。

sendmail 3 分布在4.1a和4.1c版本的BSD(beta版本),sendmail 4分布在4.2版本的BSD,sendmail 5分布在4.3版本的BSD。

17.3.3. 第三阶段:混乱阶段

在我离开伯克利分校去一个初创公司后,我能花在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,即网络文件系统(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)来继续下去。但在上世纪90年代末,已经安装的基础已经发展到几乎不可能在志愿者基础上来支持的程度。抱着得到新的资源来维持这些代码的想法,我和一个更有商业头脑的朋友一起创建了sendmail,Inc.。

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

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

17.3.6. sendmail 6和7究竟发生了什么?

sendmail 6本质上是sendmail 8的beta版本。它从没有被正式发布,但是扩散地相当的广泛。sendmail 7根本不存在。sendmail直接跳到了版本8,因为当1993年6月4.4版本BSD发布时,所有其他为了BSD的分布而写的源文件都被版本8给替换了下来。

17.4. 设计决策

17.4.1. 配置文件的语法

配置文件的语法由几个问题驱动。首先,整个应用程序必须放入一个16位的地址空间,所以解析器必须足够小。其次,早期的配置是相当短(只有一页),因此,尽管语法是晦涩的,该文件仍然是可以理解的。然而,随着时间的推移,更多的可操作的决定C代码搬到了配置文件,所以该文件开始变长。配置文件获取了晦涩难解的“美誉”。对许多人来说的一个挫折是把制表符作为有效的语法成分的决策。这是一个从当时的其他系统复制过来的错误,尤其是’’make’’。随着Windows操作系统(剪切和粘贴操作通常没有保留制表符)更多的的被使用,该特定问题变得更加尖锐。

现在回想起来,随着文件变大,32位的机器获得统治地位,重新考虑语法变得很有意义。曾几何时,我想要这样去做,但最后没有决定去做,因为我不想打破“庞大”的安装基础(在这一点上可能是好几百台机器)的时候。现在回想起来,这是一个错误。我根本就没有意识到安装基础将会增长得大到什么程度,如果我早点修改了语法我能够节省多少时间。另外,当标准稳定下来时,相当数量的一般性问题本可能被推回到C代码库中去,从而简化了配置。

特别值得注意的是更多的功能是怎么被加入配置文件的。我在SMTP发展的同一时间开发了sendmail。通过将可操作的决定移入到配置文件中,我能够迅速地对设计变化做出回应——通常在24小时内。我认为这提高了SMTP标准,因为它可以很快通过快速变化的被提议的设计来获得可操作经验,但代价仅仅是配置文件变得难以理解。

17.4.2. 重写规则

编写sendmail的时候的一个困难的决定是如何做必要的重写来允许不同网络之间的邮件传输不违反接收网络的标准。邮件的转换需要改变元字符(例如,BerkNET使用冒号作为分隔符,但是这在SMTP地址中并不合法)、重新排列地址成分、添加或删除成分等等。例如,在某些情况下需要做出如下的重写:

From 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>

正则表达式是不是一个好选择,因为他们没有很好地支持word边界、引用等等。很快变得显然的是,几乎不可能写出准确的正则表达式,而且更加难以理解。特别是,正则表达式保留了一部分元字符,包括“.”、“*”、“+”、“{[}”和“{]}”,所有这些字符都可以出现在电子邮件地址中。这些可能已经跳出了配置文件的范围,但我认为这很复杂、混乱,而且有点丑陋。(这已经由来自贝尔实验室的UPAS(第八版本Unix的邮件收发程序)尝试过了,但它从来没有流行起来。)相反,扫描阶段对于产生在常规表达式中可以像符号一样被操作的标记是必要的。一个单一的参数描述“操作者的角色”,他们自己既是标记又是标记分隔符,就足够了。空格分隔了标记但不是标记本身。重写规则只是被组织进本质上是子程序的代码中的成对的模式匹配/替换操作。

取代大量的已经被回避从而失去了他们的“神奇”特性(在正则表达式中被使用)的元字符,我用一个单一与普通字符结合的“escape”字符来表示通配符模式(例如去匹配任意字符)。传统的UNIX系统的方法是使用反斜杠符号,但反斜线符号在一些地址语法中被作为引号字符使用了。结果是,“$”是仅剩的几个没有在一些邮件语法中被用作标点字符的字符之一。

讽刺的是,一个原始的糟糕决定是关于空白是怎么被使用的事情。空格字符是一个分隔符,就像在大多数的扫描输入中一样,因此它可以被自由在各模式的标记之间使用。然而,原有的配置文件的分布不包括空格,结果导致模式远超过必要地难以理解。考虑下面两个(语义上相同的)模式之间的区别:

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

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

17.4.3. 使用重写来解析

有人建议,sendmail的应该使用传统的基于语法的解析技术来解析地址,而不是重写规则,将重写规则用于地址修改。从表面上看这似乎是有意义的,考虑到该标准定义了使用语法的邮件地址。重用重写规则的主要原因是,在某些情况下,有必要解析首部地址(例如,当从网络接收到一个没有使用正常“信封”的邮件,为了从首部提取收件人)。这样的地址是不容易使用一个LALR(1)解析器,例如YACC,和因需要一定量的先行读取的传统的扫描器。例如,解析地址:''allman@foo.bar.baz.com eric@example.com`需要由任一扫描仪或解析器先行读取。你不可能知道开始的“allman@…”不是一个地址直到看见了“<”。由于LALR(1)解析器只有一个提前的标记,这将必须在扫描仪中来完成,这实质上让这件事变得更加复杂。由于重写规则已经有了任意的回溯(也就是说他们可以向前看任意远),所以重写规则就已经足够了。

次要原因是,让模式得以识别和修复破损输入是相对容易的。最终原因是,重写已经远足够强大来完成工作,而且重用任何代码都是明智的。

关于重写规则的一个非同寻常的点是:执行模式匹配时,它对于输入和模式的标记化都是有用的。因此,一个扫描仪同时被用于输入地址和模式本身。这要求扫描仪对不同输入能够调用不同的符号表。

17.4.4. 嵌入SMTP和sendmail的队列

一个“显然”的方式来实现外发(客户端)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用户运行,从定义上来说该用户具有特殊权限,因此不应与其他网站直接通信。所有这些问题都可以通过一些工作来巧妙地解决。

17.4.5. 队列的实现

Sendmail的遵循了当时的惯例来存储队列文件。实际上,所使用的格式是非常类似于当时的lpr(Lost Packet Recovery,丢包恢复)子系统。每个工作有两个文件,??一个为控制信息,一个为数据。控制文件是每行的第一个字符表示该行意义的纯文本文件。

当sendmail想要处理队列时,它必须读取所有的控制文件,将相关信息存储在内存中,然后对列表进行排序。在消息相对较少的队列中它运行的很好,但在有10000消息在队列中时开始出现故障。具体地,当目录大到在文件系统中要求间接块时,会出现了严重的可能会减少一个数量级的性能下降。我们可以通过sendmail理解多队列目录来改善这个问题,但是这充其量只是一个敷衍之举。

可替代的实现方式可以是把所有的控制文件存储在一个数据库文件中。但是我没有这样做,因为在开始编写sendmail时,没有一般可用的数据库包,而当dbm(3)可用时,它有一些缺陷,包括无法回收空间、要求散列在一起的所有关键字装入同一个(512字节)页面、缺少锁。很多年后健壮的数据库软件包才出现。

另一可替代的实现方法是有一个单独的守护进程来将队列的状态保存在内存中,或许可能会写入日志以允许恢复。考虑到当时相对较低的电子邮件业务流量、在大多数机器上缺乏存储器、相对较高的后台进程成本、实施这样一个进程的复杂性,在当时这似乎并不是很好的折衷办法。

另一个设计决策是在队列控制文件中存储消息首部而不是存储数据文件。这样的基本原理是,大多数的首部需要从目的地到目的地的相当大的改写(消息可能有一个以上的目的地,所以他们将不得不被多次定制),而且解析报头的成本显得很高,所以将它们以预解析的格式来存储似乎是比较节约资源的。现在回想起来,这不是一个明智的决定,因为我们是以Unix标准格式来存储邮件正文(换行符结尾),而不是以收到它的格式(可以使用换行符,回车/换行,空白回车,或这换行/回车)。随着电子邮件世界的发展以及通过了一些标准,重写的需求减弱了,甚至看似无伤大雅的重写会有风险包含错误。

17.4.6. 接受和修补伪输入

由于sendmail的是在一个多种协议和一些令人不安的书面标准的世界中创造的,我决定无论在什么地方都尽可能的清理损坏的消息。此符合了在RFC中明确表示的“鲁棒性原则”(又名Postel法则,伯斯塔尔法则)。其中一些变化是显而易见的,甚至是必须的。比如,向阿帕网络发送一个UUCP消息,UUCP地址需要被转换为阿帕网络地址。又比如,如果只是让“reply”命令正常工作,线路终端需要在不同平台的约定之间互相转换。等等。还有一些改变不太明显的。比如如果收到的讯息不包含互联网规范要求的’From: ‘头字段,你应该给他添加一个’From: ‘头字段,还是直接传递这个没有’From: ‘头字段的消息,又或是拒绝该消息?当时,我首要的考虑是互操作性,所以sendmail给消息打上补丁,例如,添加’From: ‘头字段。然而,这声称已经使得其他破损的邮件系统能够在他们应该被修补或者是摒弃时得以与世长存。

我相信在当时我的决定是对,但在今天是有问题的。高度的互操作性对于让邮件流畅通无阻是非常重要的。如果我拒绝了损坏的消息,当时的大多数消息会被拒绝。如果我不修补就让把这些消息传下去,收件人就会收到他们无法答复的邮件,甚至在某些情况下,他们可能不能确定是谁发送了消息,否则这消息又将被其他邮件程序拒绝接收。

今天标准已经制定好了,并且在大多数情况下这些标准是准确和彻底的。大多数的邮件将被拒绝,或者仍有邮件软件发送损坏的消息的情况已经一去不复返了。这就不必要的给互联网上的其他软件造成了许多问题。

17.4.7. 配置以及M4的使用

在某一个时期我同时给sendmail的配置文件做定期修改,以及个人支持多台机器。由于又大量的不同机器之间的配置文件是一样,使用工具来构建配置文件是值得的。Unix包含有M4宏处理器。它被设计作为编程语言的前端(尤其是RATFOR语言)。最重要的是,它已经“include”的功能,就好像在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作为一个配制方法学暴露给最终用户本质上是将所有配置系统的尝试转换为了一个编程问题。巨大的能量缘起于此,但是却付出了巨大的代价。

17.5. 其他注意事项

一些其他的体系结构方面的开发要点值得一提。

17.5.1. 关于优化网络规模系统的只言片语

在大多数基于网络的系统中,在客户端和服务器之间存在一些紧张的矛盾。一个客户端的很好的策略对于服务器来说并不好,反之亦然。例如,服务器会尽可能地通过推尽量多的处理任务到客户端,以减少其处理成本,当然客户也做相同的事,但是在相反的方向。例如,一台服务器可能想要在做垃圾邮件处理时保持连接打开,因为这降低了拒绝消息(这在现在是常见的情况)的成本,但客户想尽快断开连接。综观整个系统,也就是说把因特网作为一个整体来看,最佳解决方案是平衡这两个需求。

已经有使用明确偏向客户端或服务器的策略的MTA的情况。他们只能做到这一点,因为他们有一个相对较小的安装基础。当你的系统被在互联网上的显著部分中使用时,你必须设计好它以平衡双方的负载来优化互联网整体。从事实来看很复杂的是,总是会有MTA彻底向其中一方或另一方倾斜——例如邮件群发系统只关心优化和发送一方。

当设计结合了双方连接的系统,避免厚此薄彼是非常重要的。请注意,这和通常的客户端和服务器端的不对称形成了鲜明的对比。例如,Web服务器和Web客户端通常不是由同一各小组开发的。

17.5.2. Milter

对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.org网站列出了由商业公司和开源组织开发的用于防垃圾邮件,防病毒,存档,内容监控,日志,流量调整等众多类别功能的milter。Postfix邮件添加了对使用相同接口milter的支持。Milters已被证明是sendmail的伟大成就之一。

17.5.3. 发布时刻表

人们在“早发布,经常更新”和“发布稳定的系统”两种观点之间的争论很多。在不同的时代这两种方式都被sendmail使用过。在变化相当重要的时候,我有时一天发布过不止一个版本。我总的理念是在每一个变更之后都发布一个版本。这类似于向公众提供资源管理系统树的访问权限。相比于提供公共源代码树,我个人更喜欢发布新版本。至少某些部分上是这样,因为是我是用一种系在被认为是未经批准的方式来管理资源。对于大的变化,我会在我写代码时检查非运行的快照。如果树是共享的,我会用分支来做快照,但在任何情况下,全世界都可以看到他们,这样会产生相当大的混乱。此外,在创建一个版本发布意味着给它分配一个号码,这使得当查看一个bug报告时能更容易地追踪变化。当然,这需要那个版本发布容易产生,可是实际并非总是如此。

随着sendmail变得适用于更加关键的生产环境中时,以下变得愈加问题突出:让别人讲清我想在那里供人测试的更新改变和那个真的打算在复杂的实际环境中使用的更新改变之间的差异并不是那么容易。给版本贴上“alpha”或者“bate”的标签稍稍减缓了这个问题,但并没有解决它。其结果是,随着sendmail日渐成熟,它的更新变得不频繁但是版本变化更大。当sendmail被并入一个商业公司时,客户想要既最新又最好的但只是稳定的版本,他们不接受两者是相互矛盾的说法。这时候,这个问题变得尤其尖锐。

开源开发者的需求和商业产品的需求之间的矛盾永远不会消失。版本发布得又早又频繁有许多优势,尤其是有巨大数目的潜在使用者。他们是勇敢的(有时愚蠢的)测试者,他们以你可能永远不会希望在一个标准的开发版本中复制操作的方式来给予系统压力。但随着一个项目变得成功,它容易变成一个产品(即使该产品是开源和免费的),和项目相比产品有??不同的需求。

17.6. 安全

Sendmail的发展历程十分动荡,但它在安全性方面是明智的。其中的一些是当之无愧的,但有些不是,因为我们的“安全”理念在我们脚下改变了。互联网起初只有一个几千人的用户群,大多数用户是在学术和研究设置领域。当时的互联网在许多方面比我们现在所知的要温和一些。该网络旨在鼓励共享,而不是建防火墙(这是另一个在早期还未出现的概念)。现在的网络是一个危险的,充满敌意的地方,这里充满了垃圾邮件发送者和破坏者。它被越来越多的描述为一个战区,而且在战区充满了平民的伤亡。

我们很难安全地编写网络服务器,特别是当协议一点也不简单时。几乎所有的程序都至少有一些小问题; 即使是常见的TCP/IP也已经被成功攻击。更高级的实现语言没有找到什么防止攻击的万能方法,甚至自己也有一些弱点。必要的观察短语是“不信任任何输入”,不管它来自哪里。不信任的输入包括次级输入,例如,来自DNS服务器和milter的输入。像大多数早期的网络软件,sendmail在它的早期版本太过于相信外部输入了。

但是sendmail最大的问题是,早期的版本它运行时是具有root权限的。必要的Root权限可以用来打开SMTP监听套接字,用来读取个人用户的转发信息,并且转交给个人用户的邮箱和主目录。不过,在今天大多数的系统上,邮箱的概念已经从一个系统用户的概念上脱离,这有效地消除了对root权限的需要,除了打开SMTP监听套接字还需要root权限。如今,sendmail能够在处理连接之前放弃root权限,这样消除了对能够支持sendmail的系统环境的担忧。值得注意的是,在那些不直接传递东西给用户的邮箱系统中,sendmail还可以在chroot的环境中运行,从而与更深层次的权限隔离开来。

不幸的是,当sendmail因为糟糕的安全性而著称时,它开始因为一些和sendmail无关的问题被指责。例如,一个系统管理员使他的’’etc’’目录可写,然后当有人取代了’’/etc/passwd’’文件时指责sendmail。正是类似这样的一些事件导致我们实质性地大幅提高安全性,包括明确检查sendmail访问的文件和目录的来源和模式。这是如此严苛的以至于我们被迫在程序中包含一个’’DontBlameSendmail’’选项来有选择的关闭这些检查。

还有一些其他方面的安全问题是与保护程序本身的地址空间不相关的。例如,垃圾邮件的兴起也导致了地址收集的兴起。SMTP中的’’VRFY’’和’’EXPN’’命令是分别专门用来验证个人地址和扩大邮件列表中的内容的。这些命令被垃圾邮件的发送者滥用地如此严重,以至于现在大多数网站将其完全关闭。这是不幸的,至少对于’’VRFY’’来说是这样,因为该命令有时被一些反垃圾邮件代理来所谓的发送地址。

同样,反病毒保护曾一度被作为桌面问题,但是其重要性上升到任何商业级MTA都不得不拥有防病毒检查可用的地步。在现代设置中的其他安全相关的要求包括强制加密敏感数据,数据丢失防护和执行法规要求,例如,HIPPA。

其中一个sendmail放在心上认真关注的原则是可靠性,每个消息应该要么被发送出去或报告给发件人。但joe-job(一种攻击者伪造邮件返回地址的垃圾邮件,这被许多人视为一个安全问题)的问题引起很多网站关闭退回邮件的创建。如果能够确定故障而SMTP连接仍然是开放的,服务器可以通过使命令失效来报告问题。但是在SMTP连接关闭后,没有有效地址的处理信息将默默地消失。为了公平起见,现在大多数合法的邮件是单跳的,这样问题虽然会被报告,但至少在原则上,已经被世界认可地是,安全的重要性胜过可靠性。

17.7. Sendmail的演变

软件如果不能不断地发展来适应这个急剧变化的环境,那么它是无法生存下来的。新硬件技术的出现,推动了操作系统的变化,推动了库和框架的变化,推动了应用程序的变化。如果一个应用程序成功了,这说明它适应了这个问题麻烦源源不断的环境。变化是不可避免的;为了成功你必须接受变化并且拥抱变化。本节介绍一些随着sendmail的演变出现的比较重要的变化。

17.7.1. 配置文件变得更加详细

sendmail的最初配置是相当简洁的。例如,选项和宏的名字都是单个字符。这有三个方面的原因。第一,它使得分析变得非常简单(这在16位的环境中十分重要)。其次,也没有太多的选择,所以想出助记名字并不难。第三,单个字符的公约已经用命令行标志建立了起来。

同样,重写规则集最初是编号的而不是命名的。也许一开始能够容忍一个小数目的规则集,但随着规则集的数目增多,让他们具有更多的助记名字变得重要。

随着操作sendmail所在的环境变得更加复杂,也随着16位环境逐步消失,一个更丰富的配置语言变得尤为需要。幸运的是,我们可以以一个向后兼容的方法来做出这些变化。这些变化显著地提高了配置文件的可理解性。

17.7.2. 更多的与其他子系统的连接:大整合

在写sendmail的时候,邮件系统与操作系统的其余部分基本上是隔离的。有一些服务是需要整合的,比如’’/etc/passwd’’和’’/etc/hosts’’的文件。服务交换机还没有被发明出来,目录服务是不存在的,配置文件规模很小并且是手工维护的。

这一切发生生了迅速改变。其中一个首先被添加的服务是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或DECnet的网络中,由于TCP/IP的知识已经被建为这么多的代码。

很多配置功能被添加了来处理这个充满敌意的世界,例如对访问表,实时黑名单,地址收集缓解,拒绝服务保护,垃圾邮件过滤这些功能的支持。这极大地复杂化了配置邮件系统的任务,但为了适应当今世界,这一切绝对是必要的。

17.7.4. 新技术的引入

这些年出现了许多新的标准,这需要sendmail做出显著的改变。例如,TLS(加密)的加入需要贯穿大部分代码的显著改变。SMTP流水线需要窥视低级别的TCP/IP数据流以避免死锁。提交端口(587)的加入需要听取多个传入端口能力,包括有取决于到达端口而做出不同的行为的能力。

其他方面的压力来自于由环境,而不是标准。例如,在邮件过滤器接口的加入是对垃圾邮件的直接反应。虽然邮件过滤器不是一个公布的标准,但这是一个重大的新技术。

在所有情况下,这些变化以某种方式增强了邮件系统,无论是提高安全性,带来更好的性能,或着带来新的功能。但是,他们都提高了开销,在几乎所有情况下都使得代码库和配置文件变得复杂。

17.8. 假如我今天做它会怎样

事后诸葛亮。有很多事情如果我如今去做会完全不同。有些在当时是无法预见的(例如,垃圾邮件将如何改变我们对电子邮件的看法,现代的工具集会是什么样子等),然而有的却完全是可以预测的。有些只是在我编写sendmail的过程中,我学到了很多有关电子邮件,有关TCP/IP,以及有关编程本身的知识,每个人都随着他们编码而不断成长。

但也有很多事情我会保持完全一样的做法,有些甚至是与标准的智慧矛盾的。

17.8.1. 那些我会以不同的方式去做的事

或许是我对于sendmail最大的错误就是没有及早认识到它将来会变得多么重要。我有好几次机会向正确的方向轻推世界,但我没有把握住。事实上,在某些情况下,我甚至做出了伤害,例如,没有使sendmail更加严格地对待不好的输入,当它适合这样做的时候。同样,我相当早地认识到配置文件的语法需要修改提升,当时只部署了也许几百个sendmail的实体,但我最终决定不去改变,因为我不希望引起已经安装的用户群太过烦恼。回想起来,早点提升配置文件语法,导致短期疼痛来获取获取一个更好的长远的结果会更好一点。

第7版Mailbox语法

这方面的一个例子是第7版mailbox区分消息的方式。他们使用一个行开头“From?”(其中“?”代表ASCII空格字符,编码为0x20)来分隔消息。如果接受到一个消息,它本身在行开头包含单词“From?”,本地mailbox软件会将其转换为“>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:’’首部字段就不接收它,而不是尝试从信封中的信息创建一个。

内部抽象

有一些内部的抽象我不会再次去尝试,还有一些我想添加。例如,我不会用零结尾字符串,转而选择一个长度/值对。尽管这意味着大部分的标准C语言库变得难以使用。仅此一各安全问题反而使它使它值得使用。相反,我不会试图建立在C语言中处理的异常,但我会创建一个将在整个代码中贯穿使用的一致的状态码系统,而不是按照惯例返回’’null’’、’’false’’或这负数来表示错误。

我一定会把邮箱名的概念从Unix用户ID抽象成别的概念。在我编写sendmail的时候,模式是你只给Unix用户发送邮件。今天,这是几乎是从来没有的情况,即使在那些使用该模型的系统中,也有一些从不接收电子邮件的系统帐户。

17.8.2. 事情我会做同样的

当然,有些事情工作得很好…

系统日志

其中一个来自sendmail的成功项目是系统日志。在编写sendmail时,一个需要记录的程序会有一个它们会去写的特定的文件。这些文件被散落在文件系统中。在当时,要写系统日志是非常困难(UDP还不存在,所以我用一个叫做mpx文件的东西),但这是非常值得的。不过,我想做出一个明确的改变:我会更加注意使记录的消息的语法能被机器解析,从本质上讲,我没有预测到记录监控的存在。

重写规则

重写规则已经被很多人所诟病,但我仍然会使用它(尽管也许不是和现在被用的一样多)。使用制表符是一个明显的失误,但鉴于ASCII和电子邮件地址的语法的局限性,一些转义字符大概是需要的。在一般情况下,使用模式替换的范例的概念运作良好而且非常灵活。

避免不必要的工具

尽管有上述,我还是会用更多现有的工具,如今我会勉为其难地使用许多可用的运行时间库。在我看来,这些运行时间库大多是那么臃肿以至于很危险。库应该被谨慎地选??择,要平衡重用的优点和使用过于强大的工具来解决一个简单的问题的问题。一个特定的我将避免的工具是XML,至少作为配置的语言我会避免使用它。我相信,它的语法对于它被使用的许多场景来说太过结构复杂了。XML有它被使用的合适的地方,但是如今它被过度使用了。

用C语言编码

有些人建议,Java或C++会是更自然的实现语言。尽管C语言有一些众所周知的问题,我仍然会用它作为我的实现语言。这部分是个人的原因:相比于Java或者C++,我更了解C语言。但是我也对大多数面向对象的语言对内存分配所采取的漫不经心的态度感到失望。分配内存有很多性能方面的难以定性的问题。Sendmail在内部适当的部分使用了面向对象的概念(例如,图类的实现),但在我看来,完全的面向对象有些浪费,也有过度的限制。

17.9. 总结

Sendmail邮件传输代理诞生于一个巨大动荡的世界里,一种类似于“西大荒”的存在。这个时候电子邮件是临时的而且当前的邮件标准也是尚未制定。在其间的31年中,“电子邮件问题”已经从仅仅需要可靠地工作变为了在大型邮件和重负载下工作来保护网站免受垃圾邮件和病毒的骚扰,一直到今天变成了大量的基于电子邮件的应用的平台。Sendmail已经演变成被大多数规避风险的企业所接受的主力平台,甚至电子邮件已经从纯文本的人对人的交流发展成为了基础设施的基于多媒体的关键部分。

成功的原因并不总是显而易见的。使用传统的软件开发方法论来仅仅靠一些业余的开发者来建立一个能够生存下来的甚至在急剧变化的世界中繁荣昌盛的程序是不可能完成的。我希望我已经提供了一些关于sendmail是怎样成功的的见解。

译者按:全文到此结束,谢谢