OK, here we go. This is my second implementation of "SRS integration
with qmail". I believe Shevek will find this one less objectionable. ;-)
PURPOSE:
The purpose of this project is to prevent being "spammed" by bounce
messages. That is, I should not have to read any alleged bounce
messages which aren't actually responses to messages that I have sent
out. Ideally, this will avoid wasting my time reading both spam that
is sent with an empty envelope sender in the first place, and also
bounces which are sent to me by innocent "joe-jobbed" MTAs which are
not yet SPF-enabled, when a spammer forges my address as the envelope
sender.
BACKGROUND:
First, we must understand exactly what the problem is. SMTP specifies
that bounce messages (responses to the sender of undeliverable mail)
should be sent with an empty envelope sender, to avoid bounce loops.
This means that "spam" (unsolicited bulk/commercial e-mail) can be
sent this way:
spammer
| MAIL FROM:<>
V RCPT TO:<greg(_at_)wooledge(_dot_)org>
wooledge.org
There's no way for the MTA at wooledge.org to know whether this is a
legitimate bounce message (a reply to a previous outgoing message).
Schemes like SPF which validate the envelope sender address can't
be used here either, because there is no sender address to validate.
The Sender Rewriting Scheme (SRS) allows me to work around this.
By changing my own envelope sender address on all of my outgoing
messages, I can reject (or filter) any incoming bounce messages which
are not legitimate responses. The criteria are simply: any incoming
messages with an empty envelope sender which go to my main e-mail
address, greg(_at_)wooledge(_dot_)org, are spam. Real bounces will go to an
SRS address instead, and will then be forwarded to my real address.
For example:
legitimate bounce
| MAIL FROM:<>
V RCPT TO:<SRS0=HHHHHHHH=TT=wooledge(_dot_)org(_at_)Xê?µ/?ï®Br'ý
wooledge.org
| Run SRS validation on the address.
V Hash HHHHHHHH must be valid, and timestamp TT must be recent.
Forward to greg(_at_)wooledge(_dot_)org(_dot_)
spam
| MAIL FROM:<>
V RCPT TO:<greg(_at_)wooledge(_dot_)org>
wooledge.org
| Empty envelope sender!
V
Drop it in the spam folder.
This scheme does not rely on any external cooperation whatsoever. I
can do it simply on my own mail system(s), and it works immediately.
IMPLEMENTATION:
This implementation is done in small pieces. I will describe each one
in turn. Alternate implementations are of course also possible; in
particular, patching qmail to include "native" SRS support would make
this a more efficient operation. However, this implementation works
right now.
First, I installed the Mail::SRS software (a perl module from CPAN).
I have safecat installed already (provides the maildir command).
Next, I set up a virtual domain called "srs.wooledge.org" which will
handle all of the incoming messages with SRS addresses. This requires
an MX record in DNS, and the following configuration in qmail on my
qmail server:
* Add srs.wooledge.org to control/rcpthosts.
* Add srs.wooledge.org:srs to control/virtualdomains.
* Add |bin/srsfilter to alias/.qmail-srs-default.
* Create a Maildir for the alias user (don't forget to chown it).
* Write the following script to alias/bin/srsfilter (and don't forget
to chmod +x it):
#!/bin/sh
PATH=/var/qmail/bin:/usr/local/bin:/usr/bin:/bin
QMAILSUSER=MAILER-DAEMON
QMAILSHOST=wooledge.org
export PATH QMAILSUSER QMAILSHOST
ad=`perl -e 'use Mail::SRS;
my $s = new Mail::SRS(
Secret => "'"\`cat $HOME/.srs-secret\`"'",
MaxAge => 10, HashLength => 8);
print $s->reverse("'"$EXT2"'\(_at_)wooledge(_dot_)org")'
2>/dev/null`
if [ -n "$ad" ]; then
qmail-inject "$ad"
else
maildir ./Maildir/
fi
* Create alias/.srs-secret which will contain a "password" (secret)
for generating and validating SRS addresses. This can be anything
you want. I used a base-64 encoding of 16 bytes from /dev/random.
I also used "chmod 600" on it.
Notes on the shell script above:
* I used qmail-inject instead of forward so that I could use the
qmail-inject environment variables (QMAILSUSER, QMAILSHOST).
This is necessary so that when the message is forwarded to my
real address, it will have a non-empty envelope sender.
* Mail::SRS's "reverse" method requires an address with an "@" sign
and domain on it, so I had to append "@wooledge.org" to the SRS
address.
* If the Mail::SRS "reverse" fails, the output will be empty (because
stderr is discarded). Any such message will not be forwarded; it
will be delivered to the Maildir instead. You could choose to
forward it to a different address, or to delete it, instead. Also
note that arbitrary random addresses within the srs.wooledge.org
domain will not be valid SRS addresses, so we will *not* be an
open SRS relay.
* The hash length and maximum age are not the default values; choose
whatever values suit your own needs.
* The .srs-secret file shouldn't contain any characters that will
cause the shell script quoting to blow up. See "FINAL NOTES" below.
That takes care of getting valid SRS bounces to me, and puts a valid
envelope sender on them (MAILER-DAEMON(_at_)wooledge(_dot_)org). The next
piece
"discards" all other bounces to my real address
(greg(_at_)wooledge(_dot_)org):
* Put |bin/spamcheck in ~greg/.qmail.
* Create maildirs named Maildir and Spamdir (if not already done).
* Write the following script to ~greg/bin/spamcheck (and don't forget
to chmod +x):
#!/bin/sh
PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
TMP=/tmp/spamcheck.$$
trap 'rm -rf $TMP; exit 111' 1 2 3 15
spamc >$TMP
if grep -q '^X-Spam-Flag: YES$' $TMP; then
maildir Spamdir <$TMP
rm -f $TMP
exit 99
else
if [ -z "$SENDER" ]; then
maildir Spamdir <$TMP
rm -f $TMP
exit 99
else
maildir Maildir <$TMP
rm -f $TMP
exit 0
fi
fi
Notes on the preceding script:
* I'm also running spamassassin, so I already had a script in place
which sent all messages through spamc for validation, and then
sent them either to my normal mailbox (Maildir) or to my special
"spam" mailbox (Spamdir). I simply extended that to write all
messages with an empty "SENDER" variable to Spamdir as well. If
you're doing this from the ground up with no preexisting .qmail
file, then a *much* simpler script will suffice. The following
example is UNTESTED:
#!/bin/sh
if [ -z "$SENDER" ]; then
maildir Spamdir
exit 99
else
maildir Maildir
fi
Finally, I needed a way to change my envelope sender on a per-outgoing-
message basis. I'm using mutt as my MUA, so this was easy using mutt's
advanced flexibility. Users of other MUAs will have to find their own
way to achieve the same thing. I did this:
* Copy .srs-secret from ~alias on the mail server to ~greg on the
box where mutt runs. (And chmod 600.)
* Put this in .muttrc:
set sendmail=$HOME/bin/sendmailwrapper
* Write the following script to ~greg/bin/sendmailwrapper (and don't
forget to chmod +x):
#!/bin/sh
QMAILSUSER=`perl -e 'use Mail::SRS;
my $s = new Mail::SRS(Secret => "'"\`cat $HOME/.srs-secret\`"'",
MaxAge => 10, HashLength => 8);
my $a = $s->forward("greg\(_at_)wooledge(_dot_)org");
$a =~ s/\(_at_)$//; print $a' 2>/dev/null`
QMAILSHOST=srs.wooledge.org
export QMAILSUSER QMAILSHOST
exec /usr/sbin/sendmail "$@"
Note that the HashLength and the secret used here must match the ones
used on the qmail side, so that the SRS addresses will validate properly.
As you can see, I'm also using QMAILSUSER and QMAILSHOST here, so of
course this means that I'm running qmail on the box where mutt runs.
If your client-side MTA is not qmail, then this will have to be adjusted
as well.
FINAL NOTES:
I hate using multiple levels of quoting in shell scripts. :( But I
didn't want to write the whole thing in perl.
--
Greg Wooledge | "Truth belongs to everybody."
greg(_at_)wooledge(_dot_)org | - The Red Hot Chili Peppers
http://wooledge.org/~greg/ |