mirror of
				https://github.com/optim-enterprises-bv/Mailu.git
				synced 2025-10-30 17:47:55 +00:00 
			
		
		
		
	Merge branch 'master' into new-release-mailu
This commit is contained in:
		| @@ -44,7 +44,7 @@ DEFAULT_CONFIG = { | |||||||
|     'AUTH_RATELIMIT_IP': '5/hour', |     'AUTH_RATELIMIT_IP': '5/hour', | ||||||
|     'AUTH_RATELIMIT_IP_V4_MASK': 24, |     'AUTH_RATELIMIT_IP_V4_MASK': 24, | ||||||
|     'AUTH_RATELIMIT_IP_V6_MASK': 48, |     'AUTH_RATELIMIT_IP_V6_MASK': 48, | ||||||
|     'AUTH_RATELIMIT_USER': '100/day', |     'AUTH_RATELIMIT_USER': '50/day', | ||||||
|     'AUTH_RATELIMIT_EXEMPTION': '', |     'AUTH_RATELIMIT_EXEMPTION': '', | ||||||
|     'AUTH_RATELIMIT_EXEMPTION_LENGTH': 86400, |     'AUTH_RATELIMIT_EXEMPTION_LENGTH': 86400, | ||||||
|     'DISABLE_STATISTICS': False, |     'DISABLE_STATISTICS': False, | ||||||
|   | |||||||
| @@ -85,6 +85,7 @@ def handle_authentication(headers): | |||||||
|         raw_user_email = urllib.parse.unquote(headers["Auth-User"]) |         raw_user_email = urllib.parse.unquote(headers["Auth-User"]) | ||||||
|         raw_password = urllib.parse.unquote(headers["Auth-Pass"]) |         raw_password = urllib.parse.unquote(headers["Auth-Pass"]) | ||||||
|         user_email = 'invalid' |         user_email = 'invalid' | ||||||
|  |         password = 'invalid' | ||||||
|         try: |         try: | ||||||
|             user_email = raw_user_email.encode("iso8859-1").decode("utf8") |             user_email = raw_user_email.encode("iso8859-1").decode("utf8") | ||||||
|             password = raw_password.encode("iso8859-1").decode("utf8") |             password = raw_password.encode("iso8859-1").decode("utf8") | ||||||
| @@ -107,6 +108,7 @@ def handle_authentication(headers): | |||||||
|                         "Auth-Server": server, |                         "Auth-Server": server, | ||||||
|                         "Auth-User": user_email, |                         "Auth-User": user_email, | ||||||
|                         "Auth-User-Exists": is_valid_user, |                         "Auth-User-Exists": is_valid_user, | ||||||
|  |                         "Auth-Password": password, | ||||||
|                         "Auth-Port": port |                         "Auth-Port": port | ||||||
|                     } |                     } | ||||||
|         status, code = get_status(protocol, "authentication") |         status, code = get_status(protocol, "authentication") | ||||||
| @@ -115,6 +117,7 @@ def handle_authentication(headers): | |||||||
|             "Auth-Error-Code": code, |             "Auth-Error-Code": code, | ||||||
|             "Auth-User": user_email, |             "Auth-User": user_email, | ||||||
|             "Auth-User-Exists": is_valid_user, |             "Auth-User-Exists": is_valid_user, | ||||||
|  |             "Auth-Password": password, | ||||||
|             "Auth-Wait": 0 |             "Auth-Wait": 0 | ||||||
|         } |         } | ||||||
|     # Unexpected |     # Unexpected | ||||||
|   | |||||||
| @@ -48,7 +48,7 @@ def nginx_authentication(): | |||||||
|     if headers.get("Auth-Status") == "OK": |     if headers.get("Auth-Status") == "OK": | ||||||
|         utils.limiter.exempt_ip_from_ratelimits(client_ip) |         utils.limiter.exempt_ip_from_ratelimits(client_ip) | ||||||
|     elif is_valid_user: |     elif is_valid_user: | ||||||
|         utils.limiter.rate_limit_user(username, client_ip) |         utils.limiter.rate_limit_user(username, client_ip, password=response.headers.get('Auth-Password', None)) | ||||||
|     elif not is_from_webmail: |     elif not is_from_webmail: | ||||||
|         utils.limiter.rate_limit_ip(client_ip, username) |         utils.limiter.rate_limit_ip(client_ip, username) | ||||||
|     return response |     return response | ||||||
|   | |||||||
| @@ -68,9 +68,13 @@ class LimitWraperFactory(object): | |||||||
|             app.logger.warn(f'Authentication attempt from {ip} for {username} has been rate-limited.') |             app.logger.warn(f'Authentication attempt from {ip} for {username} has been rate-limited.') | ||||||
|         return is_rate_limited |         return is_rate_limited | ||||||
|  |  | ||||||
|     def rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None): |     def rate_limit_user(self, username, ip, device_cookie=None, device_cookie_name=None, password=''): | ||||||
|         limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user') |         limiter = self.get_limiter(app.config["AUTH_RATELIMIT_USER"], 'auth-user') | ||||||
|         if self.is_subject_to_rate_limits(ip): |         if self.is_subject_to_rate_limits(ip): | ||||||
|  |             truncated_password = hmac.new(bytearray(username, 'utf-8'), bytearray(password, 'utf-8'), 'sha256').hexdigest()[-6:] | ||||||
|  |             if password and (self.storage.get(f'dedup2-{username}-{truncated_password}') > 0): | ||||||
|  |                 return | ||||||
|  |             self.storage.incr(f'dedup2-{username}-{truncated_password}', limits.parse(app.config['AUTH_RATELIMIT_USER']).GRANULARITY.seconds, True) | ||||||
|             limiter.hit(device_cookie if device_cookie_name == username else username) |             limiter.hit(device_cookie if device_cookie_name == username else username) | ||||||
|  |  | ||||||
|     """ Device cookies as described on: |     """ Device cookies as described on: | ||||||
|   | |||||||
| @@ -59,7 +59,7 @@ def login(): | |||||||
|                 flask.flash(msg, "error") |                 flask.flash(msg, "error") | ||||||
|             return response |             return response | ||||||
|         else: |         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, username) |             utils.limiter.rate_limit_user(username, client_ip, device_cookie, device_cookie_username, form.pw.data) if models.User.get(username) else utils.limiter.rate_limit_ip(client_ip, username) | ||||||
|             flask.current_app.logger.warn(f'Login failed for {username} from {client_ip}.') |             flask.current_app.logger.warn(f'Login failed for {username} from {client_ip}.') | ||||||
|             flask.flash('Wrong e-mail or password', 'error') |             flask.flash('Wrong e-mail or password', 'error') | ||||||
|     return flask.render_template('login.html', form=form, fields=fields) |     return flask.render_template('login.html', form=form, fields=fields) | ||||||
|   | |||||||
| @@ -128,6 +128,12 @@ class UserPasswordForm(flask_wtf.FlaskForm): | |||||||
|     pwned = fields.HiddenField(label='', default=-1) |     pwned = fields.HiddenField(label='', default=-1) | ||||||
|     submit = fields.SubmitField(_('Update password')) |     submit = fields.SubmitField(_('Update password')) | ||||||
|  |  | ||||||
|  | class UserPasswordChangeForm(flask_wtf.FlaskForm): | ||||||
|  |     current_pw = fields.PasswordField(_('Current password'), [validators.DataRequired()]) | ||||||
|  |     pw = fields.PasswordField(_('Password'), [validators.DataRequired()]) | ||||||
|  |     pw2 = fields.PasswordField(_('Password check'), [validators.DataRequired()]) | ||||||
|  |     pwned = fields.HiddenField(label='', default=-1) | ||||||
|  |     submit = fields.SubmitField(_('Update password')) | ||||||
|  |  | ||||||
| class UserReplyForm(flask_wtf.FlaskForm): | class UserReplyForm(flask_wtf.FlaskForm): | ||||||
|     reply_enabled = fields.BooleanField(_('Enable automatic reply')) |     reply_enabled = fields.BooleanField(_('Enable automatic reply')) | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ | |||||||
|         </a> |         </a> | ||||||
|       </li> |       </li> | ||||||
|       <li class="nav-item" role="none"> |       <li class="nav-item" role="none"> | ||||||
|         <a href="{{ url_for('.user_password') }}" class="nav-link" role="menuitem"> |         <a href="{{ url_for('.user_password_change') }}" class="nav-link" role="menuitem"> | ||||||
|           <i class="nav-icon fa fa-lock"></i> |           <i class="nav-icon fa fa-lock"></i> | ||||||
|           <p>{% trans %}Update password{% endtrans %}</p> |           <p>{% trans %}Update password{% endtrans %}</p> | ||||||
|         </a> |         </a> | ||||||
|   | |||||||
| @@ -99,18 +99,13 @@ def user_settings(user_email): | |||||||
|                 flask.url_for('.user_list', domain_name=user.domain.name)) |                 flask.url_for('.user_list', domain_name=user.domain.name)) | ||||||
|     return flask.render_template('user/settings.html', form=form, user=user) |     return flask.render_template('user/settings.html', form=form, user=user) | ||||||
|  |  | ||||||
|  | def _process_password_change(form, user_email): | ||||||
| @ui.route('/user/password', methods=['GET', 'POST'], defaults={'user_email': None}) |  | ||||||
| @ui.route('/user/password/<path:user_email>', methods=['GET', 'POST']) |  | ||||||
| @access.owner(models.User, 'user_email') |  | ||||||
| def user_password(user_email): |  | ||||||
|     user_email_or_current = user_email or flask_login.current_user.email |     user_email_or_current = user_email or flask_login.current_user.email | ||||||
|     user = models.User.query.get(user_email_or_current) or flask.abort(404) |     user = models.User.query.get(user_email_or_current) or flask.abort(404) | ||||||
|     form = forms.UserPasswordForm() |  | ||||||
|     if form.validate_on_submit(): |     if form.validate_on_submit(): | ||||||
|         if form.pw.data != form.pw2.data: |         if form.pw.data != form.pw2.data: | ||||||
|             flask.flash('Passwords do not match', 'error') |             flask.flash('Passwords do not match', 'error') | ||||||
|         else: |         elif user_email or models.User.login(user_email_or_current, form.current_pw.data): | ||||||
|             if msg := utils.isBadOrPwned(form): |             if msg := utils.isBadOrPwned(form): | ||||||
|                 flask.flash(msg, "error") |                 flask.flash(msg, "error") | ||||||
|                 return flask.render_template('user/password.html', form=form, user=user) |                 return flask.render_template('user/password.html', form=form, user=user) | ||||||
| @@ -121,8 +116,19 @@ def user_password(user_email): | |||||||
|             if user_email: |             if user_email: | ||||||
|                 return flask.redirect(flask.url_for('.user_list', |                 return flask.redirect(flask.url_for('.user_list', | ||||||
|                     domain_name=user.domain.name)) |                     domain_name=user.domain.name)) | ||||||
|  |         else: | ||||||
|  |             flask.flash('Wrong current password', 'error') | ||||||
|     return flask.render_template('user/password.html', form=form, user=user) |     return flask.render_template('user/password.html', form=form, user=user) | ||||||
|  |  | ||||||
|  | @ui.route('/user/password', methods=['GET', 'POST'], defaults={'user_email': None}) | ||||||
|  | @access.owner(models.User, 'user_email') | ||||||
|  | def user_password_change(user_email): | ||||||
|  |     return _process_password_change(forms.UserPasswordChangeForm(), user_email) | ||||||
|  |  | ||||||
|  | @ui.route('/user/password/<path:user_email>', methods=['GET', 'POST']) | ||||||
|  | @access.domain_admin(models.User, 'user_email') | ||||||
|  | def user_password(user_email): | ||||||
|  |     return _process_password_change(forms.UserPasswordForm(), user_email) | ||||||
|  |  | ||||||
| @ui.route('/user/reply', methods=['GET', 'POST'], defaults={'user_email': None}) | @ui.route('/user/reply', methods=['GET', 'POST'], defaults={'user_email': None}) | ||||||
| @ui.route('/user/reply/<path:user_email>', methods=['GET', 'POST']) | @ui.route('/user/reply/<path:user_email>', methods=['GET', 'POST']) | ||||||
|   | |||||||
| @@ -135,12 +135,6 @@ WEBSITE=https://mailu.io | |||||||
| # Advanced settings | # Advanced settings | ||||||
| ################################### | ################################### | ||||||
|  |  | ||||||
| # Log driver for front service. Possible values: |  | ||||||
| # json-file (default) |  | ||||||
| # journald (On systemd platforms, useful for Fail2Ban integration) |  | ||||||
| # syslog (Non systemd platforms, Fail2Ban integration. Disables `docker compose log` for front!) |  | ||||||
| LOG_DRIVER=json-file |  | ||||||
|  |  | ||||||
| # Docker-compose project name, this will prepended to containers names. | # Docker-compose project name, this will prepended to containers names. | ||||||
| COMPOSE_PROJECT_NAME=mailu | COMPOSE_PROJECT_NAME=mailu | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,7 +9,9 @@ services: | |||||||
|     restart: always |     restart: always | ||||||
|     env_file: .env |     env_file: .env | ||||||
|     logging: |     logging: | ||||||
|       driver: $LOG_DRIVER |       driver: journald | ||||||
|  |       options: | ||||||
|  |         tag: mailu-front | ||||||
|     ports: |     ports: | ||||||
|     - "$BIND_ADDRESS4:80:80" |     - "$BIND_ADDRESS4:80:80" | ||||||
|     - "$BIND_ADDRESS4:443:443" |     - "$BIND_ADDRESS4:443:443" | ||||||
| @@ -43,6 +45,10 @@ services: | |||||||
|     image: mailu/dovecot:$VERSION |     image: mailu/dovecot:$VERSION | ||||||
|     restart: always |     restart: always | ||||||
|     env_file: .env |     env_file: .env | ||||||
|  |     logging: | ||||||
|  |       driver: journald | ||||||
|  |       options: | ||||||
|  |         tag: mailu-imap | ||||||
|     volumes: |     volumes: | ||||||
|       - "$ROOT/mail:/mail" |       - "$ROOT/mail:/mail" | ||||||
|       - "$ROOT/overrides/dovecot:/overrides:ro" |       - "$ROOT/overrides/dovecot:/overrides:ro" | ||||||
| @@ -53,6 +59,10 @@ services: | |||||||
|     image: mailu/postfix:$VERSION |     image: mailu/postfix:$VERSION | ||||||
|     restart: always |     restart: always | ||||||
|     env_file: .env |     env_file: .env | ||||||
|  |     logging: | ||||||
|  |       driver: journald | ||||||
|  |       options: | ||||||
|  |         tag: mailu-smtp | ||||||
|     volumes: |     volumes: | ||||||
|       - "$ROOT/mailqueue:/queue" |       - "$ROOT/mailqueue:/queue" | ||||||
|       - "$ROOT/overrides/postfix:/overrides:ro" |       - "$ROOT/overrides/postfix:/overrides:ro" | ||||||
| @@ -63,6 +73,10 @@ services: | |||||||
|     image: mailu/rspamd:$VERSION |     image: mailu/rspamd:$VERSION | ||||||
|     restart: always |     restart: always | ||||||
|     env_file: .env |     env_file: .env | ||||||
|  |     logging: | ||||||
|  |       driver: journald | ||||||
|  |       options: | ||||||
|  |         tag: mailu-antispam | ||||||
|     volumes: |     volumes: | ||||||
|       - "$ROOT/filter:/var/lib/rspamd" |       - "$ROOT/filter:/var/lib/rspamd" | ||||||
|       - "$ROOT/dkim:/dkim:ro" |       - "$ROOT/dkim:/dkim:ro" | ||||||
| @@ -88,6 +102,10 @@ services: | |||||||
|     image: mailu/admin:$VERSION |     image: mailu/admin:$VERSION | ||||||
|     restart: always |     restart: always | ||||||
|     env_file: .env |     env_file: .env | ||||||
|  |     logging: | ||||||
|  |       driver: journald | ||||||
|  |       options: | ||||||
|  |         tag: mailu-admin | ||||||
|     volumes: |     volumes: | ||||||
|       - "$ROOT/data:/data" |       - "$ROOT/data:/data" | ||||||
|       - "$ROOT/dkim:/dkim" |       - "$ROOT/dkim:/dkim" | ||||||
|   | |||||||
| @@ -35,8 +35,6 @@ services: | |||||||
|     image: mailu/nginx:$VERSION |     image: mailu/nginx:$VERSION | ||||||
|     restart: always |     restart: always | ||||||
|     env_file: .env |     env_file: .env | ||||||
|     logging: |  | ||||||
|       driver: $LOG_DRIVER |  | ||||||
|     labels: # Traefik labels for simple reverse-proxying |     labels: # Traefik labels for simple reverse-proxying | ||||||
|       - "traefik.enable=true" |       - "traefik.enable=true" | ||||||
|       - "traefik.port=80" |       - "traefik.port=80" | ||||||
|   | |||||||
| @@ -47,10 +47,11 @@ accounts for a specific IP subnet as defined in | |||||||
| ``AUTH_RATELIMIT_IP_V4_MASK`` (default: /24) and | ``AUTH_RATELIMIT_IP_V4_MASK`` (default: /24) and | ||||||
| ``AUTH_RATELIMIT_IP_V6_MASK`` (default: /48). | ``AUTH_RATELIMIT_IP_V6_MASK`` (default: /48). | ||||||
|  |  | ||||||
| The ``AUTH_RATELIMIT_USER`` (default: 100/day) holds a security setting for fighting | The ``AUTH_RATELIMIT_USER`` (default: 50/day) holds a security setting for fighting | ||||||
| attackers that attempt to guess a user's password (typically using a password | attackers that attempt to guess a user's password (typically using a password | ||||||
| bruteforce attack). The value defines the limit of authentication attempts allowed | bruteforce attack). The value defines the limit of distinct authentication attempts | ||||||
| for any given account within a specific timeframe. | allowed for any given account within a specific timeframe. Multiple attempts for the | ||||||
|  | same account with the same password only counts for one. | ||||||
|  |  | ||||||
| The ``AUTH_RATELIMIT_EXEMPTION_LENGTH`` (default: 86400) is the number of seconds | 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. | after a successful login for which a specific IP address is exempted from rate limits. | ||||||
|   | |||||||
| @@ -579,8 +579,7 @@ down brute force attacks. The same applies to login attempts via the single sign | |||||||
| We *do* provide a possibility to export the logs from the ``front`` service and ``Admin`` service to the host. | We *do* provide a possibility to export the logs from the ``front`` service and ``Admin`` service to the host. | ||||||
| The ``front`` container logs failed logon attempts on SMTP, IMAP and POP3. | The ``front`` container logs failed logon attempts on SMTP, IMAP and POP3. | ||||||
| The ``Admin`` container logs failed logon attempt on the single sign on page. | The ``Admin`` container logs failed logon attempt on the single sign on page. | ||||||
| For this you need to set ``LOG_DRIVER=journald`` or ``syslog``, depending on the log | You will need to setup the proper Regex in the Fail2Ban configuration. | ||||||
| manager of the host. You will need to setup the proper Regex in the Fail2Ban configuration. |  | ||||||
| Below an example how to do so. | Below an example how to do so. | ||||||
|  |  | ||||||
| If you use a reverse proxy in front of Mailu, it is vital to set the environment variables REAL_IP_HEADER and REAL_IP_FROM. | If you use a reverse proxy in front of Mailu, it is vital to set the environment variables REAL_IP_HEADER and REAL_IP_FROM. | ||||||
|   | |||||||
| @@ -26,7 +26,9 @@ services: | |||||||
|     restart: always |     restart: always | ||||||
|     env_file: {{ env }} |     env_file: {{ env }} | ||||||
|     logging: |     logging: | ||||||
|       driver: {{ log_driver or 'json-file' }} |       driver: journald | ||||||
|  |       options: | ||||||
|  |         tag: mailu-front | ||||||
|     ports: |     ports: | ||||||
|     {% for port in (80, 443, 25, 465, 587, 110, 995, 143, 993) %} |     {% for port in (80, 443, 25, 465, 587, 110, 995, 143, 993) %} | ||||||
|     {% if bind4 %} |     {% if bind4 %} | ||||||
| @@ -38,8 +40,12 @@ services: | |||||||
|     {% endfor %} |     {% endfor %} | ||||||
|     networks: |     networks: | ||||||
|       - default |       - default | ||||||
|  | {% if webmail_type != 'none' %} | ||||||
|       - webmail |       - webmail | ||||||
|  | {% endif %} | ||||||
|  | {% if webdav_enabled %} | ||||||
|       - radicale |       - radicale | ||||||
|  | {% endif %} | ||||||
|     volumes: |     volumes: | ||||||
|       - "{{ root }}/certs:/certs" |       - "{{ root }}/certs:/certs" | ||||||
|       - "{{ root }}/overrides/nginx:/overrides:ro" |       - "{{ root }}/overrides/nginx:/overrides:ro" | ||||||
| @@ -62,6 +68,10 @@ services: | |||||||
|     image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}admin:${MAILU_VERSION:-{{ version }}} |     image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}admin:${MAILU_VERSION:-{{ version }}} | ||||||
|     restart: always |     restart: always | ||||||
|     env_file: {{ env }} |     env_file: {{ env }} | ||||||
|  |     logging: | ||||||
|  |       driver: journald | ||||||
|  |       options: | ||||||
|  |         tag: mailu-admin | ||||||
|     {% if not admin_enabled %} |     {% if not admin_enabled %} | ||||||
|     ports: |     ports: | ||||||
|       - 127.0.0.1:8080:80 |       - 127.0.0.1:8080:80 | ||||||
| @@ -81,6 +91,10 @@ services: | |||||||
|     image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}dovecot:${MAILU_VERSION:-{{ version }}} |     image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}dovecot:${MAILU_VERSION:-{{ version }}} | ||||||
|     restart: always |     restart: always | ||||||
|     env_file: {{ env }} |     env_file: {{ env }} | ||||||
|  |     logging: | ||||||
|  |       driver: journald | ||||||
|  |       options: | ||||||
|  |         tag: mailu-imap | ||||||
|     volumes: |     volumes: | ||||||
|       - "{{ root }}/mail:/mail" |       - "{{ root }}/mail:/mail" | ||||||
|       - "{{ root }}/overrides/dovecot:/overrides:ro" |       - "{{ root }}/overrides/dovecot:/overrides:ro" | ||||||
| @@ -96,6 +110,10 @@ services: | |||||||
|     image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}postfix:${MAILU_VERSION:-{{ version }}} |     image: ${DOCKER_ORG:-ghcr.io/mailu}/${DOCKER_PREFIX:-}postfix:${MAILU_VERSION:-{{ version }}} | ||||||
|     restart: always |     restart: always | ||||||
|     env_file: {{ env }} |     env_file: {{ env }} | ||||||
|  |     logging: | ||||||
|  |       driver: journald | ||||||
|  |       options: | ||||||
|  |         tag: mailu-smtp | ||||||
|     volumes: |     volumes: | ||||||
|       - "{{ root }}/mailqueue:/queue" |       - "{{ root }}/mailqueue:/queue" | ||||||
|       - "{{ root }}/overrides/postfix:/overrides:ro" |       - "{{ root }}/overrides/postfix:/overrides:ro" | ||||||
| @@ -127,6 +145,10 @@ services: | |||||||
|     hostname: antispam |     hostname: antispam | ||||||
|     restart: always |     restart: always | ||||||
|     env_file: {{ env }} |     env_file: {{ env }} | ||||||
|  |     logging: | ||||||
|  |       driver: journald | ||||||
|  |       options: | ||||||
|  |         tag: mailu-antispam | ||||||
| {% if oletools_enabled %} | {% if oletools_enabled %} | ||||||
|     networks: |     networks: | ||||||
|       - default |       - default | ||||||
|   | |||||||
| @@ -158,12 +158,6 @@ DOMAIN_REGISTRATION=true | |||||||
| # Advanced settings | # Advanced settings | ||||||
| ################################### | ################################### | ||||||
|  |  | ||||||
| # Log driver for front service. Possible values: |  | ||||||
| # json-file (default) |  | ||||||
| # journald (On systemd platforms, useful for Fail2Ban integration) |  | ||||||
| # syslog (Non systemd platforms, Fail2Ban integration. Disables `docker compose log` for front!) |  | ||||||
| # LOG_DRIVER={{ log_driver or 'json-file' }} |  | ||||||
|  |  | ||||||
| # Docker-compose project name, this will prepended to containers names. | # Docker-compose project name, this will prepended to containers names. | ||||||
| COMPOSE_PROJECT_NAME={{ compose_project_name or 'mailu' }} | COMPOSE_PROJECT_NAME={{ compose_project_name or 'mailu' }} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -47,7 +47,7 @@ Or in plain english: if receivers start to classify your mail as spam, this post | |||||||
|   <label>Authentication rate limit per user</label> |   <label>Authentication rate limit per user</label> | ||||||
|   <!--   Validates number input only --> |   <!--   Validates number input only --> | ||||||
|   <p><input class="form-control" style="width: 9%; display: inline;" type="number" name="auth_ratelimit_user" |   <p><input class="form-control" style="width: 9%; display: inline;" type="number" name="auth_ratelimit_user" | ||||||
|   		value="100" required > / day |   		value="50" required > / day | ||||||
|   </p> |   </p> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								towncrier/newsfragments/2726.misc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								towncrier/newsfragments/2726.misc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | Change the behaviour of AUTH_RATELIMIT_USER and only account for distinct attempts. Same username and same password is now a only accounted once per period. | ||||||
							
								
								
									
										1
									
								
								towncrier/newsfragments/2733.misc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								towncrier/newsfragments/2733.misc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | Ensure we ask for the existing password before processing a password change request. | ||||||
							
								
								
									
										2
									
								
								towncrier/newsfragments/2734.misc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								towncrier/newsfragments/2734.misc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | |||||||
|  | Remove LOG_DRIVER which never worked and replace it with journald by default | ||||||
|  | Fix a bug where front may get attached to networks that don't exist | ||||||
		Reference in New Issue
	
	Block a user
	 Dimitri Huisman
					Dimitri Huisman