From 4f96e991449b52400b5a1fe8d968f07ff9346842 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 29 Aug 2021 17:40:37 +0200 Subject: [PATCH 01/53] MTA-STS (use rather than publish policies) --- core/postfix/Dockerfile | 5 +++++ core/postfix/conf/main.cf | 2 +- core/postfix/mta-sts-daemon.yml | 10 ++++++++++ core/postfix/start.py | 10 ++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 core/postfix/mta-sts-daemon.yml diff --git a/core/postfix/Dockerfile b/core/postfix/Dockerfile index 062155c1..8efe5da4 100644 --- a/core/postfix/Dockerfile +++ b/core/postfix/Dockerfile @@ -12,10 +12,15 @@ RUN pip3 install socrate==0.2.0 RUN pip3 install "podop>0.2.5" # Image specific layers under this line +RUN apk add --no-cache --virtual .build-deps gcc musl-dev python3-dev +RUN pip3 install --no-binary :all: postfix-mta-sts-resolver==1.0.1 +RUN apk del .build-deps gcc musl-dev python3-dev + RUN apk add --no-cache postfix postfix-pcre cyrus-sasl-login COPY conf /conf COPY start.py /start.py +COPY mta-sts-daemon.yml /etc/ EXPOSE 25/tcp 10025/tcp VOLUME ["/queue"] diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index 7f84ade7..0194324f 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -59,7 +59,7 @@ tls_ssl_options = NO_COMPRESSION, NO_TICKET smtp_tls_mandatory_protocols = !SSLv2, !SSLv3 smtp_tls_protocols =!SSLv2,!SSLv3 smtp_tls_security_level = {{ OUTBOUND_TLS_LEVEL|default('may') }} -smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map +smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map, socketmap:unix:/tmp/mta-sts.socket:postfix smtp_tls_CApath = /etc/ssl/certs smtp_tls_session_cache_database = lmdb:/dev/shm/postfix/smtp_scache smtpd_tls_session_cache_database = lmdb:/dev/shm/postfix/smtpd_scache diff --git a/core/postfix/mta-sts-daemon.yml b/core/postfix/mta-sts-daemon.yml new file mode 100644 index 00000000..39f60e48 --- /dev/null +++ b/core/postfix/mta-sts-daemon.yml @@ -0,0 +1,10 @@ +path: "/tmp/mta-sts.socket" +mode: 0600 +shutdown_timeout: 20 +cache: + type: internal + options: + cache_size: 10000 +default_zone: + strict_testing: false + timeout: 4 diff --git a/core/postfix/start.py b/core/postfix/start.py index 799d42f5..50565e3d 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -30,6 +30,12 @@ def start_podop(): ("senderrate", "url", url + "sender/rate/§") ]) +def start_mta_sts_daemon(): + os.chmod("/root/", 0o755) # read access to /root/.netrc required + os.setuid(getpwnam('postfix').pw_uid) + from postfix_mta_sts_resolver import daemon + daemon.main() + def is_valid_postconf_line(line): return not line.startswith("#") \ and not line == '' @@ -68,6 +74,9 @@ for map_file in glob.glob("/overrides/*.map"): os.system("postmap {}".format(destination)) os.remove(destination) +if os.path.exists("/overrides/mta-sts-daemon.yml"): + shutil.copyfile("/overrides/mta-sts-daemon.yml", "/etc/mta-sts-daemon.yml") + if not os.path.exists("/etc/postfix/tls_policy.map.db"): with open("/etc/postfix/tls_policy.map", "w") as f: for domain in ['gmail.com', 'yahoo.com', 'hotmail.com', 'aol.com', 'outlook.com', 'comcast.net', 'icloud.com', 'msn.com', 'hotmail.co.uk', 'live.com', 'yahoo.co.in', 'me.com', 'mail.ru', 'cox.net', 'yahoo.co.uk', 'verizon.net', 'ymail.com', 'hotmail.it', 'kw.com', 'yahoo.com.tw', 'mac.com', 'live.se', 'live.nl', 'yahoo.com.br', 'googlemail.com', 'libero.it', 'web.de', 'allstate.com', 'btinternet.com', 'online.no', 'yahoo.com.au', 'live.dk', 'earthlink.net', 'yahoo.fr', 'yahoo.it', 'gmx.de', 'hotmail.fr', 'shawinc.com', 'yahoo.de', 'moe.edu.sg', 'naver.com', 'bigpond.com', 'statefarm.com', 'remax.net', 'rocketmail.com', 'live.no', 'yahoo.ca', 'bigpond.net.au', 'hotmail.se', 'gmx.at', 'live.co.uk', 'mail.com', 'yahoo.in', 'yandex.ru', 'qq.com', 'charter.net', 'indeedemail.com', 'alice.it', 'hotmail.de', 'bluewin.ch', 'optonline.net', 'wp.pl', 'yahoo.es', 'hotmail.no', 'pindotmedia.com', 'orange.fr', 'live.it', 'yahoo.co.id', 'yahoo.no', 'hotmail.es', 'morganstanley.com', 'wellsfargo.com', 'wanadoo.fr', 'facebook.com', 'yahoo.se', 'fema.dhs.gov', 'rogers.com', 'yahoo.com.hk', 'live.com.au', 'nic.in', 'nab.com.au', 'ubs.com', 'shaw.ca', 'umich.edu', 'westpac.com.au', 'yahoo.com.mx', 'yahoo.com.sg', 'farmersagent.com', 'yahoo.dk', 'dhs.gov']: @@ -81,6 +90,7 @@ if "RELAYUSER" in os.environ: # Run Podop and Postfix multiprocessing.Process(target=start_podop).start() +multiprocessing.Process(target=start_mta_sts_daemon).start() os.system("/usr/libexec/postfix/post-install meta_directory=/etc/postfix create-missing") # Before starting postfix, we need to check permissions on /queue # in the event that postfix,postdrop id have changed From 52d3a338751c6f7dd14c63de6c2164d3a9d9f8da Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 29 Aug 2021 17:41:55 +0200 Subject: [PATCH 02/53] Remove the domains that have a valid MTA-STS policy gmail.com comcast.net mail.ru googlemail.com wp.pl --- core/postfix/start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/postfix/start.py b/core/postfix/start.py index 50565e3d..de559b27 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -79,7 +79,7 @@ if os.path.exists("/overrides/mta-sts-daemon.yml"): if not os.path.exists("/etc/postfix/tls_policy.map.db"): with open("/etc/postfix/tls_policy.map", "w") as f: - for domain in ['gmail.com', 'yahoo.com', 'hotmail.com', 'aol.com', 'outlook.com', 'comcast.net', 'icloud.com', 'msn.com', 'hotmail.co.uk', 'live.com', 'yahoo.co.in', 'me.com', 'mail.ru', 'cox.net', 'yahoo.co.uk', 'verizon.net', 'ymail.com', 'hotmail.it', 'kw.com', 'yahoo.com.tw', 'mac.com', 'live.se', 'live.nl', 'yahoo.com.br', 'googlemail.com', 'libero.it', 'web.de', 'allstate.com', 'btinternet.com', 'online.no', 'yahoo.com.au', 'live.dk', 'earthlink.net', 'yahoo.fr', 'yahoo.it', 'gmx.de', 'hotmail.fr', 'shawinc.com', 'yahoo.de', 'moe.edu.sg', 'naver.com', 'bigpond.com', 'statefarm.com', 'remax.net', 'rocketmail.com', 'live.no', 'yahoo.ca', 'bigpond.net.au', 'hotmail.se', 'gmx.at', 'live.co.uk', 'mail.com', 'yahoo.in', 'yandex.ru', 'qq.com', 'charter.net', 'indeedemail.com', 'alice.it', 'hotmail.de', 'bluewin.ch', 'optonline.net', 'wp.pl', 'yahoo.es', 'hotmail.no', 'pindotmedia.com', 'orange.fr', 'live.it', 'yahoo.co.id', 'yahoo.no', 'hotmail.es', 'morganstanley.com', 'wellsfargo.com', 'wanadoo.fr', 'facebook.com', 'yahoo.se', 'fema.dhs.gov', 'rogers.com', 'yahoo.com.hk', 'live.com.au', 'nic.in', 'nab.com.au', 'ubs.com', 'shaw.ca', 'umich.edu', 'westpac.com.au', 'yahoo.com.mx', 'yahoo.com.sg', 'farmersagent.com', 'yahoo.dk', 'dhs.gov']: + for domain in ['yahoo.com', 'hotmail.com', 'aol.com', 'outlook.com', 'icloud.com', 'msn.com', 'hotmail.co.uk', 'live.com', 'yahoo.co.in', 'me.com', 'cox.net', 'yahoo.co.uk', 'verizon.net', 'ymail.com', 'hotmail.it', 'kw.com', 'yahoo.com.tw', 'mac.com', 'live.se', 'live.nl', 'yahoo.com.br', 'libero.it', 'web.de', 'allstate.com', 'btinternet.com', 'online.no', 'yahoo.com.au', 'live.dk', 'earthlink.net', 'yahoo.fr', 'yahoo.it', 'gmx.de', 'hotmail.fr', 'shawinc.com', 'yahoo.de', 'moe.edu.sg', 'naver.com', 'bigpond.com', 'statefarm.com', 'remax.net', 'rocketmail.com', 'live.no', 'yahoo.ca', 'bigpond.net.au', 'hotmail.se', 'gmx.at', 'live.co.uk', 'mail.com', 'yahoo.in', 'yandex.ru', 'qq.com', 'charter.net', 'indeedemail.com', 'alice.it', 'hotmail.de', 'bluewin.ch', 'optonline.net', 'yahoo.es', 'hotmail.no', 'pindotmedia.com', 'orange.fr', 'live.it', 'yahoo.co.id', 'yahoo.no', 'hotmail.es', 'morganstanley.com', 'wellsfargo.com', 'wanadoo.fr', 'facebook.com', 'yahoo.se', 'fema.dhs.gov', 'rogers.com', 'yahoo.com.hk', 'live.com.au', 'nic.in', 'nab.com.au', 'ubs.com', 'shaw.ca', 'umich.edu', 'westpac.com.au', 'yahoo.com.mx', 'yahoo.com.sg', 'farmersagent.com', 'yahoo.dk', 'dhs.gov']: f.write(f'{domain}\tsecure\n') os.system("postmap /etc/postfix/tls_policy.map") From a01960787362ed3fc5e7774c3bc573b3c52a86d9 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 29 Aug 2021 17:46:28 +0200 Subject: [PATCH 03/53] towncrier --- towncrier/newsfragments/1798.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 towncrier/newsfragments/1798.feature diff --git a/towncrier/newsfragments/1798.feature b/towncrier/newsfragments/1798.feature new file mode 100644 index 00000000..1b63a85c --- /dev/null +++ b/towncrier/newsfragments/1798.feature @@ -0,0 +1 @@ +Implement MTA-STS (use published policies) From 5634354911bf645ea6b8204af4964d51e06178db Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 29 Aug 2021 18:28:56 +0200 Subject: [PATCH 04/53] document how to publish an MTA-STS policy --- docs/faq.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/faq.rst b/docs/faq.rst index a2c6bd33..98026ab1 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -369,6 +369,31 @@ How do I use webdav (radicale)? .. _`575`: https://github.com/Mailu/Mailu/issues/575 .. _`1591`: https://github.com/Mailu/Mailu/issues/1591 +How do I setup a MTA-STS policy? +```````````````````````````````` + +Mailu can serve an `MTA-STS policy`_; To configure it you will need to: + +1. setup the appropriate DNS/CNAME record (``mta-sts.example.com`` -> ``mailu.example.com``) and DNS/TXT record (``_mta-sts.example.com`` -> ``v=STSv1; id=1``) paying attention to the ``TTL`` as this is used by MTA-STS. + +2. configure an override with the policy itself; for example, your ``overrides/mta-sts.conf`` could read: + +.. code-block:: bash + + location ^~ /.well-known/mta-sts.txt { + return 200 "version: STSv1 + mode: enforce + max_age: 86401 + mx: mailu.example.com\r\n"; + } + +3. add ``mta-sts.example.com`` to the ``HOSTNAMES`` configuration variable (and ensure that a valid SSL certificate is available for it) + +*issue reference:* `1798`_. + +.. _`1798`: https://github.com/Mailu/Mailu/issues/1798 +.. _`MTA-STS policy`: https://datatracker.ietf.org/doc/html/rfc8461 + Technical issues ---------------- From 5efe35329be9862cf4dcb34292b7b5a9b1976b5f Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 29 Aug 2021 18:29:44 +0200 Subject: [PATCH 05/53] doh --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index 98026ab1..92b208de 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -376,7 +376,7 @@ Mailu can serve an `MTA-STS policy`_; To configure it you will need to: 1. setup the appropriate DNS/CNAME record (``mta-sts.example.com`` -> ``mailu.example.com``) and DNS/TXT record (``_mta-sts.example.com`` -> ``v=STSv1; id=1``) paying attention to the ``TTL`` as this is used by MTA-STS. -2. configure an override with the policy itself; for example, your ``overrides/mta-sts.conf`` could read: +2. configure an override with the policy itself; for example, your ``overrides/nginx/mta-sts.conf`` could read: .. code-block:: bash From 7c5dcfa025e8b7e1f7e5bb6b3ac56331bc928287 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 29 Aug 2021 18:32:17 +0200 Subject: [PATCH 06/53] MTA-STS is a major feature --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4354b28..8c7d1640 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Main features include: - **Web access**, multiple Webmails and administration interface - **User features**, aliases, auto-reply, auto-forward, fetched accounts - **Admin features**, global admins, announcements, per-domain delegation, quotas -- **Security**, enforced TLS, Letsencrypt!, outgoing DKIM, anti-virus scanner +- **Security**, enforced TLS, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner - **Antispam**, auto-learn, greylisting, DMARC and SPF - **Freedom**, all FOSS components, no tracker included From a8142dabbe4df86a2aa87d3f323de20c045d7db3 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 30 Aug 2021 14:21:28 +0200 Subject: [PATCH 07/53] Introduce DEFER_ON_TLS_ERROR This will default to True and defer emails that fail even "loose" validation of DANE or MTA-STS It should work most of the time but if it doesn't and you would rather see your emails delivered, you can turn it off. --- core/postfix/conf/main.cf | 6 +++++- core/postfix/mta-sts-daemon.yml | 2 +- core/postfix/start.py | 1 + docs/configuration.rst | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index 0194324f..78ffcee1 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -58,13 +58,17 @@ tls_ssl_options = NO_COMPRESSION, NO_TICKET # 2. not all will have and up-to-date TLS stack. smtp_tls_mandatory_protocols = !SSLv2, !SSLv3 smtp_tls_protocols =!SSLv2,!SSLv3 -smtp_tls_security_level = {{ OUTBOUND_TLS_LEVEL|default('may') }} +smtp_tls_security_level = {{ OUTBOUND_TLS_LEVEL|default('dane') }} +smtp_tls_dane_insecure_mx_policy = dane smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map, socketmap:unix:/tmp/mta-sts.socket:postfix smtp_tls_CApath = /etc/ssl/certs smtp_tls_session_cache_database = lmdb:/dev/shm/postfix/smtp_scache smtpd_tls_session_cache_database = lmdb:/dev/shm/postfix/smtpd_scache smtp_host_lookup = dns smtp_dns_support_level = dnssec +delay_warning_time = 5m +smtp_tls_loglevel = 1 +notify_classes = resource, software, delay ############### # Virtual diff --git a/core/postfix/mta-sts-daemon.yml b/core/postfix/mta-sts-daemon.yml index 39f60e48..361bcbf9 100644 --- a/core/postfix/mta-sts-daemon.yml +++ b/core/postfix/mta-sts-daemon.yml @@ -6,5 +6,5 @@ cache: options: cache_size: 10000 default_zone: - strict_testing: false + strict_testing: {{ DEFER_ON_TLS_ERROR |default('true') }} timeout: 4 diff --git a/core/postfix/start.py b/core/postfix/start.py index de559b27..5e439bdb 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -76,6 +76,7 @@ for map_file in glob.glob("/overrides/*.map"): if os.path.exists("/overrides/mta-sts-daemon.yml"): shutil.copyfile("/overrides/mta-sts-daemon.yml", "/etc/mta-sts-daemon.yml") +conf.jinja("/etc/mta-sts-daemon.yml", os.environ, "/etc/mta-sts-daemon.yml") if not os.path.exists("/etc/postfix/tls_policy.map.db"): with open("/etc/postfix/tls_policy.map", "w") as f: diff --git a/docs/configuration.rst b/docs/configuration.rst index 27f8db7d..4fd84c07 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -73,7 +73,7 @@ mail in following format: ``[HOST]:PORT``. By default postfix uses "opportunistic TLS" for outbound mail. This can be changed by setting ``OUTBOUND_TLS_LEVEL`` to ``encrypt`` or ``secure``. This setting is highly recommended -if you are using a relayhost that supports TLS. +if you are using a relayhost that supports TLS but discouraged otherwise. ``DEFER_ON_TLS_ERROR`` (default: True) controls whether incomplete policies (DANE without DNSSEC or "testing" MTA-STS policies) will be taken into account and whether emails will be defered if the additional checks enforced by those policies fail. Similarily by default nginx uses "opportunistic TLS" for inbound mail. This can be changed by setting ``INBOUND_TLS_ENFORCE`` to ``True``. Please note that this is forbidden for From 05b57c972ec3a6fbe6a8f9cc048246e24747390c Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 30 Aug 2021 14:44:13 +0200 Subject: [PATCH 08/53] remove the static policy as it will override MTA-STS and DANE --- core/postfix/start.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/postfix/start.py b/core/postfix/start.py index 5e439bdb..69855e29 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -80,7 +80,7 @@ conf.jinja("/etc/mta-sts-daemon.yml", os.environ, "/etc/mta-sts-daemon.yml") if not os.path.exists("/etc/postfix/tls_policy.map.db"): with open("/etc/postfix/tls_policy.map", "w") as f: - for domain in ['yahoo.com', 'hotmail.com', 'aol.com', 'outlook.com', 'icloud.com', 'msn.com', 'hotmail.co.uk', 'live.com', 'yahoo.co.in', 'me.com', 'cox.net', 'yahoo.co.uk', 'verizon.net', 'ymail.com', 'hotmail.it', 'kw.com', 'yahoo.com.tw', 'mac.com', 'live.se', 'live.nl', 'yahoo.com.br', 'libero.it', 'web.de', 'allstate.com', 'btinternet.com', 'online.no', 'yahoo.com.au', 'live.dk', 'earthlink.net', 'yahoo.fr', 'yahoo.it', 'gmx.de', 'hotmail.fr', 'shawinc.com', 'yahoo.de', 'moe.edu.sg', 'naver.com', 'bigpond.com', 'statefarm.com', 'remax.net', 'rocketmail.com', 'live.no', 'yahoo.ca', 'bigpond.net.au', 'hotmail.se', 'gmx.at', 'live.co.uk', 'mail.com', 'yahoo.in', 'yandex.ru', 'qq.com', 'charter.net', 'indeedemail.com', 'alice.it', 'hotmail.de', 'bluewin.ch', 'optonline.net', 'yahoo.es', 'hotmail.no', 'pindotmedia.com', 'orange.fr', 'live.it', 'yahoo.co.id', 'yahoo.no', 'hotmail.es', 'morganstanley.com', 'wellsfargo.com', 'wanadoo.fr', 'facebook.com', 'yahoo.se', 'fema.dhs.gov', 'rogers.com', 'yahoo.com.hk', 'live.com.au', 'nic.in', 'nab.com.au', 'ubs.com', 'shaw.ca', 'umich.edu', 'westpac.com.au', 'yahoo.com.mx', 'yahoo.com.sg', 'farmersagent.com', 'yahoo.dk', 'dhs.gov']: + for domain in ['example.com']: f.write(f'{domain}\tsecure\n') os.system("postmap /etc/postfix/tls_policy.map") From 67db72d7743a5f2850e6c6a7d19b2ff99e8ea6d5 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 30 Aug 2021 17:00:12 +0200 Subject: [PATCH 09/53] Behave like documented --- core/postfix/conf/main.cf | 2 +- docs/configuration.rst | 8 ++++++-- towncrier/newsfragments/1798.feature | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index 78ffcee1..ae54326d 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -59,7 +59,7 @@ tls_ssl_options = NO_COMPRESSION, NO_TICKET smtp_tls_mandatory_protocols = !SSLv2, !SSLv3 smtp_tls_protocols =!SSLv2,!SSLv3 smtp_tls_security_level = {{ OUTBOUND_TLS_LEVEL|default('dane') }} -smtp_tls_dane_insecure_mx_policy = dane +smtp_tls_dane_insecure_mx_policy = {% if DEFER_ON_TLS_ERROR == 'false' %}may{% else %}dane{% endif %} smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map, socketmap:unix:/tmp/mta-sts.socket:postfix smtp_tls_CApath = /etc/ssl/certs smtp_tls_session_cache_database = lmdb:/dev/shm/postfix/smtp_scache diff --git a/docs/configuration.rst b/docs/configuration.rst index 4fd84c07..7cf3c926 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -72,8 +72,12 @@ mail in following format: ``[HOST]:PORT``. ``RELAYUSER`` and ``RELAYPASSWORD`` can be used when authentication is needed. By default postfix uses "opportunistic TLS" for outbound mail. This can be changed -by setting ``OUTBOUND_TLS_LEVEL`` to ``encrypt`` or ``secure``. This setting is highly recommended -if you are using a relayhost that supports TLS but discouraged otherwise. ``DEFER_ON_TLS_ERROR`` (default: True) controls whether incomplete policies (DANE without DNSSEC or "testing" MTA-STS policies) will be taken into account and whether emails will be defered if the additional checks enforced by those policies fail. +by setting ``OUTBOUND_TLS_LEVEL`` to ``encrypt`` or ``secure``. This setting is +highly recommended if you are using a relayhost that supports TLS but discouraged +otherwise. ``DEFER_ON_TLS_ERROR`` (default: True) controls whether incomplete +policies (DANE without DNSSEC or "testing" MTA-STS policies) will be taken into +account and whether emails will be defered if the additional checks enforced by +those policies fail. Similarily by default nginx uses "opportunistic TLS" for inbound mail. This can be changed by setting ``INBOUND_TLS_ENFORCE`` to ``True``. Please note that this is forbidden for diff --git a/towncrier/newsfragments/1798.feature b/towncrier/newsfragments/1798.feature index 1b63a85c..125b1767 100644 --- a/towncrier/newsfragments/1798.feature +++ b/towncrier/newsfragments/1798.feature @@ -1 +1 @@ -Implement MTA-STS (use published policies) +Implement MTA-STS and DANE validation. Introduce DEFER_ON_TLS_ERROR (default: True) to harden or loosen the policy enforcement. From fccb0cc57f82f88235d3d10d58be4aff7762a483 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 30 Aug 2021 17:16:41 +0200 Subject: [PATCH 10/53] Add a longer max_age (15days) --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index 92b208de..b5627743 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -383,7 +383,7 @@ Mailu can serve an `MTA-STS policy`_; To configure it you will need to: location ^~ /.well-known/mta-sts.txt { return 200 "version: STSv1 mode: enforce - max_age: 86401 + max_age: 1296000 mx: mailu.example.com\r\n"; } From fb34f5349348ebc22af08737a6ef641bd6a156a7 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 30 Aug 2021 17:18:19 +0200 Subject: [PATCH 11/53] Do operations in the right (safe) order --- docs/faq.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/faq.rst b/docs/faq.rst index b5627743..fb6f66df 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -374,7 +374,7 @@ How do I setup a MTA-STS policy? Mailu can serve an `MTA-STS policy`_; To configure it you will need to: -1. setup the appropriate DNS/CNAME record (``mta-sts.example.com`` -> ``mailu.example.com``) and DNS/TXT record (``_mta-sts.example.com`` -> ``v=STSv1; id=1``) paying attention to the ``TTL`` as this is used by MTA-STS. +1. add ``mta-sts.example.com`` to the ``HOSTNAMES`` configuration variable (and ensure that a valid SSL certificate is available for it) 2. configure an override with the policy itself; for example, your ``overrides/nginx/mta-sts.conf`` could read: @@ -387,7 +387,7 @@ Mailu can serve an `MTA-STS policy`_; To configure it you will need to: mx: mailu.example.com\r\n"; } -3. add ``mta-sts.example.com`` to the ``HOSTNAMES`` configuration variable (and ensure that a valid SSL certificate is available for it) +3. setup the appropriate DNS/CNAME record (``mta-sts.example.com`` -> ``mailu.example.com``) and DNS/TXT record (``_mta-sts.example.com`` -> ``v=STSv1; id=1``) paying attention to the ``TTL`` as this is used by MTA-STS. *issue reference:* `1798`_. From d607ba0ef2b345a44f9f95cd227a73f0a63f9489 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 30 Aug 2021 17:52:31 +0200 Subject: [PATCH 12/53] Clarify that a restart may be required --- docs/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.rst b/docs/faq.rst index fb6f66df..43fb8606 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -374,7 +374,7 @@ How do I setup a MTA-STS policy? Mailu can serve an `MTA-STS policy`_; To configure it you will need to: -1. add ``mta-sts.example.com`` to the ``HOSTNAMES`` configuration variable (and ensure that a valid SSL certificate is available for it) +1. add ``mta-sts.example.com`` to the ``HOSTNAMES`` configuration variable (and ensure that a valid SSL certificate is available for it; this may mean restarting your smtp container) 2. configure an override with the policy itself; for example, your ``overrides/nginx/mta-sts.conf`` could read: From a1da4daa4c30668d98325cebd640ae3cf2f5fc97 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Tue, 31 Aug 2021 20:24:06 +0200 Subject: [PATCH 13/53] Implement the DANE-only lookup policyd https://github.com/Snawoot/postfix-mta-sts-resolver/issues/67 for context --- core/admin/mailu/internal/views/postfix.py | 3 +++ core/admin/mailu/utils.py | 24 +++++++++++++++++++++- core/postfix/conf/main.cf | 2 +- core/postfix/start.py | 1 + 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/internal/views/postfix.py b/core/admin/mailu/internal/views/postfix.py index 2e7d0b9b..330fed5b 100644 --- a/core/admin/mailu/internal/views/postfix.py +++ b/core/admin/mailu/internal/views/postfix.py @@ -7,6 +7,9 @@ import idna import re import srslib +@internal.route("/postfix/dane/") +def postfix_dane_map(domain_name): + return flask.jsonify('dane-only') if utils.has_dane_record(domain_name) else flask.abort(404) @internal.route("/postfix/domain/") def postfix_mailbox_domain(domain_name): diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index 02150754..914638fa 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -6,6 +6,9 @@ try: except ImportError: import pickle +import dns +import dns.resolver + import hmac import secrets import time @@ -25,7 +28,6 @@ from itsdangerous.encoding import want_bytes from werkzeug.datastructures import CallbackDict from werkzeug.contrib import fixers - # Login configuration login = flask_login.LoginManager() login.login_view = "ui.login" @@ -37,6 +39,26 @@ def handle_needs_login(): flask.url_for('ui.login', next=flask.request.endpoint) ) +# DNS stub configured to do DNSSEC enabled queries +resolver = dns.resolver.Resolver() +resolver.use_edns(0, 0, 1500) +resolver.flags = dns.flags.AD | dns.flags.RD + +def has_dane_record(domain, timeout=5): + try: + result = resolver.query(f'_25._tcp.{domain}', dns.rdatatype.TLSA,dns.rdataclass.IN, lifetime=timeout) + if (result.response.flags & dns.flags.AD) == dns.flags.AD: + for record in result: + if isinstance(record, dns.rdtypes.ANY.TLSA.TLSA): + record.validate() + if record.usage in [2,3]: # postfix wants DANE-only + return True + except dns.resolver.NoNameservers: + # this could be an attack / a failed DNSSEC lookup + return True + except: + pass + # Rate limiter limiter = limiter.LimitWraperFactory() diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index ae54326d..16fdfa6e 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -60,7 +60,7 @@ smtp_tls_mandatory_protocols = !SSLv2, !SSLv3 smtp_tls_protocols =!SSLv2,!SSLv3 smtp_tls_security_level = {{ OUTBOUND_TLS_LEVEL|default('dane') }} smtp_tls_dane_insecure_mx_policy = {% if DEFER_ON_TLS_ERROR == 'false' %}may{% else %}dane{% endif %} -smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map, socketmap:unix:/tmp/mta-sts.socket:postfix +smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map, ${podop}dane, socketmap:unix:/tmp/mta-sts.socket:postfix smtp_tls_CApath = /etc/ssl/certs smtp_tls_session_cache_database = lmdb:/dev/shm/postfix/smtp_scache smtpd_tls_session_cache_database = lmdb:/dev/shm/postfix/smtpd_scache diff --git a/core/postfix/start.py b/core/postfix/start.py index 69855e29..03dca93c 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -21,6 +21,7 @@ def start_podop(): run_server(0, "postfix", "/tmp/podop.socket", [ ("transport", "url", url + "transport/§"), ("alias", "url", url + "alias/§"), + ("dane", "url", url + "dane/§"), ("domain", "url", url + "domain/§"), ("mailbox", "url", url + "mailbox/§"), ("recipientmap", "url", url + "recipient/map/§"), From 9f66e2672b3c646570fba1f644f1b35e44ebaeec Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Tue, 31 Aug 2021 20:44:57 +0200 Subject: [PATCH 14/53] Use DEFER_ON_TLS_ERROR here too We just don't know whether the lookup failed because we are under attack or whether it's a glitch; the safe behaviour is to defer --- core/admin/mailu/configuration.py | 1 + core/admin/mailu/utils.py | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 7cd3a56b..4c48fcc4 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -35,6 +35,7 @@ DEFAULT_CONFIG = { 'WILDCARD_SENDERS': '', 'TLS_FLAVOR': 'cert', 'INBOUND_TLS_ENFORCE': False, + 'DEFER_ON_TLS_ERROR': True, 'AUTH_RATELIMIT': '1000/minute;10000/hour', 'AUTH_RATELIMIT_SUBNET': False, 'DISABLE_STATISTICS': False, diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index 914638fa..2313a1e6 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -54,8 +54,10 @@ def has_dane_record(domain, timeout=5): if record.usage in [2,3]: # postfix wants DANE-only return True except dns.resolver.NoNameservers: - # this could be an attack / a failed DNSSEC lookup - return True + # If the DNSSEC data is invalid and the DNS resolver is DNSSEC enabled + # we will receive this non-specific exception. The safe behaviour is to + # accept to defer the email. + return app.config['DEFER_ON_TLS_ERROR'] except: pass From 489520f0673a25482294cb5b1b7d6bb28a3b8dd1 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 1 Sep 2021 08:41:39 +0200 Subject: [PATCH 15/53] forgot about alpine/lmdb --- core/postfix/conf/main.cf | 2 +- core/postfix/start.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf index 16fdfa6e..6152388c 100644 --- a/core/postfix/conf/main.cf +++ b/core/postfix/conf/main.cf @@ -60,7 +60,7 @@ smtp_tls_mandatory_protocols = !SSLv2, !SSLv3 smtp_tls_protocols =!SSLv2,!SSLv3 smtp_tls_security_level = {{ OUTBOUND_TLS_LEVEL|default('dane') }} smtp_tls_dane_insecure_mx_policy = {% if DEFER_ON_TLS_ERROR == 'false' %}may{% else %}dane{% endif %} -smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map, ${podop}dane, socketmap:unix:/tmp/mta-sts.socket:postfix +smtp_tls_policy_maps=lmdb:/etc/postfix/tls_policy.map, ${podop}dane, socketmap:unix:/tmp/mta-sts.socket:postfix smtp_tls_CApath = /etc/ssl/certs smtp_tls_session_cache_database = lmdb:/dev/shm/postfix/smtp_scache smtpd_tls_session_cache_database = lmdb:/dev/shm/postfix/smtpd_scache diff --git a/core/postfix/start.py b/core/postfix/start.py index 03dca93c..8aa279ef 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -20,7 +20,7 @@ def start_podop(): # TODO: Remove verbosity setting from Podop? run_server(0, "postfix", "/tmp/podop.socket", [ ("transport", "url", url + "transport/§"), - ("alias", "url", url + "alias/§"), + ("alias", "url", url + "alias/§"), ("dane", "url", url + "dane/§"), ("domain", "url", url + "domain/§"), ("mailbox", "url", url + "mailbox/§"), @@ -79,7 +79,7 @@ if os.path.exists("/overrides/mta-sts-daemon.yml"): shutil.copyfile("/overrides/mta-sts-daemon.yml", "/etc/mta-sts-daemon.yml") conf.jinja("/etc/mta-sts-daemon.yml", os.environ, "/etc/mta-sts-daemon.yml") -if not os.path.exists("/etc/postfix/tls_policy.map.db"): +if not os.path.exists("/etc/postfix/tls_policy.map.lmdb"): with open("/etc/postfix/tls_policy.map", "w") as f: for domain in ['example.com']: f.write(f'{domain}\tsecure\n') From 92cc664e82f3f40f9a247093d440c3b75ff768d8 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 1 Sep 2021 08:41:59 +0200 Subject: [PATCH 16/53] Cosmetic change --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c7d1640..4c19ad78 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Main features include: - **Web access**, multiple Webmails and administration interface - **User features**, aliases, auto-reply, auto-forward, fetched accounts - **Admin features**, global admins, announcements, per-domain delegation, quotas -- **Security**, enforced TLS, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner +- **Security**, enforced TLS, DANE, MTA-STS, Letsencrypt!, outgoing DKIM, anti-virus scanner - **Antispam**, auto-learn, greylisting, DMARC and SPF - **Freedom**, all FOSS components, no tracker included From c1d94bb72563430d151916de0e3d9e31708348fe Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 1 Sep 2021 09:01:04 +0200 Subject: [PATCH 17/53] Ensure that postfix will be able to use the TLSA records see https://www.huque.com/dane/testsite/ for the testcases --- core/admin/mailu/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index 2313a1e6..66cf0476 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -44,14 +44,14 @@ resolver = dns.resolver.Resolver() resolver.use_edns(0, 0, 1500) resolver.flags = dns.flags.AD | dns.flags.RD -def has_dane_record(domain, timeout=5): +def has_dane_record(domain, timeout=10): try: result = resolver.query(f'_25._tcp.{domain}', dns.rdatatype.TLSA,dns.rdataclass.IN, lifetime=timeout) if (result.response.flags & dns.flags.AD) == dns.flags.AD: for record in result: if isinstance(record, dns.rdtypes.ANY.TLSA.TLSA): record.validate() - if record.usage in [2,3]: # postfix wants DANE-only + if record.usage in [2,3] and record.selector in [0,1] and record.mtype in [0,1,2]: return True except dns.resolver.NoNameservers: # If the DNSSEC data is invalid and the DNS resolver is DNSSEC enabled From 4abf49edf429e2ebb594a002c9f5e922a6e824e7 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 1 Sep 2021 09:15:13 +0200 Subject: [PATCH 18/53] indent --- core/postfix/start.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/postfix/start.py b/core/postfix/start.py index 8aa279ef..19c23c19 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -19,10 +19,10 @@ def start_podop(): url = "http://" + os.environ["ADMIN_ADDRESS"] + "/internal/postfix/" # TODO: Remove verbosity setting from Podop? run_server(0, "postfix", "/tmp/podop.socket", [ - ("transport", "url", url + "transport/§"), - ("alias", "url", url + "alias/§"), - ("dane", "url", url + "dane/§"), - ("domain", "url", url + "domain/§"), + ("transport", "url", url + "transport/§"), + ("alias", "url", url + "alias/§"), + ("dane", "url", url + "dane/§"), + ("domain", "url", url + "domain/§"), ("mailbox", "url", url + "mailbox/§"), ("recipientmap", "url", url + "recipient/map/§"), ("sendermap", "url", url + "sender/map/§"), From fe186afb6f319fb6cc9dc5f903db7fa4a7414532 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 1 Sep 2021 18:52:35 +0200 Subject: [PATCH 19/53] Revert "Switch to openssl to workaround alpine #12763" This reverts commit f8362d04e4d46c44ab07beffb77cdd041af193c0. --- core/admin/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/admin/Dockerfile b/core/admin/Dockerfile index 97cf1736..f10d3251 100644 --- a/core/admin/Dockerfile +++ b/core/admin/Dockerfile @@ -24,9 +24,9 @@ RUN mkdir -p /app WORKDIR /app COPY requirements-prod.txt requirements.txt -RUN apk add --no-cache openssl curl postgresql-libs mariadb-connector-c \ +RUN apk add --no-cache libressl curl postgresql-libs mariadb-connector-c \ && apk add --no-cache --virtual build-dep \ - openssl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev cargo \ + libressl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev cargo \ && pip3 install -r requirements.txt \ && apk del --no-cache build-dep From 0c4455ccf5e453f5dc2e1810601696d7b4707f01 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 1 Sep 2021 18:53:20 +0200 Subject: [PATCH 20/53] Revert "Rollback to alpine 1.12" This reverts commit e1ddbb6eec85cde7a6efed13f66e887c56334a80. --- optional/unbound/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optional/unbound/Dockerfile b/optional/unbound/Dockerfile index abb45420..2b472d44 100644 --- a/optional/unbound/Dockerfile +++ b/optional/unbound/Dockerfile @@ -1,4 +1,4 @@ -ARG DISTRO=alpine:3.12 +ARG DISTRO=alpine:3.14 FROM $DISTRO # python3 shared with most images RUN apk add --no-cache \ From d7c2b510c7321cfa79989f6817f16e63e88946de Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 1 Sep 2021 18:56:44 +0200 Subject: [PATCH 21/53] Give alpine 3.14.2 a shot --- core/admin/Dockerfile | 2 +- core/dovecot/Dockerfile | 2 +- core/nginx/Dockerfile | 2 +- core/none/Dockerfile | 2 +- core/postfix/Dockerfile | 2 +- core/rspamd/Dockerfile | 2 +- optional/clamav/Dockerfile | 2 +- optional/fetchmail/Dockerfile | 2 +- optional/postgresql/Dockerfile | 2 +- optional/radicale/Dockerfile | 2 +- optional/unbound/Dockerfile | 2 +- setup/Dockerfile | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/admin/Dockerfile b/core/admin/Dockerfile index f10d3251..4b991269 100644 --- a/core/admin/Dockerfile +++ b/core/admin/Dockerfile @@ -1,5 +1,5 @@ # First stage to build assets -ARG DISTRO=alpine:3.14 +ARG DISTRO=alpine:3.14.2 ARG ARCH="" FROM ${ARCH}node:8 as assets COPY --from=balenalib/rpi-alpine:3.14 /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static diff --git a/core/dovecot/Dockerfile b/core/dovecot/Dockerfile index 22145bde..49fcb866 100644 --- a/core/dovecot/Dockerfile +++ b/core/dovecot/Dockerfile @@ -1,4 +1,4 @@ -ARG DISTRO=alpine:3.14 +ARG DISTRO=alpine:3.14.2 FROM $DISTRO as builder WORKDIR /tmp RUN apk add git build-base automake autoconf libtool dovecot-dev xapian-core-dev icu-dev diff --git a/core/nginx/Dockerfile b/core/nginx/Dockerfile index 1906ed31..3837e64b 100644 --- a/core/nginx/Dockerfile +++ b/core/nginx/Dockerfile @@ -1,4 +1,4 @@ -ARG DISTRO=alpine:3.14 +ARG DISTRO=alpine:3.14.2 FROM $DISTRO # python3 shared with most images RUN apk add --no-cache \ diff --git a/core/none/Dockerfile b/core/none/Dockerfile index 51b8d1c5..bae5e8a3 100644 --- a/core/none/Dockerfile +++ b/core/none/Dockerfile @@ -1,6 +1,6 @@ # This is an idle image to dynamically replace any component if disabled. -ARG DISTRO=alpine:3.14 +ARG DISTRO=alpine:3.14.2 FROM $DISTRO CMD sleep 1000000d diff --git a/core/postfix/Dockerfile b/core/postfix/Dockerfile index 062155c1..652404e8 100644 --- a/core/postfix/Dockerfile +++ b/core/postfix/Dockerfile @@ -1,4 +1,4 @@ -ARG DISTRO=alpine:3.14 +ARG DISTRO=alpine:3.14.2 FROM $DISTRO # python3 shared with most images RUN apk add --no-cache \ diff --git a/core/rspamd/Dockerfile b/core/rspamd/Dockerfile index 6706ef14..0b3b94f7 100644 --- a/core/rspamd/Dockerfile +++ b/core/rspamd/Dockerfile @@ -1,4 +1,4 @@ -ARG DISTRO=alpine:3.14 +ARG DISTRO=alpine:3.14.2 FROM $DISTRO # python3 shared with most images RUN apk add --no-cache \ diff --git a/optional/clamav/Dockerfile b/optional/clamav/Dockerfile index 20cebcdc..efad01ad 100644 --- a/optional/clamav/Dockerfile +++ b/optional/clamav/Dockerfile @@ -1,4 +1,4 @@ -ARG DISTRO=alpine:3.14 +ARG DISTRO=alpine:3.14.2 FROM $DISTRO # python3 shared with most images RUN apk add --no-cache \ diff --git a/optional/fetchmail/Dockerfile b/optional/fetchmail/Dockerfile index 506e409a..d3397a22 100644 --- a/optional/fetchmail/Dockerfile +++ b/optional/fetchmail/Dockerfile @@ -1,4 +1,4 @@ -ARG DISTRO=alpine:3.14 +ARG DISTRO=alpine:3.14.2 FROM $DISTRO # python3 shared with most images diff --git a/optional/postgresql/Dockerfile b/optional/postgresql/Dockerfile index 0f5034da..ab197b62 100644 --- a/optional/postgresql/Dockerfile +++ b/optional/postgresql/Dockerfile @@ -1,4 +1,4 @@ -ARG DISTRO=alpine:3.14 +ARG DISTRO=alpine:3.14.2 FROM $DISTRO # python3 shared with most images RUN apk add --no-cache \ diff --git a/optional/radicale/Dockerfile b/optional/radicale/Dockerfile index 13761164..21c1d437 100644 --- a/optional/radicale/Dockerfile +++ b/optional/radicale/Dockerfile @@ -1,4 +1,4 @@ -ARG DISTRO=alpine:3.14 +ARG DISTRO=alpine:3.14.2 FROM $DISTRO # python3 shared with most images diff --git a/optional/unbound/Dockerfile b/optional/unbound/Dockerfile index 2b472d44..da979496 100644 --- a/optional/unbound/Dockerfile +++ b/optional/unbound/Dockerfile @@ -1,4 +1,4 @@ -ARG DISTRO=alpine:3.14 +ARG DISTRO=alpine:3.14.2 FROM $DISTRO # python3 shared with most images RUN apk add --no-cache \ diff --git a/setup/Dockerfile b/setup/Dockerfile index 5775ab6b..e0f685ee 100644 --- a/setup/Dockerfile +++ b/setup/Dockerfile @@ -1,4 +1,4 @@ -ARG DISTRO=alpine:3.14 +ARG DISTRO=alpine:3.14.2 FROM $DISTRO RUN mkdir -p /app From 39d7a5c504e93f1f6940f7fb2e42256681970ee6 Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Thu, 2 Sep 2021 20:46:08 +0200 Subject: [PATCH 22/53] pngcrushed images --- core/nginx/static/android-chrome-192x192.png | Bin 6274 -> 4993 bytes core/nginx/static/android-chrome-512x512.png | Bin 18169 -> 15628 bytes core/nginx/static/apple-touch-icon.png | Bin 5802 -> 4662 bytes core/nginx/static/favicon-16x16.png | Bin 978 -> 924 bytes core/nginx/static/favicon-32x32.png | Bin 1523 -> 1483 bytes core/nginx/static/mstile-150x150.png | Bin 4788 -> 3897 bytes 6 files changed, 0 insertions(+), 0 deletions(-) diff --git a/core/nginx/static/android-chrome-192x192.png b/core/nginx/static/android-chrome-192x192.png index 86231db9042b8cf7191d469d5c1c39c164ba9514..75665d8c2e5be997b14c00a099033bee75f9a94b 100644 GIT binary patch delta 4723 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 ByypM_ delta 6014 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

NIAd)ssrbR-8y;tIHC6fL_n5E-j`A!f4 z^r6S4?oTFv=$cIS<(F;P&$}~W^7Yky#qTSw5aa3$9y~T$_WUPg$i^WL(F?Q!uyh=` zo8ng^^M8QZZnw~rWb%h@T-(p?q{%mrY-DVShgEd+r@8XgK7RIQ0{2%O@?LIG&m-7O zCIkxmW`J%*d?RHT;wJpCJ1764&_;Z6wujH02S=ptj4Ibb*U`Or?g9Ts$eyuv!fwsUQrp^D?a1v~>B=5~vHhXHTK(p=CQBMt1)k|kc^ z8D#tKKJ4oPi89i_?+J3rL?-PxQAY%#xRCIdtqdr23FvksjNc7Em(st>7lBiO9YYyE4DSMgRE_&3ZU~^-4Y*wN{-uHc zVT5^X{>^=sVje+O(GKj64E}2u$Dsw1%kSebyiX7_s+<;*LdHjkrRJ z;#ExGJ__vpp-=%>qriPgVv(YFkv6iObOF6T6n`*YJ8&0rz?hWX*RNr^UDoB%9k{Sg@n6vYcX2>c&VRPPU^ zi~#Zh3E)-+)mTv!7gz~=9~das_@TVh;a>-CME4FW?$Ha}KpDl{N)v#kv|j*^x_uXv zs^wu|IZ(9glcl_*S=lo%7k^2XJKXKNp;Q&CfR6xcN;Q5c&jrfz58yhqsIF2ub_3U9 zL*-$4@vPRCh#NR*FedxPZpa_yCM#J7kJji8>3VQe+F)%#Hi0;?AbVH zZK6L8iOEiP=iVp*XFYHx@Uk7o4|a_JR%4z6u0iJmiv4s0UjtsY^M6h4a)EM1@DOkt znlPu>M*>N8kgd5k_5NU|0IY^3ki`kzL8jQnLx=!u@%wYJR{$2ffd2xXa`wI`Rlx6o zuL8U6HhyqI09GU41U?VE;@rJa0?yxoFCb_8I)XDJfF*)gfd51f-cyQa4R9IRv#TBG z{lPgGD7V2+1E0e|6@M#%V2Ib~37de+fhPe+jUSvA zfOP>TvUGhVvZG8vGvqYQDDms3=>hOnee zgn%=ETYxDQ{d~o%yoCfyY)Nn{hRPSw77-Z8$ur*rW>&lnihq|_hfH|?NCkWTEbdg1 zx02^hK=yMU(I@OLPn}FE|;fb-P*0uq2S=$7_)|zkjD(p22uq0FdhvH3R1$OXkzu zZk946?a1o>{YddD&$&E<@xB1ESfjxrB%$s!G+gQx_8@sYHzKipPxtz{_(uR)thvGI zNZ$BD++5JBZc<2@lAD3w0WrTAeir`;0CL@;slc+#LyK`$4ih;z{T|?_z*fNX?#@v7 zQvg}4^ndEPz=gn4V4~a23OF4|$(y^8qjS@KH2f_77C;seLM^ZuNu@Xy@b9KlpcGlG{!5IUT@n5W`=A1I4M_Bsc=d#TpS7AjMQa zgoI9YvRoJ`#K(UOS;$&Nzzjc2Fa?lBgir&_Mt?+b1~3mzt;k0VS-koc@LMFV?`t1_ zB!Vq~EY=#rc%+2;8NhpxJ;~KRDx{GO=}#ehGG0K=-SuTur&y{(09mYgLLD+HychTY zvbZ*(itdMrROWaYcmntn(%TOJRblv9s^tscA4?2TU=EUkeIl~3HW3KB-9(#70-eZ2 z_J1j)V&CgPA5c|BpG)-$AQwvv4ZtiU`R;fmtU41Iga2HmG?I_gj`Z};Xa0SI?Agv$ zYV^6N00v`;A%x5sW+N`rVx)S-w9M;X%SufkRrlUPPET*mJk}#VeV`|^rJ@2D2}=+) zz-T1>Vis@&a40fDOa;awBSjs~j233*5nD%k_AX!xQh9qFQboBv^I8v(QiC5E6+o$2 zA=U_zsyzib2sjuSB_<6O}(*^XR s#$Gc254p}h($07*qoM6N<$f+aoj&j0`b diff --git a/core/nginx/static/android-chrome-512x512.png b/core/nginx/static/android-chrome-512x512.png index f029c9fd484a7603a43cb63159b8d95fca06f95a..32a69b21675b748d731d9cd624c8624a412f35f3 100644 GIT binary patch literal 15628 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 diff --git a/core/nginx/static/favicon-16x16.png b/core/nginx/static/favicon-16x16.png index ff35d4b0a7618b3b4df63d27c6d0793ced5735d1..6affc1f31df4cb428670cc8f6411d827946f7333 100644 GIT binary patch delta 212 zcmV;_04x8}2b>4690Gr)NklXFs!|adOsc*@i;xX6Q%u6(r0Rw>o2$_?F13nN|pfafd O0000xLS delta 266 zcmV+l0rmcz2hs z!O;mQAjTjL5^#2Lb#wP{GV=72010^e`1<(=1O^3%1cpj7NEv2YhJ{B&Mn%WO#>K_P zOEbtACL|^$r=%LCrDtShX2~+h$!ELf0BttR&CAa(P*7x0QZ_V#I11zx6;%dCHFbnT zHPnE9VN}!9f^fBz)fmD4)z;C~)7RJ2HP8lf0bMOF-=5jEFq6vyfdY7(lTHIZ5b{Db Ql>h($07*qoM6N<$f-bOhmjD0& diff --git a/core/nginx/static/favicon-32x32.png b/core/nginx/static/favicon-32x32.png index 2b6b749b92a5d0f19c470f28ae0cc8eb70efec23..3b9362452f4349cd82f468dfb034fdca9ccd6560 100644 GIT binary patch delta 514 zcmV+d0{#8-3(E_z9|M2VNkl+W-RJ5UNjlUU1igt&amq`y(V;VI9?p%>#>npwU5xI~-fY#yA|=s6`!x6+&IxdmIL5p%q0X1{uLEH% zpoR8nxUt|oQhp8aB~sE3M}9bQD3F9wI~+Oht#YUuMbY2QF#JP99LIoMflQaQ-lM#< z)|FEQ;$kh(YEpl8IWnCR-(68F1JX9BiWNHGvJ;s~Sj=@Vl?kK8Yf@~07*qoM6N<$ Eg1{5@=>Px# delta 555 zcmV+`0@VG>3-b%G9|M2-NklF2fFfxiu#A5*x;t&;#jFM9ESb+o= zSOufBOgs)C%m-A!C@UwAOM$$CEF%M>qEb9gAfU|1AgdCO2dIC_GN`G87zv4}APK5K zLz6*E8%QUoq^2Q*)btF90&N`zT@am_nuQ9op$g*l81z9@4oE?6UOosE6e25-VlaR! zC@L-~EiEgrs61c3s0oRfV8J`nOkHI)DW002ovPDHLkV1l>b>3;wK diff --git a/core/nginx/static/mstile-150x150.png b/core/nginx/static/mstile-150x150.png index 04c609f70489bd0c6579c3fb4723a217eb24168a..71c4702d19e1aff3ba8befca74cbfcff6431648e 100644 GIT binary patch delta 3618 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 Date: Thu, 2 Sep 2021 20:46:56 +0200 Subject: [PATCH 23/53] pre-compress assets (*.ico for now) --- core/nginx/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/nginx/Dockerfile b/core/nginx/Dockerfile index 1906ed31..8b5be4f8 100644 --- a/core/nginx/Dockerfile +++ b/core/nginx/Dockerfile @@ -16,6 +16,8 @@ COPY conf /conf COPY static /static COPY *.py / +RUN gzip -k9 /static/*.ico + EXPOSE 80/tcp 443/tcp 110/tcp 143/tcp 465/tcp 587/tcp 993/tcp 995/tcp 25/tcp 10025/tcp 10143/tcp VOLUME ["/certs"] VOLUME ["/overrides"] From f4e7ce099052486d64eafdfb112f4a4f4a68b358 Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Thu, 2 Sep 2021 20:48:44 +0200 Subject: [PATCH 24/53] enabled caching, gzip and robots.txt --- core/nginx/conf/nginx.conf | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index 9ce12980..616ddcc1 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -33,6 +33,17 @@ http { default $http_x_forwarded_proto; '' $scheme; } + map $uri $expires { + default off; + ~*\.(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ 97d; + } + + # compression + gzip on; + gzip_static on; + gzip_types text/plain text/css application/xml application/javascript + gzip_min_length 1024; + # TODO: figure out how to server pre-compressed assets from admin container {% if KUBERNETES_INGRESS != 'true' and TLS_FLAVOR in [ 'letsencrypt', 'cert' ] %} # Enable the proxy for certbot if the flavor is letsencrypt and not on kubernetes @@ -46,10 +57,16 @@ http { proxy_pass http://127.0.0.1:8008; } {% endif %} + # robots.txt + location = /robots.txt { + add_header Content-Type text/plain; + return 200 "User-agent: *\nDisallow: /\n"; + } # redirect to https location / { return 301 https://$host$request_uri; } + } {% endif %} @@ -95,6 +112,8 @@ http { proxy_hide_header X-XSS-Protection; proxy_hide_header X-Powered-By; + expires $expires; + add_header X-Frame-Options 'SAMEORIGIN'; add_header X-Content-Type-Options 'nosniff'; add_header X-Permitted-Cross-Domain-Policies 'none'; @@ -114,6 +133,12 @@ http { } {% else %} + # robots.txt + location = /robots.txt { + add_header Content-Type text/plain; + return 200 "User-agent: *\nDisallow: /\n"; + } + include /overrides/*.conf; # Actual logic From 34df8b316832b43a2962c74ff7fd53c5ae725745 Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Thu, 2 Sep 2021 22:49:36 +0200 Subject: [PATCH 25/53] AdminLTE3 optimizations & compression and caching - fixed copy of qemu-arm-static for alpine - added 'set -eu' safeguard - silenced npm update notification - added color to webpack call - changed Admin-LTE default blue (core/admin/Dockerfile) - AdminLTE 3 style tweaks (core/admin/assets/app.css) (core/admin/mailu/ui/templates/base.html) (core/admin/mailu/ui/templates/sidebar.html) - localized datatables (core/admin/Dockerfile) (core/admin/assets/app.js) (core/admin/package.json) - moved external javascript code to vendor.js (core/admin/assets/app.js) (core/admin/assets/vendor.js) (core/admin/webpack.config.js) - added mailu logo (core/admin/assets/app.js) (core/admin/assets/app.css) (core/admin/assets/mailu.png) - moved all inline javascript to app.js (core/admin/assets/app.js) (core/admin/mailu/ui/templates/domain/create.html) (core/admin/mailu/ui/templates/user/create.html) - added iframe display of rspamd page (core/admin/assets/app.js) (core/admin/mailu/ui/views/base.py) (core/admin/mailu/ui/templates/sidebar.html) (core/admin/mailu/ui/templates/antispam.html) - updated language-selector to display full language names and use post (core/admin/assets/app.js) (core/admin/mailu/__init__.py) (core/admin/mailu/utils.py) (core/admin/mailu/ui/views/languages.py) - added fieldset to group and en/disable input fields (core/admin/assets/app.js) (core/admin/mailu/ui/templates/macros.html) (core/admin/mailu/ui/templates/user/settings.html) (core/admin/mailu/ui/templates/user/reply.html) - added clipboard copy buttons (core/admin/assets/app.js) (core/admin/assets/vendor.js) (core/admin/mailu/ui/templates/macros.html) (core/admin/mailu/ui/templates/domain/details.html) - cleaned external javascript imports (core/admin/assets/vendor.js) - pre-split first hostname for further use (core/admin/mailu/__init__.py) (core/admin/mailu/models.py) (core/admin/mailu/ui/templates/client.html) (core/admin/mailu/ui/templates/domain/signup.html) - cache dns_* properties of domain object (immutable during runtime) (core/admin/mailu/models.py) (core/admin/mailu/ui/templates/domain/details.html) - fixed and splitted dns_dkim property of domain object (space missing) - added autoconfig and tlsa properties to domain object (core/admin/mailu/models.py) - suppressed extra vertical spacing in jinja2 templates - improved accessibility for screen reader (core/admin/mailu/ui/templates/**.html) - deleted unused/broken /user/forward route (core/admin/mailu/ui/templates/user/forward.html) (core/admin/mailu/ui/views/users.py) - updated gunicorn to 20.1.0 to get rid of buffering error at startup (core/admin/requirements-prod.txt) - switched webpack to production mode (core/admin/webpack.config.js) - added css and javascript minimization - added pre-compression of assets (gzip) (core/admin/webpack.config.js) (core/admin/package.json) - removed obsolte dependencies - switched from node-sass to dart-sass (core/admin/package.json) - changed startup cleaning message from error to info (core/admin/mailu/utils.py) - move client config to "my account" section when logged in (core/admin/mailu/ui/templates/sidebar.html) --- core/admin/Dockerfile | 35 +-- core/admin/assets/app.css | 54 +++-- core/admin/assets/app.js | 73 +++++- core/admin/assets/mailu.png | Bin 0 -> 4935 bytes core/admin/assets/vendor.js | 40 ++-- core/admin/mailu/__init__.py | 16 +- core/admin/mailu/configuration.py | 1 + core/admin/mailu/models.py | 47 +++- core/admin/mailu/ui/forms.py | 2 +- .../mailu/ui/templates/admin/create.html | 14 +- core/admin/mailu/ui/templates/admin/list.html | 22 +- .../mailu/ui/templates/alias/create.html | 18 +- core/admin/mailu/ui/templates/alias/edit.html | 10 +- core/admin/mailu/ui/templates/alias/list.html | 26 +-- .../ui/templates/alternative/create.html | 10 +- .../mailu/ui/templates/alternative/list.html | 26 +-- .../mailu/ui/templates/announcement.html | 14 +- core/admin/mailu/ui/templates/antispam.html | 15 ++ core/admin/mailu/ui/templates/base.html | 70 +++--- core/admin/mailu/ui/templates/client.html | 38 ++-- core/admin/mailu/ui/templates/confirm.html | 18 +- .../mailu/ui/templates/docker-error.html | 16 +- .../mailu/ui/templates/domain/create.html | 19 +- .../mailu/ui/templates/domain/details.html | 71 +++--- .../admin/mailu/ui/templates/domain/edit.html | 10 +- .../admin/mailu/ui/templates/domain/list.html | 34 +-- .../mailu/ui/templates/domain/signup.html | 26 +-- .../mailu/ui/templates/fetch/create.html | 26 +-- core/admin/mailu/ui/templates/fetch/edit.html | 10 +- core/admin/mailu/ui/templates/fetch/list.html | 26 +-- core/admin/mailu/ui/templates/form.html | 10 +- core/admin/mailu/ui/templates/login.html | 10 +- core/admin/mailu/ui/templates/macros.html | 141 +++++++----- .../mailu/ui/templates/manager/create.html | 18 +- .../mailu/ui/templates/manager/list.html | 26 +-- .../mailu/ui/templates/relay/create.html | 6 +- core/admin/mailu/ui/templates/relay/edit.html | 10 +- core/admin/mailu/ui/templates/relay/list.html | 26 +-- core/admin/mailu/ui/templates/sidebar.html | 210 +++++++++--------- .../mailu/ui/templates/token/create.html | 10 +- core/admin/mailu/ui/templates/token/list.html | 26 +-- .../admin/mailu/ui/templates/user/create.html | 27 ++- core/admin/mailu/ui/templates/user/edit.html | 10 +- .../mailu/ui/templates/user/forward.html | 25 --- core/admin/mailu/ui/templates/user/list.html | 28 +-- .../mailu/ui/templates/user/password.html | 10 +- core/admin/mailu/ui/templates/user/reply.html | 35 ++- .../mailu/ui/templates/user/settings.html | 46 ++-- .../admin/mailu/ui/templates/user/signup.html | 22 +- .../ui/templates/user/signup_domain.html | 22 +- core/admin/mailu/ui/templates/working.html | 6 +- core/admin/mailu/ui/views/base.py | 5 + core/admin/mailu/ui/views/languages.py | 6 +- core/admin/mailu/ui/views/users.py | 17 -- core/admin/mailu/utils.py | 13 +- core/admin/package.json | 17 +- core/admin/requirements-prod.txt | 2 +- core/admin/webpack.config.js | 101 +++++---- 58 files changed, 912 insertions(+), 760 deletions(-) create mode 100644 core/admin/assets/mailu.png create mode 100644 core/admin/mailu/ui/templates/antispam.html delete mode 100644 core/admin/mailu/ui/templates/user/forward.html diff --git a/core/admin/Dockerfile b/core/admin/Dockerfile index fa75e8dc..edb8c1fd 100644 --- a/core/admin/Dockerfile +++ b/core/admin/Dockerfile @@ -3,33 +3,40 @@ ARG DISTRO=alpine:3.14 ARG ARCH="" FROM ${ARCH}node:16 as assets -COPY --from=balenalib/rpi-alpine:3.14 /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static COPY package.json ./ -RUN npm install +RUN set -eu \ + && npm config set update-notifier false \ + && npm install --no-fund -COPY ./webpack.config.js ./ -COPY ./assets ./assets -RUN mkdir static \ - && ./node_modules/.bin/webpack-cli +COPY webpack.config.js ./ +COPY assets ./assets +RUN set -eu \ + && sed -i 's/#007bff/#367fa9/' node_modules/admin-lte/build/scss/_bootstrap-variables.scss \ + && for l in ca da de:de_de en:en-gb es:es_es eu fr:fr_fr he hu is it:it_it ja nb_NO:no_nb nl:nl_nl pl pt:pt_pt ru sv:sv_se zh_CN:zh; do \ + cp node_modules/datatables.net-plugins/i18n/${l#*:}.json assets/${l%:*}.json; \ + done \ + && node_modules/.bin/webpack-cli --color # Actual application FROM $DISTRO +COPY --from=balenalib/rpi-alpine:3.14 /usr/bin/qemu-arm-static /usr/bin/qemu-arm-static + # python3 shared with most images -RUN apk add --no-cache \ - python3 py3-pip git bash \ - && pip3 install --upgrade pip +RUN set -eu \ + && apk add --no-cache python3 py3-pip git bash \ + && pip3 install --upgrade pip RUN mkdir -p /app WORKDIR /app COPY requirements-prod.txt requirements.txt -RUN apk add --no-cache openssl curl postgresql-libs mariadb-connector-c \ - && apk add --no-cache --virtual build-dep \ - openssl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev cargo \ - && pip3 install -r requirements.txt \ - && apk del --no-cache build-dep +RUN set -eu \ + && apk add --no-cache openssl curl postgresql-libs mariadb-connector-c \ + && apk add --no-cache --virtual build-dep openssl-dev libffi-dev python3-dev build-base postgresql-dev mariadb-connector-c-dev cargo \ + && pip3 install -r requirements.txt \ + && apk del --no-cache build-dep COPY --from=assets static ./mailu/ui/static COPY mailu ./mailu diff --git a/core/admin/assets/app.css b/core/admin/assets/app.css index 8351eed8..12df605c 100644 --- a/core/admin/assets/app.css +++ b/core/admin/assets/app.css @@ -1,23 +1,51 @@ -.select2-search--inline .select2-search__field:focus { - border: none; +/* mailu logo */ +.mailu-logo { + opacity: .8; } -.sidebar h4 { - padding-left: 5px; - padding-right: 5px; - overflow: hidden; - text-overflow: ellipsis; +/* user image */ +.div-circle { + position: relative; + width: 2.1rem; + height: 2.1rem; + opacity: .8; + background-color: white; + border-radius: 50%; +} +.div-circle > i { + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) } -.sidebar-collapse .sidebar h4 { - display: none !important; +/* nice round preformatted configuration display */ +.pre-config { + padding: 9px; + margin: 0; + white-space: pre-wrap; + word-wrap: anywhere; + border-radius: 4px; } -.logo a { - color: #fff; +/* fieldset */ +legend { + font-size: inherit; +} +fieldset:disabled :not(legend) label { + opacity: .5; +} +fieldset:disabled .form-control:disabled { + color: gray; } -.sidebar-toggle { - padding: unset !important; +/* fix animation for icons in menu text */ +.sidebar .nav-link p i { + transition: margin-left .3s linear,opacity .3s ease,visibility .3s ease; } +/* fix select2 text color */ +.select2-container--default .select2-selection--multiple .select2-selection__choice { + color: black; +} diff --git a/core/admin/assets/app.js b/core/admin/assets/app.js index 364f8429..dc3414f2 100644 --- a/core/admin/assets/app.js +++ b/core/admin/assets/app.js @@ -1,17 +1,70 @@ require('./app.css'); -import 'admin-lte/plugins/select2/js/select2.js'; -import 'admin-lte/plugins/datatables/jquery.dataTables.js'; -import 'admin-lte/plugins/datatables-bs4/js/dataTables.bootstrap4.js'; -import 'admin-lte/plugins/datatables-responsive/js/dataTables.responsive.js'; -import 'admin-lte/plugins/datatables-responsive/js/responsive.bootstrap4.js'; +import logo from './mailu.png'; +import modules from "./*.json"; -jQuery("document").ready(function() { - jQuery(".mailselect").select2({ +// TODO: conditionally (or lazy) load select2 and dataTable +$('document').ready(function() { + + // intercept anchors with data-clicked attribute and open alternate location instead + $('[data-clicked]').click(function(e) { + e.preventDefault(); + window.location.href = $(this).data('clicked'); + }); + + // use post for language selection + $('#mailu-languages > a').click(function(e) { + e.preventDefault(); + $.post({ + url: $(this).attr('href'), + success: function() { + location.reload(); + }, + }); + }); + + // allow en-/disabling of inputs in fieldset with checkbox in legend + $('fieldset legend input[type=checkbox]').change(function() { + var fieldset = $(this).parents('fieldset'); + if (this.checked) { + fieldset.removeAttr('disabled'); + fieldset.find('input').not(this).removeAttr('disabled'); + } else { + fieldset.attr('disabled', ''); + fieldset.find('input').not(this).attr('disabled', ''); + } + }); + + // display of range input value + $('input[type=range]').each(function() { + var value_element = $('#'+this.id+'_value'); + if (value_element.length) { + value_element = $(value_element[0]); + var infinity = $(this).data('infinity'); + var step = $(this).attr('step'); + $(this).on('input', function() { + value_element.text((infinity && this.value == 0) ? '∞' : this.value/step); + }).trigger('input'); + } + }); + + // init select2 + $('.mailselect').select2({ tags: true, - tokenSeparators: [',', ' '] + tokenSeparators: [',', ' '], }); - jQuery(".dataTable").DataTable({ - "responsive": true, + + // init dataTable + var d = $(document.documentElement); + $('.dataTable').DataTable({ + 'responsive': true, + language: { + url: d.data('static') + d.attr('lang') + '.json', + }, }); + + // init clipboard.js + new ClipboardJS('.btn-clip'); + }); + diff --git a/core/admin/assets/mailu.png b/core/admin/assets/mailu.png new file mode 100644 index 0000000000000000000000000000000000000000..e4f5021f4665bf509f6bbbb5deedc399a7e65b1c GIT binary patch literal 4935 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 {{ form.hidden_tag() }} {{ macros.form_field(form.admin, class_='mailselect') }} {{ macros.form_field(form.submit) }} -{% endcall %} -{% endblock %} +{%- endcall %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/admin/list.html b/core/admin/mailu/ui/templates/admin/list.html index f2f5d229..84d954a0 100644 --- a/core/admin/mailu/ui/templates/admin/list.html +++ b/core/admin/mailu/ui/templates/admin/list.html @@ -1,17 +1,17 @@ -{% extends "base.html" %} +{%- extends "base.html" %} -{% block title %} +{%- block title %} {% trans %}Global administrators{% endtrans %} -{% endblock %} +{%- endblock %} -{% block main_action %} +{%- block main_action %} {% trans %}Add administrator{% endtrans %} -{% endblock %} +{%- endblock %} -{% block content %} -{% call macros.table() %} +{%- block content %} +{%- call macros.table() %} {% trans %}Actions{% endtrans %} @@ -19,14 +19,14 @@ - {% for admin in admins %} + {%- for admin in admins %} {{ admin }} - {% endfor %} + {%- endfor %} -{% endcall %} -{% endblock %} +{%- endcall %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/alias/create.html b/core/admin/mailu/ui/templates/alias/create.html index 2079d191..ce9f8167 100644 --- a/core/admin/mailu/ui/templates/alias/create.html +++ b/core/admin/mailu/ui/templates/alias/create.html @@ -1,15 +1,15 @@ -{% extends "base.html" %} +{%- extends "base.html" %} -{% block title %} +{%- block title %} {% trans %}Create alias{% endtrans %} -{% endblock %} +{%- endblock %} -{% block subtitle %} +{%- block subtitle %} {{ domain }} -{% endblock %} +{%- endblock %} -{% block content %} -{% call macros.card() %} +{%- block content %} +{%- call macros.card() %}

{{ form.hidden_tag() }} {{ macros.form_field(form.localpart, append='@'+domain.name+'') }} @@ -18,5 +18,5 @@ {{ macros.form_field(form.comment) }} {{ macros.form_field(form.submit) }} -{% endcall %} -{% endblock %} +{%- endcall %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/alias/edit.html b/core/admin/mailu/ui/templates/alias/edit.html index b28ea170..4dc13cce 100644 --- a/core/admin/mailu/ui/templates/alias/edit.html +++ b/core/admin/mailu/ui/templates/alias/edit.html @@ -1,9 +1,9 @@ -{% extends "alias/create.html" %} +{%- extends "alias/create.html" %} -{% block title %} +{%- block title %} {% trans %}Edit alias{% endtrans %} -{% endblock %} +{%- endblock %} -{% block subtitle %} +{%- block subtitle %} {{ alias }} -{% endblock %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/alias/list.html b/core/admin/mailu/ui/templates/alias/list.html index e8ddc862..0b784d52 100644 --- a/core/admin/mailu/ui/templates/alias/list.html +++ b/core/admin/mailu/ui/templates/alias/list.html @@ -1,19 +1,19 @@ -{% extends "base.html" %} +{%- extends "base.html" %} -{% block title %} +{%- block title %} {% trans %}Alias list{% endtrans %} -{% endblock %} +{%- endblock %} -{% block subtitle %} +{%- block subtitle %} {{ domain.name }} -{% endblock %} +{%- endblock %} -{% block main_action %} +{%- block main_action %} {% trans %}Add alias{% endtrans %} -{% endblock %} +{%- endblock %} -{% block content %} -{% call macros.table() %} +{%- block content %} +{%- call macros.table() %} {% trans %}Actions{% endtrans %} @@ -25,7 +25,7 @@ - {% for alias in domain.aliases %} + {%- for alias in domain.aliases %}   @@ -37,7 +37,7 @@ {{ alias.created_at }} {{ alias.updated_at or '' }} - {% endfor %} + {%- endfor %} -{% endcall %} -{% endblock %} +{%- endcall %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/alternative/create.html b/core/admin/mailu/ui/templates/alternative/create.html index 75461c67..f10cb718 100644 --- a/core/admin/mailu/ui/templates/alternative/create.html +++ b/core/admin/mailu/ui/templates/alternative/create.html @@ -1,9 +1,9 @@ -{% extends "form.html" %} +{%- extends "form.html" %} -{% block title %} +{%- block title %} {% trans %}Create alternative domain{% endtrans %} -{% endblock %} +{%- endblock %} -{% block subtitle %} +{%- block subtitle %} {{ domain }} -{% endblock %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/alternative/list.html b/core/admin/mailu/ui/templates/alternative/list.html index f123eb9f..b56cd751 100644 --- a/core/admin/mailu/ui/templates/alternative/list.html +++ b/core/admin/mailu/ui/templates/alternative/list.html @@ -1,19 +1,19 @@ -{% extends "base.html" %} +{%- extends "base.html" %} -{% block title %} +{%- block title %} {% trans %}Alternative domain list{% endtrans %} -{% endblock %} +{%- endblock %} -{% block subtitle %} +{%- block subtitle %} {{ domain.name }} -{% endblock %} +{%- endblock %} -{% block main_action %} +{%- block main_action %} {% trans %}Add alternative{% endtrans %} -{% endblock %} +{%- endblock %} -{% block content %} -{% call macros.table() %} +{%- block content %} +{%- call macros.table() %} {% trans %}Actions{% endtrans %} @@ -22,7 +22,7 @@ - {% for alternative in domain.alternatives %} + {%- for alternative in domain.alternatives %} @@ -30,7 +30,7 @@ {{ alternative }} {{ alternative.created_at }} - {% endfor %} + {%- endfor %} -{% endcall %} -{% endblock %} +{%- endcall %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/announcement.html b/core/admin/mailu/ui/templates/announcement.html index acdbde1a..ed7fe772 100644 --- a/core/admin/mailu/ui/templates/announcement.html +++ b/core/admin/mailu/ui/templates/announcement.html @@ -1,16 +1,16 @@ -{% extends "base.html" %} +{%- extends "base.html" %} -{% block title %} +{%- block title %} {% trans %}Public announcement{% endtrans %} -{% endblock %} +{%- endblock %} -{% block content %} -{% call macros.card() %} +{%- block content %} +{%- call macros.card() %}
{{ form.hidden_tag() }} {{ macros.form_field(form.announcement_subject) }} {{ macros.form_field(form.announcement_body, rows=10) }} {{ macros.form_field(form.submit) }}
-{% endcall %} -{% endblock %} +{%- endcall %} +{%- endblock %} diff --git a/core/admin/mailu/ui/templates/antispam.html b/core/admin/mailu/ui/templates/antispam.html new file mode 100644 index 00000000..0b2713b9 --- /dev/null +++ b/core/admin/mailu/ui/templates/antispam.html @@ -0,0 +1,15 @@ +{%- extends "base.html" %} + +{%- block title %} +{% trans %}Antispam{% endtrans %} +{%- endblock %} + +{%- block subtitle %} +{% trans %}RSPAMD status page{% endtrans %} +{%- endblock %} + +{%- block content %} +
+ +
+{%- endblock %} diff --git a/core/admin/mailu/ui/templates/base.html b/core/admin/mailu/ui/templates/base.html index 89695e50..acef4b86 100644 --- a/core/admin/mailu/ui/templates/base.html +++ b/core/admin/mailu/ui/templates/base.html @@ -1,65 +1,83 @@ -{% import "macros.html" as macros %} -{% import "bootstrap/utils.html" as utils %} +{%- import "macros.html" as macros %} +{%- import "bootstrap/utils.html" as utils %} - + - + + + + + Mailu-Admin | {{ config["SITENAME"] }} - Mailu-Admin - {{ config["SITENAME"] }}
-