Move Mail Filtering to Delivery Time with Dovecot LMTP and Sieve
Client-side rules drift across devices. Terminate local delivery at Dovecot's LMTP service and run Sieve at delivery time so every message follows one authoritative filing path.
30 Apr 2026, 00:54 UTC

If a message from your project lead lands in INBOX on your phone but disappears into a folder on your desktop, the problem is not the message—it is that each mail client applies its own rules. Client-side filtering turns every device into a separate policy engine, and the results diverge as soon as one app is open and another is not. The useful move is to stop filtering in the MUA (mail user agent, the app you read mail with) and start filtering at delivery time: terminate local SMTP at Dovecot's LMTP service and let Pigeonhole Sieve decide where the message goes before any client sees it.
Why delivery-time filtering beats client rules
When an MTA (mail transfer agent, the server that receives mail) finishes accepting a message, it normally hands the message to a local delivery agent. Dovecot's LMTP (Local Mail Transfer Protocol) service is that delivery agent. Unlike a client-side rule that runs only when a specific app synchronizes, LMTP runs once, at the moment the message arrives. Pigeonhole is Dovecot's Sieve implementation; Sieve is a small, standardized scripting language for mail filtering. Running Sieve inside LMTP means every delivery follows the same authoritative path, regardless of which device the user opens next.
Wire the Sieve plugin into LMTP
First confirm the installed Dovecot build includes the sieve plugin and the lmtp protocol. Run doveconf -n as root (or via sudo) to see the effective configuration, and doveconf -a to inspect defaults. If protocols does not include lmtp, add it. Then enable Sieve for the LMTP protocol specifically:
protocol lmtp {
mail_plugins = $mail_plugins sieve
}
Next, point the MTA at the LMTP socket. In Postfix, for example, set mailbox_transport = lmtp:unix:/var/run/dovecot/lmtp and reload the MTA. The socket path and permissions are common failure points; a wrong path usually appears as a temporary delivery failure rather than a Sieve error. A typical Dovecot listener looks like this:
service lmtp {
unix_listener /var/run/dovecot/lmtp {
mode = 0660
user = <mta-user>
group = <mta-group>
}
}
Replace <mta-user> and <mta-group> with the account your MTA runs as. If the MTA cannot write to the socket, delivery will fail before Sieve runs.
A worked filing rule
Site-wide policy belongs in sieve_before, which runs before any user script. Suppose your scanner or MTA already adds an X-Spam-Flag header. A global script can file obvious spam into Junk while leaving a copy in INBOX for auditing:
require "fileinto";
if header :contains "X-Spam-Flag" "YES" {
fileinto "Junk";
keep;
}
Because fileinto cancels the implicit keep, the explicit keep leaves the INBOX copy. A user can then add a personal script through ManageSieve (or by placing a file in their Sieve directory) to file project mail:
require "fileinto";
if header :contains "Subject" "[ProjectX]" {
fileinto "ProjectX";
}
With this composition, the global script runs first, the user script runs second, and the message is filed once—no matter which client the user opens.
Verify before you cut over
Do not enable a global script on production mail without a rehearsal. Start with one non-production mailbox. If your package includes a Sieve compiler such as sievec, validate the script outside delivery first; otherwise install it through ManageSieve and watch the log for compile errors before sending mail.
Send a controlled test message through the LMTP path. While it is in flight, check the mail log with doveadm log find (if available) or your distribution's mail log. Then confirm the destination:
doveadm mailbox list -u <user>
doveadm mailbox status -u <user> messages <mailbox>
Replace <user> and <mailbox> with the test account and the expected target. If the message is missing, the fault is usually socket permissions or a Sieve parse error, not the rule logic.
Trade-offs and limitations
Sieve is intentionally small and executes at delivery, so it fits filing and forwarding better than stateful workflows. It cannot, for example, remember that a message was already filed yesterday. Failures are asynchronous: a broken rule often shows up only in the mail log, not in the user's client. A parse error in a global sieve_before script can block every delivery on the system, so keep a known-good rollback copy of both the Dovecot configuration and the global scripts. Finally, Sieve is not a security boundary; spam or virus verdicts should come from the MTA or a scanner, with Sieve only filing the result.
Actionable close
Pilot with one mailbox. Deliver a controlled message through LMTP, verify the filing with doveadm, compare the result against the old client-side rule, and then retire that client rule to avoid double filing. Once the path is proven, extend the global policy and let users manage their own scripts through ManageSieve.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.