Compare commits

..

No commits in common. "devel" and "v0.9.1" have entirely different histories.

31 changed files with 314 additions and 1230 deletions

View File

@ -1,122 +0,0 @@
# How to install ExOTA-Milter
#### Table of contents
[docker-compose](#docker-compose)
[kubernetes](#kubernetes)
[systemd](#systemd)
## docker-compose <a name="docker-compose"/>
```
~/src/ExOTA-Milter/INSTALL/docker-compose$ docker-compose up
[+] Running 2/2
⠿ Network docker-compose_default Created 0.8s
⠿ Container docker-compose-exota-milter-1 Created 0.1s
Attaching to docker-compose-exota-milter-1
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,503: INFO 140529821924168 Logger initialized
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,503: INFO 140529821924168 ENV[MILTER_NAME]: exota-milter
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,503: INFO 140529821924168 ENV[MILTER_SOCKET]: inet:4321@0.0.0.0
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,504: INFO 140529821924168 ENV[MILTER_REJECT_MESSAGE]: CUSTOMIZE THIS! - Security policy violation!!
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,504: INFO 140529821924168 ENV[MILTER_TMPFAIL_MESSAGE]: Service temporarily not available! Please try again later.
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,504: INFO 140529821924168 ENV[MILTER_TRUSTED_AUTHSERVID]: dkimauthservid
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,504: INFO 140529821924168 ENV[MILTER_DKIM_ALIGNMENT_REQUIRED]: True
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,504: INFO 140529821924168 ENV[MILTER_DKIM_ENABLED]: True
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,504: INFO 140529821924168 ENV[MILTER_X509_TRUSTED_CN]: mail.protection.outlook.com
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,504: INFO 140529821924168 ENV[MILTER_X509_IP_WHITELIST]: ['127.0.0.1', '::1']
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,504: INFO 140529821924168 ENV[MILTER_X509_ENABLED]: True
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,505: INFO 140529821924168 ENV[MILTER_AUTHSERVID]: ThisAuthservID
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,505: INFO 140529821924168 ENV[MILTER_ADD_HEADER]: True
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,505: INFO 140529821924168 ENV[MILTER_POLICY_SOURCE]: file
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,505: INFO 140529821924168 ENV[MILTER_POLICY_FILE]: /data/exota-milter-policy.json
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,511: INFO 140529821924168 JSON policy backend initialized
docker-compose-exota-milter-1 | 2022-06-06 21:54:04,511: INFO 140529821924168 Startup exota-milter@socket: inet:4321@0.0.0.0
```
## kubernetes <a name="kubernetes"/>
By default this example installs the Exota-milter workload into the `exota-milter` namespace, which must be created in advance:
```
kubectl create ns exota-milter
namespace/exota-milter created
```
Deploy stateless workload (type `Deployment`) with `kustomize`:
```
~/src/ExOTA-Milter/INSTALL/kubernetes$ kubectl apply -k .
configmap/exota-milter-policy-cmap created
service/exota-milter created
deployment.apps/exota-milter created
```
Check status of pods, replica-sets and cluster internal service:
```
~/src/ExOTA-Milter/INSTALL/kubernetes$ kubectl -n exota-milter get all
NAME READY STATUS RESTARTS AGE
pod/exota-milter-547dbccd8b-j69mn 1/1 Running 0 64s
pod/exota-milter-547dbccd8b-7hl6c 1/1 Running 0 64s
pod/exota-milter-547dbccd8b-k4ng8 1/1 Running 0 64s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/exota-milter ClusterIP 10.43.78.163 <none> 4321/TCP 61s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/exota-milter 3/3 3 3 64s
NAME DESIRED CURRENT READY AGE
replicaset.apps/exota-milter-547dbccd8b 3 3 3 65s
```
Get logs of the pods:
```
~/src/ExOTA-Milter/INSTALL/kubernetes$ kubectl -n exota-milter logs -l app=exota-milter
2022-06-06 21:57:03,515: INFO Logger initialized
2022-06-06 21:57:03,515: INFO ENV[MILTER_NAME]: exota-milter
2022-06-06 21:57:03,515: INFO ENV[MILTER_SOCKET]: inet:4321@127.0.0.1
2022-06-06 21:57:03,515: INFO ENV[MILTER_REJECT_MESSAGE]: Security policy violation!!
2022-06-06 21:57:03,515: INFO ENV[MILTER_TMPFAIL_MESSAGE]: Service temporarily not available! Please try again later.
2022-06-06 21:57:03,515: INFO ENV[MILTER_TRUSTED_AUTHSERVID]: dkimauthservid
2022-06-06 21:57:03,515: INFO ENV[MILTER_DKIM_ALIGNMENT_REQUIRED]: True
2022-06-06 21:57:03,515: INFO ENV[MILTER_DKIM_ENABLED]: True
2022-06-06 21:57:03,515: INFO ENV[MILTER_X509_TRUSTED_CN]: mail.protection.outlook.com
2022-06-06 21:57:03,515: INFO ENV[MILTER_X509_IP_WHITELIST]: ['127.0.0.1', '::1']
2022-06-06 21:57:03,515: INFO ENV[MILTER_X509_ENABLED]: True
2022-06-06 21:57:03,516: INFO ENV[MILTER_AUTHSERVID]: some-auth-serv-id
2022-06-06 21:57:03,516: INFO ENV[MILTER_ADD_HEADER]: True
2022-06-06 21:57:03,516: INFO ENV[MILTER_POLICY_SOURCE]: file
2022-06-06 21:57:03,516: INFO ENV[MILTER_POLICY_FILE]: /data/exota-milter-policy.json
2022-06-06 21:57:03,516: INFO JSON policy backend initialized
2022-06-06 21:57:03,516: INFO Startup exota-milter@socket: inet:4321@127.0.0.1
```
Remove workload from cluster:
```
~/src/ExOTA-Milter/INSTALL/kubernetes$ kubectl delete -k .
configmap "exota-milter-policy-cmap" deleted
service "exota-milter" deleted
deployment.apps "exota-milter" deleted
~/src/ExOTA-Milter/INSTALL/kubernetes$ kubectl delete ns exota-milter
namespace "exota-milter" deleted
```
## systemd <a name="systemd"/>
If you do not want to run the ExOTA-Milter in a containerized environment but directly as a systemd-unit/-service, first you´ll need to install all necessary python and build dependencies. Start with build deps (examples refere to ubuntu/debian):
```
sudo apt install --no-install-recommends gcc libpython3-dev libmilter-dev python3-pip
```
Now install all python dependencies:
```
~/src/ExOTA-Milter/INSTALL/systemd# sudo pip3 install -r ../../requirements.txt
Requirement already satisfied: authres==1.2.0 in /usr/local/lib/python3.8/dist-packages (from -r ../../requirements.txt (line 1)) (1.2.0)
Requirement already satisfied: pymilter==1.0.4 in /usr/local/lib/python3.8/dist-packages (from -r ../../requirements.txt (line 2)) (1.0.4)
Requirement already satisfied: ldap3 in /usr/local/lib/python3.8/dist-packages (from -r ../../requirements.txt (line 3)) (2.9.1)
Requirement already satisfied: pyasn1>=0.4.6 in /usr/local/lib/python3.8/dist-packages (from ldap3->-r ../../requirements.txt (line 3)) (0.4.8)
```
At last uninstall all build dependencies, as they are not needed anymore:
```
apt purge gcc libpython3-dev libmilter-dev python3-pip
```
Next you should be able to install the ExOTA-Milter as well as the systemd-stuff by running the `install.sh` script:
```
~/src/ExOTA-Milter/INSTALL/systemd$ sudo ./install.sh
Created symlink /etc/systemd/system/multi-user.target.wants/exota-milter.service → /lib/systemd/system/exota-milter.service.
```
Use the `uninstall.sh` script to uninstall the ExOTA-Milter from your systemd environment. Files under `/etc/exota-milter/` (config and policy) are kept undeleted!

View File

@ -1,7 +0,0 @@
{
"example.com": {
"tenant_id": "abcd1234-18c5-45e8-88de-987654321cba",
"dkim_enabled": true,
"dkim_alignment_required": true
}
}

View File

@ -1,22 +0,0 @@
version: '2.4'
services:
exota-milter:
image: chillout2k/exota-milter
restart: unless-stopped
environment:
LOG_LEVEL: 'debug'
MILTER_SOCKET: 'inet:4321@0.0.0.0'
MILTER_POLICY_FILE: '/data/exota-milter-policy.json'
MILTER_DKIM_ENABLED: 'True'
MILTER_DKIM_ALIGNMENT_REQUIRED: 'True'
MILTER_TRUSTED_AUTHSERVID: 'DKIMAuthservID'
MILTER_X509_ENABLED: 'True'
MILTER_X509_TRUSTED_CN: 'mail.protection.outlook.com'
MILTER_X509_IP_WHITELIST: '127.0.0.1,::1'
MILTER_ADD_HEADER: 'True'
MILTER_AUTHSERVID: 'ThisAuthservID'
MILTER_REJECT_MESSAGE: 'CUSTOMIZE THIS! - Security policy violation!!'
volumes:
- "./data/:/data/:ro"
ports:
- "127.0.0.1:4321:4321"

View File

@ -1,14 +0,0 @@
---
kind: ConfigMap
apiVersion: v1
metadata:
name: exota-milter-policy-cmap
data:
exota-milter-policy: |
{
"example.com": {
"tenant_id": "abcd1234-18c5-45e8-88de-987654321cba",
"dkim_enabled": true,
"dkim_alignment_required": true
}
}

View File

@ -1,81 +0,0 @@
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: exota-milter
labels:
app: exota-milter
spec:
replicas: 3
selector:
matchLabels:
app: exota-milter
template:
metadata:
labels:
app: exota-milter
spec:
# Do not deploy more than one pods per node
topologySpreadConstraints:
- labelSelector:
matchLabels:
app: exota-milter
maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
# Pod eviction toleration overrides
tolerations:
- key: "node.kubernetes.io/unreachable"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 30
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 30
restartPolicy: Always
terminationGracePeriodSeconds: 10
volumes:
- name: exota-milter-policy-volume
configMap:
name: exota-milter-policy-cmap
items:
- key: exota-milter-policy
path: 'exota-milter-policy.json'
containers:
- name: exota-milter
image: chillout2k/exota-milter
imagePullPolicy: Always
volumeMounts:
- mountPath: /data
name: exota-milter-policy-volume
startupProbe:
exec:
command: ["nc", "-v", "-w1", "127.0.0.1", "4321"]
initialDelaySeconds: 5
periodSeconds: 10
env:
- name: LOG_LEVEL
value: 'info'
- name: MILTER_SOCKET
value: 'inet:4321@127.0.0.1'
- name: MILTER_POLICY_FILE
value: '/data/exota-milter-policy.json'
- name: MILTER_DKIM_ENABLED
value: 'True'
- name: MILTER_DKIM_ALIGNMENT_REQUIRED
value: 'True'
- name: MILTER_TRUSTED_AUTHSERVID
value: 'DKIMAuthservID'
- name: MILTER_X509_ENABLED
value: 'True'
- name: MILTER_X509_TRUSTED_CN
value: 'mail.protection.outlook.com'
- name: MILTER_X509_IP_WHITELIST
value: '127.0.0.1,::1'
- name: MILTER_ADD_HEADER
value: 'True'
- name: MILTER_AUTHSERVID
value: 'some-auth-serv-id'
- name: MILTER_REJECT_MESSAGE
value: 'Security policy violation!!'

View File

@ -1,12 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: exota-milter
spec:
selector:
app: exota-milter
ports:
- protocol: TCP
port: 4321
targetPort: 4321

View File

@ -1,7 +0,0 @@
namespace: exota-milter
commonLabels:
app: exota-milter
resources:
- 01_config-map.yaml
- 02_deployment.yaml
- 03_service.yaml

View File

@ -1,7 +0,0 @@
{
"example.com": {
"tenant_id": "abcd1234-18c5-45e8-88de-987654321cba",
"dkim_enabled": true,
"dkim_alignment_required": true
}
}

View File

@ -1,20 +0,0 @@
export LOG_LEVEL='info'
export MILTER_SOCKET='inet:4321@127.0.0.1'
export MILTER_POLICY_FILE='/etc/exota-milter/exota-milter-policy.json'
export MILTER_DKIM_ENABLED='True'
export MILTER_DKIM_ALIGNMENT_REQUIRED='True'
export MILTER_TRUSTED_AUTHSERVID='DKIMAuthservID'
export MILTER_X509_ENABLED='True'
export MILTER_X509_TRUSTED_CN='mail.protection.outlook.com'
export MILTER_X509_IP_WHITELIST='127.0.0.1,::1'
export MILTER_ADD_HEADER='True'
export MILTER_AUTHSERVID='some-auth-serv-id'
#export MILTER_REJECT_MESSAGE='Security policy violation!!'
# LDAP integration
#export MILTER_POLICY_SOURCE=ldap
#export MILTER_LDAP_SERVER_URI=ldaps://your.ldap.server
#export MILTER_LDAP_SEARCH_BASE=ou=your-customer-domains,dc=example,dc=org
#export MILTER_LDAP_QUERY='(domainNameAttr=%d)'
#export MILTER_LDAP_BINDDN=uid=exota-milter,ou=apps,dc=example,dc=org
#export MILTER_LDAP_BINDPW='$uPer§ecRet1!'

View File

@ -1,9 +0,0 @@
[Unit]
Description=ExOTA-Milter
[Service]
Restart=always
ExecStart=/usr/local/sbin/exota-milter.sh
[Install]
WantedBy=multi-user.target

View File

@ -1,15 +0,0 @@
#!/bin/sh
if [ ! -e /etc/exota-milter/exota-milter.conf ]; then
echo "Missing /etc/exota-milter/exota-milter.conf!"
exit 1;
fi
if [ ! -e /etc/exota-milter/exota-milter-policy.json ]; then
echo "Missing /etc/exota-milter/exota-milter-policy.json!"
exit 1;
fi
. /etc/exota-milter/exota-milter.conf
exec /usr/bin/python3 /usr/local/exota-milter/exota-milter.py 2>&1

View File

@ -1,24 +0,0 @@
#!/bin/sh
if [ "$(id -u)" != "0" ]; then
echo "You must be root!"
exit 1
fi
install -d /usr/local/exota-milter/
install ../../app/*.py /usr/local/exota-milter/
install -m 750 exota-milter.sh /usr/local/sbin/exota-milter.sh
install -d -m 660 /etc/exota-milter
if [ -e /etc/exota-milter/exota-milter-policy.json ]; then
echo "Found existing /etc/exota-milter/exota-milter-policy.json - skipping"
else
install -m 660 exota-milter-policy.json /etc/exota-milter/exota-milter-policy.json
fi
if [ -e /etc/exota-milter/exota-milter.conf ]; then
echo "Found existing /etc/exota-milter/exota-milter.conf - skipping"
else
install -m 750 exota-milter.conf /etc/exota-milter/exota-milter.conf
fi
install -m 660 exota-milter.service /lib/systemd/system/exota-milter.service
systemctl daemon-reload
systemctl enable exota-milter.service

View File

@ -1,14 +0,0 @@
#!/bin/sh
if [ "$(id -u)" != "0" ]; then
echo "You must be root!"
exit 1
fi
systemctl disable exota-milter.service
systemctl stop exota-milter.service
rm -rf /usr/local/exota-milter/
rm -f /usr/local/sbin/exota-milter.sh
rm -f /lib/systemd/system/exota-milter.service
systemctl daemon-reload
echo "/etc/exota-milter/ was kept undeleted!"

View File

@ -1,13 +1,12 @@
ARG PARENT_IMAGE=alpine:3.16
FROM ${PARENT_IMAGE}
ARG ARCH
FROM $ARCH/alpine
LABEL maintainer="Dominik Chilla <dominik@zwackl.de>"
LABEL git_repo="https://github.com/chillout2k/exota-milter"
ADD ./requirements.txt /requirements.txt
RUN apk update \
&& apk add --no-cache python3 python3-dev py3-pip \
gcc libc-dev libmilter-dev \
&& apk add 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 \
@ -24,4 +23,4 @@ RUN chown -R exota-milter /app /cmd \
VOLUME [ "/socket", "/data" ]
USER exota-milter
CMD [ "/cmd" ]
CMD [ "/cmd" ]

View File

@ -2,11 +2,7 @@
![OSSAR](https://github.com/chillout2k/ExOTA-Milter/workflows/OSSAR/badge.svg?branch=master)
# ExOTA-Milter - Exchange Online Tenant Authorisation Milter (Mail-Filter)
![ExOTA-Milter use case](use-case.png)
*Diagram created with: https://app.diagrams.net/*
# 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)**.
@ -99,8 +95,8 @@ By the way, the global setting `ENV[MILTER_DKIM_ALIGNMENT_REQUIRED]` can be over
}
```
## X-MS-Exchange-CrossTenant-Id header (OPTIONAL!)
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** extracts the tenant-ID from the *X-MS-Exchange-CrossTenant-Id* email header and uses it as a *mandatory* authentication factor. Since September 2022 Microsoft did not set this header anymore reliably.
## X-MS-Exchange-CrossTenant-Id header (policy binding)
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** extracts the tenant-ID from the *X-MS-Exchange-CrossTenant-Id* email header and uses it as a *mandatory* authentication factor.
```
[...]
X-MS-Exchange-CrossTenant-Id: <UUID-of-tenant>
@ -125,18 +121,17 @@ Finally it´s the combination of all of the above discussed aspects which may re
* matching for client certificate´s CN (ExOTA-Milter)
* verification of DKIM signatures providing *Authentication-Results* header (another milter, e.g. OpenDKIM)
* consideration of DKIM verification results per sender domain (ExOTA-Milter)
* *OPTIONAL* matching for tenant-id provided in *X-MS-Exchange-CrossTenant-Id* header (ExOTA-Milter)
* matching for tenant-id provided in *X-MS-Exchange-CrossTenant-Id* header (ExOTA-Milter)
![Activity policy](http://www.plantuml.com/plantuml/png/bPLHJzi-5CNVyoaERqKY_FzZuc7fmyYI8BhHTae3GffGPUBRriArkzXXyEsNrWsrIa3T8xw-zvppezUvC9PLjbxAm0eh2Tdpk8Z3eP2MAXWgwqhO5woq5EKBPbB_GR3f2A9X4QFKIb5fYVSH1D5LcaT8j9JDaL1pC2bHaQGdfYmMn3XLfXyeRGcIPZR2PQMN9uXhko1bHSciq2hCoTJIcXFSXSD9c3sN2wRc52QLDgOWrSmA1xnLowdKSoNCMiwGQXJ0zP89vUkWO8-aC6lKa5ycvv_FpaxNqbjFO8h_fwlNKcE06X7lnkcszkcq6ItH8_L4Kg_e6C9WD2vUKnurlhBnCCArrezhJ_MgPISK7bZP-E2-DNnZXZYqUbNVk7GPWa3CqFkvPQzhnRyUOyqAlHSonm6mhhl_LOJdy_-_e9IYyOvaX791vSO2AVOWwKqh4ErCxZNtDtNFPJQw_JKSN1TPFhrhRawB_6PIPEaq6Tq7WFDnkJO8MzN64jyR-5QSf27qb7P_0L6UVS-ImYa3nkgLkwUuc0N-_TsPdpxzCHWUaDa3kc3ciAvzLct4xj_jxeSEl4-n1HZV55UJBByTf0va1qfdCix3xU0kUpwT3fuUKPyfqC6GIJ5NIrBs42xTHCFTZ0zRWIXfDSJCjPgcazcw8WZZl13VXV1nEhkJT91YsO02_QwPXDNmDsdgDkae07p30-E5xRBNXRKJRGTU7t7tdBRULVLhVkNmLR3RHp9jtxT_UWhQTi4XrFRnwdSl-KwfdNZ0GZltJld3KDw9DymDSZUYd9RwgVtIt8Nks_NnMgvJhAhD2zyFV6gCy-sTVJnHp7aZSfX0FtXlvUCqdLvbSPhbzxjiRP9aF2e6evki7spoNJJ7zLB-1G00)
![Activity policy](http://www.plantuml.com/plantuml/png/5SKn3W8W30NGg-W1f8cZcuEZSN4tM8aq5ahAhyhjZMzvM-ciyIZXkgd0c0SYpv_q5DIunopErb4w4biZhg9gWVsBJj_BzRWxYw8ujJp_POQy1UisJ8LN6j7q1m00)
# How about using LDAP as policy backend?
For small setups, with not so many domains, the JSON-file policy backend (default) may be sufficient. If you´re an email service provider (ESP) maintaining a lot of customer domains in a LDAP server, you may want to use the LDAP backend instead. Details regarding the LDAP backend can be found [in the LDAP readme](LDAP/README.md).
# How about a docker/OCI image?
## Using prebuilt images from [dockerhub](https://hub.docker.com/)
* **OBSOLETE!** ~~AMD64: https://hub.docker.com/r/chillout2k/exota-milter-amd64~~
* **OBSOLETE!** ~~ARM32v6: https://hub.docker.com/r/chillout2k/exota-milter-arm32v6~~
* **NEW multi-architecture image:** https://hub.docker.com/r/chillout2k/exota-milter
* AMD64: https://hub.docker.com/r/chillout2k/exota-milter-amd64
* ARM32v6: https://hub.docker.com/r/chillout2k/exota-milter-arm32v6
The images are built on a weekly basis. The corresponding *Dockerfile* is located [here](OCI/Dockerfile)
@ -145,35 +140,3 @@ Take a look [here](OCI/README.md)
# How to test?
First of all please take a look at how to set up the testing environment, which is described [here](tests/README.md)
# How to install on docker/kubernetes/systemd?
The installation procedure is documented [here](INSTALL/README.md)
# How to *configure* the ExOTA-Milter?
|ENV variable|type|default|description|
|---|---|---|---|
|MILTER_NAME|`string`|`exota-milter`|Name of the milter instance. Base for socket path. Name appears in logs |
|MILTER_SOCKET|`string`|`/socket/<ENV[MITLER_NAME]>`|Defines the filesystem path of milter socket. The milter can be also exposed as a tcp-socket like `inet:4321@127.0.0.1`|
|MILTER_REJECT_MESSAGE|`string`|`Security policy violation!`|Milter reject (SMTP 5xx code) message presented to the calling MTA|
|MILTER_TMPFAIL_MESSAGE|`string`|`Service temporarily not available! Please try again later.`|Milter temporary fail (SMTP 4xx code) message presentetd to the calling MTA.|
|MILTER_TENANT_ID_REQUIRED|`bool`|`false`|Controls the requirement of the presence of the unofficial `X-MS-Exchange-CrossTenant-Id` header. Used as additional authentication factor.|
|MILTER_DKIM_ENABLED|`bool`|`false`|Enables/disables the checking of DKIM authentication results. Used as additional but strong authentication factor.|
|MILTER_DKIM_ALIGNMENT_REQUIRED|`bool`|`false`|Enables/disables the alighment checks of DKIM SDID with RFC-5322.from_domain. Requires ENV[MILTER_DKIM_ENABLED] = `true`|
|MILTER_TRUSTED_AUTHSERVID|`string`|`invalid`|Specifies the trusted DKIM-signature validating entity (DKIM-validator - producer of Authentication-Results header). The DKIM-validator must place exactly the same string as configured here into the Authentication-Results header! Requires ENV[MILTER_DKIM_ENABLED] = `true`|
|MILTER_POLICY_SOURCE|`string`|`file`|Policy source - Possible values `file` (JSON) or `ldap`|
|MILTER_POLICY_FILE|`string`|`/data/policy.json`|Filesystem path to the (JSON) policy file. Requires ENV[MILTER_POLICY_SOURCE] = `file`|
|MILTER_X509_ENABLED|`bool`|`false`|Enables/disables the checking of client x509-certificate. Used as additional authentication factor.|
|MILTER_X509_TRUSTED_CN|`string`|`mail.protection.outlook.com`|FQDN of authenticating client MTA. Requires ENV[MILTER_X509_ENABLED] = `true`|
|MILTER_X509_IP_WHITELIST|Whitespace or comma separated list of `string`|`127.0.0.1,::1`|List of IP-addresses for which the ExOTA-Milter skips x509 checks. Requires ENV[MILTER_X509_ENABLED] = `true`|
|MILTER_ADD_HEADER|`bool`|`false`|Controls if the ExOTA-Milter should write an additional `X-ExOTA-Authentication-Results` header with authentication information|
|MILTER_AUTHSERVID|`string`|empty|Provides ID of authenticating entity within `X-ExOTA-Authentication-Results` header to further validating instances. Required when ENV[MILTER_ADD_HEADER] = `true`|
|MILTER_LDAP_SERVER_URI|`string`|empty|LDAP-URI of LDAP server holding ExOTA policies. Required when ENV[MILTER_POLICY_SOURCE] = `ldap`|
|MILTER_LDAP_RECEIVE_TIMEOUT|`int`|5|Timespan the ExOTA-Milter waits for the LDAP server to respond to a request. This NOT the TCP-connect timeout! Requires ENV[MILTER_POLICY_SOURCE] = `ldap`|
|MILTER_LDAP_BINDDN|`string`|empty|Distinguished name of the binding (authenticating) *user*|
|MILTER_LDAP_BINDPW|`string`|empty|Password of the binding (authenticating) *user*|
|MILTER_LDAP_SEARCH_BASE|`string`|empty|Search base-DN on the LDAP server. Required when ENV[MILTER_POLICY_SOURCE] = `ldap`|
|MILTER_LDAP_QUERY|`string`|empty|LDAP query/filter used to match for a ExOTA-policy. A placeholder must be used to filter for the authenticating domain (`%d`), e.g. `(domain_attribute=%d)`|
|MILTER_LDAP_TENANT_ID_ATTR|`string`|`exotaMilterTenantId`|Custom LDAP attribute name unless using the ExOTA-milter LDAP schema|
|MILTER_LDAP_DKIM_ENABLED_ATTR|`string`|`exotaMilterDkimEnabled`|Custom LDAP attribute name unless using the ExOTA-milter LDAP schema|
|MILTER_LDAP_DKIM_ALIGNMENT_REQIRED_ATTR|`string`|`exotaMilterDkimAlignmentRequired`|Custom LDAP attribute name unless using the ExOTA-milter LDAP schema|

View File

@ -50,14 +50,11 @@ if (Policy found?) then (yes)
endif
else (no)
endif
if (Milter: tenant-ID header checking enabled?) then (yes)
:Looking up tenant-id in policy;
if (Found trusted tenant-ID?) then (no)
:REJECT;
stop
else (yes)
endif
else (no)
:Looking up tenant-id in policy;
if (Found trusted tenant-ID?) then (no)
:REJECT;
stop
else (yes)
endif
else (no)
:REJECT;

View File

@ -2,19 +2,22 @@ import Milter
import sys
import traceback
import os
import logging
import string
import random
import re
import email.utils
import authres
import json
from policy import (
ExOTAPolicyException, ExOTAPolicyNotFoundException,
ExOTAPolicyBackendJSON, ExOTAPolicyBackendLDAP,
ExOTAPolicyBackendJSON, ExOTAPolicyBackendLDAP, ExOTAPolicy,
ExOTAPolicyInvalidException, ExOTAPolicyBackendException
)
from logger import (
init_logger, log_info, log_error, log_debug
from ldap3 import (
Server, Connection, NONE, set_config_parameter
)
from ldap3.core.exceptions import LDAPException
# Globals with defaults. Can/should be modified by ENV-variables on startup.
# ENV[MILTER_NAME]
@ -25,8 +28,8 @@ g_milter_socket = '/socket/' + g_milter_name
g_milter_reject_message = 'Security policy violation!'
# ENV[MILTER_TMPFAIL_MESSAGE]
g_milter_tmpfail_message = 'Service temporarily not available! Please try again later.'
# ENV[MILTER_TENANT_ID_REQUIRED]
g_milter_tenant_id_required = False
# ENV[LOG_LEVEL]
g_loglevel = logging.INFO
# ENV[MILTER_DKIM_ENABLED]
g_milter_dkim_enabled = False
# ENV[MILTER_DKIM_ALIGNMENT_REQUIRED]
@ -66,6 +69,7 @@ g_milter_ldap_dkim_enabled_attr = 'exotaMilterDkimEnabled'
# ENV[MILTER_LDAP_DKIM_ALIGNMENT_REQIRED_ATTR]
g_milter_ldap_dkim_alignment_required_attr = 'exotaMilterDkimAlignmentRequired'
# Another globals
g_policy_backend = None
g_re_domain = re.compile(r'^.*@(\S+)$', re.IGNORECASE)
@ -76,11 +80,10 @@ class ExOTAMilter(Milter.Base):
def __init__(self):
self.x509_client_valid = False
self.client_ip = None
self.client_port = None
self.reset()
log_debug(self.mconn_id + " INIT: {0}".format(self.__dict__))
def reset(self):
self.conn_reused = False
self.hdr_from = None
self.hdr_from_domain = None
self.hdr_resent_from = None
@ -88,7 +91,6 @@ class ExOTAMilter(Milter.Base):
self.forwarded = False
self.hdr_tenant_id = None
self.hdr_tenant_id_count = 0
self.hdr_different_tenant_id = False
self.x509_whitelisted = False
self.dkim_valid = False
self.passed_dkim_results = []
@ -98,7 +100,7 @@ class ExOTAMilter(Milter.Base):
self.mconn_id = g_milter_name + ': ' + ''.join(
random.choice(string.ascii_lowercase + string.digits) for _ in range(8)
)
log_debug(self.mconn_id + " reset(): {0}".format(self.__dict__))
logging.debug(self.mconn_id + " reset()")
def smfir_reject(self, **kwargs):
message = g_milter_reject_message
@ -108,10 +110,9 @@ class ExOTAMilter(Milter.Base):
message = "queue_id: {0} - {1}".format(kwargs['queue_id'], message)
if 'reason' in kwargs:
message = "{0} - reason: {1}".format(message, kwargs['reason'])
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
": milter_action=reject message={0}".format(message)
)
self.reset()
self.setreply('550','5.7.1', message)
return Milter.REJECT
@ -123,10 +124,9 @@ class ExOTAMilter(Milter.Base):
message = "queue_id: {0} - {1}".format(kwargs['queue_id'], message)
if 'reason' in kwargs:
message = "{0} - reason: {1}".format(message, kwargs['reason'])
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
": milter_action=tempfail message={0}".format(message)
)
self.reset()
self.setreply('450','4.7.1', message)
return Milter.TEMPFAIL
@ -145,96 +145,76 @@ class ExOTAMilter(Milter.Base):
return self.smfir_continue()
def connect(self, IPname, family, hostaddr):
self.reset()
self.client_ip = hostaddr[0]
self.client_port = hostaddr[1]
log_debug(self.mconn_id + "/CONNECT client_ip={0} client_port={1}".format(
self.client_ip, self.client_port
))
return self.smfir_continue()
# Mandatory callback
def envfrom(self, mailfrom, *str):
log_debug(self.mconn_id + "/FROM 5321.from={0}".format(mailfrom))
log_debug(self.mconn_id + "/FROM {0}".format(self.__dict__))
logging.debug(self.mconn_id + "/FROM 5321.from={0}".format(mailfrom))
# Instance member values remain within reused SMTP-connections!
if self.conn_reused:
# Milter connection reused!
logging.debug(self.mconn_id + "/FROM connection reused!")
self.reset()
else:
self.conn_reused = True
logging.debug(self.mconn_id + "/FROM client_ip={0}".format(self.client_ip))
return self.smfir_continue()
# Mandatory callback
def envrcpt(self, to, *str):
log_debug(self.mconn_id + "/RCPT 5321.rcpt={0}".format(to))
logging.debug(self.mconn_id + "/RCPT 5321.rcpt={0}".format(to))
return self.smfir_continue()
def header(self, name, hval):
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Header: {0}, Value: {1}".format(name, hval)
)
# Parse RFC-5322-From header
if(name.lower() == "from"):
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Header: {0}, Value: {1}".format(name, hval)
)
if(name.lower() == "From".lower()):
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:
log_error(self.mconn_id + "/" + str(self.getsymval('i')) + "/HDR " +
logging.error(self.mconn_id + "/" + str(self.getsymval('i')) + "/HDR " +
"Could not determine domain-part of 5322.from=" + self.hdr_from
)
return self.smfir_reject(queue_id=self.getsymval('i'))
self.hdr_from_domain = m.group(1)
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
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 RFC-5322-Resent-From header (Forwarded)
if(name.lower() == "resent-from"):
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Header: {0}, Value: {1}".format(name, hval)
)
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:
log_error(self.mconn_id + "/" + str(self.getsymval('i')) + "/HDR " +
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()
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
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"):
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Header: {0}, Value: {1}".format(name, hval)
)
if g_milter_tenant_id_required == True:
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Tenant-ID: {0}".format(hval.lower())
)
if self.hdr_tenant_id_count > 0:
if not self.hdr_tenant_id == hval.lower():
self.hdr_different_tenant_id = True
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Different Tenant-IDs found!"
)
else:
self.hdr_tenant_id_count += 1
self.hdr_tenant_id = hval.lower()
# Just for debugging cases
elif(name.lower() == "dkim-signature"):
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Header: {0}, Value: {1}".format(name, hval)
# Parse non-standardized X-MS-Exchange-CrossTenant-Id header
elif(name.lower() == "X-MS-Exchange-CrossTenant-Id".lower()):
self.hdr_tenant_id_count += 1
self.hdr_tenant_id = hval.lower()
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Tenant-ID: {0}".format(self.hdr_tenant_id)
)
# Parse RFC-7601 Authentication-Results header
elif(name.lower() == "authentication-results"):
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Header: {0}, Value: {1}".format(name, hval)
)
elif(name.lower() == "Authentication-Results".lower()):
if g_milter_dkim_enabled == True:
ar = None
try:
@ -246,26 +226,23 @@ class ExOTAMilter(Milter.Base):
if ar_result.method == 'dkim':
if ar_result.result == 'pass':
self.passed_dkim_results.append({
"sdid": ar_result.header_d.lower()
"sdid": ar_result.header_d
})
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: DKIM passed SDID {0}".format(ar_result.header_d)
)
self.dkim_valid = True
else:
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Ignoring authentication results of {0}".format(ar.authserv_id)
)
except Exception as e:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: AR-parse exception: {0}".format(str(e))
)
elif(name == "X-ExOTA-Authentication-Results"):
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Header: {0}, Value: {1}".format(name, hval)
)
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/HDR: Found X-ExOTA-Authentication-Results header. Marking for deletion."
)
self.xar_hdr_count += 1
@ -280,7 +257,7 @@ class ExOTAMilter(Milter.Base):
if g_milter_x509_enabled:
for whitelisted_client_ip in g_milter_x509_ip_whitelist:
if self.client_ip == whitelisted_client_ip:
log_info(self.mconn_id + "/" + str(self.getsymval('i'))
logging.info(self.mconn_id + "/" + str(self.getsymval('i'))
+ "/EOM: x509 CN check: client-IP '{0}' is whitelisted".format(
whitelisted_client_ip
)
@ -289,7 +266,7 @@ class ExOTAMilter(Milter.Base):
if not self.x509_whitelisted:
cert_subject = self.getsymval('{cert_subject}')
if cert_subject is None:
log_info(self.mconn_id + "/" + str(self.getsymval('i'))
logging.info(self.mconn_id + "/" + str(self.getsymval('i'))
+ "/EOM: No trusted x509 client CN found - action=reject"
)
return self.smfir_reject(
@ -299,11 +276,11 @@ class ExOTAMilter(Milter.Base):
else:
if g_milter_x509_trusted_cn.lower() == cert_subject.lower():
self.x509_client_valid = True
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Trusted x509 client CN {0}".format(cert_subject)
)
else:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Untrusted x509 client CN {0} - action=reject".format(cert_subject)
)
return self.smfir_reject(
@ -312,7 +289,7 @@ class ExOTAMilter(Milter.Base):
)
if self.hdr_from is None:
log_error(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.error(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: exception: could not determine 5322.from header - action=reject"
)
return self.smfir_reject(
@ -320,29 +297,17 @@ class ExOTAMilter(Milter.Base):
reason = '5322.from header missing'
)
if g_milter_tenant_id_required == True:
if self.hdr_different_tenant_id == True:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Multiple/different tenant-ID headers found for {0} - action=reject".format(
self.hdr_from_domain
)
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
reason = 'Multiple/different tenant-ID headers found!'
)
# Get policy for 5322.from_domain
policy = None
try:
policy = g_policy_backend.get(self.hdr_from_domain)
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Policy for 5322.from_domain={0} fetched from backend: *{1}*".format(
self.hdr_from_domain, str(policy)
)
)
except ExOTAPolicyBackendException as e:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Policy backend problem: {0}".format(e.message)
)
return self.smfir_tempfail(
@ -350,7 +315,7 @@ class ExOTAMilter(Milter.Base):
reason = "Policy backend problem"
)
except ExOTAPolicyInvalidException as e:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Invalid policy for 5322.from_domain={0}: {1}".format(
self.hdr_from_domain, e.message
)
@ -362,26 +327,26 @@ class ExOTAMilter(Milter.Base):
)
)
except (ExOTAPolicyException, ExOTAPolicyNotFoundException) as e:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/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)
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Policy for 5322.resent_from_domain={0} fetched from backend: *{1}*".format(
self.hdr_resent_from_domain, str(policy)
)
)
self.forwarded = True
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
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 ExOTAPolicyBackendException as e:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Policy backend problem: {0}".format(e.message)
)
return self.smfir_tempfail(
@ -389,7 +354,7 @@ class ExOTAMilter(Milter.Base):
reason = "Policy backend problem"
)
except ExOTAPolicyInvalidException as e:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Invalid policy for 5322.resent_from_domain={0}: {1}".format(
self.hdr_resent_from_domain, e.message
)
@ -401,7 +366,7 @@ class ExOTAMilter(Milter.Base):
)
)
except (ExOTAPolicyException, ExOTAPolicyNotFoundException) as e:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: 5322.resent-from: {0}".format(e.message)
)
return self.smfir_reject(
@ -416,53 +381,59 @@ class ExOTAMilter(Milter.Base):
reason = "No policy for 5322.from_domain {0}".format(self.hdr_from_domain)
)
if g_milter_tenant_id_required == True:
if self.hdr_tenant_id is None:
log_error(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: exception: could not determine X-MS-Exchange-CrossTenant-Id - action=reject"
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"
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
reason = 'Tenant-ID is missing!'
)
if self.hdr_tenant_id_count > 1:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: More than one tenant-IDs for {0} found - action=reject".format(
self.hdr_from_domain
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
reason = 'Tenant-ID is missing!'
)
if self.hdr_tenant_id == policy.get_tenant_id():
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: tenant_id={0} status=match".format(self.hdr_tenant_id)
)
else:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: tenant_id={0} status=no_match - action=reject".format(
self.hdr_tenant_id
)
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
reason = 'No policy match for tenant-id'
)
return self.smfir_reject(queue_id=self.getsymval('i'))
if self.hdr_tenant_id == policy.get_tenant_id():
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: tenant_id={0} status=match".format(self.hdr_tenant_id)
)
else:
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: tenant_id={0} status=no_match - action=reject".format(
self.hdr_tenant_id
)
)
return self.smfir_reject(
queue_id = self.getsymval('i'),
reason = 'No policy match for tenant-id'
)
if g_milter_dkim_enabled and policy.is_dkim_enabled():
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: 5322.from_domain={0} dkim_auth=enabled".format(self.hdr_from_domain)
)
if self.dkim_valid:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Valid DKIM signatures found"
)
for passed_dkim_result in self.passed_dkim_results:
if self.hdr_from_domain == passed_dkim_result['sdid']:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
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:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: No aligned DKIM signatures found!"
)
if g_milter_dkim_alignment_required:
if policy.is_dkim_alignment_required() == False:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Policy overrides DKIM alignment requirement to '{0}'!".format(
policy.is_dkim_alignment_required()
)
@ -473,7 +444,7 @@ class ExOTAMilter(Milter.Base):
reason = 'DKIM alignment required!'
)
else:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: No valid DKIM authentication result found"
)
return self.smfir_reject(
@ -483,13 +454,13 @@ class ExOTAMilter(Milter.Base):
# Delete all existing X-ExOTA-Authentication-Results headers
for i in range(self.xar_hdr_count, 0, -1):
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Deleting X-ExOTA-Authentication-Results header"
)
try:
self.chgheader("X-ExOTA-Authentication-Results", i-1, '')
except Exception as e:
log_error(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.error(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Deleting X-ExOTA-Authentication-Results failed: {0}".format(str(e))
)
@ -503,64 +474,73 @@ class ExOTAMilter(Milter.Base):
g_milter_authservid, self.hdr_from_domain, policy.is_dkim_enabled(),
self.dkim_aligned, g_milter_x509_enabled, self.forwarded
)
log_debug(addhdr_value)
logging.debug(addhdr_value)
self.addheader("X-ExOTA-Authentication-Results", addhdr_value)
log_debug(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.debug(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: AR-header added"
)
except Exception as e:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: addheader(AR) failed: {0}".format(str(e))
)
if g_milter_dkim_enabled:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Tenant successfully authorized (dkim_enabled={0} dkim_aligned={1})".format(
policy.is_dkim_enabled(), self.dkim_aligned
)
)
else:
log_info(self.mconn_id + "/" + str(self.getsymval('i')) +
logging.info(self.mconn_id + "/" + str(self.getsymval('i')) +
"/EOM: Tenant successfully authorized"
)
self.reset()
return self.smfir_continue()
def abort(self):
# Client disconnected prematurely
log_debug(self.mconn_id + "/ABORT")
logging.debug(self.mconn_id + "/ABORT")
return self.smfir_continue()
def close(self):
# Always called, even when abort is called.
# Clean up any external resources here.
log_debug(self.mconn_id + "/CLOSE {0}".format(self.__dict__))
logging.debug(self.mconn_id + "/CLOSE")
return self.smfir_continue()
if __name__ == "__main__":
init_logger()
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']
log_info("ENV[MILTER_NAME]: {0}".format(g_milter_name))
logging.info("ENV[MILTER_NAME]: {0}".format(g_milter_name))
if 'MILTER_SOCKET' in os.environ:
g_milter_socket = os.environ['MILTER_SOCKET']
log_info("ENV[MILTER_SOCKET]: {0}".format(g_milter_socket))
logging.info("ENV[MILTER_SOCKET]: {0}".format(g_milter_socket))
if 'MILTER_REJECT_MESSAGE' in os.environ:
g_milter_reject_message = os.environ['MILTER_REJECT_MESSAGE']
log_info("ENV[MILTER_REJECT_MESSAGE]: {0}".format(g_milter_reject_message))
logging.info("ENV[MILTER_REJECT_MESSAGE]: {0}".format(g_milter_reject_message))
if 'MILTER_TMPFAIL_MESSAGE' in os.environ:
g_milter_tmpfail_message = os.environ['MILTER_TMPFAIL_MESSAGE']
log_info("ENV[MILTER_TMPFAIL_MESSAGE]: {0}".format(g_milter_tmpfail_message))
if 'MILTER_TENANT_ID_REQUIRED' in os.environ:
g_milter_tenant_id_required = True
log_info("ENV[MILTER_TENANT_ID_REQUIRED]: {0}".format(g_milter_tenant_id_required))
logging.info("ENV[MILTER_TMPFAIL_MESSAGE]: {0}".format(g_milter_tmpfail_message))
if 'MILTER_DKIM_ENABLED' in os.environ:
g_milter_dkim_enabled = True
if 'MILTER_TRUSTED_AUTHSERVID' in os.environ:
g_milter_trusted_authservid = os.environ['MILTER_TRUSTED_AUTHSERVID'].lower()
log_info("ENV[MILTER_TRUSTED_AUTHSERVID]: {0}".format(g_milter_trusted_authservid))
logging.info("ENV[MILTER_TRUSTED_AUTHSERVID]: {0}".format(g_milter_trusted_authservid))
else:
log_error("ENV[MILTER_TRUSTED_AUTHSERVID] is mandatory!")
logging.error("ENV[MILTER_TRUSTED_AUTHSERVID] is mandatory!")
sys.exit(1)
if 'MILTER_DKIM_ALIGNMENT_REQUIRED' in os.environ:
if os.environ['MILTER_DKIM_ALIGNMENT_REQUIRED'] == 'True':
@ -568,77 +548,77 @@ if __name__ == "__main__":
elif os.environ['MILTER_DKIM_ALIGNMENT_REQUIRED'] == 'False':
g_milter_dkim_alignment_required = False
else:
log_error("ENV[MILTER_DKIM_ALIGNMENT_REQUIRED] must be a boolean type: 'True' or 'False'!")
logging.error("ENV[MILTER_DKIM_ALIGNMENT_REQUIRED] must be a boolean type: 'True' or 'False'!")
sys.exit(1)
log_info("ENV[MILTER_DKIM_ALIGNMENT_REQUIRED]: {0}".format(
logging.info("ENV[MILTER_DKIM_ALIGNMENT_REQUIRED]: {0}".format(
g_milter_dkim_alignment_required
))
log_info("ENV[MILTER_DKIM_ENABLED]: {0}".format(g_milter_dkim_enabled))
logging.info("ENV[MILTER_DKIM_ENABLED]: {0}".format(g_milter_dkim_enabled))
if 'MILTER_X509_ENABLED' in os.environ:
g_milter_x509_enabled = True
if 'MILTER_X509_TRUSTED_CN' in os.environ:
g_milter_x509_trusted_cn = os.environ['MILTER_X509_TRUSTED_CN']
log_info("ENV[MILTER_X509_TRUSTED_CN]: {0}".format(g_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(',')
log_info("ENV[MILTER_X509_IP_WHITELIST]: {0}".format(g_milter_x509_ip_whitelist))
log_info("ENV[MILTER_X509_ENABLED]: {0}".format(g_milter_x509_enabled))
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_ADD_HEADER' in os.environ:
g_milter_add_header = True
if 'MILTER_AUTHSERVID' in os.environ:
g_milter_authservid = os.environ['MILTER_AUTHSERVID']
if not re.match(r'^\S+$', g_milter_authservid):
log_error("ENV[MILTER_AUTHSERVID] is invalid: {0}".format(g_milter_authservid))
log_info("ENV[MILTER_AUTHSERVID]: {0}".format(g_milter_authservid))
logging.error("ENV[MILTER_AUTHSERVID] is invalid: {0}".format(g_milter_authservid))
logging.info("ENV[MILTER_AUTHSERVID]: {0}".format(g_milter_authservid))
else:
log_error("ENV[MILTER_AUTHSERVID] is mandatory!")
logging.error("ENV[MILTER_AUTHSERVID] is mandatory!")
sys.exit(1)
log_info("ENV[MILTER_ADD_HEADER]: {0}".format(g_milter_add_header))
logging.info("ENV[MILTER_ADD_HEADER]: {0}".format(g_milter_add_header))
if 'MILTER_POLICY_SOURCE' in os.environ:
g_milter_policy_source = os.environ['MILTER_POLICY_SOURCE']
log_info("ENV[MILTER_POLICY_SOURCE]: {0}".format(g_milter_policy_source))
logging.info("ENV[MILTER_POLICY_SOURCE]: {0}".format(g_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']
log_info("ENV[MILTER_POLICY_FILE]: {0}".format(g_milter_policy_file))
logging.info("ENV[MILTER_POLICY_FILE]: {0}".format(g_milter_policy_file))
try:
g_policy_backend = ExOTAPolicyBackendJSON(g_milter_policy_file)
log_info("JSON policy backend initialized")
logging.info("JSON policy backend initialized")
except ExOTAPolicyException as e:
log_error("Policy backend error: {0}".format(e.message))
logging.error("Policy backend error: {0}".format(e.message))
sys.exit(1)
else:
log_error("ENV[MILTER_POLICY_FILE] is mandatory!")
logging.error("ENV[MILTER_POLICY_FILE] is mandatory!")
sys.exit(1)
elif g_milter_policy_source == 'ldap':
if 'MILTER_LDAP_SERVER_URI' not in os.environ:
log_error("ENV[MILTER_LDAP_SERVER_URI] is mandatory!")
logging.error("ENV[MILTER_LDAP_SERVER_URI] is mandatory!")
sys.exit(1)
g_milter_ldap_server_uri = os.environ['MILTER_LDAP_SERVER_URI']
if 'MILTER_LDAP_RECEIVE_TIMEOUT' in os.environ:
try:
g_milter_ldap_receive_timeout = int(os.environ['MILTER_LDAP_RECEIVE_TIMEOUT'])
except ValueError:
log_error("ENV[MILTER_LDAP_RECEIVE_TIMEOUT] must be an integer!")
logging.error("ENV[MILTER_LDAP_RECEIVE_TIMEOUT] must be an integer!")
sys.exit(1)
log_info("ENV[MILTER_LDAP_RECEIVE_TIMEOUT]: {0}".format(
logging.info("ENV[MILTER_LDAP_RECEIVE_TIMEOUT]: {0}".format(
g_milter_ldap_receive_timeout
))
if 'MILTER_LDAP_BINDDN' not in os.environ:
log_info("ENV[MILTER_LDAP_BINDDN] not set! Continue...")
logging.info("ENV[MILTER_LDAP_BINDDN] not set! Continue...")
else:
g_milter_ldap_binddn = os.environ['MILTER_LDAP_BINDDN']
if 'MILTER_LDAP_BINDPW' not in os.environ:
log_info("ENV[MILTER_LDAP_BINDPW] not set! Continue...")
logging.info("ENV[MILTER_LDAP_BINDPW] not set! Continue...")
else:
g_milter_ldap_bindpw = os.environ['MILTER_LDAP_BINDPW']
if 'MILTER_LDAP_SEARCH_BASE' not in os.environ:
log_error("ENV[MILTER_LDAP_SEARCH_BASE] is mandatory!")
logging.error("ENV[MILTER_LDAP_SEARCH_BASE] is mandatory!")
sys.exit(1)
g_milter_ldap_search_base = os.environ['MILTER_LDAP_SEARCH_BASE']
if 'MILTER_LDAP_QUERY' not in os.environ:
log_error("ENV[MILTER_LDAP_QUERY] is mandatory!")
logging.error("ENV[MILTER_LDAP_QUERY] is mandatory!")
sys.exit(1)
g_milter_ldap_query = os.environ['MILTER_LDAP_QUERY']
if 'MILTER_LDAP_TENANT_ID_ATTR' in os.environ:
@ -648,35 +628,49 @@ if __name__ == "__main__":
if 'MILTER_LDAP_DKIM_ALIGNMENT_REQUIRED_ATTR' in os.environ:
g_milter_ldap_dkim_alignment_required_attr = os.environ['MILTER_LDAP_DKIM_ALIGNMENT_REQUIRED_ATTR']
try:
ldap_config = {
'ldap_server_uri': g_milter_ldap_server_uri,
'ldap_binddn': g_milter_ldap_binddn,
'ldap_bindpw': g_milter_ldap_bindpw,
'ldap_receive_timeout': g_milter_ldap_receive_timeout,
'ldap_search_base': g_milter_ldap_search_base,
'ldap_query': g_milter_ldap_query,
'ldap_tenant_id_attr': g_milter_ldap_tenant_id_attr,
'ldap_dkim_enabled_attr': g_milter_ldap_dkim_enabled_attr,
'ldap_dkim_alignment_required_attr': g_milter_ldap_dkim_alignment_required_attr
}
g_policy_backend = ExOTAPolicyBackendLDAP(ldap_config)
log_info("LDAP policy backend initialized")
except ExOTAPolicyException as e:
log_error("Policy backend error: {0}".format(e.message))
set_config_parameter("RESTARTABLE_SLEEPTIME", 1)
set_config_parameter("RESTARTABLE_TRIES", 2)
set_config_parameter('DEFAULT_SERVER_ENCODING', 'utf-8')
set_config_parameter('DEFAULT_CLIENT_ENCODING', 'utf-8')
server = Server(g_milter_ldap_server_uri, get_info=NONE)
g_milter_ldap_conn = Connection(server,
g_milter_ldap_binddn,
g_milter_ldap_bindpw,
auto_bind = True,
raise_exceptions = True,
client_strategy = 'RESTARTABLE',
receive_timeout = g_milter_ldap_receive_timeout
)
logging.info("LDAP-Connection established")
try:
g_policy_backend = ExOTAPolicyBackendLDAP({
'ldap_conn': g_milter_ldap_conn,
'ldap_search_base': g_milter_ldap_search_base,
'ldap_query': g_milter_ldap_query,
'ldap_tenant_id_attr': g_milter_ldap_tenant_id_attr,
'ldap_dkim_enabled_attr': g_milter_ldap_dkim_enabled_attr,
'ldap_dkim_alignment_required_attr': g_milter_ldap_dkim_alignment_required_attr
})
logging.info("LDAP policy backend initialized")
except ExOTAPolicyException as e:
logging.error("Policy backend error: {0}".format(e.message))
sys.exit(1)
except LDAPException as e:
print("LDAP-Exception: {0}".format(e))
sys.exit(1)
else:
log_debug("Unsupported backend: {0}!".format(g_milter_policy_source))
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
Milter.set_flags(Milter.ADDHDRS + Milter.CHGHDRS)
log_info("Startup " + g_milter_name +
logging.info("Startup " + g_milter_name +
"@socket: " + g_milter_socket
)
Milter.runmilter(g_milter_name,g_milter_socket,timeout,True)
log_info("Shutdown " + g_milter_name)
logging.info("Shutdown " + g_milter_name)
except:
log_error("MAIN-EXCEPTION: " + traceback.format_exc())
logging.error("MAIN-EXCEPTION: " + traceback.format_exc())
sys.exit(1)

View File

@ -1,31 +0,0 @@
import logging
import re
import os
def init_logger():
log_level = logging.INFO
if 'LOG_LEVEL' in os.environ:
if re.match(r'^info$', os.environ['LOG_LEVEL'], re.IGNORECASE):
log_level = logging.INFO
elif re.match(r'^warn|warning$', os.environ['LOG_LEVEL'], re.IGNORECASE):
log_level = logging.WARN
elif re.match(r'^error$', os.environ['LOG_LEVEL'], re.IGNORECASE):
log_level = logging.ERROR
elif re.match(r'debug', os.environ['LOG_LEVEL'], re.IGNORECASE):
log_level = logging.DEBUG
log_format = '%(asctime)s: %(levelname)s %(message)s '
logging.basicConfig(
filename = None, # log to stdout
format = log_format,
level = log_level
)
logging.info("Logger initialized")
def log_info(message):
logging.info(message)
def log_error(message):
logging.error(message)
def log_debug(message):
logging.debug(message)

View File

@ -3,11 +3,6 @@ import traceback
import re
from uuid import UUID
from ldap3.core.exceptions import LDAPException
from ldap3 import (
Server, Connection, NONE, set_config_parameter,
SAFE_RESTARTABLE
)
from logger import log_debug
class ExOTAPolicyException(Exception):
def __init__(self, message):
@ -25,10 +20,7 @@ class ExOTAPolicyBackendException(Exception):
class ExOTAPolicy():
def __init__(self, policy_dict):
if 'tenant_id' in policy_dict:
self.tenant_id = policy_dict['tenant_id']
else:
self.tenant_id = ''
self.tenant_id = policy_dict['tenant_id']
if 'dkim_enabled' in policy_dict:
self.dkim_enabled = policy_dict['dkim_enabled']
else:
@ -40,7 +32,9 @@ class ExOTAPolicy():
self.dkim_alignment_required = True
def __str__(self):
return str(self.__dict__)
return "Tenant-ID={0}, DKIM={1}, DKIM-alignment-required={2}". format(
self.tenant_id, self.dkim_enabled, self.dkim_alignment_required
)
def get_tenant_id(self):
return self.tenant_id
@ -53,11 +47,18 @@ class ExOTAPolicy():
@staticmethod
def check_policy(policy_dict):
if 'tenant_id' not in policy_dict:
raise ExOTAPolicyInvalidException(
"Policy must have a 'tenant_id' key!"
)
if policy_dict['tenant_id'] is None:
raise ExOTAPolicyInvalidException(
"'tenant_id' needs a value!"
)
for policy_key in policy_dict:
if policy_key == 'tenant_id':
try:
if policy_dict[policy_key] != '':
UUID(policy_dict[policy_key])
UUID(policy_dict[policy_key])
except ValueError as e:
raise ExOTAPolicyInvalidException(
"Invalid 'tenant_id': {0}".format(str(e))
@ -134,86 +135,48 @@ class ExOTAPolicyBackendJSON(ExOTAPolicyBackend):
class ExOTAPolicyBackendLDAP(ExOTAPolicyBackend):
type = 'ldap'
def __init__(self, ldap_config):
log_debug("init ldap_query: {0}".format(ldap_config['ldap_query']))
try:
self.ldap_server_uri = ldap_config['ldap_server_uri']
self.ldap_binddn = ldap_config['ldap_binddn']
self.ldap_bindpw = ldap_config['ldap_bindpw']
self.conn = ldap_config['ldap_conn']
self.search_base = ldap_config['ldap_search_base']
self.ldap_receive_timeout = ldap_config['ldap_receive_timeout']
self.query_template = ldap_config['ldap_query']
self.query = ldap_config['ldap_query']
self.tenant_id_attr = ldap_config['ldap_tenant_id_attr']
self.dkim_enabled_attr = ldap_config['ldap_dkim_enabled_attr']
self.dkim_alignment_required_attr = ldap_config['ldap_dkim_alignment_required_attr']
self.connect()
except Exception as e:
raise ExOTAPolicyBackendException(
"An error occured while initializing LDAP backend: " + traceback.format_exc()
) from e
def connect(self):
try:
set_config_parameter("RESTARTABLE_SLEEPTIME", 1)
set_config_parameter("RESTARTABLE_TRIES", 2)
set_config_parameter('DEFAULT_SERVER_ENCODING', 'utf-8')
set_config_parameter('DEFAULT_CLIENT_ENCODING', 'utf-8')
self.conn = Connection(
Server(self.ldap_server_uri, get_info=NONE),
self.ldap_binddn,
self.ldap_bindpw,
auto_bind = True,
raise_exceptions = True,
client_strategy = 'SAFE_RESTARTABLE',
receive_timeout = self.ldap_receive_timeout
)
except LDAPException as e:
raise ExOTAPolicyBackendException(
"An error occured while connecting to LDAP backend: " + traceback.format_exc()
) from e
def sanitize_connection(self):
pass
def get(self, from_domain):
self.sanitize_connection()
log_debug("LDAP FROM_DOMAIN: {0}".format(from_domain))
ldap_query = self.query_template
ldap_query = ldap_query.replace('%d', from_domain)
log_debug("LDAP-QUERY-Template: {0}".format(self.query_template))
log_debug("LDAP-QUERY: {0}".format(ldap_query))
self.query = self.query.replace('%d', from_domain)
try:
_, _, response, _ = self.conn.search(
self.conn.search(
self.search_base,
ldap_query,
attributes = [
self.query,
attributes=[
self.tenant_id_attr,
self.dkim_enabled_attr,
self.dkim_alignment_required_attr
]
)
log_debug("LDAP ENTRY: {0}".format(response))
if len(response) == 1:
entry = response[0]['attributes']
if len(self.conn.entries) == 1:
entry = self.conn.entries[0]
policy_dict = {}
if self.tenant_id_attr in entry:
if len(entry[self.tenant_id_attr]) > 0:
policy_dict['tenant_id'] = entry[self.tenant_id_attr][0]
else:
policy_dict['tenant_id'] = ''
policy_dict['tenant_id'] = entry[self.tenant_id_attr].value
if self.dkim_enabled_attr in entry:
if entry[self.dkim_enabled_attr][0] == 'TRUE':
if entry[self.dkim_enabled_attr].value == 'TRUE':
policy_dict['dkim_enabled'] = True
else:
policy_dict['dkim_enabled'] = False
if self.dkim_alignment_required_attr in entry:
if entry[self.dkim_alignment_required_attr][0] == 'TRUE':
if entry[self.dkim_alignment_required_attr].value == 'TRUE':
policy_dict['dkim_alignment_required'] = True
else:
policy_dict['dkim_alignment_required'] = False
log_debug("POLICY_DICT: {}".format(policy_dict))
ExOTAPolicy.check_policy(policy_dict)
return ExOTAPolicy(policy_dict)
elif len(response) > 1:
elif len(self.conn.entries) > 1:
raise ExOTAPolicyInvalidException(
"Multiple policies found for domain={0}!".format(from_domain)
)

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]))

View File

@ -33,49 +33,41 @@ end
--if mt.header(conn, "fRoM", '"Blah Blubb" <O365ConnectorValidation@yad.onmicrosoft.com>') ~= nil then
-- error "mt.header(From) failed"
--end
if mt.header(conn, "fRoM", '"Blah Blubb" <O365ConnectorValidation@staging.zwackl.de>') ~= nil then
if mt.header(conn, "fRoM", '"Blah Blubb" <O365ConnectorValidation@chillout2k.de>') ~= nil then
error "mt.header(From) failed"
end
if mt.header(conn, "resent-fRoM", '"Blah Blubb" <blah@yad.onmicrosoft.COM>') ~= nil then
error "mt.header(Resent-From) failed"
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(X-MS-Exchange-CrossTenant-Id) failed"
end
dkim_sig = "v=1; a=rsa-sha256; c=relaxed/simple; d=staging.zwackl.de;\n"
.."\ts=selector-xyz; t=1685872089;\n"
.."\tbh=5/ZUJAdcuyAn6J+J6apWtAaJLbDCKkI5Ie31qVKiY0w=;\n"
.."\th=Date:From:To:Subject:MIME-Version:Content-Type;\n"
.."\tb=Bn/xAbFFjAg1b9bBFPHAYSaupsnL4pzPPDUauetfGB0hu0Qz0Dio+4Z2Vi6PMOesA\n"
.."\t72VbehuxG+b++XVL/hs3+K6p7vTgVAWiWAZLvfs5bHE5HAalsCrNenpKTk6RUcSYtw\n"
.."\tLiiYhvw0TR5LbyNoSPG2J16mXEcS+k2q+K7WfwMg="
if mt.header(conn, "DKIM-Signature", dkim_sig) ~= nil then
error "mt.header(DKIM-Signature) failed"
error "mt.header(Subject) failed"
end
--if mt.header(conn, "X-MS-Exchange-CrossTenant-Id", "4321abcd-18c5-45e8-88de-blahblubb") ~= nil then
-- error "mt.header(Subject) failed"
--end
if mt.header(conn, "Authentication-Results", "another-wrong-auth-serv-id;\n dkim=fail header.d=yad.onmicrosoft.com header.s=selector1-yad-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
error "mt.header(Authentication-Results) failed"
error "mt.header(Subject) failed"
end
if mt.header(conn, "Authentication-Results", "wrong-auth-serv-id;\n dkim=pass header.d=yad.onmicrosoft.com header.s=selector1-yad-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
error "mt.header(Authentication-Results) failed"
error "mt.header(Subject) failed"
end
if mt.header(conn, "Authentication-Results", "my-auth-serv-id;\n exota=pass") ~= nil then
error "mt.header(Authentication-Results) failed"
error "mt.header(Subject) failed"
end
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(Authentication-Results) failed"
error "mt.header(Subject) failed"
end
if mt.header(conn, "Authentication-RESULTS", "my-auth-serv-id;\n dkim=pass header.d=staging.zwackl.de header.s=selector1-yad-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
error "mt.header(Authentication-Results) failed"
if mt.header(conn, "Authentication-RESULTS", "my-auth-serv-id;\n dkim=pass header.d=chillout2k.de 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
error "mt.header(Authentication-Results) failed"
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(Authentication-Results) failed"
error "mt.header(Subject) failed"
end
if mt.header(conn, "X-ExOTA-Authentication-Results", "my-auth-serv-id;\n exota=pass") ~= nil then
error "mt.header(X-ExOTA-Authentication-Results) failed"
error "mt.header(Subject) failed"
end
-- EOM

View File

@ -1,138 +0,0 @@
-- 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
if mt.conninfo(conn, "localhost", "::1") ~= nil then
error "mt.conninfo() failed"
end
mt.set_timeout(60)
-- 5321.FROM
if mt.mailfrom(conn, "envelope.sender@example.org") ~= 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", "4CgSNs5Q9sz7SllQ", '{cert_subject}', "mail.protection.outlook.comx")
if mt.rcptto(conn, "<envelope.recipient@example.com>") ~= 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@yad.onmicrosoft.com>') ~= nil then
-- error "mt.header(From) failed"
--end
if mt.header(conn, "fRoM", '"Blah Blubb" <O365ConnectorValidation@staging.zwackl.de>') ~= nil then
error "mt.header(From) failed"
end
if mt.header(conn, "resent-fRoM", '"Blah Blubb" <blah@yad.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, "X-MS-Exchange-CrossTenant-Id", "4321abcd-18c5-45e8-88de-blahblubb") ~= nil then
-- error "mt.header(Subject) failed"
--end
if mt.header(conn, "Authentication-Results", "another-wrong-auth-serv-id;\n dkim=fail header.d=yad.onmicrosoft.com header.s=selector1-yad-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=yad.onmicrosoft.com 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 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.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=pass header.d=staging.zwackl.de 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
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
if mt.header(conn, "X-ExOTA-Authentication-Results", "my-auth-serv-id;\n exota=pass") ~= 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-ExOTA-Authentication-Results") then
mt.echo("no header added")
else
mt.echo("X-ExOTA-Authentication-Results header added")
end
-- next message
-- 5321.FROM
if mt.mailfrom(conn, "envelope.sender2@example.org") ~= 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", "4CgSNs5Q9sz7Sll2", '{cert_subject}', "mail.protection.outlook.comx")
if mt.rcptto(conn, "<envelope.recipient2@example.com>") ~= 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@staging.zwackl.de>') ~= 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", "my-auth-serv-id;\n dkim=pass header.d=staging.zwackl.de header.s=selector1-yad-onmicrosoft-com header.b=mmmjFpv8") ~= 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-ExOTA-Authentication-Results") then
mt.echo("no header added")
else
mt.echo("X-ExOTA-Authentication-Results header added")
end
-- DISCONNECT
mt.disconnect(conn)

View File

@ -1,161 +0,0 @@
-- 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
if mt.conninfo(conn, "localhost", "::1") ~= nil then
error "mt.conninfo() failed"
end
mt.set_timeout(60)
-- FIRST MESSAGE (should fail due to dkim-fail)
-- 5321.FROM
if mt.mailfrom(conn, "envelope.sender@example.org") ~= 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", "Queue-ID-1", '{cert_subject}', "mail.protection.outlook.comx")
if mt.rcptto(conn, "<envelope.recipient@example.com>") ~= 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@staging.zwackl.de>') ~= nil then
error "mt.header(From) failed"
end
if mt.header(conn, "x-mS-EXCHANGE-crosstenant-id", "1234abcd-18c5-45e8-88de-123456789abcXXX") ~= nil then
error "mt.header(x-mS-EXCHANGE-crosstenant-id) failed"
end
if mt.header(conn, "Authentication-RESULTS", "my-auth-serv-id;\n dkim=fail header.d=staging.zwackl.de header.s=selector1-yad-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
error "mt.header(Authentication-RESULTS) failed"
end
if mt.header(conn, "X-ExOTA-Authentication-Results", "my-auth-serv-id;\n exota=pass") ~= nil then
error "mt.header(X-ExOTA-Authentication-Results) 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-ExOTA-Authentication-Results") then
mt.echo("no header added")
else
mt.echo("X-ExOTA-Authentication-Results header added")
end
-- SECOND MESSAGE (should pass)
-- 5321.FROM
if mt.mailfrom(conn, "envelope.sender2@example.org") ~= 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", "Queue-ID-2", '{cert_subject}', "mail.protection.outlook.comx")
if mt.rcptto(conn, "<envelope.recipient2@example.com>") ~= 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@staging.zwackl.de>') ~= 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(x-mS-EXCHANGE-crosstenant-id) failed"
end
if mt.header(conn, "Authentication-RESULTS", "my-auth-serv-id;\n dkim=pass header.d=staging.zwackl.de header.s=selector1-yad-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
error "mt.header(Authentication-RESULTS) 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-ExOTA-Authentication-Results") then
mt.echo("no header added")
else
mt.echo("X-ExOTA-Authentication-Results header added")
end
-- THIRD MESSAGE (should fail due to dkim-fail)
-- 5321.FROM
if mt.mailfrom(conn, "envelope.sender@example.org") ~= 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", "Queue-ID-3", '{cert_subject}', "mail.protection.outlook.comx")
if mt.rcptto(conn, "<envelope.recipient@example.com>") ~= 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@staging.zwackl.de>') ~= nil then
error "mt.header(From) failed"
end
if mt.header(conn, "x-mS-EXCHANGE-crosstenant-id", "1234abcd-18c5-45e8-88de-123456789abcXXX") ~= nil then
error "mt.header(x-mS-EXCHANGE-crosstenant-id) failed"
end
if mt.header(conn, "Authentication-RESULTS", "my-auth-serv-id;\n dkim=fail header.d=staging.zwackl.de header.s=selector1-yad-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
error "mt.header(Authentication-RESULTS) failed"
end
if mt.header(conn, "X-ExOTA-Authentication-Results", "my-auth-serv-id;\n exota=pass") ~= nil then
error "mt.header(X-ExOTA-Authentication-Results) 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-ExOTA-Authentication-Results") then
mt.echo("no header added")
else
mt.echo("X-ExOTA-Authentication-Results header added")
end
-- DISCONNECT
mt.disconnect(conn)

View File

@ -1,64 +0,0 @@
-- 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
if mt.conninfo(conn, "localhost", "::1") ~= nil then
error "mt.conninfo() failed"
end
mt.set_timeout(60)
-- 5321.FROM
if mt.mailfrom(conn, "envelope.sender@example.org") ~= 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", "4CgSNs5Q9sz7FAIL", '{cert_subject}', "mail.protection.outlook.comx")
if mt.rcptto(conn, "<envelope.recipient@example.com>") ~= 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@staging.zwacklx.de>') ~= 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", "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
if mt.header(conn, "X-ExOTA-Authentication-Results", "my-auth-serv-id;\n exota=pass") ~= 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-ExOTA-Authentication-Results") then
mt.echo("no header added")
else
mt.echo("X-ExOTA-Authentication-Results header added")
end
-- DISCONNECT
mt.disconnect(conn)

View File

@ -1,70 +0,0 @@
-- 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
if mt.conninfo(conn, "localhost", "::1") ~= nil then
error "mt.conninfo() failed"
end
mt.set_timeout(60)
-- 5321.FROM
if mt.mailfrom(conn, "envelope.sender@example.org") ~= 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", "4CgSNs5Q9sz7SllQ", '{cert_subject}', "mail.protection.outlook.comx")
if mt.rcptto(conn, "<envelope.recipient@example.com>") ~= 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@staging.zwackl.de>') ~= 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(tenant-id pass) failed"
end
if mt.header(conn, "x-mS-EXCHANGE-crosstenant-id", "1234abcd-18c5-45e8-88de-123456789abc") ~= nil then
error "mt.header(tenant-id pass) failed"
end
if mt.header(conn, "X-MS-Exchange-CrossTenant-Id", "4321abcd-18c5-45e8-88de-blahblubb") ~= nil then
error "mt.header(tenant-id fail) failed"
end
if mt.header(conn, "Authentication-RESULTS", "my-auth-serv-id;\n dkim=pass header.d=staging.zwackl.de header.s=selector1-yad-onmicrosoft-com header.b=mmmjFpv8") ~= nil then
error "mt.header(DKIM-AR) failed"
end
if mt.header(conn, "X-ExOTA-Authentication-Results", "my-auth-serv-id;\n exota=pass") ~= nil then
error "mt.header(Exota-AR) 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-ExOTA-Authentication-Results") then
mt.echo("no header added")
else
mt.echo("X-ExOTA-Authentication-Results header added")
end
-- DISCONNECT
mt.disconnect(conn)

View File

@ -1,82 +0,0 @@
-- 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
if mt.conninfo(conn, "localhost", "::1") ~= nil then
error "mt.conninfo() failed"
end
mt.set_timeout(60)
-- 5321.FROM
if mt.mailfrom(conn, "envelope.sender@example.org") ~= 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", "4CgSNs5Q9sz7SllQ", '{cert_subject}', "mail.protection.outlook.comx")
if mt.rcptto(conn, "<envelope.recipient@example.com>") ~= 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@staging.zwackl.de>') ~= nil then
error "mt.header(From) failed"
end
if mt.header(conn, "resent-fRoM", '"Blah Blubb" <blah@yad.onmicrosoft.COM>') ~= nil then
error "mt.header(From) failed"
end
if mt.header(conn, "Authentication-Results", "another-wrong-auth-serv-id;\n dkim=fail header.d=yad.onmicrosoft.com header.s=selector1-yad-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=yad.onmicrosoft.com 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 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.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=pass header.d=staging.zwackl.de 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
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
if mt.header(conn, "X-ExOTA-Authentication-Results", "my-auth-serv-id;\n exota=pass") ~= 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-ExOTA-Authentication-Results") then
mt.echo("no header added")
else
mt.echo("X-ExOTA-Authentication-Results header added")
end
-- DISCONNECT
mt.disconnect(conn)

View File

@ -7,10 +7,5 @@
"example.com": {
"tenant_id": "abcd1234-18c5-45e8-88de-987654321cba",
"dkim_enabled": false
},
"staging.zwackl.de": {
"tenant_id": "",
"dkim_enabled": true,
"dkim_alignment_required": true
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

View File

@ -1 +0,0 @@
<mxfile host="app.diagrams.net" modified="2022-06-06T13:43:35.273Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.53" etag="VvPNUPgHryv2Mcg-PFl7" version="19.0.0" type="device"><diagram id="JJzjw4aGYAxIZ4NaUIE7" name="Page-1">7Vxtc5s4EP41nul9iMdIgOFjnJdeesklc+ldm37JYJBtXTBikJzY/fUngYRBwrGTgJ1em8wk1iIE7D67z+6ipAdP5suPWZDOrkiE4h4YRMsePO0B4HsW/ykEq0Lg2F4hmGY4KkTWWnCLvyMpHEjpAkeI1iYyQmKG07owJEmCQlaTBVlGnurTJiSuXzUNpsgQ3IZBbEq/4IjNpNRy/fWB3xGezuSlPTAsDswDNVk+CZ0FEXmqiOBZD55khLDi03x5gmKhO6WX4rzzDUfLG8tQwnY5YeUsk+TbRfTXn99nR9eA3YWfj4+Umh+DeCGf+CJhKEsQ6wE3mKc9OErGVPzqifV/id4mcmNuq9E445+mLJfk0GArhTduMw5tPhhxvKRCGMZkwU04epphhm7TIBTCJ+5sXDZj85iPrHKlKhwkQh5RxtCyIpLw+IjIHLFsxafIow4ozpCuClwJjqc18KFy31kF85avHE4627Rceo1H/kFC8gXwBAY6r3CYEUomHJ4D6DrC83P9NGj2/Uveke2twaBufNP29nBg2h7Yg45s7xu2NxSm1ISWQp8RWYzzI0IlGVkkEYrkqHv9Adup6c839ccdrMF3gN2R/mzbBFjEuU0OE5Kgmp4GfEQyNiNTkgTxJSGp1M6/iLGVZOZgwUhdd1xl2eprdXAnFus7ani6lIsXo5UcURZk7FgwdI7zgFIcKvE5jreahpJFFqLnqE3ihS84Rew5RcmQJrTzrKkzFAcMP9Yzg9btpu77WeA/IBbOpCZTggVlnz1yFVGptjLZEBOigM5KE1dMR1lGHtAJiUm2BsSE616JegA6A/HN5XEwRvENoZhhkgiTIXFRfkD4COYJ06U2YUwYI/PKhOMYT8UBJpA1IgsW44RfXeVt4iKBnFIurjx8vpyK9LJPJhMcon6EHvkv2o+DtFitBQ924bDmwRbwDRd2GtgPduXAFtwOhICmhfYmeClMPEpRhvnlhfJOJZ/crEVV6zfpegNmKI+cOJl+zkMC5AI8z9Nm9fsUz6f8EWM85j+DUPjIfYQzfmdE6OE8jBeUX+Oeoozbo08fpy0FXatOWn6DxRo4y+mKsiznh+Is6Np1zm8gLQe4DaTvwa4SPne7Bqsstiass7W0RVIreWs3UkNLzEou5J8rZ/HR+iQxUOe8geGGOzKc5bfNcPmpnL2DVWVCTkS0svKNEFTqC1jHGxxaVcBsnW9DTwNYcQdruJWP8gYE7pB3dppHvRBye4APeGcJ0tCw0FWA44JczDLrA+pP+0JFhDLOkr8Z1tx72PU1N7AtI+xaoIG33K54C+zAW11gvvsawN8V4uB9QdwMQmfLa37gCsesEeRcuTOuHp5ni/T7jSDXaoAoQN4kNAoGfsQNPTSetNR9cuvJnD3wDu0WZiV2Q2Icrkz9j4PwASUNbagPl6fHN/zqn26v/zStUvZ5VrwWilAGt9tmXBjyclwK+KWnuXmvi5JKOVfhdZZzKIMOtewc+tAwqNdgTx90ZE/bP2wuuebyuxqVb8glxahSu+2na6LaUT9axFT3vYOvHnMTzfMwKTLImwsDFPspqickYRXPs8FwOBqpRsuodOrKlEn+JU+VeLPAS+tzPJ6LKj1NY0kWVAxLndyL12u0vRrdU6eoKOCY2Y7dEAU66yurbLaClNM/Lq6Kh8IT/FwOeZ2iREw+fBKps+Xhk0hovkq849GGSxZU6PQ8XFAOMvHxVzez1s0U+qH9Oa9h7nNVtcS+sAYQS/WVt7QzHb8rfJhFRpAQnrUKiIjiTXR0N7teyGMe18YRDQMet08E8yRhtkqLOC7G/X7//fmlPXQP7ZdmuXzYNOhHaKm13CkzW1uW1tqytaS3uFN51nM9Mg1vzkDz3yKRMxbad+9O3VenvTtoUjvMogIKTDwtEkE2DzfhIsO5SEYeMSkjjzjKw9GHVcFcIZmnQcJv6dyMLKzo4L+EgaTI4AWdU+Y4inKva4pXdU/MU0F5Uw1bgl4cv2xtE4hjm0UbhA3vBGBX8cs2eYOzbV4Gm2wht4ERM8nYe5fPqyvSck0iaNpQ0VneC81uxi1fWlxK0bCuzbW7mD6yiac/inMaOh0/eY5XdKdpn8ej+zwa3ef5nmxat7OFZ6hxiuoi1vbw7DHjs83M42wZzoJkKqBEkrxZ9T7DagvmABozN3S/yk1X+wmkJjtKB9d9mddpmWgcHDqG6iq0wIFjqGPWuH/nxe0g5SlxEB9cY65f1xhQMeFg5Ydq9xyo2uhmV5oqStaFSNnR7aAo2bknq+DZdfXiQas/4F/AHwLXgV690wA9fhTatgeGjueCwbC+/K6VzdB4HwTzi6ov7a431DltlRYqLd7bLoCXvSx463uBdwOtoaOFfH/wOvhALRJa+q6vrgFj7t5bly2DDcTLskCki//fpMiGGj/ZDfy016zIMV8c4eRnN5OlpxF2Qxdzv2bapYuZRGYIrBijrqZXv5dt5H6VFbTN/ar7sX2P3546l9DWM0xtL+iuAdretlDXAdoshwxElcU8orR4o1qBU2M/wLT083De7pLVvyBp8DjVSH6jWY+0zRpHWtZGJhOKurHDLhsuW/Fsa4tnv95LVW/tAH9rssG5QP2PGYDlvtJLtT3i5ev7PXmpa1bce/RSBc1fXupaO9ih2uNt1Lu254V/n4t7GE2zIMJofUxmRcb2tPIEredb7oypNY5f2ec1crHqzkht50319UveDta3Dunt4OCJ2v0MFe53EYr7GfFh8UmbtcgTvhbSOaj30WyzM6xCWC2bA2/P5pKRd5Syh6/frq2nxe23x3/ST1+OdgDTC0P+K5Ky7W2oxljd8Wb7Quftp23a6yjb9V5JCNsWaumFs+3Uicd2av/e4M0vkBtxCbojG8Nb6zja7CYH4xpYN/SRZudWuKbxsc3OCc33Vp9nKETigcR/18AxNWzznurvLfbesotI74I1bN5y2ym/hVrL/0FSWHD9j1zg2X8=</diagram></mxfile>