Postfix milter: reliably setting Reply-To without breaking delivery
Why use a milter
Postfix’s cleanup and header_checks can rewrite headers, but a milter gives precise, programmable control at SMTP time and can modify or add headers safely before signing and delivery workflows run. A Python milter lets you inspect the exact inbound headers, compute a canonical Reply-To, and ensure consistent behavior across SMTP and local submissions.
Using the python code below, we will get the original From header and add it to reply-to
#!/usr/bin/env python3
import sys, os, re, email
import logging
from email.parser import BytesParser
from email.policy import default
#from Milter import Milter
import Milter
BaseClass = getattr(Milter, 'Base', getattr(Milter, 'Milter', None))
assert BaseClass is not None, "pymilter not installed (no Base/Milter class)"
BRAND = 'brandmail'
FROM_SENDER = 'mail@brandmail.io' # must be approved by your relay
LOG_LEVEL = os.environ.get('IM_LOG_LEVEL', 'INFO').upper()
logging.basicConfig(level=getattr(logging, LOG_LEVEL, logging.INFO), format='%(asctime)s %(levelname)s %(message)s')
addr_spec_re = re.compile(r'([^<,]+@[^>,\s]+)')
class BrandMilter(BaseClass):
def __init__(self):
super().__init__()
self.msg_headers = []
self.raw_from = None
def header(self, name, value):
# Record headers; capture first From we see
if name.lower() == 'from' and self.raw_from is None:
self.raw_from = value
# Keep all headers for later rewrite
self.msg_headers.append((name, value))
return Milter.CONTINUE
def _id(self):
try:
return self.idstr()
except Exception:
return "unknown-id"
def eom(self):
try:
# Build minimal header block for parsing
hdr_bytes = b''.join([f"{k}: {v}\r\n".encode('utf-8') for k, v in self.msg_headers])
raw = hdr_bytes + b"\r\n"
msg = BytesParser(policy=default).parsebytes(raw) or {}
# Extract first mailbox from original From
reply_to_addr = (self.raw_from or (msg.get('From') if hasattr(msg, 'get') else '') or '')
# Force approved From
final_from = f'"{BRAND}" <{FROM_SENDER}>'
# Ensure single Reply-To if available
if reply_to_addr:
if hasattr(msg, 'get') and msg.get('Reply-To'):
combined = f"{msg.get('Reply-To')}, {reply_to_addr}"
msg.replace_header('Reply-To', combined)
else:
msg['Reply-To'] = reply_to_addr
# Force approved From
if hasattr(msg, 'get') and msg.get('From'):
# Don't replace From because we will do it in the header_checks
# msg.replace_header('From', final_from)
pass
else:
msg['From'] = final_from
# Remove existing headers once per unique name
seen = set()
removed_count = 0
for k, _ in self.msg_headers:
kl = k.lower()
if kl in seen:
continue
seen.add(kl)
try:
self.chgheader(k, 0, None)
removed_count += 1
except Exception as ex:
logging.warning("%s failed to remove header %s: %r", self._id(), k, ex)
# Re-add normalized headers
added_count = 0
for k, v in (msg.items() if hasattr(msg, 'items') else []):
try:
self.addheader(k, str(v))
added_count += 1
except Exception as ex:
pass
return Milter.CONTINUE
except Exception as e:
# Never propagate exceptions to Postfix; log and continue
logging.error("%s eom exception: %r", self._id(), e)
# Utility to get body bytes accumulated by libmilter (may be empty)
def getbody(self):
# Milter.Base doesn’t buffer body by default; for header-only edits,
# returning empty body is fine; many MTAs provide headers before body.
return b''
def main():
sock_dir = "/var/spool/postfix/var/run/pymilter"
sock_path = f"{sock_dir}/brandmail.sock"
os.makedirs(sock_dir, mode=0o750, exist_ok=True)
try:
os.unlink(sock_path)
except FileNotFoundError:
pass
except OSError as e:
logging.error("Could not unlink socket %s: %s", SOCK_PATH, e)
raise
# Register class and run milter
Milter.factory = BrandMilter
logging.info("Starting milter on unix:%s", sock_path)
Milter.runmilter("brandmail-milter", f"unix:{sock_path}", 10)
if __name__ == "__main__":
main()
You can save the file in /opt/pymilter/milter.py or any other place you prefer (you need to give it execution permission with chmod +x)
This will ensure that python will create a socket at /var/spool/postfix/var/run/pymilter (Make sure the directory exists) and now create the service, depending on your OS, the below service is for ubuntu 20
[Unit]
Description=Postfix Python Milter
After=network.target
[Service]
Type=simple
User=postfix
Group=postfix
ExecStart=/opt/pymilter/venv/bin/python /opt/pymilter/milter.py
RuntimeDirectory=pymilter
RuntimeDirectoryMode=0750
UMask=0027
Restart=on-failure
[Install]
WantedBy=multi-user.target
Now point postfix smtpd_milters to the socket path:
milter_default_action = accept
smtpd_milters = unix:/var/run/pymilter/brandmail.sock
non_smtpd_milters = unix:/var/run/pymilter/brandmail.sock
We didn't add "/var/spool/postfix" to the path because postfix with chroot there depending on the OS, if postfix warns about the socked path, then append "/var/spool/postfix" to it
Reload the service and postfix and you are good to go! Enjoy!