mirror of
https://github.com/chillout2k/ExOTA-Milter.git
synced 2025-12-11 01:10:18 +00:00
Processing of forwarded messages
This commit is contained in:
parent
d01c98333e
commit
8326967af3
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -1,3 +1,3 @@
|
||||
{
|
||||
"python.pythonPath": "/home/dominik/src/git/ExOTA-Milter/venv/bin/python3"
|
||||
"python.pythonPath": "/home/dominik/src/github/ExOTA-Milter/venv/bin/python3"
|
||||
}
|
||||
@ -47,6 +47,7 @@ services:
|
||||
MILTER_TRUSTED_AUTHSERVID: 'my-auth-serv-id'
|
||||
MILTER_X509_ENABLED: 'some_value'
|
||||
MILTER_X509_TRUSTED_CN: 'mail.protection.outlook.com'
|
||||
MILTER_X509_IP_WHITELIST='127.0.0.1,::1'
|
||||
MILTER_ADD_HEADER: 'some_value'
|
||||
MILTER_AUTHSERVID: 'my-auth-serv-id'
|
||||
volumes:
|
||||
|
||||
@ -8,7 +8,7 @@ start
|
||||
note left: From, Authentication-Results, X-MS-Exchange-CrossTenant-Id
|
||||
|
||||
:HDR: Recognising sender domain;
|
||||
note left: Taken from RFC5322 From-header. RFC5321.mail (envelope) is NOT relevant!
|
||||
note left: Taken from RFC5322.From header and/or RFC5322.Resent-From header. RFC5321.mail (envelope) is NOT relevant!
|
||||
|
||||
:EOM: Looking up policy in backend;
|
||||
note left: Based on RFC5322.from domain
|
||||
|
||||
@ -37,6 +37,8 @@ g_milter_policy_file = '/data/policy.json'
|
||||
g_milter_x509_enabled = False
|
||||
# ENV[MILTER_X509_TRUSTED_CN]
|
||||
g_milter_x509_trusted_cn = 'mail.protection.outlook.com'
|
||||
# ENV[MILTER_X509_IP_WHITELIST]
|
||||
g_milter_x509_ip_whitelist = ['127.0.0.1','::1']
|
||||
# ENV[MILTER_ADD_HEADER]
|
||||
g_milter_add_header = False
|
||||
# ENV[MILTER_AUTHSERVID]
|
||||
@ -57,9 +59,15 @@ class ExOTAMilter(Milter.Base):
|
||||
self.conn_reused = False
|
||||
self.hdr_from = None
|
||||
self.hdr_from_domain = None
|
||||
self.hdr_resent_from = None
|
||||
self.hdr_resent_from_domain = None
|
||||
self.forwarded = False
|
||||
self.hdr_tenant_id = None
|
||||
self.hdr_tenant_id_count = 0
|
||||
self.x509_whitelisted = False
|
||||
self.dkim_valid = False
|
||||
self.passed_dkim_results = []
|
||||
self.dkim_aligned = False
|
||||
self.xar_hdr_count = 0
|
||||
# https://stackoverflow.com/a/2257449
|
||||
self.mconn_id = g_milter_name + ': ' + ''.join(
|
||||
@ -76,7 +84,7 @@ class ExOTAMilter(Milter.Base):
|
||||
if 'reason' in kwargs:
|
||||
message = "{0} - reason: {1}".format(message, kwargs['reason'])
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
": milter_action=reject"
|
||||
": milter_action=reject message={0}".format(message)
|
||||
)
|
||||
self.setreply('550','5.7.1', message)
|
||||
return Milter.REJECT
|
||||
@ -138,6 +146,23 @@ class ExOTAMilter(Milter.Base):
|
||||
self.hdr_from, self.hdr_from_domain
|
||||
)
|
||||
)
|
||||
|
||||
# Parse RFC-5322-Resent-From header (Forwarded)
|
||||
if(name.lower() == "Resent-From".lower()):
|
||||
hdr_5322_resent_from = email.utils.parseaddr(hval)
|
||||
self.hdr_resent_from = hdr_5322_resent_from[1].lower()
|
||||
m = re.match(g_re_domain, self.hdr_resent_from)
|
||||
if m is None:
|
||||
logging.error(self.mconn_id + "/" + str(self.getsymval('i')) + "/HDR " +
|
||||
"Could not determine domain-part of 5322.resent_from=" + self.hdr_resent_from
|
||||
)
|
||||
else:
|
||||
self.hdr_resent_from_domain = m.group(1).lower()
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/HDR: 5322.resentfrom={0}, 5322.resent_from_domain={1}".format(
|
||||
self.hdr_resent_from, self.hdr_resent_from_domain
|
||||
)
|
||||
)
|
||||
|
||||
# Parse non-standardized X-MS-Exchange-CrossTenant-Id header
|
||||
elif(name.lower() == "X-MS-Exchange-CrossTenant-Id".lower()):
|
||||
@ -159,6 +184,12 @@ class ExOTAMilter(Milter.Base):
|
||||
for ar_result in ar.results:
|
||||
if ar_result.method == 'dkim':
|
||||
if ar_result.result == 'pass':
|
||||
self.passed_dkim_results.append({
|
||||
"sdid": ar_result.header_d
|
||||
})
|
||||
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/HDR: DKIM passed SDID {0}".format(ar_result.header_d)
|
||||
)
|
||||
self.dkim_valid = True
|
||||
else:
|
||||
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
@ -183,33 +214,42 @@ class ExOTAMilter(Milter.Base):
|
||||
|
||||
# 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 + "/" + str(self.getsymval('i'))
|
||||
+ "/EOM: No trusted x509 client CN found - action=reject"
|
||||
)
|
||||
return self.smfir_reject(
|
||||
queue_id = self.getsymval('i'),
|
||||
reason = 'No trusted x509 client CN found'
|
||||
)
|
||||
else:
|
||||
if g_milter_x509_trusted_cn.lower() == cert_subject.lower():
|
||||
self.x509_client_valid = True
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM: Trusted x509 client CN {0}".format(cert_subject)
|
||||
for whitelisted_client_ip in g_milter_x509_ip_whitelist:
|
||||
if self.client_ip == whitelisted_client_ip:
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i'))
|
||||
+ "/EOM: x509 CN check: client-IP '{0}' is whitelisted".format(
|
||||
whitelisted_client_ip
|
||||
)
|
||||
)
|
||||
else:
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM Untrusted x509 client CN {0} - action=reject".format(cert_subject)
|
||||
self.x509_whitelisted = True
|
||||
if not self.x509_whitelisted:
|
||||
cert_subject = self.getsymval('{cert_subject}')
|
||||
if cert_subject is None:
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i'))
|
||||
+ "/EOM: No trusted x509 client CN found - action=reject"
|
||||
)
|
||||
return self.smfir_reject(
|
||||
queue_id = self.getsymval('i'),
|
||||
reason = "Untrusted x509 client CN: {0}".format(cert_subject)
|
||||
reason = 'No trusted x509 client CN found'
|
||||
)
|
||||
else:
|
||||
if g_milter_x509_trusted_cn.lower() == cert_subject.lower():
|
||||
self.x509_client_valid = True
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM: Trusted x509 client CN {0}".format(cert_subject)
|
||||
)
|
||||
else:
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM: Untrusted x509 client CN {0} - action=reject".format(cert_subject)
|
||||
)
|
||||
return self.smfir_reject(
|
||||
queue_id = self.getsymval('i'),
|
||||
reason = "Untrusted x509 client CN: {0}".format(cert_subject)
|
||||
)
|
||||
|
||||
if self.hdr_from is None:
|
||||
logging.error(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM exception: could not determine 5322.from header - action=reject"
|
||||
"/EOM: exception: could not determine 5322.from header - action=reject"
|
||||
)
|
||||
return self.smfir_reject(
|
||||
queue_id = self.getsymval('i'),
|
||||
@ -221,20 +261,48 @@ class ExOTAMilter(Milter.Base):
|
||||
try:
|
||||
policy = g_policy_backend.get(self.hdr_from_domain)
|
||||
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM Policy for 5322.from_domain={0} fetched from backend".format(self.hdr_from_domain)
|
||||
"/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 + "/" + str(self.getsymval('i')) +
|
||||
"/EOM {0}".format(e.message)
|
||||
)
|
||||
return self.smfir_reject(
|
||||
queue_id = self.getsymval('i'),
|
||||
reason = "No policy for {0}".format(self.hdr_from_domain)
|
||||
"/EOM: 5322.from: {0}".format(e.message)
|
||||
)
|
||||
# Forwarded message? Maybe the Resent-From header domain matches.
|
||||
if self.hdr_resent_from_domain is not None:
|
||||
try:
|
||||
policy = g_policy_backend.get(self.hdr_resent_from_domain)
|
||||
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM: Policy for 5322.resent_from_domain={0} fetched from backend".format(
|
||||
self.hdr_resent_from_domain
|
||||
)
|
||||
)
|
||||
self.forwarded = True
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM: Forwarded message -> Policy for 5322.resent_from_domain={0} found.".format(
|
||||
self.hdr_resent_from_domain
|
||||
)
|
||||
)
|
||||
except (ExOTAPolicyException, ExOTAPolicyNotFoundException) as e:
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM: 5322.resent-from: {0}".format(e.message)
|
||||
)
|
||||
return self.smfir_reject(
|
||||
queue_id = self.getsymval('i'),
|
||||
reason = "No policy for 5322.resent_from_domain {0}".format(
|
||||
self.hdr_resent_from_domain
|
||||
)
|
||||
)
|
||||
else:
|
||||
return self.smfir_reject(
|
||||
queue_id = self.getsymval('i'),
|
||||
reason = "No policy for 5322.from_domain {0}".format(self.hdr_from_domain)
|
||||
)
|
||||
|
||||
if self.hdr_tenant_id is None:
|
||||
logging.error(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM exception: could not determine X-MS-Exchange-CrossTenant-Id - action=reject"
|
||||
"/EOM: exception: could not determine X-MS-Exchange-CrossTenant-Id - action=reject"
|
||||
)
|
||||
return self.smfir_reject(
|
||||
queue_id = self.getsymval('i'),
|
||||
@ -268,15 +336,23 @@ class ExOTAMilter(Milter.Base):
|
||||
)
|
||||
if self.dkim_valid:
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM: Found valid DKIM authentication result for 5322.from_domain={0}".format(
|
||||
self.hdr_from_domain
|
||||
)
|
||||
"/EOM: Valid DKIM signatures found"
|
||||
)
|
||||
for passed_dkim_result in self.passed_dkim_results:
|
||||
if self.hdr_from_domain == passed_dkim_result['sdid']:
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM: Found aligned DKIM signature for SDID: {0}".format(
|
||||
passed_dkim_result['sdid']
|
||||
)
|
||||
)
|
||||
self.dkim_aligned = True
|
||||
if not self.dkim_aligned:
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM: No aligned DKIM signatures found!"
|
||||
)
|
||||
else:
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM: No valid DKIM authentication result found for 5322.from_domain={0}".format(
|
||||
self.hdr_from_domain
|
||||
)
|
||||
"/EOM: No valid DKIM authentication result found"
|
||||
)
|
||||
return self.smfir_reject(
|
||||
queue_id = self.getsymval('i'),
|
||||
@ -297,12 +373,16 @@ class ExOTAMilter(Milter.Base):
|
||||
|
||||
if g_milter_add_header:
|
||||
try:
|
||||
self.addheader("X-ExOTA-Authentication-Results",
|
||||
"{0};\n auth=pass header.d={1} dkim={2} x509_client_trust={3}".format(
|
||||
g_milter_authservid, self.hdr_from_domain, policy.is_dkim_enabled(),
|
||||
g_milter_x509_enabled
|
||||
)
|
||||
addhdr_value = str(
|
||||
"{0};\n" +
|
||||
" auth=pass 5322_from_domain={1} dkim={2} dkim_aligned={3} " +
|
||||
"x509_client_trust={4} forwarded={5}"
|
||||
).format(
|
||||
g_milter_authservid, self.hdr_from_domain, policy.is_dkim_enabled(),
|
||||
self.dkim_aligned, g_milter_x509_enabled, self.forwarded
|
||||
)
|
||||
logging.debug(addhdr_value)
|
||||
self.addheader("X-ExOTA-Authentication-Results", addhdr_value)
|
||||
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM: AR-header added"
|
||||
)
|
||||
@ -313,8 +393,8 @@ class ExOTAMilter(Milter.Base):
|
||||
|
||||
if g_milter_dkim_enabled:
|
||||
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
|
||||
"/EOM: Tenant successfully authorized (dkim_enabled={0})".format(
|
||||
str(policy.is_dkim_enabled())
|
||||
"/EOM: Tenant successfully authorized (dkim_enabled={0} dkim_aligned={1})".format(
|
||||
policy.is_dkim_enabled(), self.dkim_aligned
|
||||
)
|
||||
)
|
||||
else:
|
||||
@ -375,6 +455,10 @@ if __name__ == "__main__":
|
||||
if 'MILTER_X509_TRUSTED_CN' in os.environ:
|
||||
g_milter_x509_trusted_cn = os.environ['MILTER_X509_TRUSTED_CN']
|
||||
logging.info("ENV[MILTER_X509_TRUSTED_CN]: {0}".format(g_milter_x509_trusted_cn))
|
||||
if 'MILTER_X509_IP_WHITELIST' in os.environ:
|
||||
g_milter_x509_ip_whitelist = "".join(os.environ['MILTER_X509_IP_WHITELIST'].split())
|
||||
g_milter_x509_ip_whitelist = g_milter_x509_ip_whitelist.split(',')
|
||||
logging.info("ENV[MILTER_X509_IP_WHITELIST]: {0}".format(g_milter_x509_ip_whitelist))
|
||||
logging.info("ENV[MILTER_X509_ENABLED]: {0}".format(g_milter_x509_enabled))
|
||||
if 'MILTER_POLICY_SOURCE' in os.environ:
|
||||
g_milter_policy_source = os.environ['MILTER_POLICY_SOURCE']
|
||||
|
||||
@ -89,7 +89,7 @@ class ExOTAPolicyBackendJSON(ExOTAPolicyBackend):
|
||||
return ExOTAPolicy(self.policies[from_domain])
|
||||
except KeyError as e:
|
||||
raise ExOTAPolicyNotFoundException(
|
||||
"Policy for from_domain={0} not found".format(from_domain)
|
||||
"Policy for domain={0} not found".format(from_domain)
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise ExOTAPolicyException(
|
||||
|
||||
@ -19,6 +19,7 @@ export MILTER_DKIM_ENABLED=yepp
|
||||
export MILTER_TRUSTED_AUTHSERVID=my-auth-serv-id
|
||||
export MILTER_X509_ENABLED=yepp
|
||||
export MILTER_X509_TRUSTED_CN=mail.protection.outlook.com
|
||||
export MILTER_X509_IP_WHITELIST='127.0.0.1,::1'
|
||||
export MILTER_ADD_HEADER=yepp
|
||||
export MILTER_AUTHSERVID=my-auth-serv-id
|
||||
```
|
||||
|
||||
@ -6,6 +6,9 @@ conn = mt.connect(socket)
|
||||
if conn == nil then
|
||||
error "mt.connect() failed"
|
||||
end
|
||||
if mt.conninfo(conn, "localhost", "::1") ~= nil then
|
||||
error "mt.conninfo() failed"
|
||||
end
|
||||
|
||||
mt.set_timeout(3)
|
||||
|
||||
@ -18,7 +21,7 @@ if mt.getreply(conn) ~= SMFIR_CONTINUE then
|
||||
end
|
||||
|
||||
-- 5321.RCPT+MACROS
|
||||
mt.macro(conn, SMFIC_RCPT, '{client_addr}', "127.128.129.130", "i", "4CgSNs5Q9sz7SllQ", '{cert_subject}', "mail.protection.outlook.com")
|
||||
mt.macro(conn, SMFIC_RCPT, "i", "4CgSNs5Q9sz7SllQ", '{cert_subject}', "mail.protection.outlook.comx")
|
||||
if mt.rcptto(conn, "<envelope.recipient@example.com>") ~= nil then
|
||||
error "mt.rcptto() failed"
|
||||
end
|
||||
@ -30,6 +33,9 @@ end
|
||||
if mt.header(conn, "fRoM", '"Blah Blubb" <O365ConnectorValidation@yad.onmicrosoft.com>') ~= nil then
|
||||
error "mt.header(From) failed"
|
||||
end
|
||||
if mt.header(conn, "aaa-resent-fRoM", '"Blah Blubb" <blah@yad.onmicrosoft.COMa>') ~= 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
|
||||
@ -45,7 +51,7 @@ end
|
||||
if mt.header(conn, "Authentication-Results", "my-auth-serv-id;\n exota=pass") ~= nil then
|
||||
error "mt.header(Subject) failed"
|
||||
end
|
||||
if mt.header(conn, "Authentication-RESULTS", "my-auth-serv-id;\n dkim=pass header.d=yad.onmicrosoft.com header.s=selector1-yad-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
|
||||
if mt.header(conn, "Authentication-RESULTS", "my-auth-serv-id;\n dkim=pass header.d=yad.onmicrosoft.comx header.s=selector1-yad-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=yad.onmicrosoft.com header.s=selector2-asdf header.b=mmmjFpv8") ~= nil then
|
||||
|
||||
Loading…
Reference in New Issue
Block a user