Yesterday I described how Exim implements traditional .forward
semantics where putting your own address in your
.forward means 'deliver it to me, bypassing my .forward'. Because Exim
is a mailer construction kit, this isn't a specific
feature for .forward handling, it's a generic general feature that
happens to give you this result.
So far, so good. Now, let's talk about our .forward-nonspam feature. In the abstract, this is just
another .forward-style router that reads a different file and only
triggers under some conditions. In concrete, we need several routers
in sequence, each of them doing one step of the processing logic:
- if .forward-nonspam exists and the message is not spam, expand
.forward-nonspam
- if the message is spam, .forward-nonspam exists, and .forward does
not exist, discard the message
- if .forward exists, expand .forward
If you have both a .forward-nonspam and a .forward, the third rule will
only be triggered for spam messages because your .forward-nonspam skims
off non-spam messages first.
Well. Mostly. You see, although all three of these routers are
conceptually a single block of .forward processing, Exim doesn't
know this; as far as Exim is concerned, they are three separate and
completely unrelated routers. Now suppose you put your own address into
.forward-nonspam and also have a .forward, as you might do to create a
simple 'put all non-spam email into my regular inbox and all spam mail
into a file' system, and you get a non-spam message. Exim processes
things until it reaches the first router, expands your .forward-nonspam,
gets your address and restarts routing it, gets to the first router
again, sees that the router has already handled this address, and only
skips that router, not all three .forward-processing routers. So your
address falls through to the third router, which says 'sure, you have a
.forward, I'll handle this' and dumps the non-spam message into the file
for spam email.
Oops.
The fix for this is to split the third router into two routers, one
for the case where you do have a .forward-nonspam (where it would only
handle messages that are explicitly spam-tagged) and a second one for
the case where you have no .forward-nonspam (where it would handle
everything). However, this requires an annoying level of repetition
in the Exim configuration file.
(For technical reasons I think that you can't combine this together in a
single condition on a single router that works quite exactly right.)
Sidebar: the technical reasons
The condition you need is 'if .forward exists and either
.forward-nonspam doesn't exist or the message is non-spam'. Exim has
special support for securely and correctly checking for file existence
over NFS, but this support is only available in the require_files
router condition. However, we need to use a condition check with a
'${if ...}' string expansion to check 'is non-spam'. You can't or
together separate router conditions (they are all implicitly and'd
together instead), and the does-file-exist check that's available in
a ${if expansion doesn't work the right way over NFS.
In theory you could get around this with various evil hacks involving
Exim string expansion, maybe.
(Talking to myself: one could rephrase the condition as 'if .forward
exists and, if the message is non-spam, .forward-nonspam doesn't exist'
and then write this as a single require_files condition with a
conditional string expansion in it.)