From fd77e8f93b618456ae5d60af1649188cf622e998 Mon Sep 17 00:00:00 2001 From: Dominik Chilla Date: Wed, 7 Jun 2023 21:08:21 +0200 Subject: [PATCH] SPF-redirect + refactoring + OCI@alpine --- BASEOS | 1 - OCI/Dockerfile | 26 +++ entrypoint.sh => OCI/cmd | 0 VERSION | 1 - activate_venv | 2 - app/sos-milter.py | 111 +++++++---- app/sos-milter_eom.py | 322 -------------------------------- docker-build.sh | 28 --- docker/debian/Dockerfile | 26 --- requirements.txt | 3 + tests/miltertest.lua | 44 +---- tests/miltertest_conn_reuse.lua | 85 +++++++++ tests/miltertest_redirect.lua | 49 +++++ 13 files changed, 240 insertions(+), 458 deletions(-) delete mode 100644 BASEOS create mode 100644 OCI/Dockerfile rename entrypoint.sh => OCI/cmd (100%) mode change 100755 => 100644 delete mode 100644 VERSION delete mode 100644 activate_venv delete mode 100644 app/sos-milter_eom.py delete mode 100755 docker-build.sh delete mode 100644 docker/debian/Dockerfile create mode 100644 requirements.txt create mode 100644 tests/miltertest_conn_reuse.lua create mode 100644 tests/miltertest_redirect.lua diff --git a/BASEOS b/BASEOS deleted file mode 100644 index 2dee175..0000000 --- a/BASEOS +++ /dev/null @@ -1 +0,0 @@ -debian diff --git a/OCI/Dockerfile b/OCI/Dockerfile new file mode 100644 index 0000000..0f35c47 --- /dev/null +++ b/OCI/Dockerfile @@ -0,0 +1,26 @@ +ARG PARENT_IMAGE=alpine:3.17 +FROM ${PARENT_IMAGE} +LABEL maintainer="Dominik Chilla " +LABEL git_repo="https://github.com/chillout2k/sos-milter" + +ADD ./requirements.txt /requirements.txt + +RUN apk update \ + && apk add --no-cache python3 python3-dev py3-pip \ + gcc libc-dev libmilter-dev \ + && pip3 install -r requirements.txt \ + && apk del gcc libc-dev libmilter-dev python3-dev py3-pip \ + && apk add libmilter \ + && adduser -D sos-milter \ + && install -d -o sos-milter /socket \ + && rm -rf /var/cache/apk/* /requirements.txt + +ADD ./app/ /app/ +ADD ./OCI/cmd /cmd +RUN chown -R sos-milter /app /cmd \ + && chmod -R +x /app /cmd + +VOLUME [ "/socket" ] + +USER sos-milter +CMD [ "/cmd" ] \ No newline at end of file diff --git a/entrypoint.sh b/OCI/cmd old mode 100755 new mode 100644 similarity index 100% rename from entrypoint.sh rename to OCI/cmd diff --git a/VERSION b/VERSION deleted file mode 100644 index f56a066..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -20.02 diff --git a/activate_venv b/activate_venv deleted file mode 100644 index 3fcc873..0000000 --- a/activate_venv +++ /dev/null @@ -1,2 +0,0 @@ -. venv/bin/activate - diff --git a/app/sos-milter.py b/app/sos-milter.py index 16a1f37..740a3aa 100644 --- a/app/sos-milter.py +++ b/app/sos-milter.py @@ -115,35 +115,13 @@ class SOSMilter(Milter.Base): except LDAPException: logging.error(self.mconn_id + "/FROM " + traceback.format_exc()) - # Get TXT record of sender domain - dns_response = None - try: - dns_response = dns.resolver.resolve(self.env_from_domain, 'TXT') - except dns.resolver.NoAnswer as e: - logging.warning(self.mconn_id + " /FROM " + e.msg) - # accept message if DNS-resolver fails - return Milter.CONTINUE - except dns.resolver.NXDOMAIN as e: - logging.warning(self.mconn_id + - " /FROM " + e.msg - ) - # accept message if DNS-resolver fails - return Milter.CONTINUE - except: - logging.error(self.mconn_id + " DNS-Resolver-EXCEPTION: " + traceback.format_exc()) - # accept message if DNS-resolver fails - return Milter.CONTINUE - for rdata in dns_response: - logging.debug(self.mconn_id + "/FROM DNS-TXT: {0}".format(rdata.to_text())) - if re.match(r'^"v=spf1.*"$', rdata.to_text(), re.IGNORECASE): - # we´ve got a SPF match! - self.spf_record = rdata.to_text() - # TODO: check if spf-record is a redirect!? - logging.debug(self.mconn_id + "/FROM Found SPFv1: {0}".format(self.spf_record)) - break + # Get TXT-SPF record of sender domain + self.spf_record = self.get_spf_record(self.env_from_domain) return Milter.CONTINUE except: - logging.error(self.mconn_id + " FROM-EXCEPTION: " + traceback.format_exc()) + logging.error(self.mconn_id + + " FROM-EXCEPTION: " + traceback.format_exc() + ) self.setreply('450','4.7.1', g_milter_tmpfail_message) return Milter.TEMPFAIL @@ -152,9 +130,13 @@ class SOSMilter(Milter.Base): return Milter.CONTINUE self.next_hop = self.getsymval('{rcpt_host}') if self.next_hop is None: - logging.error(self.mconn_id + "RCPT exception: could not retrieve milter-macro ({rcpt_host})") + logging.error(self.mconn_id + + "RCPT exception: could not retrieve milter-macro ({rcpt_host})" + ) else: - logging.debug(self.mconn_id + "/RCPT Next-Hop: {0}".format(self.next_hop)) + logging.debug(self.mconn_id + + "/RCPT Next-Hop: {0}".format(self.next_hop) + ) return Milter.CONTINUE # EOM is not optional and thus, always called by MTA @@ -163,11 +145,15 @@ class SOSMilter(Milter.Base): # and therefore not available until DATA command self.queue_id = self.getsymval('i') if self.queue_id is None: - logging.error(self.mconn_id + "EOM exception: could not retrieve milter-macro (i)!") + logging.error(self.mconn_id + + "EOM exception: could not retrieve milter-macro (i)!" + ) self.setreply('450','4.7.1', g_milter_tmpfail_message) return Milter.TEMPFAIL else: - logging.debug(self.mconn_id + "/EOM Queue-ID: {0}".format(self.queue_id)) + logging.debug(self.mconn_id + + "/EOM Queue-ID: {0}".format(self.queue_id) + ) if self.is_null_sender: logging.info(self.mconn_id + '/' + self.queue_id + @@ -175,10 +161,12 @@ class SOSMilter(Milter.Base): ) return Milter.CONTINUE if self.spf_record is not None: - logging.info(self.mconn_id + '/' + self.queue_id + "/EOM " + + logging.info(self.mconn_id + + '/' + self.queue_id + "/EOM " + "SPFv1: " + str(self.spf_record) ) - logging.debug(self.mconn_id + '/' + self.queue_id + "/EOM " + + logging.debug(self.mconn_id + + '/' + self.queue_id + "/EOM " + "next-hop=" + str(self.next_hop) ) if re.match(r'^".+-all"$', self.spf_record, re.IGNORECASE) is not None: @@ -194,21 +182,25 @@ class SOSMilter(Milter.Base): else: # Expected 'include' not found in SPF-record if self.next_hop in g_ignored_next_hops: - logging.info(self.mconn_id + '/' + self.queue_id + "/EOM " + + logging.info(self.mconn_id + + '/' + self.queue_id + "/EOM " + "Passing message due to ignored next-hop=" + self.next_hop ) return Milter.CONTINUE if self.is_env_from_domain_in_ldap and g_milter_mode != 'reject': - logging.info(self.mconn_id + '/' + self.queue_id + "/EOM " + + logging.info(self.mconn_id + + '/' + self.queue_id + "/EOM " + "5321_from_domain={0} (LDAP) has a broken SPF-record!".format(self.env_from_domain) ) try: self.addheader('X-SOS-Milter', 'failed SPF-expectation') - logging.debug(self.mconn_id + '/' + self.queue_id + "/EOM " + + logging.debug(self.mconn_id + '/' + + self.queue_id + "/EOM " + 'test-mode: X-SOS-Milter header was added. ' ) except: - logging.error(self.mconn_id + '/' + self.queue_id + "/EOM " + + logging.error(self.mconn_id + + '/' + self.queue_id + "/EOM " + "addheader() failed: " + traceback.format_exc() ) ex = str( @@ -224,7 +216,8 @@ class SOSMilter(Milter.Base): ) return Milter.REJECT else: - logging.debug(self.mconn_id + '/' + self.queue_id + "/EOM " + + logging.debug(self.mconn_id + + '/' + self.queue_id + "/EOM " + "No SPF-record found for {0}".format(self.env_from_domain) ) return Milter.CONTINUE @@ -239,6 +232,46 @@ class SOSMilter(Milter.Base): # Clean up any external resources here. logging.debug(self.mconn_id + "/CLOSE") return Milter.CONTINUE + + def get_spf_record(self, from_domain): + dns_response = None + try: + dns_response = dns.resolver.resolve(from_domain, 'TXT') + except dns.resolver.NoAnswer as e: + logging.warning(self.mconn_id + "/DNS " + e.msg) + # accept message if DNS-resolver fails + return None + except dns.resolver.NXDOMAIN as e: + logging.warning(self.mconn_id + + " /DNS " + e.msg + ) + # accept message if DNS-resolver fails + return None + except: + logging.error(self.mconn_id + + "/DNS Resolver-EXCEPTION: " + traceback.format_exc() + ) + # accept message if DNS-resolver fails + return None + for rdata in dns_response: + if re.match(r'^"v=spf1.*"$', rdata.to_text(), re.IGNORECASE): + # we´ve got a SPF match! + logging.debug(self.mconn_id + "/DNS SPFv1: {0}".format(rdata.to_text())) + # check if spf-record includes a redirect!? + m = re.match( + r'^.*redirect=(?P.+).*"', + rdata.to_text(), + re.IGNORECASE + ) + if m is not None: + # SPF redirect clause found + spf_redirect_domain = m.group('redirect_domain') + logging.info(self.mconn_id + + "/DNS SPF-redirect: {}".format(spf_redirect_domain) + ) + return self.get_spf_record(spf_redirect_domain) + else: + return rdata.to_text() if __name__ == "__main__": if 'LOG_LEVEL' in os.environ: @@ -301,7 +334,7 @@ if __name__ == "__main__": sys.exit(1) try: set_config_parameter("RESTARTABLE_SLEEPTIME", 2) - set_config_parameter("RESTARTABLE_TRIES", True) + set_config_parameter("RESTARTABLE_TRIES", 2) set_config_parameter('DEFAULT_SERVER_ENCODING', 'utf-8') set_config_parameter('DEFAULT_CLIENT_ENCODING', 'utf-8') server = Server(os.environ['LDAP_SERVER_URI'], get_info=NONE) diff --git a/app/sos-milter_eom.py b/app/sos-milter_eom.py deleted file mode 100644 index a2c3c2a..0000000 --- a/app/sos-milter_eom.py +++ /dev/null @@ -1,322 +0,0 @@ -import Milter -import sys -import traceback -import os -import logging -import string -import random -import re -import dns.resolver -from ldap3 import ( - Server, Connection, NONE, ALL, set_config_parameter, ALL_ATTRIBUTES, - ALL_OPERATIONAL_ATTRIBUTES, MODIFY_REPLACE, HASHED_NONE, HASHED_SALTED_SHA, -) -from ldap3.core.exceptions import LDAPException - -# Globals with mostly senseless defaults ;) -g_milter_name = 'sos-milter' -g_milter_socket = '/socket/' + g_milter_name -g_milter_reject_message = 'Security policy violation!' -g_milter_tmpfail_message = 'Service temporarily not available! Please try again later.' -g_re_domain = re.compile(r'^.*@(\S+)$', re.IGNORECASE) -g_re_spf_regex = re.compile(r'.*', re.IGNORECASE) -g_re_expected_txt_data = '' -g_loglevel = logging.INFO -g_milter_mode = 'test' -g_ignored_next_hops = {} -g_ldap_conn = None -g_ldap_binddn = '' -g_ldap_bindpw = '' - -class SOSMilter(Milter.Base): - # Each new connection is handled in an own thread - def __init__(self): - self.is_null_sender = False - self.env_from = None - self.env_from_domain = None - self.is_env_from_domain_in_ldap = False - self.spf_record = None - self.queue_id = None - self.next_hop = None - # https://stackoverflow.com/a/2257449 - self.mconn_id = g_milter_name + ': ' + ''.join( - random.choice(string.ascii_lowercase + string.digits) for _ in range(8) - ) - - # Not registered/used callbacks - @Milter.nocallback - def connect(self, IPname, family, hostaddr): - return Milter.CONTINUE - @Milter.nocallback - def hello(self, heloname): - return Milter.CONTINUE - @Milter.nocallback - def data(self): - return Milter.CONTINUE - @Milter.nocallback - def header(self, name, hval): - return Milter.CONTINUE - @Milter.nocallback - def eoh(self): - return Milter.CONTINUE - @Milter.nocallback - def body(self, chunk): - return Milter.CONTINUE - - def envfrom(self, mailfrom, *str): - try: - # DSNs/bounces are not relevant - if(mailfrom == '<>'): - self.is_null_sender = True - return Milter.CONTINUE - mailfrom = mailfrom.replace("<","") - mailfrom = mailfrom.replace(">","") - self.env_from = mailfrom - m = g_re_domain.match(self.env_from) - if m is None: - logging.error(self.mconn_id + "/FROM " + - "Could not determine domain of 5321.from=" + self.env_from - ) - self.is_null_sender = True - return Milter.CONTINUE - self.env_from_domain = m.group(1) - logging.debug(self.mconn_id + - "/FROM 5321_from_domain=" + self.env_from_domain - ) - # Check if env_from_domain is in ldap - if(g_ldap_conn is not None): - filter = os.environ['LDAP_QUERY_FILTER'] - filter = filter.replace("%d", self.env_from_domain) - try: - g_ldap_conn.search(os.environ['LDAP_SEARCH_BASE'], - filter, - attributes=[ALL_ATTRIBUTES] - ) - if len(g_ldap_conn.entries) != 0: - self.is_env_from_domain_in_ldap = True - logging.info(self.mconn_id + - "/FROM Domain {0} found in LDAP".format(self.env_from_domain) - ) - except LDAPException: - logging.error(self.mconn_id + "/FROM " + traceback.format_exc()) - - # Get TXT record of sender domain - dns_response = None - try: - dns_response = dns.resolver.resolve(self.env_from_domain, 'TXT') - except dns.resolver.NoAnswer as e: - logging.error(self.mconn_id + - " /FROM " + e.msg - ) - # accept message if DNS-resolver fails - return Milter.CONTINUE - except dns.resolver.NXDOMAIN as e: - logging.error(self.mconn_id + - " /FROM " + e.msg - ) - # accept message if DNS-resolver fails - return Milter.CONTINUE - except: - logging.error(self.mconn_id + " DNS-Resolver-EXCEPTION: " + traceback.format_exc()) - # accept message if DNS-resolver fails - return Milter.CONTINUE - for rdata in dns_response: - logging.debug(self.mconn_id + "/FROM DNS-TXT: {0}".format(rdata.to_text())) - if re.match(r'^"v=spf1.*"$', rdata.to_text(), re.IGNORECASE): - # we´ve got a SPF match! - self.spf_record = rdata.to_text() - # TODO: check if spf-record is a redirect!? - logging.debug(self.mconn_id + "/FROM Found SPFv1: {0}".format(self.spf_record)) - break - return Milter.CONTINUE - except: - logging.error(self.mconn_id + " FROM-EXCEPTION: " + traceback.format_exc()) - self.setreply('450','4.7.1', g_milter_tmpfail_message) - return Milter.TEMPFAIL - - def envrcpt(self, to, *str): - if self.is_null_sender == True: - return Milter.CONTINUE - self.next_hop = self.getsymval('{rcpt_host}') - if self.next_hop is None: - logging.error(self.mconn_id + "RCPT exception: could not retrieve milter-macro ({rcpt_host})") - else: - logging.debug(self.mconn_id + "/RCPT Next-Hop: {0}".format(self.next_hop)) - return Milter.CONTINUE - - # EOM is not optional and thus, always called by MTA - def eom(self): - # A queue-id will be generated after the first accepted RCPT TO - # and therefore not available until DATA command - self.queue_id = self.getsymval('i') - if self.queue_id is None: - logging.error(self.mconn_id + "EOM exception: could not retrieve milter-macro (i)!") - self.setreply('450','4.7.1', g_milter_tmpfail_message) - return Milter.TEMPFAIL - else: - logging.debug(self.mconn_id + "/EOM Queue-ID: {0}".format(self.queue_id)) - - if self.is_null_sender: - logging.info(self.mconn_id + '/' + self.queue_id + - "/EOM Skipping bounce/DSN message" - ) - return Milter.CONTINUE - if self.spf_record is not None: - logging.info(self.mconn_id + '/' + self.queue_id + "/EOM " + - "SPFv1: " + str(self.spf_record) - ) - logging.debug(self.mconn_id + '/' + self.queue_id + "/EOM " + - "next-hop=" + str(self.next_hop) - ) - if re.match(r'^".+-all"$', self.spf_record, re.IGNORECASE) is not None: - # SPF record is in restrictive mode - logging.debug(self.mconn_id + '/' + self.queue_id + "/EOM " + - "SPF-record is signaling a FAIL-policy (-all)" - ) - if g_re_spf_regex.match(self.spf_record) is not None: - logging.debug(self.mconn_id + '/' + self.queue_id + "/EOM" + - " SPF-record of 5321_from_domain=" + self.env_from_domain + - " permits us to relay this message" - ) - else: - # Expected 'include' not found in SPF-record - if self.next_hop in g_ignored_next_hops: - logging.info(self.mconn_id + '/' + self.queue_id + "/EOM " + - "Passing message due to ignored next-hop=" + self.next_hop - ) - return Milter.CONTINUE - if self.is_env_from_domain_in_ldap: - logging.info(self.mconn_id + '/' + self.queue_id + "/EOM " + - "5321_from_domain={0} (LDAP) has a broken SPF-record!".format(self.env_from_domain) - ) - ex = str( - "SPF-record (-all) of 5321_from_domain=" - + self.env_from_domain + " does not permit us to relay this message!" - ) - if g_milter_mode == 'test': - logging.info(self.mconn_id + '/' + self.queue_id + "/EOM " + ex) - try: - self.addheader( - 'X-SOS-Milter', - self.mconn_id + ' ' + self.env_from_domain + ': failed SPF-expectation' - ) - logging.debug(self.mconn_id + '/' + self.queue_id + "/EOM " + - 'test-mode: X-SOS-Milter header was added. ' - ) - except: - logging.error(self.mconn_id + '/' + self.queue_id + "/EOM " + - "addheader() failed: " + traceback.format_exc() - ) - else: - logging.warning(self.mconn_id + '/' + self.queue_id + "/EOM " + ex) - self.setreply('550','5.7.1', - self.mconn_id + ' ' + ex + ' ' + g_milter_reject_message - ) - return Milter.REJECT - else: - logging.debug(self.mconn_id + '/' + self.queue_id + "/EOM " + - "No SPF-record found for {0}".format(self.env_from_domain) - ) - return Milter.CONTINUE - - def abort(self): - # Client disconnected prematurely - return Milter.CONTINUE - - def close(self): - # Always called, even when abort is called. - # Clean up any external resources here. - return Milter.CONTINUE - -if __name__ == "__main__": - if 'LOG_LEVEL' in os.environ: - if re.match(r'^info$', os.environ['LOG_LEVEL'], re.IGNORECASE): - g_loglevel = logging.INFO - elif re.match(r'^warn|warning$', os.environ['LOG_LEVEL'], re.IGNORECASE): - g_loglevel = logging.WARN - elif re.match(r'^error$', os.environ['LOG_LEVEL'], re.IGNORECASE): - g_loglevel = logging.ERROR - elif re.match(r'debug', os.environ['LOG_LEVEL'], re.IGNORECASE): - g_loglevel = logging.DEBUG - logging.basicConfig( - filename=None, # log to stdout - format='%(asctime)s: %(levelname)s %(message)s', - level=g_loglevel - ) - 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'] - if 'MILTER_NAME' in os.environ: - g_milter_name = os.environ['MILTER_NAME'] - if 'MILTER_SOCKET' in os.environ: - g_milter_socket = os.environ['MILTER_SOCKET'] - if 'MILTER_REJECT_MESSAGE' in os.environ: - g_milter_reject_message = os.environ['MILTER_REJECT_MESSAGE'] - if 'MILTER_TMPFAIL_MESSAGE' in os.environ: - g_milter_tmpfail_message = os.environ['MILTER_TMPFAIL_MESSAGE'] - if 'SPF_REGEX' in os.environ: - try: - g_re_spf_regex = re.compile(os.environ['SPF_REGEX'], re.IGNORECASE) - except: - logging.error("ENV[SPF_REGEX] exception: " + traceback.format_exc()) - sys.exit(1) - if 'IGNORED_NEXT_HOPS' in os.environ: - try: - tmp_hops = os.environ['IGNORED_NEXT_HOPS'].split(',') - for next_hop in tmp_hops: - g_ignored_next_hops[next_hop] = 'ignore' - logging.debug("next-hops: " + str(g_ignored_next_hops)) - except: - logging.error("ENV[IGNORED_NEXT_HOPS] exception: " + traceback.format_exc()) - sys.exit(1) - if 'LDAP_ENABLED' in os.environ: - if 'LDAP_SERVER_URI' not in os.environ: - logging.error("ENV[LDAP_SERVER_URI] is mandatory!") - sys.exit(1) - if 'LDAP_BINDDN' not in os.environ: - logging.info("ENV[LDAP_BINDDN] not set! Continue...") - else: - g_ldap_binddn = os.environ['LDAP_BINDDN'] - if 'LDAP_BINDPW' not in os.environ: - logging.info("ENV[LDAP_BINDPW] not set! Continue...") - else: - g_ldap_bindpw = os.environ['LDAP_BINDPW'] - if 'LDAP_SEARCH_BASE' not in os.environ: - logging.error("ENV[LDAP_SEARCH_BASE] is mandatory!") - sys.exit(1) - if 'LDAP_QUERY_FILTER' not in os.environ: - logging.error("ENV[LDAP_QUERY_FILTER] is mandatory!") - sys.exit(1) - try: - set_config_parameter("RESTARTABLE_SLEEPTIME", 2) - set_config_parameter("RESTARTABLE_TRIES", True) - set_config_parameter('DEFAULT_SERVER_ENCODING', 'utf-8') - set_config_parameter('DEFAULT_CLIENT_ENCODING', 'utf-8') - server = Server(os.environ['LDAP_SERVER_URI'], get_info=NONE) - g_ldap_conn = Connection(server, - g_ldap_binddn, - g_ldap_bindpw, - auto_bind=True, - raise_exceptions=True, - client_strategy='RESTARTABLE' - ) - logging.info("LDAP-Connection steht. PID: " + str(os.getpid())) - except LDAPException as e: - print("LDAP-Exception: " + traceback.format_exc()) - sys.exit(1) - - try: - timeout = 600 - # Register to have the Milter factory create instances of your class: - Milter.factory = SOSMilter - # Tell the MTA which features we use - flags = Milter.ADDHDRS - Milter.set_flags(flags) - logging.info("Startup " + g_milter_name + - "@socket: " + g_milter_socket + - " in mode: " + g_milter_mode - ) - Milter.runmilter(g_milter_name,g_milter_socket,timeout,True) - logging.info("Shutdown " + g_milter_name) - except: - logging.error("MAIN-EXCEPTION: " + traceback.format_exc()) \ No newline at end of file diff --git a/docker-build.sh b/docker-build.sh deleted file mode 100755 index de3a25e..0000000 --- a/docker-build.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh - -BRANCH="$(/usr/bin/git branch|/bin/grep \*|/usr/bin/awk {'print $2'})" -VERSION="$(/bin/cat VERSION)" -BASEOS="$(/bin/cat BASEOS)" -GO="" - -while getopts g opt -do - case $opt in - g) GO="go";; - esac -done - -if [ -z "${GO}" ] ; then - echo "Building sos-milter@docker on '${BASEOS}' for version '${VERSION}' in branch '${BRANCH}'!" - echo "GO serious with '-g'!" - exit 1 -fi - -IMAGES="sos-milter" - -for IMAGE in ${IMAGES}; do - /usr/bin/docker build \ - --pull=true \ - -t "${IMAGE}/${BASEOS}:${VERSION}_${BRANCH}" \ - -f "docker/${BASEOS}/Dockerfile" . -done diff --git a/docker/debian/Dockerfile b/docker/debian/Dockerfile deleted file mode 100644 index afbd7c9..0000000 --- a/docker/debian/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -ARG http_proxy -ARG https_proxy -FROM debian -LABEL maintainer="Dominik Chilla " -LABEL git_repo="https://github.com/chillout2k/sos-milter" - -ENV DEBIAN_FRONTEND=noninteractive \ - TZ=Europe/Berlin - -RUN env; set -ex ; \ - apt-get -qq update \ - && apt-get -qq --no-install-recommends install \ - python3-pip python3-setuptools \ - libmilter1.0.1 libmilter-dev procps net-tools \ - gcc python3-dev \ - && /usr/bin/pip3 install pymilter \ - && /usr/bin/pip3 install dnspython \ - && /usr/bin/pip3 install ldap3 \ - && /bin/mkdir /config /socket /app \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -COPY app/*.py /app/ -COPY entrypoint.sh /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7ae64ee --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pymilter==1.0.5 +dnspython==2.3.0 +ldap3==2.9.1 \ No newline at end of file diff --git a/tests/miltertest.lua b/tests/miltertest.lua index b579a7b..7381b73 100644 --- a/tests/miltertest.lua +++ b/tests/miltertest.lua @@ -10,51 +10,17 @@ end mt.set_timeout(3) -- 5321.FROM + MACROS -mt.macro(conn, SMFIC_MAIL, '{client_addr}', "127.128.129.130", "i", "TestQueueId",'{rcpt_host}', "test.next-host") -if mt.mailfrom(conn, "dominik@dc-it-con.de") ~= nil then +mt.macro(conn, SMFIC_MAIL, '{client_addr}', "127.128.129.130") +if mt.mailfrom(conn, "postmaster@staging.zwackl.de") ~= nil then error "mt.mailfrom() failed" end if mt.getreply(conn) ~= SMFIR_CONTINUE then error "mt.mailfrom() unexpected reply" end --- 5321.RCPT -if mt.rcptto(conn, "info@dc-it-con.de") ~= nil then - error "mt.rcptto() failed" -end -if mt.getreply(conn) ~= SMFIR_CONTINUE then - error "mt.rcptto() unexpected reply" -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 - -if not mt.eom_check(conn, MT_HDRADD, "X-SOS-Milter") then - mt.echo("no header added") -else - mt.echo("X-SOS-Milter header added -> LDAP-Domain with broken SPF") -end - ---asdf --- 5321.FROM + MACROS -mt.macro(conn, SMFIC_MAIL, '{client_addr}', "127.128.129.130", "i", "TestQueueId",'{rcpt_host}', "test.next-hostx") -if mt.mailfrom(conn, "dominik@dc-it-con.de") ~= nil then - error "mt.mailfrom() failed" -end -if mt.getreply(conn) ~= SMFIR_CONTINUE then - error "mt.mailfrom() unexpected reply" -end - --- 5321.RCPT -if mt.rcptto(conn, "info@dc-it-con.de") ~= nil then +-- 5321.RCPT + MACROS +mt.macro(conn, SMFIC_RCPT, "i", "TestQueueId-1",'{rcpt_host}', "test.next-host") +if mt.rcptto(conn, "some@recipient.somewhere") ~= nil then error "mt.rcptto() failed" end if mt.getreply(conn) ~= SMFIR_CONTINUE then diff --git a/tests/miltertest_conn_reuse.lua b/tests/miltertest_conn_reuse.lua new file mode 100644 index 0000000..53c0f1f --- /dev/null +++ b/tests/miltertest_conn_reuse.lua @@ -0,0 +1,85 @@ +-- 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 + +mt.set_timeout(3) + +-- First message within smtp-session +-- 5321.FROM + MACROS +mt.macro(conn, SMFIC_MAIL, '{client_addr}', "127.128.129.130") +if mt.mailfrom(conn, "postmaster@staging.zwackl.de") ~= nil then + error "mt.mailfrom() failed" +end +if mt.getreply(conn) ~= SMFIR_CONTINUE then + error "mt.mailfrom() unexpected reply" +end + +-- 5321.RCPT + MACROS +mt.macro(conn, SMFIC_RCPT, "i", "TestQueueId-1",'{rcpt_host}', "test.next-host") +if mt.rcptto(conn, "some@recipient.somewhere") ~= nil then + error "mt.rcptto() failed" +end +if mt.getreply(conn) ~= SMFIR_CONTINUE then + error "mt.rcptto() unexpected reply" +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 + +if not mt.eom_check(conn, MT_HDRADD, "X-SOS-Milter") then + mt.echo("no header added") +else + mt.echo("X-SOS-Milter header added -> LDAP-Domain with broken SPF") +end + +-- Second message +-- 5321.FROM + MACROS +mt.macro(conn, SMFIC_MAIL, '{client_addr}', "127.128.129.130") +if mt.mailfrom(conn, "info@staging.zwackl.de") ~= nil then + error "mt.mailfrom() failed" +end +if mt.getreply(conn) ~= SMFIR_CONTINUE then + error "mt.mailfrom() unexpected reply" +end + +-- 5321.RCPT + MACROS +mt.macro(conn, SMFIC_RCPT, "i", "TestQueueId-2",'{rcpt_host}', "test.next-host") +if mt.rcptto(conn, "some@recipient.somewhere") ~= nil then + error "mt.rcptto() failed" +end +if mt.getreply(conn) ~= SMFIR_CONTINUE then + error "mt.rcptto() unexpected reply" +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 + +if not mt.eom_check(conn, MT_HDRADD, "X-SOS-Milter") then + mt.echo("no header added") +else + mt.echo("X-SOS-Milter header added -> LDAP-Domain with broken SPF") +end + +-- DISCONNECT +mt.disconnect(conn) \ No newline at end of file diff --git a/tests/miltertest_redirect.lua b/tests/miltertest_redirect.lua new file mode 100644 index 0000000..775d223 --- /dev/null +++ b/tests/miltertest_redirect.lua @@ -0,0 +1,49 @@ +-- 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 + +mt.set_timeout(3) + +-- First message within smtp-session +-- 5321.FROM + MACROS +mt.macro(conn, SMFIC_MAIL, '{client_addr}', "127.128.129.130") +if mt.mailfrom(conn, "blubb@gmx.de") ~= nil then + error "mt.mailfrom() failed" +end +if mt.getreply(conn) ~= SMFIR_CONTINUE then + error "mt.mailfrom() unexpected reply" +end + +-- 5321.RCPT + MACROS +mt.macro(conn, SMFIC_RCPT, "i", "TestQueueId-1",'{rcpt_host}', "test.next-hostx") +if mt.rcptto(conn, "some@recipient.somewhere") ~= nil then + error "mt.rcptto() failed" +end +if mt.getreply(conn) ~= SMFIR_CONTINUE then + error "mt.rcptto() unexpected reply" +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 + +if not mt.eom_check(conn, MT_HDRADD, "X-SOS-Milter") then + mt.echo("no header added") +else + mt.echo("X-SOS-Milter header added -> LDAP-Domain with broken SPF") +end + +-- DISCONNECT +mt.disconnect(conn) \ No newline at end of file