From 68d173fda22c9016715d84ec4a7d06365236d602 Mon Sep 17 00:00:00 2001 From: Dominik Chilla Date: Sun, 6 Mar 2022 23:49:45 +0100 Subject: [PATCH] more tests; LamSession() --- app/lam.py | 207 +++++++++++++++++----------------- app/lam_exceptions.py | 2 +- app/lam_policy_backend.py | 27 +++-- app/lam_session.py | 101 +++++++++++++++++ app/run_milter.py | 3 +- tests/sasl-wildcard-dkim.lua | 2 +- tests/too_many_rcpts.lua | 55 +++++++++ tests/x509-denied-rcpts.lua | 0 tests/x509-denied-senders.lua | 0 9 files changed, 275 insertions(+), 122 deletions(-) create mode 100644 app/lam_session.py create mode 100644 tests/too_many_rcpts.lua create mode 100644 tests/x509-denied-rcpts.lua create mode 100644 tests/x509-denied-senders.lua diff --git a/app/lam.py b/app/lam.py index 54c4f9b..70ad652 100644 --- a/app/lam.py +++ b/app/lam.py @@ -1,7 +1,5 @@ import Milter import traceback -import string -import random import re import email.utils import authres @@ -14,17 +12,16 @@ from lam_session import LamSession class LdapAclMilter(Milter.Base): # Each new connection is handled in an own thread def __init__(self): - # client_addr gets overriden on any connect() - self.client_addr = None + self.session = None def do_log(self, **kwargs): log_line = '' - if hasattr(self, 'mconn_id'): - log_line = "{}".format(self.mconn_id) - if self.queue_id != 'invalid': - log_line = "{0}/{1}".format(log_line, self.queue_id) - if self.proto_stage != 'invalid': - log_line = "{0}/{1}".format(log_line, self.proto_stage) + if hasattr(self.session, 'mconn_id'): + log_line = "{}".format(self.session.get_mconn_id()) + if self.session.get_queue_id() != 'invalid': + log_line = "{0}/{1}".format(log_line, self.session.get_queue_id()) + if self.session.get_proto_stage() != 'invalid': + log_line = "{0}/{1}".format(log_line, self.session.get_proto_stage()) log_line = "{0} {1}".format(log_line, kwargs['log_message']) if kwargs['level'] == 'error': log_error(log_line) @@ -43,27 +40,7 @@ class LdapAclMilter(Milter.Base): def log_debug(self, log_message): self.do_log(level='debug', log_message=log_message) - def reset(self): - self.proto_stage = 'invalid' - self.env_from = None - self.null_sender = False - self.sasl_user = None - self.x509_subject = None - self.x509_issuer = None - self.queue_id = 'invalid' - self.env_rcpts = [] - self.hdr_from = None - self.hdr_from_domain = None - self.dkim_valid = False - self.dkim_aligned = False - self.passed_dkim_results = [] - self.log_debug("reset(): {}".format(self.__dict__)) - # https://stackoverflow.com/a/2257449 - self.mconn_id = g_config_backend.milter_name + ': ' + ''.join( - random.choice(string.ascii_lowercase + string.digits) for _ in range(8) - ) - - def milter_action(self, **kwargs): + def milter_action(self, **kwargs) -> int: if 'action' not in kwargs: raise Exception("'action' kwarg is mandatory!") message = None @@ -89,30 +66,26 @@ class LdapAclMilter(Milter.Base): if 'message' in kwargs: message = kwargs['message'] # prepend queue-id to message if it´s already available (DATA and later) - if self.queue_id != 'invalid': - message = " queue_id: {0} - {1}".format(self.queue_id, message) + if self.session.get_queue_id() != 'invalid': + message = " queue_id: {0} - {1}".format(self.session.get_queue_id(), message) # append reason to message if 'reason' in kwargs: message = "{0} - reason: {1}".format(message, kwargs['reason']) if kwargs['action'] == 'reject' or kwargs['action'] == 'tmpfail': self.log_info("{0} - milter_action={1} message={2}".format( - self.mconn_id, kwargs['action'], message + self.session.get_mconn_id(), kwargs['action'], message )) self.setreply(smtp_code, smtp_ecode, message) return smfir def connect(self, IPname, family, hostaddr): - self.reset() - self.proto_stage = 'CONNECT' - self.client_addr = hostaddr[0] - self.log_debug("client_addr={0}, client_port={1}".format( - self.client_addr, hostaddr[1]) - ) + self.session = LamSession(hostaddr[0], g_config_backend) + self.session.set_proto_stage('CONNECT') return self.milter_action(action = 'continue') def envfrom(self, mailfrom, *str): - self.reset() - self.proto_stage = 'FROM' + self.session.reset() + self.session.set_proto_stage('FROM') if g_config_backend.milter_expect_auth: try: # this may fail, if no x509 client certificate was used. @@ -121,14 +94,14 @@ class LdapAclMilter(Milter.Base): # Unfortunately, postfix only passes the CN-field of the subject/issuer DN :-/ x509_subject = self.getsymval('{cert_subject}') if x509_subject != None: - self.x509_subject = x509_subject - self.log_debug("x509_subject={}".format(self.x509_subject)) + self.session.set_x509_subject(x509_subject) + self.log_debug("x509_subject={}".format(self.session.get_x509_subject())) else: self.log_debug("No x509_subject registered") x509_issuer = self.getsymval('{cert_issuer}') if x509_issuer != None: - self.x509_issuer = x509_issuer - self.log_debug("x509_issuer={}".format(self.x509_issuer)) + self.session.set_x509_issuer(x509_issuer) + self.log_debug("x509_issuer={}".format(self.session.get_x509_issuer())) else: self.log_debug("No x509_issuer registered") except: @@ -137,20 +110,21 @@ class LdapAclMilter(Milter.Base): # this may fail, if no SASL authentication preceded sasl_user = self.getsymval('{auth_authen}') if sasl_user != None: - self.sasl_user = sasl_user - self.log_debug("sasl_user={}".format(self.sasl_user)) + self.session.set_sasl_user(sasl_user) + self.log_debug("sasl_user={}".format(self.session.get_sasl_user())) else: self.log_debug("No sasl_user registered") except: self.log_error("sasl_user exception: {}".format(traceback.format_exc())) self.log_info( - "auth: client_ip={0} x509_subject={1} x509_issuer={2} sasl_user={3}".format( - self.client_addr, self.x509_subject, self.x509_issuer, self.sasl_user + "auth: client_ip={0} sasl_user={1} x509_subject={2} x509_issuer={3}".format( + self.session.get_client_addr(), self.session.get_sasl_user(), + self.session.get_x509_subject(), self.session.get_x509_issuer() ) ) if mailfrom == '<>': - self.null_sender = True - if g_config_backend.milter_allow_null_sender and self.null_sender: + self.session.set_null_sender(True) + if g_config_backend.milter_allow_null_sender and self.session.is_null_sender(): self.log_info("Null-sender accepted - skipping policy checks") else: mailfrom = mailfrom.replace("<","") @@ -164,19 +138,21 @@ class LdapAclMilter(Milter.Base): 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 = g_rex_domain.match(self.env_from) + self.session.set_env_from(mailfrom.lower()) + self.log_debug("5321.from={}".format(self.session.get_env_from())) + m = g_rex_domain.match(self.session.get_env_from()) if m == None: return self.milter_action( action = 'reject', - reason = "Could not determine domain of 5321.from={}".format(self.env_from) + reason = "Could not determine domain of 5321.from={}".format( + self.session.get_env_from() + ) ) return self.milter_action(action = 'continue') def envrcpt(self, to, *str): - self.proto_stage = 'RCPT' - if g_config_backend.milter_allow_null_sender and self.null_sender: + self.session.set_proto_stage('RCPT') + if g_config_backend.milter_allow_null_sender and self.session.is_null_sender(): return self.milter_action(action = 'continue') to = to.replace("<","") to = to.replace(">","") @@ -189,17 +165,17 @@ class LdapAclMilter(Milter.Base): # Collect all envelope-recipients for later # investigation (EOM). Do not perform any # policy action at this protocol phase. - self.env_rcpts.append(to) + self.session.add_env_rcpt(to) else: # DKIM disabled. Policy enforcement takes place here. try: g_policy_backend.check_policy( - from_addr = self.env_from, + self.session, + from_addr = self.session.get_env_from(), rcpt_addr = to, - from_source = 'envelope', - lam_session = self + from_source = 'envelope' ) - self.env_rcpts.append(to) + self.session.add_env_rcpt(to) except LamSoftException as e: if g_config_backend.milter_mode == 'reject': return self.milter_action(action = 'tmpfail') @@ -214,24 +190,26 @@ class LdapAclMilter(Milter.Base): return self.milter_action(action = 'continue') def header(self, hname, hval): - self.proto_stage = 'HDR' - self.queue_id = self.getsymval('i') - if g_config_backend.milter_allow_null_sender and self.null_sender: + self.session.set_proto_stage('HDR') + self.session.set_queue_id(self.getsymval('i')) + if g_config_backend.milter_allow_null_sender and self.session.is_null_sender(): return self.milter_action(action = 'continue') if g_config_backend.milter_dkim_enabled == True: # Parse RFC-5322-From header if(hname.lower() == "From".lower()): hdr_5322_from = email.utils.parseaddr(hval) - self.hdr_from = hdr_5322_from[1].lower() - m = re.match(g_rex_domain, self.hdr_from) + self.session.set_hdr_from(hdr_5322_from[1].lower()) + m = re.match(g_rex_domain, self.session.get_hdr_from()) if m is None: return self.milter_action( action = 'reject', - reason = "Could not determine domain-part of 5322.from={}".format(self.hdr_from) + reason = "Could not determine domain-part of 5322.from={}".format( + self.session.get_hdr_from() + ) ) - self.hdr_from_domain = m.group(1) + self.session.set_hdr_from_domain(m.group(1)) self.log_debug("5322.from={0}, 5322.from_domain={1}".format( - self.hdr_from, self.hdr_from_domain + self.session.get_hdr_from(), self.session.get_hdr_from_domain() )) # Parse RFC-7601 Authentication-Results header elif(hname.lower() == "Authentication-Results".lower()): @@ -244,12 +222,14 @@ class LdapAclMilter(Milter.Base): for ar_result in ar.results: if ar_result.method.lower() == 'dkim': if ar_result.result.lower() == 'pass': - self.passed_dkim_results.append(ar_result.header_d.lower()) + self.session.add_passed_dkim_result(ar_result.header_d.lower()) self.log_debug("dkim=pass sdid={}".format(ar_result.header_d)) - self.dkim_valid = True + self.session.set_dkim_valid(True) else: - self.log_debug("Ignoring authentication results of {}".format( - ar.authserv_id) + self.log_debug( + "Ignoring authentication results of {}".format( + ar.authserv_id + ) ) except Exception as e: self.log_info("AR-parse exception: {0}".format(str(e))) @@ -264,63 +244,76 @@ class LdapAclMilter(Milter.Base): return self.milter_action(action = 'continue') def eom(self): - self.proto_stage = 'EOM' + self.session.set_proto_stage('EOM') if g_config_backend.milter_max_rcpt_enabled: - if len(self.env_rcpts) > int(g_config_backend.milter_max_rcpt): + if len(self.session.get_env_rcpts()) > int(g_config_backend.milter_max_rcpt): if g_config_backend.milter_mode == 'reject': return self.milter_action(action='reject', reason='Too many recipients!') else: - self.do_log("TEST-Mode: Too many recipients!") - if g_config_backend.milter_allow_null_sender and self.null_sender: + self.log_error("TEST-Mode: Too many recipients!") + if g_config_backend.milter_allow_null_sender and self.session.is_null_sender(): return self.milter_action(action = 'continue') if g_config_backend.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 - )) - if self.dkim_valid: + self.log_info( + "5321.from={0} 5322.from={1} 5322.from_domain={2} 5321.rcpt={3}".format( + self.session.get_env_from(), self.session.get_hdr_from(), + self.session.get_hdr_from_domain(), self.session.get_env_rcpts() + ) + ) + if self.session.is_dkim_valid(): # There is at least one valid DKIM signature! # Check if one of them is also aligned - for passed_dkim_sdid in self.passed_dkim_results: - if self.hdr_from_domain.lower() == passed_dkim_sdid.lower(): - self.dkim_aligned = True + for passed_dkim_sdid in self.session.get_passed_dkim_results(): + if self.session.get_hdr_from_domain().lower() == passed_dkim_sdid.lower(): + self.session.set_dkim_aligned(True) self.log_info("Found aligned DKIM signature for SDID: {0}".format( passed_dkim_sdid )) reject_message = False - for rcpt in self.env_rcpts: + for rcpt in self.session.get_env_rcpts(): if rcpt in g_config_backend.milter_whitelisted_rcpts: self.log_info("Welcome-listed rcpt={}".format(rcpt)) try: - # Check 5321.from against policy + # Check 5321.from <-> 5321.rcpt against policy g_policy_backend.check_policy( - from_addr=self.env_from, + self.session, + from_addr=self.session.get_env_from(), rcpt_addr=rcpt, - from_source='envelope', - lam_session=self + from_source='envelope' ) self.log_info( - "action=pass 5321.from={0} 5321.rcpt={1}".format(self.env_from, rcpt) + "action=pass 5321.from={0} 5321.rcpt={1}".format( + self.session.get_env_from(), rcpt + ) ) except LamSoftException as e: - self.log_info(e.message) + self.log_info(str(e)) if g_config_backend.milter_mode == 'reject': return self.milter_action(action = 'tmpfail') else: - self.log_info("TEST-Mode - tmpfail") + self.log_info("TEST-Mode - tmpfail: {}".format(str(e))) except LamHardException as e: self.log_info(e.message) - if self.dkim_aligned: + if self.session.is_dkim_aligned(): try: - # Check 5322.from against policy + # Check 5322.from <-> 5321.rcpt against policy g_policy_backend.check_policy( - from_addr=self.hdr_from, + self.session, + from_addr=self.session.get_hdr_from(), rcpt_addr=rcpt, - from_source='from-header', - lam_session=self + from_source='from-header' ) self.log_info( - "action=pass 5322.from={0} 5321.rcpt={1}".format(self.hdr_from, rcpt) + "action=pass 5322.from={0} 5321.rcpt={1}".format( + self.session.get_hdr_from(), rcpt + ) ) + except LamSoftException as e: + self.log_info(str(e)) + if g_config_backend.milter_mode == 'reject': + return self.milter_action(action = 'tmpfail') + else: + self.log_info("TEST-Mode - tmpfail: {}".format(str(e))) except LamHardException as e: reject_message = True else: @@ -337,19 +330,21 @@ class LdapAclMilter(Milter.Base): ) else: # * DKIM check disabled - # Iterate through all accepted envelope recipients and log - for rcpt in self.env_rcpts: - self.log_info("action=pass 5321.from={0} 5321.rcpt={1}".format(self.env_from, rcpt)) - + # Iterate through all accepted envelope recipients and log success + for rcpt in self.session.get_env_rcpts(): + self.log_info("action=pass 5321.from={0} 5321.rcpt={1}".format( + self.session.get_env_from(), rcpt) + ) + # No policy violations so far :-) return self.milter_action(action = 'continue') def abort(self): # Client disconnected prematurely - self.proto_stage = 'ABORT' + self.session.set_proto_stage('ABORT') return self.milter_action(action = 'continue') def close(self): # Always called, even when abort is called. # Clean up any external resources here. - self.proto_stage = 'CLOSE' + self.session.set_proto_stage('CLOSE') return self.milter_action(action = 'continue') diff --git a/app/lam_exceptions.py b/app/lam_exceptions.py index 0f234e9..d8742a0 100644 --- a/app/lam_exceptions.py +++ b/app/lam_exceptions.py @@ -17,4 +17,4 @@ class LamPolicyBackendException(LamException): pass class LamConfigBackendException(LamException): - pass \ No newline at end of file + pass diff --git a/app/lam_policy_backend.py b/app/lam_policy_backend.py index 3df2367..0250cc2 100644 --- a/app/lam_policy_backend.py +++ b/app/lam_policy_backend.py @@ -36,12 +36,11 @@ class LamPolicyBackend(): "Connection to LDAP-server failed: {}".format(str(e)) ) from e - def check_policy(self, **kwargs): + def check_policy(self, session: LamSession, **kwargs): from_addr = kwargs['from_addr'] rcpt_addr = kwargs['rcpt_addr'] from_source = kwargs['from_source'] - lam_session = kwargs['lam_session'] - mcid = "{}/Policy".format(lam_session.mconn_id) + mcid = "{}/Policy".format(session.get_mconn_id()) m = g_rex_domain.match(from_addr) if m == None: raise LamHardException( @@ -63,18 +62,22 @@ class LamPolicyBackend(): # LDAP-ACL-Milter schema enabled auth_method = '' if self.config.milter_expect_auth == True: - auth_method = "(|(allowedClientAddr=" + lam_session.client_addr + ")%SASL_AUTH%%X509_AUTH%)" - if lam_session.sasl_user: + auth_method = "(|(allowedClientAddr={})%SASL_AUTH%%X509_AUTH%)".format( + session.get_client_addr() + ) + if session.get_sasl_user(): auth_method = auth_method.replace( - '%SASL_AUTH%',"(allowedSaslUser=" + lam_session.sasl_user + ")" + '%SASL_AUTH%',"(allowedSaslUser={})".format( + session.get_sasl_user() + ) ) else: auth_method = auth_method.replace('%SASL_AUTH%','') - if lam_session.x509_subject and lam_session.x509_issuer: + if session.get_x509_subject() and session.get_x509_issuer(): auth_method = auth_method.replace('%X509_AUTH%', "(&"+ - "(allowedx509subject=" + lam_session.x509_subject + ")" + - "(allowedx509issuer=" + lam_session.x509_issuer + ")" + + "(allowedx509subject=" + session.get_x509_subject() + ")" + + "(allowedx509issuer=" + session.get_x509_issuer() + ")" + ")" ) else: @@ -168,9 +171,9 @@ class LamPolicyBackend(): query = self.config.ldap_query.replace("%rcpt%", rcpt_addr) query = query.replace("%from%", from_addr) if self.config.milter_expect_auth: - query = query.replace("%client_addr%", lam_session.client_addr) - if lam_session.sasl_user is not None: - query = query.replace("%sasl_user%", lam_session.sasl_user) + query = query.replace("%client_addr%", session.get_client_addr()) + if session.get_sasl_user() is not None: + query = query.replace("%sasl_user%", session.get_sasl_user()) query = query.replace("%from_domain%", from_domain) query = query.replace("%rcpt_domain%", rcpt_domain) log_debug("{0} LDAP query: {1}".format(mcid, query)) diff --git a/app/lam_session.py b/app/lam_session.py new file mode 100644 index 0000000..e47f445 --- /dev/null +++ b/app/lam_session.py @@ -0,0 +1,101 @@ +import string +import random +from lam_logger import log_debug +from lam_policy_backend import LamConfigBackend + +class LamSession(): + def __init__(self, client_addr: str, config: LamConfigBackend): + self.client_addr = client_addr + self.config = config + self.reset() + + def reset(self): + self.proto_stage = 'invalid' + self.env_from = None + self.null_sender = False + self.sasl_user = None + self.x509_subject = None + self.x509_issuer = None + self.queue_id = 'invalid' + self.env_rcpts = [] + self.hdr_from = None + self.hdr_from_domain = None + self.dkim_valid = False + self.dkim_aligned = False + self.passed_dkim_results = [] + log_debug("reset(): {}".format(self.__dict__)) + # https://stackoverflow.com/a/2257449 + self.mconn_id = self.config.milter_name + ': ' + ''.join( + random.choice(string.ascii_lowercase + string.digits) for _ in range(8) + ) + + def get_client_addr(self) -> str: + return self.client_addr + + def set_proto_stage(self, stage: str): + self.proto_stage = stage + def get_proto_stage(self) -> str: + return self.proto_stage + + def set_env_from(self, env_from: str): + self.env_from = env_from + def get_env_from(self) -> str: + return self.env_from + + def set_null_sender(self, null_sender: bool): + self.null_sender = null_sender + def is_null_sender(self) -> bool: + return self.null_sender + + def set_sasl_user(self, sasl_user: str): + self.sasl_user = sasl_user + def get_sasl_user(self) -> str: + return self.sasl_user + + def set_x509_subject(self, x509_subject: str): + self.x509_subject = x509_subject + def get_x509_subject(self) -> str: + return self.x509_subject + + def set_x509_issuer(self, x509_issuer: str): + self.x509_issuer = x509_issuer + def get_x509_issuer(self) -> str: + return self.x509_issuer + + def set_queue_id(self, queue_id: str): + self.queue_id = queue_id + def get_queue_id(self) -> str: + return self.queue_id + + def add_env_rcpt(self, rcpt: str): + self.env_rcpts.append(rcpt) + def get_env_rcpts(self) -> list: + return self.env_rcpts + + def set_hdr_from(self, hdr_from: str): + self.hdr_from = hdr_from + def get_hdr_from(self) -> str: + return self.hdr_from + + def set_hdr_from_domain(self, hdr_from_domain: str): + self.hdr_from_domain = hdr_from_domain + def get_hdr_from_domain(self) -> str: + return self.hdr_from_domain + + def set_dkim_valid(self, dkim_valid: bool): + self.dkim_valid = dkim_valid + def is_dkim_valid(self) -> bool: + return self.dkim_valid + + def set_dkim_aligned(self, dkim_aligned: bool): + self.dkim_aligned = dkim_aligned + def is_dkim_aligned(self) -> bool: + return self.dkim_aligned + + def add_passed_dkim_result(self, dkim_result: str): + self.passed_dkim_results.append(dkim_result) + def get_passed_dkim_results(self) -> list: + return self.passed_dkim_results + + def get_mconn_id(self) -> str: + return self.mconn_id \ No newline at end of file diff --git a/app/run_milter.py b/app/run_milter.py index 020296f..a175851 100644 --- a/app/run_milter.py +++ b/app/run_milter.py @@ -12,7 +12,6 @@ from lam import LdapAclMilter if __name__ == "__main__": try: - timeout = lam_backends.g_config_backend.milter_timeout # Register to have the Milter factory create instances of your class: Milter.factory = LdapAclMilter # Tell the MTA which features we use @@ -24,7 +23,7 @@ if __name__ == "__main__": Milter.runmilter( lam_backends.g_config_backend.milter_name, lam_backends.g_config_backend.milter_socket, - timeout, + lam_backends.g_config_backend.milter_timeout, True ) log_info("Shutdown {}".format(lam_backends.g_config_backend.milter_name)) diff --git a/tests/sasl-wildcard-dkim.lua b/tests/sasl-wildcard-dkim.lua index 6d23d79..c26c7d1 100644 --- a/tests/sasl-wildcard-dkim.lua +++ b/tests/sasl-wildcard-dkim.lua @@ -6,7 +6,7 @@ conn = mt.connect(socket) if conn == nil then error "mt.connect() failed" end -if mt.conninfo(conn, "localhost", "::1") ~= nil then +if mt.conninfo(conn, "localhost", "2001:db8:dead:beef::1234") ~= nil then error "mt.conninfo() failed" end diff --git a/tests/too_many_rcpts.lua b/tests/too_many_rcpts.lua new file mode 100644 index 0000000..6408857 --- /dev/null +++ b/tests/too_many_rcpts.lua @@ -0,0 +1,55 @@ +-- 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.128.129.130") ~= nil then + error "mt.conninfo() failed" +end + +mt.set_timeout(60) + +-- 5321.FROM +if mt.mailfrom(conn, "tester-sender@test.blah") ~= nil then + error "mt.mailfrom() failed" +end + +-- FIRST 5321.RCPT +if mt.rcptto(conn, "") ~= nil then + error "mt.rcptto() failed" +end +if mt.getreply(conn) == SMFIR_CONTINUE then + mt.echo("RCPT1-continue") +elseif mt.getreply(conn) == SMFIR_REPLYCODE then + mt.echo("RCPT1-reject") +end + +-- SECOND 5321.RCPT +if mt.rcptto(conn, "") ~= nil then + error "mt.rcptto() failed" +end +if mt.getreply(conn) == SMFIR_CONTINUE then + mt.echo("RCPT2-continue") +elseif mt.getreply(conn) == SMFIR_REPLYCODE then + mt.echo("RCPT2-reject") +end + +-- SET RCPT-MACRO +mt.macro(conn, SMFIC_RCPT, "i", "some-queue-id") + +-- 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 diff --git a/tests/x509-denied-rcpts.lua b/tests/x509-denied-rcpts.lua new file mode 100644 index 0000000..e69de29 diff --git a/tests/x509-denied-senders.lua b/tests/x509-denied-senders.lua new file mode 100644 index 0000000..e69de29