This commit is contained in:
Dominik Chilla 2020-11-29 02:56:51 +01:00
parent 4abba15358
commit f69080e6cb
9 changed files with 459 additions and 0 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"python.pythonPath": "/home/dominik/src/git/ExOTA-Milter/venv/bin/python3"
}

281
app/exota-milter.py Normal file
View File

@ -0,0 +1,281 @@
import Milter
import sys
import traceback
import os
import logging
import string
import random
import re
import email.utils
import authres
import json
# Globals with mostly senseless defaults ;)
g_milter_name = 'exota-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_loglevel = logging.INFO
g_milter_dkim_enabled = False
g_milter_dkim_authservid = 'invalid'
g_milter_policy_source = 'file' # file, ldap, etc.
g_milter_policy_file = 'invalid'
g_milter_policy = {}
class ExOTAMilter(Milter.Base):
# Each new connection is handled in an own thread
def __init__(self):
self.reset()
def reset(self):
self.client_ip = None
self.queue_id = None
self.hdr_from = None
self.hdr_from_domain = None
self.hdr_tenant_id = None
self.tenant_id_valid = False
self.dkim_results = []
self.dkim_valid = False
# 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):
return Milter.CONTINUE
@Milter.nocallback
def hello(self, heloname):
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):
# 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('550','5.7.1', g_milter_tmpfail_message)
return Milter.REJECT
else:
logging.debug(self.mconn_id + "/FROM client_ip={0}".format(self.client_ip))
return Milter.CONTINUE
def envrcpt(self, to, *str):
logging.debug(self.mconn_id + "/RCPT 5321.rcpt={0}".format(to))
return Milter.CONTINUE
def header(self, name, hval):
logging.debug(self.mconn_id + "/" + str(self.queue_id) +
"/HEADER Header: {0}, Value: {1}".format(name, hval)
)
if(name == "From"):
hdr_5322_from = email.utils.parseaddr(hval)
self.hdr_from = hdr_5322_from[1].lower()
m = re.match(g_re_domain, self.hdr_from)
if m is None:
logging.error(self.mconn_id + "/" + str(self.queue_id) + "/HEADER " +
"Could not determine domain-part of 5322.from=" + self.hdr_from
)
self.setreply('450','4.7.1', g_milter_tmpfail_message)
return Milter.TEMPFAIL
self.hdr_from_domain = m.group(1)
logging.debug(self.mconn_id + "/" + str(self.queue_id) +
"/HEADER 5322.from: {0}, 5322.from_domain: {1}".format(self.hdr_from, self.hdr_from_domain)
)
elif(name == "X-MS-Exchange-CrossTenant-Id"):
self.hdr_tenant_id = hval.lower()
logging.debug(self.mconn_id + "/" + str(self.queue_id) +
"/HEADER Tenant-ID: {0}".format(self.hdr_tenant_id)
)
elif(name == "Authentication-Results"):
if g_milter_dkim_enabled == True:
ar = None
try:
ar = authres.AuthenticationResultsHeader.parse(
"{0}: {1}".format(name, hval)
)
if ar.authserv_id == g_milter_dkim_authservid:
for ar_result in ar.results:
if ar_result.method == 'dkim':
self.dkim_results.append({
"selector": str(ar_result.header_s),
"from_domain": str(ar_result.header_d),
"result": str(ar_result.result)
})
else:
logging.debug(self.mconn_id + "/" + str(self.queue_id) +
"/HEADER Ignoring authentication results of {0}".format(ar.authserv_id)
)
except:
logging.error(self.mconn_id + "/" + str(self.queue_id) +
"/HEADER AR-parse exception: " + traceback.format_exc()
)
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.hdr_from is None:
logging.error(self.mconn_id + "/" + self.queue_id +
"/EOM exception: could not determine 5322.from header!"
)
self.setreply('550','5.7.1', g_milter_tmpfail_message)
return Milter.REJECT
if self.hdr_from_domain not in g_milter_policy:
logging.error(self.mconn_id + "/" + str(self.queue_id) + "/EOM " +
"Could not find 5322.from_domain {0} in policy!".format(self.hdr_from_domain)
)
self.setreply('550','5.7.1', g_milter_reject_message)
return Milter.REJECT
if self.hdr_tenant_id is None:
logging.error(self.mconn_id + "/" + self.queue_id +
"/EOM exception: could not determine X-MS-Exchange-CrossTenant-Id"
)
self.setreply('550','5.7.1', g_milter_reject_message)
return Milter.REJECT
if self.hdr_tenant_id == g_milter_policy[self.hdr_from_domain]['tenant_id'].lower():
logging.info(self.mconn_id + "/" + self.queue_id +
"/EOM: 5322.from_domain={1} tenant_id={0} status=match".format(self.hdr_tenant_id, self.hdr_from_domain)
)
else:
logging.error(self.mconn_id + "/" + self.queue_id +
"/EOM: 5322.from_domain={1} tenant_id={0} status=no_match".format(self.hdr_tenant_id, self.hdr_from_domain)
)
self.setreply('550','5.7.1', g_milter_reject_message)
return Milter.REJECT
if g_milter_dkim_enabled == True and g_milter_policy[self.hdr_from_domain]['dkim'] == True:
logging.info(self.mconn_id + "/" + self.queue_id +
"/EOM: 5322.from_domain={0} dkim_auth=enabled".format(self.hdr_from_domain)
)
if len(self.dkim_results) > 0:
for dkim_result in self.dkim_results:
if dkim_result['from_domain'] == self.hdr_from_domain:
logging.debug(self.mconn_id + "/" + self.queue_id +
"/EOM: Found DKIM authentication result for {0}/{1}".format(
self.hdr_from_domain, dkim_result['selector']
)
)
if dkim_result['result'] == 'pass':
logging.info(self.mconn_id + "/" + self.queue_id +
"/EOM: 5322.from_domain={0} selector={1} result=pass".format(
self.hdr_from_domain, dkim_result['selector']
)
)
self.dkim_valid = True
continue
else:
logging.info(self.mconn_id + "/" + self.queue_id +
"/EOM: 5322.from_domain={0} selector={1} result=fail".format(
self.hdr_from_domain, dkim_result['selector']
)
)
else:
logging.info(self.mconn_id + "/" + self.queue_id +
"/EOM: No DKIM authentication results (AR headers) found!"
)
self.setreply('550','5.7.1', g_milter_reject_message)
return Milter.REJECT
if self.dkim_valid == False:
logging.info(self.mconn_id + "/" + self.queue_id +
"/EOM: DKIM authentication failed!"
)
self.setreply('550','5.7.1', g_milter_reject_message)
return Milter.REJECT
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
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_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 'MILTER_DKIM_ENABLED' in os.environ:
g_milter_dkim_enabled = True
logging.info("DKIM signature authorisation enabled")
if 'MILTER_DKIM_AUTHSERVID' in os.environ:
g_milter_dkim_authservid = os.environ['MILTER_DKIM_AUTHSERVID'].lower()
logging.info("DKIM AuthServID: " + g_milter_dkim_authservid)
else:
logging.error("ENV[MILTER_DKIM_AUTHSERVID] is mandatory!")
sys.exit(1)
if 'MILTER_POLICY_SOURCE' in os.environ:
g_milter_policy_source = os.environ['MILTER_POLICY_SOURCE']
if g_milter_policy_source == 'file':
if 'MILTER_POLICY_FILE' in os.environ:
g_milter_policy_file = os.environ['MILTER_POLICY_FILE']
try:
with open(g_milter_policy_file, 'r') as policy_file:
g_milter_policy = json.load(policy_file)
policy_file.close()
logging.info("Successfully slurped policy file: {0}".format(g_milter_policy_file))
except:
logging.error("Error reading policy file: " + traceback.format_exc())
sys.exit(1)
else:
logging.error("ENV[MILTER_POLICY_FILE] is mandatory!")
sys.exit(1)
try:
timeout = 600
# Register to have the Milter factory create instances of your class:
Milter.factory = ExOTAMilter
# Tell the MTA which features we use
flags = Milter.ADDHDRS
Milter.set_flags(flags)
logging.info("Startup " + g_milter_name +
"@socket: " + g_milter_socket
)
Milter.runmilter(g_milter_name,g_milter_socket,timeout,True)
logging.info("Shutdown " + g_milter_name)
except:
logging.error("MAIN-EXCEPTION: " + traceback.format_exc())

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
authres==1.2.0
pkg-resources==0.0.0
pymilter==1.0.4

64
samples/exo_validator.eml Normal file
View File

@ -0,0 +1,64 @@
Return-Path: <>
Received: from DEU01-FR2-obe.outbound.protection.outlook.com (mail-fr2deu01lp2173.outbound.protection.outlook.com [104.47.11.173])
(using TLSv1.2 with cipher ECDHE-ECDSA-AES256-GCM-SHA384 (256/256 bits))
(Client CN "mail.protection.outlook.com", Issuer "GlobalSign Organization Validation CA - SHA256 - G3" (verified OK))
by outbound.connector.blahblubb.de (Postfix) with ESMTPS id 4CjqCQ2WRCzGjg6
for <some.recipient@example.org>; Sat, 28 Nov 2020 12:34:26 +0100 (CET)
ARC-Seal: i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none;
b=RuGSfIN1OzQHDqrF0erLAHZ3fyhtmoE5Sllj+Qp6CtbcNUkkmdhR44b8capz/J1mBpyb13udY1mhkPZCK1Cmt+mpg9yFXgkv5BxY+dV9647Fq+MboUE60Psn84d4vXFvyrWDrFW1jWZi7/NdXhjLcCqTHpAzDaRfAOfGhG/VWYJAXnD/EBpCzPfd8hh9ZOONI2UN2HQfRnx0P3WXyeVSGilP4RGPdmcCZV5ZzpjlQoKUshjq293+ZltXaeKfF/LHGX0yScHhKO2f9O+qY3hnH0P+NGwFvhIky3IyszfxpANaJnz2Jpp0sK1W16rwGSTI2gl9bpJsj+wzKLGkJV75+Q==
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com;
s=arcselector9901;
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
bh=KWHUKEKZxeQyFbKe45TV1PJMC2XrVCMTFkdwSYWR6o0=;
b=go8dFv6srV3NnETxQxaANld1if9BOsIgrhjefC4WkRrrgwEjZSNnm9DyO+GC2ZZo60At5JHOVLjqN9kjz2pFdAG0qnFEj3Wx/6NnuTfBUk0n4s32RoFuhADu8BC+aOU9Ec909uu2QQ9ucEMiVSjuyQ3QpGS5DR0yCAZLZ12B61hmoMgkXJ9ah6rluUV4GeMGKTsUn16u6mrJycXp0OoD4n19JomPpQo5o8gouK3Zz4F7DxX4lshNJ+VCsOznqS+FI4rQ2LSyU8Y0AZa9clyCSN94AJa6K0TiDgQ/gLZEWsZ1tZkgPrdMlyqi58ONW/dNQ7lyrEFz6deB4YmsusJPbQ==
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none
action=none header.from=lalalulu.onmicrosoft.com; dkim=none (message not
signed); arc=none
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=lalalulu.onmicrosoft.com; s=selector1-lalalulu-onmicrosoft-com;
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
bh=KWHUKEKZxeQyFbKe45TV1PJMC2XrVCMTFkdwSYWR6o0=;
b=DYTLJtLFjvVrSZtZQagTwuEe5PQYqrNGi7hR5bkhO+GYUV4dcQZnDO4hAPzJkOWhz8JCVJ+/yt5K8L/exegk80g9m0GJjZzJBxMy0ZE/7wg8yqiHNE+iQqWhJLtwsD23kx2+09G5dBSDI1QVqFKkL0YKBWVffSuXi+tjM4/BztffZ7ok7XZdKCFfKzK3TLdiAWYTRIp1214zdnIE0CLBhnOIWC4gnML2fXsVZsWb/CMgaW0vBsZGI/yaSivaNFPZloSb0/sEnMFMEbv2GXt9mN913M0thwCi/+NLwzaW6TNlw2Vz7l4SGRVvciGaa4s2sFnJ0ANMD2u5qBbJ8j8Z0w==
Authentication-Results: blahblubb.de; dkim=none (message not signed)
header.d=none;blahblubb.de; dmarc=none action=none
header.from=lalalulu.onmicrosoft.com;
Received: from AM6P193CA0087.EURP193.PROD.OUTLOOK.COM (2603:10a6:209:88::28)
by BEXP281MB0216.DEUP281.PROD.OUTLOOK.COM (2603:10a6:b10:6::12) with
Microsoft SMTP Server (version=TLS1_2,
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.3632.6; Sat, 28 Nov
2020 11:34:25 +0000
Received: from BE0P281MB0257.DEUP281.PROD.OUTLOOK.COM
(2603:10a6:209:88:cafe::a2) by AM6P193CA0087.outlook.office365.com
(2603:10a6:209:88::28) with Microsoft SMTP Server (version=TLS1_2,
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.3611.20 via Frontend
Transport; Sat, 28 Nov 2020 11:34:24 +0000
From: O365ConnectorValidation@lalalulu.onmicrosoft.com
Date: Sat, 28 Nov 2020 11:34:24 +0000
Message-Id: <b6d9c673-d0f3-4538-bb4e-9e099fb9a388@substrate-int.office.com>
To: some.recipient@example.org
Subject: Test email for connector validation
MIME-Version: 1.0
Content-Type: text/plain; charset=us-ascii
X-MS-PublicTrafficType: Email
X-MS-Office365-Filtering-Correlation-Id: abcd1234-abcd-471a-1234-08d893918edd
X-MS-TrafficTypeDiagnostic: BEXP281MB0216:
X-Microsoft-Antispam-PRVS:
<BEXP281MB021624EF3E3FC35524889C2AB8F70@BEXP281MB0216.DEUP281.PROD.OUTLOOK.COM>
X-MS-Oob-TLC-OOBClassifiers: OLM:2733;
X-MS-Exchange-SenderADCheck: 1
X-Microsoft-Antispam: BCL:0;
X-Microsoft-Antispam-Message-Info:
P2dut4iALZ4EsHFmDE6p0OBg/Q4PvbmhUGI6BnGbHo/u7Vza6tyXE6BPK0VrJQ8WnCYXNx7lEKtiZs8nakJ9EghgxvFRNuYyRBJcGAdlN2TJAb2/7Wp5m7vzuGp1JJhES0RC/hypLDL8miRoP1xYl/pQHZVUGczSddujsZT6im0EgDJvAB0L1vzyKvZJ1QH3vTWDKMAgetlQHiPvCfzZmUgY92g1+sfF9UwGTRXDj8cd83H+TLI7GL8kZF1H219l+DLDiZ3u+qUdprwMn9XDEBljZpczY8BhiFdmnbyJ26ePVNa5JluRboz2Gfaa6GZE+ar8FyKtepxFOyNlI+hyL/vcWNwmnjL+pyYFVPPHnODjxu8JixWg00ThTUiZbclJ
X-Forefront-Antispam-Report:
CIP:255.255.255.255;CTRY:;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:BE0P281MB0257.DEUP281.PROD.OUTLOOK.COM;PTR:;CAT:NONE;SFS:(376002)(346002)(39830400003)(34036004)(366004)(396003)(136003)(31686004)(78352004)(6916009)(508600001)(5660300002)(42882007)(2906002)(8936002)(558084003)(31696002)(17440700003)(316002)(9686003)(85236043)(68406010)(8676002)(83380400001)(16130700016)(100380200003)(20230700015);DIR:OUT;SFP:1501;
X-OriginatorOrg: lalalulu.onmicrosoft.com
X-MS-Exchange-CrossTenant-OriginalArrivalTime: 28 Nov 2020 11:34:24.7460
(UTC)
X-MS-Exchange-CrossTenant-Network-Message-Id: abcd1234-abcd-471a-1234-08d893918edd
X-MS-Exchange-CrossTenant-AuthSource: AM6P193CA0087.EURP193.PROD.OUTLOOK.COM
X-MS-Exchange-CrossTenant-AuthAs: Internal
X-MS-Exchange-CrossTenant-Id: 1234abcd-18c5-45e8-88de-123456789abc
X-MS-Exchange-CrossTenant-FromEntityHeader: Internet
X-MS-Exchange-Transport-CrossTenantHeadersStamped: BEXP281MB0216
This test email message was sent from Office 365 to check that email can be delivered to you using your new or modified connector. No need to reply.

View File

@ -0,0 +1,2 @@
From: from2@example.com
From: from1@example.org

View File

@ -0,0 +1,16 @@
import sys
import email, email.header
from email.utils import getaddresses
f = open("../samples/exo_validator.eml", "r")
email = email.message_from_file(f)
from_hdr = email.get_all("From")
print("from_hdr: " + str(from_hdr))
if(len(from_hdr) > 1):
print("Multiple From-headers found!")
sys.exit(1)
elif(len(from_hdr) == 1):
print("Exactly one From-header found :)")
print(from_hdr)
from_addr = getaddresses(from_hdr)
print(str(from_addr[0][1]))

17
tests/README.md Normal file
View File

@ -0,0 +1,17 @@
# prepare testing env
```
export LOG_LEVEL=debug
export MILTER_SOCKET=/tmp/exota-milter
export MILTER_POLICY_FILE=tests/policy.json
export MILTER_DKIM_ENABLED=yepp
export MILTER_DKIM_AUTHSERVID=my-auth-serv-id
```
# start milter
`python3 app/exota-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=/tmp/exota-milter -s tests/miltertest.lua`

67
tests/miltertest.lua Normal file
View File

@ -0,0 +1,67 @@
-- 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", "i", "4CgSNs5Q9sz7SllQ")
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
error "mt.rcptto() failed"
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
error "mt.rcptto() unexpected reply"
end
-- HEADER
if mt.header(conn, "From", '"Blah Blubb" <O365ConnectorValidation@lalalulu.onmicrosoft.com>') ~= nil then
error "mt.header(From) failed"
end
if mt.header(conn, "X-MS-Exchange-CrossTenant-Id", "1234abcd-18c5-45e8-88de-123456789abc") ~= nil then
error "mt.header(Subject) failed"
end
if mt.header(conn, "Authentication-Results", "another-wrong-auth-serv-id;\n dkim=fail header.d=lalalulu.onmicrosoft.com header.s=selector1-lalalulu-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
error "mt.header(Subject) failed"
end
if mt.header(conn, "Authentication-Results", "wrong-auth-serv-id;\n dkim=pass header.d=lalalulu.onmicrosoft.com header.s=selector1-lalalulu-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
error "mt.header(Subject) failed"
end
if mt.header(conn, "Authentication-Results", "my-auth-serv-id;\n dkim=pass header.d=lalalulu.onmicrosoft.com header.s=selector1-lalalulu-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
error "mt.header(Subject) failed"
end
if mt.header(conn, "Authentication-Results", "some-validating-host;\n dkim=pass header.d=paypal.de header.s=pp-dkim1 header.b=PmTtUzer;\n dmarc=pass (policy=reject) header.from=paypal.de;\n spf=pass (some-validating-host: domain of service@paypal.de designates 173.0.84.226 as permitted sender) smtp.mailfrom=service@paypal.de") ~= nil then
error "mt.header(Subject) 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
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)

6
tests/policy.json Normal file
View File

@ -0,0 +1,6 @@
{
"lalalulu.onmicrosoft.com": {
"tenant_id": "1234abcd-18c5-45e8-88de-123456789abc",
"dkim": true
}
}