ken_nogi/NodeJS/postfixconsole_org2/postfix_oldconfig/milter.py
Kenichiro NOGI 88a402ce0f up
2026-07-10 18:13:30 +09:00

154 lines
4.8 KiB
Python

#!/usr/bin/env python3
import os
import re
import signal
import logging
from email.utils import parseaddr
import Milter
ALLOWED_RCPT_DOMAINS_FILE = "/etc/postfix/policy/allowed_rcpt_domains.regexp"
ALLOWED_FROM_ADDR_FILE = "/etc/postfix/policy/allowed_external_senders"
BLOCKED_FROM_DOMAINS_FILE = "/etc/postfix/policy/blocked_header_from_domains.txt"
LOG_FILE = "/var/log/postfix-policy-milter/milter.log"
def load_allowed_rcpt_domain_regexes(path):
regs = []
if not os.path.exists(path):
return regs
with open(path, "r", encoding="utf-8") as f:
for line in f:
s = line.strip()
if not s or s.startswith("#"):
continue
m = re.match(r'^/(.*?)/\s+OK\b', s)
if not m:
continue
try:
regs.append(re.compile(m.group(1), re.IGNORECASE))
except re.error:
pass
return regs
def load_allowed_from_addresses(path):
out = set()
if not os.path.exists(path):
return out
with open(path, "r", encoding="utf-8") as f:
for line in f:
s = line.strip()
if not s or s.startswith("#"):
continue
parts = s.split()
if len(parts) >= 2 and parts[1].upper() == "OK":
out.add(parts[0].lower())
return out
def load_blocked_domains(path):
out = set()
if not os.path.exists(path):
return out
with open(path, "r", encoding="utf-8") as f:
for line in f:
s = line.strip().lower()
if not s or s.startswith("#"):
continue
out.add(s)
return out
def extract_domain(addr):
if not addr or "@" not in addr:
return ""
return addr.rsplit("@", 1)[1].lower()
class PolicyMilter(Milter.Base):
allowed_rcpt_regexes = []
allowed_from_addrs = set()
blocked_from_domains = set()
@classmethod
def reload_config(cls):
cls.allowed_rcpt_regexes = load_allowed_rcpt_domain_regexes(ALLOWED_RCPT_DOMAINS_FILE)
cls.allowed_from_addrs = load_allowed_from_addresses(ALLOWED_FROM_ADDR_FILE)
cls.blocked_from_domains = load_blocked_domains(BLOCKED_FROM_DOMAINS_FILE)
def __init__(self):
self.id = Milter.uniqueID()
self.mail_from = ""
self.rcpt_tos = []
self.header_from_addr = ""
self.header_from_domain = ""
self.log = logging.getLogger("policy-milter")
def envfrom(self, mailfrom, *args):
self.mail_from = (mailfrom or "").strip("<>").lower()
return Milter.CONTINUE
def envrcpt(self, rcpt, *args):
self.rcpt_tos.append((rcpt or "").strip("<>").lower())
return Milter.CONTINUE
def header(self, name, value):
if name.lower() == "from" and not self.header_from_addr:
_, addr = parseaddr(value or "")
self.header_from_addr = (addr or "").lower()
self.header_from_domain = extract_domain(self.header_from_addr)
return Milter.CONTINUE
def eom(self):
# Rule 1: pass if any recipient domain is explicitly allowed.
for rcpt in self.rcpt_tos:
d = extract_domain(rcpt)
for rgx in self.allowed_rcpt_regexes:
if rgx.search("@" + d):
self.log.info("ACCEPT rule=1 rcpt=%s from_hdr=%s", rcpt, self.header_from_addr)
return Milter.ACCEPT
# Rule 3: pass if header From address is explicitly allowed.
if self.header_from_addr in self.allowed_from_addrs:
self.log.info("ACCEPT rule=3 from_hdr=%s", self.header_from_addr)
return Milter.ACCEPT
# Rule 2: reject if header From domain is blocked.
if self.header_from_domain in self.blocked_from_domains:
self.log.warning(
"REJECT rule=2 from_hdr=%s domain=%s mail_from=%s rcpt=%s",
self.header_from_addr, self.header_from_domain, self.mail_from, ",".join(self.rcpt_tos)
)
self.setreply("550", "5.7.1", "Header From domain is not permitted.")
return Milter.REJECT
self.log.info("ACCEPT default from_hdr=%s rcpt=%s", self.header_from_addr, ",".join(self.rcpt_tos))
return Milter.ACCEPT
def setup_logging():
os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
logging.basicConfig(
filename=LOG_FILE,
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
def main():
setup_logging()
PolicyMilter.reload_config()
def hup(signum, frame):
PolicyMilter.reload_config()
logging.getLogger("policy-milter").info("Reloaded policy by SIGHUP")
signal.signal(signal.SIGHUP, hup)
Milter.factory = PolicyMilter
Milter.runmilter("postfix-policy-milter", "unix:/run/postfix-policy-milter/milter.sock", 600)
if __name__ == "__main__":
main()