diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0bd40ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +venv/ + +*.pyc +__pycache__/ + +instance/ + +.pytest_cache/ +.coverage +htmlcov/ + +dist/ +build/ +*.egg-info/ + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cc67606 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.linting.pylintEnabled": true, + "python.linting.enabled": true +} \ No newline at end of file diff --git a/BASEOS b/BASEOS deleted file mode 100644 index 2dee175..0000000 --- a/BASEOS +++ /dev/null @@ -1 +0,0 @@ -debian diff --git a/Dockerfile b/Dockerfile deleted file mode 120000 index 894720b..0000000 --- a/Dockerfile +++ /dev/null @@ -1 +0,0 @@ -docker/debian/Dockerfile \ No newline at end of file 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/OCI/README.md b/OCI/README.md new file mode 100644 index 0000000..dc8a1b7 --- /dev/null +++ b/OCI/README.md @@ -0,0 +1,11 @@ +# OCI +**Note:** You need to be in the root path of the repo! + +Build local docker image: +``` +$ docker build -t sos-milter:local -f OCI/Dockerfile . +``` +Run it (with default ENV-values!): +``` +$ docker run -d sos-milter:local +``` 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/README.md b/README.md index c291642..75ffa24 100644 --- a/README.md +++ b/README.md @@ -18,15 +18,36 @@ services: image: "sos-milter/debian:19.06_master" restart: unless-stopped environment: + # default: info, possible: info, warning, error, debug LOG_LEVEL: debug # default: test, possible: test,reject MILTER_MODE: reject + # Default: sos-milter MILTER_NAME: sos-milter - #MILTER_SOCKET: inet6:8020 - #MILTER_REJECT_MESSAGE: Message rejected due to security policy violation! - #MILTER_TMPFAIL_MESSAGE: Message temporary rejected. Please try again later ;) + # Default socket /socket/${MILTER_NAME} + # MILTER_SOCKET: inet6:8020 + # MILTER_REJECT_MESSAGE: Message rejected due to security policy violation! + # MILTER_TMPFAIL_MESSAGE: Message temporary rejected. Please try again later ;) + + # Expected Content of the spf-record, like a specific include + # docker-compose pitfall: Dollar-sign ($) must be escaped as $$ SPF_REGEX: '^.*include:secure-mailgate\.com.*$$' + + # If next-hop relay is one of the following, message will be ignored IGNORED_NEXT_HOPS: 'some-mailrelay.xyz:123, another.relay, and.so.on:125' + + # Search for sender domain in LDAP. Can be used to mark (add header) + # and identify (log) internal sender domains with broken SPF-records + # for further processing (log report or header-based routing). + # After a message was marked with an additional header, it can be + # routed other than usual (e.g. through a bounce-/fwd-relay) + LDAP_ENABLED: 'some_value' + LDAP_SERVER_URI: 'ldaps://some.ldap.server' + LDAP_BINDDN: 'some-ldap-user-dn' + LDAP_BINDPW: 'some-secret-pw' + LDAP_SEARCH_BASE: 'ou=domains,dc=SLD,dc=TLD' + # %d will be replaced by recognized 5321.env_from_domain + LDAP_QUERY_FILTER: '(dc=%d)' hostname: sos-milter volumes: - "sosm_socket:/socket/:rw" 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/app/sos-milter.py b/app/sos-milter.py index 022dc3e..bd90a86 100644 --- a/app/sos-milter.py +++ b/app/sos-milter.py @@ -7,37 +7,45 @@ import string import random import re import dns.resolver -from timeit import default_timer as timer -import pprint +from ldap3 import ( + Server, Connection, NONE, set_config_parameter +) +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*@(\S+)$', re.IGNORECASE) +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.time_start = timer() - self.null_sender = False + self.reset() + + def reset(self): + self.client_ip = None + 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.add_header = False 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) ) + logging.debug(self.mconn_id + " RESET") + # Not registered/used callbacks @Milter.nocallback def connect(self, IPname, family, hostaddr): @@ -46,6 +54,9 @@ class SOSMilter(Milter.Base): 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 @@ -56,10 +67,22 @@ class SOSMilter(Milter.Base): return Milter.CONTINUE def envfrom(self, mailfrom, *str): + # Instance member values remain within reused SMTP-connections! + if self.client_ip is not None: + # Milter connection reused! + logging.debug(self.mconn_id + "/FROM connection reused!") + self.reset() + self.client_ip = self.getsymval('{client_addr}') + if self.client_ip is None: + logging.error(self.mconn_id + " FROM exception: could not retrieve milter-macro ({client_addr})!") + self.setreply('450','4.7.1', g_milter_tmpfail_message) + return Milter.TEMPFAIL + else: + logging.debug(self.mconn_id + "/FROM client_ip={0}".format(self.client_ip)) try: # DSNs/bounces are not relevant if(mailfrom == '<>'): - self.null_sender = True + self.is_null_sender = True return Milter.CONTINUE mailfrom = mailfrom.replace("<","") mailfrom = mailfrom.replace(">","") @@ -69,115 +92,186 @@ class SOSMilter(Milter.Base): logging.error(self.mconn_id + "/FROM " + "Could not determine domain of 5321.from=" + self.env_from ) - self.setreply('450','4.7.1', g_milter_tmpfail_message) - return Milter.TEMPFAIL + 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 + "/FROM 5321_from_domain=" + self.env_from_domain ) - # Get TXT record of sender domain - dns_response = None - try: - dns_response = dns.resolver.query(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: - if re.match(r'^"v=spf1.*"$', rdata.to_text(), re.IGNORECASE): - # we´ve got a SPF match! - self.spf_record = rdata.to_text() - break + # 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=[] + ) + 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-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 def envrcpt(self, to, *str): - if self.null_sender == True: + if self.is_null_sender == True: return Milter.CONTINUE - try: - self.next_hop = self.getsymval('{rcpt_host}') - except: - logging.error(self.mconn_id + "RCPT exception: " + traceback.format_exc()) - sys.exit(1) + 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 - def data(self): + # 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.null_sender: + 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 + - "/DATA Skipping bounce/DSN message" + "/EOM Skipping bounce/DSN message" ) return Milter.CONTINUE if self.spf_record is not None: - logging.info(self.mconn_id + '/' + self.queue_id + "/DATA " + - "SPFv1: " + self.spf_record + logging.info(self.mconn_id + + '/' + self.queue_id + "/EOM " + + "SPFv1: " + str(self.spf_record) ) - logging.debug(self.mconn_id + '/' + self.queue_id + "/DATA " + - "next-hop=" + self.next_hop + 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 + "/DATA" + - " SPF-record of 5321.from-domain=" + self.env_from_domain + + 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 + "/DATA " + + logging.info(self.mconn_id + + '/' + self.queue_id + "/EOM " + "Passing message due to ignored next-hop=" + self.next_hop ) return Milter.CONTINUE - ex = "Restrictive 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 + "/DATA " + ex) - logging.debug(self.mconn_id + '/' + self.queue_id + "/DATA " + - 'test-mode: X-SOS-Milter header will be added. ' + if self.is_env_from_domain_in_ldap and g_milter_mode != 'reject': + logging.info(self.mconn_id + + '/' + self.queue_id + "/EOM " + + "5321_from_domain={0} (LDAP) has a broken SPF-record!".format(self.env_from_domain) ) - self.add_header = True - else: - logging.error(self.mconn_id + '/' + self.queue_id + "/DATA " + ex) + try: + self.addheader('X-SOS-Milter', '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() + ) + ex = str( + "SPF-record (-all) of 5321_from_domain=" + + self.env_from_domain + " does not permit us to relay this message!" + ) + logging.info(self.mconn_id + '/' + self.queue_id + "/EOM " + + "mode=" + g_milter_mode + ' client=' + self.client_ip + ' ' + ex + ) + if g_milter_mode == 'reject': self.setreply('550','5.7.1', self.mconn_id + ' ' + ex + ' ' + g_milter_reject_message ) return Milter.REJECT - return Milter.CONTINUE - - def eom(self): - # EOM is not optional and thus, always called by MTA - if self.add_header == True: - self.addheader( - 'X-SOS-Milter', - self.mconn_id + ' ' + self.env_from_domain + ': failed SPF-expectation' + 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 + logging.debug(self.mconn_id + "/ABORT") return Milter.CONTINUE def close(self): # Always called, even when abort is called. # 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: @@ -216,10 +310,45 @@ if __name__ == "__main__": tmp_hops = os.environ['IGNORED_NEXT_HOPS'].split(',') for next_hop in tmp_hops: g_ignored_next_hops[next_hop] = 'ignore' - logging.error("next-hops: " + str(g_ignored_next_hops)) + 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", 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) + g_ldap_conn = Connection(server, + g_ldap_binddn, + g_ldap_bindpw, + auto_bind=True, + raise_exceptions=True, + client_strategy='RESTARTABLE' + ) + logging.info("LDAP-Connection established. PID: " + str(os.getpid())) + except LDAPException as e: + print("LDAP-Exception: " + traceback.format_exc()) + sys.exit(1) try: timeout = 600 @@ -235,4 +364,4 @@ if __name__ == "__main__": Milter.runmilter(g_milter_name,g_milter_socket,timeout,True) logging.info("Shutdown " + g_milter_name) except: - logging.error("MAIN-EXCEPTION: " + traceback.format_exc()) + 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 32a7298..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 netcat-traditional \ - && /usr/bin/pip3 install pymilter \ - && /usr/bin/pip3 install dnspython \ - && /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"] -CMD ["/usr/bin/python3", "/app/sos-milter.py"] 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/README.md b/tests/README.md new file mode 100644 index 0000000..865559c --- /dev/null +++ b/tests/README.md @@ -0,0 +1,23 @@ +# prepare testing env +``` +export TLD=de +export SLD=domain +export MILTER_MODE=reject +export MILTER_SOCKET=inet:12345 +export LOG_LEVEL=debug +export SPF_REGEX="^.*include:_spf\.blah\.blub.*$" +export LDAP_ENABLED=yepp +export LDAP_SERVER_URI="ldap://ldap-master-staging.int.${SLD}.${TLD}" +export LDAP_SEARCH_BASE="ou=domains,dc=${SLD},dc=${TLD}" +export LDAP_QUERY_FILTER='(dc=%d)' +export IGNORED_NEXT_HOPS=test.next-host +``` + +# start milter +`python3 app/sos-milter.py` + +# execute `miltertest` +First of all install the `miltertest` binary. Under debian based distros +it´s located in the `opendkim-tools` package. + +`miltertest -v -D socket=inet:12345@127.0.0.111 -s tests/miltertest.lua` \ No newline at end of file diff --git a/tests/miltertest.lua b/tests/miltertest.lua new file mode 100644 index 0000000..7381b73 --- /dev/null +++ b/tests/miltertest.lua @@ -0,0 +1,48 @@ +-- 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) + +-- 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 + +-- DISCONNECT +mt.disconnect(conn) \ No newline at end of file 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