Prevent exim backscatter by checking maildir quotas at smtp time
If you’re familiar with the Exim way of dealing with mails, quotas and ACLs, you’ve probably got a good idea of what the problem is.
If not, then a good place to start is this very old post on mail.exim.user.
In short, in Exim, quota checks happen at delivery (i.e. transport) time. If a mailbox is overquota, then the incoming message can’t be delivered. It is then queued, retried later (according to the retry rules) and eventually delivered as soon as some room is made (e.g. via downloading and deleting older messages).
However, if a mailbox is permanently overquota, or if the incoming message is way too fat to fit, then, when the retry time is over, exim bounces it to the sender.
Now, while this is good in theory, it’s actually a naive move in the internet of today where the vast majority of mail is spam and the vast majority of spam senders is forged: Exim is bouncing back spam to innocent senders. In a word: backscatter.
A better way to handle quotas is to enforce them at rcpt time and 4xx-reject the incoming message right away. This way no queuing happens and no bouncing takes place (well not directly from YOUR server at least).
And here the problems begin…
The first issue is that you can’t possibly know what the size of the message is before actually receiving it; and once you’ve received it, it’s too late to 4xx.
This means that, in order to enforce quotas at smtp time, you’ll have to let your lusers go slightly overquota before acting. This is simply a matter of setting “no_quota_is_inclusive” in your transport.
The second issue is that Exim does ACL verification as an unprivileged user which isn’t powered enough to look into the mail storage.
If you deliver to mboxes however the problem is easy enough to work around: just set the mail storage directories o+x.
In the appendfile transport you can e.g. set:
create_directory
directory_mode = 0771
mode = 660
In the ACL you can then simply ${stat} the mailbox and compare its size against the allowed quota. E.g., in the rcpt ACL you put something like:
defer
domains = +local_domains
verify = recipient
condition = ${extract{size}{${stat:/var/mail/virtual_domains/${extract{mbox}{$address_data}{$value}{}}}}{$value}{0}}
condition = ${if >= {${extract{size}{${stat:/var/mail/virtual_domains/${extract{mbox}{$address_data}{$value}{}}}}{$value}{-1}}}{USERQUOTA}}
message = Mailbox over quota
where USERQUOTA is your quota macro of choice and $address_data['mbox'] is the path to the virtual mbox.
However, if you are delivering to maildir spools, then the whole thing becomes tricky as there’s not a single file to ${stat} but a possibly complex imap-generated hierarchy of directories and subdirectories.
The good news is, the Maildir++ format comes to the rescue…
It’s in fact just a matter of parsing the maildirsize file and compare the calculated size against the quota; if it’s lareger than what we allow, then we can reject with 4xx.
Now that’s cool, however, while parsing the maildirsize file could be possible with some ${perl} code or other expansion trickery, being able to actually read the file itself as an unprivileged user is a major obstacle (just ${stat}’ting it is not enough anymore).
Instead of doing the whole thing in Exim, however, I decided to write a separate daemon to be run as the “mailspool” user which could be queried via a unix socket to get the current quota. E.g.:
defer
domains = +local_domains
verify = recipient
condition = ${if > {${readsocket{/path/to/socket}{/var/mail/virtual_domains/${extract{mbox}{$address_data}{$value}{nothere}}\n}{10s}{}{0}}} {USERQUOTA}}
message = Mailbox over quota
So here’s the code.
And that’s the sample usage:
$ ./exim-quotad --help
Usage: ./exim-quotad
Options:
-s, --socket <path> creates socket in <path> (mandatory)
-m, --socketmode <mode> set socket mode to <mode> (default: 666)
-u, --user <user> change to user <user> (default: mail)
-p, --pidfile <path> writes pid to the file in <path> (default: no pidfile)
-f, --foreground do not fork into background (default: daemonize)
-d, --debug enable debug logging (default: debug is off)
-l, --log <path> or --log syslog set logfile to <path> (default: logging is disabled)
-t, --timeout <msecs> sets socket timeout to <msecs> (default: 10000)
-c, --max-children <num> limits the number of forked children to <num> (default: 20)
-h, --help print this help
Please be aware that the whole thing is untetested and not guaranteed to work for you. Also note that there are security concerns when running programs as the mail user.