diff --git a/data/Dockerfiles/postfix-tlspol/Dockerfile b/data/Dockerfiles/postfix-tlspol/Dockerfile index 68f6ecced..fa363845f 100644 --- a/data/Dockerfiles/postfix-tlspol/Dockerfile +++ b/data/Dockerfiles/postfix-tlspol/Dockerfile @@ -1,17 +1,17 @@ -FROM golang:1.25-bookworm AS builder +FROM golang:1.26-trixie AS builder WORKDIR /src ENV CGO_ENABLED=0 \ GO111MODULE=on \ NOOPT=1 \ - VERSION=1.8.22 + VERSION=1.11.0 RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \ cd /src/postfix-tlspol && \ scripts/build.sh build-only -FROM debian:bookworm-slim +FROM debian:trixie-slim LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive diff --git a/data/Dockerfiles/postfix-tlspol/syslog-ng-redis_slave.conf b/data/Dockerfiles/postfix-tlspol/syslog-ng-redis_slave.conf index 3862a3547..022dfc3da 100644 --- a/data/Dockerfiles/postfix-tlspol/syslog-ng-redis_slave.conf +++ b/data/Dockerfiles/postfix-tlspol/syslog-ng-redis_slave.conf @@ -1,4 +1,4 @@ -@version: 3.38 +@version: 4.8 @include "scl.conf" options { chain_hostnames(off); @@ -7,7 +7,7 @@ options { dns_cache(no); use_fqdn(no); owner("root"); group("adm"); perm(0640); - stats_freq(0); + stats(freq(0)); bad_hostname("^gconfd$"); }; source s_src { diff --git a/data/Dockerfiles/postfix-tlspol/syslog-ng.conf b/data/Dockerfiles/postfix-tlspol/syslog-ng.conf index 7126c1250..70c92b5c0 100644 --- a/data/Dockerfiles/postfix-tlspol/syslog-ng.conf +++ b/data/Dockerfiles/postfix-tlspol/syslog-ng.conf @@ -1,4 +1,4 @@ -@version: 3.38 +@version: 4.8 @include "scl.conf" options { chain_hostnames(off); @@ -7,7 +7,7 @@ options { dns_cache(no); use_fqdn(no); owner("root"); group("adm"); perm(0640); - stats_freq(0); + stats(freq(0)); bad_hostname("^gconfd$"); }; source s_src { diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index f65ec7ebd..2b22be27d 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -1,6 +1,6 @@ # SOGo built from source to enable security patch application # Repository: https://github.com/Alinto/sogo -# Version: SOGo-5.12.9 +# Version: SOGo-5.12.11 # # Applied security patches: # - @@ -12,8 +12,8 @@ FROM debian:bookworm LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ARG SOGO_VERSION=SOGo-5.12.10 -ARG SOPE_VERSION=SOPE-5.12.10 +ARG SOGO_VERSION=SOGo-5.12.11 +ARG SOPE_VERSION=SOPE-5.12.11 # Security patches to apply (space-separated commit hashes) ARG SOGO_SECURITY_PATCHES="" # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?.*)$ diff --git a/data/Dockerfiles/unbound/Dockerfile b/data/Dockerfiles/unbound/Dockerfile index b1ec99e4f..321158e07 100644 --- a/data/Dockerfiles/unbound/Dockerfile +++ b/data/Dockerfiles/unbound/Dockerfile @@ -3,7 +3,8 @@ FROM alpine:3.23 LABEL maintainer = "The Infrastructure Company GmbH " # install unbound from alpine:edge to get security patches -RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main unbound +# minimum version is pinned +RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main "unbound>=1.26.1-r0" # install other packages from regular alpine stable repo RUN apk add --update --no-cache \ diff --git a/data/conf/dovecot/auth/passwd-verify.lua b/data/conf/dovecot/auth/passwd-verify.lua index ea847932d..a22488ea8 100644 --- a/data/conf/dovecot/auth/passwd-verify.lua +++ b/data/conf/dovecot/auth/passwd-verify.lua @@ -32,8 +32,17 @@ function auth_password_verify(request, password) -- Returning PASSDB_RESULT_PASSWORD_MISMATCH will reset the user's auth cache entry. -- Returning PASSDB_RESULT_INTERNAL_FAILURE keeps the existing cache entry, -- even if the TTL has expired. Useful to avoid cache eviction during backend issues. + + -- On a network-level failure (nginx unreachable, DNS failure, timeout) https.request + -- returns nil plus an error string, so c is not a numeric HTTP status code. Treat this + -- as a backend outage and keep the cache entry, rather than wiping it as a mismatch. + if type(c) ~= "number" then + dovecot.i_info("HTTP request to auth backend failed with " .. tostring(c) .. " for user " .. request.user) + return dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE, "Upstream unreachable" + end + if c ~= 200 and c ~= 401 then - dovecot.i_info("HTTP request failed with " .. c .. " for user " .. request.user) + dovecot.i_info("HTTP request failed with " .. tostring(c) .. " for user " .. request.user) return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error" end diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index 080be499a..4329f5487 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -150,7 +150,7 @@ smtp_sasl_auth_enable = yes smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf smtp_sasl_security_options = smtp_sasl_mechanism_filter = plain, login -smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf socketmap:inet:postfix-tlspol:8642:QUERY +smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf socketmap:inet:postfix-tlspol:8642:QUERYwithTLSRPT smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre mail_name = Postcow # local_transport map catches local destinations and prevents routing local dests when the next map would route "*" diff --git a/data/conf/postfix/postscreen_access.cidr b/data/conf/postfix/postscreen_access.cidr index 3801fe31e..aa33dd3d8 100644 --- a/data/conf/postfix/postscreen_access.cidr +++ b/data/conf/postfix/postscreen_access.cidr @@ -1,6 +1,6 @@ -# Whitelist generated by Postwhite v3.4 on Sat Aug 1 00:35:20 UTC 2026 +# Whitelist generated by Postwhite v3.4 on Tue Sep 1 00:34:07 UTC 2026 # https://github.com/stevejenkins/postwhite/ -# 2256 total rules +# 2243 total rules 2a00:1450:4000::/36 permit 2a00:1450:4864::/56 permit 2a01:111:f400::/48 permit @@ -125,7 +125,6 @@ 23.249.208.0/20 permit 23.251.224.0/19 permit 23.253.141.0/24 permit -23.253.182.0/23 permit 23.253.182.103 permit 23.253.183.145 permit 23.253.183.146 permit @@ -147,24 +146,7 @@ 27.123.206.80/28 permit 31.47.251.17 permit 31.186.239.0/24 permit -34.2.64.0/22 permit -34.2.68.0/23 permit -34.2.70.0/23 permit -34.2.71.64/26 permit -34.2.72.0/22 permit -34.2.75.0/26 permit -34.2.78.0/23 permit -34.2.80.0/23 permit -34.2.82.0/23 permit -34.2.84.0/24 permit -34.2.84.64/26 permit -34.2.85.0/24 permit -34.2.85.64/26 permit -34.2.86.0/23 permit -34.2.88.0/23 permit -34.2.90.0/23 permit -34.2.92.0/23 permit -34.2.94.0/23 permit +34.2.64.0/19 permit 34.70.158.162 permit 34.74.74.140 permit 34.83.159.189 permit @@ -191,6 +173,7 @@ 35.205.92.9 permit 35.228.216.85 permit 35.242.169.159 permit +37.187.220.204 permit 37.188.97.188 permit 37.218.248.47 permit 37.218.249.47 permit @@ -264,8 +247,8 @@ 46.228.39.112/31 permit 46.243.88.174 permit 46.243.88.175 permit -46.243.88.176 permit -46.243.88.177 permit +46.243.88.176/30 permit +46.243.95.96/27 permit 46.243.95.179 permit 46.243.95.180 permit 50.16.246.183 permit @@ -283,6 +266,7 @@ 50.56.130.221 permit 50.56.130.222 permit 50.112.246.219 permit +50.152.146.25 permit 51.83.17.38 permit 52.1.14.157 permit 52.5.230.59 permit @@ -654,6 +638,7 @@ 79.135.106.0/24 permit 79.135.107.0/24 permit 80.225.160.128/25 permit +81.88.38.0/23 permit 81.169.146.243 permit 81.169.146.245 permit 81.169.146.246 permit @@ -738,6 +723,7 @@ 96.43.148.64/28 permit 96.43.148.64/31 permit 96.43.151.64/28 permit +98.77.0.0/16 permit 98.97.248.0/21 permit 98.136.44.181 permit 98.136.44.182/31 permit @@ -1233,8 +1219,6 @@ 104.43.243.237 permit 104.44.112.128/25 permit 104.47.0.0/17 permit -104.130.96.0/28 permit -104.130.122.0/23 permit 106.10.144.64/27 permit 106.10.144.100/31 permit 106.10.144.103 permit @@ -1456,6 +1440,7 @@ 130.35.116.0/25 permit 130.61.9.72 permit 130.162.39.83 permit +130.162.194.23 permit 130.248.172.0/24 permit 130.248.173.0/24 permit 131.186.12.0/25 permit @@ -1539,9 +1524,6 @@ 146.20.14.105 permit 146.20.14.106 permit 146.20.14.107 permit -146.20.112.0/26 permit -146.20.113.0/24 permit -146.20.191.0/24 permit 146.20.215.0/24 permit 146.20.215.182 permit 146.88.28.0/24 permit @@ -1564,10 +1546,11 @@ 149.97.173.180 permit 149.118.160.128/25 permit 150.136.21.199 permit -150.171.109.72 permit +150.171.109.183 permit 150.230.98.160 permit 151.145.38.14 permit 152.67.105.195 permit +152.69.165.251 permit 152.69.200.236 permit 152.70.155.126 permit 155.248.135.128/25 permit @@ -1589,6 +1572,7 @@ 158.247.16.0/20 permit 158.247.100.0/25 permit 159.13.4.0/25 permit +159.13.33.181 permit 159.92.154.0/24 permit 159.92.155.0/24 permit 159.92.157.0/24 permit @@ -1616,6 +1600,8 @@ 159.183.121.182 permit 159.183.129.172 permit 160.1.62.192 permit +161.33.94.110 permit +161.33.228.244 permit 161.38.192.0/20 permit 161.38.204.0/22 permit 161.71.32.0/19 permit @@ -1647,7 +1633,6 @@ 165.173.182.0/24 permit 165.173.182.250/31 permit 165.173.189.205 permit -166.78.68.0/22 permit 166.78.68.221 permit 166.78.69.169 permit 166.78.69.170 permit @@ -1802,9 +1787,9 @@ 188.125.85.234/31 permit 188.125.85.236/31 permit 188.125.85.238 permit -188.165.51.139 permit 188.172.128.0/20 permit 192.0.64.0/18 permit +192.9.166.184 permit 192.18.139.154 permit 192.18.145.36 permit 192.18.152.58 permit @@ -1829,7 +1814,6 @@ 192.30.252.0/22 permit 192.161.144.0/20 permit 192.162.87.0/24 permit -192.237.158.0/23 permit 192.237.159.42 permit 192.237.159.43 permit 192.254.112.0/20 permit @@ -1872,7 +1856,6 @@ 198.21.0.0/21 permit 198.37.144.0/20 permit 198.37.152.186 permit -198.61.254.0/23 permit 198.61.254.21 permit 198.61.254.231 permit 198.178.234.57 permit @@ -1959,6 +1942,7 @@ 204.14.232.0/21 permit 204.14.232.64/28 permit 204.14.234.64/28 permit +204.75.18.128/27 permit 204.75.142.0/24 permit 204.92.114.187 permit 204.92.114.203 permit @@ -1966,7 +1950,9 @@ 204.141.32.0/23 permit 204.141.42.0/23 permit 204.216.164.202 permit -204.220.160.0/21 permit +204.220.90.0/23 permit +204.220.92.0/22 permit +204.220.160.0/20 permit 204.220.168.0/21 permit 204.220.176.0/20 permit 204.220.181.105 permit @@ -2002,6 +1988,8 @@ 207.211.31.0/25 permit 207.211.41.113 permit 207.211.132.0/25 permit +207.211.147.87 permit +207.211.159.110 permit 207.218.90.0/24 permit 207.218.90.122 permit 207.250.68.0/24 permit @@ -2051,7 +2039,6 @@ 209.43.22.0/28 permit 209.46.117.168 permit 209.46.117.179 permit -209.61.151.0/24 permit 209.61.151.236 permit 209.61.151.249 permit 209.61.151.251 permit @@ -2238,7 +2225,7 @@ 2001:748:400:3301::4 permit 2404:6800:4000::/36 permit 2404:6800:4864::/56 permit -2603:1061:14:75::1 permit +2603:1061:14:102::1 permit 2607:13c0:0001:0000:0000:0000:0000:7000/116 permit 2607:13c0:0002:0000:0000:0000:0000:1000/116 permit 2607:13c0:0004:0000:0000:0000:0000:0000/116 permit diff --git a/data/web/inc/ajax/dns_diagnostics.php b/data/web/inc/ajax/dns_diagnostics.php index 7ed92db77..5efbaa472 100644 --- a/data/web/inc/ajax/dns_diagnostics.php +++ b/data/web/inc/ajax/dns_diagnostics.php @@ -442,6 +442,16 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm unset($record); + // Make a hostname RHS absolute so it is not read relative to $ORIGIN. + // Already-absolute names, the SRV root target "." and IP addresses are left alone. + $absolutize = function($host) { + $host = trim($host); + if ($host === '' || $host === '.' || substr($host, -1) === '.' || filter_var($host, FILTER_VALIDATE_IP)) { + return $host; + } + return $host . '.'; + }; + $dns_data = sprintf("\$ORIGIN %s.\n", $domain); foreach ($records as $record) { if ($domain == substr($record[0], -strlen($domain))) { @@ -462,16 +472,26 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm $val = str_replace(state_optional, '', $val); $val = str_replace(state_good, '', $val); if (strlen($val) > 0) { + // these are all TXT values, their RHS is a character string, not a name $vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val); } } } else { + if ($record[1] == 'MX' || $record[1] == 'CNAME') { + $val = $absolutize($val); + } + elseif ($record[1] == 'SRV') { + // format here is "target port"; only the target is a name + $parts = explode(' ', $val, 2); + $parts[0] = $absolutize($parts[0]); + $val = implode(' ', $parts); + } $vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val); } foreach ($vals as $val) { - $dns_data .= str_replace($domain, $domain . '.', $val); + $dns_data .= $val; } } } diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 90c830b69..09e8ec193 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -633,6 +633,18 @@ function logger($_data = false) { return true; } } +function is_local_mailcow_domain($domain) { + // True if domain is a locally managed, active primary or alias domain + global $pdo; + $domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46); + if (empty($domain)) { + return false; + } + $stmt = $pdo->prepare("SELECT 1 FROM `domain` WHERE `domain` = :d AND `active` = 1 + UNION SELECT 1 FROM `alias_domain` WHERE `alias_domain` = :d2 AND `active` = 1 LIMIT 1"); + $stmt->execute(array(':d' => $domain, ':d2' => $domain)); + return (bool)$stmt->fetchColumn(); +} function hasDomainAccess($username, $role, $domain) { global $pdo; if (empty($domain) || !is_valid_domain_name($domain)) { @@ -3551,7 +3563,16 @@ function protect_route($allowed_roles = ['admin', 'domainadmin', 'user'], $redir if (isset($redirects['unauthenticated'])) { header('Location: ' . $redirects['unauthenticated']); } else { - header('Location: /'); + // Send a deep link to the login page for its area instead of the user login at /, + // e.g. /admin/dashboard -> /admin rather than / + $request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; + if (strpos($request_uri, '/admin/') === 0) { + header('Location: /admin'); + } elseif (strpos($request_uri, '/domainadmin/') === 0) { + header('Location: /domainadmin'); + } else { + header('Location: /'); + } } exit(); } diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index acbbb94d4..37ccbd85c 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -750,6 +750,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $goto_domain = idn_to_ascii(substr(strstr($goto, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46); $goto_local_part = strstr($goto, '@', true); $goto = $goto_local_part.'@'.$goto_domain; + // Deny external goto domains: global switch (all roles) overrides the per-DA ACL + if (($GLOBALS['ALIAS_DISABLE_EXTERNAL_DOMAINS'] === true || + (isset($_SESSION['acl']['alias_external_goto']) && $_SESSION['acl']['alias_external_goto'] != "1")) && + !is_local_mailcow_domain($goto_domain)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('external_goto_denied', htmlspecialchars($goto)) + ); + unset($gotos[$i]); + continue; + } $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` REGEXP 'location|thing|group' AND `username`= :goto"); @@ -2715,6 +2727,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { unset($gotos[$i]); continue; } + // Deny external goto domains: global switch (all roles) overrides the per-DA ACL + $goto_domain = idn_to_ascii(substr(strstr($goto, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46); + if (($GLOBALS['ALIAS_DISABLE_EXTERNAL_DOMAINS'] === true || + (isset($_SESSION['acl']['alias_external_goto']) && $_SESSION['acl']['alias_external_goto'] != "1")) && + !is_local_mailcow_domain($goto_domain)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('external_goto_denied', htmlspecialchars($goto)) + ); + unset($gotos[$i]); + continue; + } if ($goto == $address) { $_SESSION['return'][] = array( 'type' => 'danger', diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 23aa5e77f..f6b0cd792 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -4,7 +4,7 @@ function init_db_schema() try { global $pdo; - $db_version = "19022026_1220"; + $db_version = "18082026_1200"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -719,7 +719,8 @@ function init_db_schema() "alias_domains" => "TINYINT(1) NOT NULL DEFAULT '0'", "mailbox_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'", "domain_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'", - "domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'" + "domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'", + "alias_external_goto" => "TINYINT(1) NOT NULL DEFAULT '1'" ), "keys" => array( "primary" => array( diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index cec402a96..2265c5e1c 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -246,6 +246,10 @@ $PW_RESET_TOKEN_LIMIT = 3; // Maximum time in minutes a password reset token is valid $PW_RESET_TOKEN_LIFETIME = 15; +// Globally forbid aliases with external (non-local) goto domains for ALL roles (incl. admins), +// overriding the per-domain-admin da_acl. false = defer to da_acl. +$ALIAS_DISABLE_EXTERNAL_DOMAINS = false; + // UV flag handling in FIDO2/WebAuthn - defaults to false to allow iOS logins // true = required // false = preferred diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 28ad0d72b..938f9b929 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -1,6 +1,7 @@ { "acl": { "alias_domains": "Alias-Domains hinzufügen", + "alias_external_goto": "Aliase mit externen Ziel-Domains erlauben", "app_passwds": "App-Passwörter verwalten", "bcc_maps": "BCC-Maps", "delimiter_action": "Delimiter-Aktionen (tags)", @@ -443,6 +444,7 @@ "domain_not_found": "Domain %s nicht gefunden", "domain_quota_m_in_use": "Domain-Speicherplatzlimit muss größer oder gleich %d MiB sein", "extended_sender_acl_denied": "Keine Rechte zum Setzen von externen Absenderadressen", + "external_goto_denied": "Externe Ziel-Adresse %s ist nicht erlaubt", "extra_acl_invalid": "Externe Absenderadresse \"%s\" ist ungültig", "extra_acl_invalid_domain": "Externe Absenderadresse \"%s\" verwendet eine ungültige Domain", "fido2_verification_failed": "FIDO2-Verifizierung fehlgeschlagen: %s", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index b8a11e5a5..c958e68eb 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -1,6 +1,7 @@ { "acl": { "alias_domains": "Add alias domains", + "alias_external_goto": "Allow aliases with external goto domains", "app_passwds": "Manage app passwords", "bcc_maps": "BCC maps", "delimiter_action": "Delimiter action", @@ -444,6 +445,7 @@ "domain_not_found": "Domain %s not found", "domain_quota_m_in_use": "Domain quota must be greater or equal to %s MiB", "extended_sender_acl_denied": "missing ACL to set external sender addresses", + "external_goto_denied": "External goto address %s is not allowed", "extra_acl_invalid": "External sender address \"%s\" is invalid", "extra_acl_invalid_domain": "External sender \"%s\" uses an invalid domain", "fido2_verification_failed": "FIDO2 verification failed: %s", diff --git a/data/web/lang/lang.it-it.json b/data/web/lang/lang.it-it.json index a2f0a87e3..de228b2d7 100644 --- a/data/web/lang/lang.it-it.json +++ b/data/web/lang/lang.it-it.json @@ -109,7 +109,10 @@ "bcc_dest_format": "Il destinatario in copia nascosta deve essere un singolo indirizzo email.
Se si vuole spedire una copia del messaggio a più destinatari, bisogna creare un alias ed utilizzarlo per questa opzione.", "app_passwd_protocols": "Protocolli consentiti per la password dell'app", "tags": "Tag", - "dry": "Simula sincronizzazione" + "dry": "Simula sincronizzazione", + "internal": "Interno", + "internal_info": "Gli alias interni sono accessibili solo dal proprio dominio o dai domini alias.", + "sender_allowed": "Consenti l'invio con questo alias" }, "admin": { "access": "Accedi", @@ -131,7 +134,7 @@ "additional_rows": " righe aggiuntive inserite", "admin": "Amministratore", "admin_details": "Modifica impostazioni amministratore", - "admin_domains": "Assengazioni di dominio", + "admin_domains": "Assegnazioni di dominio", "admins": "Amministratori", "admins_ldap": "Amministratori LDAP", "advanced_settings": "Impostazioni avanzate", diff --git a/data/web/lang/lang.si-si.json b/data/web/lang/lang.si-si.json index 2deb82712..368c0cd12 100644 --- a/data/web/lang/lang.si-si.json +++ b/data/web/lang/lang.si-si.json @@ -29,7 +29,8 @@ "extend_sender_acl": "Dovoli razširitev pošiljateljevega ACL z zunanjimi e-poštnimi naslovi", "quarantine_category": "Spremeni kategorijo obvestil o karanteni", "syncjobs": "Sinhronizacijska opravila", - "pw_reset": "Dovoli ponastavitev uporabniškega gesla mailcow" + "pw_reset": "Dovoli ponastavitev uporabniškega gesla mailcow", + "alias_external_goto": "Dovoli vzdevke z zunanjimi domenami goto" }, "add": { "active": "Aktivno", @@ -559,7 +560,8 @@ "mx_invalid": "Zapis MX %s je neveljaven", "version_invalid": "Različica %s je neveljavna", "tfa_removal_blocked": "Dvofaktorske avtentikacije ni mogoče odstraniti, ker je obvezna za vaš račun.", - "quarantine_category_invalid": "Kategorija karantene mora biti ena od: add_header, reject, all" + "quarantine_category_invalid": "Kategorija karantene mora biti ena od: add_header, reject, all", + "external_goto_denied": "Zunanji naslov goto %s ni dovoljen" }, "debug": { "containers_info": "Informacije o zabojniku", @@ -785,7 +787,8 @@ "internal": "Notranje", "internal_info": "Notranji vzdevki so dostopni samo iz lastne domene ali vzdevkov domen.", "sender_allowed": "Dovoli pošiljanje kot ta vzdevek", - "sender_allowed_info": "Če je onemogočeno, lahko ta vzdevek samo prejema pošto. Za preglasitev in dodelitev dovoljenja za pošiljanje določenim poštnim predalom uporabite seznam za nadzor dostopa pošiljatelja." + "sender_allowed_info": "Če je onemogočeno, lahko ta vzdevek samo prejema pošto. Za preglasitev in dodelitev dovoljenja za pošiljanje določenim poštnim predalom uporabite seznam za nadzor dostopa pošiljatelja.", + "mta_sts_active_info": "Če ni označeno, pravilnik MTA-STS ne bo objavljen in za poddomeno mta-sts ne bo zahtevano nobeno potrdilo prek ACME." }, "footer": { "restart_container_info": "Pomembno: Eleganten ponovni zagon lahko traja nekaj časa, zato počakajte, da se konča.", diff --git a/data/web/lang/lang.zh-cn.json b/data/web/lang/lang.zh-cn.json index fe225a5df..2a1a080b9 100644 --- a/data/web/lang/lang.zh-cn.json +++ b/data/web/lang/lang.zh-cn.json @@ -29,7 +29,8 @@ "syncjobs": "同步任务", "tls_policy": "TLS 策略", "unlimited_quota": "无限邮箱容量配额", - "pw_reset": "允许重置mailcow使用者密码" + "pw_reset": "允许重置mailcow使用者密码", + "alias_external_goto": "允许带有外部目标地址的别名" }, "add": { "activate_filter_warn": "当“启用”选项被勾选后,其它所有的过滤器都会被禁用。", @@ -111,7 +112,8 @@ "validation_success": "验证成功", "dry": "模拟同步(Dry run)", "internal_info": "内部的别名只能在域内部或者别名域内部访问。", - "internal": "内部的" + "internal": "内部的", + "sender_allowed": "允许以该别名发送" }, "admin": { "access": "权限管理", @@ -555,7 +557,10 @@ "max_age_invalid": "最大有效时间 %s 无效", "mode_invalid": "模式 %s 无效", "mx_invalid": "MX 记录 %s 无效", - "version_invalid": "版本 %s 无效" + "version_invalid": "版本 %s 无效", + "tfa_removal_blocked": "无法移除两步验证,你的账户必须使用两步验证。", + "external_goto_denied": "不允许使用外部目标地址 %s", + "quarantine_category_invalid": "隔离分类必须是 add_header, reject, all 的其中一个" }, "debug": { "chart_this_server": "图表 (此服务器)", @@ -754,7 +759,10 @@ "mta_sts_max_age_info": "接收方邮件服务器可缓存该策略的时长(秒),超出后需重新获取策略。", "mta_sts_mx": "MX 服务器", "mta_sts_mx_info": "仅允许向明确列出的邮件服务器发送邮件;发送方 MTA 会验证 DNS MX 记录的主机名是否与策略列表匹配,并仅允许携带有效 TLS 证书的投递(可防范中间人攻击)。", - "mta_sts_mx_notice": "可配置多个 MX 服务器(以逗号分隔)。" + "mta_sts_mx_notice": "可配置多个 MX 服务器(以逗号分隔)。", + "sender_allowed": "允许以此别名发送", + "sender_allowed_info": "如果禁用了,此别名只能接收邮件。使用发件者 ACL 来覆盖此选项并赋予指定的邮箱发送权限。", + "mta_sts_active_info": "如果未选中,MTA-STS 策略不会被公布并且不会通过 ACME 为 MTA-STS 子域名请求证书。" }, "fido2": { "confirm": "确认", @@ -1002,7 +1010,8 @@ "templates": "模板", "template": "模板", "iam": "身份提供者(IDP)", - "internal": "内部的" + "internal": "内部的", + "force_tfa": "两步验证" }, "oauth2": { "access_denied": "请作为邮箱所有者登录以使用 OAuth2 授权。", @@ -1210,7 +1219,12 @@ "waiting_usb_auth": "等待 USB 设备中...

现在请触碰你的 WebAuthn USB 设备上的按钮。", "waiting_usb_register": "等待 USB 设备中...

请在上方输入你的密码并请触碰你的 WebAuthn USB 设备上的按钮以确认注册该 WebAuthn 设备。", "yubi_otp": "Yubico OTP 认证", - "authenticators": "验证器(Authenticators)" + "authenticators": "验证器(Authenticators)", + "force_tfa": "在登录时强制启用 2FA", + "force_tfa_info": "在访问面板前用户会被要求设置两步验证。", + "setup_title": "需要设置两步验证", + "setup_required": "你的账户需要设置两步验证。请添加一个两步验证方法以继续。", + "cancel_setup": "取消并注销" }, "user": { "action": "操作", @@ -1384,7 +1398,8 @@ "overview": "概览", "expire_never": "永不过期", "forever": "永久", - "spam_aliases_info": "垃圾邮件别名是一种临时电子邮件地址,可用于保护真实电子邮件地址。
还可以选择设置过期时间,以便在设定的时间后自动停用别名,从而有效地销毁被滥用或泄露的地址。" + "spam_aliases_info": "垃圾邮件别名是一种临时电子邮件地址,可用于保护真实电子邮件地址。
还可以选择设置过期时间,以便在设定的时间后自动停用别名,从而有效地销毁被滥用或泄露的地址。", + "pw_update_required": "你的账户需要修改一次密码。请设置一个新密码以继续。" }, "warning": { "cannot_delete_self": "不能删除已登录的用户", diff --git a/data/web/templates/admin_index.twig b/data/web/templates/admin_index.twig index 0dcb9145e..44b3564cc 100644 --- a/data/web/templates/admin_index.twig +++ b/data/web/templates/admin_index.twig @@ -49,6 +49,7 @@
+
diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index 9e20b08a5..e0f3b5b35 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -473,6 +473,10 @@ function recursiveBase64StrToArrayBuffer(obj) { window.location.reload(); } else { var msg = (data && data[0] && data[0].msg) ? data[0].msg : 'Password change failed.'; + // the API returns raw language keys, resolve them like the alert box does + if (Object.prototype.hasOwnProperty.call(lang_danger, msg)) { + msg = lang_danger[msg]; + } $('#changePWAlert').show().text(msg); } }, diff --git a/data/web/templates/domainadmin_index.twig b/data/web/templates/domainadmin_index.twig index 2fc7bad98..af667756a 100644 --- a/data/web/templates/domainadmin_index.twig +++ b/data/web/templates/domainadmin_index.twig @@ -49,6 +49,7 @@
+
diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig index 34e869ce1..db258fe49 100644 --- a/data/web/templates/edit/mailbox.twig +++ b/data/web/templates/edit/mailbox.twig @@ -267,6 +267,7 @@ {{ lang.admin.password_reset_info }}
+ {% endif %}
@@ -279,7 +280,6 @@ {% endif %}
- {% endif %}
diff --git a/data/web/templates/user_index.twig b/data/web/templates/user_index.twig index 28eb6389c..a06cdb3f6 100644 --- a/data/web/templates/user_index.twig +++ b/data/web/templates/user_index.twig @@ -56,6 +56,7 @@
+
diff --git a/docker-compose.yml b/docker-compose.yml index 7dce45691..75c6c46cb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: unbound-mailcow: - image: ghcr.io/mailcow/unbound:1.25.1-1 + image: ghcr.io/mailcow/unbound:1.26.1-1 environment: - TZ=${TZ} - SKIP_UNBOUND_HEALTHCHECK=${SKIP_UNBOUND_HEALTHCHECK:-n} @@ -42,7 +42,7 @@ services: - mysql redis-mailcow: - image: redis:7.4.10-alpine + image: redis:7.4.11-alpine entrypoint: ["/bin/sh","/redis-conf.sh"] volumes: - redis-vol-1:/data/ @@ -200,7 +200,7 @@ services: - phpfpm sogo-mailcow: - image: ghcr.io/mailcow/sogo:5.12.10-1 + image: ghcr.io/mailcow/sogo:5.12.11-1 environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} @@ -382,7 +382,7 @@ services: - postfix postfix-tlspol-mailcow: - image: ghcr.io/mailcow/postfix-tlspol:1.8.23 + image: ghcr.io/mailcow/postfix-tlspol:1.11.0 depends_on: unbound-mailcow: condition: service_healthy