Browse Source

Merge pull request #6838 from mailcow/staging

Update 2025-10
FreddleSpl0it 4 days ago
parent
commit
586b3a2ed1

+ 1 - 1
data/Dockerfiles/netfilter/docker-entrypoint.sh

@@ -1,6 +1,6 @@
 #!/bin/sh
 
-backend=iptables
+backend=nftables
 
 nft list table ip filter &>/dev/null
 nftables_found=$?

+ 5 - 0
data/Dockerfiles/netfilter/main.py

@@ -449,6 +449,11 @@ if __name__ == '__main__':
     tables = NFTables(chain_name, logger)
   else:
     logger.logInfo('Using IPTables backend')
+    logger.logWarn(
+        "DEPRECATION: iptables-legacy is deprecated and will be removed in future releases. "
+        "Please switch to nftables on your host to ensure complete compatibility."
+    )
+    time.sleep(5)
     tables = IPTables(chain_name, logger)
 
   clear()

+ 19 - 7
data/Dockerfiles/netfilter/modules/Logger.py

@@ -1,5 +1,6 @@
 import time
 import json
+import datetime
 
 class Logger:
   def __init__(self):
@@ -8,17 +9,28 @@ class Logger:
   def set_redis(self, redis):
     self.r = redis
 
+  def _format_timestamp(self):
+    # Local time with milliseconds
+    return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
   def log(self, priority, message):
-    tolog = {}
-    tolog['time'] = int(round(time.time()))
-    tolog['priority'] = priority
-    tolog['message'] = message
-    print(message)
+    # build redis-friendly dict
+    tolog = {
+      'time': int(round(time.time())),  # keep raw timestamp for Redis
+      'priority': priority,
+      'message': message
+    }
+
+    # print human-readable message with timestamp
+    ts = self._format_timestamp()
+    print(f"{ts} {priority.upper()}: {message}", flush=True)
+
+    # also push JSON to Redis if connected
     if self.r is not None:
       try:
         self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
       except Exception as ex:
-        print('Failed logging to redis: %s'  % (ex))
+        print(f'{ts} WARN: Failed logging to redis: {ex}', flush=True)
 
   def logWarn(self, message):
     self.log('warn', message)
@@ -27,4 +39,4 @@ class Logger:
     self.log('crit', message)
 
   def logInfo(self, message):
-    self.log('info', message)
+    self.log('info', message)

+ 2 - 2
data/Dockerfiles/phpfpm/Dockerfile

@@ -3,11 +3,11 @@ FROM php:8.2-fpm-alpine3.21
 LABEL maintainer = "The Infrastructure Company GmbH <[email protected]>"
 
 # renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
-ARG APCU_PECL_VERSION=5.1.26
+ARG APCU_PECL_VERSION=5.1.27
 # renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
 ARG IMAGICK_PECL_VERSION=3.8.0
 # renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
-ARG MAILPARSE_PECL_VERSION=3.1.8
+ARG MAILPARSE_PECL_VERSION=3.1.9
 # renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
 ARG MEMCACHED_PECL_VERSION=3.3.0
 # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$

+ 3 - 3
data/Dockerfiles/rspamd/Dockerfile

@@ -2,7 +2,7 @@ FROM debian:bookworm-slim
 LABEL maintainer="The Infrastructure Company GmbH <[email protected]>"
 
 ARG DEBIAN_FRONTEND=noninteractive
-ARG RSPAMD_VER=rspamd_3.12.1-1~6dbfca2fa
+ARG RSPAMD_VER=rspamd_3.13.2-1~8bf602278
 ARG CODENAME=bookworm
 ENV LC_ALL=C
 
@@ -14,8 +14,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
   dnsutils \
   netcat-traditional \
   wget \
-  redis-tools \ 
-  procps \ 
+  redis-tools \
+  procps \
   nano \
   lua-cjson \
   && arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \

+ 9 - 1
data/conf/phpfpm/php-conf.d/opcache-recommended.ini

@@ -1,7 +1,15 @@
+; NOTE: Restart phpfpm on ANY manual changes to PHP files!
+
+; opcache
 opcache.enable=1
 opcache.enable_cli=1
 opcache.interned_strings_buffer=16
 opcache.max_accelerated_files=10000
 opcache.memory_consumption=128
 opcache.save_comments=1
-opcache.revalidate_freq=1
+opcache.revalidate_freq=120
+opcache.validate_timestamps=0
+
+; JIT
+opcache.jit=1255
+opcache.jit_buffer_size=8M

+ 2 - 0
data/web/autodiscover.php

@@ -7,6 +7,8 @@ if(file_exists('inc/vars.local.inc.php')) {
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.inc.php';
 $default_autodiscover_config = $autodiscover_config;
 $autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
 

+ 16 - 1
data/web/inc/functions.inc.php

@@ -1006,7 +1006,7 @@ function edit_user_account($_data) {
     update_sogo_static_view();
   }
   // edit password recovery email
-  elseif (isset($pw_recovery_email)) {
+  elseif (!empty($password_old) && isset($pw_recovery_email)) {
     if (!isset($_SESSION['acl']['pw_reset']) || $_SESSION['acl']['pw_reset'] != "1" ) {
       $_SESSION['return'][] = array(
         'type' => 'danger',
@@ -1016,6 +1016,21 @@ function edit_user_account($_data) {
       return false;
     }
 
+    $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
+        WHERE `kind` NOT REGEXP 'location|thing|group'
+          AND `username` = :user AND authsource = 'mailcow'");
+    $stmt->execute(array(':user' => $username));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+    if (!verify_hash($row['password'], $password_old)) {
+      $_SESSION['return'][] =  array(
+        'type' => 'danger',
+        'log' => array(__FUNCTION__, $_data_log),
+        'msg' => 'access_denied'
+      );
+      return false;
+    }
+
     $pw_recovery_email = (!filter_var($pw_recovery_email, FILTER_VALIDATE_EMAIL)) ? '' : $pw_recovery_email;
     $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email)
       WHERE `username` = :username AND authsource = 'mailcow'");

+ 1 - 1
data/web/index.php

@@ -12,7 +12,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
   $user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']);
   $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
   if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual && getenv('SKIP_SOGO') != "y") {
-    header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}");
+    header("Location: /SOGo/so/");
   } else {
     header("Location: /user");
   }

+ 3 - 4
data/web/js/site/user.js

@@ -97,7 +97,7 @@ jQuery(function($){
               var datetime = new Date(item.datetime.replace(/-/g, "/"));
               var local_datetime = datetime.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
               var service = '<div class="badge bg-secondary">' + item.service.toUpperCase() + '</div>';
-              var app_password = item.app_password ? ' <a href="/edit/app-passwd/' + item.app_password + '"><i class="bi bi-app-indicator"></i> ' + escapeHtml(item.app_password_name || "App") + '</a>' : '';
+              var app_password = item.app_password ? ' <a href="/edit/app-passwd/' + item.app_password + '"><i class="bi bi-key-fill"></i><span class="ms-1">' + escapeHtml(item.app_password_name || "App") + '</span></a>' : '';
               var real_rip = item.real_rip.startsWith("Web") ? item.real_rip : '<a href="https://bgp.tools/prefix/' + item.real_rip + '" target="_blank">' + item.real_rip + "</a>";
               var ip_location = item.location ? ' <span class="flag-icon flag-icon-' + item.location.toLowerCase() + '"></span>' : '';
               var ip_data = real_rip + ip_location + app_password;
@@ -105,10 +105,9 @@ jQuery(function($){
               $(".last-sasl-login").append(`
                 <li class="list-group-item d-flex justify-content-between align-items-start">
                   <div class="ms-2 me-auto d-flex flex-column">
-                    <div class="fw-bold">` + real_rip + `</div>
-                    <small class="fst-italic mt-2">` + service + ` ` + local_datetime + `</small>
+                    <div class="fw-bold">` + ip_location + real_rip + `</div>
+                    <small class="fst-italic mt-2">` + service + ` ` + local_datetime + `</small>` + app_password + `
                   </div>
-                  <span>` + ip_location + `</span>
                 </li>
               `);
             })

+ 105 - 15
data/web/lang/lang.pt-br.json

@@ -1,7 +1,7 @@
 {
     "acl": {
-        "alias_domains": "Adicionar domínios alias",
-        "app_passwds": "Gerenciar senhas de aplicativos",
+        "alias_domains": "Adicionar alias de domínios",
+        "app_passwds": "Gerenciar senhas de app",
         "bcc_maps": "Mapas BCC",
         "delimiter_action": "Ação delimitadora",
         "domain_desc": "Alterar descrição do domínio",
@@ -9,7 +9,7 @@
         "eas_reset": "Redefinir dispositivos EAS",
         "extend_sender_acl": "Permitir estender a ACL do remetente por endereços externos",
         "filters": "Filtros",
-        "login_as": "Faça login como usuário da mailbox",
+        "login_as": "Fazer login como usuário da mailbox",
         "mailbox_relayhost": "Alterar relayhost para uma mailbox",
         "prohibited": "Proibido pela ACL",
         "protocol_access": "Alterar o acesso ao protocolo",
@@ -109,7 +109,9 @@
         "username": "Nome de usuário",
         "validate": "Validar",
         "validation_success": "Validado com sucesso",
-        "dry": "Simular sincronização"
+        "dry": "Simular sincronização",
+        "internal": "Interno",
+        "internal_info": "Aliases internos são acessíveis apenas a partir do próprio domínio ou alias de domínio."
     },
     "admin": {
         "access": "Acesso",
@@ -364,7 +366,52 @@
         "iam_client_secret": "Senha de cliente",
         "iam_auth_flow": "Fluxo de autenticação",
         "iam_client_scopes": "Escopo do cliente",
-        "iam_default_template": "Template Padrão"
+        "iam_default_template": "Template Padrão",
+        "admin_quicklink": "Ocultar link rápido para página de login do administrador",
+        "app_hide": "Ocultar para login",
+        "login_page": "Página de login",
+        "domainadmin_quicklink": "Ocultar link rápido para página de login do administrador de domínio",
+        "filter": "Filtro",
+        "force_sso_text": "Se um provedor OIDC externo for configurado, esta opção oculta os formulários de login padrão do mailcow e mostra apenas o botão de single sign-on",
+        "force_sso": "Desabilitar login do mailcow e mostrar apenas single sign-on",
+        "iam": "Provedor de identidade",
+        "iam_attribute_field": "Campo de atributo",
+        "iam_authorize_url": "Endpoint de autorização",
+        "iam_auth_flow_info": "Além do fluxo de código de autorização (fluxo padrão no Keycloak), que é usado para login de single sign-on, o mailcow também suporta fluxo de autenticação com credenciais diretas. O fluxo Mailpassword tenta validar as credenciais do usuário usando a API REST do administrador do Keycloak. O mailcow recupera a senha hash do atributo <code>mailcow_password</code>, que é mapeado no Keycloak.",
+        "iam_basedn": "DN base",
+        "iam_default_template_description": "Se nenhum template for atribuído a um usuário, o template padrão será usado para criar a caixa de correio, mas não para atualizar a caixa de correio.",
+        "iam_description": "Configure um provedor externo para autenticação<br>As caixas de correio dos usuários serão criadas automaticamente no primeiro login, desde que um mapeamento de atributos tenha sido definido.",
+        "iam_extra_permission": "Para que as configurações a seguir funcionem, o cliente mailcow no Keycloak precisa de uma <code>conta de serviço</code> e a permissão para <code>visualizar usuários</code>.",
+        "iam_host": "Host",
+        "iam_host_info": "Digite um ou mais hosts LDAP, separados por vírgulas.",
+        "iam_import_users": "Importar usuários",
+        "iam_login_provisioning": "Criar usuários automaticamente no login",
+        "iam_mapping": "Mapeamento de atributos",
+        "iam_bindpass": "Senha de vinculação",
+        "iam_periodic_full_sync": "Sincronização completa periódica",
+        "iam_port": "Porta",
+        "iam_realm": "Realm",
+        "iam_redirect_url": "URL de redirecionamento",
+        "iam_rest_flow": "Fluxo Mailpassword",
+        "iam_server_url": "URL do servidor",
+        "iam_sso": "Single sign-on",
+        "iam_sync_interval": "Intervalo de sincronização/importação (min)",
+        "iam_test_connection": "Testar conexão",
+        "iam_token_url": "Endpoint de token",
+        "iam_userinfo_url": "Endpoint de informações do usuário",
+        "iam_username_field": "Campo de nome de usuário",
+        "iam_binddn": "DN de vinculação",
+        "iam_use_ssl": "Usar SSL",
+        "iam_use_ssl_info": "Se habilitar SSL e a porta estiver definida como 389, ela será automaticamente substituída para usar 636.",
+        "iam_use_tls": "Usar StartTLS",
+        "iam_use_tls_info": "Se habilitar TLS, você deve usar a porta padrão para seu servidor LDAP (389). Portas SSL não podem ser usadas.",
+        "iam_version": "Versão",
+        "ignore_ssl_error": "Ignorar erros SSL",
+        "needs_restart": "precisa reiniciar",
+        "quicklink_text": "Mostrar ou ocultar links rápidos para outras páginas de login abaixo do formulário de login",
+        "task": "Tarefa",
+        "user_link": "Link do usuário",
+        "user_quicklink": "Ocultar link rápido para página de login do usuário"
     },
     "danger": {
         "access_denied": "Acesso negado ou dados de formulário inválidos",
@@ -501,7 +548,15 @@
         "username_invalid": "O nome de usuário %s não pode ser usado",
         "validity_missing": "Por favor, atribua um período de validade",
         "value_missing": "Forneça todos os valores",
-        "yotp_verification_failed": "Falha na verificação do Yubico OTP: %s"
+        "yotp_verification_failed": "Falha na verificação do Yubico OTP: %s",
+        "authsource_in_use": "O provedor de identidade não pode ser alterado ou excluído pois está sendo usado por um ou mais usuários.",
+        "generic_server_error": "Ocorreu um erro inesperado no servidor. Entre em contato com seu administrador.",
+        "iam_test_connection": "Falha na conexão",
+        "max_age_invalid": "Idade máxima %s é inválida",
+        "mode_invalid": "Modo %s é inválido",
+        "mx_invalid": "Registro MX %s é inválido",
+        "required_data_missing": "Dados obrigatórios %s estão ausentes",
+        "version_invalid": "Versão %s é inválida"
     },
     "datatables": {
         "collapse_all": "Recolher tudo",
@@ -708,7 +763,25 @@
         "title": "Editar objeto",
         "unchanged_if_empty": "Se inalterado, deixe em branco",
         "username": "Nome de usuário",
-        "validate_save": "Valide e salve"
+        "validate_save": "Validar e salvar",
+        "internal": "Interno",
+        "internal_info": "Aliases internos são acessíveis apenas a partir do próprio domínio ou domínios alias.",
+        "mailbox_rename": "Renomear caixa de correio",
+        "mailbox_rename_agree": "Eu criei um backup.",
+        "mailbox_rename_warning": "IMPORTANTE! Crie um backup antes de renomear a caixa de correio.",
+        "mailbox_rename_alias": "Criar alias automaticamente",
+        "mailbox_rename_title": "Novo nome da caixa de correio local",
+        "mta_sts": "MTA-STS",
+        "mta_sts_info": "<a href='https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security' target='_blank'>MTA-STS</a> é um padrão que força a entrega de email entre servidores de email para usar TLS com certificados válidos. <br>É usado quando <a target='_blank' href='https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities'>DANE</a> não é possível devido ao DNSSEC ausente ou não suportado.<br><b>Nota</b>: Se o domínio de recepção suporta DANE com DNSSEC, DANE é <b>sempre</b> preferido – MTA-STS atua apenas como fallback.",
+        "mta_sts_version": "Versão",
+        "mta_sts_version_info": "Define a versão do padrão MTA-STS – atualmente apenas <code>STSv1</code> é válido.",
+        "mta_sts_mode": "Modo",
+        "mta_sts_mode_info": "Há três modos para escolher:<ul><li><em>testing</em> – política é apenas monitorada, violações não têm impacto.</li><li><em>enforce</em> – política é rigorosamente aplicada, conexões sem TLS válido são rejeitadas.</li><li><em>none</em> – política é publicada mas não aplicada.</li></ul>",
+        "mta_sts_max_age": "Idade máxima",
+        "mta_sts_max_age_info": "Tempo em segundos que servidores de email de recepção podem armazenar esta política em cache até buscar novamente.",
+        "mta_sts_mx": "Servidor MX",
+        "mta_sts_mx_info": "Permite envio apenas para nomes de host de servidor de email explicitamente listados; o MTA de envio verifica se o nome do host DNS MX corresponde à lista de políticas e permite entrega apenas com certificado TLS válido (protege contra MITM).",
+        "mta_sts_mx_notice": "Múltiplos servidores MX podem ser especificados (separados por vírgulas)."
     },
     "fido2": {
         "confirm": "Confirme",
@@ -771,7 +844,15 @@
         "password": "Senha",
         "reset_password": "Recuperar a senha",
         "request_reset_password": "Solicitar troca de senha",
-        "username": "Nome de usuário"
+        "username": "Nome de usuário",
+        "login_linkstext": "Login incorreto?",
+        "login_usertext": "Entrar como usuário",
+        "login_domainadmintext": "Entrar como administrador de domínio",
+        "login_admintext": "Entrar como administrador",
+        "login_user": "Login de usuário",
+        "login_dadmin": "Login como administrador de domínio",
+        "login_admin": "Login como administrador",
+        "email": "Endereço de email"
     },
     "mailbox": {
         "action": "Ação",
@@ -946,7 +1027,9 @@
         "username": "Nome de usuário",
         "waiting": "Esperando",
         "weekly": "Semanalmente",
-        "yes": "✓"
+        "yes": "✓",
+        "iam": "Provedor de Identidade",
+        "internal": "Interno"
     },
     "oauth2": {
         "access_denied": "Faça login como proprietário da mailbox para conceder acesso via OAuth2.",
@@ -961,8 +1044,8 @@
         "action": "Ação",
         "atts": "Anexos",
         "check_hash": "Arquivo de pesquisa hash @ VT",
-        "confirm": "Confirme",
-        "confirm_delete": "Confirme a exclusão desse elemento.",
+        "confirm": "Confirmar",
+        "confirm_delete": "Confirmar exclusão desse elemento.",
         "danger": "Perigo",
         "deliver_inbox": "Entregar na caixa de entrada",
         "disabled_by_config": "A configuração atual do sistema desativa a funcionalidade de quarentena. Defina “retenções por mailbox” e um “tamanho máximo” para os elementos de quarentena.",
@@ -1123,12 +1206,15 @@
         "verified_fido2_login": "Login FIDO2 verificado",
         "verified_totp_login": "Login TOTP verificado",
         "verified_webauthn_login": "Login verificado do WebAuthn",
-        "verified_yotp_login": "Login OTP verificado do Yubico"
+        "verified_yotp_login": "Login OTP verificado do Yubico",
+        "custom_login_modified": "Personalização de login foi salva com sucesso",
+        "iam_test_connection": "Conexão bem-sucedida",
+        "mailbox_renamed": "Caixa de correio foi renomeada de %s para %s"
     },
     "tfa": {
         "authenticators": "Autenticadores",
         "api_register": "%s usa a API Yubico Cloud. Obtenha uma chave de API para sua chave <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">aqui</a>",
-        "confirm": "Confirme",
+        "confirm": "Confirmar",
         "confirm_totp_token": "Confirme suas alterações inserindo o token gerado",
         "delete_tfa": "Desativar o TFA",
         "disable_tfa": "Desative o TFA até o próximo login bem-sucedido",
@@ -1141,7 +1227,7 @@
         "reload_retry": "- (recarregue o navegador se o erro persistir)",
         "scan_qr_code": "Escaneie o código a seguir com seu aplicativo autenticador ou insira o código manualmente.",
         "select": "Por favor, selecione",
-        "set_tfa": "Defina o método de autenticação de dois fatores",
+        "set_tfa": "Método de autenticação de dois fatores",
         "start_webauthn_validation": "Iniciar validação",
         "tfa": "Autenticação de dois fatores",
         "tfa_token_invalid": "Token TFA inválido",
@@ -1318,7 +1404,11 @@
         "weeks": "semanas",
         "with_app_password": "com senha do aplicativo",
         "year": "ano",
-        "years": "anos"
+        "years": "anos",
+        "authentication": "Autenticação",
+        "overview": "Visão geral",
+        "protocols": "Protocolos",
+        "tfa_info": "A autenticação de dois fatores ajuda a proteger sua conta. Se você habilitá-la, precisará de senhas de aplicativo para fazer login em aplicativos ou serviços que não suportam autenticação de dois fatores (por exemplo, clientes de email)."
     },
     "warning": {
         "cannot_delete_self": "Não é possível excluir o usuário conectado",

+ 27 - 16
data/web/lang/lang.zh-cn.json

@@ -24,7 +24,7 @@
         "sogo_access": "允许管理 SOGo 访问权限",
         "sogo_profile_reset": "重置 SOGo 个人资料",
         "spam_alias": "临时别名",
-        "spam_policy": "黑名单/白名单",
+        "spam_policy": "阻止名单/允许名单",
         "spam_score": "垃圾邮件分数",
         "syncjobs": "同步任务",
         "tls_policy": "TLS 策略",
@@ -149,7 +149,7 @@
         "arrival_time": "到达时间 (服务器时间)",
         "authed_user": "已认证用户",
         "ays": "确定继续操作?",
-        "ban_list_info": "以下为被封禁的 IP 列表: <b>网络 (剩余封禁时间) - [操作]</b>。<br />被取消封禁的 IP 将会在几秒之内从封禁列表中移除<br />红色标签表示因名单而导致的永久封禁。",
+        "ban_list_info": "以下为被封禁的 IP 列表: <b>网络 (剩余封禁时间) - [操作]</b>。<br />被取消封禁的 IP 将会在几秒之内从封禁列表中移除<br />红色标签表示因阻止名单而导致的永久封禁。",
         "change_logo": "更改 Logo",
         "configuration": "配置",
         "convert_html_to_text": "将 HTML 转换为纯文本内容",
@@ -181,16 +181,16 @@
         "empty": "结果为空",
         "excludes": "除了",
         "f2b_ban_time": "封禁时间 (秒)",
-        "f2b_blacklist": "网络/主机名单",
+        "f2b_blacklist": "网络/主机阻止名单",
         "f2b_filter": "正则表达式过滤器",
-        "f2b_list_info": "黑名单的优先级总是高于白名单。 <b>列表更新将会在几秒之后完成。</b>",
+        "f2b_list_info": "阻止名单的优先级总是高于允许名单。 <b>列表更新将会在几秒之后完成。</b>",
         "f2b_max_attempts": "最多尝试次数",
         "f2b_netban_ipv4": "应用封禁的 IPv4 子网大小 (8-32)",
         "f2b_netban_ipv6": "应用封禁的 IPv6 子网大小 (8-128)",
         "f2b_parameters": "Fail2ban 参数",
         "f2b_regex_info": "将会过滤这些应用的日志: SOGo,Postfix,Dovecot 和 PHP-FPM。",
         "f2b_retry_window": "最多尝试次数重试窗口 (秒)",
-        "f2b_whitelist": "网络/主机名单",
+        "f2b_whitelist": "网络/主机允许名单",
         "filter_table": "筛选表格",
         "forwarding_hosts": "转发主机",
         "forwarding_hosts_add_hint": "你可以指定 IPv4/IPv6 地址、CIDR 表示的网络、主机名 (解析为 IP 地址),或者邮箱域名 (查询 SPF 记录或 MX 记录并解析为 IP 地址)。",
@@ -298,8 +298,8 @@
         "rspamd_com_settings": "设置名称将会自动生成,请看参考下方的示例预设。查看<a href=\"https://rspamd.com/doc/configuration/settings.html#settings-structure\" target=\"_blank\">Rspamd 文档</a>以了解更多的细节",
         "rspamd_global_filters": "全局过滤规则",
         "rspamd_global_filters_agree": "我会小心谨慎的!",
-        "rspamd_global_filters_info": "全局过滤规则包含了不同类型的全局黑名单和白名单。",
-        "rspamd_global_filters_regex": "它们的名字解释了它们的用途。所有内容必须包含 \"/pattern/options\" 格式的合法表达式 (例如 <code>/.+@domain\\.tld/i</code>)。<br>\r\n 因为仅对正则表达式执行了基本的检查,Rspamd 的功能仍可能因正则表达式语法问题出现错误。<br>\r\n  Rspamd 会在规则更改后读取其内容。 如果你遇到了问题,<a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">重启 Rspamd</a> 以强制重载规则。<br>名单中的项目会被系统排除。",
+        "rspamd_global_filters_info": "全局过滤规则包含了不同类型的全局阻止名单和允许名单。",
+        "rspamd_global_filters_regex": "它们的名字解释了它们的用途。所有内容必须包含 \"/pattern/options\" 格式的合法表达式 (例如 <code>/.+@domain\\.tld/i</code>)。<br>\n 因为仅对正则表达式执行了基本的检查,Rspamd 的功能仍可能因正则表达式语法问题出现错误。<br>\n  Rspamd 会在规则更改后读取其内容。 如果你遇到了问题,<a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">重启 Rspamd</a> 以强制重载规则。<br>阻止名单中的项目会被系统排除。",
         "rspamd_settings_map": "Rspamd 设置",
         "sal_level": "Moo 等级",
         "save": "保存更改",
@@ -409,7 +409,8 @@
         "force_sso": "强制要求单点登录(SSO)",
         "user_link": "自定义链接",
         "app_hide": "在登入界面隐藏",
-        "needs_restart": "需要重启"
+        "needs_restart": "需要重启",
+        "iam_use_tls_info": "如果使用了 TLS,必须使用 LDAP 服务器的默认端口(389)。SSL 的端口不能使用。"
     },
     "danger": {
         "access_denied": "访问被拒绝或者表单数据无效",
@@ -744,7 +745,16 @@
         "internal": "内部的",
         "internal_info": "内部的别名只能在域内部或者别名域内部访问。",
         "mta_sts": "邮件传输代理严格传输安全协议(MTA-STS)",
-        "mta_sts_version": "版本"
+        "mta_sts_version": "版本",
+        "mta_sts_info": "<a href='https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security' target='_blank'>MTA-STS</a> 是一项 MTA 标准,它强制要求 MTA 间传输必须使用 TLS 和有效的证书。<br>当因没有 DNSSEC 而无法使用 <a target='_blank' href='https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities'>DANE</a> 时,该标准将被启用。<br><b>注意</b>:若收件域支持基于 DNSSEC 的 DANE 协议,则系统将<b>始终</b>优先采用 DANE —— MTA-STS 仅作为备用机制存在。",
+        "mta_sts_version_info": "定义 MTA-STS 标准的版本——当前仅 <code>STSv1</code> 为有效版本。",
+        "mta_sts_mode": "模式",
+        "mta_sts_mode_info": "提供三种可选模式:<ul><li><em>测试模式</em>——仅监控策略执行情况,违反策略不会产生实际影响。</li><li><em>强制模式</em>——严格执行策略,拒绝所有未使用有效 TLS 加密的连接。</li><li><em>禁用模式</em>——发布策略但不生效。</li></ul>",
+        "mta_sts_max_age": "最长有效期",
+        "mta_sts_max_age_info": "接收方邮件服务器可缓存该策略的时长(秒),超出后需重新获取策略。",
+        "mta_sts_mx": "MX 服务器",
+        "mta_sts_mx_info": "仅允许向明确列出的邮件服务器发送邮件;发送方 MTA 会验证 DNS MX 记录的主机名是否与策略列表匹配,并仅允许携带有效 TLS 证书的投递(可防范中间人攻击)。",
+        "mta_sts_mx_notice": "可配置多个 MX 服务器(以逗号分隔)。"
     },
     "fido2": {
         "confirm": "确认",
@@ -814,7 +824,8 @@
         "login_linkstext": "不是正确的登陆页面?",
         "login_usertext": "以用户身份登陆",
         "login_domainadmintext": "以域管理员身份登陆",
-        "login_admintext": "以管理员身份登陆"
+        "login_admintext": "以管理员身份登陆",
+        "email": "邮箱地址"
     },
     "mailbox": {
         "action": "操作",
@@ -920,7 +931,7 @@
         "recipient_map_new": "新收件人",
         "recipient_map_new_info": "收件人映射的目标必须为合法的邮件地址或域名。",
         "recipient_map_old": "原收件人",
-        "recipient_map_old_info": "原收件人必须为合法的邮箱地址。",
+        "recipient_map_old_info": "原收件人必须为合法的邮箱地址或域名。",
         "recipient_maps": "收件人映射",
         "relay_all": "中继所有收件人",
         "remove": "删除",
@@ -1023,7 +1034,7 @@
         "notified": "已发送通知",
         "qhandler_success": "已成功向系统发送请求,现在你可以关闭这个窗口了。",
         "qid": "Rspamd 队列ID(QID)",
-        "qinfo": "隔离系统会把已被拒绝接收的邮件以及作为拷贝发送到垃圾箱的邮件保存到数据库中 (发件人<em>不</em>会知道)。\r\n  <br>\"学习为垃圾并删除\" 会根据贝叶斯定理将消息作为垃圾学习并计算其模糊特征以拒绝未来收到相似消息。\r\n  <br>请注意,这取决于你的系统资源,学习多个消息可能会花费较长时间。<br>名单中项目会被隔离系统排除。",
+        "qinfo": "隔离系统会把已被拒绝接收的邮件以及作为拷贝发送到垃圾箱的邮件保存到数据库中 (发件人<em>不</em>会知道)。\n  <br>\"学习为垃圾并删除\" 会根据贝叶斯定理将消息作为垃圾学习并计算其模糊特征以拒绝未来收到相似消息。\n  <br>请注意,这取决于你的系统资源,学习多个消息可能会花费较长时间。<br>阻止名单中项目会被隔离系统排除。",
         "qitem": "隔离项目",
         "quarantine": "隔离",
         "quick_actions": "操作",
@@ -1314,8 +1325,8 @@
         "spam_score_reset": "重置为服务器默认值",
         "spamfilter": "垃圾邮件过滤器",
         "spamfilter_behavior": "分数",
-        "spamfilter_bl": "名单",
-        "spamfilter_bl_desc": "名单中地址<b>总是会</b>被标记为垃圾邮件。被拒绝的邮件<b>不会</b>进入隔离区。此处可以使用通配符 \"*\"。此过滤器也会应用到直接别名 (只指向一个目标邮箱),但不会应用到\"接收所有\"别名和邮箱地址本身。",
+        "spamfilter_bl": "阻止名单",
+        "spamfilter_bl_desc": "阻止名单中地址<b>总是会</b>被标记为垃圾邮件。被拒绝的邮件<b>不会</b>进入隔离区。此处可以使用通配符 \"*\"。此过滤器也会应用到直接别名 (只指向一个目标邮箱),但不会应用到\"接收所有\"别名和邮箱地址本身。",
         "spamfilter_default_score": "默认值",
         "spamfilter_green": "绿色: 此消息不是垃圾邮件",
         "spamfilter_hint": "第一个值表示\"低垃圾邮件分数\",第二个值表示\"高垃圾邮件分数\"。",
@@ -1326,8 +1337,8 @@
         "spamfilter_table_empty": "数据为空",
         "spamfilter_table_remove": "删除",
         "spamfilter_table_rule": "规则",
-        "spamfilter_wl": "名单",
-        "spamfilter_wl_desc": "名单中地址<b>永远不会</b>被标记为垃圾邮件。此处可以使用通配符 \"*\"。此过滤器也会应用到直接别名 (只指向一个目标邮箱),但不会应用到\"接收所有\"别名和邮箱地址本身。",
+        "spamfilter_wl": "允许名单",
+        "spamfilter_wl_desc": "允许名单中地址<b>永远不会</b>被标记为垃圾邮件。此处可以使用通配符 \"*\"。此过滤器也会应用到直接别名 (只指向一个目标邮箱),但不会应用到\"接收所有\"别名和邮箱地址本身。",
         "spamfilter_yellow": "黄色: 此为垃圾邮件,会被标记为垃圾邮件并且移入垃圾邮件文件夹",
         "status": "状态",
         "sync_jobs": "同步任务",

+ 6 - 0
data/web/templates/modals/user.twig

@@ -326,6 +326,12 @@
               <small class="text-muted">{{ lang.user.password_reset_info }}</small>
             </div>
           </div>
+          <div class="row mb-4">
+            <label class="control-label col-sm-3" for="user_old_pass">{{ lang.user.password_now }}</label>
+            <div class="col-sm-9">
+              <input type="password" class="form-control" name="user_old_pass" autocomplete="off" required>
+            </div>
+          </div>
           <div class="row">
             <div class="offset-sm-3 col-sm-9">
               <button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="edit_selected" data-id="pw_recovery_change" data-item="null" data-api-url='edit/self' data-api-attr='{}' href="#">{{ lang.user.save }}</button>

+ 4 - 4
docker-compose.yml

@@ -42,7 +42,7 @@ services:
             - mysql
 
     redis-mailcow:
-      image: redis:7.4.2-alpine
+      image: redis:7.4.6-alpine
       entrypoint: ["/bin/sh","/redis-conf.sh"]
       volumes:
         - redis-vol-1:/data/
@@ -84,7 +84,7 @@ services:
             - clamd
 
     rspamd-mailcow:
-      image: ghcr.io/mailcow/rspamd:2.3
+      image: ghcr.io/mailcow/rspamd:2.4
       stop_grace_period: 30s
       depends_on:
         - dovecot-mailcow
@@ -117,7 +117,7 @@ services:
             - rspamd
 
     php-fpm-mailcow:
-      image: ghcr.io/mailcow/phpfpm:1.93
+      image: ghcr.io/mailcow/phpfpm:1.94
       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
       depends_on:
         - redis-mailcow
@@ -502,7 +502,7 @@ services:
             - acme
 
     netfilter-mailcow:
-      image: ghcr.io/mailcow/netfilter:1.62
+      image: ghcr.io/mailcow/netfilter:1.63
       stop_grace_period: 30s
       restart: always
       privileged: true