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.targetNow 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.sockWe 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!