Client certificate CN matching; Docs

This commit is contained in:
Dominik Chilla 2020-11-30 00:24:25 +01:00
parent 0b4849a726
commit da1b91b7e3
6 changed files with 203 additions and 85 deletions

View File

@ -1,2 +1,83 @@
# ExOTA-Milter
Exchange Online Tenant Autorisation Milter (Mail-Filter)
# ExOTA-Milter - Exchange Online Tenant Authorisation Milter (Mail-Filter)
The **ExOTA-[Milter](https://en.wikipedia.org/wiki/Milter)** application is written in python3 and derives from **[sdgathman´s pymilter](https://github.com/sdgathman/pymilter)**.
# Abstract/problem/motivation
Fact is that more and more companies are migrating their Outlook/Exchange environments to the [Microsoft cloud](https://www.microsoft.com/microsoft-365).
The **ExOTA-Milter** is kind of an authentication helper which plays an essential as well as security-related role in the so called [*smarthost connector for Microsoft Exchange-Online*](https://docs.microsoft.com/de-de/exchange/mail-flow-best-practices/use-connectors-to-configure-mail-flow/set-up-connectors-to-route-mail) scenario. The milter tries to close the gap of missing [client SMTP-Auth capability](https://tools.ietf.org/html/rfc4954) of MS Exchange-Online when there´s a need to relay emails over a third party smarthost. According to this the MS Exchange-Online platform expects the smarthost to accept emails without any further authentication of users/tenants, which can lead to many misconfigured SMTP-relays!
So, the question is: *How can an Exchange-Online user/tenant be identified by a smarthost?*
# Identification possibilities provided by Microsoft
## Client-IP ranges specified by Microsoft
Microsoft provides an ACL as [JSON file (ID: 10)](https://endpoints.office.com/endpoints/worldwide?clientrequestid=b10c5ed1-bad1-445f-b386-b919946339a7), which looks like this.
```
[...]
{
"id": 10,
"serviceArea": "Exchange",
"serviceAreaDisplayName": "Exchange Online",
"urls": [
"*.mail.protection.outlook.com"
],
"ips": [
"40.92.0.0/15",
"40.107.0.0/16",
"52.100.0.0/14",
"104.47.0.0/17",
"2a01:111:f400::/48",
"2a01:111:f403::/48"
],
"tcpPorts": "25",
"expressRoute": true,
"category": "Allow",
"required": true
}
[...]
```
The problem of this IP based ACL is that many other Exchange-Online customers/tenants are sending from the same IP-ranges as well! **This means that nearly any Exchange-Online smarthost tends to be an open relay unless additional authentication mechanism on a higher layer than IP takes place! IP-ACLs are definitely not enough!**
## x509 client certificate presented by Exchange-Online
The Exchange-Online platform also *presents* a x509 client certificate to identitfy onself to the smarthost. Taking a closer look at the received header we´ll notice that the certificates common name (CN) *mail.protection.outlook.com* is not realy tenant specific. Although the certificate provides additional security regarding the identity of the client system, it does not provide identity regarding the tenant. **IMHO that´s stil not enough to permit relaying!**
```
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 some.secure.smarthost.example.com (Postfix) with ESMTPS id 4CjqCQ2WRCzGjg6
for <blah.blubb@example.com>; Sat, 28 Nov 2020 12:34:26 +0100 (CET)
```
Client certificate verification is the job of the underlying MTA. So the **ExOTA-Milter** does not validate the client certificate itself, but it can be enabled (disabled per default) to match for the *expected* client CN. The emailserver [Postfix](http://postfix.org), for example, only provides the client CN to the milter API if the client certificate was successfully validated. Otherwise the **ExOTA-Milter** will not *see* the client CN over the milter protocol ([postfix milter macro](http://www.postfix.org/MILTER_README.html) `{cert_subject}`) which results in a milter reject action.
## DKIM - DomainKey Identified Message
Nevertheless, as [Microsoft supports DKIM-signing for outbound email traffic](https://docs.microsoft.com/de-de/microsoft-365/security/office-365-security/use-dkim-to-validate-outbound-email?view=o365-worldwide) the **ExOTA-Milter** can be used to authenticate sending tenants, respectively their sender domains, based on the cryptographic capabilities of [DKIM](https://tools.ietf.org/html/rfc6376). In fact the **ExOTA-Milter** does not validate the DKIM-signatures itself. Instead it simply parses DKIM-specific *Authentication-Results* headers produced by any previously DKIM-validating milter (like [OpenDKIM](http://www.opendkim.org/), [Rspamd](https://rspamd.com/) or [AMavis](https://www.ijs.si/software/amavisd/)) in the chain. I personally prefer OpenDKIM as it´s lightweight and fully focused on DKIM.
*DKIM-Signature* headers appended by the Exchange-Online platform look like this:
```
[...]
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
d=tenantdomain.onmicrosoft.com; s=selector1-tenantdomain-onmicrosoft-com;
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
bh=KWHUKEKZxeQyFbKe45TV1PJMC2XrVCMTFkdwSYWR6o0=;
b=DYTLJtLFjvVrSZtZQagTwuEe5PQYqrNGi7hR5bkhO[...snip...]
[...]
```
*Authentication-Results* headers provided by OpenDKIM (signature validated) look like this:
```
[...]
Authentication-Results: trusted.dkim.validating.relay; dkim=pass header.d=tenantdomain.onmicrosoft.com header.s=selector1-tenantdomain-onmicrosoft-com header.b=mmmjFpv8"
[...]
```
## Microsoft tenant-ID
Further each Microsoft Exchange-Online tenant has a unique tenant-ID in form of a UUID ([RFC 4122](https://tools.ietf.org/html/rfc4122)). **ExOTA-Milter** determines the tenant-ID from the *X-MS-Exchange-CrossTenant-Id* email header and uses it as an *mandatory* authentication factor.
# The solution
The answer to the question "*How can an Exchange-Online user/tenant be identified by a smarthost?*" can be answered as follows.
Finally it´s the combination of all of the above discussed aspects which result in a robust-enough smarthost setup for the Exchange-Online platform:
* restriction of client IPs via ACL (MTA)
* verification of Microsoft´s x509 client certificate + CN (MTA + ExOTA-Milter)
* consideration of DKIM verification results per sender domain (ExOTA-Milter)
* matching of tenant-id provided in email header (ExOTA-Milter)

View File

@ -22,30 +22,32 @@ g_milter_tmpfail_message = 'Service temporarily not available! Please try again
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_trusted_authservid = 'invalid'
g_milter_policy_source = 'file'
g_milter_policy_file = None
g_milter_policy_backend = None
g_milter_x509_enabled = False
g_milter_x509_trusted_cn = 'mail.protection.outlook.com'
class ExOTAMilter(Milter.Base):
# Each new connection is handled in an own thread
def __init__(self):
self.reset()
self.reset_milter()
def reset(self):
def reset_milter(self):
self.client_ip = None
self.queue_id = None
self.hdr_from = None
self.hdr_from_domain = None
self.hdr_tenant_id = None
self.hdr_tenant_id_count = 0
self.dkim_results = []
self.dkim_valid = False
self.x509_client_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")
logging.debug(self.mconn_id + " reset_milter()")
# Not registered/used callbacks
@Milter.nocallback
@ -61,12 +63,13 @@ class ExOTAMilter(Milter.Base):
def body(self, chunk):
return Milter.CONTINUE
# Mandatory callback
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.reset_milter()
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})!")
@ -74,15 +77,16 @@ class ExOTAMilter(Milter.Base):
return Milter.REJECT
else:
logging.debug(self.mconn_id + "/FROM client_ip={0}".format(self.client_ip))
return Milter.CONTINUE
return Milter.CONTINUE
# Mandatory callback
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)
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Header: {0}, Value: {1}".format(name, hval)
)
# Parse RFC-5322-From header
@ -91,22 +95,24 @@ class ExOTAMilter(Milter.Base):
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 " +
logging.error(self.mconn_id + "/" + str(self.getsymval('i')) + "/HDR " +
"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)
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: 5322.from={0}, 5322.from_domain={1}".format(
self.hdr_from, self.hdr_from_domain
)
)
# Parse non-standardized X-MS-Exchange-CrossTenant-Id header
elif(name == "X-MS-Exchange-CrossTenant-Id"):
self.hdr_tenant_id_count += 1
self.hdr_tenant_id = hval.lower()
logging.debug(self.mconn_id + "/" + str(self.queue_id) +
"/HEADER Tenant-ID: {0}".format(self.hdr_tenant_id)
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Tenant-ID: {0}".format(self.hdr_tenant_id)
)
# Parse RFC-7601 Authentication-Results header
@ -117,7 +123,7 @@ class ExOTAMilter(Milter.Base):
ar = authres.AuthenticationResultsHeader.parse(
"{0}: {1}".format(name, hval)
)
if ar.authserv_id == g_milter_dkim_authservid:
if ar.authserv_id == g_milter_trusted_authservid:
for ar_result in ar.results:
if ar_result.method == 'dkim':
self.dkim_results.append({
@ -126,30 +132,44 @@ class ExOTAMilter(Milter.Base):
"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)
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: 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()
logging.error(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: AR-parse exception: " + traceback.format_exc()
)
return Milter.CONTINUE
# EOM is not optional and thus always called by MTA
# EOM is mandatory as well 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))
# Here in EoM the final policy logic happens.
# Check if client certificate CN matches trusted CN
if g_milter_x509_enabled:
cert_subject = self.getsymval('{cert_subject}')
if cert_subject is None:
logging.info(self.mconn_id + "/" + self.getsymval('i')
+ "/EOM: No trusted x509 client CN found - action=reject"
)
self.setreply('550','5.7.1', g_milter_tmpfail_message)
return Milter.REJECT
else:
if g_milter_x509_trusted_cn.lower() == cert_subject.lower():
self.x509_client_valid = True
logging.info(self.mconn_id + "/" + self.getsymval('i') +
"/EOM: Trusted x509 client CN {0}".format(cert_subject)
)
else:
logging.info(self.mconn_id + "/" + self.getsymval('i') +
"/EOM Untrusted x509 client CN {0} - action=reject".format(cert_subject)
)
self.setreply('550','5.7.1', g_milter_tmpfail_message)
return Milter.REJECT
if self.hdr_from is None:
logging.error(self.mconn_id + "/" + self.queue_id +
"/EOM exception: could not determine 5322.from header!"
logging.error(self.mconn_id + "/" + self.getsymval('i') +
"/EOM exception: could not determine 5322.from header - action=reject"
)
self.setreply('550','5.7.1', g_milter_tmpfail_message)
return Milter.REJECT
@ -158,84 +178,80 @@ class ExOTAMilter(Milter.Base):
policy = None
try:
policy = g_milter_policy_backend.get(self.hdr_from_domain)
logging.debug(self.mconn_id + "/" + self.queue_id +
logging.debug(self.mconn_id + "/" + self.getsymval('i') +
"/EOM Policy for 5322.from_domain={0} fetched from backend".format(self.hdr_from_domain)
)
except (ExOTAPolicyException, ExOTAPolicyNotFoundException) as e:
logging.info(self.mconn_id + "/" + self.queue_id +
logging.info(self.mconn_id + "/" + self.getsymval('i') +
"/EOM {0}".format(e.message)
)
self.setreply('550','5.7.1', g_milter_tmpfail_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"
logging.error(self.mconn_id + "/" + self.getsymval('i') +
"/EOM exception: could not determine X-MS-Exchange-CrossTenant-Id - action=reject"
)
self.setreply('550','5.7.1', g_milter_reject_message)
return Milter.REJECT
if self.hdr_tenant_id_count > 1:
logging.info(self.mconn_id + "/" + self.queue_id +
"/EOM: More than one tenant-IDs for {0} found!".format(self.hdr_from_domain)
logging.info(self.mconn_id + "/" + self.getsymval('i') +
"/EOM: More than one tenant-IDs for {0} found - action=reject".format(
self.hdr_from_domain
)
)
self.setreply('550','5.7.1', g_milter_reject_message)
return Milter.REJECT
if self.hdr_tenant_id == policy.get_tenant_id():
logging.info(self.mconn_id + "/" + self.queue_id +
"/EOM: 5322.from_domain={0} tenant_id={1} status=match".format(
self.hdr_from_domain, self.hdr_tenant_id
)
logging.info(self.mconn_id + "/" + self.getsymval('i') +
"/EOM: tenant_id={0} status=match".format(self.hdr_tenant_id)
)
else:
logging.error(self.mconn_id + "/" + self.queue_id +
"/EOM: 5322.from_domain={0} tenant_id={1} status=no_match".format(
self.hdr_from_domain, self.hdr_tenant_id
logging.info(self.mconn_id + "/" + self.getsymval('i') +
"/EOM: tenant_id={0} status=no_match - action=reject".format(
self.hdr_tenant_id
)
)
self.setreply('550','5.7.1', g_milter_reject_message)
return Milter.REJECT
if g_milter_dkim_enabled and policy.is_dkim_enabled():
logging.debug(self.mconn_id + "/" + self.queue_id +
logging.debug(self.mconn_id + "/" + self.getsymval('i') +
"/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 +
logging.debug(self.mconn_id + "/" + self.getsymval('i') +
"/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} dkim_selector={1} result=pass".format(
self.hdr_from_domain, dkim_result['selector']
)
logging.info(self.mconn_id + "/" + self.getsymval('i') +
"/EOM: dkim_selector={0} result=pass".format(dkim_result['selector'])
)
self.dkim_valid = True
continue
else:
logging.info(self.mconn_id + "/" + self.queue_id +
"/EOM: 5322.from_domain={0} dkim_selector={1} result=fail".format(
self.hdr_from_domain, dkim_result['selector']
)
logging.info(self.mconn_id + "/" + self.getsymval('i') +
"/EOM: dkim_selector={0} result=fail".format(dkim_result['selector'])
)
else:
logging.info(self.mconn_id + "/" + self.queue_id +
"/EOM: No DKIM authentication results (AR headers) found!"
logging.info(self.mconn_id + "/" + self.getsymval('i') +
"/EOM: No DKIM authentication results (AR headers) found - action=reject"
)
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!"
logging.info(self.mconn_id + "/" + self.getsymval('i') +
"/EOM: DKIM authentication failed - action=reject"
)
self.setreply('550','5.7.1', g_milter_reject_message)
return Milter.REJECT
logging.info(self.mconn_id + "/" + self.queue_id +
"/EOM: Authentication successful (dkim_enabled={0})".format(
logging.info(self.mconn_id + "/" + self.getsymval('i') +
"/EOM: Tenant authentication successful (dkim_enabled={0})".format(
str(policy.is_dkim_enabled())
)
)
@ -278,12 +294,24 @@ if __name__ == "__main__":
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)
if 'MILTER_TRUSTED_AUTHSERVID' in os.environ:
g_milter_trusted_authservid = os.environ['MILTER_TRUSTED_AUTHSERVID'].lower()
logging.info("Trusted AuthServID: " + g_milter_trusted_authservid)
else:
logging.error("ENV[MILTER_DKIM_AUTHSERVID] is mandatory!")
logging.error("ENV[MILTER_TRUSTED_AUTHSERVID] is mandatory!")
sys.exit(1)
if 'MILTER_X509_ENABLED' in os.environ:
g_milter_x509_enabled = True
logging.info("x509 client certificate CN validation enabled")
if 'MILTER_X509_TRUSTED_CN' in os.environ:
g_milter_x509_trusted_cn = os.environ['MILTER_X509_TRUSTED_CN']
logging.info("Trusted x509 client CN: '{0}'".format(
g_milter_x509_trusted_cn
))
else:
logging.info("ENV[MILTER_X509_TRUSTED_CN]: using default '{0}'".format(
g_milter_x509_trusted_cn
))
if 'MILTER_POLICY_SOURCE' in os.environ:
g_milter_policy_source = os.environ['MILTER_POLICY_SOURCE']
if g_milter_policy_source == 'file':
@ -298,17 +326,21 @@ if __name__ == "__main__":
else:
logging.error("ENV[MILTER_POLICY_FILE] is mandatory!")
sys.exit(1)
elif g_milter_policy_source == 'ldap':
logging.debug("LDAP-Backend not supported yet!")
sys.exit(1)
else:
logging.debug("Unsupported backend: {0}!".format(g_milter_policy_source))
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())
logging.error("MAIN-EXCEPTION: " + traceback.format_exc())
sys.exit(1)

View File

@ -15,13 +15,13 @@ class ExOTAPolicyInvalidException(ExOTAPolicyException):
class ExOTAPolicy():
def __init__(self, policy_dict):
self.tenant_id = policy_dict['tenant_id']
self.dkim = policy_dict['dkim']
self.dkim_enabled = policy_dict['dkim_enabled']
def get_tenant_id(self):
return self.tenant_id
def is_dkim_enabled(self):
return self.dkim
return self.dkim_enabled
@staticmethod
def check_policy(policy_dict):
@ -38,14 +38,14 @@ class ExOTAPolicy():
raise ExOTAPolicyInvalidException(
"'tenant_id' must not contain whitespace characters!"
)
if 'dkim' not in policy_dict:
if 'dkim_enabled' not in policy_dict:
raise ExOTAPolicyInvalidException(
"Policy must have a 'dkim' attribute!"
"Policy must have a 'dkim_enabled' attribute!"
)
else:
if not isinstance(policy_dict['dkim'], bool):
if not isinstance(policy_dict['dkim_enabled'], bool):
raise ExOTAPolicyInvalidException(
"'dkim'({0}) must be boolean!".format(policy_dict['dkim'])
"'dkim_enabled'({0}) must be boolean!".format(policy_dict['dkim_enabled'])
)
class ExOTAPolicyBackend():

View File

@ -4,7 +4,9 @@ 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
export MILTER_TRUSTED_AUTHSERVID=my-auth-serv-id
export MILTER_X509_ENABLED=yepp
export MILTER_X509_TRUSTED_CN=mail.protection.outlook.com
```
# start milter

View File

@ -10,8 +10,8 @@ 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
mt.macro(conn, SMFIC_MAIL, '{client_addr}', "127.128.129.130", "i", "4CgSNs5Q9sz7SllQ", '{cert_subject}', "mail.protection.outlook.com")
if mt.mailfrom(conn, "envelope.sender@example.org") ~= nil then
error "mt.mailfrom() failed"
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
@ -19,7 +19,7 @@ if mt.getreply(conn) ~= SMFIR_CONTINUE then
end
-- 5321.RCPT
if mt.rcptto(conn, "<info@dc-it-con.de>") ~= nil then
if mt.rcptto(conn, "<envelope.recipient@example.com>") ~= nil then
error "mt.rcptto() failed"
end
if mt.getreply(conn) ~= SMFIR_CONTINUE then
@ -45,6 +45,9 @@ 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", "my-auth-serv-id;\n dkim=fail header.d=lalalulu.onmicrosoft.com header.s=selector2-asdf 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

View File

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