An interesting gotcha with Exim and .forward processing

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:

  1. if .forward-nonspam exists and the message is not spam, expand .forward-nonspam
  2. if the message is spam, .forward-nonspam exists, and .forward does not exist, discard the message
  3. 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.)