Sendmail Tips

This is a quick document on how to get yourself out of some binds I've run into with sendmail. These aren't going to be the most organized, but I figure someone else may find these useful.

Mail Spool

Sendmail generally stores its queued mail in the /var/spool/mqueue folder. Queued messages are messages that have not reached their final destination yet. There are two files associated with each e-mail message, one beginning with qf, the other beginning df. qf files contain the message header, df files contain the body of the mail message. You can tell which files are tied together because they have the same ending; for example the header file qfPAA22846 has an associated "body" file named dfPAA22846.

Stopping/Starting Sendmail

Depending on which type of OS you are running on, here's some of the ways you may start/stop sendmail:

Linux
/etc/init.d/sendmail stop | start
FreeBSD
cd /etc/mail
make stop | start
Solaris
/etc/rc2.d/S88sendmail stop

You'll generally want to let sendmail slowly come to a halt -- it tries to stop its connections gracefully. When you're being mail bombed you might want to be a little more insistant on killing off sendmail. Note that people local to the machine (e.g. pine users and certain cgi-bin programs) may still be able to put their new messages into mqueue for delivery even while sendmail is stopped.

Large Mail Spool

If sendmail is having problems, after stopping it you might want to move the current queue directory to someplace else, to be processed specially. This is simple enough --

mv /var/spool/mqueue /var/spool/mqueue-fixme
mkdir /var/spool/mqueue
chmod 755 /var/spool/mqueue
chown root:daemon /var/spool/mqueue

Cleaning the Mail Spool

There could be a lot of "trash" qf or df files left behind following a bout of misbehaving sendmails. If the size of either file is 0, you should be able to trash them safely.

cd /var/spool/mqueue-fixme

to get into your queue directory, and

find . -size 0 -exec rm {} \;

to find everything with a size of 0 and execute the remove command on the found files. This is unwise to run in your default queue directory, even when sendmail is stopped.

Perhaps a sleazy spammer, cretinous chain-mailer, naughty Novell server, or administrator accidentally sending out e-mail to everyone is the cause of your mail woes. You can pull these messages from the mail queue thusly:

cd /var/spool/mqueue-fixme
mkdir /var/tmp/EVIDENCE-OF-ILLDOING
grep idiot@wherever qf* | cut -d":" -f1  | uniq | cut -d"f" -f2- | \
 xargs -i echo "mv *{} /var/tmp/EVIDENCE-OF-ILLDOING" > RUNME
chmod 700 RUNME
./RUNME

A sendmail problem could result in lots of "qf" files with no corresponding "df" file, and vice versa. You can get rid of these unmatched files with:

cd /var/spool/mqueue-fixme
ls -1 | cut -c 3-16 | sort | uniq -c | sort -n | grep " 1 " | awk '{print "*"$2}' > ~/rmfile

Then, execute this perl script OR bash script:

#!/usr/local/bin/perl

use strict;

open (RMFILE, "rmfile");
while (my $line=) {
        chomp $line;
        print $line."\n";
        system ("rm /var/spool/mqueue/$line");
}
close (RMFILE);

OR

for i in $(cat rmfile)
do
  rm -rf "/var/spool/mqueue/$i"
done

I realize that this is not pretty, but it was the best that I could come up with since echo and xargs were not cooperating with me whatsoever.

Parsing an Alternate Mail Spool

To process mail lurking in an alternate queue, you can run sendmail manually. For example, to see exactly what happens:

sendmail -oQ/var/spool/mqueue-fixme -q -v

will process the mail queue (-q) located in /var/spool/mqueue-fixme (-oQ/var/spool/mqueue-fixme) verbosely one message at a time (-v). This should produce something like the following:

Running /home/rnejdl/var/spool/mqueue/i05HgAGV098325 (sequence 3352 of 4476)
<7868@7218.j8j87hyrf.com>... Connecting to 7218.j8j87hyrf.com. via esmtp...

Filtering Bad HELO's

by Neil W Rickert <rickert+nn@cs.niu.edu>
There has been recent discussion on bad HELO parameters.  I have been
experimenting with the rules below.  Use at your own risk.  They may
require some local changes.

You can put at the top of "Local_check_mail".  In my case, I
am using `delay_checks' so I put them in a "check_mail" ruleset.

They should not be in "Local_check_relay" because you would want to
first know if the client has authenticated.

Basic strategy:

  Allow any HELO parameter if:

    The client authenticated, or
    the client is one of ours (its IP address begins with a component
    in class $=R).

    Else reject if the HELO hostname is our own name or our own
    IP address (tested against $=w), or if the HELO hostname is
    unqualified (no "." in the middle).

    This is a conservative check.  I don't require that the HELO
    parameter match the sending hostname -- only that it not match
    our name and be syntactically valid at least to the extent of
    containing a ".".

I have been testing this afternoon.  So far it has only rejected one
message that didn't look obviousl spammish.  But if the client says

HELO LIILXQMAIL01

I am not going to shed any tears over blocking that message.  I
suspect (from recent history) that this is to a "User unknown" and
the sender never cleans up its mailing lists.

Limitations:

 This might not stop a lot of spam.  It is blocking about 1 message
 every 4 minutes.  But most of those would have been blocked anyway.
 It is blocking  and  viruses,
 and some other viruses.  It is blocking a lot of asian mail that I
 was probably blocked before.  So far I haven't seen any 200.*
 addresses blocked.

 The spammers will probably retune their spamware, so within a few
 months this might stop having much effect.

 I check for SASL authentication, but not for SSL/client certificate
 authentication.  If you use that, you will need to make changes.

 I check for the connecting client having an IP that starts in $=R.
 I do not check for the client hostname ending in $=R.  If that
 is important, you may need to add a line or two.

 There is no provision to whitelist particular clients.  If you need
 that, you will need to add rules that do an access lookup.

 Also read the comments in the m4 rules source below (they begin "dnl`'").

Note that what this will mainly do for me (until spammers retool) is
reduce the amount of spam sent to postmaster.  With `delay_checks' I
was exempting postmaster from blocking.  But I have placed these
rules where they will reject mail prior to that exempting.

# helo/ehlo checks of $s
dnl`'Rationale:
dnl`'Client software is often broken.  We don't want to reject
dnl`'our own users client connections.  Therefore we attempt
dnl`'to allow our users to pass the checks.  Otherwise, block
dnl`'sites with a HELO/EHLO hostname that is unqualified, or
dnl`'is one of our own names
dnl`'
dnl`'Note that I had to at "127.0.0.1" to class $=R, so that
dnl`'local client software would bypass these tests.  I also
dnl`'added "[127.0.0.1]" to class $=w, so that the localhost
dnl`'IP would count as one of our IPs.
dnl`'
R$*			$:$1 $| <$&{auth_authen}>	Check if authenticated
dnl`'Bypass the test for users who have authenticated.
R$* $| <$+>		$:$1				skip if auth
R$* $| <$*>		$:$1$|<$&{client_addr}>[$&s]	Get connection info
dnl`'Bypass for local clients -- IP address starts with $=R
R$* $| <$=R $*>[$*]	$:$1				skip if local client
dnl`'Bypass a "sendmail -bs" session, which use 0 for client ip address
R$* $| <0>[$*]		$:$1				skip if sendmail -bs
dnl`'Reject our IP - assumes "[ip]" is in class $=w
R$* $| <$*> $=w		$#error $@5.7.1 $:"550 Access denied - bogus HELO " $&s
dnl`'Reject our hostname
R$* $| <$*> [$=w]	$#error $@5.7.1 $:"550 Access denied - bogus HELO " $&s
dnl`'Pass anything else with a "." in the domain parameter
R$* $| <$*> [$+.$+]	$:$1				qualified domain ok
dnl`'Reject if there was no "." or only an initial or final "."
R$* $| <$*> [$*]	$#error $@5.7.1 $:"550 Access denied - bogus HELO " $&s

Unresolvable client address

Some people want to accept only mail from systems which have a valid PTR entry in DNS. In many cases this will reject also legitimate mail hence it is not a feature provided by sendmail. However, if you really want to do this, here is how:

LOCAL_RULESETS
SLocal_check_relay
R$*		$: $&{client_resolve}
RTEMP		$#error $@ 4.7.1 $: "450 Access denied. Cannot resolve PTR record for " $&{client_addr}
RFORGED		$#error $@ 4.7.1 $: "450 Access denied. IP name possibly forged " $&{client_name}
RFAIL		$#error $@ 4.7.1 $: "450 Access denied. IP name lookup failed " $&{client_name}

You can put these rules in any of the Local_check_* rulesets depending on your needs.