diff --git a/app/ldap-acl-milter.py b/app/lam.py similarity index 88% rename from app/ldap-acl-milter.py rename to app/lam.py index 74686c5..cc99450 100644 --- a/app/ldap-acl-milter.py +++ b/app/lam.py @@ -1,19 +1,14 @@ import Milter -import sys import traceback import string import random import re import email.utils import authres -from lam_config import LamConfig, LamConfigException -from lam_rex import rex_domain, rex_srs -from lam_logger import init_logger, log_debug, log_info, log_warning, log_error -from lam_policy import LamPolicyBackend, LamPolicyBackendException, LamSoftException, LamHardException - -# Globals... -g_config = None -g_policy_backend = None +from lam_globals import g_config, g_policy_backend +from lam_rex import g_rex_domain, g_rex_srs +from lam_logger import log_debug, log_info, log_warning, log_error +from lam_exceptions import LamSoftException, LamHardException class LdapAclMilter(Milter.Base): # Each new connection is handled in an own thread @@ -38,9 +33,6 @@ class LdapAclMilter(Milter.Base): log_info(log_line) elif kwargs['level'] == 'debug': log_debug(log_line) - else: - print("do_log(): invalid 'level' {}".format(kwargs['level'])) - sys.exit(1) def log_error(self, log_message): self.do_log(level='error', log_message=log_message) def log_warn(self, log_message): @@ -132,7 +124,6 @@ class LdapAclMilter(Milter.Base): # this may fail, if no x509 client certificate was used. # postfix only passes this macro to milters if the TLS connection # with the authenticating client was trusted in a x509 manner! - # http://postfix.1071664.n5.nabble.com/verification-levels-and-Milter-tp91634p91638.html # Unfortunately, postfix only passes the CN-field of the subject/issuer DN :-/ x509_subject = self.getsymval('{cert_subject}') if x509_subject != None: @@ -169,14 +160,14 @@ class LdapAclMilter(Milter.Base): # Strip out Simple Private Signature (PRVS) mailfrom = re.sub(r"^prvs=.{10}=", '', mailfrom) # SRS (https://www.libsrs2.org/srs/srs.pdf) - m_srs = rex_srs.match(mailfrom) + m_srs = g_rex_srs.match(mailfrom) if m_srs != None: self.log_info("Found SRS-encoded envelope-sender: {}".format(mailfrom)) mailfrom = m_srs.group(2) + '@' + m_srs.group(1) self.log_info("SRS envelope-sender replaced with: {}".format(mailfrom)) self.env_from = mailfrom.lower() self.log_debug("5321.from={}".format(self.env_from)) - m = rex_domain.match(self.env_from) + m = g_rex_domain.match(self.env_from) if m == None: return self.milter_action( action = 'reject', @@ -224,7 +215,7 @@ class LdapAclMilter(Milter.Base): if(hname.lower() == "From".lower()): hdr_5322_from = email.utils.parseaddr(hval) self.hdr_from = hdr_5322_from[1].lower() - m = re.match(rex_domain, self.hdr_from) + m = re.match(g_rex_domain, self.hdr_from) if m is None: return self.milter_action( action = 'reject', @@ -329,31 +320,3 @@ class LdapAclMilter(Milter.Base): # Clean up any external resources here. self.proto_stage = 'CLOSE' return self.milter_action(action = 'continue') - -if __name__ == "__main__": - init_logger() - try: - g_config = LamConfig() - except LamConfigException as e: - log_info("A config error was raised: {}".format(e)) - sys.exit(1) - try: - g_policy_backend = LamPolicyBackend(g_config) - except LamPolicyBackendException as e: - log_error("An backend init error was raised: {}".format(e)) - sys.exit(1) - try: - timeout = 600 - # Register to have the Milter factory create instances of your class: - Milter.factory = LdapAclMilter - # Tell the MTA which features we use - flags = Milter.ADDHDRS - Milter.set_flags(flags) - log_info("Starting {0}@socket: {1} in mode {2}".format( - g_config.milter_name, g_config.milter_socket, g_config.milter_mode - )) - Milter.runmilter(g_config.milter_name, g_config.milter_socket, timeout, True) - log_info("Shutdown {}".format(g_config.milter_name)) - except: - log_error("MAIN-EXCEPTION: {}".format(traceback.format_exc())) - sys.exit(1) diff --git a/app/lam_config.py b/app/lam_config.py index aa5fa91..7e57f28 100644 --- a/app/lam_config.py +++ b/app/lam_config.py @@ -1,13 +1,8 @@ import re import os -from lam_rex import rex_email from lam_logger import log_info - -class LamConfigException(Exception): - def __init__(self, message): - self.message = message - def __str__(self): - return self.message +from lam_exceptions import LamConfigException +from lam_rex import g_rex_email class LamConfig(): def __init__(self): @@ -73,7 +68,7 @@ class LamConfig(): # A blank separated list is expected whitelisted_rcpts_str = os.environ['MILTER_WHITELISTED_RCPTS'] for whitelisted_rcpt in re.split(',|\s', whitelisted_rcpts_str): - if rex_email.match(whitelisted_rcpt) == None: + if g_rex_email.match(whitelisted_rcpt) == None: raise LamConfigException( "ENV[MILTER_WHITELISTED_RCPTS]: invalid email address: {}" .format(whitelisted_rcpt) diff --git a/app/lam_exceptions.py b/app/lam_exceptions.py new file mode 100644 index 0000000..3d44cf2 --- /dev/null +++ b/app/lam_exceptions.py @@ -0,0 +1,20 @@ +class LamException(Exception): + def __init__(self, message="General exception message"): + self.message = message + def __str__(self) -> str: + return self.message + +class LamInitException(LamException): + pass + +class LamSoftException(LamException): + pass + +class LamHardException(LamException): + pass + +class LamPolicyBackendException(LamException): + pass + +class LamConfigException(LamException): + pass \ No newline at end of file diff --git a/app/lam_globals.py b/app/lam_globals.py new file mode 100644 index 0000000..b7b7595 --- /dev/null +++ b/app/lam_globals.py @@ -0,0 +1,26 @@ +import sys +from lam_exceptions import ( + LamInitException, LamPolicyBackendException, LamConfigException +) +from lam_config import LamConfig +from lam_logger import init_logger +from lam_policy_backend import LamPolicyBackend + +init_logger() + +g_config = None +try: + if g_config is None: + g_config = LamConfig() +except LamConfigException as e: + raise LamInitException(e.message) from e + +# Instantiate the LDAP policy backend and +# establish a permanent connection with the LDAP server +# which will be reused on any milter connection +g_policy_backend = None +try: + if g_policy_backend is None: + g_policy_backend = LamPolicyBackend(g_config) +except LamPolicyBackendException as e: + raise LamInitException(e.message) from e \ No newline at end of file diff --git a/app/lam_policy.py b/app/lam_policy_backend.py similarity index 88% rename from app/lam_policy.py rename to app/lam_policy_backend.py index bcb71b2..0e2b722 100644 --- a/app/lam_policy.py +++ b/app/lam_policy_backend.py @@ -1,25 +1,13 @@ import re from lam_logger import log_info, log_debug, log_error -from lam_rex import rex_domain +from lam_rex import g_rex_domain from ldap3 import ( Server, Connection, NONE, set_config_parameter ) from ldap3.core.exceptions import LDAPException - -class LamException(Exception): - def __init__(self, message="General exception message"): - self.message = message - def __str__(self) -> str: - return self.message - -class LamSoftException(LamException): - pass - -class LamHardException(LamException): - pass - -class LamPolicyBackendException(LamException): - pass +from lam_exceptions import ( + LamPolicyBackendException, LamHardException, LamSoftException +) class LamPolicyBackend(): def __init__(self, lam_config): @@ -28,11 +16,17 @@ class LamPolicyBackend(): try: set_config_parameter("RESTARTABLE_SLEEPTIME", 2) set_config_parameter("RESTARTABLE_TRIES", 2) - server = Server(self.config.ldap_server, get_info=NONE) + server = Server( + self.config.ldap_server, + connect_timeout = 3, + get_info = NONE + ) self.ldap_conn = Connection(server, - self.config.ldap_binddn, self.config.ldap_bindpw, - auto_bind=True, raise_exceptions=True, - client_strategy='RESTARTABLE' + self.config.ldap_binddn, + self.config.ldap_bindpw, + auto_bind = True, + raise_exceptions = True, + client_strategy = 'RESTARTABLE' ) log_info("Connected to LDAP-server: {}".format(self.config.ldap_server)) except LDAPException as e: @@ -45,7 +39,7 @@ class LamPolicyBackend(): rcpt_addr = kwargs['rcpt_addr'] from_source = kwargs['from_source'] lam_session = kwargs['lam_session'] - m = rex_domain.match(from_addr) + m = g_rex_domain.match(from_addr) if m == None: log_info("Could not determine domain of from={0}".format( from_addr @@ -53,7 +47,7 @@ class LamPolicyBackend(): raise LamSoftException() from_domain = m.group(1) log_debug("from_domain={}".format(from_domain)) - m = rex_domain.match(rcpt_addr) + m = g_rex_domain.match(rcpt_addr) if m == None: raise LamHardException( "Could not determine domain of rcpt={0}".format( diff --git a/app/lam_rex.py b/app/lam_rex.py index 7ea9942..45f8b1d 100644 --- a/app/lam_rex.py +++ b/app/lam_rex.py @@ -1,6 +1,7 @@ import re -rex_domain = re.compile(r'^\S*@(\S+)$') +# globaly used regex definitions +g_rex_domain = re.compile(r'^\S*@(\S+)$') # http://emailregex.com/ -> Python -rex_email = re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)") -rex_srs = re.compile(r"^SRS0=.+=.+=(\S+)=(\S+)\@.+$") \ No newline at end of file +g_rex_email = re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)") +g_rex_srs = re.compile(r"^SRS0=.+=.+=(\S+)=(\S+)\@.+$") \ No newline at end of file diff --git a/app/run_milter.py b/app/run_milter.py new file mode 100644 index 0000000..d54d77e --- /dev/null +++ b/app/run_milter.py @@ -0,0 +1,35 @@ +import Milter +import sys +import traceback +from lam_exceptions import LamInitException +from lam_logger import log_info, log_error +try: + import lam_globals +except LamInitException as e: + log_error("Init exception: {}".format(e.message)) + sys.exit(1) +from lam import LdapAclMilter + +if __name__ == "__main__": + try: + timeout = 600 + # Register to have the Milter factory create instances of your class: + Milter.factory = LdapAclMilter + # Tell the MTA which features we use + flags = Milter.ADDHDRS + Milter.set_flags(flags) + log_info("Starting {0}@socket: {1} in mode {2}".format( + lam_globals.g_config.milter_name, + lam_globals.g_config.milter_socket, + lam_globals.g_config.milter_mode + )) + Milter.runmilter( + lam_globals.g_config.milter_name, + lam_globals.g_config.milter_socket, + timeout, + True + ) + log_info("Shutdown {}".format(lam_globals.g_config.milter_name)) + except: + log_error("MAIN-EXCEPTION: {}".format(traceback.format_exc())) + sys.exit(1)