Postoffice is free software released under the terms of a BSD-style license.

If you find it useful, please consider making a contribution to help support onward development.


Postoffice 1.5.12, released 9-Aug-2018


Postoffice is a simple SMTP mail server and client. I wrote it for pell because spam was getting out of control and I would rather write my own mail service than continue to hack up sendmail for antispammery.

Postoffice provides a greylist to slow down the rate of incoming spam, and can be configured to do antivirus checking on incoming mail either via the sendmail milter protocol or a traditional hardcoded AV program.

Postoffice can be configured to deliver mail to accounts inside vm-pop3d-style virtual domains, and it can further be enabled to support AUTH LOGIN for people inside virtual domains, so they can use postoffice as a mail forwarder when replying to mail (I’ve also written some virtual domain utility programs to go along with the virtual domain code in postoffice and vm-pop3d.)

Postoffice accepts (and ignores) many of the same command line options that are passed to sendmail, and it comes with the usual crop of sendmail compatable aliases; runq, mailq, newaliases, and sendmail.

Source Code

=version 1.5.12= 1.5.12 cleans up a couple of long-standing buglets, plus portability tweaking for ARM, modern Linuxes, and Minix v3.

The long standing buglets are

1. If the incoming mail didn't have a Date: header, [postoffice](class:caps)
   would generate one every time it attempted to deliver the mail, so if
   the mail was rejected by a greylist subsequent deliveries would have
   more than one Date: header.   Most MTAs don't care, and will cheerfully
   accept a message with an arbitrary # of them, but some will become
   _very_ cross and will throw the message back with a 5xx "son, I don't
   like the tone of your headers" message.   This is not good behavior
   on [postoffice](class:caps)'s part, so I reworked the code that
   generates missing headers to only generate them once when the mail
   is received.

   (This is related to the Message-ID: bug in 1.5.10; the patch
   I did for that was incomplete and on reflection turned out to be a bit
   of a kludge, so the kludginess has been discarded for an _entirely new_
   collection of bodges!)
2. If a piece of mail is not going to be accepted (for reasons of
   `/etc/hosts.deny` blacklist, sender is pretending to be me, bogus
   recipient or sender, or whatnot, I was spitting out a 5xx error
   that included the reason why.

   The problem here is that sometimes I didn't have a reason why, and
   thus the error would be "sorry, but (null)" (or possibly a coredump)
   so now I give a different deny message if there's no recorded reason
   for the failure.
3. My dns resolver code had some places where it wasn't doing boundary
   checks, so a malformed dns record could possibly pull in garbage from
   the stack when fabricating a hostname from the dns record.

The portability tweaks are minor, but keep modern C compilers from
whining quite so much when the code is built:

1. change the order of `time_t` sizing; now `long`, `long long`, `int`
   (on ARM, the C compiler whines when `time_t` is `%d` despite
   `sizeof(int) == sizeof(long)`)
2. abstract `flock()` into locker.c (`locker()` function) to work around
   Minix (and V7?) lack of flock;  have the fcntl/flock tester verify
   that fcntl can unlock a file too
3. some machines have a 64 bit `time_t`; configure `TIME_T_FMT` as a
   printf/scanf format for it and use string concatenation to build format
   strings as needed.

=version 1.5.10= A few configuration tweaks, a few minimal code tweaks to make the code compile on Minux 3.3, and a couple of bugfixes:

The bugfix is that the spooler was adding a new message id
unnecessarily when resending messages that came in w/o a
message id (if a message was posted locally with a mua that
didn't add a message id, the message-id would be stuck in
the additional headers section of the control file in the
queue, so after every attempt to send it would scan be body,
not find a message id, then add another one) and I added
a short fix that would make it never add a message id on
respool (because it would have been added on the initial entry
into the queue.)

The minix 3 compatibility patches are to use `waitpid()` instead
of `wait3()` -- `wait3()` is defined as `wait350` in the system
headers, but that doesn't exist in libc -- and to change the
prototype for the local copy of `setproctitle()` to take
`const char*` instead of `char*` -- again, because `setproctitle()`
was defined in the system headers but, again, wasn't in libc.

There were also a bunch of `` tweaks done to make
everything continue to configure and build on my pile of machines
(Freebsd 4.1, Darwin 16.6, Centos 7.?, some modern Debian, and
now the Minux 3.3 vm I'm playing around with.)

=version 1.5.10(b3)= Half a years worth of updates, plus multiple passes through to make it work more placidly on modern Linuxes. I’d set the version to (b3) so I could see if anything exploded before turning around and putting TLS into the thing, but TLS is awful enough (due to all the published TLS libraries having extremely complex published interfaces) so that this bit of coding is running extremely slowly.

But `1.5.10(b3)` has the advantage of a `` that works on
modern linuxes without dying on older systems, plus a small pile of
revisions to improve reliability:

*   Use strlcpy (or a local version of it on systems that don't have it) instead of strncpy/d[len-1]=0 pairs
*   Add a service file for linux machines that use systemd
*   Substitute for in email/www addresses in the documentation
*   RFC 3865 support (NO-SOLICITING reply to EHLO)
*   In `anotherheader()` we need to remove newlines from the data we're adding.
*   If immediate is set, note it in DEBUG output

=version 1.5.9= A year’s worth of updates to make the code work better on Apple OSX and modern versions of Linux: * Tweak the punctuation on the various “you are tcp wrappered; go away” messages * Display env->localhost as part of the DEBUG output * Set a default for env->localhost after doing all the configfile and command line options. * Add the option ohostname=auto|literal|name; 1. if auto, do as we’ve always done (get machine name from uname, then use gethostbyname to pick up the canonical dns name) 2. if literal, get machine name from uname 3. otherwise use “name”.

  This means I needed to do some changes to the way I process
  `/etc/` (or whatever config file is set with `-C`);
  I now make a pass of the command line processing everything EXCEPT
  `-o` arguments, then process `/etc/` (`-C` config files
  were processed when they were encountered on the command line),
  and finally make a second pass on the command line to process
  `-o` options.)
* If PAM is configured, add "PAM: T" to the output from DEBUG
* The flock sanity test was bizarrely & brokenly wrong?
* Add PAM support for AUTH LOGIN (`--with-pam` configuration option)
* Check for existance of pw && pass && crypt(pass,pw) before strcmp'ing them.
* If --with-gdbm, don't even bother to look for ndbm routines (check $WITH_GDBM)
* Handle SIGTSTP (signal 18) on modern Unices (temp kludge;  need a wrapper)
* add `postoffice -bD` (fake daemonize w/o going to bg for Linux `systemd`
  rc files)
* have the ns debug program display AAAA records as AAAA records instead of unknown type records
* Fix mf.c & smtp.c to compile if milters are not defined
* Add plists for osx daemonisation (tested on osx 10.5, nothing newer)
* If the message is being dumped to the spam folder because of tcpwrappers,
  add a X-Spam header with the reason why.

=version 1.5.8= Fix a longstanding bug in the milter code where the headers weren’t being processed properly. It turns out that I keep the FROM: and RCPT: addresses with the trailing \r\n, and the mail filter I use to talk to spamassassin (and I suspect other people use as well) was happily constructing new headers (X-Envelope… ) out of those addresses and passing them along.

The extra newlines meant that everything after the first new
header was treated as part of the body, with the expected
results of an annoyingly large collection of false positives
for spam.    This is not the way I want a spam filter to work,
so I went in and fixed this particular wagon, which demands a
new release after 49 months.

=version 1.5.5= Add one new option (“msp” in which tells postoffice to accept connections on the message submission port as well as on port 25 (the standard smtp port.) This is so clients that live behind firewalls that block port 25 can actually connect to the mta to send mail.

No additional restrictions are applied to connections that
come in via the msp port.

=version 1.5.4= A few defects were fixed in this release and I cleaned up some of the code flow in smtp.c to reduce some duplication. There are no changes in functionality (I’m in the throes of trying to implement STARTTLS, but openssl is somewhat painful to try and comprehend, so this is not going to be happening just yet :-(

The bugfixes are:

1. `auth.c` had a defect in that if you passed it a null username,
   it would never set `addr->user` and thus pass a **NULL** to the
   library function `getpwnam()`.   This is an undefined behavior,
   and the way it works on FreeBSD 7.1 is that the application segfaults
   deep in the bowels of the library auth code associated with `getpwnam()`.
   So I had to correct the offending function to check to see if `addr->user`
   was null, and then to return a failure if it was.
2. I corrected three problems with the milter interface:
   a. only handle failure results in the DATA phase of the protocol,
   b. it was doing the protocol phase handshake incorrectly (it
  needed to tell milter libraries that it was willing to
  not to any phases, and then the milter library would
  return a bitmask of the phases it was expecting,) and
   c. I was not handling temporary failure results at all gracefully.
3. The `-C` option now uses the specified configuration files _instead_
   of `/etc/`, not in addition to it.
4. a `HUP` signal is now ignored by the smtp server.
5. `listq` now truncates the reason-for-delay line, then manually adds a
   newline instead of the old behavior of printing out the first 65
   characters of the line and hoping that there's a newline there.

=version 1.5.3= This version fixes 1 fairly annoying defect; I had miscoded part of the milter library so that when it sent the mail message off to the milter for processing it would only process one header line as a header, and the rest as the body of the message. This (embarrassingly trivial) fix needs to be a new version, because I’m not adding new features very quickly these days.

=version 1.5.2= 1. On FreeBSD 7.1, setreuid() does not work as expected; it doesn’t give up privileges, but was instead writing files AS ROOT. That is bad. So I’m not even going to try to give up and regain privileges inline; instead I’ll just fork off a child process which will give up privileges and THEN attempt to write. 2. It turns out that my home router drops packets when they’re pushed in too quickly. I discovered that when mail sessions, including ones from gehenna, started timing out during the DATA part of a transaction. But while debugging this feature (which I can’t work around, because a new router would cost >$100; I’m working around it by putting the mail server on gehenna and popping in to check mail) I found a few things that I needed to check.

   * I'm checking for the existance of `setlinebuf()` in, but was not actually bothering to use
   the resulting #define in my code.
   * if the close-on-exec fcntl exists, use it on the
   server socket.

3. Pay attention to the active flag for virtual domains; if
   that flag is 0, the domain is not active (previously I
   was just checking to see if the flag existed to see if it
   was active.)
4. I was handling MXes improperly; the queue running code
   was trying to deliver mail to the highest _numeric_ value
   of MX, instead of the highest _priority_ MX (MX priorities
   are like D&D armor classes; lower is better.)   This was
   fixed, plus I randomise MXes of the same priority so that
   they pseudo-round robin.
5. `data()` has been reworked to clean up the dot state machine
   and to make it smaller.   Dropping \r's with prejudice made
   the handling of EOL.EOL a lot easier, but I still had a lot
   of stubby nonsense from the old code there.
6. `smtpbugcheck()` now uses `mfcomplain()` to dump out whatever
   error messages milters give me for rejecting a letter.  `mfcomplain()`
   strips the extraneous numeric codes off the front of the error
   message, which cuts down on clutter.
7. Finally, I was using `open()`/`read()`/`close()` to read the
   contents of a `.forward` file into memory.  This left me open
   to fun attacks because I wasn't `bzero()`ing the buffer between
   reads, and a short .forward would just be overlaid on top of
   a long  .forward instead of replacing it.

   I changed that to use `FILE*`s which do all the trimming and 
   stuff by magic.

=version 1.5.1= Add support for * mailq -q{pattern} – not implemented but now it doesn’t complain. * better support for smtpauth blacklists; change the diagnostic messages for a blacklisted address so it doesn’t give a “come back in (some large# of) seconds” message. * Add the new feature of being able to smtpauth blacklist on MAIL FROM: addresses.

=version 1.5.0= 1.5.0 introduces the next option mxpool, which is used for domains that have multiple mail exhangers but which still want all the mail to go to one server for local processing. If you set mxpool (-omxpool on the command line, or mxpool=1 in, postoffice will relay local mail to higher priority mail exchangers if they exist in the dns, otherwise it will deliver them to the local machine.

For example, the domain []( has two
mail exchangers   IN MX   10   IN MX   5

If a client attempts to deliver mail for <> to
``, the copy of [postoffice](class:caps)
there will accept it as if it was a local delivery, but then
put it into the outbound mail queue for forwarding to
``.    Once relayed to ``,
the mail will be delivered locally, because `postoffice` is the
highest priority mail exchanger in the forest.

=version 1.4.10= There appear to be mailers out there that spit out extraneous CR’s when they try to pad things up to make the end of lines be CRLF. And postoffice would get very confused about it, and transpose the CR and the character immediately before it. Which led to some really interesting looking unreadable mail. For a long time, the only mail I got with this sort of transposition was spam, so I ignored it, but I’ve recently started to see real mail with this sort of line ending; I redid the line ending code to stop transpositions, but then I’d get really-long-lined mail with CR at the end of every single line, which would lead to mailers (like apple’s thinking there was nothing to the message except headers containing embedded CRs.

So, [version 1.4.10](postoffice-1.4.10.tar.gz) fixes that by the
simple RFC2811-breaking expedient of silently discarding naked
`CR`s in the SMTP body.   And now the mail that previously had
`CR`s just before the last character in a line doesn't, and I
can read the mail with both `mutt` and ``.

=version 1.4.9= AUTH LOGIN was not working, because the Username: string was being uppercased during message writing and, in addition, domain 0 handling wasn’t working inside auth.c. 1.4.9 corrects this defect and makes postoffice work better with networked mail clients.

=version 1.4.8= 1.4.8 adds no features, but fixes a couple of problems when trying to build with Xcode on macos 10.5; 1. INT_MAX is no longer found in the twisty maze of include files postoffice uses, so I had to add <limits.h> to the offending modules. 2. In AC_SUB, blank expansions were coming out as ^?, which is not what I had in mind. Macos uses the gnu sh clone instead of a real Bourne shell, and that was having echo ${@} expand to ^? inside the shell? Okay, colo(u)r me confused, but I fixed it, I think.

(there is no 1.4.7; I bobbled the version#s and skipped a

=version 1.4.6= 1.4.6 adds a new configuration option – trusted=hostname, which tells postoffice to treat hostname as if it was localhost. In addition, I’ve tweaked the mx getter so that if you set localmx, you need to to explicitly set the MX for the remote machine before it’s treated as localhost.

On the bugfix front, when I went in and attempted to make
`gcc -Wall` quieter, I managed to break builds without the
milter code enabled.   So 1.4.6 fixes that.

=version 1.4.5c= This version sweeps some of the grottier parts of the milter interface up into tidy stub functions, plus changes the way postoffice handles RCPT TO: before MAIL FROM:; it used to spit out a 501 error before it dropped the address, but it now returns a 250 before it drops the address.

I'm willing to do this because of the milter problem I corrected
in [[postoffice](class:caps) 1.4.0](#1.4.0) -- since some common
milters (spamassassin, clamav) drop into an infinite loop when
they get **`RCPT TO:`** before **`MAIL FROM:`**, that means that 
it never happens under the higher-priced brand and so I won't
have to worry about legitimate mail tripping over this feature.

=version 1.4.5a= 1.4.5a finished the project to make it so that you can configure it with CC=`gcc -Wall` and get a clean build without errors. The code changes are gross (and inefficient, and quite possibly bugridden,) but now stupid gcc will pass them right on by without complaint.

=version 1.4.5= 1.4.5 fixes a minor bug (spam=bounce wasn’t setting spam=bounce; this was reported by Bob Dunlop, who was evaluating postoffice for inclusion in an ARM-based system)) and has a fairly extensive code scrub to sweep out things that the FSF’s so-called “C” compiler complains about. Some of this was simply housekeeping (I added a configure check for the volatile keyword, then plastered volatile prefixes on everything that gcc complained about in functions containing setjmp/longjmp; on systems that don’t support volatile it #defines that keyword to an empty comment,) some involved some minor code rework (forcing an assignment to a variable that wouldn’t be set if a loop failed; previously I was failing the condition when loop counter==max,) and some involved tweaking configure to disable the stupid warning (I combine assignments and tests by doing if ( var = setter() ), and gcc whines bitterly about this unless I change my coding style or append -Wno-parentheses to CFLAGS. I’m not going to change my coding style after 30 years of programming, so I have changed CFLAGS instead.)

=version 1.4.4= 1.4.4 adds spam=-like configuration options to manage connections from blacklisted sites similar to how spam-ridden messages are currently handled. Like spam=, blacklist= supports bounce, accept, and file options, which work much like they work with spam= =blacklist=bounce= Refuse connections from blacklisted hosts (this is the default.) =blacklist=accept= Accept connections from blacklisted hosts. Aside from getting a REJECT message in syslog, this is as if you didn’t have any blacklisting at all. =blacklist=file:folder= Accept connections from blacklisted hosts, but drop all (local) mail from those hosts into a blacklist folder. The format of the folder name is identical to spam=folder, with a ~/ prefix meaning a folder in the user’s home directory, otherwise a folder in the mailspool.

=version 1.4.3b= 1.4.3b contains one tiny bugfix that corrects an annoying (but not, as far as I can tell, fatal) error; when a message is initially passed into postoffice, it is scanned to see if it contains a Date:, Message-ID:, or From: header. If it does not, those headers are generated and placed into the additional headers section of the control file. The bug is that postoffice didn’t bother to ever check the additional headers section for those required headers, and would thus add a new copy of the originally missing headers every time a queuerun would happen. If you’re attempting to send mail to a site that refuses to accept it, the additional headers section will grow without bound. It’s a dumb coding error, but by the simple expedient of pulling the header scanner into a separate function I could fix it without causing too much additional code bloat.

=version 1.4.3a= 1.4.3a revises the junkfolder= feature that I introduced in 1.4.3; I’ve renamed the setting to spam=, which can now take three values: =bounce[:why]= Bounce the spam (the traditional – and default – behavior.) why is an optional reason that you can give (one line only) for bouncing the spam-infested mail. =accept= Treat the spam-infested message as a regular mail message. The only way to automatically tell that it’s a spam-infested message is that postoffice adds an X-Spam: header to the message that contains the diagnostic message from the milter or av program. If the incoming message already contains an X-Spam header, that header will not be removed. =folder:path= This is what junkfolder= has become. And the behavior of spam=folder has slightly changed here; local users will have their spam-infested mail delivered to the spamfolder, but remote (and virtual domain) users will have the spam delivered as if the setting was spam=accept. Spam=folder is now a fancy enhancement to spam=accept that doesn’t require every user to use procmail to weed out the X-Spam'med messages. The folder format is slightly different. There are now two types of valid path: 1. ~/path, which writes the spam to the folder path in the users home directory.  path can be anything (it’s treated like a /path in a .forward file), so the restrictions on file redirections apply here. 2. suffix, which writes the spam to the folder username:suffix in the mail spool directory. If you’re using imap, this might be a better solution than writing the spam to the users home directory, though the usual caveats about the file growning without bound still apply.

As an extra treat, I've actually documented the spam= option now.

=version 1.4.3= 1.4.3 introduces one new feature; if you set the config file option junkfolder=, unwanted mail will not be bounced but will instead be put into a junkmail folder named <user>:<junkfolder>. This is a first release of this feature, implemented in a hurry because I needed to change my spamcatching behavior so I could catch spamalike mail coming in from my relatives.

As an artifact of the hasty release, there is no documentation
other than what's written here.  It's possible that the
functionality will alter over the next few weeks, but if you
desire adventure this is the software release for you.

=version 1.4.2a= No new features, no new bugfixes, but I’ve switched to git for my SCCS, so I wanted to publish a warning release to see if the new version control system will end up eating my brane.

=version 1.4.2= No new features in this one, just bugfixes: 1. the am64 ubuntu port ran into a debug statement where I didn’t wrap a pair of vfprintf()s in va_start/va_end, but spread them around both of them. On the 32 bit Linuxes I’ve installed on this doesn’t make a difference, but the 64 bit world is, um, different. Ooops. 2. the other bug was the half-expected rebreaking on MacOS support. Despite having a macbook (which I am even now typing these release notes into), I hadn’t actually bothered to get my own copy of XCode and didn’t test the resolver detection changes for MacOS on a MacOS system. Ooops(2).

=version 1.4.1= 1.4.1 fixes some of the bugs that were in 1.4.0, adds missing features (and the missing manual page for usermap(7),) and has been tweaked so that it builds on another linux variant (ubuntu on an am86 machine.)

####New features
 1. Adds the usermap target ~, which is the username matched
    by a ~ in a usermap pattern.
 2. Allows multiple usermaps.
 3. List usermaps as part of the SMTP **DEBUG** output.
 4. The missing `usermap(7)` manpage.

 1. Does case-insensitive matching when reading a personal alias file
(the Lego shop-at-home website [UPPERCASES](class:caps) all user names,
so **`SATH-ORC`** was not matching the sath-orc entry in my
~/.alias file.  Ooops.)
 2. Use the new function `AC_CHECK_RESOLVER` (in
to look for the presence of the Berkeley resolver library.
This code attempts to autodetect the broken
resolver library (Darwin, being essentially a FreeBSD
branch, has library interfaces that mutate just as fast as
gl\*bc does. Using `BIND_8_COMPAT` is only a temporary patch,
and I'll probably have to switch to the djbdns client
library to get a more-stable replacement for `res_query()`,)
so it may break Darwin anew.
 3. Lots and lots of code cleanup to remove unused variables
    and add missing headers.

The resolver library detection fix and the code cleanup was
because of a bug report from <cite>Wink Saville</cite>, who
tried to build [postoffice](class:caps) on a 64-bit [ubuntu
7.04]( machine.  The resolver library
wasn't detected because there doesn't appear to be a `res_query()`
in glibc 2.8 (it's a `#define` for `__res_query()`.  Ugh,)
then, after I tweaked so that it would actually
DETECT the resolver, it dumped core because I didn't include
&lt;time.h&gt; before using `strftime()` [`sizeof(time_t)` !=
`sizeof(int)` on an am64 machine] so I had to go on a binge of
adding the appropriate `#include`'s to circumvent any
that were in the code.  (There are still quite a few implicit
global functions inside [postoffice](class:caps), but those
are functions that return small scalars (I'm not _overly_
worried about what happens when a message gets more than 2^31
unique recipients on it; I suspect that by that time the machine
would be so far into swap that the heat death of the Sun would
happen before it finished processing the headers.)

=version 1.4.0= 1.4.0 introduces the new feature of usermaps, which are a way to let users have temporary mail addresses which they can use when they deal with possible spammers. A usermap is simply a personal alias file (formatted like the aliases(5) file) which is placed in a home directory, and which is referred to by a usermap option in

The `usermap` option is formatted as _pattern_:_target_{,_target_};
the pattern is a shell-style wildcard, with the addition of using
the **~** to match any valid user in the domain.   A match is either
an alias or the special token **~/filename**, which is the address
of a personal alias file.  When a usermap is called,
[postoffice](class:caps) will try each _target_, stopping when it
matches one.

> For example, the usermap **\*-~**:**~/.alias**,**bounce** will
> match any mail address of the format _something_-_user_.  If
> it matches an address, [postoffice](class:caps) will first
> see if the address is in **~user/.alias**, and then if it doesn't
> find a match there it will map to the fixed address **bounce**.

There are also a couple of trivial bugfixes in 1.4.0
 1. [Postoffice](class:caps) wraps smtp sessions in several
layers of timeout.   There were some cases where an alarm
would fire when the default signal catcher was running, so
you'd end up with core dump and a crash warning for a
completely normal timeout.
 2. A second, and more annoying, bug was discovered when testing the
new usermap code.   [Postoffice](class:caps) is fairly
relaxed about the order of **`MAIL FROM`** and
**`RCPT TO`** commands and will accept them in any
order as long as both of them have been issued when a **`DATA`**
command arrives.   But some of the sendmail filters (milters)
I use are not so forgiving, and if they get a **`RCPT TO`**
prior to a **`MAIL FROM,`** they will freeze and lock the
mail session.   This is bad programming, to say the least,
but it's broken in a sendmail-compatable fashion so it's
not likely that it will ever change.  So I've crippled
postoffice (if built with `--with-milter`) to whine bitterly
about **`RCPT TO`** without a prior MAIL FROM.  <li>A
super-trivial bugfix is a tweak to to test
for the existance of `malloc.h` so I can only include it
on machines that actually have it.   This is a generalization
of the **`OS_DARWIN`** support that Andras Salamon contributed
for 1.3.8c, and should make the code a little bit more
portable to other machines that use C compilers that blindly
follow the whims of the (break-all-of-the-)standards
Older versions of the code are still available.