The Architecture of Open Source Applications (Volume 1)

Eric Allman

Most people think of electronic mail as the program that they interact with—their mail client, technically known as a Mail User Agent (MUA). But another important part of electronic mail is the software that actually transfers the mail from the sender to the recipient—the Mail Transfer Agent (MTA). The first MTA on the Internet, and still the most prevalent, was sendmail.

Sendmail was first created before the Internet officially existed. It has been extraordinarily successful, having grown from 1981, when it wasn't at all obvious that the Internet was going to be more than an academic experiment with only a few hundred hosts, to today, with over 800 million Internet hosts as of January 20111. Sendmail remains among the most used implementations of SMTP on the Internet.

17.1. Once Upon a Time…

The first versions of the program that would become known as sendmail were written in 1980. It started as a quick hack to forward messages between different networks. The Internet was being developed but was not functional at that time. In fact, many different networks had been proposed with no obvious consensus emerging. The Arpanet was in use in the United States and the Internet was being designed as an upgrade, but Europe had thrown its weight behind the OSI (Open Systems Interconnect) effort, and for a while it appeared that OSI might triumph. Both of these used leased lines from the phone companies; in the US that speed was 56 Kbps.

Probably the most successful network of the time, in terms of numbers of computers and people connected, was the UUCP network, which was unusual in that it had absolutely no central authority. It was, in some sense, the original peer-to-peer network, which ran over dialup phone lines: 9600 bps was about the fastest available for some time. The fastest network (at 3 Mbps) was based on the Ethernet from Xerox, which ran a protocol called XNS (Xerox Network Systems)—but it didn't work outside of a local installation.

The environment of the time was rather different than what exists today. Computers were highly heterogeneous, to the extent that there wasn't even complete agreement to use 8-bit bytes. For example, other machines included the PDP-10 (36 bit words, 9 bit bytes), the PDP-11 (16 bit words, 8 bit bytes), the CDC 6000 series (60 bit words, 6 bit characters), the IBM 360 (32 bit words, 8 bit bytes), the XDS 940, the ICL 470, and the Sigma 7. One of the up-and-coming platforms was Unix, which at that time came from Bell Laboratories. Most Unix-based machines had 16-bit addresses spaces: at that time the PDP-11 was the major Unix machine, with the Data General 8/32 and the VAX-11/780 just appearing. Threads didn't exist—in fact, the concept of dynamic processes was still fairly new (Unix had them, but "serious" systems such as IBM's OS/360 did not). File locking was not supported in the Unix kernel (but tricks were possible using filesystem links).

To the extent they existed at all, networks were generally low speed (many based on 9600-baud TTY lines; the truly rich might have had Ethernet available, but for local use only). The venerable socket interface wasn't going to be invented for many years. Public key encryption hadn't been invented either, so most network security as we know it today wasn't feasible.

Network email already existed on Unix, but it was created using hacks. The primary user agent at the time was the /bin/mail command (today sometimes referred to as binmail or v7mail), but some sites had other user agents such as Mail from Berkeley, which actually understood how to treat messages as individual items rather than being a glorified cat program. Every user agent read (and usually wrote!) /usr/spool/mail directly; there was no abstraction for how the messages were actually stored.

The logic to route a message to the network versus local e-mail was nothing more than seeing if the address contained an exclamation point (UUCP) or a colon (BerkNET). People with Arpanet access had to use a completely separate mail program, which would not interoperate with other networks, and which even stored local mail in a different place and in a different format.

To make things even more interesting, there was virtually no standardization on the format of the messages themselves. There was general agreement that there would be a block of header fields at the top of the message, that each header field would be on a new line, and that header field names and values would be separated by a colon. Beyond that, there was very little standardization in either the selection of header field names or the syntaxes of individual fields. For example, some systems used Subj: instead of Subject:, Date: fields were different syntaxes, and some systems didn't understand full names in a From: field. On top of all of this, what was documented was often ambiguous or not quite what was actually in use. In particular, RFC 733 (which purported to describe the format of Arpanet messages) was different from what was actually used in subtle but sometimes important ways, and the method of actually transmitting messages was not officially documented at all (although several RFCs made reference to the mechanism, none defined it). The result was that there was somewhat of a priesthood around messaging systems.

In 1979, the INGRES Relational Database Management Project (a.k.a. my day job) got a DARPA grant, and with it a 9600bps Arpanet connection to our PDP-11. At the time it was the only Arpanet connection available in the Computer Science Division, so everyone wanted access to our machine so they could get to the Arpanet. However, that machine was already maxed out, and so we could only make two login ports available for everyone in the department to share. This caused substantial contention and frequent conflicts. However, I noticed that what people wanted most of all was not remote login or file transfer, but e-mail.

Into this, sendmail (initially called delivermail) emerged as an attempt to unify the chaos into one place. Every MUA (mail user agent, or mail client) would just call delivermail to deliver email rather than figuring out how to do it on an ad hoc (and often incompatible) basis. Delivermail/sendmail made no attempt to dictate how local mail should be stored or delivered; it did absolutely nothing except shuffle mail between other programs. (This changed when SMTP was added, as we'll see shortly.) In some sense it was just glue to hold the various mail systems together rather than being a mail system in its own right.

During the development of sendmail the Arpanet was transformed into the Internet. The changes were extensive, from the low level packets on the wire up through application protocols, and did not happen instantly. Sendmail was literally developed concurrently with the standards, and in some cases influenced them. It's also notable that sendmail has survived and even thrived as "the network" (as we think of it today) scaled from a few hundred hosts to hundreds of millions of hosts.

Another Network

It's worth mentioning that another completely separate mail standard was proposed at the time called X.400, which was a part of ISO/OSI (International Standards Organization/Open Systems Interconnect). X.400 was a binary protocol, with the message encoded using ASN.1 (Abstract Syntax Notation 1), which is still in use in some Internet protocols today such as LDAP. LDAP was in turn a simplification of X.500, which was the directory service used by X.400. Sendmail made no attempt whatsoever to be directly compatible with X.400, although there were some gateway services extant at the time. Although X.400 was initially adopted by many of the commercial vendors at the time, Internet mail and SMTP ended up winning in the marketplace.

17.2. Design Principles

While developing sendmail, I adhered to several design principles. All of these in some sense came down to one thing: do as little as possible. This is in sharp contrast to some of the other efforts of the time that had much broader goals and required much larger implementations.

17.2.1. Accept that One Programmer Is Finite

I wrote sendmail as a part-time, unpaid project. It was intended to be a quick way of making Arpanet mail more accessible to people at U.C. Berkeley. The key was to forward mail between existing networks, all of which were implemented as standalone programs that were unaware that more than one network even existed. Modifying more than a tiny amount of the existing software was infeasible with only one part-time programmer. The design had to minimize the amount of existing code that needed to be modified as well as the amount of new code that needed to be written. This constraint drove most of the rest of the design principles. As it turned out, in most cases they would have been the right thing to do even if there had been a larger team available.

17.2.2. Don't Redesign User Agents

A Mail User Agent (MUA) is what most end users think of as the "mail system"—it's the program that they use to read, write, and answer mail. It is quite distinct from the Mail Transfer Agent (MTA), which routes email from the sender to the receiver. At the time sendmail was written, many implementations at least partly combined these two functions, so they were often developed in tandem. Trying to work on both at the same time would have been too much, so Sendmail completely punted on the user interface problem: the only changes to MUAs were to have them invoke sendmail instead of doing their own routing. In particular, there were already several user agents, and people were often quite emotional about how they interacted with mail. Trying to work on both at the same time would have been too much. This separation of the MUA from the MTA is accepted wisdom now, but was far from standard practice at the time.

17.2.3. Don't Redesign the Local Mail Store

The local mail store (where messages would be saved until the recipient came along to read them) was not formally standardized. Some sites liked to store them in a centralized place, such as /usr/mail, /var/mail, or /var/spool/mail. Other sites liked to store them in the recipient's home directory (e.g., as a file called .mail). Most sites started each message with a line beginning "From" followed by a space character (an extraordinarily bad decision, but that was the convention at the time), but sites that were Arpanet-focused usually stored messages separated by a line containing four control-A characters. Some sites attempted to lock the mailbox to prevent collisions, but they used different locking conventions (file locking primitives were not yet available). In short, the only reasonable thing to do was treat local mail storage as a black box.

On nearly all sites, the actual mechanism for doing local mailbox storage was embodied in the /bin/mail program. This had a (quite primitive) user interface, routing, and storage built into one program. To incorporate sendmail, the routing portion was pulled out and replaced with a call to sendmail. A -d flag was added to force final delivery, i.e., it prevented /bin/mail from calling sendmail to do the routing. In later years the code used to deliver a message to a physical mailbox was extracted into another program called mail.local. The /bin/mail program exists today only to include a lowest common denominator for scripts to send mail.

17.2.4. Make Sendmail Adapt to the World, Not the Other Way Around

Protocols such as UUCP and BerkNET were already implemented as separate programs that had their own, sometimes quirky, command line structure. In some cases they were being actively developed at the same time as sendmail. It was clear that reimplementing them (for example, to convert them to standard calling conventions) was going to be painful. This led directly to the principle that sendmail should adapt to the rest of the world rather than trying to make the rest of the world adapt to sendmail.

17.2.5. Change as Little as Possible

To the fullest extent possible, during the development of sendmail I didn't touch anything I didn't absolutely have to touch. Besides just not having enough time to do it, there was a culture at Berkeley at the time that eschewed most formal code ownership in favor of a policy of "the last person who touched the code is the go-to person for that program" (or more simply, "you touch it, you own it"). Although that sounds chaotic by most modern-day standards, it worked quite well in a world where no one at Berkeley was assigned full time to work on Unix; individuals worked on parts of the system that they were interested in and committed to and didn't touch the rest of the code base except in dire circumstances.

17.2.6. Think About Reliability Early

The mail system prior to sendmail (including most of the transport systems) wasn't terribly concerned about reliability. For example, versions of Unix prior to 4.2BSD did not have native file locking, although it could be simulated by creating a temporary file and then linking it to a lock file (if the lock file already existed the link call would fail). However, sometimes different programs writing the same data file wouldn't agree on how the locking should be done (for example, they might use a different lock file name or even make no attempt to do locking at all), and so it wasn't that uncommon to lose mail. Sendmail took the approach that losing mail wasn't an option (possibly a result of my background as a database guy, where losing data is a mortal sin).

17.2.7. What Was Left Out

There were many things that were not done in the early versions. I did not try to re-architect the mail system or build a completely general solution: functionality could be added as the need arose. Very early versions were not even intended to be completely configurable without access to the source code and a compiler (although this changed fairly early on). In general, the modus operandi for sendmail was to get something working quickly and then enhance working code as needed and as the problem was better understood.

17.3. Development Phases

Like most long-lived software, sendmail was developed in phases, each with its own basic theme and feeling.

17.3.1. Wave 1: delivermail

The first instantiation of sendmail was known as delivermail. It was extremely simple, if not simplistic. Its sole job was to forward mail from one program to another; in particular, it had no SMTP support, and so never made any direct network connections. No queuing was necessary because each network already had its own queue, so the program was really just a crossbar switch. Since delivermail had no direct network protocol support, there was no reason for it to run as a daemon—it would be invoked to route each message as it was submitted, pass it to the appropriate program that would implement the next hop, and terminate. Also, there was no attempt to rewrite headers to match the network to which a message was being delivered. This commonly resulted in messages being forwarded that could not be replied to. The situation was so bad that an entire book was written about addressing mail (called, fittingly, !%@:: A Directory of Electronic Mail Addressing & Networks [AF94]).

All configuration in delivermail was compiled in and was based only on special characters in each address. The characters had precedence. For example, a host configuration might search for an "@" sign and, if one was found, send the entire address to a designated Arpanet relay host. Otherwise, it might search for a colon, and send the message to BerkNET with the designated host and user if it found one, then could check for an exclamation point ("!") signalling that the message should be forwarded to a designated UUCP relay. Otherwise it would attempt local delivery. This configuration might result in the following:

Input Sent 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}

Note that address delimiters differed in their associativity, resulting in ambiguities that could only be resolved using heuristics. For example, the last example might reasonably be parsed as {Uucp, foo, bar@baz} at another site.

The configuration was compiled in for several reasons: first, with a 16 bit address space and limited memory, parsing a runtime configuration was too expensive. Second, the systems of the time had been so highly customized that recompiling was a good idea, just to make sure you had the local versions of the libraries (shared libraries did not exist with Unix 6th Edition).

Delivermail was distributed with 4.0 and 4.1 BSD and was more successful than expected; Berkeley was far from the only site with hybrid network architectures. It became clear that more work was required.

17.3.2. Wave 2: sendmail 3, 4, and 5

Versions 1 and 2 were distributed under the delivermail name. In March 1981 work began on version 3, which would be distributed under the sendmail name. At this point the 16-bit PDP-11 was still in common use but the 32-bit VAX-11 was becoming popular, so many of the original constraints associated with small address spaces were starting to be relaxed.

The initial goals of sendmail were to convert to runtime configuration, allow message modification to provide compatibility across networks for forwarded mail, and have a richer language on which to make routing decisions. The technique used was essentially textual rewriting of addresses (based on tokens rather than character strings), a mechanism used in some expert systems at the time. There was ad hoc code to extract and save any comment strings (in parentheses) as well as to re-insert them after the programmatic rewriting completed. It was also important to be able to add or augment header fields (e.g., adding a Date header field or including the full name of the sender in the From header if it was known).

SMTP development started in November 1981. The Computer Science Research Group (CSRG) at U.C. Berkeley had gotten the DARPA contract to produce a Unix-based platform to support DARPA funded research, with the intent of making sharing between projects easier. The initial work on the TCP/IP stack was done by that time, although the details of the socket interface were still changing. Basic application protocols such as Telnet and FTP were done, but SMTP had yet to be implemented. In fact, the SMTP protocol wasn't even finalized at that point; there had been a huge debate about how mail should be sent using a protocol to be creatively named Mail Transfer Protocol (MTP). As the debate raged, MTP got more and more complex until in frustration SMTP (Simple Mail Transfer Protocol) was drafted more-or-less by fiat (but not officially published until August 1982). Officially, I was working on the INGRES Relational Database Management System, but since I knew more about the mail system than anyone else around Berkeley at the time, I got talked into implementing SMTP.

My initial thought was to create a separate SMTP mailer that would have its own queueing and daemon; that subsystem would attach to sendmail to do the routing. However, several features of SMTP made this problematic. For example, the EXPN and VRFY commands required access to the parsing, aliasing, and local address verification modules. Also, at the time I thought it was important that the RCPT command return immediately if the address was unknown, rather than accepting the message and then having to send a delivery failure message later. This turns out to have been a prescient decision. Ironically, later MTAs often got this wrong, exacerbating the spam backscatter problem. These issues drove the decision to include SMTP as part of sendmail itself.

Sendmail 3 was distributed with 4.1a and 4.1c BSD (beta versions), sendmail 4 was distributed with 4.2 BSD, and sendmail 5 was distributed with 4.3 BSD.

17.3.3. Wave 3: The Chaos Years

After I left Berkeley and went to a startup company, my time available to work on sendmail rapidly decreased. But the Internet was starting to seriously explode and sendmail was being used in a variety of new (and larger) environments. Most of the Unix system vendors (Sun, DEC, and IBM in particular) created their own versions of sendmail, all of which were mutually incompatible. There were also attempts to build open source versions, notably IDA sendmail and KJS.

IDA sendmail came from Linköping University. IDA included extensions to make it easier to install and manage in larger environments and a completely new configuration system. One of the major new features was the inclusion of dbm(3) database maps to support highly dynamic sites. These were available using a new syntax in the configuration file and were used for many functions including mapping of addresses to and from external syntax (for example, sending out mail as instead of and routing.

King James Sendmail (KJS, produced by Paul Vixie) was an attempt to unify all the various versions of sendmail that had sprung up. Unfortunately, it never really got enough traction to have the desired effect. This era was also driven by a plethora of new technologies that were reflected in the mail system. For example, Sun's creation of diskless clusters added the YP (later NIS) directory services and NFS, the Network File System. In particular, YP had to be visible to sendmail, since aliases were stored in YP rather than in local files.

17.3.4. Wave 4: sendmail 8

After several years, I returned to Berkeley as a staff member. My job was to manage a group installing and supporting shared infrastructure for research around the Computer Science department. For that to succeed, the largely ad hoc environments of individual research groups had to be unified in some rational way. Much like the early days of the Internet, different research groups were running on radically different platforms, some of which were quite old. In general, every research group ran its own systems, and although some of them were well managed, most of them suffered from "deferred maintenance."

In most cases email was similarly fractured. Each person's email address was "", where host was the name of the workstation in their office or the shared server they used (the campus didn't even have internal subdomains) with the exception of a few special people who had addresses. The goal was to switch to internal subdomains (so all individual hosts would be in the subdomain) and have a unified mail system (so each person would have an address). This goal was most easily realized by creating a new version of sendmail that could be used throughout the department.

I began by studying many of the variants of sendmail that had become popular. My intent was not to start from a different code base but rather to understand the functionality that others had found useful. Many of those ideas found their way into sendmail 8, often with modifications to merge related ideas or make them more generic. For example, several versions of sendmail had the ability to access external databases such as dbm(3) or NIS; sendmail 8 merged these into one "map" mechanism that could handle multiple types of databases (and even arbitrary non-database transformations). Similarly, the "generics" database (internal to external name mapping) from IDA sendmail was incorporated.

Sendmail 8 also included a new configuration package using the m4(1) macro processor. This was intended to be more declarative than the sendmail 5 configuration package, which had been largely procedural. That is, the sendmail 5 configuration package required the administrator to essentially lay out the entire configuration file by hand, really only using the "include" facility from m4 as shorthand. The sendmail 8 configuration file allowed the administrator to just declare what features, mailers, and so on were required, and m4 laid out the final configuration file.

Much of Section 17.7 discusses the enhancements in sendmail 8.

17.3.5. Wave 5: The Commercial Years

As the Internet grew and the number of sendmail sites expanded, support for the ever larger user base became more problematic. For a while I was able to continue support by setting up a group of volunteers (informally called the "Sendmail Consortium", a.k.a. who provided free support via e-mail and newsgroup. But by the late 1990s, the installed base had grown to such an extent that it was nearly impossible to support it on a volunteer basis. Together with a more business-savvy friend I founded Sendmail, Inc.2, with the expectation of getting new resources to bear on the code.

Although the commercial product was originally based largely on configuration and management tools, many new features were added to the open-source MTA to support the needs of the commercial world. Notably, the company added support for TLS (connection encryption), SMTP Authentication, site security enhancements such as Denial of Service protection, and most importantly mail filtering plugins (the Milter interface discussed below).

At of this writing the commercial product has expanded to include a large suite of e-mail based applications, nearly all of which are constructed on the extensions added to sendmail during the first few years of the company.

17.3.6. Whatever Happened to sendmail 6 and 7?

Sendmail 6 was essentially the beta for sendmail 8. It was never officially released, but was distributed fairly widely. Sendmail 7 never existed at all; sendmail jumped directly to version 8 because all the other source files for the BSD distribution were bumped to version 8 when 4.4 BSD was released in June 1993.

17.4. Design Decisions

Some design decisions were right. Some started out right and became wrong as the world changed. Some were dubious and haven't become any less so.

17.4.1. The Syntax of the Configuration File

The syntax of the configuration file was driven by a couple of issues. First, the entire application had to fit into a 16-bit address space, so the parser had to be small. Second, early configurations were quite short (under one page), so while the syntax was obscure, the file was still comprehensible. However, as time passed, more operational decisions moved out of the C code into the configuration file, and the file started to grow. The configuration file acquired a reputation for being arcane. One particular frustration for many people was the choice of the tab character as an active syntax item. This was a mistake that was copied from other systems of the time, notably make. That particular problem became more acute as window systems (and hence cut-and-paste, which usually did not preserve the tabs) became available.

In retrospect, as the file got larger and 32-bit machines took over, it would have made sense to reconsider the syntax. There was a time when I thought about doing this but decided against it because I didn't want to break the "large" installed base (which at that point was probably a few hundred machines). In retrospect this was a mistake; I had simply not appreciated how large the install base would grow and how many hours it would save me had I changed the syntax early. Also, when the standards stabilized a fair amount of the generality could have been pushed back into the C code base, thus simplifying the configurations.

Of particular interest was how more functionality got moved into the configuration file. I was developing sendmail at the same time as the SMTP standard was evolving. By moving operational decisions into the configuration file I was able to respond rapidly to design changes—usually in under 24 hours. I believe that this improved the SMTP standard, since it was possible to get operational experience with a proposed design change quite quickly, but only at the cost of making the configuration file difficult to understand.

17.4.2. Rewriting Rules

One of the difficult decisions when writing sendmail was how to do the necessary rewriting to allow forwarding between networks without violating the standards of the receiving network. The transformations required changing metacharacters (for example, BerkNET used colon as a separator, which was not legal in SMTP addresses), rearranging address components, adding or deleting components, etc. For example, the following rewrites would be needed under certain circumstances:

From To
a!b!c b!c@a.uucp
<,> <>

Regular expressions were not a good choice because they didn't have good support for word boundaries, quoting, etc. It quickly became obvious that it would be nearly impossible to write regular expressions that were accurate, much less intelligible. In particular, regular expressions reserve a number of metacharacters, including ".", "*", "+", "{[}", and "{]}", all of which can appear in e-mail addresses. These could have been escaped in configuration files, but I deemed that to be complicated, confusing, and a bit ugly. (This was tried by UPAS from Bell Laboratories, the mailer for Unix Eighth Edition, but it never caught on3.) Instead, a scanning phase was necessary to produce tokens that could then be manipulated much like characters in regular expressions. A single parameter describing "operator characters", which were themselves both tokens and token separators, was sufficient. Blank spaces separated tokens but were not tokens themselves. The rewriting rules were just pattern match/replace pairs organized into what were essentially subroutines.

Instead of a large number of metacharacters that had to be escaped to lose their "magic" properties (as used in regular expressions), I used a single "escape" character that combined with ordinary characters to represent wildcard patterns (to match an arbitrary word, for example). The traditional Unix approach would be to use backslash, but backslash was already used as a quote character in some address syntaxes. As it turned out, "$" was one of the few characters that had not already been used as a punctuation character in some email syntax.

One of the original bad decisions was, ironically, just a matter of how white space was used. A space character was a separator, just as in most scanned input, and so could have been used freely between tokens in patterns. However, the original configuration files distributed did not include spaces, resulting in patterns that were far harder to understand than necessary. Consider the difference between the following two (semantically identical) patterns:

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

17.4.3. Using Rewriting for Parsing

Some have suggested that sendmail should have used conventional grammar-based parsing techniques to parse addresses rather than rewriting rules and leave the rewriting rules for address modification. On the surface this would seem to make sense, given that the standards define addresses using a grammar. The main reason for reusing rewriting rules is that in some cases it was necessary to parse header field addresses (e.g., in order to extract the sender envelope from a header when receiving mail from a network that didn't have a formal envelope). Such addresses aren't easy to parse using (say) an LALR(1) parser such as YACC and a traditional scanner because of the amount of lookahead required. For example, parsing the address: <> requires lookahead by either the scanner or the parser; you can't know that the initial "allman@…" is not an address until you see the "<". Since LALR(1) parsers only have one token of lookahead this would have had to be done in the scanner, which would have complicated it substantially. Since the rewriting rules already had arbitrary backtracking (i.e., they could look ahead arbitrarily far), they were sufficient.

A secondary reason was that it was relatively easy to make the patterns recognize and fix broken input. Finally, rewriting was more than powerful enough to do the job, and reusing any code was wise.

One unusual point about the rewriting rules: when doing the pattern matching, it is useful for both the input and the pattern to be tokenized. Hence, the same scanner is used for both the input addresses and the patterns themselves. This requires that the scanner be called with different character type tables for differing input.

17.4.4. Embedding SMTP and Queueing in sendmail

An "obvious" way to implement outgoing (client) SMTP would have been to build it as an external mailer, similarly to UUCP, But this would raise a number of other questions. For example, would queueing be done in sendmail or in the SMTP client module? If it was done in sendmail then either separate copies of messages would have to be sent to each recipient (i.e., no "piggybacking", wherein a single connection can be opened and then multiple RCPT commands can be sent) or a much richer communication back-path would be necessary to convey the necessary per-recipient status than was possible using simple Unix exit codes. If queueing was done in the client module then there was a potential for large amounts of replication; in particular, at the time other networks such as XNS were still possible contenders. Additionally, including the queue into sendmail itself provided a more elegant way of dealing with certain kinds of failures, notably transient problems such as resource exhaustion.

Incoming (server) SMTP involved a different set of decisions. At the time, I felt it was important to implement the VRFY and EXPN SMTP commands faithfully, which required access to the alias mechanism. This would once again require a much richer protocol exchange between the server SMTP module and sendmail than was possible using command lines and exit codes—in fact, a protocol akin to SMTP itself.

I would be much more inclined today to leave queueing in the core sendmail but move both sides of the SMTP implementation into other processes. One reason is to gain security: once the server side has an open instance of port 25 it no longer needs access to root permissions. Modern extensions such as TLS and DKIM signing complicate the client side (since the private keys should not be accessible to unprivileged users), but strictly speaking root access is still not necessary. Although the security issue is still an issue here, if the client SMTP is running as a non-root user who can read the private keys, that user by definition has special privileges, and hence should not be communicating directly with other sites. All of these issues can be finessed with a bit of work.

17.4.5. The Implementation of the Queue

Sendmail followed the conventions of the time for storing queue files. In fact, the format used is extremely similar to the lpr subsystem of the time. Each job had two files, one with the control information and one with the data. The control file was a flat text file with the first character of each line representing the meaning of that line.

When sendmail wanted to process the queue it had to read all of the control files, storing the relevant information in memory, and then sort the list. That worked fine with a relatively small number of messages in the queue, but started to break down at around 10,000 queued messages. Specifically, when the directory got large enough to require indirect blocks in the filesystem, there was a serious performance knee that could reduce performance by as much as an order of magnitude. It was possible to ameliorate this problem by having sendmail understand multiple queue directories, but that was at best a hack.

An alternative implementation might be to store all the control files in one database file. This wasn't done because when sendmail coding began there was no generally available database package, and when dbm(3) became available it had several flaws, including the inability to reclaim space, a requirement that all keys that hashed together fit on one (512 byte) page, and a lack of locking. Robust database packages didn't appear for many years.

Another alternative implementation would have been to have a separate daemon that would keep the state of the queue in memory, probably writing a log to allow recovery. Given the relatively low email traffic volumes of the time, the lack of memory on most machines, the relatively high cost of background processes, and the complexity of implementing such a process, this didn't seem like a good tradeoff at the time.

Another design decision was to store the message header in the queue control file rather than the data file. The rationale was that most headers needed considerable rewriting that varied from destination to destination (and since messages could have more than one destination, they would have to be customized multiple times), and the cost of parsing the headers seemed high, so storing them in a pre-parsed format seemed like a savings. In retrospect this was not a good decision, as was storing the message body in Unix-standard format (with newline endings) rather than in the format in which it was received (which could use newlines, carriage-return/line-feed, bare carriage-return, or line-feed/carriage-return). As the e-mail world evolved and standards were adopted, the need for rewriting diminished, and even seemingly innocuous rewriting has the risk of error.

17.4.6. Accepting and Fixing Bogus Input

Since sendmail was created in a world of multiple protocols and disturbingly few written standards, I decided to clean up malformed messages wherever possible. This matches the "Robustness Principle" (a.k.a. Postel's Law) articulated in RFC 7934. Some of these changes were obvious and even required: when sending a UUCP message to the Arpanet, the UUCP addresses needed to be converted to Arpanet addresses, if only to allow "reply" commands to work correctly, line terminations needed to be converted between the conventions used by various platforms, and so on. Some were less obvious: if a message was received that did not include a From: header field required in the Internet specifications, should you add a From: header field, pass the message on without the From: header field, or reject the message? At the time, my prime consideration was interoperability, so sendmail patched the message, e.g., by adding the From: header field. However, this is claimed to have allowed other broken mail systems to be perpetuated long past the time when they should have been fixed or killed off.

I believe my decision was correct for the time, but is problematic today. A high degree of interoperability was important to let mail flow unimpeded. Had I rejected malformed messages, most messages at the time would have been rejected. Had I passed them through unfixed, recipients would have received messages that they couldn't reply to and in some cases couldn't even determine who sent the message—that or the message would have been rejected by another mailer.

Today the standards are written, and for the most part those standards are accurate and complete. It is no longer the case that most messages would be rejected, and yet there is still mail software out there that send out mangled messages. This unnecessarily creates numerous problems for other software on the Internet.

17.4.7. Configuration and the Use of M4

For a period I was both making regular changes to the sendmail configuration files and personally supporting many machines. Since a large amount of the configuration file was the same between different machines, the use of a tool to build the configuration files was desirable. The m4 macro processor was included with Unix. It was designed as a front end for programming languages (notably ratfor). Most importantly, it had "include" capabilities, like "#include" in the C language. The original configuration files used little more than this capability and some minor macro expansions.

IDA sendmail also used m4, but in a dramatically different way. In retrospect I should have probably studied these prototypes in more detail. They contained many clever ideas, in particular the way they handled quoting.

Starting with sendmail 6, the m4 configuration files were completely rewritten to be in a more declarative style and much smaller. This used considerably more of the power of the m4 processor, which was problematic when the introduction of GNU m4 changed some of the semantics in subtle ways.

The original plan was that the m4 configurations would follow the 80/20 rule: they would be simple (hence 20% of the work), and would cover 80% of the cases. This broke down fairly quickly, for two reasons. The minor reason was that it turned out to be relatively easy to handle the vast majority of the cases, at least in the beginning. It became much harder as sendmail and the world evolved, notably with the inclusion of features such as TLS encryption and SMTP Authentication, but those didn't come until quite a bit later.

The important reason was that it was becoming clear that the raw configuration file was just too difficult for most people to manage. In essence, the .cf (raw) format had become assembly code—editable in principle, but in reality quite opaque. The "source code" was an m4 script stored in the .mc file.

Another important distinction is that the raw format configuration file was really a programming language. It had procedural code (rulesets), subroutine calls, parameter expansion, and loops (but no gotos). The syntax was obscure, but in many ways resembled the sed and awk commands, at least conceptually. The m4 format was declarative: although it was possible to drop into the low-level raw language, in practice these details were hidden from the user.

It isn't clear that this decision was correct or incorrect. I felt at the time (and still feel) that with complex systems it can be useful to implement what amounts to a Domain Specific Language (DSL) for building certain portions of that system. However, exposing that DSL to end users as a configuration methodology essentially converts all attempts to configure a system into a programming problem. Great power results from this, but at a non-trivial cost.

17.5. Other Considerations

Several other architectural and development points deserve to be mentioned.

17.5.1. A Word About Optimizing Internet Scale Systems

In most network-based systems there is a tension between the client and the server. A good strategy for the client may be the wrong thing for the server and vice versa. For example, when possible the server would like to minimize its processing costs by pushing as much as possible back to the client, and of course the client feels the same way but in the opposite direction. For example, a server might want to keep a connection open while doing spam processing since that lowers the cost of rejecting a message (which these days is the common case), but the client wants to move on as quickly as possible. Looking at the entire system, that is, the Internet as a whole, the optimum solution may be to balance these two needs.

There have been cases of MTAs that have used strategies that explicitly favor either the client or the server. They can do this only because they have a relatively small installed base. When your system is used on a significant portion of the Internet you have to design it in order to balance the load between both sides in an attempt to optimize the Internet as a whole. This is complicated by the fact that there will always be MTAs completely skewed in one direction or the other—for example, mass mailing systems only care about optimizing the outgoing side.

When designing a system that incorporates both sides of the connection, it is important to avoid playing favorites. Note that this is in stark contrast to the usual asymmetry of clients and services—for example, web servers and web clients are generally not developed by the same groups.

17.5.2. Milter

One of the most important additions to sendmail was the milter (mail filter) interface. Milter allows for the use of offboard plugins (i.e., they run in a separate process) for mail processing. These were originally designed for anti-spam processing. The milter protocol runs synchronously with the server SMTP protocol. As each new SMTP command is received from the client, sendmail calls the milters with the information from that command. The milter has the opportunity to accept the command or send a rejection, which rejects the phase of the protocol appropriate for the SMTP command. Milters are modeled as callbacks, so as an SMTP command comes in, the appropriate milter subroutine is called. Milters are threaded, with a per-connection context pointer handed in to each routine to allow passing state.

In theory milters could work as loadable modules in the sendmail address space. We declined to do this for three reasons. First, the security issues were too significant: even if sendmail were running as a unique non-root user id, that user would have access to all of the state of other messages. Similarly, it was inevitable that some milter authors would try to access internal sendmail state.

Second, we wanted to create a firewall between sendmail and the milters: if a milter crashed, we wanted it to be clear who was at fault, and for mail to (potentially) continue to flow. Third, it was much easier for a milter author to debug a standalone process than sendmail as a whole.

It quickly became clear that the milter was useful for more than anti-spam processing. In fact, the milter.org5 web site lists milters for anti-spam, anti-virus, archiving, content monitoring, logging, traffic shaping, and many other categories, produced by commercial companies and open source projects. The postfix mailer6 has added support for milters using the same interface. Milters have proven to be one of sendmail's great successes.

17.5.3. Release Schedules

There is a popular debate between "release early and often" and "release stable systems" schools of thought. Sendmail has used both of these at various times. During times of considerable change I was sometimes doing more than one release a day. My general philosophy was to make a release after each change. This is similar to providing public access to the source management system tree. I personally prefer doing releases over providing public source trees, at least in part because I use source management in what is now considered an unapproved way: for large changes, I will check in non-functioning snapshots while I am writing the code. If the tree is shared I will use branches for these snapshots, but in any case they are available for the world to see and can create considerable confusion. Also, creating a release means putting a number on it, which makes it easier to track the changes when going through a bug report. Of course, this requires that releases be easy to generate, which is not always true.

As sendmail became used in ever more critical production environments this started to become problematic. It wasn't always easy for others to tell the difference between changes that I wanted out there for people to test versus changes that were really intended to be used in the wild. Labeling releases as "alpha" or "beta" alleviates but does not fix the problem. The result was that as sendmail matured it moved toward less frequent but larger releases. This became especially acute when sendmail got folded into a commercial company which had customers who wanted both the latest and greatest but also only stable versions, and wouldn't accept that the two are incompatible.

This tension between open source developer needs and commercial product needs will never go away. There are many advantages to releasing early and often, notably the potentially huge audience of brave (and sometimes foolish) testers who stress the system in ways that you could almost never expect to reproduce in a standard development system. But as a project becomes successful it tends to turn into a product (even if that product is open source and free), and products have different needs than projects.

17.6. Security

Sendmail has had a tumultuous life, security-wise. Some of this is well deserved, but some not, as our concept of "security" changed beneath us. The Internet started out with a user base of a few thousand people, mostly in academic and research settings. It was, in many ways, a kinder, gentler Internet than we know today. The network was designed to encourage sharing, not to build firewalls (another concept that did not exist in the early days). The net is now a dangerous, hostile place, filled with spammers and crackers. Increasingly it is being described as a war zone, and in war zones there are civilian casualties.

It's hard to write network servers securely, especially when the protocol is anything beyond the most simple. Nearly all programs have had at least minor problems; even common TCP/IP implementations have been successfully attacked. Higher-level implementation languages have proved no panacea, and have even created vulnerabilities of their own. The necessary watch phrase is "distrust all input," no matter where it comes from. Distrusting input includes secondary input, for example, from DNS servers and milters. Like most early network software, sendmail was far too trusting in its early versions.

But the biggest problem with sendmail was that early versions ran with root permissions. Root permission is needed in order to open the SMTP listening socket, to read individual users' forwarding information, and to deliver to individual users' mailboxes and home directories. However, on most systems today the concept of a mailbox name has been divorced from the concept of a system user, which effectively eliminates the need for root access except to open the SMTP listening socket. Today sendmail has the ability to give up root permissions before it processes a connection, eliminating this concern for environments that can support it. It's worth noting that on those systems that do not deliver directly to users' mailboxes, sendmail can also run in a chrooted environment, allowing further permission isolation.

Unfortunately, as sendmail gained a reputation for poor security, it started to be blamed for problems that had nothing to do with sendmail. For example, one system administrator made his /etc directory world writable and then blamed sendmail when someone replaced the /etc/passwd file. It was incidents like this that caused us to tighten security substantially, including explicitly checking the ownerships and modes on files and directories that sendmail accesses. These were so draconian that we were obliged to include the DontBlameSendmail option to (selectively) turn off these checks.

There are other aspects of security that are not related to protecting the address space of the program itself. For example, the rise of spam also caused a rise in address harvesting. The VRFY and EXPN commands in SMTP were designed specifically to validate individual addresses and expand the contents of mailing lists respectively. These have been so badly abused by spammers that most sites now turn them off entirely. This is unfortunate, at least with VRFY, as this command was sometimes used by some anti-spam agents to validate the purported sending address.

Similarly, anti-virus protection was once seen as a desktop problem, but rose in importance to the point where any commercial-grade MTA had to have anti-virus checking available. Other security-related requirements in modern settings include mandatory encryption of sensitive data, data loss protection, and enforcement of regulatory requirements, for example, for HIPPA.

One of the principles that sendmail took to heart early on was reliability—every message should either be delivered or reported back to the sender. But the problem of joe-jobs (attackers forging the return address on a message, viewed by many as a security issue) has caused many sites to turn off the creation of bounce messages. If a failure can be determined while the SMTP connection is still open, the server can report the problem by failing the command, but after the SMTP connection is closed an incorrectly addressed message will silently disappear. To be fair, most legitimate mail today is single hop, so problems will be reported, but at least in principle the world has decided that security wins over reliability.

17.7. Evolution of Sendmail

Software doesn't survive in a rapidly changing environment without evolving to fit the changing environment. New hardware technologies appear, which push changes in the operating system, which push changes in libraries and frameworks, which push changes in applications. If an application succeeds, it gets used in ever more problematic environments. Change is inevitable; to succeed you have to accept and embrace change. This section describes some of the more important changes that have occurred as sendmail evolved.

17.7.1. Configuration Became More Verbose

The original configuration of sendmail was quite terse. For example, the names of options and macros were all single characters. There were three reasons for this. First, it made parsing very simple (important in a 16-bit environment). Second, there weren't very many options, so it wasn't hard to come up with mnemonic names. Third, the single character convention was already established with command-line flags.

Similarly, rewriting rulesets were originally numbered instead of named. This was perhaps tolerable with a small number of rulesets, but as their number grew it became important that they have more mnemonic names.

As the environment in which sendmail operated became more complex, and as the 16-bit environment faded away, the need for a richer configuration language became evident. Fortunately, it was possible to make these changes in a backward compatible way. These changes dramatically improved the understandability of the configuration file.

17.7.2. More Connections with Other Subsystems: Greater Integration

When sendmail was written the mail system was largely isolated from the rest of the operating system. There were a few services that required integration, e.g., the /etc/passwd and /etc/hosts files. Service switches had not been invented, directory services were nonexistent, and configuration was small and hand-maintained.

That quickly changed. One of the first additions was DNS. Although the system host lookup abstraction (gethostbyname) worked for looking up IP addresses, email had to use other queries such as MX. Later, IDA sendmail included an external database lookup functionality using dbm(3) files. Sendmail 8 updated that to a general mapping service that allowed other database types, including external databases and internal transformations that could not be done using rewriting (e.g., dequoting an address).

Today, the email system relies on many external services that are, in general, not designed specifically for the exclusive use of email. This has moved sendmail toward more abstractions in the code. It has also made maintaining the mail system more difficult as more "moving parts" are added.

17.7.3. Adaptation to a Hostile World

Sendmail was developed in a world that seems completely foreign by today's standards. The user population on the early network were mostly researchers who were relatively benign, despite the sometimes vicious academic politics. Sendmail reflected the world in which it was created, putting a lot of emphasis on getting the mail through as reliably as possible, even in the face of user errors.

Today's world is much more hostile. The vast majority of email is malicious. The goal of an MTA has transitioned from getting the mail through to keeping the bad mail out. Filtering is probably the first priority for any MTA today. This required a number of changes in sendmail.

For example, many rulesets have been added to allow checking of parameters on incoming SMTP commands in order to catch problems as early as possible. It is much cheaper to reject a message when reading the envelope than after you have committed to reading the entire message, and even more expensive after you have accepted the message for delivery. In the early days filtering was generally done by accepting the message, passing it to a filter program, and then sending it to another instance of sendmail if the message passed (the so-called "sandwich" configuration). This is just far too expensive in today's world.

Similarly, sendmail has gone from being a quite vanilla consumer of TCP/IP connections to being much more sophisticated, doing things like "peeking" at network input to see if the sender is transmitting commands before the previous command has been acknowledged. This breaks down some of the previous abstractions that were designed to make sendmail adaptable to multiple network types. Today, it would involve considerable work to connect sendmail to an XNS or DECnet network, for example, since the knowledge of TCP/IP has been built into so much of the code.

Many configuration features were added to address the hostile world, such as support for access tables, Realtime Blackhole Lists, address harvesting mitigation, denial-of-service protection, and spam filtering. This has dramatically complicated the task of configuring a mail system, but was absolutely necessary to adapt to today's world.

17.7.4. Incorporation of New Technologies

Many new standards have come along over the years that required significant changes to sendmail. For example, the addition of TLS (encryption) required significant changes through much of the code. SMTP pipelining required peering into the low-level TCP/IP stream to avoid deadlocks. The addition of the submission port (587) required the ability to listen to multiple incoming ports, including having different behaviors depending on the arrival port.

Other pressures were forced by circumstances rather than standards. For example, the addition of the milter interface was a direct response to spam. Although milter was not a published standard, it was a major new technology.

In all cases, these changes enhanced the mail system in some way, be it increased security, better performance, or new functionality. However, they all came with costs, in nearly all cases complicating both the code base and the configuration file.

17.8. What If I Did It Today?

Hindsight is 20/20. There are many things I would do differently today. Some were unforeseeable at the time (e.g., how spam would change our perception of e-mail, what modern toolsets would look like, etc.), and some were eminently predictable. Some were just that in the process of writing sendmail I learned a lot about e-mail, about TCP/IP, and about programming itself—everyone grows as they code.

But there are also many things I would do the same, some in contradiction to the standard wisdom.

17.8.1. Things I Would Do Differently

Perhaps my biggest mistake with sendmail was to not recognize early enough how important it was going to be. I had several opportunities to nudge the world in the correct direction but didn't take them; in fact, in some cases I did damage, e.g., by not making sendmail stricter about bad input when it became appropriate to do so. Similarly, I recognized that the configuration file syntax needed to be improved fairly early on, when there were perhaps a few hundred sendmail instances deployed, but decided not to change things because I didn't want to cause the installed user base undue pain. In retrospect it would have been better to improve things early and cause temporary pain in order to produce a better long-term result.

Version 7 Mailbox Syntax

One example of this was the way version 7 mailboxes separated messages. They used a line beginning "From␣" (where "␣" represents the ASCII space character, 0x20) to separate messages. If a message came in containing the word "From␣" at the beginning of the line, local mailbox software converted it to ">From␣". One refinement on some but not all systems was to require a preceding blank line, but this could not be relied upon. To this day, ">From" appears in extremely unexpected places that aren't obviously related to email (but clearly were processed by email at one time or another). In retrospect I probably could have converted the BSD mail system to use a new syntax. I would have been roundly cursed at the time, but I would have saved the world a heap of trouble.

Syntax and Contents of Configuration File

Perhaps my biggest mistake in the syntax of the configuration file was the use of tab (HT, 0x09) in rewriting rules to separate the pattern from the replacement. At the time I was emulating make, only to learn years later that Stuart Feldman, the author of make, thought that was one of his biggest mistakes. Besides being non-obvious when looking at the configuration on a screen, the tab character doesn't survive cut-and-paste in most window systems.

Although I believe that rewriting rules were the correct idea (see below), I would change the general structure of the configuration file. For example, I did not anticipate the need for hierarchies in the configuration (e.g., options that would be set differently for different SMTP listener ports). At the time the configuration file was designed there were no "standard" formats. Today, I would be inclined to use an Apache-style configuration—it's clean, neat, and has adequate expressive power—or perhaps even embed a language such as Lua.

When sendmail was developed the address spaces were small and the protocols were still in flux. Putting as much as possible into the configuration file seemed like a good idea. Today, that looks like a mistake: we have plenty of address space (for an MTA) and the standards are fairly static. Furthermore, part of the "configuration file" is really code that needs to be updated in new releases. The .mc configuration file fixes that, but having to rebuild your configuration every time you update the software is a pain. A simple solution to this would simply be to have two configuration files that sendmail would read, one hidden and installed with each new software release and the other exposed and used for local configuration.

Use of Tools

There are many new tools available today—for example, for configuring and building the software. Tools can be good leverage if you need them, but they can also be overkill, making it harder than necessary to understand the system. For example, you should never use a yacc(1) grammar when all you need is strtok(3). But reinventing the wheel isn't a good idea either. In particular, despite some reservations I would almost certainly use autoconf today.

Backward Compatibility

With the benefit of hindsight, and knowing how ubiquitous sendmail became, I would not worry so much about breaking existing installations in the early days of development. When existing practice is seriously broken it should be fixed, not accommodated for. That said, I would still not do strict checking of all message formats; some problems can be easily and safely ignored or patched. For example, I would probably still insert a Message-Id: header field into messages that did not have one, but I would be more inclined to reject messages without a From: header field rather than try to create one from the information in the envelope.

Internal Abstractions

There are certain internal abstractions that I would not attempt again, and others that I would add. For example, I would not use null-terminated strings, opting instead for a length/value pair, despite the fact that this means that much of the Standard C Library becomes difficult to use. The security implications of this alone make it worthwhile. Conversely, I would not attempt to build exception handling in C, but I would create a consistent status code system that would be used throughout the code rather than having routines return null, false, or negative numbers to represent errors.

I would certainly abstract the concept of mailbox names from Unix user ids. At the time I wrote sendmail the model was that you only sent messages to Unix users. Today, that is almost never the case; even on systems that do use that model, there are system accounts that should never receive e-mail.

17.8.2. Things I Would Do The Same

Of course, some things did work well…


One of the successful side projects from sendmail was syslog. At the time sendmail was written, programs that needed to log had a specific file that they would write. These were scattered around the filesystem. Syslog was difficult to write at the time (UDP didn't exist yet, so I used something called mpx files), but well worth it. However, I would make one specific change: I would pay more attention to making the syntax of logged messages machine parseable—essentially, I failed to predict the existence of log monitoring.

Rewriting Rules

Rewriting rules have been much maligned, but I would use them again (although probably not for as many things as they are used for now). Using the tab character was a clear mistake, but given the limitations of ASCII and the syntax of e-mail addresses, some escape character is probably required7. In general, the concept of using a pattern-replace paradigm worked well and was very flexible.

Avoid Unnecessary Tools

Despite my comment above that I would use more existing tools, I am reluctant to use many of the run-time libraries available today. In my opinion far too many of them are so bloated as to be dangerous. Libraries should be chosen with care, balancing the merits of reuse against the problems of using an overly powerful tool to solve a simple problem. One particular tool I would avoid is XML, at least as a configuration language. I believe that the syntax is too baroque for much of what it is used for. XML has its place, but it is overused today.

Code in C

Some people have suggested that a more natural implementation language would be Java or C++. Despite the well-known problems with C, I would still use it as my implementation language. In part this is personal: I know C much better than I know Java or C++. But I'm also disappointed by the cavalier attitude that most object-oriented languages take toward memory allocation. Allocating memory has many performance concerns that can be difficult to characterize. Sendmail uses object-oriented concepts internally where appropriate (for example, the implementation of map classes), but in my opinion going completely object-oriented is wasteful and overly restrictive.

17.9. Conclusions

The sendmail MTA was born into a world of immense upheaval, a sort of "wild west" that existed when e-mail was ad hoc and the current mail standards were not yet formulated. In the intervening 31 years the "e-mail problem" has changed from just working reliably to working with large messages and heavy load to protecting sites from spam and viruses and finally today to being used as a platform for a plethora of e-mail-based applications. Sendmail has evolved into a work-horse that is embraced by even the most risk-averse corporations, even as e-mail has evolved from pure text person-to-person communications into a multimedia-based mission-critical part of the infrastructure.

The reasons for this success are not always obvious. Building a program that survives and even thrives in a rapidly changing world with only a handful of part-time developers can't be done using conventional software development methodologies. I hope I've provided some insights into how sendmail succeeded.


  4. "Be conservative in what you do, be liberal in what you accept from others"
  7. Somehow I suspect that using Unicode for configuration would not prove popular.