lu‑milter is a lightweight Milter written in Rust (built on indymilter) that automatically generates a standards‑compliant `List-Unsubscribe` header for outbound messages. Mail clients such as Gmail detect this header and render a one‑click Unsubscribe link. The header contains a URL with an HMAC‑SHA‑256 token so you can safely verify unsubscribe requests server‑side.
- Secure HMAC token verifies that unsubscribe requests are genuine.
- Uses
MAIL FROMandRCPT TOenvelope addresses (no need to parse message bodies). - Simple TOML configuration.
- Runs as a Postfix / Sendmail Milter over TCP.
- Drops root privileges after binding.
- Includes a sample PHP endpoint that validates the token and records unsubscribes.
Edit `config.toml` (example below) and adjust the fields:
listen_host = "127.0.0.1" # where the milter listens
listen_port = 3000 # TCP port
url = "https://example.com" # do not include trailing slash
stump = "/rx/sample.php/" # path *after* base URL (include trailing slash)
delim = "/-/-/" # delimiter between address & hash (avoid plain "/")
secret = "secretkittens" # shared HMAC key
drop_user = "lumilter" # user the daemon drops toTip:
stumpmust end with/, whileurlmust not.
# Requires Rust toolchain (https://rustup.rs)
$ cargo build --releaseThe resulting binary is at target/release/lu-milter.
$ sudo ./lu-milter \
-p /run/lu-milter.pid \
-l /var/lumilter/lu-milter.log \
-c /var/lumilter/config.toml \
-d| Option | Description |
|---|---|
-c, --config <FILE> |
Path to TOML config |
-p, --pid <FILE> |
Write PID file |
-d, --daemon |
Run as background daemon |
-l, --log <FILE> |
Log file (default: /var/log/lu-milter.log) |
-h, --help |
Show help |
-V, --version |
Show version |
Add to `main.cf`:
milter_protocol = 6
milter_default_action = accept
smtpd_milters = inet:127.0.0.1:3000
non_smtpd_milters = $smtpd_milters
If you also sign with DKIM, list the DKIM milter after lu-milter so the added header doesn’t invalidate the signature.
sample.php demonstrates how to:
- URL‑decode the
from,to, andhashparameters. - Re‑compute the HMAC with the shared
secret. - Save the pair to a TAB‑separated
.rems.tabfile.
Configure the same values you used in config.toml:
$secret = "secretkittens";
$stump = "/rx/sample.php/";
$delim = "/-/-/";
$dotfile = ".rems.tab";Nginx security note – prevent access to dot‑files:
location ~ /\.
{
deny all;
access_log off;
log_not_found off;
} Copyright (C) 2025 Waitman Gobble
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, see <https://www.gnu.org/licenses/>.
Contact by email: <[email protected]>
<https://quantificant.com/contact>