Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117787000
D5271.1775253463.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
44 KB
Referenced Files
None
Subscribers
None
D5271.1775253463.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 9:57 PM (3 h, 1 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18826830
Default Alt Text
D5271.1775253463.diff (44 KB)
Attached To
Mode
D5271: Fold rate-limiting into the submission policy and get mailfilter to work
Attached
Detach File
Event Timeline