133 lines
3.9 KiB
Python
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()
|