csc-milter/csc_milter/main.py

133 lines
3.9 KiB
Python

#!/usr/bin/env python3
import argparse
from email.utils import parseaddr
import ipaddress
import logging
import os
import pwd
import subprocess
import Milter
SOCKETPATH = "/run/csc-milter/csc-milter.sock"
TIMEOUT = 5
MYORIGIN = ''
MYNETWORKS = []
REJECT_MESSAGE = 'You may not send from a CSC address on port 25 - please use port 587 instead.'
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
log_handler = logging.StreamHandler()
log_handler.setLevel(logging.DEBUG)
log_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
logger.addHandler(log_handler)
def is_in_myorigin(domain):
return domain == MYORIGIN or domain.endswith('.' + MYORIGIN)
def is_in_mynetworks(addr_str):
addr = ipaddress.ip_address(addr_str)
return any(addr in subnet for subnet in MYNETWORKS)
class CSCMilter(Milter.Base):
def __init__(self): # A new instance with each new connection.
super()
def connect(self, IPname, family, hostaddr):
"""
Called on connect to MTA.
:param IPname: the PTR name or bracketed IP of the SMTP client
:param family: one of (socket.AF_INET, socket.AF_INET6, socket.AF_UNIX)
:param hostaddr: a tuple or string with peer IP or socketname
Examples:
(self, 'ip068.subnet71.example.com', AF_INET, ('215.183.71.68', 4720) )
(self, 'ip6.mxout.example.com', AF_INET6, ('3ffe:80e8:d8::1', 4720, 1, 0) )
"""
self.IP = hostaddr[0]
if is_in_mynetworks(self.IP):
return Milter.ACCEPT
self.daemon_port = self.getsymval('{daemon_port}')
if self.daemon_port is not None and self.daemon_port != '25':
return Milter.ACCEPT
return Milter.CONTINUE
@Milter.noreply
def envfrom(self, mailfrom, *str):
self.mailfrom = mailfrom
return Milter.CONTINUE
def header(self, field, value):
"""Called for each header field in the message body."""
if field.lower() != 'from':
return Milter.CONTINUE
addr = parseaddr(value)[1]
if addr == '':
return Milter.CONTINUE
domain = addr.split('@')[1].lower()
if is_in_myorigin(domain):
logger.info('Rejecting message (MAIL FROM: %s, From: %s)' % (self.mailfrom, value))
# See RFC 1893
self.setreply('550', '5.7.1', REJECT_MESSAGE)
return Milter.REJECT
return Milter.CONTINUE
def check_postfix_user():
postfix_passwd = pwd.getpwnam('postfix')
uid = postfix_passwd.pw_uid
gid = postfix_passwd.pw_gid
if os.geteuid() != uid or os.getegid() != gid:
raise Exception('this program should be running as the Postfix user')
def get_postconf(param):
proc = subprocess.run(
rf"postconf -x | grep -oP '^{param} = \K(.*)'",
shell=True, capture_output=True, check=True
)
value = proc.stdout.decode().strip()
logger.debug(f'Retrieved from postconf: {param} = {value}')
return value
def main():
global MYORIGIN
global MYNETWORKS
check_postfix_user()
parser = argparse.ArgumentParser(description='Custom milter for CSC.')
parser.add_argument('--myorigin', help='domain name for locally-posted mail')
parser.add_argument('--mynetworks', help='space-separated list of trusted subnets')
args = parser.parse_args()
if args.myorigin is not None:
MYORIGIN = args.myorigin
else:
MYORIGIN = get_postconf('myorigin')
if args.mynetworks is not None:
networks = args.mynetworks.split()
else:
networks = get_postconf('mynetworks').split()
MYNETWORKS = [
ipaddress.ip_network(s.replace('[', '').replace(']', ''))
for s in networks
]
Milter.factory = CSCMilter
logger.debug("milter startup")
Milter.runmilter("csc-milter", SOCKETPATH, TIMEOUT)
logger.debug("milter shutdown")
if __name__ == "__main__":
main()