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
.
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
If the incoming mail didn’t have a Date: header, postoffice 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’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!)
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.
The portability tweaks are minor, but keep modern C compilers from whining quite so much when the code is built:
time_t
sizing; now long
, long long
, int
(on ARM, the C compiler whines when time_t
is %d
despite
sizeof(int) == sizeof(long)
)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 tootime_t
; configure TIME_T_FMT
as a
printf/scanf format for it and use string concatenation to build format
strings as needed.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 configure.sh
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.)
Half a years worth of updates, plus multiple passes through
configure.sh
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 configure.sh
that works on
modern linuxes without dying on older systems, plus a small pile of
revisions to improve reliability:
anotherheader()
we need to remove newlines from the data we’re adding.ohostname=auto|literal|name
;
auto
, do as we’ve always done (get machine name from uname,
then use gethostbyname to pick up the canonical dns name)literal
, get machine name from unameThis means I needed to do some changes to the way I process
/etc/postoffice.cf
(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/postoffice.cf
(-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.)
--with-pam
configuration option)postoffice -bD
(fake daemonize w/o going to bg for Linux systemd
rc files)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.
Add one new option (“msp” in postoffice.cf
) 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.
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:
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.-C
option now uses the specified configuration files instead
of /etc/postoffice.cf
, not in addition to it.HUP
signal is now ignored by the smtp server.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.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.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 pop
ping in to check mail) I found a few things that I needed to
check.
setlinebuf()
in
configure.sh, but was not actually bothering to use
the resulting #define in my code.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.)
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.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.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.
mailq -q{pattern}
– not implemented but now
it doesn’t complain.smtpauth
blacklists; change
the diagnostic messages for a blacklisted address
so it doesn’t give a “come back in (some large#
of) seconds” message.MAIL FROM:
addresses.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.cf
), 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 tsfr.org has two mail exchangers
tsfr.org. IN MX 10 parcelpost.tsfr.org.
tsfr.org. IN MX 5 postoffice.tsfr.org.
If a client attempts to deliver mail for orc@tsfr.org to
parcelpost.tsfr.org
, the copy of postoffice
there will accept it as if it was a local delivery, but then
put it into the outbound mail queue for forwarding to
postoffice.tsfr.org
. Once relayed to postoffice.tsfr.org
,
the mail will be delivered locally, because postoffice
is the
highest priority mail exchanger in the forest.
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 Mail.app
) thinking
there was nothing to the message except headers containing embedded
CR
s.
So, version 1.4.10 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 Mail.app
.
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.<limits.h>
to the offending modules.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 number)
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.
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 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.
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.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.)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=
spam=folder
,
with a ~/
prefix meaning a folder in the user’s home
directory, otherwise a folder in the mailspool.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.junkfolder=
feature that I
introduced in 1.4.3; I’ve renamed the setting to
spam=
, which can now take three values:
why
is an optional reason that you can give (one line
only) for bouncing the spam-infested mail.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:
~/
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.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.
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.
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.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.)
usermap(7)
manpage.SATH-ORC
was not matching the sath-orc entry in my
~/.alias file. Ooops.)AC_CHECK_RESOLVER
(in
configure.sh)
to look for the presence of the Berkeley resolver library.
This code attempts to autodetect the broken
Darwin
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.The resolver library detection fix and the code cleanup was
because of a bug report from Wink Saville, who
tried to build postoffice 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 configure.inc so that it would actually
DETECT the resolver, it dumped core because I didn’t include
<time.h> 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
all-the-world’s-a-VAX‘isms
that were in the code. (There are still quite a few implicit
global functions inside postoffice, but those
are functions that return small scalars (I’m not overly
worried about what happens when a message gets more than 231
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.)
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 postoffice.cf(5)
.
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 will try each target, stopping when it
matches one.
There are also a couple of trivial bugfixes in 1.4.0For example, the usermap *-~:~/.alias,bounce will match any mail address of the format something-user. If it matches an address, postoffice 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.
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. 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
committees.