Upd: Add new integration and unit tests

Test a few more functionalities in the image. Change how postfix
message IDs are detected. Message ID can be set by the client and
hence detecting them via regex is not the best way to go around it.

This fix will actually look at the log line and try to determine
if we're looking at the message ID or not.
This commit is contained in:
Bojan Čekrlić 2022-04-10 17:45:54 +02:00
parent 81a600db10
commit c7c56d3ff1
4 changed files with 148 additions and 13 deletions

View file

@ -0,0 +1,53 @@
version: '3.7'
services:
postfix_test_587:
hostname: "postfix"
image: "boky/postfix"
build:
context: ../..
restart: always
healthcheck:
test: [ "CMD", "sh", "-c", "netstat -an | fgrep 587 | fgrep -q LISTEN" ]
interval: 10s
timeout: 5s
start_period: 10s
retries: 2
environment:
FORCE_COLOR: "1"
ANONYMIZE_EMAILS: smart
ALLOWED_SENDER_DOMAINS: "mydomain.example.com"
MASQUERADED_DOMAINS: "mydomain.example.com"
RELAYHOST: "smtp.gmail.com:587"
RELAYHOST_USERNAME: "test@gmail.com"
RELAYHOST_PASSWORD: "test12345"
POSTFIX_myhostname: "mailinator"
POSTFIX_mydomain: "target.example.com"
POSTFIX_mydestination: "mailinator localhost.target.example.com localhost target.example.com"
POSTFIX_luser_relay: "root+@target.example.com"
POSTFIX_smtpd_end_of_data_restrictions: "check_client_access static:discard"
POSTFIX_smtp_sender_dependent_authentication: "yes"
POSTFIX_smtp_sasl_auth_enable: "yes"
POSTFIX_smtp_sasl_security_options: "noanonymous"
POSTFIX_smtp_sasl_mechanism_filter: "login, plain, digest-md5"
POSTFIX_smtpd_relay_restrictions: "permit_sasl_authenticated, reject_unauth_destination"
POSTFIX_smtpd_sasl_auth_enable: "yes"
POSTFIX_smtpd_sasl_authenticated_header: "yes"
POSTFIX_smtpd_sasl_security_options: "noanonymous"
POSTFIX_smtpd_sasl_local_domain: "$myhostname"
POSTFIX_broken_sasl_auth_clients: "yes"
POSTFIX_smtpd_client_restrictions: ""
POSTFIX_message_size_limit: "40960000"
LOG_FORMAT: "json"
tests:
image: "boky/postfix-integration-test"
restart: "no"
volumes:
- "../tester:/code"
build:
context: ../tester
command: "/" # relative path to /code
environment:
FROM: "demo@mydomain.example.com"
TO: "root@target.example.com"
SKIP_INVALID_DOMAIN_SEND: "1"

View file

@ -1,7 +1,12 @@
#!/usr/bin/env bats
if [[ "$#" -gt 0 ]]; then
FROM=$1
fi
if [[ "$#" -gt 1 ]]; then
TO=$2
fi
if [ -z "$FROM" ]; then
FROM="demo@example.org"

View file

@ -37,11 +37,6 @@ EMAIL_CATCH_ALL_PATTERN = '([^ "\\[\\]<]+|".+")@(\[([0-9]+\.[0-9]+\.[0-9]+\.[0-9
EMAIL_CATCH_ALL = re.compile(EMAIL_CATCH_ALL_PATTERN)
EMPTY_RESPONSE = json.dumps({})
# Postfix formats message IDs like this: 20211207101128.0805BA272@31bfa77a2cab
# Let's not mask them.
MESSAGE_ID_PATTERN = '[0-9]+\.[0-9A-F]+@[0-9a-f]+'
MESSAGE_ID = re.compile(MESSAGE_ID_PATTERN)
"""A default filter, if none other is provided."""
DEFAULT_FILTER_CLASS: str = 'SmartFilter'
@ -80,15 +75,46 @@ def is_truthy(val: any, name: str) -> bool:
Abstract base for all filters. Does nothing.
"""
class Filter():
MESSAGE_ID_LINE = "message-id="
MESSAGE_ID_LINE_LEN = len(MESSAGE_ID_LINE)
def init(self, args: 'dict[str, list[str]]') -> None:
pass
def replace(self, match: re.match) -> str:
def is_message_id(self, match: re.match, msg: str) -> bool:
start = match.start()
email = match.group()
# Note that our regex will match thigs like "message-id=Issue1649523226559@postfix-mail.mail-system.svc.cluster.local"
# so we need to filter / check for these first
if email.startswith(self.MESSAGE_ID_LINE):
return True
if start >= self.MESSAGE_ID_LINE_LEN:
pos = start-1
while True:
char = msg[pos]
if char == '=':
break
elif char in '{<["\'':
pos = pos - 1
continue
return False
check = msg[pos-self.MESSAGE_ID_LINE_LEN+1:pos+1]
if check == self.MESSAGE_ID_LINE:
return True
return False
def replace(self, match: re.match, msg: str) -> str:
return match.group()
def processMessage(self, msg: str) -> typing.Optional[str]:
result = EMAIL_CATCH_ALL.sub(
lambda x: self.replace(x), msg
lambda x: self.replace(x, msg), msg
)
return json.dumps({'msg': result}, ensure_ascii=False) if result != msg else EMPTY_RESPONSE
@ -144,11 +170,11 @@ class SmartFilter(Filter):
else: # Local domain
return len(domain) * self.mask_symbol
def replace(self, match: re.match) -> str:
def replace(self, match: re.match, msg: str) -> str:
email = match.group()
# Return the details unchanged if they look like Postfix message ID
if bool(MESSAGE_ID.match(email)):
if self.is_message_id(match, msg):
return email
# The "@" can show up in the local part, but shouldn't appear in the
@ -237,11 +263,11 @@ class HashFilter(Filter):
super().init(args)
def replace(self, match: re.match) -> str:
def replace(self, match: re.match, msg: str) -> str:
email = match.group()
# Return the details unchanged if they look like Postfix message ID
if bool(MESSAGE_ID.match(email)):
if self.is_message_id(match, msg):
return email
if not self.case_sensitive:

View file

@ -0,0 +1,51 @@
#!/usr/bin/env bats
mapfile EMAILS <<'EOF'
prettyandsimple@example.com
9453312: message-id=<1649523226559@test.example-org>
2F92E13: message-id=<8b38c47e1dd21675a07cf3bb674db074@example.com>
62D2E12: to=<demo@example.com>, relay=smtp.sendgrid.net[54.228.39.88]:587, delay=0.94, delays=0.1\/0.11\/0.61\/0.12, dsn=2.0.0, status=sent (250 Ok: queued as 5wukd4NoS6GaNrC3ggB83A)
77FCF13: message-id=<Issue1649425486405@postfix-mail.mail-system.svc.cluster.local> from=<test1@demo1.example.com> to=<test2@demo2.example.com>
77FCF13: message-id=Issue1649425486405@postfix-mail.mail-system.svc.cluster.local from=test1@demo1.example.com to=test2@demo2.example.com
9453312: message-id=<Issue1649523226559@postfix-mail.mail-system.svc.cluster.local>
9453312: message-id="Issue1649523226559@postfix-mail.mail-system.svc.cluster.local"
message-id=<Issue1649523226559@postfix-mail.mail-system.svc.cluster.local>
message-id=Issue1649523226559@postfix-mail.mail-system.svc.cluster.local
message-id=Issue1649523226559@postfix-mail.mail-system.svc.cluster.local
9453312: message-id='Issue1649523226559@postfix-mail.mail-system.svc.cluster.local'
9453312: message-id=Issue1649523226559@postfix-mail.mail-system.svc.cluster.local
EOF
mapfile EXPECTED <<'EOF'
{"msg": "*@*.com"}
{}
{}
{"msg": "62D2E12: to=<*@*.com>, relay=smtp.sendgrid.net[54.228.39.88]:587, delay=0.94, delays=0.1\\/0.11\\/0.61\\/0.12, dsn=2.0.0, status=sent (250 Ok: queued as 5wukd4NoS6GaNrC3ggB83A)"}
{"msg": "77FCF13: message-id=<Issue1649425486405@postfix-mail.mail-system.svc.cluster.local> from=<*@*.com> to=<*@*.com>"}
{"msg": "77FCF13: message-id=Issue1649425486405@postfix-mail.mail-system.svc.cluster.local *@*.com *@*.com"}
{}
{}
{}
{}
{}
{}
{}
EOF
@test "verify email anonymizer regex" {
local email
for index in "${!EMAILS[@]}"; do
email="${EMAILS[$index]}"
email=${email%$'\n'} # Remove trailing new line
result="$(echo "$email" | /code/scripts/email-anonymizer.sh paranoid)"
result=${result%$'\n'} # Remove trailing new line
expected="${EXPECTED[$index]}"
expected=${expected%$'\n'} # Remove trailing new line
if [ "$result" != "$expected" ]; then
echo "Expected '$expected', got: '$result'" >&2
exit 1
fi
done
}