[Web][Postfix] Add mail policies: per-recipient send/receive rules for mailboxes and groups

This commit is contained in:
FreddleSpl0it committed 2026-11-07 15:55:43 +01:00
1 parent 329bca1d46
commit 4484cfcd42
36 files changed
+2677 -42

No files matched your search

+3
View File
@@ -26,6 +26,7 @@ RUN groupadd -g 102 postfix \
postfix \
postfix-mysql \
postfix-pcre \
python3 \
redis-tools \
sasl2-bin \
sudo \
@@ -46,6 +47,7 @@ COPY postfix.sh /opt/postfix.sh
COPY rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham
COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam
COPY whitelist_forwardinghosts.sh /usr/local/bin/whitelist_forwardinghosts.sh
COPY mailcow-policy.py /usr/local/bin/mailcow-policy.py
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
COPY docker-entrypoint.sh /docker-entrypoint.sh
@@ -53,6 +55,7 @@ RUN chmod +x /opt/postfix.sh \
/usr/local/bin/rspamd-pipe-ham \
/usr/local/bin/rspamd-pipe-spam \
/usr/local/bin/whitelist_forwardinghosts.sh \
/usr/local/bin/mailcow-policy.py \
/usr/local/sbin/stop-supervisor.sh
RUN rm -rf /tmp/* /var/tmp/*
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/python3
# Policy service of the mail policies (mailcow-policy in master.cf, spawned by Postfix): reads the policy requests
# of smtpd and asks the mailcow UI for the action (nginx:8081/mailpolicy.php). Without an answer the recipient is
# deferred; smtpd only asks while policies or internal aliases exist (mysql_mail_policy_maps.cf).
import http.client
import re
import socket
import sys
from urllib.parse import urlencode
# one line: DUNNO or a rejection with code and text
valid_action = re.compile(rb'DUNNO|[45]\d\d [45]\.\d{1,3}\.\d{1,3} [\x20-\x7e]{1,200}')
unavailable = b'DEFER_IF_PERMIT Mail policy check temporarily unavailable'
# address of nginx, resolved once per process (a lookup costs more than the check)
nginx = None
def check(request):
global nginx
body = urlencode(sorted(request.items())).encode()
try:
if nginx is None:
nginx = socket.gethostbyname('nginx')
# http.client sets TCP_NODELAY, so the body is not held back until the headers are acknowledged
conn = http.client.HTTPConnection(nginx, 8081, timeout=10)
try:
conn.request('POST', '/mailpolicy.php', body, {'Host': 'nginx', 'Content-Type': 'application/x-www-form-urlencoded'})
response = conn.getresponse()
action = response.read()
finally:
conn.close()
except (OSError, http.client.HTTPException):
# nginx may have a new address after a restart
nginx = None
return unavailable
return action if response.status == 200 and valid_action.fullmatch(action) else unavailable
def main():
request = {}
# attributes as bytes: addresses may contain any 8-bit characters
for line in sys.stdin.buffer:
line = line.rstrip(b'\r\n')
if line:
name, separator, value = line.partition(b'=')
if separator:
request[name] = value
continue
sys.stdout.buffer.write(b'action=' + check(request) + b'\n\n')
sys.stdout.buffer.flush()
request = {}
if __name__ == '__main__':
main()
+29
View File
@@ -15,6 +15,21 @@ until dig +short mailcow.email > /dev/null; do
sleep 1
done
# Wait for updated schema (DB_VERSION from docker-compose.yml), the maps read tables the init_db of php-fpm creates on updates
DBV_NOW=$(mariadb --ssl=false --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN 2>/dev/null)
DBV_WAIT=0
while [[ "${DBV_NOW}" != "${DB_VERSION}" ]] && [[ ${DBV_WAIT} -lt 60 ]]; do
echo "Waiting for schema update..."
sleep 5
DBV_WAIT=$((DBV_WAIT + 1))
DBV_NOW=$(mariadb --ssl=false --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN 2>/dev/null)
done
if [[ "${DBV_NOW}" != "${DB_VERSION}" ]]; then
echo -e "\e[31mDB schema is ${DBV_NOW}, expected ${DB_VERSION} - starting anyway, check the php-fpm log (init_db)\e[0m"
else
echo "DB schema is ${DBV_NOW}"
fi
cat <<EOF > /etc/aliases
# Autogenerated by mailcow
null: /dev/null
@@ -224,6 +239,20 @@ query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM transports
LIMIT 1;
EOF
# Mail policies: while policies or internal aliases exist, smtpd asks the policy service (mailcow_mail_policy in main.cf)
cat <<EOF > /opt/postfix/conf/sql/mysql_mail_policy_maps.cf
# Autogenerated by mailcow
user = ${DBUSER}
password = ${DBPASS}
hosts = unix:/var/run/mysqld/mysqld.sock
dbname = ${DBNAME}
query = SELECT 'mailcow_mail_policy' FROM mail_policy_member
JOIN mail_policy ON mail_policy.id = mail_policy_member.policy_id
WHERE mail_policy.active = '1' AND '%s' != ''
UNION SELECT 'mailcow_mail_policy' FROM alias WHERE internal = '1' AND active = '1'
LIMIT 1;
EOF
cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf
# Autogenerated by mailcow
user = ${DBUSER}