From a516ded2c81c297c73f00ca2fe6f83ce4db23e57 Mon Sep 17 00:00:00 2001 From: Dominik Chilla Date: Sun, 20 Feb 2022 00:29:51 +0100 Subject: [PATCH] ENV[MILTER_MAX_RCPT]: limits number of envelope recipients --- app/ldap-acl-milter.py | 59 ++++++++++++++++++++++-------------- tests/miltertest-ip-fail.lua | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 23 deletions(-) create mode 100644 tests/miltertest-ip-fail.lua diff --git a/app/ldap-acl-milter.py b/app/ldap-acl-milter.py index da31b50..ceaa3b0 100644 --- a/app/ldap-acl-milter.py +++ b/app/ldap-acl-milter.py @@ -10,7 +10,6 @@ import logging import string import random import re -from timeit import default_timer as timer import email.utils import authres @@ -29,7 +28,6 @@ g_re_domain = re.compile(r'^\S*@(\S+)$') # http://emailregex.com/ -> Python g_re_email = re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)") g_milter_mode = 'test' -g_milter_default_policy = 'reject' g_milter_schema = False g_milter_schema_wildcard_domain = False # works only if g_milter_schema == True g_milter_expect_auth = False @@ -37,6 +35,8 @@ g_milter_whitelisted_rcpts = {} g_milter_dkim_enabled = False g_milter_trusted_authservid = None g_re_srs = re.compile(r"^SRS0=.+=.+=(\S+)=(\S+)\@.+$") +g_milter_max_rcpt_enabled = False +g_milter_max_rcpt = 1 class LamException(Exception): def __init__(self, message="General exception message"): @@ -152,9 +152,6 @@ class LdapAclMilter(Milter.Base): from_addr = kwargs['from_addr'] rcpt_addr = kwargs['rcpt_addr'] from_source = kwargs['from_source'] - self.log_info("check_policy: from={0} rcpt={1} from_source={2}".format( - from_addr, rcpt_addr, from_source - )) m = g_re_domain.match(from_addr) if m == None: self.log_info("Could not determine domain of from={0}".format( @@ -243,19 +240,23 @@ class LdapAclMilter(Milter.Base): if len(g_ldap_conn.entries) == 0: # Policy not found in LDAP if g_milter_expect_auth == True: - self.log_info("policy mismatch from={0} rcpt={1} auth_method={2}".format( - from_addr, rcpt_addr, auth_method - )) + self.log_info( + "policy mismatch: from={0} from_src={1} rcpt={2} auth_method={3}".format( + from_addr, from_source, rcpt_addr, auth_method + ) + ) else: - self.log_info("policy mismatch from={0} rcpt={1}".format( - from_addr, rcpt_addr - )) + self.log_info( + "policy mismatch: from={0} from_src={1} rcpt={2}".format( + from_addr, from_source, rcpt_addr + ) + ) raise LamHardException("policy mismatch!") elif len(g_ldap_conn.entries) == 1: # Policy found in LDAP, but which one? entry = g_ldap_conn.entries[0] - self.log_info("policy match: {}".format( - entry.policyID.value + self.log_info("policy match: '{0}' from_src={1}".format( + entry.policyID.value, from_source )) elif len(g_ldap_conn.entries) > 1: # Something went wrong!? There shouldn´t be more than one entries! @@ -276,10 +277,15 @@ class LdapAclMilter(Milter.Base): self.log_debug("LDAP query: {}".format(query)) g_ldap_conn.search(g_ldap_base, query) if len(g_ldap_conn.entries) == 0: - self.log_info("policy mismatch from={0} rcpt={1}".format( - from_addr, rcpt_addr - )) + self.log_info( + "policy mismatch from={0} from_src={1} rcpt={2}".format( + from_addr, from_source, rcpt_addr + ) + ) raise LamHardException("policy mismatch") + self.log_info("policy match: '{0}' from_src={1}".format( + entry.policyID.value, from_source + )) except LDAPException as e: self.log_error("LDAP exception: {}".format(str(e))) raise LamSoftException("LDAP exception: " + str(e)) from e; @@ -435,6 +441,12 @@ class LdapAclMilter(Milter.Base): def eom(self): self.proto_stage = 'EOM' + if g_milter_max_rcpt_enabled: + if len(self.env_rcpts) > int(g_milter_max_rcpt): + if g_milter_mode == 'reject': + return self.milter_action(action='reject', reason='Too many recipients!') + else: + self.do_log("TEST-Mode: Too many recipients!") if g_milter_dkim_enabled: self.log_info("5321.from={0} 5322.from={1} 5322.from_domain={2} 5321.rcpt={3}".format( self.env_from, self.hdr_from, self.hdr_from_domain, self.env_rcpts @@ -519,13 +531,6 @@ if __name__ == "__main__": if 'MILTER_MODE' in os.environ: if re.match(r'^test|reject$',os.environ['MILTER_MODE'], re.IGNORECASE): g_milter_mode = os.environ['MILTER_MODE'].lower() - if 'MILTER_DEFAULT_POLICY' in os.environ: - if re.match(r'^reject|permit$',os.environ['MILTER_DEFAULT_POLICY'], re.IGNORECASE): - g_milter_default_policy = str(os.environ['MILTER_DEFAULT_POLICY']).lower() - else: - logging.warning("MILTER_DEFAULT_POLICY invalid value: {}" - .format(os.environ['MILTER_DEFAULT_POLICY']) - ) if 'MILTER_NAME' in os.environ: g_milter_name = os.environ['MILTER_NAME'] if 'MILTER_SCHEMA' in os.environ: @@ -589,6 +594,14 @@ if __name__ == "__main__": logging.error("ENV[MILTER_TRUSTED_AUTHSERVID] is mandatory!") sys.exit(1) logging.info("ENV[MILTER_DKIM_ENABLED]: {0}".format(g_milter_dkim_enabled)) + if 'MILTER_MAX_RCPT_ENABLED' in os.environ: + g_milter_max_rcpt_enabled = True + if 'MILTER_MAX_RCPT' in os.environ: + if os.environ['MILTER_MAX_RCPT'].isnumeric(): + g_milter_max_rcpt = os.environ['MILTER_MAX_RCPT'] + else: + print("ENV[MILTER_MAX_RCPT] must be numeric!") + sys.exit(1) try: set_config_parameter("RESTARTABLE_SLEEPTIME", 2) set_config_parameter("RESTARTABLE_TRIES", 2) diff --git a/tests/miltertest-ip-fail.lua b/tests/miltertest-ip-fail.lua new file mode 100644 index 0000000..fc644f8 --- /dev/null +++ b/tests/miltertest-ip-fail.lua @@ -0,0 +1,56 @@ +-- https://mopano.github.io/sendmail-filter-api/constant-values.html#com.sendmail.milter.MilterConstants +-- http://www.opendkim.org/miltertest.8.html + +-- socket must be defined as miltertest global variable (-D) +conn = mt.connect(socket) +if conn == nil then + error "mt.connect() failed" +end +if mt.conninfo(conn, "blubb-ip.host", "127.6.6.6") ~= nil then + error "mt.conninfo() failed" +end + +mt.set_timeout(60) + +-- 5321.FROM +if mt.mailfrom(conn, "tester-ip-fail@test.blah") ~= nil then + error "mt.mailfrom() failed" +end +if mt.getreply(conn) == SMFIR_CONTINUE then + mt.echo("FROM-continue") +elseif mt.getreply(conn) == SMFIR_REPLYCODE then + error("FROM-reject") +end + +-- 5321.RCPT+MACROS +mt.macro(conn, SMFIC_RCPT, "i", "4CgSNs5Q9sz7SllQ") +if mt.rcptto(conn, "") ~= nil then + error "mt.rcptto() failed" +end +if mt.getreply(conn) == SMFIR_CONTINUE then + mt.echo("RCPT-continue") +elseif mt.getreply(conn) == SMFIR_REPLYCODE then + mt.echo("RCPT-reject") +end + +-- 5322.HEADERS +if mt.header(conn, "fRoM", '"Blah Blubb" ') ~= nil then + error "mt.header(From) failed" +end +if mt.header(conn, "Authentication-REsuLTS", "my-auth-serv-id;\n dkim=fail header.d=test.blah header.s=selector1-test-blah header.b=mumble") ~= nil then + error "mt.header(Authentication-Results) failed" +end + +-- EOM +if mt.eom(conn) ~= nil then + error "mt.eom() failed" +end +mt.echo("EOM: " .. mt.getreply(conn)) +if mt.getreply(conn) == SMFIR_CONTINUE then + mt.echo("EOM-continue") +elseif mt.getreply(conn) == SMFIR_REPLYCODE then + mt.echo("EOM-reject") +end + +-- DISCONNECT +mt.disconnect(conn) \ No newline at end of file