the Maildir standard
The Maildir standard is a blessing to mail reliability. You can read
about it here and
here.
A maildir is a directory with 3 subdirectories, named
tmp,
new and
cur.
The following is my interpretation of a completely compliant Maildir delivery:
-
Generate a name for the message, consisting of time.pid.host.
time is a normal UNIX timestamp (looks like 982493758).
pid is something that doesn't repeat within one second on a
single machine. host is the hostname of the machine that is
running the delivery process.
pid is often the process ID of the delivering process,
sometimes extended with the value of a counter internal to that
process. It may also be the output from some reliable sequence
number generator prepended with "#".
-
Start a 24 hour timer. If the timer expires, abort delivery.
-
chdir() into the Maildir.
-
stat() tmp/time.pid.host. If it returns
ENOENT, continue with the next step. Otherwise, wait 2 seconds
and try again (generating a new compliant filename first), for a limited
number of times.
-
Create the file in tmp/.
-
Write the message to the file, checking every returnvalue of
write() during this process.
-
fsync() and close() the file, checking the
returnvalues of both syscalls.
-
link() the message to new/time.pid.host
and check the returnvalue of the systemcall.
-
Message delivered.
An implementation may attempt to unlink() the file in
tmp/ before exiting, irregardless of whether delivery
succeeded.
Maildir readers look in new/ for new messages. Readers may
freely read and remove new/unique.
They may also rename messages from new/unique to
cur/unique:info. [I think this is a mistake.
The rename should be replaced by a link() and unlink().
What if, somehow, a message gets delivered, read, moved to
cur/, and then another message is delivered into
new/ with the *same* 'unique' filename? The rename will
overwrite the old copy.]
The following is a list of named 'requirements'. Those with
Name mentioned in
bold are part of the Maildir standard and
are required for a compliant implementation. All the others are features
that can be implemented for good karma but are not required.
Some of the requirementes listed only apply to readers or only to MDA's.
|
Name
|
Requirement
|
|
24hourtimer
|
Implementing the 24hour timer.
|
|
stattmp
|
stat()'ing the name for tmp
|
|
unique
|
generating a unique filename according to the spec.
|
|
write-check
|
checking the returnvalue of write() everytime.
|
|
fsync
|
doing fsync().
|
|
close
|
doing close().
|
|
fsync-check
|
checking fsync()'s returnvalue.
|
|
close-check
|
checking close()'s returnvalue.
|
|
link
|
using link() to move the message into new/.
|
|
link-check
|
checking link()'s returnvalue.
|
|
success-unlink
|
unlinking from tmp/ after successful delivery.
|
|
failure-unlink
|
unlinking from tmp/ after failed delivery.
|
|
clean-tmp
|
(readers) cleaning up files older than 36 hours in tmp/
|
|
skip-dot
|
(readers) skipping filenames starting with dot when reading a maildir
|
|
quit-rename
|
renaming files from new/ to
cur/unique:info when a reader exits
|
|
quit-rename-link
|
implementing quit-rename through link() and
unlink()
|
I have audited the Maildir-implementations in several pieces of software.
My findings:
-
qmail-local implements
all writer-requirements correctly,
including success-unlink and failure-unlink. It tries
stattmp 3 times.
-
qmail-pop3d implements
clean-tmp, skip-dot
and quit-rename.
-
procmail-3.22 implements
link somewhat weird.
link-check is implemented by checking the return value
of link(), and if this indicates an error, procmail compares
inode, uid, gid and device numbers for the original and the attempted
link. If these match, link() is considered successful. Also,
if even then link() has failed, it will plainly use
rename(). I am unable to discern from the code whether
24hourtimer is implemented correctly.
stattmp is implemented by immediately
unlink()ing any file that may already exist (eek).
-
topdrop-0.11
does not implement 24hourtimer and
failure-unlink. It does 101 stattmp rounds.
In it's default config, it writes a "From " line at the beginning
of the message, mishandling the returnvalue from write().
-
safecat-1.8
implements all writer-requirements correctly,
including success-unlink and failure-unlink. It tries
stattmp 5 times.
- maildirdeliver-0.50
implements all writer-requirements correctly,
including success-unlink and failure-unlink. It tries
stattmp 2 times.
- getmail-2.1.4
implements all writer-requirements correctly,
including success-unlink and failure-unlink. It tries
stattmp only 1 time. This is no problem since a failing
stattmp indicates a problem anyway.
-
maildrop (from courier-0.39.3)
does not implement stattmp and
close-check. It does implements
success-unlink and failure-unlink. It tries
stattmp for 24 hours straight. From the code it looks
like it will actually append to a file in tmp/ if
that file already exists (eek).
peter(at)dataloss.nl