diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py
index d2d34d88..7cd3a56b 100644
--- a/core/admin/mailu/configuration.py
+++ b/core/admin/mailu/configuration.py
@@ -32,6 +32,7 @@ DEFAULT_CONFIG = {
'DOMAIN': 'mailu.io',
'HOSTNAMES': 'mail.mailu.io,alternative.mailu.io,yetanother.mailu.io',
'POSTMASTER': 'postmaster',
+ 'WILDCARD_SENDERS': '',
'TLS_FLAVOR': 'cert',
'INBOUND_TLS_ENFORCE': False,
'AUTH_RATELIMIT': '1000/minute;10000/hour',
@@ -46,6 +47,7 @@ DEFAULT_CONFIG = {
'DKIM_SELECTOR': 'dkim',
'DKIM_PATH': '/dkim/{domain}.{selector}.key',
'DEFAULT_QUOTA': 1000000000,
+ 'MESSAGE_RATELIMIT': '200/day',
# Web settings
'SITENAME': 'Mailu',
'WEBSITE': 'https://mailu.io',
diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py
index 3f5582cc..5e60cd0c 100644
--- a/core/admin/mailu/internal/nginx.py
+++ b/core/admin/mailu/internal/nginx.py
@@ -81,6 +81,13 @@ def handle_authentication(headers):
raw_password = urllib.parse.unquote(headers["Auth-Pass"])
password = raw_password.encode("iso8859-1").decode("utf8")
ip = urllib.parse.unquote(headers["Client-Ip"])
+ service_port = int(urllib.parse.unquote(headers["Auth-Port"]))
+ if service_port == 25:
+ return {
+ "Auth-Status": "AUTH not supported",
+ "Auth-Error-Code": "502 5.5.1",
+ "Auth-Wait": 0
+ }
user = models.User.query.get(user_email)
if check_credentials(user, password, ip, protocol):
return {
diff --git a/core/admin/mailu/internal/views/postfix.py b/core/admin/mailu/internal/views/postfix.py
index c358c37f..2e7d0b9b 100644
--- a/core/admin/mailu/internal/views/postfix.py
+++ b/core/admin/mailu/internal/views/postfix.py
@@ -1,5 +1,6 @@
-from mailu import models
+from mailu import models, utils
from mailu.internal import internal
+from flask import current_app as app
import flask
import idna
@@ -31,7 +32,6 @@ def postfix_alias_map(alias):
destination = models.Email.resolve_destination(localpart, domain_name)
return flask.jsonify(",".join(destination)) if destination else flask.abort(404)
-
@internal.route("/postfix/transport/{% trans %}User settings{% endtrans %}
{% trans %}Email{% endtrans %}
{% trans %}Features{% endtrans %}
- {% trans %}Quota{% endtrans %}
+ {% trans %}Storage Quota{% endtrans %}
+ {% trans %}Sending Quota{% endtrans %}
{% trans %}Comment{% endtrans %}
{% trans %}Created{% endtrans %}
{% trans %}Last edit{% endtrans %}
@@ -41,6 +42,8 @@
{% if user.enable_pop %}pop3{% endif %}
{{ user.quota_bytes_used | filesizeformat }} / {{ (user.quota_bytes | filesizeformat) if user.quota_bytes else '∞' }}
+ {% set limiter = user.sender_limiter %}
+ {{ limiter.get_window_stats()[1] }} / {{ limiter.limit }}
{{ user.comment or '-' }}
{{ user.created_at }}
{{ user.updated_at or '' }}
diff --git a/core/nginx/conf/dhparam.pem b/core/nginx/conf/dhparam.pem
index 3cf0fcbc..4f25f663 100644
--- a/core/nginx/conf/dhparam.pem
+++ b/core/nginx/conf/dhparam.pem
@@ -1,13 +1,11 @@
-----BEGIN DH PARAMETERS-----
-MIICCAKCAgEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz
-+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a
-87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7
-YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi
-7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD
-ssbzSibBsu/6iGtCOGEfz9zeNVs7ZRkDW7w09N75nAI4YbRvydbmyQd62R0mkff3
-7lmMsPrBhtkcrv4TCYUTknC0EwyTvEN5RPT9RFLi103TZPLiHnH1S/9croKrnJ32
-nuhtK8UiNjoNq8Uhl5sN6todv5pC1cRITgq80Gv6U93vPBsg7j/VnXwl5B0rZp4e
-8W5vUsMWTfT7eTDp5OWIV7asfV9C1p9tGHdjzx1VA0AEh/VbpX4xzHpxNciG77Qx
-iu1qHgEtnmgyqQdgCpGBMMRtx3j5ca0AOAkpmaMzy4t6Gh25PXFAADwqTs6p+Y0K
-zAqCkc3OyX3Pjsm1Wn+IpGtNtahR9EGC4caKAH5eZV9q//////////8CAQI=
+MIIBiAKCAYEAtQlUSOKGjpdXJ154qmMEa1pEs+9CdSxWiZFkiXBJb0lTafOh8cfF
+2IkcWSwzxWwjW4Ad26UQQFh1poGf2QBzVk2vuKCekYzPAs/WqH8VwiXBiWR5R9lh
+v/+CkEBYuQOzAhXLN6ZGdPPa2sjdI49rlaIqyLJE4D0TI/VHYmC/vEwqkJUgaGrS
+19LhHZimnmouvrnyBPyf00czXlMow0RnmYeHVZ7W5hu7t9TH9o3QAN/GKiFfxFj+
+RkdLM7beQdS0He5YeTaElM5l1YT5d5gHFbOzEQyKHd10ux+bgVcgUeVbBnI1SAIC
+w53yc1PkDAiRijSP5j5aWq1djtJPheS13o35HyIf0cHzkNYhKfX5JWPj/cbgdM+C
+FL1bnRc8sL5oxmkDoGJhiNZIf4n2WtS8Zu28gUgat6S+vCm/4yavIc/T1g6UiNKE
+X41HPbsma/QWUwOL6S+b2qr+7rKqjI5TzVek8vBMellEV4mBvfQU3NDSQ4WvxbTq
+ZEOgLPA178nrAgEC
-----END DH PARAMETERS-----
diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf
index 5158ca5c..9ce12980 100644
--- a/core/nginx/conf/nginx.conf
+++ b/core/nginx/conf/nginx.conf
@@ -250,6 +250,7 @@ mail {
listen 10025;
protocol smtp;
smtp_auth plain;
+ auth_http_header Auth-Port 10025;
}
# Default IMAP server for the webmail (no encryption, but authentication)
@@ -257,6 +258,7 @@ mail {
listen 10143;
protocol imap;
smtp_auth plain;
+ auth_http_header Auth-Port 10043;
}
# SMTP is always enabled, to avoid losing emails when TLS is failing
@@ -271,6 +273,7 @@ mail {
{% endif %}
protocol smtp;
smtp_auth none;
+ auth_http_header Auth-Port 25;
}
# All other protocols are disabled if TLS is failing
@@ -283,6 +286,7 @@ mail {
{% endif %}
protocol imap;
imap_auth plain;
+ auth_http_header Auth-Port 143;
}
server {
@@ -293,6 +297,7 @@ mail {
{% endif %}
protocol pop3;
pop3_auth plain;
+ auth_http_header Auth-Port 110;
}
server {
@@ -303,6 +308,7 @@ mail {
{% endif %}
protocol smtp;
smtp_auth plain login;
+ auth_http_header Auth-Port 587;
}
{% if TLS %}
@@ -311,6 +317,7 @@ mail {
listen [::]:465 ssl;
protocol smtp;
smtp_auth plain login;
+ auth_http_header Auth-Port 465;
}
server {
@@ -318,6 +325,7 @@ mail {
listen [::]:993 ssl;
protocol imap;
imap_auth plain;
+ auth_http_header Auth-Port 993;
}
server {
@@ -325,6 +333,7 @@ mail {
listen [::]:995 ssl;
protocol pop3;
pop3_auth plain;
+ auth_http_header Auth-Port 995;
}
{% endif %}
{% endif %}
diff --git a/core/nginx/conf/tls.conf b/core/nginx/conf/tls.conf
index 5d7ec031..f663bfd2 100644
--- a/core/nginx/conf/tls.conf
+++ b/core/nginx/conf/tls.conf
@@ -1,5 +1,10 @@
ssl_certificate {{ TLS[0] }};
ssl_certificate_key {{ TLS[1] }};
+{% if TLS_FLAVOR in ['letsencrypt','mail-letsencrypt'] %}
+ssl_certificate {{ TLS[2] }};
+ssl_certificate_key {{ TLS[3] }};
+ssl_trusted_certificate /etc/ssl/certs/ca-cert-DST_Root_CA_X3.pem;
+{% endif %}
ssl_session_timeout 1d;
ssl_session_tickets off;
ssl_dhparam /conf/dhparam.pem;
diff --git a/core/nginx/config.py b/core/nginx/config.py
index 6fc9c082..9fa75877 100755
--- a/core/nginx/config.py
+++ b/core/nginx/config.py
@@ -26,11 +26,11 @@ cert_name = os.getenv("TLS_CERT_FILENAME", default="cert.pem")
keypair_name = os.getenv("TLS_KEYPAIR_FILENAME", default="key.pem")
args["TLS"] = {
"cert": ("/certs/%s" % cert_name, "/certs/%s" % keypair_name),
- "letsencrypt": ("/certs/letsencrypt/live/mailu/fullchain.pem",
- "/certs/letsencrypt/live/mailu/privkey.pem"),
+ "letsencrypt": ("/certs/letsencrypt/live/mailu/nginx-chain.pem",
+ "/certs/letsencrypt/live/mailu/privkey.pem", "/certs/letsencrypt/live/mailu-ecdsa/nginx-chain.pem", "/certs/letsencrypt/live/mailu-ecdsa/privkey.pem"),
"mail": ("/certs/%s" % cert_name, "/certs/%s" % keypair_name),
- "mail-letsencrypt": ("/certs/letsencrypt/live/mailu/fullchain.pem",
- "/certs/letsencrypt/live/mailu/privkey.pem"),
+ "mail-letsencrypt": ("/certs/letsencrypt/live/mailu/nginx-chain.pem",
+ "/certs/letsencrypt/live/mailu/privkey.pem", "/certs/letsencrypt/live/mailu-ecdsa/nginx-chain.pem", "/certs/letsencrypt/live/mailu-ecdsa/privkey.pem"),
"notls": None
}[args["TLS_FLAVOR"]]
diff --git a/core/nginx/letsencrypt.py b/core/nginx/letsencrypt.py
index 3fe8ea92..1dd2cba4 100755
--- a/core/nginx/letsencrypt.py
+++ b/core/nginx/letsencrypt.py
@@ -4,7 +4,6 @@ import os
import time
import subprocess
-
command = [
"certbot",
"-n", "--agree-tos", # non-interactive
@@ -14,16 +13,45 @@ command = [
"--cert-name", "mailu",
"--preferred-challenges", "http", "--http-01-port", "8008",
"--keep-until-expiring",
- "--rsa-key-size", "4096",
+ "--config-dir", "/certs/letsencrypt",
+ "--post-hook", "/config.py"
+]
+command2 = [
+ "certbot",
+ "-n", "--agree-tos", # non-interactive
+ "-d", os.environ["HOSTNAMES"],
+ "-m", "{}@{}".format(os.environ["POSTMASTER"], os.environ["DOMAIN"]),
+ "certonly", "--standalone",
+ "--cert-name", "mailu-ecdsa",
+ "--preferred-challenges", "http", "--http-01-port", "8008",
+ "--keep-until-expiring",
+ "--key-type", "ecdsa",
"--config-dir", "/certs/letsencrypt",
"--post-hook", "/config.py"
]
+def format_for_nginx(fullchain, output):
+ """ We may want to strip ISRG Root X1 out
+ """
+ certs = []
+ with open(fullchain, 'r') as pem:
+ cert = ''
+ for line in pem:
+ cert += line
+ if '-----END CERTIFICATE-----' in line:
+ certs += [cert]
+ cert = ''
+ with open(output, 'w') as pem:
+ for cert in certs[:-1] if len(certs)>2 and os.getenv('LETSENCRYPT_SHORTCHAIN', default="False") else certs:
+ pem.write(cert)
+
# Wait for nginx to start
time.sleep(5)
-# Run certbot every hour
+# Run certbot every day
while True:
subprocess.call(command)
- time.sleep(3600)
-
+ format_for_nginx('/certs/letsencrypt/live/mailu/fullchain.pem', '/certs/letsencrypt/live/mailu/nginx-chain.pem')
+ subprocess.call(command2)
+ format_for_nginx('/certs/letsencrypt/live/mailu-ecdsa/fullchain.pem', '/certs/letsencrypt/live/mailu-ecdsa/nginx-chain.pem')
+ time.sleep(86400)
diff --git a/core/postfix/conf/main.cf b/core/postfix/conf/main.cf
index e102f51a..e7a88a7c 100644
--- a/core/postfix/conf/main.cf
+++ b/core/postfix/conf/main.cf
@@ -33,7 +33,8 @@ relayhost = {{ RELAYHOST }}
{% if RELAYUSER %}
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = lmdb:/etc/postfix/sasl_passwd
-smtp_sasl_security_options = noanonymous
+smtp_sasl_security_options = noanonymous, noplaintext
+smtp_sasl_tls_security_options = noanonymous
{% endif %}
# Recipient delimiter for extended addresses
@@ -57,10 +58,11 @@ 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_session_cache_database = lmdb:${data_directory}/smtp_scache
smtp_tls_security_level = {{ OUTBOUND_TLS_LEVEL|default('may') }}
smtp_tls_policy_maps=hash:/etc/postfix/tls_policy.map
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
###############
# Virtual
@@ -101,6 +103,8 @@ smtpd_sender_login_maps = ${podop}senderlogin
# Restrictions for incoming SMTP, other restrictions are applied in master.cf
smtpd_helo_required = yes
+check_ratelimit = check_sasl_access ${podop}senderrate
+
smtpd_client_restrictions =
permit_mynetworks,
check_sender_access ${podop}senderaccess,
diff --git a/core/postfix/conf/master.cf b/core/postfix/conf/master.cf
index e45a8ccf..15613476 100644
--- a/core/postfix/conf/master.cf
+++ b/core/postfix/conf/master.cf
@@ -7,7 +7,8 @@ smtp inet n - n - - smtpd
# Internal SMTP service
10025 inet n - n - - smtpd
-o smtpd_sasl_auth_enable=yes
- -o smtpd_client_restrictions=reject_unlisted_sender,reject_authenticated_sender_login_mismatch,permit
+ -o smtpd_discard_ehlo_keywords=pipelining
+ -o smtpd_client_restrictions=$check_ratelimit,reject_unlisted_sender,reject_authenticated_sender_login_mismatch,permit
-o smtpd_reject_unlisted_recipient={% if REJECT_UNLISTED_RECIPIENT %}{{ REJECT_UNLISTED_RECIPIENT }}{% else %}no{% endif %}
-o cleanup_service_name=outclean
outclean unix n - n - 0 cleanup
diff --git a/core/postfix/start.py b/core/postfix/start.py
index 83954192..799d42f5 100755
--- a/core/postfix/start.py
+++ b/core/postfix/start.py
@@ -15,6 +15,7 @@ log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING"))
def start_podop():
os.setuid(getpwnam('postfix').pw_uid)
+ os.mkdir('/dev/shm/postfix',mode=0o700)
url = "http://" + os.environ["ADMIN_ADDRESS"] + "/internal/postfix/"
# TODO: Remove verbosity setting from Podop?
run_server(0, "postfix", "/tmp/podop.socket", [
@@ -25,7 +26,8 @@ def start_podop():
("recipientmap", "url", url + "recipient/map/§"),
("sendermap", "url", url + "sender/map/§"),
("senderaccess", "url", url + "sender/access/§"),
- ("senderlogin", "url", url + "sender/login/§")
+ ("senderlogin", "url", url + "sender/login/§"),
+ ("senderrate", "url", url + "sender/rate/§")
])
def is_valid_postconf_line(line):
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 88e60095..27f8db7d 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -37,6 +37,8 @@ The ``POSTMASTER`` is the local part of the postmaster email address. It is
recommended to setup a generic value and later configure a mail alias for that
address.
+The ``WILDCARD_SENDERS`` setting is a comma delimited list of user email addresses that are allowed to send emails from any existing address (spoofing the sender).
+
The ``AUTH_RATELIMIT`` holds a security setting for fighting attackers that
try to guess user passwords. The value is the limit of failed authentication attempts
that a single IP address can perform against IMAP, POP and SMTP authentication endpoints.
@@ -163,6 +165,11 @@ See the `python docs`_ for more information.
.. _`python docs`: https://docs.python.org/3.6/library/logging.html#logging-levels
+The ``LETSENCRYPT_SHORTCHAIN`` (default: False) setting controls whether we send the ISRG Root X1 certificate in TLS handshakes. This is required for `android handsets older than 7.1.1` but slows down the performance of modern devices.
+
+.. _`android handsets older than 7.1.1`: https://community.letsencrypt.org/t/production-chain-changes/150739
+
+
Antivirus settings
------------------
diff --git a/docs/webadministration.rst b/docs/webadministration.rst
index 86ce41c0..03b07ba2 100644
--- a/docs/webadministration.rst
+++ b/docs/webadministration.rst
@@ -315,6 +315,21 @@ This page is also accessible for domain managers. On the users page new users ca
* Fetched accounts. Access the fetched accounts page of the user. See the :ref:`fetched accounts page
/ day +
+