From 1157868370d2767009fc587601fa71b0e2f7f7e9 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Thu, 23 Sep 2021 16:08:52 +0200 Subject: [PATCH 01/61] Document how to setup autoconfig --- docs/faq.rst | 52 +++++++++++++++++++++++++ towncrier/newsfragments/224.enhancement | 1 + 2 files changed, 53 insertions(+) create mode 100644 towncrier/newsfragments/224.enhancement diff --git a/docs/faq.rst b/docs/faq.rst index 01557237..177e65d7 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -394,6 +394,58 @@ Mailu can serve an `MTA-STS policy`_; To configure it you will need to: .. _`1798`: https://github.com/Mailu/Mailu/issues/1798 .. _`MTA-STS policy`: https://datatracker.ietf.org/doc/html/rfc8461 +How do I setup client autoconfiguration? +```````````````````````````````````````` + +Mailu can serve an `XML file for autoconfiguration`_; To configure it you will need to: + +1. add ``autoconfig.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/autoconfiguration.conf`` could read: + +.. code-block:: bash + + location ^~ /mail/config-v1.1.xml { + return 200 " + + + %EMAILDOMAIN% + + Email + Email + + + mailu.example.com + 993 + SSL + %EMAILADDRESS% + password-cleartext + + + + mailu.example.com + 465 + SSL + %EMAILADDRESS% + password-cleartext + true + true + + + + Configure your email client + + + \r\n"; + } + +3. setup the appropriate DNS/CNAME record (``autoconfig.example.com`` -> ``mailu.example.com``). + +*issue reference:* `224`_. + +.. _`224`: https://github.com/Mailu/Mailu/issues/224 +.. _`XML file for autoconfiguration`: https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat + Technical issues ---------------- diff --git a/towncrier/newsfragments/224.enhancement b/towncrier/newsfragments/224.enhancement new file mode 100644 index 00000000..9e4edccf --- /dev/null +++ b/towncrier/newsfragments/224.enhancement @@ -0,0 +1 @@ +Document how to setup client autoconfig using an override From 89ea51d5700e3b8a7cf88fd06b45144c8e1631eb Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Thu, 23 Sep 2021 18:40:49 +0200 Subject: [PATCH 02/61] Implement rate-limits --- core/admin/mailu/__init__.py | 1 + core/admin/mailu/configuration.py | 7 +++- core/admin/mailu/internal/nginx.py | 13 +++++- core/admin/mailu/internal/views/auth.py | 49 ++++++++++++++++------ core/admin/mailu/limiter.py | 56 ++++++++++++++++++++++++- core/admin/mailu/ui/views/base.py | 18 ++++++-- core/admin/mailu/utils.py | 11 +++++ core/nginx/conf/nginx.conf | 1 + docs/configuration.rst | 21 ++++++---- docs/swarm/master/README.md | 3 ++ setup/flavors/compose/mailu.env | 2 +- towncrier/newsfragments/116.feature | 1 + towncrier/newsfragments/1194.bugfix | 1 + towncrier/newsfragments/1612.feature | 1 + 14 files changed, 157 insertions(+), 28 deletions(-) create mode 100644 towncrier/newsfragments/116.feature create mode 100644 towncrier/newsfragments/1194.bugfix create mode 100644 towncrier/newsfragments/1612.feature diff --git a/core/admin/mailu/__init__.py b/core/admin/mailu/__init__.py index 9b712512..43e58171 100644 --- a/core/admin/mailu/__init__.py +++ b/core/admin/mailu/__init__.py @@ -28,6 +28,7 @@ def create_app_from_config(config): utils.proxy.init_app(app) utils.migrate.init_app(app, models.db) + app.device_cookie_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('DEVICE_COOKIE_KEY', 'utf-8'), 'sha256').digest() app.temp_token_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('WEBMAIL_TEMP_TOKEN_KEY', 'utf-8'), 'sha256').digest() # Initialize list of translations diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 4401888a..b0a1bf7b 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -36,8 +36,11 @@ DEFAULT_CONFIG = { 'TLS_FLAVOR': 'cert', 'INBOUND_TLS_ENFORCE': False, 'DEFER_ON_TLS_ERROR': True, - 'AUTH_RATELIMIT': '1000/minute;10000/hour', - 'AUTH_RATELIMIT_SUBNET': False, + 'AUTH_RATELIMIT_IP': '10/hour', + 'AUTH_RATELIMIT_IP_V4_MASK': 24, + 'AUTH_RATELIMIT_IP_V6_MASK': 56, + 'AUTH_RATELIMIT_USER': '100/day', + 'AUTH_RATELIMIT_EXEMPTION_LENGTH': 86400, 'DISABLE_STATISTICS': False, # Mail settings 'DMARC_RUA': None, diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index 167341e2..78f83e99 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -19,6 +19,11 @@ STATUSES = { "encryption": ("Must issue a STARTTLS command first", { "smtp": "530 5.7.0" }), + "ratelimit": ("Temporary authentication failure (rate-limit)", { + "imap": "LIMIT", + "smtp": "451 4.3.2", + "pop3": "-ERR [LOGIN-DELAY] Retry later" + }), } def check_credentials(user, password, ip, protocol=None): @@ -71,8 +76,9 @@ def handle_authentication(headers): } # Authenticated user elif method == "plain": + is_valid_user = False service_port = int(urllib.parse.unquote(headers["Auth-Port"])) - if service_port == 25: + if 'Auth-Port' in headers and service_port == 25: return { "Auth-Status": "AUTH not supported", "Auth-Error-Code": "502 5.5.1", @@ -91,18 +97,23 @@ def handle_authentication(headers): app.logger.warn(f'Received undecodable user/password from nginx: {raw_user_email!r}/{raw_password!r}') else: user = models.User.query.get(user_email) + is_valid_user = True ip = urllib.parse.unquote(headers["Client-Ip"]) if check_credentials(user, password, ip, protocol): server, port = get_server(headers["Auth-Protocol"], True) return { "Auth-Status": "OK", "Auth-Server": server, + "Auth-User": user_email, + "Auth-User-Exists": is_valid_user, "Auth-Port": port } status, code = get_status(protocol, "authentication") return { "Auth-Status": status, "Auth-Error-Code": code, + "Auth-User": user_email, + "Auth-User-Exists": is_valid_user, "Auth-Wait": 0 } # Unexpected diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index 1686e1cb..457efd4b 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -5,19 +5,17 @@ from flask import current_app as app import flask import flask_login import base64 -import ipaddress - @internal.route("/auth/email") def nginx_authentication(): """ Main authentication endpoint for Nginx email server """ - limiter = utils.limiter.get_limiter(app.config["AUTH_RATELIMIT"], "auth-ip") client_ip = flask.request.headers["Client-Ip"] - if not limiter.test(client_ip): + if utils.limiter.should_rate_limit_ip(client_ip): + status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit') response = flask.Response() - response.headers['Auth-Status'] = 'Authentication rate limit from one source exceeded' - response.headers['Auth-Error-Code'] = '451 4.3.2' + response.headers['Auth-Status'] = status + response.headers['Auth-Error-Code'] = code if int(flask.request.headers['Auth-Login-Attempt']) < 10: response.headers['Auth-Wait'] = '3' return response @@ -25,14 +23,25 @@ def nginx_authentication(): response = flask.Response() for key, value in headers.items(): response.headers[key] = str(value) + is_valid_user = False + if "Auth-User-Exists" in response.headers and response.headers["Auth-User-Exists"]: + username = response.headers["Auth-User"] + if utils.limiter.should_rate_limit_user(username, client_ip): + # FIXME could be done before handle_authentication() + status, code = nginx.get_status(flask.request.headers['Auth-Protocol'], 'ratelimit') + response = flask.Response() + response.headers['Auth-Status'] = status + response.headers['Auth-Error-Code'] = code + if int(flask.request.headers['Auth-Login-Attempt']) < 10: + response.headers['Auth-Wait'] = '3' + return response + is_valid_user = True if ("Auth-Status" not in headers) or (headers["Auth-Status"] != "OK"): - limit_subnet = str(app.config["AUTH_RATELIMIT_SUBNET"]) != 'False' - subnet = ipaddress.ip_network(app.config["SUBNET"]) - if limit_subnet or ipaddress.ip_address(client_ip) not in subnet: - limiter.hit(flask.request.headers["Client-Ip"]) + utils.limiter.rate_limit_user(username, client_ip) if is_valid_user else rate_limit_ip(client_ip) + elif ("Auth-Status" in headers) and (headers["Auth-Status"] == "OK"): + utils.limiter.exempt_ip_from_ratelimits(client_ip) return response - @internal.route("/auth/admin") def admin_authentication(): """ Fails if the user is not an authenticated admin. @@ -60,15 +69,29 @@ def user_authentication(): def basic_authentication(): """ Tries to authenticate using the Authorization header. """ + client_ip = flask.request.headers["X-Real-IP"] if 'X-Real-IP' in flask.request.headers else flask.request.remote_addr + if utils.limiter.should_rate_limit_ip(client_ip): + response = flask.Response(status=401) + response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit from one source exceeded"' + response.headers['Retry-After'] = '60' + return response authorization = flask.request.headers.get("Authorization") if authorization and authorization.startswith("Basic "): encoded = authorization.replace("Basic ", "") user_email, password = base64.b64decode(encoded).split(b":", 1) - user = models.User.query.get(user_email.decode("utf8")) - if nginx.check_credentials(user, password.decode('utf-8'), flask.request.remote_addr, "web"): + user_email = user_email.decode("utf8") + if utils.limiter.should_rate_limit_user(user_email, client_ip): + response = flask.Response(status=401) + response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit for this username exceeded"' + response.headers['Retry-After'] = '60' + return response + user = models.User.query.get(user_email) + if user and nginx.check_credentials(user, password.decode('utf-8'), flask.request.remote_addr, "web"): response = flask.Response() response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, user.email, "") + utils.limiter.exempt_ip_from_ratelimits(client_ip) return response + utils.limiter.rate_limit_user(user_email, client_ip) if user else utils.limiter.rate_limit_ip(client_ip) response = flask.Response(status=401) response.headers["WWW-Authenticate"] = 'Basic realm="Login Required"' return response diff --git a/core/admin/mailu/limiter.py b/core/admin/mailu/limiter.py index b5f99915..70b09f21 100644 --- a/core/admin/mailu/limiter.py +++ b/core/admin/mailu/limiter.py @@ -1,7 +1,12 @@ +from mailu import utils +from flask import current_app as app +import base64 import limits import limits.storage import limits.strategies +import hmac +import secrets class LimitWrapper(object): """ Wraps a limit by providing the storage, item and identifiers @@ -31,4 +36,53 @@ class LimitWraperFactory(object): self.limiter = limits.strategies.MovingWindowRateLimiter(self.storage) def get_limiter(self, limit, *args): - return LimitWrapper(self.limiter, limits.parse(limit), *args) \ No newline at end of file + return LimitWrapper(self.limiter, limits.parse(limit), *args) + + def is_subject_to_rate_limits(self, ip): + return not (self.storage.get(f'exempt-{ip}') > 0) + + def exempt_ip_from_ratelimits(self, ip): + self.storage.incr(f'exempt-{ip}', app.config["AUTH_RATELIMIT_EXEMPTION_LENGTH"], True) + + def should_rate_limit_ip(self, ip): + limiter = self.get_limiter(app.config["AUTH_RATELIMIT_IP"], 'auth-ip') + client_network = utils.extract_network_from_ip(ip) + return self.is_subject_to_rate_limits(ip) and not limiter.test(client_network) + + def rate_limit_ip(self, ip): + if ip != app.config['WEBMAIL_ADDRESS']: + limiter = self.get_limiter(app.config["AUTH_RATELIMIT_IP"], 'auth-ip') + client_network = utils.extract_network_from_ip(ip) + if self.is_subject_to_rate_limits(ip): + limiter.hit(client_network) + + def should_rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None): + limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user') + return self.is_subject_to_rate_limits(ip) and not limiter.test(device_cookie if device_cookie_name == username else username) + + def rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None): + limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user') + if self.is_subject_to_rate_limits(ip): + limiter.hit(device_cookie if device_cookie_name == username else username) + + """ Device cookies as described on: + https://owasp.org/www-community/Slow_Down_Online_Guessing_Attacks_with_Device_Cookies + """ + def parse_device_cookie(self, cookie): + try: + login, nonce, _ = cookie.split('$') + if hmac.compare_digest(cookie, self.device_cookie(login, nonce)): + return nonce, login + except: + pass + return None, None + + """ Device cookies don't require strong crypto: + 72bits of nonce, 96bits of signature is more than enough + and these values avoid padding in most cases + """ + def device_cookie(self, username, nonce=None): + if not nonce: + nonce = secrets.token_urlsafe(9) + sig = str(base64.urlsafe_b64encode(hmac.new(app.device_cookie_key, bytearray(f'device_cookie|{username}|{nonce}', 'utf-8'), 'sha256').digest()[20:]), 'utf-8') + return f'{username}${nonce}${sig}' diff --git a/core/admin/mailu/ui/views/base.py b/core/admin/mailu/ui/views/base.py index ecd3089a..30173acf 100644 --- a/core/admin/mailu/ui/views/base.py +++ b/core/admin/mailu/ui/views/base.py @@ -1,4 +1,4 @@ -from mailu import models +from mailu import models, utils from mailu.ui import ui, forms, access from flask import current_app as app @@ -14,16 +14,28 @@ def index(): @ui.route('/login', methods=['GET', 'POST']) def login(): + client_ip = flask.request.headers["X-Real-IP"] if 'X-Real-IP' in flask.request.headers else flask.request.remote_addr form = forms.LoginForm() if form.validate_on_submit(): - user = models.User.login(form.email.data, form.pw.data) + device_cookie, device_cookie_username = utils.limiter.parse_device_cookie(flask.request.cookies.get('rate_limit')) + username = form.email.data + if username != device_cookie_username and utils.limiter.should_rate_limit_ip(client_ip): + flask.flash('Too many attempts from your IP (rate-limit)', 'error') + return flask.render_template('login.html', form=form) + if utils.limiter.should_rate_limit_user(username, client_ip, device_cookie, device_cookie_username): + flask.flash('Too many attempts for this user (rate-limit)', 'error') + return flask.render_template('login.html', form=form) + user = models.User.login(username, form.pw.data) if user: flask.session.regenerate() flask_login.login_user(user) endpoint = flask.request.args.get('next', '.index') - return flask.redirect(flask.url_for(endpoint) + response = flask.redirect(flask.url_for(endpoint) or flask.url_for('.index')) + response.set_cookie('rate_limit', utils.limiter.device_cookie(username), max_age=31536000, path=flask.url_for('ui.login')) + return response else: + utils.limiter.rate_limit_user(username, client_ip, device_cookie, device_cookie_username) if models.User.get(username) else utils.limiter.rate_limit_ip(client_ip) flask.flash('Wrong e-mail or password', 'error') return flask.render_template('login.html', form=form) diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index 34f52d8c..6fce17bc 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -17,10 +17,12 @@ from multiprocessing import Value from mailu import limiter +from flask import current_app as app import flask import flask_login import flask_migrate import flask_babel +import ipaddress import redis from flask.sessions import SessionMixin, SessionInterface @@ -70,6 +72,15 @@ def has_dane_record(domain, timeout=10): # Rate limiter limiter = limiter.LimitWraperFactory() +def extract_network_from_ip(ip): + n = ipaddress.ip_network(ip) + if isinstance(n, ipaddress.IPv4Network): + return str(n.supernet(prefixlen_diff=(32-int(app.config["AUTH_RATELIMIT_IP_V4_MASK"]))).network_address) + elif isinstance(n, ipaddress.IPv6Network): + return str(n.supernet(prefixlen_diff=(128-int(app.config["AUTH_RATELIMIT_IP_V6_MASK"]))).network_address) + else: # not sure what to do with it + return ip + # Application translation babel = flask_babel.Babel() diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index bfd664de..f013176d 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -217,6 +217,7 @@ http { location /internal { internal; + proxy_set_header X-Real-IP $remote_addr; proxy_set_header Authorization $http_authorization; proxy_pass_header Authorization; proxy_pass http://$admin; diff --git a/docs/configuration.rst b/docs/configuration.rst index 5f17b57e..73b56204 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -39,14 +39,21 @@ 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. +The ``AUTH_RATELIMIT_IP`` (default: 10/hour) holds a security setting for fighting +attackers that waste server ressources by trying to guess user passwords (typically +using a password spraying attack). The value defines the limit of authentication +attempts that will be processed on non-existing accounts for a specific IP subnet +(as defined in ``AUTH_RATELIMIT_IP_V4_MASK`` and ``AUTH_RATELIMIT_IP_V6_MASK`` below). -If ``AUTH_RATELIMIT_SUBNET`` is ``True`` (default: False), the ``AUTH_RATELIMIT`` -rules does also apply to auth requests coming from ``SUBNET``, especially for the webmail. -If you disable this, ensure that the rate limit on the webmail is enforced in a different -way (e.g. roundcube plug-in), otherwise an attacker can simply bypass the limit using webmail. +The ``AUTH_RATELIMIT_USER`` (default: 100/day) holds a security setting for fighting +attackers that attempt to guess a user's password (typically using a password +bruteforce attack). The value defines the limit of authentication attempts allowed +for any given account within a specific timeframe. + +The ``AUTH_RATELIMIT_EXEMPTION_LENGTH`` (default: 86400) is the number of seconds +after a successful login for which a specific IP address is exempted from rate limits. +This ensures that users behind a NAT don't get locked out when a single client is +misconfigured... but also potentially allow for users to attack each-other. The ``TLS_FLAVOR`` sets how Mailu handles TLS connections. Setting this value to ``notls`` will cause Mailu not to server any web content! More on :ref:`tls_flavor`. diff --git a/docs/swarm/master/README.md b/docs/swarm/master/README.md index 42e742da..0e933308 100644 --- a/docs/swarm/master/README.md +++ b/docs/swarm/master/README.md @@ -100,6 +100,9 @@ https://github.com/moby/moby/issues/25526#issuecomment-336363408 ### Don't create an open relay ! As a side effect of this ingress mode "feature", make sure that the ingress subnet is not in your RELAYHOST, otherwise you would create an smtp open relay :-( +### Ratelimits + +When using ingress mode you probably want to disable rate limits, because all requests originate from the same ip address. Otherwise automatic login attempts can easily DoS the legitimate users. ## Scalability - smtp and imap are scalable diff --git a/setup/flavors/compose/mailu.env b/setup/flavors/compose/mailu.env index 52f4ee04..0ba39019 100644 --- a/setup/flavors/compose/mailu.env +++ b/setup/flavors/compose/mailu.env @@ -29,7 +29,7 @@ POSTMASTER={{ postmaster }} # Choose how secure connections will behave (value: letsencrypt, cert, notls, mail, mail-letsencrypt) TLS_FLAVOR={{ tls_flavor }} -# Authentication rate limit (per source IP address) +# Authentication rate limit (per /24 on ipv4 and /56 on ipv6) {% if auth_ratelimit_pm > '0' %} AUTH_RATELIMIT={{ auth_ratelimit_pm }}/minute {% endif %} diff --git a/towncrier/newsfragments/116.feature b/towncrier/newsfragments/116.feature new file mode 100644 index 00000000..4f73e7a8 --- /dev/null +++ b/towncrier/newsfragments/116.feature @@ -0,0 +1 @@ + Make the rate limit apply to a subnet rather than a specific IP (/24 for v4 and /56 for v6) diff --git a/towncrier/newsfragments/1194.bugfix b/towncrier/newsfragments/1194.bugfix new file mode 100644 index 00000000..866c6c3b --- /dev/null +++ b/towncrier/newsfragments/1194.bugfix @@ -0,0 +1 @@ +Fix rate-limiting on /webdav/ diff --git a/towncrier/newsfragments/1612.feature b/towncrier/newsfragments/1612.feature new file mode 100644 index 00000000..04d8d508 --- /dev/null +++ b/towncrier/newsfragments/1612.feature @@ -0,0 +1 @@ +Refactor the rate limiter to ensure that it performs as intented. From a9340e61f538eccc8f8aaa7ae27eb9e303aaca42 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Thu, 23 Sep 2021 18:48:23 +0200 Subject: [PATCH 03/61] Log auth attempts on /admin --- core/admin/mailu/ui/views/base.py | 2 ++ towncrier/newsfragments/1926.feature | 1 + 2 files changed, 3 insertions(+) create mode 100644 towncrier/newsfragments/1926.feature diff --git a/core/admin/mailu/ui/views/base.py b/core/admin/mailu/ui/views/base.py index 30173acf..05211804 100644 --- a/core/admin/mailu/ui/views/base.py +++ b/core/admin/mailu/ui/views/base.py @@ -33,9 +33,11 @@ def login(): response = flask.redirect(flask.url_for(endpoint) or flask.url_for('.index')) response.set_cookie('rate_limit', utils.limiter.device_cookie(username), max_age=31536000, path=flask.url_for('ui.login')) + flask.current_app.logger.info(f'Login succeeded for {username} from {client_ip}.') return response else: utils.limiter.rate_limit_user(username, client_ip, device_cookie, device_cookie_username) if models.User.get(username) else utils.limiter.rate_limit_ip(client_ip) + flask.current_app.logger.warn(f'Login failed for {username} from {client_ip}.') flask.flash('Wrong e-mail or password', 'error') return flask.render_template('login.html', form=form) diff --git a/towncrier/newsfragments/1926.feature b/towncrier/newsfragments/1926.feature new file mode 100644 index 00000000..fdd4ae87 --- /dev/null +++ b/towncrier/newsfragments/1926.feature @@ -0,0 +1 @@ +Log authentication attempts on the admin portal From cab0ce2017c10828944a71100210e70a959a54fe Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Thu, 23 Sep 2021 19:01:09 +0200 Subject: [PATCH 04/61] doh --- core/admin/mailu/internal/nginx.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index 78f83e99..ff7070ab 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -77,8 +77,7 @@ def handle_authentication(headers): # Authenticated user elif method == "plain": is_valid_user = False - service_port = int(urllib.parse.unquote(headers["Auth-Port"])) - if 'Auth-Port' in headers and service_port == 25: + if 'Auth-Port' in headers and int(urllib.parse.unquote(headers["Auth-Port"])) == 25: return { "Auth-Status": "AUTH not supported", "Auth-Error-Code": "502 5.5.1", From 64bc7972cc41ad0cf42eb23a7fb8fefb3f97cd2d Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Fri, 24 Sep 2021 09:57:28 +0200 Subject: [PATCH 05/61] Make AUTH_RATELIMIT_IP 60/hour as discussed --- core/admin/mailu/configuration.py | 2 +- docs/configuration.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index b0a1bf7b..fa6723f1 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -36,7 +36,7 @@ DEFAULT_CONFIG = { 'TLS_FLAVOR': 'cert', 'INBOUND_TLS_ENFORCE': False, 'DEFER_ON_TLS_ERROR': True, - 'AUTH_RATELIMIT_IP': '10/hour', + 'AUTH_RATELIMIT_IP': '60/hour', 'AUTH_RATELIMIT_IP_V4_MASK': 24, 'AUTH_RATELIMIT_IP_V6_MASK': 56, 'AUTH_RATELIMIT_USER': '100/day', diff --git a/docs/configuration.rst b/docs/configuration.rst index 73b56204..c736f30b 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -39,7 +39,7 @@ 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_IP`` (default: 10/hour) holds a security setting for fighting +The ``AUTH_RATELIMIT_IP`` (default: 60/hour) holds a security setting for fighting attackers that waste server ressources by trying to guess user passwords (typically using a password spraying attack). The value defines the limit of authentication attempts that will be processed on non-existing accounts for a specific IP subnet From 24aadf2f520eca1d61f8bd98550b64cccf736a9e Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Fri, 24 Sep 2021 10:07:41 +0200 Subject: [PATCH 06/61] ensure we log when the rate limiter hits --- core/admin/mailu/limiter.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/limiter.py b/core/admin/mailu/limiter.py index 70b09f21..88319012 100644 --- a/core/admin/mailu/limiter.py +++ b/core/admin/mailu/limiter.py @@ -47,7 +47,10 @@ class LimitWraperFactory(object): def should_rate_limit_ip(self, ip): limiter = self.get_limiter(app.config["AUTH_RATELIMIT_IP"], 'auth-ip') client_network = utils.extract_network_from_ip(ip) - return self.is_subject_to_rate_limits(ip) and not limiter.test(client_network) + is_rate_limited = self.is_subject_to_rate_limits(ip) and not limiter.test(client_network) + if is_rate_limited: + app.logger.warn(f'Authentication attempt from {ip} has been rate-limited.') + return is_rate_limited def rate_limit_ip(self, ip): if ip != app.config['WEBMAIL_ADDRESS']: @@ -58,7 +61,10 @@ class LimitWraperFactory(object): def should_rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None): limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user') - return self.is_subject_to_rate_limits(ip) and not limiter.test(device_cookie if device_cookie_name == username else username) + is_rate_limited = self.is_subject_to_rate_limits(ip) and not limiter.test(device_cookie if device_cookie_name == username else username) + if is_rate_limited: + app.logger.warn(f'Authentication attempt from {ip} for {username} has been rate-limited.') + return is_rate_limited def rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None): limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user') From ac496eed19955effbd262eaeb71f043a523061c4 Mon Sep 17 00:00:00 2001 From: Diman0 Date: Fri, 24 Sep 2021 12:57:17 +0200 Subject: [PATCH 07/61] Update setup with new rate limit config vars. --- setup/flavors/compose/mailu.env | 11 ++++++++--- setup/templates/steps/config.html | 14 +++++++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/setup/flavors/compose/mailu.env b/setup/flavors/compose/mailu.env index 0ba39019..fd95b725 100644 --- a/setup/flavors/compose/mailu.env +++ b/setup/flavors/compose/mailu.env @@ -29,9 +29,14 @@ POSTMASTER={{ postmaster }} # Choose how secure connections will behave (value: letsencrypt, cert, notls, mail, mail-letsencrypt) TLS_FLAVOR={{ tls_flavor }} -# Authentication rate limit (per /24 on ipv4 and /56 on ipv6) -{% if auth_ratelimit_pm > '0' %} -AUTH_RATELIMIT={{ auth_ratelimit_pm }}/minute +# Authentication rate limit per IP (per /24 on ipv4 and /56 on ipv6) +{% if auth_ratelimit_ip > '0' %} +AUTH_RATELIMIT_IP={{ auth_ratelimit_ip }}/hour +{% endif %} + +# Authentication rate limit per user (per /24 on ipv4 and /56 on ipv6) +{% if auth_ratelimit_user > '0' %} +AUTH_RATELIMIT_USER={{ auth_ratelimit_user }}/day {% endif %} # Opt-out of statistics, replace with "True" to opt out diff --git a/setup/templates/steps/config.html b/setup/templates/steps/config.html index f532f757..4b4bb281 100644 --- a/setup/templates/steps/config.html +++ b/setup/templates/steps/config.html @@ -48,10 +48,18 @@ Or in plain english: if receivers start to classify your mail as spam, this post
- + -

/ minute +

/ hour +

+
+ +
+ + +

/ day

From 862fdda55b9f13f4e5bc82f525441a7716210bf6 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Fri, 24 Sep 2021 13:35:41 +0200 Subject: [PATCH 08/61] Tweak the wording --- setup/flavors/compose/mailu.env | 2 +- setup/templates/steps/config.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup/flavors/compose/mailu.env b/setup/flavors/compose/mailu.env index fd95b725..60516013 100644 --- a/setup/flavors/compose/mailu.env +++ b/setup/flavors/compose/mailu.env @@ -34,7 +34,7 @@ TLS_FLAVOR={{ tls_flavor }} AUTH_RATELIMIT_IP={{ auth_ratelimit_ip }}/hour {% endif %} -# Authentication rate limit per user (per /24 on ipv4 and /56 on ipv6) +# Authentication rate limit per user (regardless of the source-IP) {% if auth_ratelimit_user > '0' %} AUTH_RATELIMIT_USER={{ auth_ratelimit_user }}/day {% endif %} diff --git a/setup/templates/steps/config.html b/setup/templates/steps/config.html index 4b4bb281..74a45800 100644 --- a/setup/templates/steps/config.html +++ b/setup/templates/steps/config.html @@ -48,7 +48,7 @@ Or in plain english: if receivers start to classify your mail as spam, this post
- +

/ hour @@ -56,7 +56,7 @@ Or in plain english: if receivers start to classify your mail as spam, this post

- +

/ day From 464a117e9f19f42af01a47f19c7cd488a88db959 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Fri, 24 Sep 2021 13:37:00 +0200 Subject: [PATCH 09/61] this should be changed too --- setup/flavors/compose/mailu.env | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/setup/flavors/compose/mailu.env b/setup/flavors/compose/mailu.env index 60516013..3e53e7d2 100644 --- a/setup/flavors/compose/mailu.env +++ b/setup/flavors/compose/mailu.env @@ -155,9 +155,8 @@ DOMAIN_REGISTRATION=true # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME={{ compose_project_name or 'mailu' }} -# Default password scheme used for newly created accounts and changed passwords -# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT) -PASSWORD_SCHEME={{ password_scheme or 'PBKDF2' }} +# Number of rounds used by the password hashing scheme +CREDENTIAL_ROUNDS=12 # Header to take the real ip from REAL_IP_HEADER={{ real_ip_header }} From 16691e83adcdcae01948cfa3b6601dd9f0450894 Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Fri, 24 Sep 2021 18:15:00 +0200 Subject: [PATCH 10/61] re-enable mod_rewrite in roundcube moved chown/mkdir/symlink from start.py to Dockerfile --- webmails/roundcube/Dockerfile | 10 +++++++--- webmails/roundcube/start.py | 6 +----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/webmails/roundcube/Dockerfile b/webmails/roundcube/Dockerfile index df83bc83..32f71ea5 100644 --- a/webmails/roundcube/Dockerfile +++ b/webmails/roundcube/Dockerfile @@ -33,13 +33,17 @@ RUN apt-get update && apt-get install -y \ && mv roundcubemail-* html \ && mv carddav html/plugins/ \ && cd html \ - && rm -rf CHANGELOG INSTALL LICENSE README.md UPGRADING composer.json-dist installer \ + && rm -rf CHANGELOG INSTALL LICENSE README.md UPGRADING composer.json-dist installer composer.* \ && sed -i 's,mod_php5.c,mod_php7.c,g' .htaccess \ && sed -i 's,^php_value.*post_max_size,#&,g' .htaccess \ && sed -i 's,^php_value.*upload_max_filesize,#&,g' .htaccess \ - && chown -R www-data: logs temp \ + && ln -sf index.php /var/www/html/sso.php \ + && chown -R root:root . \ + && touch logs/errors.log \ + && chown -R www-data:www-data logs temp \ + && chmod -R a+rX . \ && rm -rf /var/lib/apt/lists \ - && a2enmod deflate expires headers + && a2enmod rewrite deflate expires headers COPY php.ini /php.ini COPY config.inc.php /var/www/html/config/ diff --git a/webmails/roundcube/start.py b/webmails/roundcube/start.py index cd42ba06..82c4c26f 100755 --- a/webmails/roundcube/start.py +++ b/webmails/roundcube/start.py @@ -34,11 +34,7 @@ else: conf.jinja("/php.ini", os.environ, "/usr/local/etc/php/conf.d/roundcube.ini") # Create dirs, setup permissions -os.system("mkdir -p /data/gpg /var/www/html/logs") -os.system("touch /var/www/html/logs/errors.log") -os.system("chown -R www-data:www-data /var/www/html/logs") -os.system("chmod -R a+rX /var/www/html/") -os.system("ln -sf /var/www/html/index.php /var/www/html/sso.php") +os.system("mkdir -p /data/gpg") try: print("Initializing database") From e9f84d7d994656162e41136e375d6cdfa86446ed Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 25 Sep 2021 16:25:59 +0200 Subject: [PATCH 11/61] Improve the unbound configuration --- optional/unbound/unbound.conf | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/optional/unbound/unbound.conf b/optional/unbound/unbound.conf index 6c8fc64d..42b2d4d8 100644 --- a/optional/unbound/unbound.conf +++ b/optional/unbound/unbound.conf @@ -1,19 +1,21 @@ server: verbosity: 1 interface: 0.0.0.0 - interface: ::0 + {{ 'interface: ::0' if SUBNET6 }} logfile: "" do-ip4: yes - do-ip6: yes + do-ip6: {{ 'yes' if SUBNET6 else 'no' }} do-udp: yes do-tcp: yes do-daemonize: no access-control: {{ SUBNET }} allow + {{ 'access-control: {{ SUBNET6 }} allow' if SUBNET6 }} directory: "/etc/unbound" username: unbound auto-trust-anchor-file: trusted-key.key root-hints: "/etc/unbound/root.hints" hide-identity: yes hide-version: yes - max-udp-size: 4096 - msg-buffer-size: 65552 + cache-min-ttl: 300 + qname-minimisation: yes + From 739702a0349d099bf70cb71f18a2bd12180897ab Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 25 Sep 2021 16:31:11 +0200 Subject: [PATCH 12/61] doc --- towncrier/newsfragments/1992.enhancement | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 towncrier/newsfragments/1992.enhancement diff --git a/towncrier/newsfragments/1992.enhancement b/towncrier/newsfragments/1992.enhancement new file mode 100644 index 00000000..56a11538 --- /dev/null +++ b/towncrier/newsfragments/1992.enhancement @@ -0,0 +1,3 @@ +Make unbound work with ipv6 +Add a cache-min-ttl of 5minutes +Enable qname minimisation (privacy) From 1cf0f76b529389b47eff0c7e9800c53263e073af Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Mon, 27 Sep 2021 09:04:15 +0200 Subject: [PATCH 13/61] not required anymore --- optional/unbound/unbound.conf | 1 - 1 file changed, 1 deletion(-) diff --git a/optional/unbound/unbound.conf b/optional/unbound/unbound.conf index 42b2d4d8..df0c76ff 100644 --- a/optional/unbound/unbound.conf +++ b/optional/unbound/unbound.conf @@ -17,5 +17,4 @@ server: hide-identity: yes hide-version: yes cache-min-ttl: 300 - qname-minimisation: yes From 65133a960af543687c00c09068cdbdf10c2f7ff1 Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Tue, 28 Sep 2021 10:38:37 +0200 Subject: [PATCH 14/61] Prevent traceback when using non-email in login There's a traceback when the username used to log via SMTPAUTH in is not an email address: === before === ``` [...] ERROR in app: Exception on /internal/auth/email [GET] Traceback (most recent call last): File "/usr/lib/python3.9/site-packages/sqlalchemy/engine/base.py", line 1179, in _execute_context context = constructor(dialect, self, conn, *args) File "/usr/lib/python3.9/site-packages/sqlalchemy/engine/default.py", line 719, in _init_compiled param.append(processors[key](compiled_params[key])) File "/usr/lib/python3.9/site-packages/sqlalchemy/sql/type_api.py", line 1201, in process return process_param(value, dialect) File "/app/mailu/models.py", line 60, in process_bind_param localpart, domain_name = value.lower().rsplit('@', 1) ValueError: not enough values to unpack (expected 2, got 1) [...] [parameters: [{'%(140657157923216 param)s': 'foobar'}]] ``` === after === ``` [...] WARNING in nginx: Invalid user 'foobar': (builtins.ValueError) invalid email address (no "@") ``` --- core/admin/mailu/internal/nginx.py | 23 ++++++++++++++--------- core/admin/mailu/models.py | 2 ++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index 167341e2..f011bf5a 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -5,6 +5,7 @@ import re import urllib import ipaddress import socket +import sqlalchemy.exc import tenacity SUPPORTED_AUTH_METHODS = ["none", "plain"] @@ -90,15 +91,19 @@ def handle_authentication(headers): except: app.logger.warn(f'Received undecodable user/password from nginx: {raw_user_email!r}/{raw_password!r}') else: - user = models.User.query.get(user_email) - ip = urllib.parse.unquote(headers["Client-Ip"]) - if check_credentials(user, password, ip, protocol): - server, port = get_server(headers["Auth-Protocol"], True) - return { - "Auth-Status": "OK", - "Auth-Server": server, - "Auth-Port": port - } + try: + user = models.User.query.get(user_email) + except sqlalchemy.exc.StatementError as exc: + exc = str(exc).split('\n', 1)[0] + app.logger.warn(f'Invalid user {user_email!r}: {exc}') + else: + if check_credentials(user, password, urllib.parse.unquote(headers["Client-Ip"]), protocol): + server, port = get_server(headers["Auth-Protocol"], True) + return { + "Auth-Status": "OK", + "Auth-Server": server, + "Auth-Port": port + } status, code = get_status(protocol, "authentication") return { "Auth-Status": status, diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index f93b158f..01711a60 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -57,6 +57,8 @@ class IdnaEmail(db.TypeDecorator): def process_bind_param(self, value, dialect): """ encode unicode domain part of email address to punycode """ + if not '@' in value: + raise ValueError('invalid email address (no "@")') localpart, domain_name = value.lower().rsplit('@', 1) if '@' in localpart: raise ValueError('email local part must not contain "@"') From cd17aa0c43f33821a36fbc6fca50bf9cd059a3ff Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Tue, 28 Sep 2021 11:06:59 +0200 Subject: [PATCH 15/61] repair failing health-check --- webmails/roundcube/Dockerfile | 2 +- webmails/roundcube/mailu.php | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/webmails/roundcube/Dockerfile b/webmails/roundcube/Dockerfile index 32f71ea5..2905d30e 100644 --- a/webmails/roundcube/Dockerfile +++ b/webmails/roundcube/Dockerfile @@ -55,4 +55,4 @@ VOLUME ["/data"] CMD /start.py -HEALTHCHECK CMD curl -f -L http://localhost/ || exit 1 +HEALTHCHECK CMD curl -f -L -H 'User-Agent: health' http://localhost/ || exit 1 diff --git a/webmails/roundcube/mailu.php b/webmails/roundcube/mailu.php index bb4d65e9..f5079e98 100644 --- a/webmails/roundcube/mailu.php +++ b/webmails/roundcube/mailu.php @@ -52,6 +52,12 @@ class mailu extends rcube_plugin } function login_failed($args) { + $ua = $_SERVER['HTTP_USER_AGENT']; + $ra = $_SERVER['REMOTE_ADDR']; + if ($ua == 'health' and ($ra == '127.0.0.1' or $ra == '::1')) { + echo "OK"; + exit; + } header('Location: sso.php'); exit(); } From 7380b248cf9cca970f7cb26e2cc54ff8d2553888 Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Tue, 28 Sep 2021 11:16:40 +0200 Subject: [PATCH 16/61] direct logging of php errors to stderr --- webmails/roundcube/Dockerfile | 4 ++-- webmails/roundcube/start.py | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/webmails/roundcube/Dockerfile b/webmails/roundcube/Dockerfile index 2905d30e..c751b6bf 100644 --- a/webmails/roundcube/Dockerfile +++ b/webmails/roundcube/Dockerfile @@ -38,9 +38,9 @@ RUN apt-get update && apt-get install -y \ && sed -i 's,^php_value.*post_max_size,#&,g' .htaccess \ && sed -i 's,^php_value.*upload_max_filesize,#&,g' .htaccess \ && ln -sf index.php /var/www/html/sso.php \ + && ln -sf /dev/stderr /var/www/html/logs/errors.log \ && chown -R root:root . \ - && touch logs/errors.log \ - && chown -R www-data:www-data logs temp \ + && chown www-data:www-data logs temp \ && chmod -R a+rX . \ && rm -rf /var/lib/apt/lists \ && a2enmod rewrite deflate expires headers diff --git a/webmails/roundcube/start.py b/webmails/roundcube/start.py index 82c4c26f..efaac357 100755 --- a/webmails/roundcube/start.py +++ b/webmails/roundcube/start.py @@ -57,8 +57,5 @@ except subprocess.CalledProcessError as e: # Setup database permissions os.system("chown -R www-data:www-data /data") -# Tail roundcube logs -subprocess.Popen(["tail", "-f", "-n", "0", "/var/www/html/logs/errors.log"]) - # Run apache os.execv("/usr/local/bin/apache2-foreground", ["apache2-foreground"]) From ef9e1ac27997128ee982fb92c5183ade34accf4c Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Tue, 28 Sep 2021 12:29:57 +0200 Subject: [PATCH 17/61] remove health check from log --- webmails/roundcube/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webmails/roundcube/Dockerfile b/webmails/roundcube/Dockerfile index c751b6bf..1f788918 100644 --- a/webmails/roundcube/Dockerfile +++ b/webmails/roundcube/Dockerfile @@ -11,7 +11,8 @@ FROM build_${QEMU} RUN apt-get update && apt-get install -y \ python3 curl python3-pip git python3-multidict \ && rm -rf /var/lib/apt/lists \ - && echo "ServerSignature Off" >> /etc/apache2/apache2.conf + && echo "ServerSignature Off\nServerName roundcube" >> /etc/apache2/apache2.conf \ + && sed -i 's,CustomLog.*combined$,\0 "'"expr=!(%{HTTP_USER_AGENT}=='health'\&\&(-R '127.0.0.1/8' || -R '::1'))"'",' /etc/apache2/sites-available/000-default.conf # Shared layer between nginx, dovecot, postfix, postgresql, rspamd, unbound, rainloop, roundcube RUN pip3 install socrate From 995ce8d4374e501e3733ea2cf2f1d96070b172ef Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Fri, 1 Oct 2021 14:54:04 +0200 Subject: [PATCH 18/61] Remove OUTCLEAN_ADDRESS I believe that this isn't relevant anymore as we don't use OpenDKIM anymore Background on: https://bofhskull.wordpress.com/2014/03/25/postfix-opendkim-and-missing-from-header/ --- core/postfix/conf/outclean_header_filter.cf | 5 +---- core/postfix/start.py | 9 --------- towncrier/newsfragments/446.feature | 1 + 3 files changed, 2 insertions(+), 13 deletions(-) create mode 100644 towncrier/newsfragments/446.feature diff --git a/core/postfix/conf/outclean_header_filter.cf b/core/postfix/conf/outclean_header_filter.cf index 7e0e92d3..6a5d6b5b 100644 --- a/core/postfix/conf/outclean_header_filter.cf +++ b/core/postfix/conf/outclean_header_filter.cf @@ -1,10 +1,7 @@ # This configuration was copied from Mailinabox. The original version is available at: # https://raw.githubusercontent.com/mail-in-a-box/mailinabox/master/conf/postfix_outgoing_mail_header_filters -# Remove the first line of the Received: header. Note that we cannot fully remove the Received: header -# because OpenDKIM requires that a header be present when signing outbound mail. The first line is -# where the user's home IP address would be. -/^\s*Received:[^\n]*(.*)/ REPLACE Received: from authenticated-user ({{OUTCLEAN}} [{{OUTCLEAN_ADDRESS}}])$1 +/^\s*Received:[^\n]*(.*)/ IGNORE # Remove other typically private information. /^\s*User-Agent:/ IGNORE diff --git a/core/postfix/start.py b/core/postfix/start.py index 12610bd0..c889dce1 100755 --- a/core/postfix/start.py +++ b/core/postfix/start.py @@ -46,15 +46,6 @@ os.environ["FRONT_ADDRESS"] = system.get_host_address_from_environment("FRONT", os.environ["ADMIN_ADDRESS"] = system.get_host_address_from_environment("ADMIN", "admin") os.environ["ANTISPAM_MILTER_ADDRESS"] = system.get_host_address_from_environment("ANTISPAM_MILTER", "antispam:11332") os.environ["LMTP_ADDRESS"] = system.get_host_address_from_environment("LMTP", "imap:2525") -os.environ["OUTCLEAN"] = os.environ["HOSTNAMES"].split(",")[0] -try: - _to_lookup = os.environ["OUTCLEAN"] - # Ensure we lookup a FQDN: @see #1884 - if not _to_lookup.endswith('.'): - _to_lookup += '.' - os.environ["OUTCLEAN_ADDRESS"] = system.resolve_hostname(_to_lookup) -except: - os.environ["OUTCLEAN_ADDRESS"] = "10.10.10.10" for postfix_file in glob.glob("/conf/*.cf"): conf.jinja(postfix_file, os.environ, os.path.join("/etc/postfix", os.path.basename(postfix_file))) diff --git a/towncrier/newsfragments/446.feature b/towncrier/newsfragments/446.feature new file mode 100644 index 00000000..12049b94 --- /dev/null +++ b/towncrier/newsfragments/446.feature @@ -0,0 +1 @@ +Remove the Received header with PRIMARY_HOSTNAME [PUBLIC_IP] From 10d78a888bf4a509e36bb96cd0548cd11ee2b585 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Fri, 1 Oct 2021 15:00:10 +0200 Subject: [PATCH 19/61] Derive a new subkey for SRS --- core/admin/mailu/__init__.py | 1 + core/admin/mailu/internal/views/postfix.py | 4 ++-- towncrier/newsfragments/1999.enhancement | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 towncrier/newsfragments/1999.enhancement diff --git a/core/admin/mailu/__init__.py b/core/admin/mailu/__init__.py index 9b712512..51532968 100644 --- a/core/admin/mailu/__init__.py +++ b/core/admin/mailu/__init__.py @@ -29,6 +29,7 @@ def create_app_from_config(config): utils.migrate.init_app(app, models.db) app.temp_token_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('WEBMAIL_TEMP_TOKEN_KEY', 'utf-8'), 'sha256').digest() + app.srs_key = hmac.new(bytearray(app.secret_key, 'utf-8'), bytearray('SRS_KEY', 'utf-8'), 'sha256').digest() # Initialize list of translations config.translations = { diff --git a/core/admin/mailu/internal/views/postfix.py b/core/admin/mailu/internal/views/postfix.py index 330fed5b..928f4faf 100644 --- a/core/admin/mailu/internal/views/postfix.py +++ b/core/admin/mailu/internal/views/postfix.py @@ -108,7 +108,7 @@ def postfix_recipient_map(recipient): This is meant for bounces to go back to the original sender. """ - srs = srslib.SRS(flask.current_app.config["SECRET_KEY"]) + srs = srslib.SRS(flask.current_app.srs_key) if srslib.SRS.is_srs_address(recipient): try: return flask.jsonify(srs.reverse(recipient)) @@ -123,7 +123,7 @@ def postfix_sender_map(sender): This is for bounces to come back the reverse path properly. """ - srs = srslib.SRS(flask.current_app.config["SECRET_KEY"]) + srs = srslib.SRS(flask.current_app.srs_key) domain = flask.current_app.config["DOMAIN"] try: localpart, domain_name = models.Email.resolve_domain(sender) diff --git a/towncrier/newsfragments/1999.enhancement b/towncrier/newsfragments/1999.enhancement new file mode 100644 index 00000000..bd025141 --- /dev/null +++ b/towncrier/newsfragments/1999.enhancement @@ -0,0 +1 @@ +Derive a new subkey (from SECRET_KEY) for SRS From 65ee1c1ef27eb7fe3e8c6c6f8b116180c6fcd7bc Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Fri, 1 Oct 2021 15:04:45 +0200 Subject: [PATCH 20/61] doh --- towncrier/newsfragments/{446.feature => 466.feature} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename towncrier/newsfragments/{446.feature => 466.feature} (100%) diff --git a/towncrier/newsfragments/446.feature b/towncrier/newsfragments/466.feature similarity index 100% rename from towncrier/newsfragments/446.feature rename to towncrier/newsfragments/466.feature From 4a78d646db1648a0277c4ffe4684cdc435da0391 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Fri, 1 Oct 2021 15:05:38 +0200 Subject: [PATCH 21/61] doh --- towncrier/newsfragments/{1999.enhancement => 2002.enhancement} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename towncrier/newsfragments/{1999.enhancement => 2002.enhancement} (100%) diff --git a/towncrier/newsfragments/1999.enhancement b/towncrier/newsfragments/2002.enhancement similarity index 100% rename from towncrier/newsfragments/1999.enhancement rename to towncrier/newsfragments/2002.enhancement From a349190e5232fddd927b460215f71325caf76b05 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 2 Oct 2021 10:19:57 +0200 Subject: [PATCH 22/61] simplify --- core/postfix/conf/outclean_header_filter.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/postfix/conf/outclean_header_filter.cf b/core/postfix/conf/outclean_header_filter.cf index 6a5d6b5b..35b90ff5 100644 --- a/core/postfix/conf/outclean_header_filter.cf +++ b/core/postfix/conf/outclean_header_filter.cf @@ -1,7 +1,7 @@ # This configuration was copied from Mailinabox. The original version is available at: # https://raw.githubusercontent.com/mail-in-a-box/mailinabox/master/conf/postfix_outgoing_mail_header_filters -/^\s*Received:[^\n]*(.*)/ IGNORE +/^\s*Received:[^\n]*/ IGNORE # Remove other typically private information. /^\s*User-Agent:/ IGNORE From 502affbe66eafb6a8eba4f8d36ab8fef94e5eeb7 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sun, 3 Oct 2021 10:14:49 +0200 Subject: [PATCH 23/61] Use the regexp engine since we have one --- core/postfix/conf/outclean_header_filter.cf | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/core/postfix/conf/outclean_header_filter.cf b/core/postfix/conf/outclean_header_filter.cf index 35b90ff5..9c880843 100644 --- a/core/postfix/conf/outclean_header_filter.cf +++ b/core/postfix/conf/outclean_header_filter.cf @@ -1,14 +1,8 @@ # This configuration was copied from Mailinabox. The original version is available at: # https://raw.githubusercontent.com/mail-in-a-box/mailinabox/master/conf/postfix_outgoing_mail_header_filters -/^\s*Received:[^\n]*/ IGNORE - -# Remove other typically private information. -/^\s*User-Agent:/ IGNORE -/^\s*X-Enigmail:/ IGNORE -/^\s*X-Mailer:/ IGNORE -/^\s*X-Originating-IP:/ IGNORE -/^\s*X-Pgp-Agent:/ IGNORE +# Remove typically private information. +/^\s*(Received|User-Agent|X-(Enigmail|Mailer|Originating-IP|Pgp-Agent)):/ IGNORE # The Mime-Version header can leak the user agent too, e.g. in Mime-Version: 1.0 (Mac OS X Mail 8.1 \(2010.6\)). /^\s*(Mime-Version:\s*[0-9\.]+)\s.+/ REPLACE $1 From b48779ea7084278d494e436386d06fd5e55161d6 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Fri, 8 Oct 2021 10:17:03 +0200 Subject: [PATCH 24/61] SESSION_COOKIE_SECURE and HTTP won't work --- core/admin/mailu/ui/templates/login.html | 12 ++++++++++++ towncrier/newsfragments/1996.enhancement | 1 + 2 files changed, 13 insertions(+) create mode 100644 towncrier/newsfragments/1996.enhancement diff --git a/core/admin/mailu/ui/templates/login.html b/core/admin/mailu/ui/templates/login.html index fb8e5bd4..118173cb 100644 --- a/core/admin/mailu/ui/templates/login.html +++ b/core/admin/mailu/ui/templates/login.html @@ -7,3 +7,15 @@ {%- block subtitle %} {% trans %}to access the administration tools{% endtrans %} {%- endblock %} + +{%+ block content %} +{% if config["SESSION_COOKIE_SECURE"] %} + +{% endif %} +{{ super() }} +{%+ endblock %} diff --git a/towncrier/newsfragments/1996.enhancement b/towncrier/newsfragments/1996.enhancement new file mode 100644 index 00000000..d1bc2ccf --- /dev/null +++ b/towncrier/newsfragments/1996.enhancement @@ -0,0 +1 @@ +Disable the login page if SESSION_COOKIE_SECURE is incompatible with how Mailu is accessed as this seems to be a common misconfiguration. From aaf3ddd002b87dfccd09e82a00830364a19aeed1 Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Fri, 8 Oct 2021 19:54:31 +0200 Subject: [PATCH 25/61] moved javascript to app.js --- core/admin/assets/app.js | 7 +++++++ core/admin/mailu/ui/templates/login.html | 15 ++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/core/admin/assets/app.js b/core/admin/assets/app.js index dc3414f2..5df8052c 100644 --- a/core/admin/assets/app.js +++ b/core/admin/assets/app.js @@ -66,5 +66,12 @@ $('document').ready(function() { // init clipboard.js new ClipboardJS('.btn-clip'); + // disable login if not possible + var l = $('#login_needs_https'); + if (l.length && window.location.protocol != 'https:') { + l.removeClass("d-none"); + $('form :input').prop('disabled', true); + } + }); diff --git a/core/admin/mailu/ui/templates/login.html b/core/admin/mailu/ui/templates/login.html index 118173cb..4c38d134 100644 --- a/core/admin/mailu/ui/templates/login.html +++ b/core/admin/mailu/ui/templates/login.html @@ -8,14 +8,11 @@ {% trans %}to access the administration tools{% endtrans %} {%- endblock %} -{%+ block content %} {% if config["SESSION_COOKIE_SECURE"] %} - -{% endif %} +{%- block content %} +

{{ super() }} -{%+ endblock %} +{%- endblock %} +{% endif %} From d131d863baca9616bb2251aa44dd658167888966 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 9 Oct 2021 15:44:56 +0200 Subject: [PATCH 26/61] The if needs to be inside the block --- core/admin/mailu/ui/templates/login.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/ui/templates/login.html b/core/admin/mailu/ui/templates/login.html index 4c38d134..d4d115db 100644 --- a/core/admin/mailu/ui/templates/login.html +++ b/core/admin/mailu/ui/templates/login.html @@ -8,11 +8,11 @@ {% trans %}to access the administration tools{% endtrans %} {%- endblock %} -{% if config["SESSION_COOKIE_SECURE"] %} {%- block content %} +{% if config["SESSION_COOKIE_SECURE"] %} +{% endif %} {{ super() }} {%- endblock %} -{% endif %} From 1d571dedfc5df67dd9b47d1f235edbc1198afbca Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Sat, 9 Oct 2021 17:11:12 +0200 Subject: [PATCH 27/61] split localpart into user and tag --- core/admin/mailu/internal/views/postfix.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/admin/mailu/internal/views/postfix.py b/core/admin/mailu/internal/views/postfix.py index 928f4faf..d2c9f877 100644 --- a/core/admin/mailu/internal/views/postfix.py +++ b/core/admin/mailu/internal/views/postfix.py @@ -140,7 +140,8 @@ def postfix_sender_login(sender): localpart, domain_name = models.Email.resolve_domain(sender) if localpart is None: return flask.jsonify(",".join(wildcard_senders)) if wildcard_senders else flask.abort(404) - destination = models.Email.resolve_destination(localpart, domain_name, True) + user, plus = localpart.split("+", 1) + destination = models.Email.resolve_destination(user, domain_name, True) destination = [*destination, *wildcard_senders] if destination else [*wildcard_senders] return flask.jsonify(",".join(destination)) if destination else flask.abort(404) From 22ed2b7f904271d2a7b524b390ccbbc00536fdfd Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Sat, 9 Oct 2021 17:17:40 +0200 Subject: [PATCH 28/61] add newsfragment --- towncrier/newsfragments/2006.enhancement | 1 + 1 file changed, 1 insertion(+) create mode 100644 towncrier/newsfragments/2006.enhancement diff --git a/towncrier/newsfragments/2006.enhancement b/towncrier/newsfragments/2006.enhancement new file mode 100644 index 00000000..802e6d36 --- /dev/null +++ b/towncrier/newsfragments/2006.enhancement @@ -0,0 +1 @@ +allow sending emails as user+detail@domain.tld From 6a8066c0ae1597fc0b85d9130978ff1af778d70f Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Sat, 9 Oct 2021 17:18:53 +0200 Subject: [PATCH 29/61] renamed newsfragment --- towncrier/newsfragments/{2006.enhancement => 2007.enhancement} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename towncrier/newsfragments/{2006.enhancement => 2007.enhancement} (100%) diff --git a/towncrier/newsfragments/2006.enhancement b/towncrier/newsfragments/2007.enhancement similarity index 100% rename from towncrier/newsfragments/2006.enhancement rename to towncrier/newsfragments/2007.enhancement From 8c59f3569760513774ad8a48d1a30d336ce2f259 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 9 Oct 2021 17:43:09 +0200 Subject: [PATCH 30/61] use RECIPIENT_DELIMITER for splitting --- core/admin/mailu/configuration.py | 1 + core/admin/mailu/internal/views/postfix.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 4401888a..8d4d334a 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -49,6 +49,7 @@ DEFAULT_CONFIG = { 'DKIM_PATH': '/dkim/{domain}.{selector}.key', 'DEFAULT_QUOTA': 1000000000, 'MESSAGE_RATELIMIT': '200/day', + 'RECIPIENT_DELIMITER': None, # Web settings 'SITENAME': 'Mailu', 'WEBSITE': 'https://mailu.io', diff --git a/core/admin/mailu/internal/views/postfix.py b/core/admin/mailu/internal/views/postfix.py index d2c9f877..44a685c9 100644 --- a/core/admin/mailu/internal/views/postfix.py +++ b/core/admin/mailu/internal/views/postfix.py @@ -140,8 +140,9 @@ def postfix_sender_login(sender): localpart, domain_name = models.Email.resolve_domain(sender) if localpart is None: return flask.jsonify(",".join(wildcard_senders)) if wildcard_senders else flask.abort(404) - user, plus = localpart.split("+", 1) - destination = models.Email.resolve_destination(user, domain_name, True) + if delim := flask.current_app.config.get('RECIPIENT_DELIMITER'): + localpart = localpart.split(delim, 1)[0] + destination = models.Email.resolve_destination(localpart, domain_name, True) destination = [*destination, *wildcard_senders] if destination else [*wildcard_senders] return flask.jsonify(",".join(destination)) if destination else flask.abort(404) From 14360f8926461c62c019c62338fef1be2308f133 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 9 Oct 2021 18:28:50 +0200 Subject: [PATCH 31/61] RECIPIENT_DELIMITER can have several characters --- core/admin/mailu/configuration.py | 2 +- core/admin/mailu/internal/views/postfix.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 8d4d334a..1f2a9239 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -49,7 +49,7 @@ DEFAULT_CONFIG = { 'DKIM_PATH': '/dkim/{domain}.{selector}.key', 'DEFAULT_QUOTA': 1000000000, 'MESSAGE_RATELIMIT': '200/day', - 'RECIPIENT_DELIMITER': None, + 'RECIPIENT_DELIMITER': '', # Web settings 'SITENAME': 'Mailu', 'WEBSITE': 'https://mailu.io', diff --git a/core/admin/mailu/internal/views/postfix.py b/core/admin/mailu/internal/views/postfix.py index 44a685c9..ab965967 100644 --- a/core/admin/mailu/internal/views/postfix.py +++ b/core/admin/mailu/internal/views/postfix.py @@ -140,8 +140,7 @@ def postfix_sender_login(sender): localpart, domain_name = models.Email.resolve_domain(sender) if localpart is None: return flask.jsonify(",".join(wildcard_senders)) if wildcard_senders else flask.abort(404) - if delim := flask.current_app.config.get('RECIPIENT_DELIMITER'): - localpart = localpart.split(delim, 1)[0] + localpart = localpart[:next((i for i, ch in enumerate(localpart) if ch in flask.current_app.config.get('RECIPIENT_DELIMITER')), None)] destination = models.Email.resolve_destination(localpart, domain_name, True) destination = [*destination, *wildcard_senders] if destination else [*wildcard_senders] return flask.jsonify(",".join(destination)) if destination else flask.abort(404) From e127e6b32f8cf83123a5bcfa9431f4caeab80e22 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 9 Oct 2021 18:58:51 +0200 Subject: [PATCH 32/61] clarify the documentation --- docs/configuration.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 5f17b57e..5d8e87b1 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -93,9 +93,10 @@ go and fetch new email if available. Do not use too short delays if you do not want to be blacklisted by external services, but not too long delays if you want to receive your email in time. -The ``RECIPIENT_DELIMITER`` is a character used to delimit localpart from a -custom address part. For instance, if set to ``+``, users can use addresses -like ``localpart+custom@domain.tld`` to deliver mail to ``localpart@domain.tld``. +The ``RECIPIENT_DELIMITER`` is a list of characters used to delimit localpart +from a custom address part. For instance, if set to ``+-``, users can use +addresses like ``localpart+custom@example.com`` or ``localpart-custom@example.com`` +to deliver mail to ``localpart@example.com``. This is useful to provide external parties with different email addresses and later classify incoming mail based on the custom part. From 251eea55539856eeb984fa6ac93e601b5017f1a2 Mon Sep 17 00:00:00 2001 From: qy117121 Date: Thu, 14 Oct 2021 15:03:23 +0800 Subject: [PATCH 33/61] Update messages.po Updated translation --- .../zh_CN/LC_MESSAGES/messages.po | 546 +++++++++--------- 1 file changed, 261 insertions(+), 285 deletions(-) diff --git a/core/admin/mailu/translations/zh_CN/LC_MESSAGES/messages.po b/core/admin/mailu/translations/zh_CN/LC_MESSAGES/messages.po index ee204fec..a3363169 100644 --- a/core/admin/mailu/translations/zh_CN/LC_MESSAGES/messages.po +++ b/core/admin/mailu/translations/zh_CN/LC_MESSAGES/messages.po @@ -3,9 +3,11 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: POEditor.com\n" +"X-Generator: Poedit 1.5.7\n" "Project-Id-Version: Mailu\n" -"Language: zh-CN\n" +"Language: zh\n" +"Last-Translator: Chris Chuan \n" +"Language-Team: \n" #: mailu/ui/forms.py:32 msgid "Invalid email address." @@ -28,7 +30,7 @@ msgstr "密码" #: mailu/ui/forms.py:42 mailu/ui/templates/login.html:4 #: mailu/ui/templates/sidebar.html:111 msgid "Sign in" -msgstr "注册" +msgstr "登录" #: mailu/ui/forms.py:46 mailu/ui/forms.py:56 #: mailu/ui/templates/domain/details.html:27 @@ -44,6 +46,14 @@ msgstr "最大用户数" msgid "Maximum alias count" msgstr "最大别名数" +#: mailu/ui/forms.py:49 +msgid "Maximum user quota" +msgstr "最大用户配额" + +#: mailu/ui/forms.py:50 +msgid "Enable sign-up" +msgstr "启用注册" + #: mailu/ui/forms.py:51 mailu/ui/forms.py:72 mailu/ui/forms.py:83 #: mailu/ui/forms.py:128 mailu/ui/forms.py:140 #: mailu/ui/templates/alias/list.html:21 mailu/ui/templates/domain/list.html:21 @@ -57,10 +67,30 @@ msgstr "说明" msgid "Create" msgstr "创建" +#: mailu/ui/forms.py:57 +msgid "Initial admin" +msgstr "初始管理员" + +#: mailu/ui/forms.py:58 +msgid "Admin password" +msgstr "管理员密码" + #: mailu/ui/forms.py:59 mailu/ui/forms.py:79 mailu/ui/forms.py:91 msgid "Confirm password" msgstr "确认密码" +#: mailu/ui/forms.py:65 +msgid "Alternative name" +msgstr "备用名称" + +#: mailu/ui/forms.py:70 +msgid "Relayed domain name" +msgstr "中继域域名" + +#: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18 +msgid "Remote host" +msgstr "远程主机" + #: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22 #: mailu/ui/templates/user/signup_domain.html:16 msgid "Quota" @@ -68,16 +98,30 @@ msgstr "配额" #: mailu/ui/forms.py:81 msgid "Allow IMAP access" -msgstr "允许IMAP访问" +msgstr "允许 IMAP 访问" #: mailu/ui/forms.py:82 msgid "Allow POP3 access" -msgstr "允许POP3访问" +msgstr "允许 POP3 访问" + +#: mailu/ui/forms.py:84 +msgid "Enabled" +msgstr "启用" #: mailu/ui/forms.py:85 msgid "Save" msgstr "保存" +#: mailu/ui/forms.py:89 +msgid "Email address" +msgstr "邮件地址" + +#: mailu/ui/forms.py:93 mailu/ui/templates/sidebar.html:117 +#: mailu/ui/templates/user/signup.html:4 +#: mailu/ui/templates/user/signup_domain.html:4 +msgid "Sign up" +msgstr "注册" + #: mailu/ui/forms.py:97 msgid "Displayed name" msgstr "显示名称" @@ -86,10 +130,23 @@ msgstr "显示名称" msgid "Enable spam filter" msgstr "启用垃圾邮件过滤" -#: mailu/ui/forms.py:80 -msgid "Spam filter threshold" +#: mailu/ui/forms.py:99 +msgid "Spam filter tolerance" msgstr "垃圾邮件过滤器阈值" +#: mailu/ui/forms.py:100 +msgid "Enable forwarding" +msgstr "启用转发" + +#: mailu/ui/forms.py:101 +msgid "Keep a copy of the emails" +msgstr "保留电子邮件副本" + +#: mailu/ui/forms.py:103 mailu/ui/forms.py:139 +#: mailu/ui/templates/alias/list.html:20 +msgid "Destination" +msgstr "目的地址" + #: mailu/ui/forms.py:105 msgid "Save settings" msgstr "保存设置" @@ -102,19 +159,6 @@ msgstr "检查密码" msgid "Update password" msgstr "更新密码" -#: mailu/ui/forms.py:100 -msgid "Enable forwarding" -msgstr "启用转发" - -#: mailu/ui/forms.py:103 mailu/ui/forms.py:139 -#: mailu/ui/templates/alias/list.html:20 -msgid "Destination" -msgstr "目的地址" - -#: mailu/ui/forms.py:120 -msgid "Update" -msgstr "更新" - #: mailu/ui/forms.py:115 msgid "Enable automatic reply" msgstr "启用自动回复" @@ -127,6 +171,22 @@ msgstr "回复主题" msgid "Reply body" msgstr "回复正文" +#: mailu/ui/forms.py:119 +msgid "End of vacation" +msgstr "假期结束" + +#: mailu/ui/forms.py:120 +msgid "Update" +msgstr "更新" + +#: mailu/ui/forms.py:125 +msgid "Your token (write it down, as it will never be displayed again)" +msgstr "您的令牌(请记录,它只显示这一次)" + +#: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20 +msgid "Authorized IP" +msgstr "授权 IP" + #: mailu/ui/forms.py:136 msgid "Alias" msgstr "别名" @@ -169,70 +229,67 @@ msgstr "启用TLS" msgid "Username" msgstr "用户名" +#: mailu/ui/forms.py:163 +msgid "Keep emails on the server" +msgstr "在服务器上保留电子邮件" + +#: mailu/ui/forms.py:168 +msgid "Announcement subject" +msgstr "公告主题" + +#: mailu/ui/forms.py:170 +msgid "Announcement body" +msgstr "公告正文" + +#: mailu/ui/forms.py:172 +msgid "Send" +msgstr "发送" + +#: mailu/ui/templates/announcement.html:4 +msgid "Public announcement" +msgstr "公开公告" + +#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82 +msgid "Client setup" +msgstr "客户端设置" + +#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43 +msgid "Mail protocol" +msgstr "邮件协议" + +#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51 +msgid "Server name" +msgstr "服务器名称" + #: mailu/ui/templates/confirm.html:4 msgid "Confirm action" msgstr "确认操作" #: mailu/ui/templates/confirm.html:13 +#, python-format msgid "You are about to %(action)s. Please confirm your action." msgstr "即将%(action)s,请确认您的操作。" #: mailu/ui/templates/docker-error.html:4 msgid "Docker error" -msgstr "Docker错误" +msgstr "Docker 错误" #: mailu/ui/templates/docker-error.html:12 msgid "An error occurred while talking to the Docker server." msgstr "Docker服务器通信出错" -#: mailu/admin/templates/login.html:6 -msgid "Your account" -msgstr "你的帐户" - #: mailu/ui/templates/login.html:8 msgid "to access the administration tools" -msgstr "访问管理员工具" - -#: mailu/ui/templates/services.html:4 mailu/ui/templates/sidebar.html:39 -msgid "Services status" -msgstr "服务状态" - -#: mailu/ui/templates/services.html:10 -msgid "Service" -msgstr "服务" - -#: mailu/ui/templates/fetch/list.html:23 mailu/ui/templates/services.html:11 -msgid "Status" -msgstr "状态" - -#: mailu/ui/templates/services.html:12 -msgid "PID" -msgstr "进程ID" - -#: mailu/ui/templates/services.html:13 -msgid "Image" -msgstr "镜像" - -#: mailu/ui/templates/services.html:14 -msgid "Started" -msgstr "已开始" - -#: mailu/ui/templates/services.html:15 -msgid "Last update" -msgstr "最后更新" +msgstr "访问管理工具" #: mailu/ui/templates/sidebar.html:8 msgid "My account" -msgstr "我的帐户" +msgstr "我的账户" #: mailu/ui/templates/sidebar.html:11 mailu/ui/templates/user/list.html:34 msgid "Settings" msgstr "设置" -#: mailu/ui/templates/user/settings.html:22 -msgid "Auto-forward" -msgstr "自动转发" - #: mailu/ui/templates/sidebar.html:21 mailu/ui/templates/user/list.html:35 msgid "Auto-reply" msgstr "自动回复" @@ -240,39 +297,71 @@ msgstr "自动回复" #: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26 #: mailu/ui/templates/user/list.html:36 msgid "Fetched accounts" -msgstr "代收帐户" +msgstr "代收账户" -#: mailu/ui/templates/sidebar.html:105 -msgid "Sign out" -msgstr "登出" +#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4 +msgid "Authentication tokens" +msgstr "认证令牌" #: mailu/ui/templates/sidebar.html:35 msgid "Administration" msgstr "管理" +#: mailu/ui/templates/sidebar.html:44 +msgid "Announcement" +msgstr "公告" + #: mailu/ui/templates/sidebar.html:49 msgid "Administrators" msgstr "管理员" +#: mailu/ui/templates/sidebar.html:54 +msgid "Relayed domains" +msgstr "中继域" + +#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15 +msgid "Antispam" +msgstr "发垃圾邮件" + #: mailu/ui/templates/sidebar.html:66 msgid "Mail domains" msgstr "邮件域" +#: mailu/ui/templates/sidebar.html:72 +msgid "Go to" +msgstr "转到" + +#: mailu/ui/templates/sidebar.html:76 +msgid "Webmail" +msgstr "网页邮箱" + +#: mailu/ui/templates/sidebar.html:87 +msgid "Website" +msgstr "网站" + #: mailu/ui/templates/sidebar.html:92 msgid "Help" msgstr "帮助" +#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:98 +msgid "Register a domain" +msgstr "注册域名" + +#: mailu/ui/templates/sidebar.html:105 +msgid "Sign out" +msgstr "登出" + #: mailu/ui/templates/working.html:4 msgid "We are still working on this feature!" msgstr "该功能开发中……" #: mailu/ui/templates/admin/create.html:4 msgid "Add a global administrator" -msgstr "添加超级管理员" +msgstr "添加全局管理员" #: mailu/ui/templates/admin/list.html:4 msgid "Global administrators" -msgstr "超级管理员" +msgstr "全局管理员" #: mailu/ui/templates/admin/list.html:9 msgid "Add administrator" @@ -323,7 +412,7 @@ msgstr "添加别名" #: mailu/ui/templates/relay/list.html:20 mailu/ui/templates/token/list.html:21 #: mailu/ui/templates/user/list.html:24 msgid "Created" -msgstr "创建" +msgstr "已创建" #: mailu/ui/templates/alias/list.html:23 mailu/ui/templates/domain/list.html:23 #: mailu/ui/templates/fetch/list.html:25 mailu/ui/templates/relay/list.html:21 @@ -337,6 +426,22 @@ msgstr "上次编辑" msgid "Edit" msgstr "编辑" +#: mailu/ui/templates/alternative/create.html:4 +msgid "Create alternative domain" +msgstr "创建替代域" + +#: mailu/ui/templates/alternative/list.html:4 +msgid "Alternative domain list" +msgstr "替代域名列表" + +#: mailu/ui/templates/alternative/list.html:12 +msgid "Add alternative" +msgstr "添加替代" + +#: mailu/ui/templates/alternative/list.html:19 +msgid "Name" +msgstr "名称" + #: mailu/ui/templates/domain/create.html:4 #: mailu/ui/templates/domain/list.html:9 msgid "New domain" @@ -344,11 +449,15 @@ msgstr "新域" #: mailu/ui/templates/domain/details.html:4 msgid "Domain details" -msgstr "域详情" +msgstr "域详细信息" #: mailu/ui/templates/domain/details.html:15 msgid "Regenerate keys" -msgstr "重新生成密钥" +msgstr "重新生成秘钥" + +#: mailu/ui/templates/domain/details.html:17 +msgid "Generate keys" +msgstr "生成秘钥" #: mailu/ui/templates/domain/details.html:31 msgid "DNS MX entry" @@ -356,19 +465,19 @@ msgstr "DNS MX条目" #: mailu/ui/templates/domain/details.html:35 msgid "DNS SPF entries" -msgstr "DNS SPF条目" +msgstr "DNS SPF 条目" #: mailu/ui/templates/domain/details.html:42 msgid "DKIM public key" -msgstr "DKIM公钥" +msgstr "DKIM 公钥" #: mailu/ui/templates/domain/details.html:46 msgid "DNS DKIM entry" -msgstr "DNS DKIM条目" +msgstr "DNS DKIM 条目" #: mailu/ui/templates/domain/details.html:50 msgid "DNS DMARC entry" -msgstr "DNS DMARC条目" +msgstr "DNS DMARC 条目" #: mailu/ui/templates/domain/edit.html:4 msgid "Edit domain" @@ -392,7 +501,7 @@ msgstr "别名数量" #: mailu/ui/templates/domain/list.html:28 msgid "Details" -msgstr "详情" +msgstr "详细信息" #: mailu/ui/templates/domain/list.html:35 msgid "Users" @@ -406,26 +515,60 @@ msgstr "别名" msgid "Managers" msgstr "管理员" +#: mailu/ui/templates/domain/list.html:39 +msgid "Alternatives" +msgstr "备选方案" + +#: mailu/ui/templates/domain/signup.html:13 +msgid "" +"In order to register a new domain, you must first setup the\n" +" domain zone so that the domain MX points to this server" +msgstr "在注册一个新的域名前,您必须先为该域名设置 MX 记录,并使其指向本服务器" + +#: mailu/ui/templates/domain/signup.html:18 +msgid "" +"If you do not know how to setup an MX record for your DNS " +"zone,\n" +" please contact your DNS provider or administrator. Also, please wait " +"a\n" +" couple minutes after the MX is set so the local server " +"cache\n" +" expires." +msgstr "如果您不知道如何为域名设置 MX 记录,请联系你的DNS提供商或者系统管理员。在设置完成 MX 记录后,请等待本地域名服务器的缓存过期。" + + #: mailu/ui/templates/fetch/create.html:4 msgid "Add a fetched account" -msgstr "添加一个代收帐户" +msgstr "添加一个代收账户" #: mailu/ui/templates/fetch/edit.html:4 msgid "Update a fetched account" -msgstr "更新代收帐户" +msgstr "更新代收账户" #: mailu/ui/templates/fetch/list.html:12 msgid "Add an account" -msgstr "添加一个帐户" +msgstr "添加一个账户" #: mailu/ui/templates/fetch/list.html:19 msgid "Endpoint" msgstr "端点" +#: mailu/ui/templates/fetch/list.html:21 +msgid "Keep emails" +msgstr "保留电子邮件" + #: mailu/ui/templates/fetch/list.html:22 msgid "Last check" msgstr "上次检查" +#: mailu/ui/templates/fetch/list.html:35 +msgid "yes" +msgstr "是" + +#: mailu/ui/templates/fetch/list.html:35 +msgid "no" +msgstr "否" + #: mailu/ui/templates/manager/create.html:4 msgid "Add a manager" msgstr "添加一个管理员" @@ -438,41 +581,49 @@ msgstr "管理员列表" msgid "Add manager" msgstr "添加管理员" -#: mailu/ui/forms.py:168 -msgid "Announcement subject" -msgstr "公告主题" +#: mailu/ui/templates/relay/create.html:4 +msgid "New relay domain" +msgstr "新的中继域" -#: mailu/ui/forms.py:170 -msgid "Announcement body" -msgstr "公告正文" +#: mailu/ui/templates/relay/edit.html:4 +msgid "Edit relayd domain" +msgstr "编辑中继域" -#: mailu/ui/forms.py:172 -msgid "Send" -msgstr "发送" +#: mailu/ui/templates/relay/list.html:4 +msgid "Relayed domain list" +msgstr "中继域列表" -#: mailu/ui/templates/announcement.html:4 -msgid "Public announcement" -msgstr "公告" +#: mailu/ui/templates/relay/list.html:9 +msgid "New relayed domain" +msgstr "新的中继域" -#: mailu/ui/templates/announcement.html:8 -msgid "from" -msgstr "来自" +#: mailu/ui/templates/token/create.html:4 +msgid "Create an authentication token" +msgstr "创建一个认证令牌" -#: mailu/ui/templates/sidebar.html:44 -msgid "Announcement" -msgstr "公告" +#: mailu/ui/templates/token/list.html:12 +msgid "New token" +msgstr "新令牌" #: mailu/ui/templates/user/create.html:4 msgid "New user" msgstr "新用户" +#: mailu/ui/templates/user/create.html:15 +msgid "General" +msgstr "通用" + +#: mailu/ui/templates/user/create.html:22 +msgid "Features and quotas" +msgstr "功能和配额" + #: mailu/ui/templates/user/edit.html:4 msgid "Edit user" msgstr "编辑用户" #: mailu/ui/templates/user/forward.html:4 msgid "Forward emails" -msgstr "转发电子邮件" +msgstr "转发邮件" #: mailu/ui/templates/user/list.html:4 msgid "User list" @@ -492,201 +643,15 @@ msgstr "功能" #: mailu/ui/templates/user/password.html:4 msgid "Password update" -msgstr "密码更新" +msgstr "更新密码" #: mailu/ui/templates/user/reply.html:4 msgid "Automatic reply" msgstr "自动回复" -#: mailu/ui/forms.py:49 -msgid "Maximum user quota" -msgstr "最大用户容量" - -#: mailu/ui/forms.py:101 -msgid "Keep a copy of the emails" -msgstr "保留电子邮件副本" - -#: mailu/ui/forms.py:163 -msgid "Keep emails on the server" -msgstr "保留电子邮件在服务器上" - -#: mailu/ui/templates/fetch/list.html:21 -msgid "Keep emails" -msgstr "保存电子邮件" - -#: mailu/ui/templates/fetch/list.html:35 -msgid "yes" -msgstr "是" - -#: mailu/ui/templates/fetch/list.html:35 -msgid "no" -msgstr "否" - -#: mailu/ui/forms.py:65 -msgid "Alternative name" -msgstr "替代名称" - -#: mailu/ui/forms.py:70 -msgid "Relayed domain name" -msgstr "中继域域名" - -#: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18 -msgid "Remote host" -msgstr "远程主机" - -#: mailu/ui/templates/sidebar.html:54 -msgid "Relayed domains" -msgstr "中继域" - -#: mailu/ui/templates/alternative/create.html:4 -msgid "Create alternative domain" -msgstr "创建替代域" - -#: mailu/ui/templates/alternative/list.html:4 -msgid "Alternative domain list" -msgstr "替代域名列表" - -#: mailu/ui/templates/alternative/list.html:12 -msgid "Add alternative" -msgstr "添加替代" - -#: mailu/ui/templates/alternative/list.html:19 -msgid "Name" -msgstr "名称" - -#: mailu/ui/templates/domain/list.html:39 -msgid "Alternatives" -msgstr "备择方案" - -#: mailu/ui/templates/relay/create.html:4 -msgid "New relay domain" -msgstr "新的中继域" - -#: mailu/ui/templates/relay/edit.html:4 -msgid "Edit relayd domain" -msgstr "编辑中继域" - -#: mailu/ui/templates/relay/list.html:4 -msgid "Relayed domain list" -msgstr "中继域列表" - -#: mailu/ui/templates/relay/list.html:9 -msgid "New relayed domain" -msgstr "新的中继域" - -#: mailu/ui/forms.py:125 -msgid "Your token (write it down, as it will never be displayed again)" -msgstr "您的令牌(请记录,它只显示这一次)" - -#: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20 -msgid "Authorized IP" -msgstr "授权IP" - -#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4 -msgid "Authentication tokens" -msgstr "认证令牌" - -#: mailu/ui/templates/sidebar.html:72 -msgid "Go to" -msgstr "转到" - -#: mailu/ui/templates/sidebar.html:76 -msgid "Webmail" -msgstr "网页邮箱" - -#: mailu/ui/templates/sidebar.html:87 -msgid "Website" -msgstr "网站" - -#: mailu/ui/templates/token/create.html:4 -msgid "Create an authentication token" -msgstr "创建一个认证令牌" - -#: mailu/ui/templates/token/list.html:12 -msgid "New token" -msgstr "新的令牌" - -#: mailu/ui/templates/user/create.html:15 -msgid "General" -msgstr "通用" - -#: mailu/ui/templates/user/create.html:22 -msgid "Features and quotas" -msgstr "功能和配额" - -#: mailu/ui/templates/user/settings.html:14 -msgid "General settings" -msgstr "常规设置" - -#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15 -msgid "Antispam" -msgstr "反垃圾邮件" - -#: mailu/ui/forms.py:99 -msgid "Spam filter tolerance" -msgstr "垃圾邮件过滤器容忍度" - -#: mailu/ui/forms.py:50 -msgid "Enable sign-up" -msgstr "启用用户注册" - -#: mailu/ui/forms.py:57 -msgid "Initial admin" -msgstr "初始管理员" - -#: mailu/ui/forms.py:58 -msgid "Admin password" -msgstr "管理员密码" - -#: mailu/ui/forms.py:84 -msgid "Enabled" -msgstr "启用" - -#: mailu/ui/forms.py:89 -msgid "Email address" -msgstr "邮件地址" - -#: mailu/ui/forms.py:93 mailu/ui/templates/sidebar.html:117 -#: mailu/ui/templates/user/signup.html:4 -#: mailu/ui/templates/user/signup_domain.html:4 -msgid "Sign up" -msgstr "注册" - -#: mailu/ui/forms.py:119 -msgid "End of vacation" -msgstr "假期结束" - -#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82 -msgid "Client setup" -msgstr "客户端设置" - -#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43 -msgid "Mail protocol" -msgstr "邮件协议" - -#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51 -msgid "Server name" -msgstr "服务器名" - -#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:98 -msgid "Register a domain" -msgstr "注册域名" - -#: mailu/ui/templates/domain/details.html:17 -msgid "Generate keys" -msgstr "生成密钥" - -#: mailu/ui/templates/domain/signup.html:13 -msgid "In order to register a new domain, you must first setup the\n" -" domain zone so that the domain MX points to this server" -msgstr "在注册一个新的域名前,您必须先为该域名设置 MX 记录,并使其指向本服务器" - -#: mailu/ui/templates/domain/signup.html:18 -msgid "If you do not know how to setup an MX record for your DNS zone,\n" -" please contact your DNS provider or administrator. Also, please wait a\n" -" couple minutes after the MX is set so the local server cache\n" -" expires." -msgstr "如果您不知道如何为域名设置 MX 记录,请联系你的DNS提供商或者系统管理员。在设置完成 MX 记录后,请等待本地域名服务器的缓存过期。" +#: mailu/ui/templates/user/settings.html:22 +msgid "Auto-forward" +msgstr "自动转发" #: mailu/ui/templates/user/signup_domain.html:8 msgid "pick a domain for the new account" @@ -700,3 +665,14 @@ msgstr "域名" msgid "Available slots" msgstr "可用" +#~ msgid "Your account" +#~ msgstr "" + +#~ msgid "Spam filter threshold" +#~ msgstr "" + +#~ msgid "from" +#~ msgstr "" + +#~ msgid "General settings" +#~ msgstr "" From 866f784d06348b0328a17565203526c890c48564 Mon Sep 17 00:00:00 2001 From: qy117121 Date: Thu, 14 Oct 2021 15:05:32 +0800 Subject: [PATCH 34/61] Create messages.po Update the translation --- .../translations/zh/LC_MESSAGES/messages.po | 678 ++++++++++++++++++ 1 file changed, 678 insertions(+) create mode 100644 core/admin/mailu/translations/zh/LC_MESSAGES/messages.po diff --git a/core/admin/mailu/translations/zh/LC_MESSAGES/messages.po b/core/admin/mailu/translations/zh/LC_MESSAGES/messages.po new file mode 100644 index 00000000..a3363169 --- /dev/null +++ b/core/admin/mailu/translations/zh/LC_MESSAGES/messages.po @@ -0,0 +1,678 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.7\n" +"Project-Id-Version: Mailu\n" +"Language: zh\n" +"Last-Translator: Chris Chuan \n" +"Language-Team: \n" + +#: mailu/ui/forms.py:32 +msgid "Invalid email address." +msgstr "无效的邮件地址" + +#: mailu/ui/forms.py:36 +msgid "Confirm" +msgstr "确认" + +#: mailu/ui/forms.py:40 mailu/ui/forms.py:77 +msgid "E-mail" +msgstr "电子邮件" + +#: mailu/ui/forms.py:41 mailu/ui/forms.py:78 mailu/ui/forms.py:90 +#: mailu/ui/forms.py:109 mailu/ui/forms.py:162 +#: mailu/ui/templates/client.html:32 mailu/ui/templates/client.html:59 +msgid "Password" +msgstr "密码" + +#: mailu/ui/forms.py:42 mailu/ui/templates/login.html:4 +#: mailu/ui/templates/sidebar.html:111 +msgid "Sign in" +msgstr "登录" + +#: mailu/ui/forms.py:46 mailu/ui/forms.py:56 +#: mailu/ui/templates/domain/details.html:27 +#: mailu/ui/templates/domain/list.html:18 mailu/ui/templates/relay/list.html:17 +msgid "Domain name" +msgstr "域名" + +#: mailu/ui/forms.py:47 +msgid "Maximum user count" +msgstr "最大用户数" + +#: mailu/ui/forms.py:48 +msgid "Maximum alias count" +msgstr "最大别名数" + +#: mailu/ui/forms.py:49 +msgid "Maximum user quota" +msgstr "最大用户配额" + +#: mailu/ui/forms.py:50 +msgid "Enable sign-up" +msgstr "启用注册" + +#: mailu/ui/forms.py:51 mailu/ui/forms.py:72 mailu/ui/forms.py:83 +#: mailu/ui/forms.py:128 mailu/ui/forms.py:140 +#: mailu/ui/templates/alias/list.html:21 mailu/ui/templates/domain/list.html:21 +#: mailu/ui/templates/relay/list.html:19 mailu/ui/templates/token/list.html:19 +#: mailu/ui/templates/user/list.html:23 +msgid "Comment" +msgstr "说明" + +#: mailu/ui/forms.py:52 mailu/ui/forms.py:61 mailu/ui/forms.py:66 +#: mailu/ui/forms.py:73 mailu/ui/forms.py:132 mailu/ui/forms.py:141 +msgid "Create" +msgstr "创建" + +#: mailu/ui/forms.py:57 +msgid "Initial admin" +msgstr "初始管理员" + +#: mailu/ui/forms.py:58 +msgid "Admin password" +msgstr "管理员密码" + +#: mailu/ui/forms.py:59 mailu/ui/forms.py:79 mailu/ui/forms.py:91 +msgid "Confirm password" +msgstr "确认密码" + +#: mailu/ui/forms.py:65 +msgid "Alternative name" +msgstr "备用名称" + +#: mailu/ui/forms.py:70 +msgid "Relayed domain name" +msgstr "中继域域名" + +#: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18 +msgid "Remote host" +msgstr "远程主机" + +#: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22 +#: mailu/ui/templates/user/signup_domain.html:16 +msgid "Quota" +msgstr "配额" + +#: mailu/ui/forms.py:81 +msgid "Allow IMAP access" +msgstr "允许 IMAP 访问" + +#: mailu/ui/forms.py:82 +msgid "Allow POP3 access" +msgstr "允许 POP3 访问" + +#: mailu/ui/forms.py:84 +msgid "Enabled" +msgstr "启用" + +#: mailu/ui/forms.py:85 +msgid "Save" +msgstr "保存" + +#: mailu/ui/forms.py:89 +msgid "Email address" +msgstr "邮件地址" + +#: mailu/ui/forms.py:93 mailu/ui/templates/sidebar.html:117 +#: mailu/ui/templates/user/signup.html:4 +#: mailu/ui/templates/user/signup_domain.html:4 +msgid "Sign up" +msgstr "注册" + +#: mailu/ui/forms.py:97 +msgid "Displayed name" +msgstr "显示名称" + +#: mailu/ui/forms.py:98 +msgid "Enable spam filter" +msgstr "启用垃圾邮件过滤" + +#: mailu/ui/forms.py:99 +msgid "Spam filter tolerance" +msgstr "垃圾邮件过滤器阈值" + +#: mailu/ui/forms.py:100 +msgid "Enable forwarding" +msgstr "启用转发" + +#: mailu/ui/forms.py:101 +msgid "Keep a copy of the emails" +msgstr "保留电子邮件副本" + +#: mailu/ui/forms.py:103 mailu/ui/forms.py:139 +#: mailu/ui/templates/alias/list.html:20 +msgid "Destination" +msgstr "目的地址" + +#: mailu/ui/forms.py:105 +msgid "Save settings" +msgstr "保存设置" + +#: mailu/ui/forms.py:110 +msgid "Password check" +msgstr "检查密码" + +#: mailu/ui/forms.py:111 mailu/ui/templates/sidebar.html:16 +msgid "Update password" +msgstr "更新密码" + +#: mailu/ui/forms.py:115 +msgid "Enable automatic reply" +msgstr "启用自动回复" + +#: mailu/ui/forms.py:116 +msgid "Reply subject" +msgstr "回复主题" + +#: mailu/ui/forms.py:117 +msgid "Reply body" +msgstr "回复正文" + +#: mailu/ui/forms.py:119 +msgid "End of vacation" +msgstr "假期结束" + +#: mailu/ui/forms.py:120 +msgid "Update" +msgstr "更新" + +#: mailu/ui/forms.py:125 +msgid "Your token (write it down, as it will never be displayed again)" +msgstr "您的令牌(请记录,它只显示这一次)" + +#: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20 +msgid "Authorized IP" +msgstr "授权 IP" + +#: mailu/ui/forms.py:136 +msgid "Alias" +msgstr "别名" + +#: mailu/ui/forms.py:138 +msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)" +msgstr "使用SQL LIKE语法(例如,用于全部别名)" + +#: mailu/ui/forms.py:145 +msgid "Admin email" +msgstr "管理员邮箱" + +#: mailu/ui/forms.py:146 mailu/ui/forms.py:151 mailu/ui/forms.py:164 +msgid "Submit" +msgstr "提交" + +#: mailu/ui/forms.py:150 +msgid "Manager email" +msgstr "管理员邮箱" + +#: mailu/ui/forms.py:155 +msgid "Protocol" +msgstr "协议" + +#: mailu/ui/forms.py:158 +msgid "Hostname or IP" +msgstr "主机名或IP" + +#: mailu/ui/forms.py:159 mailu/ui/templates/client.html:20 +#: mailu/ui/templates/client.html:47 +msgid "TCP port" +msgstr "TCP端口" + +#: mailu/ui/forms.py:160 +msgid "Enable TLS" +msgstr "启用TLS" + +#: mailu/ui/forms.py:161 mailu/ui/templates/client.html:28 +#: mailu/ui/templates/client.html:55 mailu/ui/templates/fetch/list.html:20 +msgid "Username" +msgstr "用户名" + +#: mailu/ui/forms.py:163 +msgid "Keep emails on the server" +msgstr "在服务器上保留电子邮件" + +#: mailu/ui/forms.py:168 +msgid "Announcement subject" +msgstr "公告主题" + +#: mailu/ui/forms.py:170 +msgid "Announcement body" +msgstr "公告正文" + +#: mailu/ui/forms.py:172 +msgid "Send" +msgstr "发送" + +#: mailu/ui/templates/announcement.html:4 +msgid "Public announcement" +msgstr "公开公告" + +#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82 +msgid "Client setup" +msgstr "客户端设置" + +#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43 +msgid "Mail protocol" +msgstr "邮件协议" + +#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51 +msgid "Server name" +msgstr "服务器名称" + +#: mailu/ui/templates/confirm.html:4 +msgid "Confirm action" +msgstr "确认操作" + +#: mailu/ui/templates/confirm.html:13 +#, python-format +msgid "You are about to %(action)s. Please confirm your action." +msgstr "即将%(action)s,请确认您的操作。" + +#: mailu/ui/templates/docker-error.html:4 +msgid "Docker error" +msgstr "Docker 错误" + +#: mailu/ui/templates/docker-error.html:12 +msgid "An error occurred while talking to the Docker server." +msgstr "Docker服务器通信出错" + +#: mailu/ui/templates/login.html:8 +msgid "to access the administration tools" +msgstr "访问管理工具" + +#: mailu/ui/templates/sidebar.html:8 +msgid "My account" +msgstr "我的账户" + +#: mailu/ui/templates/sidebar.html:11 mailu/ui/templates/user/list.html:34 +msgid "Settings" +msgstr "设置" + +#: mailu/ui/templates/sidebar.html:21 mailu/ui/templates/user/list.html:35 +msgid "Auto-reply" +msgstr "自动回复" + +#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26 +#: mailu/ui/templates/user/list.html:36 +msgid "Fetched accounts" +msgstr "代收账户" + +#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4 +msgid "Authentication tokens" +msgstr "认证令牌" + +#: mailu/ui/templates/sidebar.html:35 +msgid "Administration" +msgstr "管理" + +#: mailu/ui/templates/sidebar.html:44 +msgid "Announcement" +msgstr "公告" + +#: mailu/ui/templates/sidebar.html:49 +msgid "Administrators" +msgstr "管理员" + +#: mailu/ui/templates/sidebar.html:54 +msgid "Relayed domains" +msgstr "中继域" + +#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15 +msgid "Antispam" +msgstr "发垃圾邮件" + +#: mailu/ui/templates/sidebar.html:66 +msgid "Mail domains" +msgstr "邮件域" + +#: mailu/ui/templates/sidebar.html:72 +msgid "Go to" +msgstr "转到" + +#: mailu/ui/templates/sidebar.html:76 +msgid "Webmail" +msgstr "网页邮箱" + +#: mailu/ui/templates/sidebar.html:87 +msgid "Website" +msgstr "网站" + +#: mailu/ui/templates/sidebar.html:92 +msgid "Help" +msgstr "帮助" + +#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:98 +msgid "Register a domain" +msgstr "注册域名" + +#: mailu/ui/templates/sidebar.html:105 +msgid "Sign out" +msgstr "登出" + +#: mailu/ui/templates/working.html:4 +msgid "We are still working on this feature!" +msgstr "该功能开发中……" + +#: mailu/ui/templates/admin/create.html:4 +msgid "Add a global administrator" +msgstr "添加全局管理员" + +#: mailu/ui/templates/admin/list.html:4 +msgid "Global administrators" +msgstr "全局管理员" + +#: mailu/ui/templates/admin/list.html:9 +msgid "Add administrator" +msgstr "添加管理员" + +#: mailu/ui/templates/admin/list.html:16 mailu/ui/templates/alias/list.html:18 +#: mailu/ui/templates/alternative/list.html:18 +#: mailu/ui/templates/domain/list.html:16 mailu/ui/templates/fetch/list.html:18 +#: mailu/ui/templates/manager/list.html:18 +#: mailu/ui/templates/relay/list.html:16 mailu/ui/templates/token/list.html:18 +#: mailu/ui/templates/user/list.html:18 +msgid "Actions" +msgstr "操作" + +#: mailu/ui/templates/admin/list.html:17 mailu/ui/templates/alias/list.html:19 +#: mailu/ui/templates/manager/list.html:19 mailu/ui/templates/user/list.html:20 +msgid "Email" +msgstr "电子邮件" + +#: mailu/ui/templates/admin/list.html:22 mailu/ui/templates/alias/list.html:29 +#: mailu/ui/templates/alternative/list.html:25 +#: mailu/ui/templates/domain/list.html:31 mailu/ui/templates/fetch/list.html:31 +#: mailu/ui/templates/manager/list.html:24 +#: mailu/ui/templates/relay/list.html:27 mailu/ui/templates/token/list.html:26 +#: mailu/ui/templates/user/list.html:31 +msgid "Delete" +msgstr "删除" + +#: mailu/ui/templates/alias/create.html:4 +msgid "Create alias" +msgstr "创建别名" + +#: mailu/ui/templates/alias/edit.html:4 +msgid "Edit alias" +msgstr "编辑别名" + +#: mailu/ui/templates/alias/list.html:4 +msgid "Alias list" +msgstr "别名列表" + +#: mailu/ui/templates/alias/list.html:12 +msgid "Add alias" +msgstr "添加别名" + +#: mailu/ui/templates/alias/list.html:22 +#: mailu/ui/templates/alternative/list.html:20 +#: mailu/ui/templates/domain/list.html:22 mailu/ui/templates/fetch/list.html:24 +#: mailu/ui/templates/relay/list.html:20 mailu/ui/templates/token/list.html:21 +#: mailu/ui/templates/user/list.html:24 +msgid "Created" +msgstr "已创建" + +#: mailu/ui/templates/alias/list.html:23 mailu/ui/templates/domain/list.html:23 +#: mailu/ui/templates/fetch/list.html:25 mailu/ui/templates/relay/list.html:21 +#: mailu/ui/templates/user/list.html:25 +msgid "Last edit" +msgstr "上次编辑" + +#: mailu/ui/templates/alias/list.html:28 mailu/ui/templates/domain/list.html:30 +#: mailu/ui/templates/fetch/list.html:30 mailu/ui/templates/relay/list.html:26 +#: mailu/ui/templates/user/list.html:30 +msgid "Edit" +msgstr "编辑" + +#: mailu/ui/templates/alternative/create.html:4 +msgid "Create alternative domain" +msgstr "创建替代域" + +#: mailu/ui/templates/alternative/list.html:4 +msgid "Alternative domain list" +msgstr "替代域名列表" + +#: mailu/ui/templates/alternative/list.html:12 +msgid "Add alternative" +msgstr "添加替代" + +#: mailu/ui/templates/alternative/list.html:19 +msgid "Name" +msgstr "名称" + +#: mailu/ui/templates/domain/create.html:4 +#: mailu/ui/templates/domain/list.html:9 +msgid "New domain" +msgstr "新域" + +#: mailu/ui/templates/domain/details.html:4 +msgid "Domain details" +msgstr "域详细信息" + +#: mailu/ui/templates/domain/details.html:15 +msgid "Regenerate keys" +msgstr "重新生成秘钥" + +#: mailu/ui/templates/domain/details.html:17 +msgid "Generate keys" +msgstr "生成秘钥" + +#: mailu/ui/templates/domain/details.html:31 +msgid "DNS MX entry" +msgstr "DNS MX条目" + +#: mailu/ui/templates/domain/details.html:35 +msgid "DNS SPF entries" +msgstr "DNS SPF 条目" + +#: mailu/ui/templates/domain/details.html:42 +msgid "DKIM public key" +msgstr "DKIM 公钥" + +#: mailu/ui/templates/domain/details.html:46 +msgid "DNS DKIM entry" +msgstr "DNS DKIM 条目" + +#: mailu/ui/templates/domain/details.html:50 +msgid "DNS DMARC entry" +msgstr "DNS DMARC 条目" + +#: mailu/ui/templates/domain/edit.html:4 +msgid "Edit domain" +msgstr "编辑域" + +#: mailu/ui/templates/domain/list.html:4 +msgid "Domain list" +msgstr "域列表" + +#: mailu/ui/templates/domain/list.html:17 +msgid "Manage" +msgstr "管理" + +#: mailu/ui/templates/domain/list.html:19 +msgid "Mailbox count" +msgstr "邮箱数量" + +#: mailu/ui/templates/domain/list.html:20 +msgid "Alias count" +msgstr "别名数量" + +#: mailu/ui/templates/domain/list.html:28 +msgid "Details" +msgstr "详细信息" + +#: mailu/ui/templates/domain/list.html:35 +msgid "Users" +msgstr "用户" + +#: mailu/ui/templates/domain/list.html:36 +msgid "Aliases" +msgstr "别名" + +#: mailu/ui/templates/domain/list.html:37 +msgid "Managers" +msgstr "管理员" + +#: mailu/ui/templates/domain/list.html:39 +msgid "Alternatives" +msgstr "备选方案" + +#: mailu/ui/templates/domain/signup.html:13 +msgid "" +"In order to register a new domain, you must first setup the\n" +" domain zone so that the domain MX points to this server" +msgstr "在注册一个新的域名前,您必须先为该域名设置 MX 记录,并使其指向本服务器" + +#: mailu/ui/templates/domain/signup.html:18 +msgid "" +"If you do not know how to setup an MX record for your DNS " +"zone,\n" +" please contact your DNS provider or administrator. Also, please wait " +"a\n" +" couple minutes after the MX is set so the local server " +"cache\n" +" expires." +msgstr "如果您不知道如何为域名设置 MX 记录,请联系你的DNS提供商或者系统管理员。在设置完成 MX 记录后,请等待本地域名服务器的缓存过期。" + + +#: mailu/ui/templates/fetch/create.html:4 +msgid "Add a fetched account" +msgstr "添加一个代收账户" + +#: mailu/ui/templates/fetch/edit.html:4 +msgid "Update a fetched account" +msgstr "更新代收账户" + +#: mailu/ui/templates/fetch/list.html:12 +msgid "Add an account" +msgstr "添加一个账户" + +#: mailu/ui/templates/fetch/list.html:19 +msgid "Endpoint" +msgstr "端点" + +#: mailu/ui/templates/fetch/list.html:21 +msgid "Keep emails" +msgstr "保留电子邮件" + +#: mailu/ui/templates/fetch/list.html:22 +msgid "Last check" +msgstr "上次检查" + +#: mailu/ui/templates/fetch/list.html:35 +msgid "yes" +msgstr "是" + +#: mailu/ui/templates/fetch/list.html:35 +msgid "no" +msgstr "否" + +#: mailu/ui/templates/manager/create.html:4 +msgid "Add a manager" +msgstr "添加一个管理员" + +#: mailu/ui/templates/manager/list.html:4 +msgid "Manager list" +msgstr "管理员列表" + +#: mailu/ui/templates/manager/list.html:12 +msgid "Add manager" +msgstr "添加管理员" + +#: mailu/ui/templates/relay/create.html:4 +msgid "New relay domain" +msgstr "新的中继域" + +#: mailu/ui/templates/relay/edit.html:4 +msgid "Edit relayd domain" +msgstr "编辑中继域" + +#: mailu/ui/templates/relay/list.html:4 +msgid "Relayed domain list" +msgstr "中继域列表" + +#: mailu/ui/templates/relay/list.html:9 +msgid "New relayed domain" +msgstr "新的中继域" + +#: mailu/ui/templates/token/create.html:4 +msgid "Create an authentication token" +msgstr "创建一个认证令牌" + +#: mailu/ui/templates/token/list.html:12 +msgid "New token" +msgstr "新令牌" + +#: mailu/ui/templates/user/create.html:4 +msgid "New user" +msgstr "新用户" + +#: mailu/ui/templates/user/create.html:15 +msgid "General" +msgstr "通用" + +#: mailu/ui/templates/user/create.html:22 +msgid "Features and quotas" +msgstr "功能和配额" + +#: mailu/ui/templates/user/edit.html:4 +msgid "Edit user" +msgstr "编辑用户" + +#: mailu/ui/templates/user/forward.html:4 +msgid "Forward emails" +msgstr "转发邮件" + +#: mailu/ui/templates/user/list.html:4 +msgid "User list" +msgstr "用户列表" + +#: mailu/ui/templates/user/list.html:12 +msgid "Add user" +msgstr "添加用户" + +#: mailu/ui/templates/user/list.html:19 mailu/ui/templates/user/settings.html:4 +msgid "User settings" +msgstr "用户设置" + +#: mailu/ui/templates/user/list.html:21 +msgid "Features" +msgstr "功能" + +#: mailu/ui/templates/user/password.html:4 +msgid "Password update" +msgstr "更新密码" + +#: mailu/ui/templates/user/reply.html:4 +msgid "Automatic reply" +msgstr "自动回复" + +#: mailu/ui/templates/user/settings.html:22 +msgid "Auto-forward" +msgstr "自动转发" + +#: mailu/ui/templates/user/signup_domain.html:8 +msgid "pick a domain for the new account" +msgstr "为新用户选择一个域名" + +#: mailu/ui/templates/user/signup_domain.html:14 +msgid "Domain" +msgstr "域名" + +#: mailu/ui/templates/user/signup_domain.html:15 +msgid "Available slots" +msgstr "可用" + +#~ msgid "Your account" +#~ msgstr "" + +#~ msgid "Spam filter threshold" +#~ msgstr "" + +#~ msgid "from" +#~ msgstr "" + +#~ msgid "General settings" +#~ msgstr "" From 632ce663ee41277e57ad56f522d29e213a030f81 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Thu, 14 Oct 2021 18:04:49 +0200 Subject: [PATCH 35/61] Prevent logins with no password --- core/admin/mailu/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index 01711a60..3c79e661 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -562,6 +562,8 @@ class User(Base, Email): """ verifies password against stored hash and updates hash if outdated """ + if password == '': + return False cache_result = self._credential_cache.get(self.get_id()) current_salt = self.password.split('$')[3] if len(self.password.split('$')) == 5 else None if cache_result and current_salt: From 893705169e38bc9fbc82873f2f8fef0bad146d0d Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Thu, 14 Oct 2021 19:07:11 +0200 Subject: [PATCH 36/61] PoC rspamd use dkimkeys from admin using vault api --- core/admin/mailu/internal/views/__init__.py | 2 +- core/admin/mailu/internal/views/rspamd.py | 30 +++++++++++++++++++++ core/rspamd/conf/arc.conf | 8 +++--- core/rspamd/conf/dkim_signing.conf | 6 +++-- core/rspamd/start.py | 1 + 5 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 core/admin/mailu/internal/views/rspamd.py diff --git a/core/admin/mailu/internal/views/__init__.py b/core/admin/mailu/internal/views/__init__.py index a32106c0..762b2a38 100644 --- a/core/admin/mailu/internal/views/__init__.py +++ b/core/admin/mailu/internal/views/__init__.py @@ -1,3 +1,3 @@ __all__ = [ - 'auth', 'postfix', 'dovecot', 'fetch' + 'auth', 'postfix', 'dovecot', 'fetch', 'rspamd' ] diff --git a/core/admin/mailu/internal/views/rspamd.py b/core/admin/mailu/internal/views/rspamd.py new file mode 100644 index 00000000..61e27a59 --- /dev/null +++ b/core/admin/mailu/internal/views/rspamd.py @@ -0,0 +1,30 @@ +from mailu import models, dkim +from mailu.internal import internal + +import flask + +def vault_error(*messages, status=404): + return flask.make_response(flask.jsonify({'errors':messages}), status) + +# rspamd key format: +# {"selectors":[{"pubkey":"...","domain":"...","valid_start":TS,"valid_end":TS,"key":"...","selector":"...","bits":...,"alg":"..."}]} + +# hashicorp vault answer format: +# {"request_id":"...","lease_id":"","renewable":false,"lease_duration":2764800,"data":{...see above...},"wrap_info":null,"warnings":null,"auth":null} + +@internal.route("/rspamd/vault/v1/dkim/") +def rspamd_dkim_key(domain_name): + domain = models.Domain.query.get(domain_name) or flask.abort(vault_error('unknown domain')) + key = domain.dkim_key or flask.abort(vault_error('no dkim key', status=400)) + return flask.jsonify({ + 'data': { + 'selectors': [ + { + 'domain' : domain.name, + 'key' : key.decode('utf8'), + 'selector': 'dkim', + } + ] + } + }) + diff --git a/core/rspamd/conf/arc.conf b/core/rspamd/conf/arc.conf index 205d4284..93414a96 100644 --- a/core/rspamd/conf/arc.conf +++ b/core/rspamd/conf/arc.conf @@ -1,4 +1,6 @@ -try_fallback = true; -path = "/dkim/$domain.$selector.key"; -selector = "dkim" +try_fallback = false; use_esld = false; +allow_username_mismatch = true; +use_vault = true; +vault_url = "http://{{ ADMIN_ADDRESS }}/internal/rspamd/vault"; +vault_token = "mailu"; diff --git a/core/rspamd/conf/dkim_signing.conf b/core/rspamd/conf/dkim_signing.conf index e00e8d67..93414a96 100644 --- a/core/rspamd/conf/dkim_signing.conf +++ b/core/rspamd/conf/dkim_signing.conf @@ -1,4 +1,6 @@ -try_fallback = true; -path = "/dkim/$domain.$selector.key"; +try_fallback = false; use_esld = false; allow_username_mismatch = true; +use_vault = true; +vault_url = "http://{{ ADMIN_ADDRESS }}/internal/rspamd/vault"; +vault_token = "mailu"; diff --git a/core/rspamd/start.py b/core/rspamd/start.py index e2e72bcb..fcb33a97 100755 --- a/core/rspamd/start.py +++ b/core/rspamd/start.py @@ -11,6 +11,7 @@ log.basicConfig(stream=sys.stderr, level=os.environ.get("LOG_LEVEL", "WARNING")) # Actual startup script os.environ["REDIS_ADDRESS"] = system.get_host_address_from_environment("REDIS", "redis") +os.environ["ADMIN_ADDRESS"] = system.get_host_address_from_environment("ADMIN", "admin") if os.environ.get("ANTIVIRUS") == 'clamav': os.environ["ANTIVIRUS_ADDRESS"] = system.get_host_address_from_environment("ANTIVIRUS", "antivirus:3310") From dc9f970a91b0aeb9ae8e710c9bfc328587056608 Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Thu, 14 Oct 2021 23:15:42 +0200 Subject: [PATCH 37/61] removed zh_CN and updated locale-map for datatables --- core/admin/Dockerfile | 2 +- .../zh_CN/LC_MESSAGES/messages.po | 678 ------------------ 2 files changed, 1 insertion(+), 679 deletions(-) delete mode 100644 core/admin/mailu/translations/zh_CN/LC_MESSAGES/messages.po diff --git a/core/admin/Dockerfile b/core/admin/Dockerfile index 083c0f39..cdc426c6 100644 --- a/core/admin/Dockerfile +++ b/core/admin/Dockerfile @@ -13,7 +13,7 @@ COPY webpack.config.js ./ COPY assets ./assets RUN set -eu \ && sed -i 's/#007bff/#55a5d9/' 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 \ + && 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; do \ cp node_modules/datatables.net-plugins/i18n/${l#*:}.json assets/${l%:*}.json; \ done \ && node_modules/.bin/webpack-cli --color diff --git a/core/admin/mailu/translations/zh_CN/LC_MESSAGES/messages.po b/core/admin/mailu/translations/zh_CN/LC_MESSAGES/messages.po deleted file mode 100644 index a3363169..00000000 --- a/core/admin/mailu/translations/zh_CN/LC_MESSAGES/messages.po +++ /dev/null @@ -1,678 +0,0 @@ -msgid "" -msgstr "" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 1.5.7\n" -"Project-Id-Version: Mailu\n" -"Language: zh\n" -"Last-Translator: Chris Chuan \n" -"Language-Team: \n" - -#: mailu/ui/forms.py:32 -msgid "Invalid email address." -msgstr "无效的邮件地址" - -#: mailu/ui/forms.py:36 -msgid "Confirm" -msgstr "确认" - -#: mailu/ui/forms.py:40 mailu/ui/forms.py:77 -msgid "E-mail" -msgstr "电子邮件" - -#: mailu/ui/forms.py:41 mailu/ui/forms.py:78 mailu/ui/forms.py:90 -#: mailu/ui/forms.py:109 mailu/ui/forms.py:162 -#: mailu/ui/templates/client.html:32 mailu/ui/templates/client.html:59 -msgid "Password" -msgstr "密码" - -#: mailu/ui/forms.py:42 mailu/ui/templates/login.html:4 -#: mailu/ui/templates/sidebar.html:111 -msgid "Sign in" -msgstr "登录" - -#: mailu/ui/forms.py:46 mailu/ui/forms.py:56 -#: mailu/ui/templates/domain/details.html:27 -#: mailu/ui/templates/domain/list.html:18 mailu/ui/templates/relay/list.html:17 -msgid "Domain name" -msgstr "域名" - -#: mailu/ui/forms.py:47 -msgid "Maximum user count" -msgstr "最大用户数" - -#: mailu/ui/forms.py:48 -msgid "Maximum alias count" -msgstr "最大别名数" - -#: mailu/ui/forms.py:49 -msgid "Maximum user quota" -msgstr "最大用户配额" - -#: mailu/ui/forms.py:50 -msgid "Enable sign-up" -msgstr "启用注册" - -#: mailu/ui/forms.py:51 mailu/ui/forms.py:72 mailu/ui/forms.py:83 -#: mailu/ui/forms.py:128 mailu/ui/forms.py:140 -#: mailu/ui/templates/alias/list.html:21 mailu/ui/templates/domain/list.html:21 -#: mailu/ui/templates/relay/list.html:19 mailu/ui/templates/token/list.html:19 -#: mailu/ui/templates/user/list.html:23 -msgid "Comment" -msgstr "说明" - -#: mailu/ui/forms.py:52 mailu/ui/forms.py:61 mailu/ui/forms.py:66 -#: mailu/ui/forms.py:73 mailu/ui/forms.py:132 mailu/ui/forms.py:141 -msgid "Create" -msgstr "创建" - -#: mailu/ui/forms.py:57 -msgid "Initial admin" -msgstr "初始管理员" - -#: mailu/ui/forms.py:58 -msgid "Admin password" -msgstr "管理员密码" - -#: mailu/ui/forms.py:59 mailu/ui/forms.py:79 mailu/ui/forms.py:91 -msgid "Confirm password" -msgstr "确认密码" - -#: mailu/ui/forms.py:65 -msgid "Alternative name" -msgstr "备用名称" - -#: mailu/ui/forms.py:70 -msgid "Relayed domain name" -msgstr "中继域域名" - -#: mailu/ui/forms.py:71 mailu/ui/templates/relay/list.html:18 -msgid "Remote host" -msgstr "远程主机" - -#: mailu/ui/forms.py:80 mailu/ui/templates/user/list.html:22 -#: mailu/ui/templates/user/signup_domain.html:16 -msgid "Quota" -msgstr "配额" - -#: mailu/ui/forms.py:81 -msgid "Allow IMAP access" -msgstr "允许 IMAP 访问" - -#: mailu/ui/forms.py:82 -msgid "Allow POP3 access" -msgstr "允许 POP3 访问" - -#: mailu/ui/forms.py:84 -msgid "Enabled" -msgstr "启用" - -#: mailu/ui/forms.py:85 -msgid "Save" -msgstr "保存" - -#: mailu/ui/forms.py:89 -msgid "Email address" -msgstr "邮件地址" - -#: mailu/ui/forms.py:93 mailu/ui/templates/sidebar.html:117 -#: mailu/ui/templates/user/signup.html:4 -#: mailu/ui/templates/user/signup_domain.html:4 -msgid "Sign up" -msgstr "注册" - -#: mailu/ui/forms.py:97 -msgid "Displayed name" -msgstr "显示名称" - -#: mailu/ui/forms.py:98 -msgid "Enable spam filter" -msgstr "启用垃圾邮件过滤" - -#: mailu/ui/forms.py:99 -msgid "Spam filter tolerance" -msgstr "垃圾邮件过滤器阈值" - -#: mailu/ui/forms.py:100 -msgid "Enable forwarding" -msgstr "启用转发" - -#: mailu/ui/forms.py:101 -msgid "Keep a copy of the emails" -msgstr "保留电子邮件副本" - -#: mailu/ui/forms.py:103 mailu/ui/forms.py:139 -#: mailu/ui/templates/alias/list.html:20 -msgid "Destination" -msgstr "目的地址" - -#: mailu/ui/forms.py:105 -msgid "Save settings" -msgstr "保存设置" - -#: mailu/ui/forms.py:110 -msgid "Password check" -msgstr "检查密码" - -#: mailu/ui/forms.py:111 mailu/ui/templates/sidebar.html:16 -msgid "Update password" -msgstr "更新密码" - -#: mailu/ui/forms.py:115 -msgid "Enable automatic reply" -msgstr "启用自动回复" - -#: mailu/ui/forms.py:116 -msgid "Reply subject" -msgstr "回复主题" - -#: mailu/ui/forms.py:117 -msgid "Reply body" -msgstr "回复正文" - -#: mailu/ui/forms.py:119 -msgid "End of vacation" -msgstr "假期结束" - -#: mailu/ui/forms.py:120 -msgid "Update" -msgstr "更新" - -#: mailu/ui/forms.py:125 -msgid "Your token (write it down, as it will never be displayed again)" -msgstr "您的令牌(请记录,它只显示这一次)" - -#: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20 -msgid "Authorized IP" -msgstr "授权 IP" - -#: mailu/ui/forms.py:136 -msgid "Alias" -msgstr "别名" - -#: mailu/ui/forms.py:138 -msgid "Use SQL LIKE Syntax (e.g. for catch-all aliases)" -msgstr "使用SQL LIKE语法(例如,用于全部别名)" - -#: mailu/ui/forms.py:145 -msgid "Admin email" -msgstr "管理员邮箱" - -#: mailu/ui/forms.py:146 mailu/ui/forms.py:151 mailu/ui/forms.py:164 -msgid "Submit" -msgstr "提交" - -#: mailu/ui/forms.py:150 -msgid "Manager email" -msgstr "管理员邮箱" - -#: mailu/ui/forms.py:155 -msgid "Protocol" -msgstr "协议" - -#: mailu/ui/forms.py:158 -msgid "Hostname or IP" -msgstr "主机名或IP" - -#: mailu/ui/forms.py:159 mailu/ui/templates/client.html:20 -#: mailu/ui/templates/client.html:47 -msgid "TCP port" -msgstr "TCP端口" - -#: mailu/ui/forms.py:160 -msgid "Enable TLS" -msgstr "启用TLS" - -#: mailu/ui/forms.py:161 mailu/ui/templates/client.html:28 -#: mailu/ui/templates/client.html:55 mailu/ui/templates/fetch/list.html:20 -msgid "Username" -msgstr "用户名" - -#: mailu/ui/forms.py:163 -msgid "Keep emails on the server" -msgstr "在服务器上保留电子邮件" - -#: mailu/ui/forms.py:168 -msgid "Announcement subject" -msgstr "公告主题" - -#: mailu/ui/forms.py:170 -msgid "Announcement body" -msgstr "公告正文" - -#: mailu/ui/forms.py:172 -msgid "Send" -msgstr "发送" - -#: mailu/ui/templates/announcement.html:4 -msgid "Public announcement" -msgstr "公开公告" - -#: mailu/ui/templates/client.html:4 mailu/ui/templates/sidebar.html:82 -msgid "Client setup" -msgstr "客户端设置" - -#: mailu/ui/templates/client.html:16 mailu/ui/templates/client.html:43 -msgid "Mail protocol" -msgstr "邮件协议" - -#: mailu/ui/templates/client.html:24 mailu/ui/templates/client.html:51 -msgid "Server name" -msgstr "服务器名称" - -#: mailu/ui/templates/confirm.html:4 -msgid "Confirm action" -msgstr "确认操作" - -#: mailu/ui/templates/confirm.html:13 -#, python-format -msgid "You are about to %(action)s. Please confirm your action." -msgstr "即将%(action)s,请确认您的操作。" - -#: mailu/ui/templates/docker-error.html:4 -msgid "Docker error" -msgstr "Docker 错误" - -#: mailu/ui/templates/docker-error.html:12 -msgid "An error occurred while talking to the Docker server." -msgstr "Docker服务器通信出错" - -#: mailu/ui/templates/login.html:8 -msgid "to access the administration tools" -msgstr "访问管理工具" - -#: mailu/ui/templates/sidebar.html:8 -msgid "My account" -msgstr "我的账户" - -#: mailu/ui/templates/sidebar.html:11 mailu/ui/templates/user/list.html:34 -msgid "Settings" -msgstr "设置" - -#: mailu/ui/templates/sidebar.html:21 mailu/ui/templates/user/list.html:35 -msgid "Auto-reply" -msgstr "自动回复" - -#: mailu/ui/templates/fetch/list.html:4 mailu/ui/templates/sidebar.html:26 -#: mailu/ui/templates/user/list.html:36 -msgid "Fetched accounts" -msgstr "代收账户" - -#: mailu/ui/templates/sidebar.html:31 mailu/ui/templates/token/list.html:4 -msgid "Authentication tokens" -msgstr "认证令牌" - -#: mailu/ui/templates/sidebar.html:35 -msgid "Administration" -msgstr "管理" - -#: mailu/ui/templates/sidebar.html:44 -msgid "Announcement" -msgstr "公告" - -#: mailu/ui/templates/sidebar.html:49 -msgid "Administrators" -msgstr "管理员" - -#: mailu/ui/templates/sidebar.html:54 -msgid "Relayed domains" -msgstr "中继域" - -#: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15 -msgid "Antispam" -msgstr "发垃圾邮件" - -#: mailu/ui/templates/sidebar.html:66 -msgid "Mail domains" -msgstr "邮件域" - -#: mailu/ui/templates/sidebar.html:72 -msgid "Go to" -msgstr "转到" - -#: mailu/ui/templates/sidebar.html:76 -msgid "Webmail" -msgstr "网页邮箱" - -#: mailu/ui/templates/sidebar.html:87 -msgid "Website" -msgstr "网站" - -#: mailu/ui/templates/sidebar.html:92 -msgid "Help" -msgstr "帮助" - -#: mailu/ui/templates/domain/signup.html:4 mailu/ui/templates/sidebar.html:98 -msgid "Register a domain" -msgstr "注册域名" - -#: mailu/ui/templates/sidebar.html:105 -msgid "Sign out" -msgstr "登出" - -#: mailu/ui/templates/working.html:4 -msgid "We are still working on this feature!" -msgstr "该功能开发中……" - -#: mailu/ui/templates/admin/create.html:4 -msgid "Add a global administrator" -msgstr "添加全局管理员" - -#: mailu/ui/templates/admin/list.html:4 -msgid "Global administrators" -msgstr "全局管理员" - -#: mailu/ui/templates/admin/list.html:9 -msgid "Add administrator" -msgstr "添加管理员" - -#: mailu/ui/templates/admin/list.html:16 mailu/ui/templates/alias/list.html:18 -#: mailu/ui/templates/alternative/list.html:18 -#: mailu/ui/templates/domain/list.html:16 mailu/ui/templates/fetch/list.html:18 -#: mailu/ui/templates/manager/list.html:18 -#: mailu/ui/templates/relay/list.html:16 mailu/ui/templates/token/list.html:18 -#: mailu/ui/templates/user/list.html:18 -msgid "Actions" -msgstr "操作" - -#: mailu/ui/templates/admin/list.html:17 mailu/ui/templates/alias/list.html:19 -#: mailu/ui/templates/manager/list.html:19 mailu/ui/templates/user/list.html:20 -msgid "Email" -msgstr "电子邮件" - -#: mailu/ui/templates/admin/list.html:22 mailu/ui/templates/alias/list.html:29 -#: mailu/ui/templates/alternative/list.html:25 -#: mailu/ui/templates/domain/list.html:31 mailu/ui/templates/fetch/list.html:31 -#: mailu/ui/templates/manager/list.html:24 -#: mailu/ui/templates/relay/list.html:27 mailu/ui/templates/token/list.html:26 -#: mailu/ui/templates/user/list.html:31 -msgid "Delete" -msgstr "删除" - -#: mailu/ui/templates/alias/create.html:4 -msgid "Create alias" -msgstr "创建别名" - -#: mailu/ui/templates/alias/edit.html:4 -msgid "Edit alias" -msgstr "编辑别名" - -#: mailu/ui/templates/alias/list.html:4 -msgid "Alias list" -msgstr "别名列表" - -#: mailu/ui/templates/alias/list.html:12 -msgid "Add alias" -msgstr "添加别名" - -#: mailu/ui/templates/alias/list.html:22 -#: mailu/ui/templates/alternative/list.html:20 -#: mailu/ui/templates/domain/list.html:22 mailu/ui/templates/fetch/list.html:24 -#: mailu/ui/templates/relay/list.html:20 mailu/ui/templates/token/list.html:21 -#: mailu/ui/templates/user/list.html:24 -msgid "Created" -msgstr "已创建" - -#: mailu/ui/templates/alias/list.html:23 mailu/ui/templates/domain/list.html:23 -#: mailu/ui/templates/fetch/list.html:25 mailu/ui/templates/relay/list.html:21 -#: mailu/ui/templates/user/list.html:25 -msgid "Last edit" -msgstr "上次编辑" - -#: mailu/ui/templates/alias/list.html:28 mailu/ui/templates/domain/list.html:30 -#: mailu/ui/templates/fetch/list.html:30 mailu/ui/templates/relay/list.html:26 -#: mailu/ui/templates/user/list.html:30 -msgid "Edit" -msgstr "编辑" - -#: mailu/ui/templates/alternative/create.html:4 -msgid "Create alternative domain" -msgstr "创建替代域" - -#: mailu/ui/templates/alternative/list.html:4 -msgid "Alternative domain list" -msgstr "替代域名列表" - -#: mailu/ui/templates/alternative/list.html:12 -msgid "Add alternative" -msgstr "添加替代" - -#: mailu/ui/templates/alternative/list.html:19 -msgid "Name" -msgstr "名称" - -#: mailu/ui/templates/domain/create.html:4 -#: mailu/ui/templates/domain/list.html:9 -msgid "New domain" -msgstr "新域" - -#: mailu/ui/templates/domain/details.html:4 -msgid "Domain details" -msgstr "域详细信息" - -#: mailu/ui/templates/domain/details.html:15 -msgid "Regenerate keys" -msgstr "重新生成秘钥" - -#: mailu/ui/templates/domain/details.html:17 -msgid "Generate keys" -msgstr "生成秘钥" - -#: mailu/ui/templates/domain/details.html:31 -msgid "DNS MX entry" -msgstr "DNS MX条目" - -#: mailu/ui/templates/domain/details.html:35 -msgid "DNS SPF entries" -msgstr "DNS SPF 条目" - -#: mailu/ui/templates/domain/details.html:42 -msgid "DKIM public key" -msgstr "DKIM 公钥" - -#: mailu/ui/templates/domain/details.html:46 -msgid "DNS DKIM entry" -msgstr "DNS DKIM 条目" - -#: mailu/ui/templates/domain/details.html:50 -msgid "DNS DMARC entry" -msgstr "DNS DMARC 条目" - -#: mailu/ui/templates/domain/edit.html:4 -msgid "Edit domain" -msgstr "编辑域" - -#: mailu/ui/templates/domain/list.html:4 -msgid "Domain list" -msgstr "域列表" - -#: mailu/ui/templates/domain/list.html:17 -msgid "Manage" -msgstr "管理" - -#: mailu/ui/templates/domain/list.html:19 -msgid "Mailbox count" -msgstr "邮箱数量" - -#: mailu/ui/templates/domain/list.html:20 -msgid "Alias count" -msgstr "别名数量" - -#: mailu/ui/templates/domain/list.html:28 -msgid "Details" -msgstr "详细信息" - -#: mailu/ui/templates/domain/list.html:35 -msgid "Users" -msgstr "用户" - -#: mailu/ui/templates/domain/list.html:36 -msgid "Aliases" -msgstr "别名" - -#: mailu/ui/templates/domain/list.html:37 -msgid "Managers" -msgstr "管理员" - -#: mailu/ui/templates/domain/list.html:39 -msgid "Alternatives" -msgstr "备选方案" - -#: mailu/ui/templates/domain/signup.html:13 -msgid "" -"In order to register a new domain, you must first setup the\n" -" domain zone so that the domain MX points to this server" -msgstr "在注册一个新的域名前,您必须先为该域名设置 MX 记录,并使其指向本服务器" - -#: mailu/ui/templates/domain/signup.html:18 -msgid "" -"If you do not know how to setup an MX record for your DNS " -"zone,\n" -" please contact your DNS provider or administrator. Also, please wait " -"a\n" -" couple minutes after the MX is set so the local server " -"cache\n" -" expires." -msgstr "如果您不知道如何为域名设置 MX 记录,请联系你的DNS提供商或者系统管理员。在设置完成 MX 记录后,请等待本地域名服务器的缓存过期。" - - -#: mailu/ui/templates/fetch/create.html:4 -msgid "Add a fetched account" -msgstr "添加一个代收账户" - -#: mailu/ui/templates/fetch/edit.html:4 -msgid "Update a fetched account" -msgstr "更新代收账户" - -#: mailu/ui/templates/fetch/list.html:12 -msgid "Add an account" -msgstr "添加一个账户" - -#: mailu/ui/templates/fetch/list.html:19 -msgid "Endpoint" -msgstr "端点" - -#: mailu/ui/templates/fetch/list.html:21 -msgid "Keep emails" -msgstr "保留电子邮件" - -#: mailu/ui/templates/fetch/list.html:22 -msgid "Last check" -msgstr "上次检查" - -#: mailu/ui/templates/fetch/list.html:35 -msgid "yes" -msgstr "是" - -#: mailu/ui/templates/fetch/list.html:35 -msgid "no" -msgstr "否" - -#: mailu/ui/templates/manager/create.html:4 -msgid "Add a manager" -msgstr "添加一个管理员" - -#: mailu/ui/templates/manager/list.html:4 -msgid "Manager list" -msgstr "管理员列表" - -#: mailu/ui/templates/manager/list.html:12 -msgid "Add manager" -msgstr "添加管理员" - -#: mailu/ui/templates/relay/create.html:4 -msgid "New relay domain" -msgstr "新的中继域" - -#: mailu/ui/templates/relay/edit.html:4 -msgid "Edit relayd domain" -msgstr "编辑中继域" - -#: mailu/ui/templates/relay/list.html:4 -msgid "Relayed domain list" -msgstr "中继域列表" - -#: mailu/ui/templates/relay/list.html:9 -msgid "New relayed domain" -msgstr "新的中继域" - -#: mailu/ui/templates/token/create.html:4 -msgid "Create an authentication token" -msgstr "创建一个认证令牌" - -#: mailu/ui/templates/token/list.html:12 -msgid "New token" -msgstr "新令牌" - -#: mailu/ui/templates/user/create.html:4 -msgid "New user" -msgstr "新用户" - -#: mailu/ui/templates/user/create.html:15 -msgid "General" -msgstr "通用" - -#: mailu/ui/templates/user/create.html:22 -msgid "Features and quotas" -msgstr "功能和配额" - -#: mailu/ui/templates/user/edit.html:4 -msgid "Edit user" -msgstr "编辑用户" - -#: mailu/ui/templates/user/forward.html:4 -msgid "Forward emails" -msgstr "转发邮件" - -#: mailu/ui/templates/user/list.html:4 -msgid "User list" -msgstr "用户列表" - -#: mailu/ui/templates/user/list.html:12 -msgid "Add user" -msgstr "添加用户" - -#: mailu/ui/templates/user/list.html:19 mailu/ui/templates/user/settings.html:4 -msgid "User settings" -msgstr "用户设置" - -#: mailu/ui/templates/user/list.html:21 -msgid "Features" -msgstr "功能" - -#: mailu/ui/templates/user/password.html:4 -msgid "Password update" -msgstr "更新密码" - -#: mailu/ui/templates/user/reply.html:4 -msgid "Automatic reply" -msgstr "自动回复" - -#: mailu/ui/templates/user/settings.html:22 -msgid "Auto-forward" -msgstr "自动转发" - -#: mailu/ui/templates/user/signup_domain.html:8 -msgid "pick a domain for the new account" -msgstr "为新用户选择一个域名" - -#: mailu/ui/templates/user/signup_domain.html:14 -msgid "Domain" -msgstr "域名" - -#: mailu/ui/templates/user/signup_domain.html:15 -msgid "Available slots" -msgstr "可用" - -#~ msgid "Your account" -#~ msgstr "" - -#~ msgid "Spam filter threshold" -#~ msgstr "" - -#~ msgid "from" -#~ msgstr "" - -#~ msgid "General settings" -#~ msgstr "" From 303fae00fb3a5bcc125c6223a3f3caae3a334ec5 Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Thu, 14 Oct 2021 23:25:42 +0200 Subject: [PATCH 38/61] cleanup modules. use dkim selector from config --- core/admin/mailu/internal/views/rspamd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/internal/views/rspamd.py b/core/admin/mailu/internal/views/rspamd.py index 61e27a59..aa969a5a 100644 --- a/core/admin/mailu/internal/views/rspamd.py +++ b/core/admin/mailu/internal/views/rspamd.py @@ -1,4 +1,4 @@ -from mailu import models, dkim +from mailu import models from mailu.internal import internal import flask @@ -22,7 +22,7 @@ def rspamd_dkim_key(domain_name): { 'domain' : domain.name, 'key' : key.decode('utf8'), - 'selector': 'dkim', + 'selector': flask.current_app.config.get('DKIM_SELECTOR', 'dkim'), } ] } From 7b0c5935a8c5d1b99ccf6927594ae0cf2b20afb1 Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Fri, 15 Oct 2021 13:16:37 +0200 Subject: [PATCH 39/61] only support GET method in vault --- core/admin/mailu/internal/views/rspamd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/internal/views/rspamd.py b/core/admin/mailu/internal/views/rspamd.py index aa969a5a..8551eb8f 100644 --- a/core/admin/mailu/internal/views/rspamd.py +++ b/core/admin/mailu/internal/views/rspamd.py @@ -12,7 +12,7 @@ def vault_error(*messages, status=404): # hashicorp vault answer format: # {"request_id":"...","lease_id":"","renewable":false,"lease_duration":2764800,"data":{...see above...},"wrap_info":null,"warnings":null,"auth":null} -@internal.route("/rspamd/vault/v1/dkim/") +@internal.route("/rspamd/vault/v1/dkim/", methods=['GET']) def rspamd_dkim_key(domain_name): domain = models.Domain.query.get(domain_name) or flask.abort(vault_error('unknown domain')) key = domain.dkim_key or flask.abort(vault_error('no dkim key', status=400)) From 135c5119c568a065c74292bf6232a3970c70c788 Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Fri, 15 Oct 2021 13:36:41 +0200 Subject: [PATCH 40/61] added newsfragment --- towncrier/newsfragments/2017.enhancement | 1 + 1 file changed, 1 insertion(+) create mode 100644 towncrier/newsfragments/2017.enhancement diff --git a/towncrier/newsfragments/2017.enhancement b/towncrier/newsfragments/2017.enhancement new file mode 100644 index 00000000..076914d2 --- /dev/null +++ b/towncrier/newsfragments/2017.enhancement @@ -0,0 +1 @@ +rspamd: get dkim keys via REST API instead of filesystem From 7fe15ea9cf81a1d29485c6d64e2dc68645ad6eef Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Fri, 15 Oct 2021 14:22:50 +0200 Subject: [PATCH 41/61] added dmarc record for report domain --- core/admin/mailu/models.py | 7 +++++++ core/admin/mailu/ui/templates/domain/details.html | 10 ++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index 01711a60..561a4ab6 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -243,6 +243,13 @@ class Domain(Base): ruf = f' ruf=mailto:{ruf}@{domain};' if ruf else '' return f'_dmarc.{self.name}. 600 IN TXT "v=DMARC1; p=reject;{rua}{ruf} adkim=s; aspf=s"' + @cached_property + def dns_dmarc_report(self): + """ return DMARC report record for mailu server """ + if self.dkim_key: + domain = app.config['DOMAIN'] + return f'{self.name}._report._dmarc.{domain}. 600 IN TXT "v=DMARC1"' + @cached_property def dns_autoconfig(self): """ return list of auto configuration records (RFC6186) """ diff --git a/core/admin/mailu/ui/templates/domain/details.html b/core/admin/mailu/ui/templates/domain/details.html index b90ea3de..a30b9357 100644 --- a/core/admin/mailu/ui/templates/domain/details.html +++ b/core/admin/mailu/ui/templates/domain/details.html @@ -46,7 +46,10 @@ {% trans %}DNS DMARC entry{% endtrans %} - {{ macros.clip("dns_dmark") }}
{{ domain.dns_dmarc }}
+ + {{ macros.clip("dns_dmarc") }}
{{ domain.dns_dmarc }}
+ {{ macros.clip("dns_dmarc_report") }}
{{ domain.dns_dmarc_report }}
+ {%- endif %} {%- set tlsa_record=domain.dns_tlsa %} @@ -58,12 +61,11 @@ {%- endif %} {% trans %}DNS client auto-configuration (RFC6186) entries{% endtrans %} - - {{ macros.clip("dns_autoconfig") }}
+  {{ macros.clip("dns_autoconfig") }}
 {%- for line in domain.dns_autoconfig %}
 {{ line }}
 {%- endfor -%}
-    
+
{%- endcall %} {%- endblock %} From b1425015ef080696cb11c8bf4416efd7ea8698a5 Mon Sep 17 00:00:00 2001 From: qy117121 Date: Sat, 16 Oct 2021 03:51:22 +0800 Subject: [PATCH 42/61] Update messages.po Fix wrong text --- .../translations/zh/LC_MESSAGES/messages.po | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/core/admin/mailu/translations/zh/LC_MESSAGES/messages.po b/core/admin/mailu/translations/zh/LC_MESSAGES/messages.po index a3363169..5543c5e8 100644 --- a/core/admin/mailu/translations/zh/LC_MESSAGES/messages.po +++ b/core/admin/mailu/translations/zh/LC_MESSAGES/messages.po @@ -98,11 +98,11 @@ msgstr "配额" #: mailu/ui/forms.py:81 msgid "Allow IMAP access" -msgstr "允许 IMAP 访问" +msgstr "允许IMAP访问" #: mailu/ui/forms.py:82 msgid "Allow POP3 access" -msgstr "允许 POP3 访问" +msgstr "允许POP3访问" #: mailu/ui/forms.py:84 msgid "Enabled" @@ -185,7 +185,7 @@ msgstr "您的令牌(请记录,它只显示这一次)" #: mailu/ui/forms.py:130 mailu/ui/templates/token/list.html:20 msgid "Authorized IP" -msgstr "授权 IP" +msgstr "授权IP" #: mailu/ui/forms.py:136 msgid "Alias" @@ -272,7 +272,7 @@ msgstr "即将%(action)s,请确认您的操作。" #: mailu/ui/templates/docker-error.html:4 msgid "Docker error" -msgstr "Docker 错误" +msgstr "Docker错误" #: mailu/ui/templates/docker-error.html:12 msgid "An error occurred while talking to the Docker server." @@ -321,7 +321,7 @@ msgstr "中继域" #: mailu/ui/templates/sidebar.html:59 mailu/ui/templates/user/settings.html:15 msgid "Antispam" -msgstr "发垃圾邮件" +msgstr "反垃圾邮件" #: mailu/ui/templates/sidebar.html:66 msgid "Mail domains" @@ -465,19 +465,19 @@ msgstr "DNS MX条目" #: mailu/ui/templates/domain/details.html:35 msgid "DNS SPF entries" -msgstr "DNS SPF 条目" +msgstr "DNS SPF条目" #: mailu/ui/templates/domain/details.html:42 msgid "DKIM public key" -msgstr "DKIM 公钥" +msgstr "DKIM公钥" #: mailu/ui/templates/domain/details.html:46 msgid "DNS DKIM entry" -msgstr "DNS DKIM 条目" +msgstr "DNS DKIM条目" #: mailu/ui/templates/domain/details.html:50 msgid "DNS DMARC entry" -msgstr "DNS DMARC 条目" +msgstr "DNS DMARC条目" #: mailu/ui/templates/domain/edit.html:4 msgid "Edit domain" From 57b0dd490c55dd28f03a2e5fa92dd6385ed6d111 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 09:29:17 +0200 Subject: [PATCH 43/61] Initialize user_email in all cases --- core/admin/mailu/internal/nginx.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index ff7070ab..7613c48a 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -89,6 +89,7 @@ def handle_authentication(headers): # we need to manually decode. raw_user_email = urllib.parse.unquote(headers["Auth-User"]) raw_password = urllib.parse.unquote(headers["Auth-Pass"]) + user_email = 'invalid' try: user_email = raw_user_email.encode("iso8859-1").decode("utf8") password = raw_password.encode("iso8859-1").decode("utf8") From 265ab7b5afd4be97810f8c3ea9fb9608654c83d8 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 09:31:09 +0200 Subject: [PATCH 44/61] Remove PASSWORD_SCHEME from test envs --- tests/compose/core/mailu.env | 4 ---- tests/compose/fetchmail/mailu.env | 4 ---- tests/compose/filters/mailu.env | 4 ---- tests/compose/rainloop/mailu.env | 4 ---- tests/compose/roundcube/mailu.env | 4 ---- tests/compose/webdav/mailu.env | 4 ---- 6 files changed, 24 deletions(-) diff --git a/tests/compose/core/mailu.env b/tests/compose/core/mailu.env index a78515b8..254c3c7d 100644 --- a/tests/compose/core/mailu.env +++ b/tests/compose/core/mailu.env @@ -128,10 +128,6 @@ WEBSITE=https://mailu.io # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME=mailu -# Default password scheme used for newly created accounts and changed passwords -# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT) -PASSWORD_SCHEME=PBKDF2 - # Header to take the real ip from REAL_IP_HEADER= diff --git a/tests/compose/fetchmail/mailu.env b/tests/compose/fetchmail/mailu.env index afb57751..a015eaa8 100644 --- a/tests/compose/fetchmail/mailu.env +++ b/tests/compose/fetchmail/mailu.env @@ -128,10 +128,6 @@ WEBSITE=https://mailu.io # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME=mailu -# Default password scheme used for newly created accounts and changed passwords -# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT) -PASSWORD_SCHEME=PBKDF2 - # Header to take the real ip from REAL_IP_HEADER= diff --git a/tests/compose/filters/mailu.env b/tests/compose/filters/mailu.env index 4c8c219d..1b4fb93d 100644 --- a/tests/compose/filters/mailu.env +++ b/tests/compose/filters/mailu.env @@ -128,10 +128,6 @@ WEBSITE=https://mailu.io # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME=mailu -# Default password scheme used for newly created accounts and changed passwords -# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT) -PASSWORD_SCHEME=PBKDF2 - # Header to take the real ip from REAL_IP_HEADER= diff --git a/tests/compose/rainloop/mailu.env b/tests/compose/rainloop/mailu.env index 1f5fce0c..944dd376 100644 --- a/tests/compose/rainloop/mailu.env +++ b/tests/compose/rainloop/mailu.env @@ -128,10 +128,6 @@ WEBSITE=https://mailu.io # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME=mailu -# Default password scheme used for newly created accounts and changed passwords -# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT) -PASSWORD_SCHEME=PBKDF2 - # Header to take the real ip from REAL_IP_HEADER= diff --git a/tests/compose/roundcube/mailu.env b/tests/compose/roundcube/mailu.env index ba153ac2..9db7fcbe 100644 --- a/tests/compose/roundcube/mailu.env +++ b/tests/compose/roundcube/mailu.env @@ -128,10 +128,6 @@ WEBSITE=https://mailu.io # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME=mailu -# Default password scheme used for newly created accounts and changed passwords -# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT) -PASSWORD_SCHEME=PBKDF2 - # Header to take the real ip from REAL_IP_HEADER= diff --git a/tests/compose/webdav/mailu.env b/tests/compose/webdav/mailu.env index 939f453b..f1de013d 100644 --- a/tests/compose/webdav/mailu.env +++ b/tests/compose/webdav/mailu.env @@ -128,10 +128,6 @@ WEBSITE=https://mailu.io # Docker-compose project name, this will prepended to containers names. COMPOSE_PROJECT_NAME=mailu -# Default password scheme used for newly created accounts and changed passwords -# (value: PBKDF2, BLF-CRYPT, SHA512-CRYPT, SHA256-CRYPT) -PASSWORD_SCHEME=PBKDF2 - # Header to take the real ip from REAL_IP_HEADER= From 4fff45bb30b69946a130d3047aad16cab5095953 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 09:31:33 +0200 Subject: [PATCH 45/61] Fix typo --- docs/configuration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index c736f30b..f4c3c255 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -40,7 +40,7 @@ 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_IP`` (default: 60/hour) holds a security setting for fighting -attackers that waste server ressources by trying to guess user passwords (typically +attackers that waste server resources by trying to guess user passwords (typically using a password spraying attack). The value defines the limit of authentication attempts that will be processed on non-existing accounts for a specific IP subnet (as defined in ``AUTH_RATELIMIT_IP_V4_MASK`` and ``AUTH_RATELIMIT_IP_V6_MASK`` below). From 068170c0ff48206c866eb0011b3526c738c55e93 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 09:35:01 +0200 Subject: [PATCH 46/61] Use app instead of flask.current_app where possible --- core/admin/mailu/utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index 9817287a..4711ff20 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -59,14 +59,14 @@ def has_dane_record(domain, timeout=10): # 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. - flask.current_app.logger.warn(f'Unable to lookup the TLSA record for {domain}. Is the DNSSEC zone okay on https://dnsviz.net/d/{domain}/dnssec/?') - return flask.current_app.config['DEFER_ON_TLS_ERROR'] + app.logger.warn(f'Unable to lookup the TLSA record for {domain}. Is the DNSSEC zone okay on https://dnsviz.net/d/{domain}/dnssec/?') + return app.config['DEFER_ON_TLS_ERROR'] except dns.exception.Timeout: - flask.current_app.logger.warn(f'Timeout while resolving the TLSA record for {domain} ({timeout}s).') + app.logger.warn(f'Timeout while resolving the TLSA record for {domain} ({timeout}s).') except dns.resolver.NXDOMAIN: pass # this is expected, not TLSA record is fine except Exception as e: - flask.current_app.logger.error(f'Error while looking up the TLSA record for {domain} {e}') + app.logger.error(f'Error while looking up the TLSA record for {domain} {e}') pass # Rate limiter @@ -88,8 +88,8 @@ babel = flask_babel.Babel() def get_locale(): """ selects locale for translation """ language = flask.session.get('language') - if not language in flask.current_app.config.translations: - language = flask.request.accept_languages.best_match(flask.current_app.config.translations.keys()) + if not language in app.config.translations: + language = flask.request.accept_languages.best_match(app.config.translations.keys()) flask.session['language'] = language return language @@ -486,7 +486,7 @@ class MailuSessionExtension: with cleaned.get_lock(): if not cleaned.value: cleaned.value = True - flask.current_app.logger.info('cleaning session store') + app.logger.info('cleaning session store') MailuSessionExtension.cleanup_sessions(app) app.before_first_request(cleaner) From 2dd9ea150616ab84517c0b6d248499eb1b62e3b3 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 09:36:49 +0200 Subject: [PATCH 47/61] simplify --- core/admin/mailu/internal/views/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index 457efd4b..0ed967ba 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -24,7 +24,7 @@ def nginx_authentication(): for key, value in headers.items(): response.headers[key] = str(value) is_valid_user = False - if "Auth-User-Exists" in response.headers and response.headers["Auth-User-Exists"]: + if response.headers.get("Auth-User-Exists"): username = response.headers["Auth-User"] if utils.limiter.should_rate_limit_user(username, client_ip): # FIXME could be done before handle_authentication() @@ -69,7 +69,7 @@ def user_authentication(): def basic_authentication(): """ Tries to authenticate using the Authorization header. """ - client_ip = flask.request.headers["X-Real-IP"] if 'X-Real-IP' in flask.request.headers else flask.request.remote_addr + client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr) if utils.limiter.should_rate_limit_ip(client_ip): response = flask.Response(status=401) response.headers["WWW-Authenticate"] = 'Basic realm="Authentication rate limit from one source exceeded"' From 3bda8368e41f5cbce30cc796896823eed75eef47 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 09:39:34 +0200 Subject: [PATCH 48/61] simplify the Auth-Status check --- core/admin/mailu/internal/views/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index 0ed967ba..1ea2828a 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -36,9 +36,9 @@ def nginx_authentication(): response.headers['Auth-Wait'] = '3' return response is_valid_user = True - if ("Auth-Status" not in headers) or (headers["Auth-Status"] != "OK"): + if headers.get('Auth-Status') != 'OK': utils.limiter.rate_limit_user(username, client_ip) if is_valid_user else rate_limit_ip(client_ip) - elif ("Auth-Status" in headers) and (headers["Auth-Status"] == "OK"): + elif headers["Auth-Status"] == "OK": utils.limiter.exempt_ip_from_ratelimits(client_ip) return response From de276a682285a4f568fb35d11a3f93f5d2785901 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 09:45:10 +0200 Subject: [PATCH 49/61] Simplify extract_network_from_ip --- core/admin/mailu/utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index 4711ff20..db760280 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -74,12 +74,10 @@ limiter = limiter.LimitWraperFactory() def extract_network_from_ip(ip): n = ipaddress.ip_network(ip) - if isinstance(n, ipaddress.IPv4Network): + if n.version == 4: return str(n.supernet(prefixlen_diff=(32-int(app.config["AUTH_RATELIMIT_IP_V4_MASK"]))).network_address) - elif isinstance(n, ipaddress.IPv6Network): + else: return str(n.supernet(prefixlen_diff=(128-int(app.config["AUTH_RATELIMIT_IP_V6_MASK"]))).network_address) - else: # not sure what to do with it - return ip # Application translation babel = flask_babel.Babel() From abaa2e8cc3e07e5f5a759072bdc83148f44438da Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 09:46:21 +0200 Subject: [PATCH 50/61] simplify client_ip --- core/admin/mailu/ui/views/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/ui/views/base.py b/core/admin/mailu/ui/views/base.py index 05211804..515d6055 100644 --- a/core/admin/mailu/ui/views/base.py +++ b/core/admin/mailu/ui/views/base.py @@ -14,7 +14,7 @@ def index(): @ui.route('/login', methods=['GET', 'POST']) def login(): - client_ip = flask.request.headers["X-Real-IP"] if 'X-Real-IP' in flask.request.headers else flask.request.remote_addr + client_ip = flask.request.headers.get('X-Real-IP', flask.request.remote_addr) form = forms.LoginForm() if form.validate_on_submit(): device_cookie, device_cookie_username = utils.limiter.parse_device_cookie(flask.request.cookies.get('rate_limit')) From e14d2e7c03eca6adecff3afe6e2de0d7247c399f Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 09:48:08 +0200 Subject: [PATCH 51/61] Error out explictely if Auth-Port isn't set --- core/admin/mailu/internal/nginx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/internal/nginx.py b/core/admin/mailu/internal/nginx.py index 7613c48a..4df8f55a 100644 --- a/core/admin/mailu/internal/nginx.py +++ b/core/admin/mailu/internal/nginx.py @@ -77,7 +77,7 @@ def handle_authentication(headers): # Authenticated user elif method == "plain": is_valid_user = False - if 'Auth-Port' in headers and int(urllib.parse.unquote(headers["Auth-Port"])) == 25: + if headers["Auth-Port"] == '25': return { "Auth-Status": "AUTH not supported", "Auth-Error-Code": "502 5.5.1", From 99c81c20a721d8b379e4af0399328c2a8cc0238a Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 10:26:38 +0200 Subject: [PATCH 52/61] Introduce AUTH_RATELIMIT_EXEMPTION This disables rate limiting on specific CIDRs --- core/admin/mailu/configuration.py | 1 + core/admin/mailu/limiter.py | 2 +- core/admin/mailu/utils.py | 6 ++++++ docs/configuration.rst | 4 ++++ 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 37f84b7c..6a3641fe 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -40,6 +40,7 @@ DEFAULT_CONFIG = { 'AUTH_RATELIMIT_IP_V4_MASK': 24, 'AUTH_RATELIMIT_IP_V6_MASK': 56, 'AUTH_RATELIMIT_USER': '100/day', + 'AUTH_RATELIMIT_EXEMPTION': '', 'AUTH_RATELIMIT_EXEMPTION_LENGTH': 86400, 'DISABLE_STATISTICS': False, # Mail settings diff --git a/core/admin/mailu/limiter.py b/core/admin/mailu/limiter.py index 88319012..ddaa07b3 100644 --- a/core/admin/mailu/limiter.py +++ b/core/admin/mailu/limiter.py @@ -39,7 +39,7 @@ class LimitWraperFactory(object): return LimitWrapper(self.limiter, limits.parse(limit), *args) def is_subject_to_rate_limits(self, ip): - return not (self.storage.get(f'exempt-{ip}') > 0) + return False if utils.is_subject_to_rate_limits(ip) else not (self.storage.get(f'exempt-{ip}') > 0) def exempt_ip_from_ratelimits(self, ip): self.storage.incr(f'exempt-{ip}', app.config["AUTH_RATELIMIT_EXEMPTION_LENGTH"], True) diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index db760280..d2bcc7a3 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -79,6 +79,12 @@ def extract_network_from_ip(ip): else: return str(n.supernet(prefixlen_diff=(128-int(app.config["AUTH_RATELIMIT_IP_V6_MASK"]))).network_address) +def is_exempt_from_ratelimits(ip): + for range in [net.strip() for net in app.config['AUTH_RATELIMIT_EXEMPTION'].split(',')]: + if ipaddress.ip_address(ip) in ipaddress.ip_network(ip, False): + return False + return True + # Application translation babel = flask_babel.Babel() diff --git a/docs/configuration.rst b/docs/configuration.rst index 75f397c7..f5bd9582 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -55,6 +55,10 @@ after a successful login for which a specific IP address is exempted from rate l This ensures that users behind a NAT don't get locked out when a single client is misconfigured... but also potentially allow for users to attack each-other. +The ``AUTH_RATELIMIT_EXEMPTION`` (default: '') is a comma separated list of network +CIDRs that won't be subject to any form of rate limiting. Specifying ``0.0.0.0/0, ::/0`` +there is a good way to disable rate limiting altogether. + The ``TLS_FLAVOR`` sets how Mailu handles TLS connections. Setting this value to ``notls`` will cause Mailu not to server any web content! More on :ref:`tls_flavor`. From c5bd82650fbe6bf26fafb2e9af454f0729574e1d Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 10:30:57 +0200 Subject: [PATCH 53/61] doh --- core/admin/mailu/limiter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/limiter.py b/core/admin/mailu/limiter.py index ddaa07b3..3bc65f4f 100644 --- a/core/admin/mailu/limiter.py +++ b/core/admin/mailu/limiter.py @@ -39,7 +39,7 @@ class LimitWraperFactory(object): return LimitWrapper(self.limiter, limits.parse(limit), *args) def is_subject_to_rate_limits(self, ip): - return False if utils.is_subject_to_rate_limits(ip) else not (self.storage.get(f'exempt-{ip}') > 0) + return False if utils.is_exempt_from_ratelimits(ip) else not (self.storage.get(f'exempt-{ip}') > 0) def exempt_ip_from_ratelimits(self, ip): self.storage.incr(f'exempt-{ip}', app.config["AUTH_RATELIMIT_EXEMPTION_LENGTH"], True) From 94bbed9746333dc81f136dbf2b8675cd1e84dab5 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 10:39:43 +0200 Subject: [PATCH 54/61] Ensure we have the right IP --- core/admin/mailu/internal/views/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index 1ea2828a..2c9a5d31 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -86,7 +86,7 @@ def basic_authentication(): response.headers['Retry-After'] = '60' return response user = models.User.query.get(user_email) - if user and nginx.check_credentials(user, password.decode('utf-8'), flask.request.remote_addr, "web"): + if user and nginx.check_credentials(user, password.decode('utf-8'), client_ip, "web"): response = flask.Response() response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, user.email, "") utils.limiter.exempt_ip_from_ratelimits(client_ip) From 98742268e6bae14dc778ecad69812068bc2038d5 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 15:12:20 +0200 Subject: [PATCH 55/61] Make it more readable --- core/admin/mailu/internal/views/auth.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/core/admin/mailu/internal/views/auth.py b/core/admin/mailu/internal/views/auth.py index 2c9a5d31..c5cd9e28 100644 --- a/core/admin/mailu/internal/views/auth.py +++ b/core/admin/mailu/internal/views/auth.py @@ -36,10 +36,12 @@ def nginx_authentication(): response.headers['Auth-Wait'] = '3' return response is_valid_user = True - if headers.get('Auth-Status') != 'OK': - utils.limiter.rate_limit_user(username, client_ip) if is_valid_user else rate_limit_ip(client_ip) - elif headers["Auth-Status"] == "OK": + if headers.get("Auth-Status") == "OK": utils.limiter.exempt_ip_from_ratelimits(client_ip) + elif is_valid_user: + utils.limiter.rate_limit_user(username, client_ip) + else: + rate_limit_ip(client_ip) return response @internal.route("/auth/admin") From 19b784b1982789f4548874a892a84c3e1828ab14 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 15:18:41 +0200 Subject: [PATCH 56/61] Parse the network configuration only once thanks @ghostwheel42 --- core/admin/mailu/configuration.py | 1 + core/admin/mailu/utils.py | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 6a3641fe..a4a42d03 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -152,6 +152,7 @@ class ConfigManager(dict): self.config['SESSION_COOKIE_HTTPONLY'] = True self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME'])) hostnames = [host.strip() for host in self.config['HOSTNAMES'].split(',')] + self.config['AUTH_RATELIMIT_EXEMPTION'] = set(ipaddress.ip_network(net.strip(), False) for net in config['AUTH_RATELIMIT_EXEMPTION'].split(',')) self.config['HOSTNAMES'] = ','.join(hostnames) self.config['HOSTNAME'] = hostnames[0] # update the app config itself diff --git a/core/admin/mailu/utils.py b/core/admin/mailu/utils.py index d2bcc7a3..dda927b0 100644 --- a/core/admin/mailu/utils.py +++ b/core/admin/mailu/utils.py @@ -80,10 +80,8 @@ def extract_network_from_ip(ip): return str(n.supernet(prefixlen_diff=(128-int(app.config["AUTH_RATELIMIT_IP_V6_MASK"]))).network_address) def is_exempt_from_ratelimits(ip): - for range in [net.strip() for net in app.config['AUTH_RATELIMIT_EXEMPTION'].split(',')]: - if ipaddress.ip_address(ip) in ipaddress.ip_network(ip, False): - return False - return True + ip = ipaddress.ip_address(ip) + return any(ip in cidr for cidr in app.config['AUTH_RATELIMIT_EXEMPTION']) # Application translation babel = flask_babel.Babel() From 5b72c32251daaa283314e80de236bc47cac5a48f Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 15:44:26 +0200 Subject: [PATCH 57/61] Doh --- core/admin/mailu/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index a4a42d03..2136e624 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -152,7 +152,7 @@ class ConfigManager(dict): self.config['SESSION_COOKIE_HTTPONLY'] = True self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME'])) hostnames = [host.strip() for host in self.config['HOSTNAMES'].split(',')] - self.config['AUTH_RATELIMIT_EXEMPTION'] = set(ipaddress.ip_network(net.strip(), False) for net in config['AUTH_RATELIMIT_EXEMPTION'].split(',')) + self.config['AUTH_RATELIMIT_EXEMPTION'] = set(ipaddress.ip_network(net.strip(), False) for net in self.config['AUTH_RATELIMIT_EXEMPTION'].split(',')) self.config['HOSTNAMES'] = ','.join(hostnames) self.config['HOSTNAME'] = hostnames[0] # update the app config itself From e8871dd77f12e00e63ab6c6c1355ee03860afa9d Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 16:06:13 +0200 Subject: [PATCH 58/61] doh --- core/admin/mailu/configuration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 2136e624..c1444ee5 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -2,6 +2,7 @@ import os from datetime import timedelta from socrate import system +import ipaddress DEFAULT_CONFIG = { # Specific to the admin UI From 34497cff209fa8c3cbf7a14756e0c6bff6660cea Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 16:35:48 +0200 Subject: [PATCH 59/61] doh --- core/admin/mailu/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index c1444ee5..4d4ee3e9 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -153,7 +153,7 @@ class ConfigManager(dict): self.config['SESSION_COOKIE_HTTPONLY'] = True self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME'])) hostnames = [host.strip() for host in self.config['HOSTNAMES'].split(',')] - self.config['AUTH_RATELIMIT_EXEMPTION'] = set(ipaddress.ip_network(net.strip(), False) for net in self.config['AUTH_RATELIMIT_EXEMPTION'].split(',')) + self.config['AUTH_RATELIMIT_EXEMPTION'] = set(ipaddress.ip_network(net.strip(), False) for net in self.config['AUTH_RATELIMIT_EXEMPTION'].split(',')) if self.config['AUTH_RATELIMIT_EXEMPTION'] else set() self.config['HOSTNAMES'] = ','.join(hostnames) self.config['HOSTNAME'] = hostnames[0] # update the app config itself From 1c6165213cc1420b16384a8c30017b2a35b33ff6 Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 16:54:56 +0200 Subject: [PATCH 60/61] better that way --- core/admin/mailu/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 4d4ee3e9..352bf431 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -153,7 +153,7 @@ class ConfigManager(dict): self.config['SESSION_COOKIE_HTTPONLY'] = True self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME'])) hostnames = [host.strip() for host in self.config['HOSTNAMES'].split(',')] - self.config['AUTH_RATELIMIT_EXEMPTION'] = set(ipaddress.ip_network(net.strip(), False) for net in self.config['AUTH_RATELIMIT_EXEMPTION'].split(',')) if self.config['AUTH_RATELIMIT_EXEMPTION'] else set() + self.config['AUTH_RATELIMIT_EXEMPTION'] = set(ipaddress.ip_network(cidr, False) for cidr in (cidr.strip() for cidr in self.config['AUTH_RATELIMIT_EXEMPTION'].split(',')) if cidr.strip()) self.config['HOSTNAMES'] = ','.join(hostnames) self.config['HOSTNAME'] = hostnames[0] # update the app config itself From 693b578bbb5ee60f5286405b54c38b783db6062a Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Sat, 16 Oct 2021 17:24:12 +0200 Subject: [PATCH 61/61] The second strip isn't necessary --- core/admin/mailu/configuration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/admin/mailu/configuration.py b/core/admin/mailu/configuration.py index 352bf431..d33efa12 100644 --- a/core/admin/mailu/configuration.py +++ b/core/admin/mailu/configuration.py @@ -153,7 +153,7 @@ class ConfigManager(dict): self.config['SESSION_COOKIE_HTTPONLY'] = True self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME'])) hostnames = [host.strip() for host in self.config['HOSTNAMES'].split(',')] - self.config['AUTH_RATELIMIT_EXEMPTION'] = set(ipaddress.ip_network(cidr, False) for cidr in (cidr.strip() for cidr in self.config['AUTH_RATELIMIT_EXEMPTION'].split(',')) if cidr.strip()) + self.config['AUTH_RATELIMIT_EXEMPTION'] = set(ipaddress.ip_network(cidr, False) for cidr in (cidr.strip() for cidr in self.config['AUTH_RATELIMIT_EXEMPTION'].split(',')) if cidr) self.config['HOSTNAMES'] = ','.join(hostnames) self.config['HOSTNAME'] = hostnames[0] # update the app config itself