System: Using qtool.pl to manage sendmail queues
The qtool.pl perl script is included with most distributions of sendmail and allows you to move, bounce or delete queued messages based on the message id or other characteristics.
As with everything sendmail- and perl-related, however, the syntax can be tricky to comprehend and it's difficult to find working examples online, so we've provided some below.
qtool.pl syntax
qtool.pl [options] target_directory source [source ...]
qtool.pl [-Q][-d|-b] [options] source [source ...]
The simplest option is -d for deleting a queued message:
/usr/share/sendmail/qtool.pl -d <qf-file>
or (safer) you can move a queued email to another directory:
/usr/share/sendmail/qtool.pl <target-directory> <qf-file>
More advanced options include the use of regular expressions to identify which emails you want to move in or out of the queue. We'll get into that below.
The point of using qtool.pl instead of just mv or rm is that it can safely be used while sendmail is running because it uses the same locking mechanism so avoids clobbering files that are in use.
If you're going to be calling qtool.pl a lot from the command line, you should set up an alias. For example:
alias qtool='/usr/share/sendmail/qtool.pl'
Sendmail queue directories
So where can we find these magical qf files you might be asking?
On Debian at least queued messages are stored in /var/spool/mqueue/ (QUEUE_DIR) and /var/spool/mqueue-client/ (MSP_QUEUE_DIR for internally generated emails). We're mostly interested in the former. Busy networks might have a number of separate queues.
Each email in the queue consists of a df file containing the message contents, and a qf file containing meta information about the email and its current status. Here we can see the contents of a queue directory with six (6) queues emails:
# ls -ltr /var/spool/mqueue
-rw-r----- 1 root smmsp 52 Nov 12 01:33 dftABDxS6e024439
-rw-r----- 1 root smmsp 34200 Nov 13 06:14 dftACJEHSZ013941
-rw-r----- 1 root smmsp 12216 Nov 13 13:51 dftAD2p34s012129
-rw-r----- 1 root smmsp 240 Nov 13 22:11 dftADBBuZl027798
-rw-r----- 1 root smmsp 1149 Nov 13 23:18 dftADCIacE001585
-rw-r----- 1 root smmsp 12216 Nov 14 17:06 dftAE666SH027158
-rw-r----- 1 root smmsp 1530 Nov 14 21:10 qftADCIacE001585
-rw-r----- 1 root smmsp 1452 Nov 14 21:10 qftADBBuZl027798
-rw-r----- 1 root smmsp 2622 Nov 14 21:10 qftAD2p34s012129
-rw-r----- 1 root smmsp 2586 Nov 14 21:21 qftAE666SH027158
-rw-r----- 1 root smmsp 1319 Nov 14 21:21 qftACJEHSZ013941
-rw-r----- 1 root smmsp 930 Nov 14 21:33 qftABDxS6e024439
The file names here match the message id in the output of mailq and in the server mail logs. In fact the qf files are the source of this information.
The df file contains the body of the email that has been queued. The qf file contains all the message headers, plus some other information, such as the (most recent) failure message and the number of attempts made to send this particular email.
The df files remain static (until deleted) while the qf files are updated every time this queue is run.
You should never pass the address of a df file to the qtool.pl script, only qf files.
qf file format
Data in the qf file is generally identified by the first letter of the line, and in the qtool.pl source we can see this useful table being defined:
my %parse_table =
(
'A' => 'auth',
'B' => 'body_type',
'C' => 'controlling_user',
'D' => 'data_file_name',
'E' => 'error_recipient',
'F' => 'flags',
'H' => 'parse_header',
'I' => 'inode_number',
'K' => 'next_delivery_time',
'L' => 'content-length',
'M' => 'message',
'N' => 'num_delivery_attempts',
'P' => 'priority',
'Q' => 'original_recipient',
'R' => 'recipient',
'S' => 'sender',
'T' => 'creation_time',
'V' => 'version',
'X' => 'charset',
'Z' => 'envid',
'$' => 'macro'
);
So a line that start with M is the (most recent) message received from the destination server, or a connection or timeout error. And a line starting with N contains a number indicating the number of failed send attempts.
Here is a sample qf file with the first character bolded:
V8
T1447493191
K1447495556
N2
P123490
I202/0/54669
MDeferred: Connection timed out with mail.spammer.com.
Frs
$_localhost
$r
$slocalhost
${daemon_flags}
${if_addr}XXX.XXX.XX.XX
SMAILER-DAEMON
MDeferred: Connection timed out with mail.spammer.com.
rRFC822; idiot@spammer.com
RPF:<idiot@spammer.com>
H?P?Return-Path: <81>g>
H??Received: from localhost (localhost)
by mail.example.net (8.14.4/8.14.4/Debian-8) id tAE9QVHl018335;
Sat, 14 Nov 2015 20:26:31 +1100
H?D?Date: Sat, 14 Nov 2015 20:26:31 +1100
H?F?From: Mail Delivery Subsystem <MAILER-DAEMON>
H?x?Full-Name: Mail Delivery Subsystem
H?M?Message-Id: <201511140926.tAE9QVHl018335@mail.example.net>
H??To: <idiot@spammer.com>
H??MIME-Version: 1.0
H??Content-Type: multipart/report; report-type=delivery-status;
boundary="tAE9QVHl018335.1447493191/mail.example.net"
H??Subject: Returned mail: see transcript for details
H??Auto-Submitted: auto-generated (failure)
.
This is for a typical bounce email being sent from our mail server with the Subject: "Returned mail: see transcript for details", but the destination server is not responding - most likely because the original email arrived from a spoofed address. So far there have been two (2) attempts at sending.
The values present in the qf file become avaiiable in expression matching, appearing as $msg{message} and $msg{num_delivery_attempts} for the (M) and (N) lines respectively.
As you can see the message (H)eaders are all prefixed with H so we have to look at bit deeper to differentiate between them. You'll see how that works shortly.
Scanning the mail queue
The simplest way to view queued messages is to use the mailq command:
# mailq
MTA Queue status...
/var/spool/mqueue (5 requests)
-----Q-ID----- --Size-- -----Q-Time----- ------------Sender/Recipient-----------
tAF1sTJd014941- 4404 Sun Nov 15 12:54 MAILER-DAEMON
(Deferred: 452 <anon@example.com> Mailbox size )
<anon@example.com>
tAEKrTik019603- 4310 Sun Nov 15 07:53 MAILER-DAEMON
(Deferred: 452 <anon@example.com> Mailbox size )
<anon@example.com>
tAEKOExT016777- 5374 Sun Nov 15 07:24 MAILER-DAEMON
(Deferred: 450 4.1.1 <apache@example.org>: Recipient addre)
<apache@example.org>
tACJEHSZ013941- 34200 Fri Nov 13 06:14 <affiliations@spammer.com>
7BIT (Deferred: 452 4.1.0 ... temporary failure)
<booking@example.net>
tABDxS6e024439- 52 Thu Nov 12 00:59 MAILER-DAEMON
(Deferred: 403 4.7.0 TLS handshake failed.)
<ben@example.org>
Total requests: 5
Alternatively, we can look directly into the queue files and extract a little more detail. Here we extract the data we want from the same five emails:
# egrep -h \(^M\|^N\|^R\|Subject:\|^\\.\) /var/spool/mqueue/qf* | uniq | sed 's/^\(.\)/\1 /'
N 143
M Deferred: 403 4.7.0 TLS handshake failed.
R PF:<ben@example.org>
H ??Subject: Returned mail: see transcript for details
.
N 109
M Deferred: 452 4.1.0 ... temporary failure
R PFD:<booking@example.net>
H ??Subject: ADP Payroll Invoice
.
N 29
M Deferred: 450 4.1.1 <apache@example.org>: Recipient address rejected: User unknown in local recipient table
R PF:<apache@example.org>
H ??Subject: Returned mail: see transcript for details
.
N 28
M Deferred: 452 <anon@example.com> Mailbox size limit exceeded
R PF:<anon@example.com>
H ??Subject: Returned mail: see transcript for details
.
N 18
M Deferred: 452 <anon@example.com> Mailbox size limit exceeded
R PF:<anon@example.com>
H ??Subject: Returned mail: see transcript for details
.
By modifying the grep pattern you can select any rows from the qf file. We've chosen the (N) and (Subject) lines because they are used in our regular expression matching in the next section.
Using qtool.pl with expression matching
Here's where some scripting magic can silently detect and remove emails from the queue that we know aren't important due to their subject or status.
For example, we can detect and 'de-queue' messages similar to the one above using this simple script:
#!/bin/bash
QTOOL=/usr/share/sendmail/qtool.pl
MQUEUE=/var/spool/mqueue/
REMOVED=/var/spool/removed/
$QTOOL -e '(($msg{message}[0] =~ /Connection timed out/) && ($msg{headers}->{ubject} =~ /Returned mail: see transcript for details/))' $REMOVED $MQUEUE
The tricky part is in the regular expression test. While most parameters can be found using just $msg{keyword} the (M)essage and (H)eaders both require special treatment.
Because there are two lines in the qf file with (M)essage they will be folded into an array. That means you need to match against $msg{message}[0] instead of just $msg{message}. This applies to any prefixes which appear more than once.
We already know there are lots of (H)eader lines, but matching is tricky because in some (all?) versions of qtool.pl the first letter is excluded from matching so we have to exclude it from our regular expression. That's why in the command above you will see $msg{headers}->{ubject} instead of $msg{headers}->{Subject}.
In combination:
($msg{message}[0] =~ /Connection timed out/) \
&& \
($msg{headers}->{ubject} =~ /Returned mail: see transcript for details/)
these expressions will match all emails with the subject "Returned mail: see transcript for details" where connection to the recipient server has timed out. Our script will move them out of the active mail queue and into another directory (# mkdir /var/spool/removed/).
qtool.pl -e '(match conditions)' target-directory live-queue-dir
To instead delete the emails in question from the live queue you would use:
qtool.pl -d -e '(match conditions)' live-queue-dir
And for bouncing use -b instead of -d in the last command.
For testing it is good practice to work with a copy of the live mail queue directory, and to move matched emails to a temporary location rather than deleting them immediately.
For good measure, here are some more working examples:
$QTOOL -e '($msg{message}[0] =~ /Deferred: 45.* Relay access denied/)' $REMOVED $MQUEUE
$QTOOL -e '($msg{message}[0] =~ /Deferred: 45.* Domain of sender address .* does not (resolve|exist)/)' $REMOVED $MQUEUE
$QTOOL -e '($msg{message}[0] =~ /Deferred: 45.* Sender address rejected: Domain not found/)' $REMOVED $MQUEUE
$QTOOL -e '($msg{message}[0] =~ /Deferred: 45.* spam/i)' $REMOVED $MQUEUE
$QTOOL -e '(($msg{num_delivery_attempts} > 50) && ($msg{message}[0] =~ /Network is unreachable/))' $REMOVED $MQUEUE
$QTOOL -e '(($msg{num_delivery_attempts} > 50) && ($msg{message}[0] =~ /Connection (timed out|refused)/))' $REMOVED $MQUEUE
In each case the matching emails are moved from the live queue and placed in a temporary directory /var/spool/removed/ so they can be verified before manual deletion.
We can also trigger a bounce instead of de-queueing the message as follows:
$QTOOL -b -e '(($msg{num_delivery_attempts} > 10) && ($msg{message}[0] =~ /Network is unreachable/))' $MQUEUE
This will bounce an email after ten (10) attempts when the status message contains "Network is unreachable". The original sender will receive a slightly cryptic bounce message:
Returned mail: see transcript for details
...
----- Transcript of session follows -----
Message could not be delivered for too long
Message will be deleted from queue
...
Unfortunately what seems to be missing from qtool.pl is any facility for testing or logging.
Why mess with the mail queue?
In the age of rampant email spam running a normal mail server where you notify the sender of any email failures can land you in hot water. Email spoofing means that these messages are not always going back to the original sender, and you can be penalised for backscatter among other things.
In normal operation the mail server will keep trying to send an email for up to 5 days. And after 4 hours an email will be sent back to the sender notifiying them of a delay. That email will also be tried for up to five days if it's not accepted. Together that can add up to hundreds of delivery attempts.
Having a script like the one outlined above which can be called at regular intervals by CRON is a good way of reining in backscatter without entirely disabling non-delivery notifications (NDN).
By analyzing what's hanging round in our mail queue we can create custom rules for filtering out, deleting or bouncing queued emails that we know or suspect aren't going anywhere.
Patch for better header matching
In the qtool.pl source code you can replace this line in ControlFile:parse_header
<<< $line = substr($line, 3);
>>> $line =~ s/^\?.*?\?//;
Now you can match the full header names "Subject" instead of "ubject".
This enables the following filter expressions:
$QTOOL -e '(($msg{sender} eq "MAILER-DAEMON") && ($msg{message}[0] =~ /^Deferred/) && ($msg{headers}->{Subject} =~ /Returned mail|could not send message/))' $REMOVED $MQUEUE
$QTOOL -e '(($msg{message}[0] =~ /^Deferred/) && ($msg{headers}->{"X-Spam-Flag"} =~ /YES/))' $REMOVED $MQUEUE
Some points to note here are: we use eq for string comparision; quotes are required around "X-Spam-Flag"; and the value we're matching is actually " YES" (with a leading space) which is why we're using an expression instead of eq.
For the curious the X-Spam-* headers are being inserted by SpamAssassin.
References
Related Articles - Sendmail
- PHP Signing outbound emails with DKIM
- PHP Generating a Key Pair for DKIM
- System Using qtool.pl to manage sendmail queues
- System Analysing mailq and the mqueue directory
- System DKIM Key Pair Generator
- System Analysing the mail.log
- System Expanding IPv6 Addresses for DNSBL Checks