Blocking Spam with Sendmail
By Leo Savage
Like a great many people on the net, I found myself increasingly annoyed
by the rising tide of spam. By which I do not mean the delectible Hormel
canned meat product (which the little lady especially likes fried in a
fried egg sandwich), but the phenomenon of unsolicited commercial email.
Spam is unlike regular junk mail from the post office for several reasons:
- Normal junk mail costs money to send. Spam does not. The cost is
born by the recipients and by the servers in between. While normal junk
mail is a revenue source for the post office and actually helps to pay
for delivering regular mail, spam pays for nothing. Spam can hence be
viewed as theft of services.
- Spam has detrimental effects. Historically Internet mail servers
have passed along email between third parties as a gesture of friendship
and good will. Much like tossing your neighbor's paper over the fence if
the paperboy misses his throw. This is happening less and less because
spammers take advantage of it.
- Spam reduces the utility of email. People become discouraged about
checking their mailboxes if they are always cluttered with spam.
Enough ranting. I decided to do something. Being the sort who favors
technical fixes over legal ones,
I started doing some research
on
the web, ordered a copy of
the Bat
book, and spent some time reading my sendmail configuration and
scratching my head. I present here the result.
First, if you're not using sendmail I can't help you. Second, you need
the latest version of sendmail or
these tricks won't work. And finally, we had several conditions that had
to be met:
- We wanted to be able to block spam by domain name, network number, or
by specific address for maximum flexibility.
- We needed to be able to allow spam to selected mailboxes for
customers who do not want spam blocked. I may disagree with them, but
they are paying for a service and it is, after all, their mail. Spam
blocking had to be a value added service that could be turned off.
- We host a number of "virtual domains" and needed to be able to route
email for them to the proper mailboxes. We had already been doing that,
but it was a factor that had to be considered in our antispam measures
so that spam could be blocked or not as desired by the mailbox owners.
- We wanted to stop "third party relay" going through our mail server
while allowing for exceptions for customers with their own domains and
mail servers or for roaming customers.
I think I have come up with a set of sendmail rules which accomplish this.
First, we need to add a few entries in the local configuration section:
LOCAL_CONFIG
Fw/etc/vdomain.cw
Kvdomain hash /etc/vdomain
# list of people who like spam
F{fools} /etc/WantSpam
# list of known spammers
Kjunk hash -a@JUNK /etc/spammers
# List of network addresses we will relay for
F{LocalIP} /etc/LocalIP
# List of domains we will relay to
F{RelayTo} /etc/RelayTo
Click on the filename of any of these files for an explanation of its
purpose and contents.
Now we get to the rules themselves. First, an entry must be added to
your local rule zero, like so:
LOCAL_RULE_0
R$* $: $>vmap $1
Not very interesting, is it? It just calls another rule set, named
"vmap", which handles virtual domain address mapping. Note: I don't know
what the "right way" is to do these things, but it works to just list all
the rest of these rulesets right under LOCAL_RULE_0, so that's what I
do. Here then is the "vmap" ruleset:
Svmap
R$+ < @ $+ . > $: $1 < @ $2 > .
R$+ < @ $+ > $* $: $(vdomain $1@$2 $: $1 < @ $2 > $3 $)
R$+ < @ $+ > $* $: $(vdomain $2 $: $1 < @ $2 > $3 $)
R$+ < @ $+ > . $: $1 < @ $2 . >
I made this a separate ruleset since I do it again in the rest of the
rules, as you will see. I have obscure reasons for not just calling
local rule zero as needed.
Next I define a "junk" ruleset to look up a domain name or email address in
the /etc/spammers.db database:
Sjunk
R$* $: $(junk $1$) look for host in spammer list
R$+@JUNK $@ $1@JUNK return message if found
R@JUNK $@ Spam refused @JUNK generic message
R$-.$+ $: $1 . $>junk $2 retry skipping lead subdomain
R$-.$+@JUNK $@ $2@JUNK return message if found
Next, a "junkIP" ruleset to look up an IP address or network number in
the /etc/spammers.db database:
SjunkIP
R$* $: $(junk $1$) look for host in spammer list
R$+@JUNK $@ $1@JUNK return message if found
R@JUNK $@ Spam refused @JUNK generic message
R$+.$- $: $2 . $>junkIP $1 retry without trailing number
R$-.$+@JUNK $@ $2@JUNK return message if found
R$-.$+ $@ $2.$1 fix order if not spammer
Now for the heart of it, the "check_rcpt" ruleset. Spam blocking is more
often done in the "check_mail" ruleset, but we can't do it that way since
we need to check the recipient to see if they want spam. Hence, this
ruleset gets a bit long.
Scheck_rcpt
R$* $: $>vmap $>3 $1 normalize address
# Refuse to relay mail between nonlocal systems
R$* $: $(dequote "" $&{client_addr} $) $| $1
R0 $| $* $@ ok no client addr: directly invoked
R$={LocalIP}$* $| $* $@ ok from here
R$* $| $* $: $2 not from local, check recipient
R$*<@$=w.>$* $>3 $1 $3 remove our aliases, maybe repeatedly
R$*<@$*$={RelayTo}.>$* $>3 $1 $4 remove domains we relay to
# still something left?
R$*<@$+>$* $#error $@ 5.5.4 $: "554 we do not relay from " $&{client_name} " to " $1@$2$3
# Allow mail to fools who like spam, and otherwise block spammers
R$={fools} $@ ok recipient listed as wanting spam
# Block by host or domain name
R$* $: $(dequote "" $&{client_name} $)
R$* $: $>junk $1
R$*@JUNK $#error $@ 5.5.4 $: "554 " $1 ": " $&{client_name}
# Block by network or host IP address
R$* $: $(dequote "" $&{client_addr} $)
R$* $: $>junkIP $1
R$*@JUNK $#error $@ 5.5.4 $: "554 " $1 ": " $&{client_addr}
# Block by specific email address
R$* $: $(dequote "" $&f $)
R$* $: $>junk $1
R$*@JUNK $#error $@ 5.5.4 $: "554 " $1 ": " $&f
R$* @ $* $: $1 @ $>junk $2
R$* @ $*@JUNK $#error $@ 5.5.4 $: "554 " $2 ": " $&f
# Block mail from invalid addresses
R$* $: $>3 $1 make domain canonical
R$* < @ $+ .> $* $@ ok name resolved ok
# Killer case -- single token domain
R$* < @ $- > $* $#error $@ 5.5.1 $: "551 Invalid host name: " $2
# Delay case -- domain doesn't resolve
R$* < @ $+ > $* $#error $@ 4.5.1 $: "451 Unknown domain: " $2
And that's it. If you'd like, you can download a
text version of this for easier editing.
Oh, one last thing. The rejection messages all get logged in
/var/log/maillog (at least on our system). Here's a PERL script for
maillog.scan that gives us a nightly report of spam blocks:
#!/usr/bin/perl
while($lt;$gt;) {
if(/rejection:.*\.\.\. ? ?(.*)/) {
$spam{$1} += 1;
}
}
print "\nSpam blocks:\n\n";
foreach $msg (sort keys %spam) {
printf "%5d %s\n", $spam{$msg}, $msg;
}
print "\n";
Page accesses to date:
leo@esva.net