Sort Incoming Mail with procmail Recipes Safely
One bad regex in a procmail recipe can silently swallow your mail for good, so this guide tests every rule in a throwaway mailbox first. By the end, a message from a chosen sender will be filed by a real recipe, verified in a temporary mailbox before it ever touches your live mail. Allow about 20 minutes, including the dry run and inspection of the log.
The route
Jump straight to the step you need, or tick off Done means at the end.
Before you start
This guide uses procmail 3.24, installed here as Ubuntu package 3.24-1ubuntu2. It expects a local procmail installation and a shell account whose home directory you control. You do not need root for a personal ~/.procmailrc. A system-wide /etc/procmailrc, mail transport integration, or explicit delivery for another user requires an administrator and is outside this safe first test.
Procmail reads a message from standard input. With no rcfile argument it looks for $HOME/.procmailrc; if processing reaches the end without delivering the message, it falls back to the default mailbox. Its defaults can override environment variables supplied before startup, so set test values as command-line assignments when using -m.
1. Make a test filter
Do not begin by editing the file used by your live mail delivery. Create a separate rcfile and two empty destinations under /tmp. The example has one recipe: it examines the header by default and files messages whose From: header contains the example address.
testdir=$(mktemp -d /tmp/procmail-test.XXXXXX)
mkdir -p "$testdir/maildir"
cat > "$testdir/filter.rc" <<'EOF'
LOGFILE=$MAILDIR/procmail.log
VERBOSE=yes
:0:
* ^From:.*[email protected]
alerts
EOF
The trailing colon on :0: asks procmail to use a local lockfile for the destination. The condition begins with *; conditions are combined with AND, and header matching is the default. A plain filename is an mbox-style file relative to MAILDIR, while a directory ending in / is treated as a maildir.
Checkpoint
Confirm that the rcfile contains the recipe and that the test directory is not a path you care about:
sed -n '1,20p' "$testdir/filter.rc"
find "$testdir" -maxdepth 2 -type d -print
2. Feed it a harmless message
Use -m to select the test rcfile. The two assignments appear after the defaults, which makes the temporary paths authoritative for this run. The input is a complete, ordinary message with a blank line separating its header and body.
printf 'From: [email protected]\nTo: [email protected]\nSubject: Server alert\n\nDisk space is low.\n' \
| procmail -m "MAILDIR=$testdir/maildir" \
"DEFAULT=$testdir/maildir/inbox" "$testdir/filter.rc"
There should now be an alerts file and a log. The message is consumed by the matching recipe, so it should not also appear in inbox.
find "$testdir/maildir" -maxdepth 1 -type f -printf '%f\n' | sort
sed -n '1,12p' "$testdir/maildir/alerts"
tail -n 8 "$testdir/maildir/procmail.log"
Expected file names are:
alerts
procmail.log
The log should mention the destination and a successful folder delivery. Exact timestamps and byte counts vary. VERBOSE=yes is useful while developing a rule; remove it or set VERBOSE=no when you no longer want per-message logging.
3. Add a second rule without losing unmatched mail
Recipes are considered in order. A delivering recipe normally ends processing for that message, so put specific rules before broad ones. Add a newsletter rule, then a final default delivery. The default keeps messages that match neither earlier condition.
:0:
* ^Subject:.*newsletter
news
:0
$DEFAULT
The last action uses the DEFAULT variable rather than duplicating a path. This example deliberately leaves the default action without a local lockfile because the normal default mailbox has its own delivery semantics. If several processes write the same ordinary file, use a recipe lock and confirm the destination is suitable for mbox delivery.
To check the new branch, feed a message with Subject: Weekly newsletter and look for news. Feed a third message with neither marker and look for inbox. Keep each test message disposable.
4. Move the rule into your real rcfile carefully
Warning
A recipe changes mail delivery immediately. A typo can divert mail, invoke a program, forward private content, or discard a message. Keep a copy of your current rcfile, add one tested recipe at a time, and start by delivering to a folder you can inspect. Do not use /dev/null, a forwarding action beginning with !, or a pipe beginning with | until the matching condition has been tested.
cp -- "$HOME/.procmailrc" "$HOME/.procmailrc.before-mail-rule"
mkdir -p "$HOME/mail"
cat >> "$HOME/.procmailrc" <<'EOF'
# File alerts after confirming the condition in a temporary mailbox.
:0:
* ^From:.*[email protected]
mail/alerts
EOF
In this form MAILDIR defaults to $HOME, so mail/alerts means a file below your home directory. Ensure the rcfile is private enough for procmail's security checks: it must be owned by you or root, and your home rcfile must not be group-writable or live in a group-writable directory.
Undo this exact change by restoring the saved copy:
cp -- "$HOME/.procmailrc.before-mail-rule" "$HOME/.procmailrc"
That recovery step replaces later edits too, so use it only if the backup was made immediately before this recipe was added. Otherwise remove just the new recipe in an editor, then send yourself a known test message and inspect the destination.
Common traps
- Header matching is case-insensitive by default. Add the
Dflag if you need case sensitivity. Conditions go to procmail's internal extended regular expression engine, not a shell, so comments cannot sit on the condition line. - A message can silently lose its header or body. This happens if a delivering recipe uses only the
horbflag withoutc. Leave those flags alone until you have a specific filtering need. - A pipe is a program action, not a harmless preview. Add
wwhen you need procmail to wait for the program and check its exit status. Treat shell expansion, forwarding loops, and autoreplies as security-sensitive. - Weighted scoring uses a prefix. Put a value such as
10^1before a condition. The recipe matches only when its final score is positive. Use$=in verbose diagnostics to inspect the score, and readprocmailsc(5)before composing a scoring rule.
Done means
- Version is confirmed. Procmail reports 3.24 2022/03/02.
- A test message was filed. A matching recipe caught it and the log confirmed delivery.
- Unmatched mail has a home. An explicit, reviewed default destination catches everything else.
- The live rcfile is protected. It has a backup, a tested change, and a recovery path.
- Nothing forwards or discards blindly. No recipe forwards, pipes, or discards mail until its condition is proven.