Page MenuHomePhorge

D5271.1775253169.diff
No OneTemporary

Authored By
Unknown
Size
44 KB
Referenced Files
None
Subscribers
None

D5271.1775253169.diff

diff --git a/bin/podman_shared b/bin/podman_shared
--- a/bin/podman_shared
+++ b/bin/podman_shared
@@ -334,7 +334,7 @@
podman__run amavis \
-e APP_DOMAIN \
-e POSTFIX_HOST=localhost \
- -e DB_HOST=localhost \
+ -e DB_HOST=127.0.0.1 \
-e DB_USERNAME \
-e DB_PASSWORD \
-e DB_DATABASE \
diff --git a/ci/testctl b/ci/testctl
--- a/ci/testctl
+++ b/ci/testctl
@@ -444,10 +444,21 @@
fi
}
+kolab__policytest() {
+ POD=${POD:-dev}
+ OUTPUT=$($PODMAN exec $POD-postfix bash -c 'echo -e "request=smtpd_access_policy\nsender=admin@kolab.org\nrecipient=admin@kolab.org\ninstance=testinstance\nprotocol_state=DATA\n\n" | /usr/libexec/postfix/kolab_policy_submission')
+ if [[ $OUTPUT == "action=DUNNO" ]]; then
+ echo "Success"
+ else
+ echo "Fail: $OUTPUT"
+ exit 1
+ fi
+}
+
kolab__mailtransporttest() {
POD=${POD:-dev}
- if $PODMAN run --rm -ti --pod=$POD kolab-utils:latest ./mailtransporttest.py --timeout 1 --sender-username admin@kolab.local --sender-password simple123 --sender-host localhost --sender-port 6465 --recipient-username noreply@kolab.local --recipient-password simple123 --recipient-host localhost --recipient-port 6993 --validate; then
+ if $PODMAN run --rm -ti --pod=$POD kolab-utils:latest ./mailtransporttest.py --timeout 1 --sender-username admin@kolab.local --sender-password simple123 --sender-host localhost --sender-port 6465 --recipient-username admin@kolab.local --recipient-password simple123 --recipient-host localhost --recipient-port 6993 --validate; then
echo "Success"
else
exit 1
diff --git a/docker/postfix/rootfs/etc/postfix/main.cf b/docker/postfix/rootfs/etc/postfix/main.cf
--- a/docker/postfix/rootfs/etc/postfix/main.cf
+++ b/docker/postfix/rootfs/etc/postfix/main.cf
@@ -519,7 +519,7 @@
#permit_mynetworks,
reject_invalid_helo_hostname,
reject_non_fqdn_helo_hostname,
- #reject_rhsbl_helo dbl.spamhaus.org
+ # reject_rhsbl_helo dbl.spamhaus.org
# Local clients and authenticated clients may specify any destination domain
smtpd_relay_restrictions =
permit_mynetworks,
@@ -532,10 +532,10 @@
reject_non_fqdn_recipient,
reject_unauth_destination,
#reject_rhsbl_recipient dbl.spamhaus.org,
+ # TODO block suspended recipients (by domain or account)
#check_recipient_access ldap:/etc/postfix/ldap/domain_suspended.cf,
#check_recipient_access ldap:/etc/postfix/ldap/account_suspended.cf,
- #FIXME greylisting doesn't currently work because postfix doesn't see the client IP.
- #check_policy_service unix:private/policy_greylist,
+ check_policy_service unix:private/policy_greylist,
permit
smtpd_peername_lookup = yes
smtpd_sasl_auth_enable = yes
@@ -545,6 +545,7 @@
smtpd_sender_restrictions =
#permit_mynetworks,
+ # We used to also block spammers via sender_access
check_sender_access hash:/etc/postfix/sender_access,
reject_unknown_sender_domain,
#check_client_access hash:/etc/postfix/client_access,
@@ -553,16 +554,17 @@
# Uses smtpd_sender_login_maps
reject_unauthenticated_sender_login_mismatch,
reject_unauth_destination,
- #FIXME SPF doesn't currently work because postfix doesn't see the client IP.
- #check_policy_service unix:private/policy_spf,
+ check_policy_service unix:private/policy_spf,
#reject_rhsbl_sender dbl.spamhaus.org,
permit
# Outbound
submission_data_restrictions =
+ # Final hook for rate-limit request and decision
+ # to allow/deny sender access.
+ # We use this in place of reject_sender_login_mismatch to support e.g. delegation.
check_policy_service unix:private/policy_submission
- check_policy_service unix:private/policy_ratelimit
submission_client_restrictions =
#reject_unknown_reverse_client_hostname,
#reject_rbl_client zen.spamhaus.org,
@@ -571,13 +573,10 @@
reject_non_fqdn_sender,
# We used to also block spammers via sender_access
check_sender_access hash:/etc/postfix/sender_access,
- # Uses smtpd_sender_login_maps
- reject_sender_login_mismatch,
- check_policy_service unix:private/policy_ratelimit
permit_sasl_authenticated,
reject
submission_helo_restrictions =
-# permit_mynetworks,
+ #permit_mynetworks,
reject_invalid_helo_hostname,
reject_non_fqdn_helo_hostname,
#reject_rhsbl_helo dbl.spamhaus.org
@@ -586,27 +585,19 @@
permit_sasl_authenticated,
reject_unauth_destination
submission_recipient_restrictions =
- # check_recipient_access hash:/etc/postfix/recipient_access,
- # check_recipient_mx_access hash:/etc/postfix/recipient_mx_access,
- # check_recipient_ns_access hash:/etc/postfix/recipient_ns_access,
- check_policy_service unix:private/policy_ratelimit,
+ # FIXME These were lists of spammers to block by recipient, recipient mx and recipient domain
+ # check_recipient_access hash:/etc/postfix/recipient_access,
+ # check_recipient_mx_access hash:/etc/postfix/recipient_mx_access,
+ # check_recipient_ns_access hash:/etc/postfix/recipient_ns_access,
+ # Hook to collect recipients
+ check_policy_service unix:private/policy_submission,
permit_sasl_authenticated,
- # permit_mynetworks,
+ # permit_mynetworks,
reject
-# LEGACY restrictions
-#smtpd_data_restrictions = permit_mynetworks, check_policy_service unix:private/recipient_policy_incoming
-#smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_pipelining, reject_rbl_client zen.spamhaus.org, reject_non_fqdn_recipient, reject_invalid_helo_hostname, reject_unknown_recipient_domain, reject_unauth_destination, check_policy_service unix:private/recipient_policy_incoming, permit
-#smtpd_sender_restrictions = permit_mynetworks, reject_sender_login_mismatch, check_policy_service unix:private/sender_policy_incoming
-
-#submission_recipient_restrictions = check_policy_service unix:private/submission_policy, permit_sasl_authenticated, reject
-#submission_sender_restrictions = reject_non_fqdn_sender, check_policy_service unix:private/submission_policy, permit_sasl_authenticated, reject
-#submission_data_restrictions = check_policy_service unix:private/submission_policy
-
# Disable BDAT support without the useless "discarding EHLO keywords: CHUNKING" message
smtpd_discard_ehlo_keywords = chunking, silent-discard
-#content_filter = smtp-wallace:[127.0.0.1]:10026
content_filter = smtp-amavis:[AMAVIS_HOST]:13024
maillog_file = /dev/stdout
diff --git a/docker/postfix/rootfs/etc/postfix/master.cf b/docker/postfix/rootfs/etc/postfix/master.cf
--- a/docker/postfix/rootfs/etc/postfix/master.cf
+++ b/docker/postfix/rootfs/etc/postfix/master.cf
@@ -11,6 +11,7 @@
10025 inet n - n - - smtpd
-o content_filter=smtp-amavis:[AMAVIS_HOST]:13024
-o cleanup_service_name=cleanup_inbound
+
# Internal Submission, no tls, no starttls
10587 inet n - - - - smtpd
-o syslog_name=postfix/submission
@@ -102,7 +103,6 @@
-o disable_dns_lookups=yes
-o smtp_send_xforward_command=yes
-o max_use=20
- # -o smtp_bind_address=127.0.0.1
# Listener to re-inject email from Amavisd into Postfix
0.0.0.0:13025 inet n - n - 100 smtpd
@@ -119,73 +119,20 @@
-o smtpd_authorized_xforward_hosts=MYNETWORKS
-o syslog_name=postfix/amavis
-# Filter email through Wallace
-# smtp-wallace unix - - n - 3 smtp
-# -o default_destination_recipient_limit=1
-# -o smtp_data_done_timeout=1800
-# -o disable_dns_lookups=yes
-# -o smtp_send_xforward_command=yes
-# -o max_use=20
-
-# Listener to re-inject email from Wallace into Postfix
-# 127.0.0.1:10028 inet n - n - 100 smtpd
-# -o cleanup_service_name=cleanup_internal
-# -o content_filter=
-# -o local_recipient_maps=
-# -o relay_recipient_maps=
-# -o smtpd_restriction_classes=
-# -o smtpd_client_restrictions=
-# -o smtpd_helo_restrictions=
-# -o smtpd_sender_restrictions=
-# -o smtpd_recipient_restrictions=permit_mynetworks,reject
-# -o mynetworks=127.0.0.0/8
-# -o smtpd_authorized_xforward_hosts=127.0.0.0/8
-
-# Filter email through Amavisd
-# amavis unix - - n - 3 smtp
-# -o disable_dns_lookups=yes
-# -o max_use=20
-# -o smtp_bind_address=127.0.0.1
-# -o smtp_data_done_timeout=1800
-# -o smtp_send_xforward_command=yes
-# -o smtp_tls_security_level=none
-
-# Reinjection from amavis
-# 127.0.0.1:10025 inet n - n - 100 smtpd
-# -o cleanup_service_name=cleanup_outbound
-# -o content_filter=
-# -o local_recipient_maps=
-# -o mydestination=
-# -o mynetworks=127.0.0.0/8
-# -o relay_domains=
-# -o relay_recipient_maps=
-# -o smtpd_authorized_xforward_hosts=127.0.0.0/8
-# -o smtpd_restriction_classes=
-# -o smtpd_client_restrictions=
-# -o smtpd_data_restrictions=
-# -o smtpd_helo_restrictions=
-# -o smtpd_sender_restrictions=
-# -o smtpd_recipient_restrictions=permit_mynetworks,reject
-# -o syslog_name=postfix/amavis
-
-# Outbound
-policy_ratelimit unix - n n - - spawn
- user=nobody argv=/usr/libexec/postfix/kolab_policy_ratelimit
-
# Outbound
policy_submission unix - n n - - spawn
user=nobody argv=/usr/libexec/postfix/kolab_policy_submission
# Inbound
policy_greylist unix - n n - - spawn
- user=nobody argv=/usr/libexec/postfix/kolab_policy_greylist
+ user=nobody argv=/usr/libexec/postfix/kolab_policy greylist /api/webhooks/policy/greylist
# Inbound
policy_spf unix - n n - - spawn
- user=nobody argv=/usr/libexec/postfix/kolab_policy_spf
+ user=nobody argv=/usr/libexec/postfix/kolab_policy spf /api/webhooks/policy/spf
# Mailfilter via commandline, to be reinjected via sendmail.
policy_mailfilter unix - n n - 10 pipe
flags=Rq user=nobody null_sender=
- argv=/usr/libexec/postfix/kolab_contentfilter_cli.py -f ${sender} -- ${recipient}
+ argv=/usr/libexec/postfix/kolab_contentfilter_cli -f ${sender} -- ${recipient}
diff --git a/docker/postfix/rootfs/init.sh b/docker/postfix/rootfs/init.sh
--- a/docker/postfix/rootfs/init.sh
+++ b/docker/postfix/rootfs/init.sh
@@ -8,6 +8,13 @@
chmod 655 /etc/pki/tls/private/postfix.pem
fi
+mkdir /var/log/kolab
+touch /var/log/kolab/postfix-policy-submission.log
+touch /var/log/kolab/postfix-policy-spf.log
+touch /var/log/kolab/postfix-policy-greylist.log
+touch /var/log/kolab/postfix-content-filter.log
+chmod -R 777 /var/log/kolab
+
chown -R postfix:mail /var/lib/postfix
chown -R postfix:mail /var/spool/postfix
/usr/sbin/postfix set-permissions
diff --git a/docker/postfix/rootfs/usr/libexec/postfix/kolab_contentfilter_cli.py b/docker/postfix/rootfs/usr/libexec/postfix/kolab_contentfilter_cli
rename from docker/postfix/rootfs/usr/libexec/postfix/kolab_contentfilter_cli.py
rename to docker/postfix/rootfs/usr/libexec/postfix/kolab_contentfilter_cli
--- a/docker/postfix/rootfs/usr/libexec/postfix/kolab_contentfilter_cli.py
+++ b/docker/postfix/rootfs/usr/libexec/postfix/kolab_contentfilter_cli
@@ -1,5 +1,5 @@
#!/usr/bin/python3
-# Test like so: cat test.mime | /usr/libexec/postfix/kolab_contentfilter_cli.py -f admin@kolab.local -- noreply@kolab.local
+# Test like so: cat test.mime | /usr/libexec/postfix/kolab_contentfilter_cli -f admin@kolab.local -- noreply@kolab.local
import sys
import logging
import requests
@@ -10,19 +10,16 @@
EX_UNAVAILABLE = 69
# Postfix will retry via deferred queue
EX_TEMPFAIL = 75
-verbose = True
def get_args():
try:
- if verbose:
- logging.debug("ARGV : %r" % sys.argv)
+ logging.debug("ARGV : %r" % sys.argv)
cli_from = sys.argv[2].lower()
cli_to = sys.argv[4:]
return (cli_from, cli_to)
except Exception:
- if verbose:
- logging.error("Invalid to / from : %r" % sys.argv)
+ logging.error("Invalid to / from : %r" % sys.argv)
sys.exit(EX_UNAVAILABLE)
@@ -36,67 +33,61 @@
(stdout, stderr) = process.communicate(content.encode())
retval = process.wait()
if retval == 0:
- if verbose:
- logging.debug("Mail resent via sendmail, stdout: %s, stderr: %s" % (stdout, stderr))
+ logging.debug("Mail resent via sendmail, stdout: %s, stderr: %s" % (stdout, stderr))
return EX_SUCCESS
else:
raise Exception("retval not zero - %s" % retval)
except Exception as e:
print(f"Error re-injecting via {command}")
- if verbose:
- logging.error("Exception while re-injecting mail: %s -- stdout:%s, stderr:%s, retval: %s" % (e, stdout, stderr, retval))
- logging.error(f"Commandline used: {command}")
+ logging.error("Exception while re-injecting mail: %s -- stdout:%s, stderr:%s, retval: %s" % (e, stdout, stderr, retval))
+ logging.error(f"Commandline used: {command}")
return EX_TEMPFAIL
if __name__ == "__main__":
(cli_from, cli_to) = get_args()
- if verbose:
- logging.basicConfig(level=logging.DEBUG,
- format='%(asctime)s %(levelname)s %(message)s',
- filename='/tmp/content-filter.log',
- filemode='a')
+ logging.root.handlers = []
+ logging.basicConfig(level=logging.ERROR,
+ format='%(asctime)s %(levelname)s %(message)s',
+ filename='/var/log/kolab/postfix-content-filter.log',
+ filemode='a')
content = ''.join(sys.stdin.readlines())
- URL = 'SERVICES_HOST/api/webhooks/policy/mail/filter'
try:
+ to = ','.join(cli_to)
+ request_url = f"SERVICES_HOST/api/webhooks/policy/mail/filter?recipient={to}&sender={cli_from}"
+ logging.debug(request_url)
response = requests.post(
- URL + f"?recipient={cli_to}",
+ request_url,
data=content,
verify=True
)
- if response.status_code == 201:
- if verbose:
- logging.warning("No changes requested, reinjecting original content.")
+ if response.status_code == 204:
+ logging.debug("No changes requested, reinjecting original content.")
print("Unmodified email")
elif response.status_code == 200:
- if verbose:
- logging.warning("Updating content.")
+ logging.debug("Updating content.")
print("Modified email")
content = response.text
elif response.status_code == 460:
- if verbose:
- logging.warning("Rejecting email.")
+ logging.debug("Rejecting email.")
print("Rejecting email")
sys.exit(EX_UNAVAILABLE)
elif response.status_code == 461:
- if verbose:
- logging.warning("Ignoring email.")
+ logging.debug("Ignoring email.")
print("Dropping email")
sys.exit(EX_SUCCESS)
else:
- if verbose:
- logging.warning(f"Unknown status code {response.status_code}.")
- print("Unknown status code")
+ logging.warning(f"Unknown status code {response.status_code}.")
+ print(f"Unknown status code {response.status_code}")
sys.exit(EX_TEMPFAIL)
# pylint: disable=broad-except
except Exception:
- if verbose:
- logging.warning("Request failed.")
+ logging.warning("Request failed.")
print("Request failed")
sys.exit(EX_TEMPFAIL)
diff --git a/docker/postfix/rootfs/usr/libexec/postfix/kolab_policy_spf b/docker/postfix/rootfs/usr/libexec/postfix/kolab_policy
rename from docker/postfix/rootfs/usr/libexec/postfix/kolab_policy_spf
rename to docker/postfix/rootfs/usr/libexec/postfix/kolab_policy
--- a/docker/postfix/rootfs/usr/libexec/postfix/kolab_policy_spf
+++ b/docker/postfix/rootfs/usr/libexec/postfix/kolab_policy
@@ -1,20 +1,14 @@
#!/usr/bin/python3
"""
-This is the implementation of a (postfix) MTA policy service to enforce the
-Sender Policy Framework.
+This is the implementation of a (postfix) MTA policy service
"""
import json
import time
import sys
import logging
-from logging.handlers import SysLogHandler
-
import requests
-logger = logging.getLogger(__name__)
-logger.setLevel(logging.INFO)
-logger.addHandler(SysLogHandler())
def read_request_input():
"""
@@ -28,7 +22,7 @@
while not end_of_request:
if (time.time() - start_time) >= 10:
- logger.warning("policy/spf took too long reading the request.")
+ logging.warning("took too long reading the request.")
print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
sys.stdout.flush()
sys.exit(0)
@@ -49,9 +43,17 @@
if __name__ == "__main__":
- URL = 'SERVICES_HOST/api/webhooks/policy/spf'
+ name = sys.argv[1]
+ path = sys.argv[2]
+ URL = f"SERVICES_HOST{path}"
+
+ logging.root.handlers = []
+ # Set to logging.DEBUG to enable logging
+ logging.basicConfig(level=logging.ERROR,
+ format='%(asctime)s %(levelname)s %(message)s',
+ filename=f"/var/log/kolab/postfix-policy-{name}.log",
+ filemode='a')
- # Start the work
while True:
REQUEST = read_request_input()
@@ -63,7 +65,7 @@
)
# pylint: disable=broad-except
except Exception:
- logger.warning("policy/spf request failed.")
+ logging.warning(f"request failed.")
print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
sys.stdout.flush()
sys.exit(0)
@@ -72,7 +74,7 @@
R = json.loads(RESPONSE.text)
# pylint: disable=broad-except
except Exception:
- logger.warning("Failed to load json from policy/spf request.")
+ logging.warning(f"Failed to load json from request.")
print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
sys.stdout.flush()
sys.exit(0)
@@ -83,13 +85,14 @@
if 'log' in R:
for line in R['log']:
- logger.warning(line)
+ logging.info(line)
if RESPONSE.ok:
print("action={0}\n".format(R['response']))
+ logging.debug("success with response: %s" % R['response'])
else:
print("action={0} {1}\n".format(R['response'], R['reason']))
- logger.warning("spf failed with reason: %s" % R['reason'])
+ logging.info("failed with reason: %s" % R['reason'])
sys.stdout.flush()
sys.exit(0)
diff --git a/docker/postfix/rootfs/usr/libexec/postfix/kolab_policy_greylist b/docker/postfix/rootfs/usr/libexec/postfix/kolab_policy_greylist
deleted file mode 100755
--- a/docker/postfix/rootfs/usr/libexec/postfix/kolab_policy_greylist
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/python3
-"""
-An example implementation of a policy service.
-"""
-
-import json
-import time
-import sys
-
-import requests
-
-
-def read_request_input():
- """
- Read a single policy request from sys.stdin, and return a dictionary
- containing the request.
- """
- start_time = time.time()
-
- policy_request = {}
- end_of_request = False
-
- while not end_of_request:
- if (time.time() - start_time) >= 10:
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- request_line = sys.stdin.readline()
-
- if request_line.strip() == '':
- if 'request' in policy_request:
- end_of_request = True
- else:
- request_line = request_line.strip()
- request_key = request_line.split('=')[0]
- request_value = '='.join(request_line.split('=')[1:])
-
- policy_request[request_key] = request_value
-
- return policy_request
-
-
-if __name__ == "__main__":
- URL = 'SERVICES_HOST/api/webhooks/policy/greylist'
-
- # Start the work
- while True:
- REQUEST = read_request_input()
-
- try:
- RESPONSE = requests.post(
- URL,
- data=REQUEST,
- verify=True
- )
- # pylint: disable=broad-except
- except Exception:
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- try:
- R = json.loads(RESPONSE.text)
- # pylint: disable=broad-except
- except Exception:
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- if 'prepend' in R:
- for prepend in R['prepend']:
- print("action=PREPEND {0}".format(prepend))
-
- if RESPONSE.ok:
- print("action={0}\n".format(R['response']))
- else:
- print("action={0} {1}\n".format(R['response'], R['reason']))
-
- sys.stdout.flush()
- sys.exit(0)
diff --git a/docker/postfix/rootfs/usr/libexec/postfix/kolab_policy_ratelimit b/docker/postfix/rootfs/usr/libexec/postfix/kolab_policy_ratelimit
deleted file mode 100755
--- a/docker/postfix/rootfs/usr/libexec/postfix/kolab_policy_ratelimit
+++ /dev/null
@@ -1,144 +0,0 @@
-#!/usr/bin/python3
-"""
-This policy applies rate limitations
-"""
-
-import json
-import time
-import sys
-
-import requests
-
-
-class PolicyRequest:
- """
- A holder of policy request instances.
- """
- db = None
- recipients = []
- sender = None
-
- def __init__(self, request):
- """
- Initialize a policy request, usually in RCPT protocol state.
- """
- if 'sender' in request:
- self.sender = request['sender']
-
- if 'recipient' in request:
- request['recipient'] = request['recipient']
-
- self.recipients.append(request['recipient'])
-
- def add_request(self, request):
- """
- Add an additional request from an instance to the existing instance
- """
- # Normalize email addresses (they may contain recipient delimiters)
- if 'recipient' in request:
- request['recipient'] = request['recipient']
-
- if not request['recipient'].strip() == '':
- self.recipients.append(request['recipient'])
-
- def check_rate(self):
- """
- Check the rates at which this sender is hitting our mailserver.
- """
- if self.sender == "":
- return {'response': 'DUNNO'}
-
- try:
- response = requests.post(
- URL,
- data={
- 'sender': self.sender,
- 'recipients': self.recipients
- },
- verify=False
- )
-
- # pylint: disable=broad-except
- except Exception:
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- return response
-
-
-def read_request_input():
- """
- Read a single policy request from sys.stdin, and return a dictionary
- containing the request.
- """
- start_time = time.time()
-
- policy_request = {}
- end_of_request = False
-
- while not end_of_request:
- if (time.time() - start_time) >= 10:
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- request_line = sys.stdin.readline()
-
- if request_line.strip() == '':
- if 'request' in policy_request:
- end_of_request = True
- else:
- request_line = request_line.strip()
- request_key = request_line.split('=')[0]
- request_value = '='.join(request_line.split('=')[1:])
-
- policy_request[request_key] = request_value
-
- return policy_request
-
-
-if __name__ == "__main__":
- URL = 'SERVICES_HOST/api/webhooks/policy/ratelimit'
-
- POLICY_REQUESTS = {}
-
- # Start the work
- while True:
- POLICY_REQUEST = read_request_input()
-
- INSTANCE = POLICY_REQUEST['instance']
-
- if INSTANCE in POLICY_REQUESTS:
- POLICY_REQUESTS[INSTANCE].add_request(POLICY_REQUEST)
- else:
- POLICY_REQUESTS[INSTANCE] = PolicyRequest(POLICY_REQUEST)
-
- protocol_state = POLICY_REQUEST['protocol_state'].strip().lower()
-
- if not protocol_state == 'data':
- print("action=DUNNO\n")
- sys.stdout.flush()
- sys.exit(0)
- else:
- RESPONSE = POLICY_REQUESTS[INSTANCE].check_rate()
-
- try:
- R = json.loads(RESPONSE.text)
- # pylint: disable=broad-except
- except Exception:
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- if 'prepend' in R:
- for prepend in R['prepend']:
- print("action=PREPEND {0}".format(prepend))
-
- if 'reason' in R:
- print("action={0} {1}\n".format(R['response'], R['reason']))
- else:
- print("action={0}\n".format(R['response']))
-
- sys.stdout.flush()
- sys.exit(0)
diff --git a/extras/kolab_policy_submission b/docker/postfix/rootfs/usr/libexec/postfix/kolab_policy_submission
rename from extras/kolab_policy_submission
rename to docker/postfix/rootfs/usr/libexec/postfix/kolab_policy_submission
--- a/extras/kolab_policy_submission
+++ b/docker/postfix/rootfs/usr/libexec/postfix/kolab_policy_submission
@@ -1,15 +1,16 @@
#!/usr/bin/python3
"""
-This policy applies submission access policies
+This policy applies submission policies
To manually test this you can issue something like this (see https://www.postfix.org/SMTPD_POLICY_README.html from more info on the protocol):
-echo -e "request=smtpd_access_policy\nsasl_sender=test1@kolab.org\nsender=test1@kolab.org\nrecipient=test2@kolab.org\ninstance=testinstance\nprotocol_state=DATA\n\n" | /usr/libexec/postfix/kolab_policy_submission
+echo -e "request=smtpd_access_policy\nsender=test1@kolab.org\nrecipient=test2@kolab.org\ninstance=testinstance\nprotocol_state=DATA\n\n" | /usr/libexec/postfix/kolab_policy_submission
"""
-import json
import time
import sys
+import json
+import logging
import requests
@@ -25,34 +26,41 @@
"""
Initialize a policy request, usually in RCPT protocol state.
"""
+ logging.debug("Initializing request.")
if 'sender' in request:
self.sender = request['sender']
if 'recipient' in request:
+ request['recipient'] = request['recipient']
+
self.recipients.append(request['recipient'])
- if 'sasl_sender' in request:
+ if 'sasl_sender' in request and request['sasl_sender']:
self.user = request['sasl_sender']
- elif 'sasl_username' in request:
+ elif 'sasl_username' in request and request['sasl_username']:
self.user = request['sasl_username']
+ logging.debug(f" {self.sender}")
+ logging.debug(f" {self.recipients}")
+
def add_request(self, request):
"""
Add an additional request from an instance to the existing instance
"""
# Normalize email addresses (they may contain recipient delimiters)
if 'recipient' in request:
+ request['recipient'] = request['recipient']
+
if not request['recipient'].strip() == '':
self.recipients.append(request['recipient'])
+ logging.debug(f" {self.recipients}")
def check_policy(self):
"""
Pass the request to Kolab API
"""
- if self.sender == "" or self.user is None:
- return {'response': 'DUNNO'}
-
try:
+ logging.debug(f" Checking policy for {self.sender}")
response = requests.post(
URL,
data={
@@ -60,7 +68,8 @@
'recipients': self.recipients,
'user': self.user
},
- verify=True
+ # Skip certificate verification for this internal request
+ verify=False
)
# pylint: disable=broad-except
@@ -71,12 +80,14 @@
return response
+
def read_request_input():
"""
Read a single policy request from sys.stdin, and return a dictionary
containing the request.
"""
start_time = time.time()
+
policy_request = {}
end_of_request = False
@@ -103,34 +114,47 @@
if __name__ == "__main__":
- URL = 'https://services.kolabnow.com/api/webhooks/policy/submission'
+ URL = 'SERVICES_HOST/api/webhooks/policy/submission'
+
+ policy_requests = {}
- POLICY_REQUESTS = {}
+ logging.root.handlers = []
+ # Set to logging.DEBUG to enable logging
+ logging.basicConfig(level=logging.ERROR,
+ format='%(asctime)s %(levelname)s %(message)s',
+ filename='/var/log/kolab/postfix-policy-submission.log',
+ filemode='a')
# Start the work
while True:
- POLICY_REQUEST = read_request_input()
+ input = read_request_input()
+ logging.debug(input)
- INSTANCE = POLICY_REQUEST['instance']
+ instance = input['instance']
- if INSTANCE in POLICY_REQUESTS:
- POLICY_REQUESTS[INSTANCE].add_request(POLICY_REQUEST)
+ if instance in policy_requests:
+ policy_requests[instance].add_request(input)
else:
- POLICY_REQUESTS[INSTANCE] = PolicyRequest(POLICY_REQUEST)
+ policy_requests[instance] = PolicyRequest(input)
- protocol_state = POLICY_REQUEST['protocol_state'].strip().lower()
+ protocol_state = input['protocol_state'].strip().lower()
- if not protocol_state == 'data':
+ # Only proceed with the request once we have collected sender and recipient information
+ if protocol_state != 'data' or not policy_requests[instance].sender or not policy_requests[instance].user:
print("action=DUNNO\n")
+ logging.debug("DUNNO before state data.")
sys.stdout.flush()
sys.exit(0)
- else:
- RESPONSE = POLICY_REQUESTS[INSTANCE].check_policy()
+
+ response = policy_requests[instance].check_policy()
+ logging.debug(response)
try:
- R = json.loads(RESPONSE.text)
+ R = json.loads(response.text)
+ logging.debug(R)
# pylint: disable=broad-except
except Exception:
+ logging.warning("Failed to load json response.")
print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
sys.stdout.flush()
sys.exit(0)
@@ -140,8 +164,10 @@
print("action=PREPEND {0}".format(prepend))
if 'reason' in R:
+ logging.info("action={0} {1}\n".format(R['response'], R['reason']))
print("action={0} {1}\n".format(R['response'], R['reason']))
else:
+ logging.info("action={0}\n".format(R['response']))
print("action={0}\n".format(R['response']))
sys.stdout.flush()
diff --git a/docker/utils/rootfs/opt/app-root/src/mailtransporttest.py b/docker/utils/rootfs/opt/app-root/src/mailtransporttest.py
--- a/docker/utils/rootfs/opt/app-root/src/mailtransporttest.py
+++ b/docker/utils/rootfs/opt/app-root/src/mailtransporttest.py
@@ -205,7 +205,7 @@
parser.add_argument("--starttls", action='store_true', help="Use SMTP starttls over 587")
parser.add_argument("--verbose", action='store_true', help="Verbose mode")
parser.add_argument("--from-address", help="Source address instead of the sender username")
-parser.add_argument("--target-address", help="Target address instead of the recipient username")
+parser.add_argument("--target-address", help="Target address instead of the recipient username (can be specified multiple times)", action='append')
parser.add_argument("--body", help="Body text to include")
parser.add_argument("--validate", action='store_true', help="Validate the received message")
parser.add_argument('--bulk-send', help='Bulk send email, then exit', type=int, default=0)
diff --git a/extras/kolab_policy_greylist b/extras/kolab_policy_greylist
deleted file mode 100755
--- a/extras/kolab_policy_greylist
+++ /dev/null
@@ -1,81 +0,0 @@
-#!/usr/bin/python3
-"""
-An example implementation of a policy service.
-"""
-
-import json
-import time
-import sys
-
-import requests
-
-
-def read_request_input():
- """
- Read a single policy request from sys.stdin, and return a dictionary
- containing the request.
- """
- start_time = time.time()
-
- policy_request = {}
- end_of_request = False
-
- while not end_of_request:
- if (time.time() - start_time) >= 10:
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- request_line = sys.stdin.readline()
-
- if request_line.strip() == '':
- if 'request' in policy_request:
- end_of_request = True
- else:
- request_line = request_line.strip()
- request_key = request_line.split('=')[0]
- request_value = '='.join(request_line.split('=')[1:])
-
- policy_request[request_key] = request_value
-
- return policy_request
-
-
-if __name__ == "__main__":
- URL = 'https://services.kolabnow.com/api/webhooks/policy/greylist'
-
- # Start the work
- while True:
- REQUEST = read_request_input()
-
- try:
- RESPONSE = requests.post(
- URL,
- data=REQUEST,
- verify=True
- )
- # pylint: disable=broad-except
- except Exception:
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- try:
- R = json.loads(RESPONSE.text)
- # pylint: disable=broad-except
- except Exception:
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- if 'prepend' in R:
- for prepend in R['prepend']:
- print("action=PREPEND {0}".format(prepend))
-
- if RESPONSE.ok:
- print("action={0}\n".format(R['response']))
- else:
- print("action={0} {1}\n".format(R['response'], R['reason']))
-
- sys.stdout.flush()
- sys.exit(0)
diff --git a/extras/kolab_policy_ratelimit b/extras/kolab_policy_ratelimit
deleted file mode 100755
--- a/extras/kolab_policy_ratelimit
+++ /dev/null
@@ -1,149 +0,0 @@
-#!/usr/bin/python3
-"""
-This policy applies rate limitations
-
-To manually test this you can issue something like this (see https://www.postfix.org/SMTPD_POLICY_README.html from more info on the protocol):
-echo -e "request=smtpd_access_policy\nsender=test1@kolab.org\nrecipient=test2@kolab.org\ninstance=testinstance\nprotocol_state=DATA\n\n" | /usr/libexec/postfix/kolab_policy_ratelimit
-
-"""
-
-import json
-import time
-import sys
-
-import requests
-
-
-class PolicyRequest:
- """
- A holder of policy request instances.
- """
- db = None
- recipients = []
- sender = None
-
- def __init__(self, request):
- """
- Initialize a policy request, usually in RCPT protocol state.
- """
- if 'sender' in request:
- self.sender = request['sender']
-
- if 'recipient' in request:
- request['recipient'] = request['recipient']
-
- self.recipients.append(request['recipient'])
-
- def add_request(self, request):
- """
- Add an additional request from an instance to the existing instance
- """
- # Normalize email addresses (they may contain recipient delimiters)
- if 'recipient' in request:
- request['recipient'] = request['recipient']
-
- if not request['recipient'].strip() == '':
- self.recipients.append(request['recipient'])
-
- def check_rate(self):
- """
- Check the rates at which this sender is hitting our mailserver.
- """
- if self.sender == "":
- return {'response': 'DUNNO'}
-
- try:
- response = requests.post(
- URL,
- data={
- 'sender': self.sender,
- 'recipients': self.recipients
- },
- verify=True
- )
-
- # pylint: disable=broad-except
- except Exception:
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- return response
-
-
-def read_request_input():
- """
- Read a single policy request from sys.stdin, and return a dictionary
- containing the request.
- """
- start_time = time.time()
-
- policy_request = {}
- end_of_request = False
-
- while not end_of_request:
- if (time.time() - start_time) >= 10:
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- request_line = sys.stdin.readline()
-
- # The empty line terminates the request
- if request_line.strip() == '':
- if 'request' in policy_request:
- end_of_request = True
- else:
- request_line = request_line.strip()
- request_key = request_line.split('=')[0]
- request_value = '='.join(request_line.split('=')[1:])
-
- policy_request[request_key] = request_value
-
- return policy_request
-
-
-if __name__ == "__main__":
- URL = 'https://services.kolabnow.com/api/webhooks/policy/ratelimit'
-
- POLICY_REQUESTS = {}
-
- # Start the work
- while True:
- POLICY_REQUEST = read_request_input()
-
- INSTANCE = POLICY_REQUEST['instance']
-
- if INSTANCE in POLICY_REQUESTS:
- POLICY_REQUESTS[INSTANCE].add_request(POLICY_REQUEST)
- else:
- POLICY_REQUESTS[INSTANCE] = PolicyRequest(POLICY_REQUEST)
-
- protocol_state = POLICY_REQUEST['protocol_state'].strip().lower()
-
- if not protocol_state == 'data':
- print("action=DUNNO\n")
- sys.stdout.flush()
- sys.exit(0)
- else:
- RESPONSE = POLICY_REQUESTS[INSTANCE].check_rate()
-
- try:
- R = json.loads(RESPONSE.text)
- # pylint: disable=broad-except
- except Exception:
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- if 'prepend' in R:
- for prepend in R['prepend']:
- print("action=PREPEND {0}".format(prepend))
-
- if 'reason' in R:
- print("action={0} {1}\n".format(R['response'], R['reason']))
- else:
- print("action={0}\n".format(R['response']))
-
- sys.stdout.flush()
- sys.exit(0)
diff --git a/extras/kolab_policy_spf b/extras/kolab_policy_spf
deleted file mode 100755
--- a/extras/kolab_policy_spf
+++ /dev/null
@@ -1,95 +0,0 @@
-#!/usr/bin/python3
-"""
-This is the implementation of a (postfix) MTA policy service to enforce the
-Sender Policy Framework.
-"""
-
-import json
-import time
-import sys
-import logging
-from logging.handlers import SysLogHandler
-
-import requests
-
-logger = logging.getLogger(__name__)
-logger.setLevel(logging.INFO)
-logger.addHandler(SysLogHandler())
-
-def read_request_input():
- """
- Read a single policy request from sys.stdin, and return a dictionary
- containing the request.
- """
- start_time = time.time()
-
- policy_request = {}
- end_of_request = False
-
- while not end_of_request:
- if (time.time() - start_time) >= 10:
- logger.warning("policy/spf took too long reading the request.")
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- request_line = sys.stdin.readline()
-
- if request_line.strip() == '':
- if 'request' in policy_request:
- end_of_request = True
- else:
- request_line = request_line.strip()
- request_key = request_line.split('=')[0]
- request_value = '='.join(request_line.split('=')[1:])
-
- policy_request[request_key] = request_value
-
- return policy_request
-
-
-if __name__ == "__main__":
- URL = 'https://services.kolabnow.com/api/webhooks/policy/spf'
-
- # Start the work
- while True:
- REQUEST = read_request_input()
-
- try:
- RESPONSE = requests.post(
- URL,
- data=REQUEST,
- verify=True
- )
- # pylint: disable=broad-except
- except Exception:
- logger.warning("policy/spf request failed.")
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- try:
- R = json.loads(RESPONSE.text)
- # pylint: disable=broad-except
- except Exception:
- logger.warning("Failed to load json from policy/spf request.")
- print("action=DEFER_IF_PERMIT Temporary error, try again later.\n")
- sys.stdout.flush()
- sys.exit(0)
-
- if 'prepend' in R:
- for prepend in R['prepend']:
- print("action=PREPEND {0}".format(prepend))
-
- if 'log' in R:
- for line in R['log']:
- logger.warning(line)
-
- if RESPONSE.ok:
- print("action={0}\n".format(R['response']))
- else:
- print("action={0} {1}\n".format(R['response'], R['reason']))
- logger.warning("spf failed with reason: %s" % R['reason'])
-
- sys.stdout.flush()
- sys.exit(0)
diff --git a/src/app/Policy/Mailfilter.php b/src/app/Policy/Mailfilter.php
--- a/src/app/Policy/Mailfilter.php
+++ b/src/app/Policy/Mailfilter.php
@@ -47,12 +47,18 @@
// then we'd send body in another request, but only if needed. For example, a text/plain
// message from same domain sender does not include an iTip, nor needs a footer injection.
+ // Email with multiple recipients, which we don't handle at the moment.
+ // Likely an outgoing email, so we just accept.
+ if (str_contains($request->recipient, ",")) {
+ return response('', self::CODE_ACCEPT_EMPTY);
+ }
+
// Find the recipient user
$user = User::where('email', $request->recipient)->first();
+ // Not a local recipient, so e.g. an outgoing email
if (empty($user)) {
- // FIXME: Better code? Should we use custom header instead?
- return response('', self::CODE_REJECT);
+ return response('', self::CODE_ACCEPT_EMPTY);
}
// Get list of enabled modules for the recipient user
diff --git a/src/app/Policy/SmtpAccess.php b/src/app/Policy/SmtpAccess.php
--- a/src/app/Policy/SmtpAccess.php
+++ b/src/app/Policy/SmtpAccess.php
@@ -45,11 +45,18 @@
return new Response(Response::ACTION_REJECT, $reason, 403);
}
+ // TODO: should we be using the $user or the $sender?
+ $response = RateLimit::verifyRequest($user, (array) $data['recipients']);
+ if ($response->action != Response::ACTION_DUNNO) {
+ return $response;
+ }
+
// TODO: Prepending Sender/X-Sender/X-Authenticated-As headers?
// TODO: Recipient policies here?
- // TODO: Check rate limit here?
- return new Response(Response::ACTION_PERMIT);
+ // Leave it up to the postfix configuration how to proceed
+ // (accept would stop processing)
+ return new Response(Response::ACTION_DUNNO);
}
/**
diff --git a/src/tests/Feature/Policy/MailfilterTest.php b/src/tests/Feature/Policy/MailfilterTest.php
--- a/src/tests/Feature/Policy/MailfilterTest.php
+++ b/src/tests/Feature/Policy/MailfilterTest.php
@@ -49,7 +49,7 @@
$request = new Request($get, [], [], [], [], [], $mail);
$response = Mailfilter::handle($request);
- $this->assertSame(Mailfilter::CODE_REJECT, $response->status());
+ $this->assertSame(Mailfilter::CODE_ACCEPT_EMPTY, $response->status());
$this->assertSame('', $response->content());
$john = $this->getTestUser('john@kolab.org');

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 9:52 PM (10 h, 20 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18826830
Default Alt Text
D5271.1775253169.diff (44 KB)

Event Timeline