From 08278a8be9f8e44357516250a8a5993458c737f5 Mon Sep 17 00:00:00 2001 From: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:51:05 +0200 Subject: [PATCH 01/68] Update RSPAMD version to 4.0.0 in Dockerfile --- data/Dockerfiles/rspamd/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index d0c710428..aed261ab1 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -2,7 +2,7 @@ FROM debian:trixie-slim LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ARG RSPAMD_VER=rspamd_3.14.3-1~236eb65 +ARG RSPAMD_VER=rspamd_4.0.0-1~4a2ab73 ARG CODENAME=trixie ENV LC_ALL=C From 694d7519155cdc9365fc1c886b6eab8e2e537197 Mon Sep 17 00:00:00 2001 From: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:28:14 +0200 Subject: [PATCH 02/68] Update RSPAMD version in Dockerfile --- data/Dockerfiles/rspamd/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index aed261ab1..4a6666136 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -2,7 +2,7 @@ FROM debian:trixie-slim LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ARG RSPAMD_VER=rspamd_4.0.0-1~4a2ab73 +ARG RSPAMD_VER=rspamd_4.0.-1~8700333 ARG CODENAME=trixie ENV LC_ALL=C From b0873edb6a49d3ff0ffd2043d3ce3b8b9e856ac8 Mon Sep 17 00:00:00 2001 From: Dmitriy Alekseev <1865999+dragoangel@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:28:32 +0200 Subject: [PATCH 03/68] Update RSPAMD version in Dockerfile --- data/Dockerfiles/rspamd/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 4a6666136..ab9aaa5ad 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -2,7 +2,7 @@ FROM debian:trixie-slim LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ARG RSPAMD_VER=rspamd_4.0.-1~8700333 +ARG RSPAMD_VER=rspamd_4.0.1-1~8700333 ARG CODENAME=trixie ENV LC_ALL=C From 8ead057ce9f6015327ce4eb8f51fef41a1292190 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Wed, 15 Apr 2026 09:33:23 +0200 Subject: [PATCH 04/68] [Web] Create default mailbox template with eas and dav access --- data/web/inc/init_db.inc.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 72018a6bc..23aa5e77f 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -1464,6 +1464,8 @@ function init_db_schema() "pop3_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['pop3_access']), "smtp_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['smtp_access']), "sieve_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sieve_access']), + "eas_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['eas_access']), + "dav_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['dav_access']), "acl_spam_alias" => 1, "acl_tls_policy" => 1, "acl_spam_score" => 1, From 104af5862889dcadfd51c354d55dfbdeea47ace6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 May 2026 18:33:03 +0000 Subject: [PATCH 05/68] Update dependency php/pecl-mail-mailparse to v3.2.0 Signed-off-by: milkmaker --- data/Dockerfiles/phpfpm/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index 7917d7f7d..a23a0425a 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -7,7 +7,7 @@ ARG APCU_PECL_VERSION=5.1.28 # renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?.*)$ ARG IMAGICK_PECL_VERSION=3.8.1 # renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?.*)$ -ARG MAILPARSE_PECL_VERSION=3.1.9 +ARG MAILPARSE_PECL_VERSION=3.2.0 # renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?.*)$ ARG MEMCACHED_PECL_VERSION=3.4.0 # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?.*)$ From ffbc37a00c0245a5ad6aa87d8aa34f677da698c5 Mon Sep 17 00:00:00 2001 From: Michael Kuron <1748330+mkuron@users.noreply.github.com> Date: Sat, 2 May 2026 16:24:01 +0200 Subject: [PATCH 06/68] Escape generated password in mobileconfig Escape ampersand, less than, greater than to avoid generating invalid XML. Fixes #7171 --- data/web/mobileconfig.php | 1 + 1 file changed, 1 insertion(+) diff --git a/data/web/mobileconfig.php b/data/web/mobileconfig.php index 7c0ead7f5..c57c48857 100644 --- a/data/web/mobileconfig.php +++ b/data/web/mobileconfig.php @@ -65,6 +65,7 @@ if (isset($_GET['app_password'])) { $attr['protocols'][] = 'dav_access'; } app_passwd("add", $attr); + $password = htmlspecialchars($password, ENT_NOQUOTES); } else { $app_password = false; } From 23e4e4f3739549d43a8f7ea3ae1bc03874c775aa Mon Sep 17 00:00:00 2001 From: goodygh <101992094+goodygh@users.noreply.github.com> Date: Fri, 29 May 2026 01:39:38 +0200 Subject: [PATCH 07/68] [SOGo] Update to 5.12.9 --- data/Dockerfiles/sogo/Dockerfile | 6 +++--- docker-compose.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 1c7287200..b4b401e8c 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.8 +# Version: SOGo-5.12.9 # # 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.8 -ARG SOPE_VERSION=SOPE-5.12.8 +ARG SOGO_VERSION=SOGo-5.12.9 +ARG SOPE_VERSION=SOPE-5.12.9 # 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/docker-compose.yml b/docker-compose.yml index 615e19f57..4da149d7c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -200,7 +200,7 @@ services: - phpfpm sogo-mailcow: - image: ghcr.io/mailcow/sogo:5.12.8-1 + image: ghcr.io/mailcow/sogo:5.12.9-1 environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} From 843db5854c2dd9d85f89cd35cde6361f43d9a999 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:04:42 +0000 Subject: [PATCH 08/68] Update dependency composer/composer to v2.10.1 Signed-off-by: milkmaker --- data/Dockerfiles/phpfpm/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index 7917d7f7d..9181374ad 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -13,7 +13,7 @@ ARG MEMCACHED_PECL_VERSION=3.4.0 # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?.*)$ ARG REDIS_PECL_VERSION=6.3.0 # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?.*)$ -ARG COMPOSER_VERSION=2.9.5 +ARG COMPOSER_VERSION=2.10.1 RUN apk add -U --no-cache autoconf \ aspell-dev \ From 0fafda696bc9ee997e3c2a368b28f4600037aaca Mon Sep 17 00:00:00 2001 From: Snafu Date: Sun, 7 Jun 2026 16:42:47 +0200 Subject: [PATCH 09/68] Fix force_tfa not available in mailbox template #7216 --- data/web/api/openapi.yaml | 14 +++++++++++++- data/web/inc/functions.mailbox.inc.php | 1 + data/web/js/site/mailbox.js | 11 +++++++++++ data/web/lang/lang.en-gb.json | 1 + data/web/templates/edit/mailbox-templates.twig | 9 +++++++++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index cf54f6add..77c58b609 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -1072,6 +1072,7 @@ paths: password2: "*" quota: "3072" force_pw_update: "1" + force_tfa: "1" tls_enforce_in: "1" tls_enforce_out: "1" tags: ["tag1", "tag2"] @@ -1118,6 +1119,7 @@ paths: password2: atedismonsin quota: "3072" force_pw_update: "1" + force_tfa: "1" tls_enforce_in: "1" tls_enforce_out: "1" tags: ["tag1", "tag2"] @@ -1151,6 +1153,9 @@ paths: force_pw_update: description: forces the user to update its password on first login type: boolean + force_tfa: + description: force 2FA enrollment at login + type: boolean tls_enforce_in: description: force inbound email tls encryption type: boolean @@ -2510,7 +2515,7 @@ paths: description: >- Using this endpoint you can perform actions on quarantine items. It is possible to release emails from quarantine into to the inbox, or learn them as ham to improve Rspamd filtering. - You must provide the quarantine item IDs. You can get the IDs using the GET method. + You must provide the quarantine item IDs. You can get the IDs using the GET method. operationId: Edit mails in Quarantine requestBody: content: @@ -3414,6 +3419,7 @@ paths: - mailbox - active: "1" force_pw_update: "0" + force_tfa: "0" name: Full name password: "*" password2: "*" @@ -3464,6 +3470,7 @@ paths: attr: active: "1" force_pw_update: "0" + force_tfa: "0" name: Full name authsource: mailcow password: "" @@ -3487,6 +3494,9 @@ paths: force_pw_update: description: force user to change password on next login type: boolean + force_tfa: + description: force 2FA enrollment at login + type: boolean name: description: Full name of the mailbox user type: string @@ -4881,6 +4891,7 @@ paths: - active: "1" attributes: force_pw_update: "0" + force_tfa: "0" mailbox_format: "maildir:" quarantine_notification: never sogo_access: "1" @@ -5805,6 +5816,7 @@ paths: - active: "1" attributes: force_pw_update: "0" + force_tfa: "0" mailbox_format: "maildir:" quarantine_notification: never sogo_access: "1" diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index adb330ea8..b6d683e1b 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3828,6 +3828,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : $is_now['rl_frame']; $attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : $is_now['rl_value']; $attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : $is_now['force_pw_update']; + $attr["force_tfa"] = isset($_data['force_tfa']) ? intval($_data['force_tfa']) : $is_now['force_tfa']; $attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : $is_now['sogo_access']; $attr["active"] = isset($_data['active']) ? intval($_data['active']) : $is_now['active']; $attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in']; diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index e8edb9940..0a0192798 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -424,6 +424,11 @@ $(document).ready(function() { } else { $('#force_pw_update').prop('checked', false); } + if (template.force_tfa == 1){ + $('#force_tfa').prop('checked', true); + } else { + $('#force_tfa').prop('checked', false); + } if (template.sogo_access == 1){ $('#sogo_access').prop('checked', true); } else { @@ -1242,6 +1247,7 @@ jQuery(function($){ item.attributes.eas_access = '' + (item.attributes.eas_access == 1 ? '1' : '0') + ''; item.attributes.dav_access = '' + (item.attributes.dav_access == 1 ? '1' : '0') + ''; item.attributes.sogo_access = '' + (item.attributes.sogo_access == 1 ? '1' : '0') + ''; + item.attributes.force_tfa = '' + (item.attributes.force_tfa == 1 ? '1' : '0') + ''; if (item.attributes.quarantine_notification === 'never') { item.attributes.quarantine_notification = lang.never; } else if (item.attributes.quarantine_notification === 'hourly') { @@ -1385,6 +1391,11 @@ jQuery(function($){ return 1==data?'':''; } }, + { + title: lang.force_tfa, + data: 'attributes.force_tfa', + defaultContent: '' + }, { title: lang_edit.ratelimit, data: 'attributes.ratelimit', diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index e786bcbe5..d2412fbf7 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -929,6 +929,7 @@ "filters": "Filters", "fname": "Full name", "force_pw_update": "Force password update at next login", + "force_tfa": "TFA", "gal": "Global Address List", "goto_ham": "Learn as ham", "goto_spam": "Learn as spam", diff --git a/data/web/templates/edit/mailbox-templates.twig b/data/web/templates/edit/mailbox-templates.twig index ddc139586..8384fa053 100644 --- a/data/web/templates/edit/mailbox-templates.twig +++ b/data/web/templates/edit/mailbox-templates.twig @@ -8,6 +8,7 @@ + @@ -165,6 +166,14 @@ +
+
+
+ + {{ lang.tfa.force_tfa_info }} +
+
+
{% if not skip_sogo %}
From cce02e2b15ae96984803cc5a1906b0a8d268e587 Mon Sep 17 00:00:00 2001 From: Bobby Date: Mon, 8 Jun 2026 00:01:43 +0700 Subject: [PATCH 10/68] Refresh SOGo view after mailbox activation --- data/web/inc/functions.mailbox.inc.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index adb330ea8..326f1a091 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3502,11 +3502,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'msg' => array('mailbox_modified', $username) ); - // Track affected mailboxes for SOGo update - $update_sogo_mailboxes[] = $username; - } - return true; - break; + // Track affected mailboxes for SOGo update + $update_sogo_mailboxes[] = $username; + } + break; case 'mailbox_rename': $domain = $_data['domain']; $old_local_part = $_data['old_local_part']; From 15891960f78fd10e90aac0cc3ab5267720a0b38f Mon Sep 17 00:00:00 2001 From: Dmytro Alieksieiev <1865999+dragoangel@users.noreply.github.com> Date: Tue, 9 Jun 2026 01:19:06 +0200 Subject: [PATCH 11/68] Update RSPAMD version to 4.1.0 --- data/Dockerfiles/rspamd/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index ab9aaa5ad..d37d1226a 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -2,7 +2,7 @@ FROM debian:trixie-slim LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ARG RSPAMD_VER=rspamd_4.0.1-1~8700333 +ARG RSPAMD_VER=rspamd_4.1.0-1~e2b0b18 ARG CODENAME=trixie ENV LC_ALL=C From c0050e8836a2b11233d3fb2ec132cf252b63e24b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:19:09 +0200 Subject: [PATCH 12/68] Update devops-infra/action-pull-request action to v1.3.0 (#7272) --- .github/workflows/pr_to_nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_to_nightly.yml b/.github/workflows/pr_to_nightly.yml index 0b0738e93..01d7a91aa 100644 --- a/.github/workflows/pr_to_nightly.yml +++ b/.github/workflows/pr_to_nightly.yml @@ -12,7 +12,7 @@ jobs: with: fetch-depth: 0 - name: Run the Action - uses: devops-infra/action-pull-request@v1.2.1 + uses: devops-infra/action-pull-request@v1.3.0 with: github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }} title: Automatic PR to nightly from ${{ github.event.repository.updated_at}} From 03c31f825ab128c22c2ce69398d2f3f61754c972 Mon Sep 17 00:00:00 2001 From: milkmaker Date: Tue, 9 Jun 2026 12:20:53 +0200 Subject: [PATCH 13/68] update postscreen_access.cidr (#7269) --- data/conf/postfix/postscreen_access.cidr | 73 ++---------------------- 1 file changed, 5 insertions(+), 68 deletions(-) diff --git a/data/conf/postfix/postscreen_access.cidr b/data/conf/postfix/postscreen_access.cidr index 285ea7a81..70fd26511 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 Fri May 1 00:43:37 UTC 2026 +# Whitelist generated by Postwhite v3.4 on Mon Jun 1 00:52:17 UTC 2026 # https://github.com/stevejenkins/postwhite/ -# 2249 total rules +# 2186 total rules 2a00:1450:4000::/36 permit 2a00:1450:4864::/56 permit 2a01:111:f400::/48 permit @@ -56,14 +56,8 @@ 8.25.194.0/23 permit 8.25.196.0/23 permit 8.36.116.0/24 permit -8.39.54.0/23 permit -8.39.54.250/31 permit 8.39.144.0/24 permit -8.40.222.0/23 permit -8.40.222.250/31 permit 12.130.86.238 permit -13.107.213.40 permit -13.107.246.40 permit 13.108.16.0/20 permit 13.110.208.0/21 permit 13.110.209.0/24 permit @@ -73,7 +67,6 @@ 13.111.191.0/24 permit 13.216.7.111 permit 13.216.54.180 permit -13.247.164.219 permit 15.200.21.50 permit 15.200.44.248 permit 15.200.201.185 permit @@ -177,7 +170,6 @@ 34.215.104.144 permit 34.218.115.239 permit 34.225.212.172 permit -34.241.242.183 permit 35.83.148.184 permit 35.155.198.111 permit 35.158.23.94 permit @@ -201,7 +193,6 @@ 40.233.64.216 permit 40.233.83.78 permit 40.233.88.28 permit -43.239.212.33 permit 44.206.138.57 permit 44.210.169.44 permit 44.217.45.156 permit @@ -284,7 +275,6 @@ 51.83.17.38 permit 52.1.14.157 permit 52.5.230.59 permit -52.6.74.205 permit 52.12.53.23 permit 52.13.214.179 permit 52.26.1.71 permit @@ -341,7 +331,6 @@ 54.244.54.130 permit 54.244.242.0/24 permit 54.255.61.23 permit -56.124.6.228 permit 57.103.64.0/18 permit 57.129.93.249 permit 62.13.128.0/24 permit @@ -408,7 +397,6 @@ 65.110.161.77 permit 65.123.29.213 permit 65.123.29.220 permit -65.154.166.0/24 permit 65.212.180.36 permit 66.102.0.0/20 permit 66.119.150.192/26 permit @@ -1222,9 +1210,6 @@ 99.78.197.208/28 permit 103.9.96.0/22 permit 103.28.42.0/24 permit -103.84.217.15 permit -103.84.217.238 permit -103.89.75.238 permit 103.151.192.0/23 permit 103.168.172.128/27 permit 103.237.104.0/22 permit @@ -1385,9 +1370,6 @@ 117.120.16.0/21 permit 119.42.242.52/31 permit 119.42.242.156 permit -121.244.91.48 permit -121.244.91.52 permit -122.15.156.182 permit 123.126.78.64/29 permit 124.108.96.24/31 permit 124.108.96.28/31 permit @@ -1470,21 +1452,7 @@ 134.170.141.64/26 permit 134.170.143.0/24 permit 134.170.174.0/24 permit -135.84.80.0/24 permit -135.84.81.0/24 permit -135.84.82.0/24 permit -135.84.83.0/24 permit 135.84.216.0/22 permit -136.143.160.0/24 permit -136.143.161.0/24 permit -136.143.162.0/24 permit -136.143.176.0/24 permit -136.143.177.0/24 permit -136.143.178.49 permit -136.143.182.0/23 permit -136.143.184.0/24 permit -136.143.188.0/24 permit -136.143.190.0/23 permit 136.146.128.0/20 permit 136.147.128.0/20 permit 136.147.135.0/24 permit @@ -1504,7 +1472,6 @@ 139.138.46.219 permit 139.138.57.55 permit 139.138.58.119 permit -139.167.79.86 permit 139.177.108.0/25 permit 139.180.17.0/24 permit 140.238.148.191 permit @@ -1554,12 +1521,14 @@ 147.243.128.26 permit 148.105.0.0/16 permit 148.105.8.0/21 permit +148.116.32.128/25 permit 149.72.0.0/16 permit 149.72.234.184 permit 149.72.248.236 permit 149.97.173.180 permit 149.118.160.128/25 permit 150.136.21.199 permit +150.171.109.118 permit 150.230.98.160 permit 151.145.38.14 permit 152.67.105.195 permit @@ -1636,11 +1605,6 @@ 164.152.25.241 permit 164.177.132.168/30 permit 165.1.100.0/25 permit -165.173.128.0/24 permit -165.173.180.1 permit -165.173.180.250/31 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 @@ -1673,19 +1637,6 @@ 168.245.12.252 permit 168.245.46.9 permit 168.245.127.231 permit -169.148.129.0/24 permit -169.148.131.0/24 permit -169.148.138.0/24 permit -169.148.142.10 permit -169.148.142.33 permit -169.148.144.0/25 permit -169.148.144.10 permit -169.148.146.0/23 permit -169.148.174.10 permit -169.148.175.3 permit -169.148.179.3 permit -169.148.188.0/24 permit -169.148.188.182 permit 170.9.232.254 permit 170.10.128.0/24 permit 170.10.129.0/24 permit @@ -1891,16 +1842,7 @@ 199.16.156.0/22 permit 199.33.145.1 permit 199.33.145.32 permit -199.34.22.36 permit 199.59.148.0/22 permit -199.67.80.2 permit -199.67.80.20 permit -199.67.82.2 permit -199.67.82.20 permit -199.67.84.0/24 permit -199.67.86.0/24 permit -199.67.88.0/24 permit -199.67.90.0/24 permit 199.101.161.130 permit 199.101.162.0/25 permit 199.122.120.0/21 permit @@ -1956,8 +1898,6 @@ 204.92.114.187 permit 204.92.114.203 permit 204.92.114.204/31 permit -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.168.0/21 permit @@ -2231,10 +2171,7 @@ 2001:748:400:3301::4 permit 2404:6800:4000::/36 permit 2404:6800:4864::/56 permit -2603:1061:14:72::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 +2603:1061:14:40::1 permit 2607:f8b0:4000::/36 permit 2607:f8b0:4864::/56 permit 2620:109:c003:104::/64 permit From 24cc369d84935428f58a4c6bef5b18a58ad248ed Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:34:27 +0200 Subject: [PATCH 14/68] [Rspamd] Migrate metadata_exporter to multipart formatter --- .../rspamd/local.d/metadata_exporter.conf | 6 +- data/conf/rspamd/meta_exporter/pipe.php | 59 +++++++++---------- data/conf/rspamd/meta_exporter/pushover.php | 48 +++++++-------- docker-compose.yml | 2 +- 4 files changed, 52 insertions(+), 63 deletions(-) diff --git a/data/conf/rspamd/local.d/metadata_exporter.conf b/data/conf/rspamd/local.d/metadata_exporter.conf index daaa79b4e..cc85faacd 100644 --- a/data/conf/rspamd/local.d/metadata_exporter.conf +++ b/data/conf/rspamd/local.d/metadata_exporter.conf @@ -3,8 +3,7 @@ rules { backend = "http"; url = "http://nginx:9081/pipe.php"; selector = "reject_no_global_bl"; - formatter = "default"; - meta_headers = true; + formatter = "multipart"; } RLINFO { backend = "http"; @@ -16,8 +15,7 @@ rules { backend = "http"; url = "http://nginx:9081/pushover.php"; selector = "mailcow_rcpt"; - formatter = "json"; - meta_headers = true; + formatter = "multipart"; } } diff --git a/data/conf/rspamd/meta_exporter/pipe.php b/data/conf/rspamd/meta_exporter/pipe.php index 003639b08..0a9ed59e2 100644 --- a/data/conf/rspamd/meta_exporter/pipe.php +++ b/data/conf/rspamd/meta_exporter/pipe.php @@ -32,47 +32,42 @@ function parse_email($email) { $a = strrpos($email, '@'); return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1)); } -if (!function_exists('getallheaders')) { - function getallheaders() { - if (!is_array($_SERVER)) { - return array(); - } - $headers = array(); - foreach ($_SERVER as $name => $value) { - if (substr($name, 0, 5) == 'HTTP_') { - $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; - } - } - return $headers; - } +// rspamd metadata_exporter (multipart formatter): +// - $_POST['metadata'] JSON with the rspamd metadata +// - $_FILES['message'] raw RFC822 message +if (empty($_POST['metadata']) || !isset($_FILES['message']) || $_FILES['message']['error'] !== UPLOAD_ERR_OK) { + error_log("QUARANTINE: missing multipart parts from rspamd" . PHP_EOL); + http_response_code(400); + exit; } -$raw_data_content = file_get_contents('php://input'); +$meta = json_decode($_POST['metadata'], true); +if (!is_array($meta)) { + error_log("QUARANTINE: cannot decode metadata JSON" . PHP_EOL); + http_response_code(400); + exit; +} + +$raw_data_content = file_get_contents($_FILES['message']['tmp_name']); $raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8"); -$headers = getallheaders(); +$raw_size = (int)$_FILES['message']['size']; -$qid = $headers['X-Rspamd-Qid']; -$fuzzy = $headers['X-Rspamd-Fuzzy']; -$subject = iconv_mime_decode($headers['X-Rspamd-Subject']); -$score = $headers['X-Rspamd-Score']; -$rcpts = $headers['X-Rspamd-Rcpt']; -$user = $headers['X-Rspamd-User']; -$ip = $headers['X-Rspamd-Ip']; -$action = $headers['X-Rspamd-Action']; -$sender = $headers['X-Rspamd-From']; -$symbols = $headers['X-Rspamd-Symbols']; - -$raw_size = (int)$_SERVER['CONTENT_LENGTH']; +$qid = $meta['qid'] ?? 'unknown'; +$subject = iconv_mime_decode($meta['subject'] ?? ''); +$score = $meta['score'] ?? 0; +$rcpts = $meta['rcpt'] ?? array(); +$user = $meta['user'] ?? 'unknown'; +$ip = $meta['ip'] ?? 'unknown'; +$action = $meta['action'] ?? 'no action'; +$sender = $meta['from'] ?? ''; +$symbols = json_encode($meta['symbols'] ?? array()); +$fuzzy = json_encode(is_array($meta['fuzzy'] ?? null) ? $meta['fuzzy'] : array()); if (empty($sender)) { error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL); $sender = 'empty-env-from@localhost'; } -if ($fuzzy == 'unknown') { - $fuzzy = '[]'; -} - try { $max_size = (int)$redis->Get('Q_MAX_SIZE'); if (($max_size * 1048576) < $raw_size) { @@ -94,7 +89,7 @@ catch (RedisException $e) { $rcpt_final_mailboxes = array(); // Loop through all rcpts -foreach (json_decode($rcpts, true) as $rcpt) { +foreach ($rcpts as $rcpt) { // Remove tag $rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt); diff --git a/data/conf/rspamd/meta_exporter/pushover.php b/data/conf/rspamd/meta_exporter/pushover.php index efe5fdd52..a2cd0995b 100644 --- a/data/conf/rspamd/meta_exporter/pushover.php +++ b/data/conf/rspamd/meta_exporter/pushover.php @@ -32,50 +32,46 @@ function parse_email($email) { $a = strrpos($email, '@'); return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1)); } -if (!function_exists('getallheaders')) { - function getallheaders() { - if (!is_array($_SERVER)) { - return array(); - } - $headers = array(); - foreach ($_SERVER as $name => $value) { - if (substr($name, 0, 5) == 'HTTP_') { - $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; - } - } - return $headers; - } +// rspamd metadata_exporter (multipart formatter): metadata JSON arrives as $_POST['metadata']. +if (empty($_POST['metadata'])) { + error_log("NOTIFY: missing metadata part from rspamd" . PHP_EOL); + http_response_code(400); + exit; } -$headers = getallheaders(); -$json_body = json_decode(file_get_contents('php://input')); +$meta = json_decode($_POST['metadata'], true); +if (!is_array($meta)) { + error_log("NOTIFY: cannot decode metadata JSON" . PHP_EOL); + http_response_code(400); + exit; +} -$qid = $headers['X-Rspamd-Qid']; -$rcpts = $headers['X-Rspamd-Rcpt']; -$sender = $headers['X-Rspamd-From']; -$ip = $headers['X-Rspamd-Ip']; -$subject = iconv_mime_decode($headers['X-Rspamd-Subject']); -$messageid= $json_body->message_id; +$qid = $meta['qid'] ?? 'unknown'; +$rcpts = $meta['rcpt'] ?? array(); +$sender = $meta['from'] ?? ''; +$ip = $meta['ip'] ?? 'unknown'; +$subject = iconv_mime_decode($meta['subject'] ?? ''); +$messageid= $meta['message_id'] ?? ''; $priority = 0; -$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true); +$symbols_array = $meta['symbols'] ?? array(); if (is_array($symbols_array)) { foreach ($symbols_array as $symbol) { - if ($symbol['name'] == 'HAS_X_PRIO_ONE') { + if (($symbol['name'] ?? null) == 'HAS_X_PRIO_ONE') { $priority = 1; break; } } } -$sender_address = $json_body->header_from[0]; +$sender_address = $meta['header_from'][0] ?? ''; $sender_name = '-'; if (preg_match('/(?.*?)<(?
.*?)>/i', $sender_address, $matches)) { $sender_address = $matches['address']; $sender_name = trim($matches['name'], '"\' '); } -$to_address = $json_body->header_to[0]; +$to_address = $meta['header_to'][0] ?? ''; $to_name = '-'; if (preg_match('/(?.*?)<(?
.*?)>/i', $to_address, $matches)) { $to_address = $matches['address']; @@ -85,7 +81,7 @@ if (preg_match('/(?.*?)<(?
.*?)>/i', $to_address, $matches)) { $rcpt_final_mailboxes = array(); // Loop through all rcpts -foreach (json_decode($rcpts, true) as $rcpt) { +foreach ($rcpts as $rcpt) { // Remove tag $rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt); diff --git a/docker-compose.yml b/docker-compose.yml index 615e19f57..16370a7bd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,7 +84,7 @@ services: - clamd rspamd-mailcow: - image: ghcr.io/mailcow/rspamd:3.14.3-1 + image: ghcr.io/mailcow/rspamd:4.1.0-1 stop_grace_period: 30s depends_on: - dovecot-mailcow From 277a307fb91f701f2f2619debd50ec183ca34192 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:55:32 +0200 Subject: [PATCH 15/68] [Web] Fix refresh SOGo view on mailbox deletion --- data/web/inc/functions.mailbox.inc.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 326f1a091..127374e81 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -3502,10 +3502,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { 'msg' => array('mailbox_modified', $username) ); - // Track affected mailboxes for SOGo update - $update_sogo_mailboxes[] = $username; - } - break; + // Track affected mailboxes for SOGo update + $update_sogo_mailboxes[] = $username; + } + break; case 'mailbox_rename': $domain = $_data['domain']; $old_local_part = $_data['old_local_part']; @@ -6126,7 +6126,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // Track affected mailboxes for SOGo update $update_sogo_mailboxes[] = $username; } - return true; break; case 'mailbox_templates': if ($_SESSION['mailcow_cc_role'] != "admin") { From fa154c71f3fc855c99fae503a222790b92e1e42e Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:29:01 +0200 Subject: [PATCH 16/68] [PHP] Rebuild Image --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 87b739ed0..cccf3a216 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -117,7 +117,7 @@ services: - rspamd php-fpm-mailcow: - image: ghcr.io/mailcow/phpfpm:8.2.29-2 + image: ghcr.io/mailcow/phpfpm:8.2.29-3 command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" depends_on: - redis-mailcow From 7e6c36dce1c106e95334410ad8313d1174fe15fa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:35:03 +0200 Subject: [PATCH 17/68] Update alpine Docker tag to v3.24 (#7280) --- .../EXTERNAL_MYSQL_SOCKET/docker-compose.override.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper-scripts/docker-compose.override.yml.d/EXTERNAL_MYSQL_SOCKET/docker-compose.override.yml b/helper-scripts/docker-compose.override.yml.d/EXTERNAL_MYSQL_SOCKET/docker-compose.override.yml index 5ef729a48..fd47e15fe 100644 --- a/helper-scripts/docker-compose.override.yml.d/EXTERNAL_MYSQL_SOCKET/docker-compose.override.yml +++ b/helper-scripts/docker-compose.override.yml.d/EXTERNAL_MYSQL_SOCKET/docker-compose.override.yml @@ -25,6 +25,6 @@ services: - /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock mysql-mailcow: - image: alpine:3.23 + image: alpine:3.24 command: /bin/true restart: "no" From ddcce811c1f77658d857ec3357505eba1fce264d Mon Sep 17 00:00:00 2001 From: Chris Haumesser <5400416-wryfi@users.noreply.gitlab.com> Date: Fri, 12 Jun 2026 16:32:22 -0600 Subject: [PATCH 18/68] Fixes #7150 (Dovecot passwd-verify.lua wipes auth cache during any nginx restart or unavailability) - distinguishes between text and numeric responses from nginx - prevents auth cache from being invalidated due to nginx unavailability --- data/conf/dovecot/auth/passwd-verify.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 From 47e4552b44ddfd528b99987545ccdaf61135f2c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 08:34:02 +0200 Subject: [PATCH 19/68] Update actions/checkout action to v7 (#7300) --- .github/workflows/image_builds.yml | 2 +- .github/workflows/pr_to_nightly.yml | 2 +- .github/workflows/rebuild_backup_image.yml | 2 +- .github/workflows/update_postscreen_access_list.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/image_builds.yml b/.github/workflows/image_builds.yml index c8ec7ccae..aa56b0381 100644 --- a/.github/workflows/image_builds.yml +++ b/.github/workflows/image_builds.yml @@ -27,7 +27,7 @@ jobs: - "watchdog-mailcow" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup Docker run: | curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh diff --git a/.github/workflows/pr_to_nightly.yml b/.github/workflows/pr_to_nightly.yml index 01d7a91aa..5c361af72 100644 --- a/.github/workflows/pr_to_nightly.yml +++ b/.github/workflows/pr_to_nightly.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Run the Action diff --git a/.github/workflows/rebuild_backup_image.yml b/.github/workflows/rebuild_backup_image.yml index b18d339ba..4490afee1 100644 --- a/.github/workflows/rebuild_backup_image.yml +++ b/.github/workflows/rebuild_backup_image.yml @@ -13,7 +13,7 @@ jobs: packages: write steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up QEMU uses: docker/setup-qemu-action@v4 diff --git a/.github/workflows/update_postscreen_access_list.yml b/.github/workflows/update_postscreen_access_list.yml index 066616aee..ed63281c7 100644 --- a/.github/workflows/update_postscreen_access_list.yml +++ b/.github/workflows/update_postscreen_access_list.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Generate postscreen_access.cidr run: | From 9e723e942d02de31ba8ca78aa5e34a68b62ed38b Mon Sep 17 00:00:00 2001 From: milkmaker Date: Sat, 20 Jun 2026 13:57:15 +0200 Subject: [PATCH 20/68] Translations update from Weblate (#7302) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Web] Updated lang.fr-fr.json Co-authored-by: Romain Ayme * [Web] Updated lang.si-si.json Co-authored-by: Matjaž Tekavec --------- Co-authored-by: Romain Ayme Co-authored-by: Matjaž Tekavec --- data/web/lang/lang.fr-fr.json | 23 +++++++++++++++++------ data/web/lang/lang.si-si.json | 3 ++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/data/web/lang/lang.fr-fr.json b/data/web/lang/lang.fr-fr.json index 43c9c065a..9ee916713 100644 --- a/data/web/lang/lang.fr-fr.json +++ b/data/web/lang/lang.fr-fr.json @@ -111,7 +111,8 @@ "app_passwd_protocols": "Protocoles autorisés pour le mot de passe de l'application", "dry": "Simuler la synchronisation", "internal": "Interne", - "internal_info": "Les alias internes sont accessibles uniquement depuis le domaine ou les alias du domaine." + "internal_info": "Les alias internes sont accessibles uniquement depuis le domaine ou les alias du domaine.", + "sender_allowed": "Autoriser l'envoi sous cet alias" }, "admin": { "access": "Accès", @@ -556,7 +557,9 @@ "max_age_invalid": "L'âge maximum %s est invalide", "mode_invalid": "Le mode %s est invalide", "mx_invalid": "L'enregistrement MX %s est invalide", - "version_invalid": "La version %s est invalide" + "version_invalid": "La version %s est invalide", + "tfa_removal_blocked": "L’authentification à deux facteurs ne peut pas être supprimée, car elle est requise pour votre compte.", + "quarantine_category_invalid": "La catégorie de quarantaine doit être l’une des suivantes : add_header, reject, all\"" }, "debug": { "chart_this_server": "Graphique (ce serveur)", @@ -755,7 +758,9 @@ "mta_sts_info": "MTA-STS est un standard qui oblige la délivrance des courriels entre les serveurs de courriels à utiliser TLS avec des certificats valides.
Il est utilisé quand DANE n'est pas possible à cause d'un manque ou d'un non support de DNSSEC.
Note : Si le domaine du destinataire supporte DANE avec DNSSEC, DANE est toujours préféré – MTA-STS sert seulement en secours.", "mta_sts_mode_info": "Il y a trois modes parmi lesquels choisir :
  • testing – la politique est seulement surveillée, les violations n'ont pas d'impact.
  • enforce – la politique est appliquée strictement, les connexions sans TLS valide sont rejetées.
  • none – la politique est publiée mais non appliquée.
", "mta_sts_max_age_info": "Durée en secondes pendant laquelle les serveurs de courriel peuvent mettre en cache cette politique avant de revérifier.", - "mta_sts_mx_info": "Autoriser l'envoi uniquement aux noms d'hôtes des serveurs de courriels indiqués explicitement ; le MTA émetteur vérifie si le nom d'hôte DNS du MX correspond à la liste de la politique, et autorise la délivrance seulement avec un certificat TLS valide (protège contre le MITM)." + "mta_sts_mx_info": "Autoriser l'envoi uniquement aux noms d'hôtes des serveurs de courriels indiqués explicitement ; le MTA émetteur vérifie si le nom d'hôte DNS du MX correspond à la liste de la politique, et autorise la délivrance seulement avec un certificat TLS valide (protège contre le MITM).", + "sender_allowed": "Autoriser l'envoi sous cet alias", + "sender_allowed_info": "Si cette option est désactivée, cet alias peut uniquement recevoir des courriels. Utilisez les ACL d’expéditeur pour autoriser certaines boîtes aux lettres à envoyer des messages via cet alias." }, "footer": { "cancel": "Annuler", @@ -988,7 +993,8 @@ "recipient": "Destinataire", "open_logs": "Afficher les journaux", "iam": "Fournisseur d'identité", - "internal": "Interne" + "internal": "Interne", + "force_tfa": "TFA" }, "oauth2": { "access_denied": "Veuillez vous connecter en tant que propriétaire de la boîte de réception pour accorder l’accès via Oauth2.", @@ -1189,7 +1195,11 @@ "yubi_otp": "Authentification OTP Yubico", "authenticators": "Authentificateurs", "u2f_deprecated_important": "Veuillez enregistrer votre clé dans le panneau d'administration avec la nouvelle méthode WebAuthn.", - "u2f_deprecated": "Il semble que votre clé ait été enregistrée à l'aide de la méthode U2F obsolète. Nous allons désactiver l'authentification à deux facteurs pour vous et supprimer votre clé." + "u2f_deprecated": "Il semble que votre clé ait été enregistrée à l'aide de la méthode U2F obsolète. Nous allons désactiver l'authentification à deux facteurs pour vous et supprimer votre clé.", + "force_tfa": "Imposer l'authentification à deux facteurs lors de la connexion", + "force_tfa_info": "L’utilisateur doit configurer l’authentification à deux facteurs avant de pouvoir accéder à l’interface.", + "setup_title": "Authentification à deux facteurs requise", + "setup_required": "Votre compte nécessite l’authentification à deux facteurs. Veuillez configurer une méthode 2FA pour continuer." }, "fido2": { "set_fn": "Définir un nom", @@ -1378,7 +1388,8 @@ "forever": "Pour toujours", "spam_aliases_info": "Un alias de spam est une adresse de courriel temporaire qui peut être utilisée pour protéger les véritables adresses de courriel.
De manière optionnelle, une durée d'expiration peut être définie afin que l'alias soit automatiquement désactivé après la période définie, éliminant ainsi les adresses étant abusées ou ayant fuité.", "authentication": "Authentification", - "protocols": "Protocoles" + "protocols": "Protocoles", + "pw_update_required": "Votre compte nécessite un changement de mot de passe. Veuillez définir un nouveau mot de passe pour continuer." }, "warning": { "cannot_delete_self": "Impossible de supprimer l’utilisateur connecté", diff --git a/data/web/lang/lang.si-si.json b/data/web/lang/lang.si-si.json index f5ba53f98..39d57eb31 100644 --- a/data/web/lang/lang.si-si.json +++ b/data/web/lang/lang.si-si.json @@ -1001,7 +1001,8 @@ "weekly": "Tedensko", "sieve_info": "Na uporabnika lahko shranite več filtrov, vendar je lahko hkrati aktiven le en predfilter in en postfilter.
\nVsak filter bo obdelan v opisanem vrstnem redu. Niti neuspešen skript niti izdan ukaz »keep;« ne bosta ustavila obdelave nadaljnjih skript. Spremembe globalnih skriptov sita bodo sprožile ponovni zagon Dovecota.

Globalni predfilter sita • Predfilter • Uporabniški skripti • Postfilter • Globalni postfilter sita", "tls_policy_maps_info": "Ta preslikava pravilnikov preglasi pravila odhodnega prenosa TLS neodvisno od uporabnikovih nastavitev pravilnikov TLS.
\n Za več informacij preverite dokumentacijo »smtp_tls_policy_maps«.", - "internal": "Notranje" + "internal": "Notranje", + "force_tfa": "TFA" }, "fido2": { "known_ids": "Znani ID-ji", From 1f8b4679ccb92cdb27c99e9a42fcf99637b71fe6 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Thu, 25 Jun 2026 12:17:37 +0200 Subject: [PATCH 21/68] [Nignx] Update to 1.30.3 --- data/Dockerfiles/nginx/Dockerfile | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/Dockerfiles/nginx/Dockerfile b/data/Dockerfiles/nginx/Dockerfile index 30297362f..65b34838e 100644 --- a/data/Dockerfiles/nginx/Dockerfile +++ b/data/Dockerfiles/nginx/Dockerfile @@ -1,4 +1,4 @@ -FROM nginx:1.30.2-alpine +FROM nginx:1.30.3-alpine LABEL maintainer "The Infrastructure Company GmbH " ENV PIP_BREAK_SYSTEM_PACKAGES=1 diff --git a/docker-compose.yml b/docker-compose.yml index cccf3a216..6f579a8a0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -419,7 +419,7 @@ services: - php-fpm-mailcow - sogo-mailcow - rspamd-mailcow - image: ghcr.io/mailcow/nginx:1.30.2-1 + image: ghcr.io/mailcow/nginx:1.30.3-1 dns: - ${IPV4_NETWORK:-172.22.1}.254 environment: From 1840074991c4c2f07163519dc0caef1a2ddc33f4 Mon Sep 17 00:00:00 2001 From: milkmaker Date: Wed, 1 Jul 2026 11:31:58 +0200 Subject: [PATCH 22/68] update postscreen_access.cidr (#7311) --- data/conf/postfix/postscreen_access.cidr | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/data/conf/postfix/postscreen_access.cidr b/data/conf/postfix/postscreen_access.cidr index 70fd26511..6fcedff95 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 Mon Jun 1 00:52:17 UTC 2026 +# Whitelist generated by Postwhite v3.4 on Wed Jul 1 00:51:20 UTC 2026 # https://github.com/stevejenkins/postwhite/ -# 2186 total rules +# 2251 total rules 2a00:1450:4000::/36 permit 2a00:1450:4864::/56 permit 2a01:111:f400::/48 permit @@ -1528,7 +1528,7 @@ 149.97.173.180 permit 149.118.160.128/25 permit 150.136.21.199 permit -150.171.109.118 permit +150.171.109.183 permit 150.230.98.160 permit 151.145.38.14 permit 152.67.105.195 permit @@ -1605,6 +1605,12 @@ 164.152.25.241 permit 164.177.132.168/30 permit 165.1.100.0/25 permit +165.173.128.0/24 permit +165.173.180.0/24 permit +165.173.180.250/31 permit +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 @@ -1651,6 +1657,7 @@ 173.203.81.39 permit 173.224.161.128/25 permit 173.224.165.0/26 permit +173.224.166.48/28 permit 174.36.84.8/29 permit 174.36.84.16/29 permit 174.36.84.32/29 permit @@ -2171,7 +2178,10 @@ 2001:748:400:3301::4 permit 2404:6800:4000::/36 permit 2404:6800:4864::/56 permit -2603:1061:14:40::1 permit +2603:1061:14:1c2::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 2607:f8b0:4000::/36 permit 2607:f8b0:4864::/56 permit 2620:109:c003:104::/64 permit From 94e23a3cd33a2147e61af1e56e62ef11172a5a13 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:32:23 +0000 Subject: [PATCH 23/68] Update dependency composer/composer to v2.10.2 Signed-off-by: milkmaker --- data/Dockerfiles/phpfpm/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index 556c50128..ca252163a 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -13,7 +13,7 @@ ARG MEMCACHED_PECL_VERSION=3.4.0 # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?.*)$ ARG REDIS_PECL_VERSION=6.3.0 # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?.*)$ -ARG COMPOSER_VERSION=2.10.1 +ARG COMPOSER_VERSION=2.10.2 RUN apk add -U --no-cache autoconf \ aspell-dev \ From 3098caebbc747f7b329a13f8ee9c381d7cc0452a Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Fri, 10 Jul 2026 18:23:58 +0200 Subject: [PATCH 24/68] migrate postfix to trixie --- data/Dockerfiles/postfix/Dockerfile | 2 +- data/Dockerfiles/postfix/syslog-ng-redis_slave.conf | 4 ++-- data/Dockerfiles/postfix/syslog-ng.conf | 4 ++-- data/conf/postfix/main.cf | 2 -- data/conf/postfix/master.cf | 2 -- 5 files changed, 5 insertions(+), 9 deletions(-) diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index 994612ec4..cf0647692 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:bookworm-slim +FROM debian:trixie-slim LABEL maintainer="The Infrastructure Company GmbH " diff --git a/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf b/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf index 8e15932a2..bbc9420fe 100644 --- a/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf +++ b/data/Dockerfiles/postfix/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/syslog-ng.conf b/data/Dockerfiles/postfix/syslog-ng.conf index fc7d1aa0f..8f09d04a4 100644 --- a/data/Dockerfiles/postfix/syslog-ng.conf +++ b/data/Dockerfiles/postfix/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/conf/postfix/main.cf b/data/conf/postfix/main.cf index f091cb3f9..a8e2521a8 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -107,8 +107,6 @@ smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch, reject_unknown_sender_domain smtpd_soft_error_limit = 3 smtpd_tls_auth_only = yes -smtpd_tls_dh1024_param_file = /etc/ssl/mail/dhparams.pem -smtpd_tls_eecdh_grade = auto smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA smtpd_tls_loglevel = 1 diff --git a/data/conf/postfix/master.cf b/data/conf/postfix/master.cf index d5114df28..fb49f2df2 100644 --- a/data/conf/postfix/master.cf +++ b/data/conf/postfix/master.cf @@ -29,7 +29,6 @@ smtps inet n - n - - smtpd # TLS protocol can be modified by setting submission_smtpd_tls_mandatory_protocols in extra.cf submission inet n - n - - smtpd -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject - -o smtpd_enforce_tls=yes -o smtpd_tls_security_level=encrypt -o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols -o tls_preempt_cipherlist=yes @@ -38,7 +37,6 @@ submission inet n - n - - smtpd 10587 inet n - n - - smtpd -o smtpd_upstream_proxy_protocol=haproxy -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject - -o smtpd_enforce_tls=yes -o smtpd_tls_security_level=encrypt -o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols -o tls_preempt_cipherlist=yes From 98e26c56038f5918c585d7fe8e12a2b46a084ea3 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Fri, 10 Jul 2026 18:41:58 +0200 Subject: [PATCH 25/68] compose: upgrade Postfix --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6f579a8a0..f2a939218 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -339,7 +339,7 @@ services: - dovecot postfix-mailcow: - image: ghcr.io/mailcow/postfix:3.7.11-2 + image: ghcr.io/mailcow/postfix:3.10.12 depends_on: mysql-mailcow: condition: service_started From f6630e5b43e019a306c29985936ac9fbd5469ca6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Jul 2026 15:19:49 +0200 Subject: [PATCH 26/68] Update actions/stale action to v10.4.0 (#7322) Signed-off-by: milkmaker Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/close_old_issues_and_prs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/close_old_issues_and_prs.yml b/.github/workflows/close_old_issues_and_prs.yml index 855684df9..bba04b0d1 100644 --- a/.github/workflows/close_old_issues_and_prs.yml +++ b/.github/workflows/close_old_issues_and_prs.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - name: Mark/Close Stale Issues and Pull Requests 🗑️ - uses: actions/stale@v10.3.0 + uses: actions/stale@v10.4.0 with: repo-token: ${{ secrets.STALE_ACTION_PAT }} days-before-stale: 60 From d65aa11870bcc5dbcac108f9452b148b2f4e700a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 12 Jul 2026 15:20:18 +0200 Subject: [PATCH 27/68] Update devops-infra/action-pull-request action to v1.4.0 (#7321) Signed-off-by: milkmaker Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/pr_to_nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_to_nightly.yml b/.github/workflows/pr_to_nightly.yml index 5c361af72..8d6b2524d 100644 --- a/.github/workflows/pr_to_nightly.yml +++ b/.github/workflows/pr_to_nightly.yml @@ -12,7 +12,7 @@ jobs: with: fetch-depth: 0 - name: Run the Action - uses: devops-infra/action-pull-request@v1.3.0 + uses: devops-infra/action-pull-request@v1.4.0 with: github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }} title: Automatic PR to nightly from ${{ github.event.repository.updated_at}} From e696ee2f6c1dd2db6ef3fbe415059b0b4192e7ff Mon Sep 17 00:00:00 2001 From: Ralf Bergs Date: Sun, 12 Jul 2026 17:22:15 +0200 Subject: [PATCH 28/68] Refined wording for displaying of active settings on quarantine page. --- data/web/lang/lang.de-de.json | 2 +- data/web/lang/lang.en-gb.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 17277a03b..c19eb209b 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -1086,7 +1086,7 @@ "rspamd_result": "Rspamd-Ergebnis", "sender": "Sender (SMTP)", "sender_header": "Sender (\"From\"-Header)", - "settings_info": "Maximale Anzahl der zurückgehaltenen E-Mails: %s
Maximale Größe einer zu speichernden E-Mail: %s MiB", + "settings_info": "Maximale Anzahl der zurückgehaltenen E-Mails (pro Mailbox): %s
Maximale Größe einer zu speichernden E-Mail: %s MiB", "show_item": "Details", "spam": "Spam", "spam_score": "Bewertung", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index d2412fbf7..873d7b98d 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -1087,7 +1087,7 @@ "rspamd_result": "Rspamd result", "sender": "Sender (SMTP)", "sender_header": "Sender (\"From\" header)", - "settings_info": "Maximum amount of elements to be quarantined: %s
Maximum email size: %s MiB", + "settings_info": "Maximum amount of elements to be quarantined (per mailbox): %s
Maximum email size: %s MiB", "show_item": "Show item", "spam": "Spam", "spam_score": "Score", From 31e1550668d21ccad6127bee38b2eecc8fc4facc Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Mon, 13 Jul 2026 08:45:56 +0200 Subject: [PATCH 29/68] [Postfix] change postscreen blacklist_action to denylist_action --- data/conf/postfix/main.cf | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index a8e2521a8..080be499a 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -37,7 +37,7 @@ postscreen_access_list = permit_mynetworks, cidr:/opt/postfix/conf/postscreen_access.cidr, tcp:127.0.0.1:10027 postscreen_bare_newline_enable = no -postscreen_blacklist_action = drop +postscreen_denylist_action = drop postscreen_cache_cleanup_interval = 24h postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache postscreen_dnsbl_action = enforce diff --git a/docker-compose.yml b/docker-compose.yml index f2a939218..30a7ba96b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -339,7 +339,7 @@ services: - dovecot postfix-mailcow: - image: ghcr.io/mailcow/postfix:3.10.12 + image: ghcr.io/mailcow/postfix:3.10.12-1 depends_on: mysql-mailcow: condition: service_started From 6f7fee49cd7ba844dc924285de07166e5af86bd2 Mon Sep 17 00:00:00 2001 From: Mohamed Fall Date: Mon, 13 Jul 2026 15:29:48 +0000 Subject: [PATCH 30/68] fix: cors allowed origins settings validation --- data/web/api/openapi.yaml | 4 ++-- data/web/inc/functions.inc.php | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index 77c58b609..0aca69970 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -5856,7 +5856,7 @@ paths: response: value: - type: "success" - log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}] + log: ["cors", "edit", {"allowed_origins": ["*", "https://mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}] msg: "cors_headers_edited" description: OK headers: { } @@ -5873,7 +5873,7 @@ paths: schema: example: attr: - allowed_origins: ["*", "mail.mailcow.tld"] + allowed_origins: ["*", "https://mail.mailcow.tld"] allowed_methods: ["POST", "GET", "DELETE", "PUT"] properties: attr: diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 89f14b574..f9b0f6ab3 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -53,6 +53,28 @@ function valid_network($network) { function valid_hostname($hostname) { return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME); } +// Validates a browser-style Origin: scheme://host[:port], no path/query/fragment/credentials +function valid_origin($origin) { + $parts = parse_url($origin); + if ($parts === false || !isset($parts['scheme']) || !isset($parts['host'])) { + return false; + } + if (!in_array(strtolower($parts['scheme']), array('http', 'https'), true)) { + return false; + } + if (isset($parts['user']) || isset($parts['pass']) || isset($parts['path']) || isset($parts['query']) || isset($parts['fragment'])) { + return false; + } + $host = $parts['host']; + $host_without_brackets = trim($host, '[]'); + if (!valid_hostname($host_without_brackets) && !filter_var($host_without_brackets, FILTER_VALIDATE_IP)) { + return false; + } + // Reject anything that doesn't round-trip to the exact same origin (e.g. stray trailing slash) + $port = isset($parts['port']) ? ':' . $parts['port'] : ''; + $rebuilt = strtolower($parts['scheme']) . '://' . strtolower($host) . $port; + return strtolower($origin) === $rebuilt; +} // Thanks to https://stackoverflow.com/a/49373789 // Validates exact ip matches and ip-in-cidr, ipv4 and ipv6 function ip_acl($ip, $networks) { @@ -2283,10 +2305,13 @@ function cors($action, $data = null) { return false; } - $allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']); + $allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array(getBaseURL()); $allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins; - foreach ($allowed_origins as $origin) { - if (!filter_var($origin, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) && $origin != '*') { + foreach ($allowed_origins as &$origin) { + if ($origin === '*') { + continue; + } + if (!valid_origin($origin)) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $action, $data), @@ -2294,7 +2319,10 @@ function cors($action, $data = null) { ); return false; } + // browsers always send a lowercase scheme/host in the Origin header, so normalize to match + $origin = strtolower($origin); } + unset($origin); $allowed_methods = isset($data['allowed_methods']) ? $data['allowed_methods'] : array('GET', 'POST', 'PUT', 'DELETE'); $allowed_methods = !is_array($allowed_methods) ? array_map('trim', preg_split( "/( |,|;|\n)/", $allowed_methods)) : $allowed_methods; From fffe1aec3d16a2933cbfbbb30ceb8b4a2cd862dd Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Mon, 13 Jul 2026 19:16:10 +0200 Subject: [PATCH 31/68] postfix-tlspol: upgrade to trixie + 1.11.0 update --- data/Dockerfiles/postfix-tlspol/Dockerfile | 6 +++--- data/Dockerfiles/postfix-tlspol/syslog-ng-redis_slave.conf | 4 ++-- data/Dockerfiles/postfix-tlspol/syslog-ng.conf | 4 ++-- docker-compose.yml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) 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/docker-compose.yml b/docker-compose.yml index 30a7ba96b..b3cb8bef6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 From 0f06c8e5637ebe2ec1b95d8ef469a1f5a9c803c9 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Mon, 13 Jul 2026 19:34:58 +0200 Subject: [PATCH 32/68] postfix: prepare TLSRPT compatibility for tlspol --- data/conf/postfix/main.cf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 "*" From bd5c18b145707b1488411f2dafea8e0603e62047 Mon Sep 17 00:00:00 2001 From: Paul Seebach Date: Sat, 21 Mar 2026 15:54:34 +0100 Subject: [PATCH 33/68] [Web] Add show/hide password toggle on login pages Adds an eye icon button to the password field on the user, admin, and domain admin login pages. Clicking it toggles between hidden and visible password text, using Bootstrap Icons (bi-eye / bi-eye-slash). Closes #6893 --- data/web/templates/admin_index.twig | 1 + data/web/templates/domainadmin_index.twig | 1 + data/web/templates/user_index.twig | 1 + 3 files changed, 3 insertions(+) diff --git a/data/web/templates/admin_index.twig b/data/web/templates/admin_index.twig index 0dcb9145e..12e1b6cda 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/domainadmin_index.twig b/data/web/templates/domainadmin_index.twig index 2fc7bad98..3e76cf2e8 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/user_index.twig b/data/web/templates/user_index.twig index 28eb6389c..2e843e0d7 100644 --- a/data/web/templates/user_index.twig +++ b/data/web/templates/user_index.twig @@ -56,6 +56,7 @@
+
From 5408bc809d4304806fb1a54baf099e0bc9dd9e56 Mon Sep 17 00:00:00 2001 From: Paul Seebach Date: Sat, 21 Mar 2026 15:59:29 +0100 Subject: [PATCH 34/68] [Web] Use input-group-text for password toggle button Replaces btn-outline-secondary with input-group-text so the toggle button border matches the surrounding input group consistently. --- data/web/templates/admin_index.twig | 2 +- data/web/templates/domainadmin_index.twig | 2 +- data/web/templates/user_index.twig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/web/templates/admin_index.twig b/data/web/templates/admin_index.twig index 12e1b6cda..44b3564cc 100644 --- a/data/web/templates/admin_index.twig +++ b/data/web/templates/admin_index.twig @@ -49,7 +49,7 @@
- +
diff --git a/data/web/templates/domainadmin_index.twig b/data/web/templates/domainadmin_index.twig index 3e76cf2e8..af667756a 100644 --- a/data/web/templates/domainadmin_index.twig +++ b/data/web/templates/domainadmin_index.twig @@ -49,7 +49,7 @@
- +
diff --git a/data/web/templates/user_index.twig b/data/web/templates/user_index.twig index 2e843e0d7..a06cdb3f6 100644 --- a/data/web/templates/user_index.twig +++ b/data/web/templates/user_index.twig @@ -56,7 +56,7 @@
- +
From 7dab6c63d1daf37281c624a73d236a21cdf23934 Mon Sep 17 00:00:00 2001 From: Stephen Ritz <127270018+smpaz7467@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:54:28 -0700 Subject: [PATCH 35/68] [Nginx] only bind IPv6 default_server when ENABLE_IPV6 is set The HTTP-to-HTTPS redirect server block bound `listen [::]:{{ HTTP_PORT }} default_server` unconditionally, while every other IPv6 listen directive in this template is guarded by `{% if ENABLE_IPV6 %}`. On hosts where IPv6 is disabled at the kernel level, nginx cannot bind `::` and fails to start. This only triggers when HTTP_REDIRECT is enabled and ENABLE_IPV6 is false, which is why it survives testing with ENABLE_IPV6=false alone. Guard the directive like the other six IPv6 listeners in the template. Refs #7296 Co-Authored-By: Claude Opus 4.8 (1M context) --- data/conf/nginx/templates/nginx.conf.j2 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/conf/nginx/templates/nginx.conf.j2 b/data/conf/nginx/templates/nginx.conf.j2 index 08a85a144..8f7e5a24f 100644 --- a/data/conf/nginx/templates/nginx.conf.j2 +++ b/data/conf/nginx/templates/nginx.conf.j2 @@ -47,7 +47,9 @@ http { server { root /web; listen {{ HTTP_PORT }} default_server; + {% if ENABLE_IPV6 %} listen [::]:{{ HTTP_PORT }} default_server; + {%endif%} server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }}; From 4cba4486715c63e3b7727d26a1a92b6465a7619c Mon Sep 17 00:00:00 2001 From: Stephen Ritz <127270018+smpaz7467@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:11:17 -0700 Subject: [PATCH 36/68] [Web] translate password errors in the forced password change modal The forced password change modal posts to the JSON API and renders data[0].msg directly. The API returns raw language keys rather than translated strings, so a user who fails the complexity policy is shown the literal text "password_complexity" instead of a message. Every other password form renders errors through alertbox_log_parser(), which resolves the key against $lang. The modal is the only one that talks to the API directly, and it never resolved the key. Resolve it against lang_danger, which base.twig already exposes for this purpose. This also covers password_mismatch, password_empty and access_denied, and applies to the admin path as well. Unknown keys still fall through unchanged. The lookup is guarded with hasOwnProperty because msg is attacker- independent but dynamic: a bare lang_danger[msg] would resolve inherited Object.prototype members such as "constructor" to a function, which jQuery's .text() would then invoke as a callback. Language files are untouched: prerequisites.inc.php loads lang.en-gb.json as the base and merges the active locale over it, so locales that lack the key inherit the English string. Fixes #7301 Co-Authored-By: Claude Opus 4.8 (1M context) --- data/web/templates/base.twig | 4 ++++ 1 file changed, 4 insertions(+) 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); } }, From c17cc8a7922946434f84a3220e0f5ec32419cc4a Mon Sep 17 00:00:00 2001 From: Stephen Ritz <127270018+smpaz7467@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:23:31 -0700 Subject: [PATCH 37/68] [Web] fix add/time_limited_alias silently discarding requests and validity Three defects in add/time_limited_alias: The description was read as $_data['description'] without a guard. When a client omits it, null is bound to spamalias.description, which is TEXT NOT NULL, so the insert raises a PDOException. The global exception handler is terminal, so process_add_return() never echoes anything and the caller sees HTTP 200 with an empty body while no alias was created. Default it to an empty string instead. The validity guard used a single condition whose else branch also caught the success case, so every valid validity was overwritten with the 8760 hour default and the parameter did nothing. Only invalid values were rejected. Nest the range check so a valid value survives. The OpenAPI spec documented only username and domain, while the code also reads description, validity and permanent. Spec driven clients therefore could not construct a working request. Document all three. spamalias.description is the only NOT NULL description column in the schema, which is why the same unguarded read in add/domain and add/resource does not fail, both of those columns are nullable. This does not change the generic exception handling. A database error is still swallowed into an empty HTTP 200, and the message the handler builds carries the raw PDOException, so surfacing it to API clients would need sanitising first. That is left for a separate change. Refs #7287 Co-Authored-By: Claude Opus 4.8 (1M context) --- data/web/api/openapi.yaml | 12 ++++++++++++ data/web/inc/functions.mailbox.inc.php | 19 +++++++++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index 77c58b609..ee154c4da 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -185,6 +185,9 @@ paths: example: username: info@domain.tld domain: domain.tld + description: my time limited alias + validity: 8760 + permanent: false properties: username: description: 'the mailbox an alias should be created for' @@ -192,6 +195,15 @@ paths: domain: description: "the domain" type: string + description: + description: "a description for the alias, defaults to an empty string" + type: string + validity: + description: "how many hours the alias stays valid, 1 to 87600, defaults to 8760 (one year)" + type: integer + permanent: + description: "keep the alias after it expired, defaults to false" + type: boolean type: object summary: Create time limited alias /api/v1/add/app-passwd: diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index a9f17705d..3a002864d 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -41,13 +41,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { else { $username = $_SESSION['mailcow_cc_username']; } - if (isset($_data["validity"]) && !filter_var($_data["validity"], FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 87600)))) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'validity_missing' - ); - return false; + if (isset($_data["validity"])) { + if (!filter_var($_data["validity"], FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 87600)))) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'validity_missing' + ); + return false; + } } else { // Default to 1 yr @@ -60,7 +62,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $permanent = 0; } $domain = $_data['domain']; - $description = $_data['description']; + // spamalias.description is NOT NULL, an absent description must not become a null insert + $description = $_data['description'] ?? ''; $valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain']; $valid_alias_domains = user_get_alias_details($username)['alias_domains']; if (!empty($valid_alias_domains)) { From e5d0ed8c4c092f8a63b697c41e2f5408b9267028 Mon Sep 17 00:00:00 2001 From: Stephen Ritz <127270018+smpaz7467@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:33:46 -0700 Subject: [PATCH 38/68] [Web] use absolute RHS names in generated DNS zonefile The DNS overview "Download" produces a $ORIGIN zonefile, but the right-hand side of MX, CNAME and SRV records was emitted as a relative name. A target such as mail.example.net is then read relative to the origin and expands to mail.example.net.example.org., which is wrong. The only prior attempt at making names absolute was str_replace($domain, $domain . '.', ...), which appended a dot only to targets that happened to contain the origin domain, so cross-domain targets stayed relative. That same replace also corrupted any TXT value containing the origin (e.g. a DMARC rua=mailto:x@example.org became ...@example.org.). Absolutize the RHS per record type at export time only: MX and CNAME targets, and the SRV target token, get a trailing dot; ports, the SRV root target ".", IP addresses and TXT character strings are left as is. The records used for the on-page DNS validation are untouched, so matching against dns_get_record() output still works. Fixes #6984 Co-Authored-By: Claude Opus 4.8 (1M context) --- data/web/inc/ajax/dns_diagnostics.php | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/data/web/inc/ajax/dns_diagnostics.php b/data/web/inc/ajax/dns_diagnostics.php index 95e34e886..1987a88cd 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; } } } From 14772c3a204b14d3249936d56873a82a3870d47e Mon Sep 17 00:00:00 2001 From: Stephen Ritz <127270018+smpaz7467@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:36:40 -0700 Subject: [PATCH 39/68] [Web] return sender_acl in get/mailbox API sender_acl can be set through edit/mailbox but was never returned by get/mailbox, so an API client could not read back what it had written, and get/mailbox/all / get/mailbox/{mailbox} both omitted it. Add the mailbox's internal send-as ACL (the sender_acl table rows with external = 0) to mailbox_details as sender_acl, an array of send_as values, mirroring the field edit/mailbox accepts. It is added in the same block as the other detailed fields, so the lightweight get/mailbox/reduced endpoint is unaffected. Fixes #7011 Co-Authored-By: Claude Opus 4.8 (1M context) --- data/web/inc/functions.mailbox.inc.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index a9f17705d..ba2756a4f 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -5320,6 +5320,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $mailboxdata['rl_scope'] = 'domain'; } $mailboxdata['is_relayed'] = $row['backupmx']; + // Internal send-as ACL for this mailbox, mirrors the sender_acl field accepted by edit/mailbox + $mailboxdata['sender_acl'] = array(); + $stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `external` = '0'"); + $stmt->execute(array(':username' => $_data)); + $sender_acl_rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + foreach ($sender_acl_rows as $sender_acl_row) { + $mailboxdata['sender_acl'][] = $sender_acl_row['send_as']; + } } $stmt = $pdo->prepare("SELECT `tag_name` FROM `tags_mailbox` WHERE `username`= :username"); From 90ca1bf25ac356e02525f33a1c4ecc8980fadc03 Mon Sep 17 00:00:00 2001 From: Stephen Ritz <127270018+smpaz7467@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:39:19 -0700 Subject: [PATCH 40/68] [Web] redirect deep links to the matching login page An unauthenticated request to a deep link such as /admin/dashboard was redirected to /, the user login, instead of the admin login. protect_route always sent unauthenticated visitors to /. Pick the login page from the request path: /admin/* redirects to /admin, /domainadmin/* to /domainadmin, everything else to / as before. Fixes #7284 Co-Authored-By: Claude Opus 4.8 (1M context) --- data/web/inc/functions.inc.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 89f14b574..a796f1587 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -3503,7 +3503,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(); } From 4b62af9d023d5bda57f85293513631013eccd8df Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Wed, 15 Jul 2026 08:29:46 +0200 Subject: [PATCH 41/68] update README.md sponsors --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 40288f10e..8844c7482 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ A big thank you to everyone supporting us on GitHub Sponsors—your contribution + ### 50$/Month Sponsors Date: Mon, 20 Jul 2026 21:31:57 +0200 Subject: [PATCH 42/68] Update Dockerfile bump nginx version to 1.30.4 --- data/Dockerfiles/nginx/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/Dockerfiles/nginx/Dockerfile b/data/Dockerfiles/nginx/Dockerfile index 65b34838e..d3280a33d 100644 --- a/data/Dockerfiles/nginx/Dockerfile +++ b/data/Dockerfiles/nginx/Dockerfile @@ -1,4 +1,4 @@ -FROM nginx:1.30.3-alpine +FROM nginx:1.30.4-alpine LABEL maintainer "The Infrastructure Company GmbH " ENV PIP_BREAK_SYSTEM_PACKAGES=1 From 8c9524a2fe5d765e82a65565fffde967ba726306 Mon Sep 17 00:00:00 2001 From: SYNLINQ <69972447+SYNLINQ@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:32:57 +0200 Subject: [PATCH 43/68] Update docker-compose.yml reference patched nginx image --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 30a7ba96b..dab078c88 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -419,7 +419,7 @@ services: - php-fpm-mailcow - sogo-mailcow - rspamd-mailcow - image: ghcr.io/mailcow/nginx:1.30.3-1 + image: ghcr.io/mailcow/nginx:1.30.4 dns: - ${IPV4_NETWORK:-172.22.1}.254 environment: From ddd76d99cdf0ec36e5e83e0cc3939d1e1149bfd8 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:10:29 +0200 Subject: [PATCH 44/68] [Web] Add sogo_auth_internal nginx marker for sogo-auth.php --- data/conf/nginx/templates/nginx.conf.j2 | 5 +++++ data/conf/nginx/templates/sites-default.conf.j2 | 2 ++ data/web/sogo-auth.php | 8 ++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/data/conf/nginx/templates/nginx.conf.j2 b/data/conf/nginx/templates/nginx.conf.j2 index 08a85a144..aaf4a6fb3 100644 --- a/data/conf/nginx/templates/nginx.conf.j2 +++ b/data/conf/nginx/templates/nginx.conf.j2 @@ -42,6 +42,11 @@ http { https https; } + map $server_port $sogo_auth_internal { + 65510 "1"; + default ""; + } + {% if HTTP_REDIRECT %} # HTTP to HTTPS redirect server { diff --git a/data/conf/nginx/templates/sites-default.conf.j2 b/data/conf/nginx/templates/sites-default.conf.j2 index 84bd8eae4..688f2baa6 100644 --- a/data/conf/nginx/templates/sites-default.conf.j2 +++ b/data/conf/nginx/templates/sites-default.conf.j2 @@ -116,6 +116,8 @@ location ~ \.php$ { include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; + # trusted internal-auth marker; empty for external clients (see nginx.conf map) + fastcgi_param SOGO_AUTH_INTERNAL $sogo_auth_internal; fastcgi_read_timeout 3600; fastcgi_send_timeout 3600; } diff --git a/data/web/sogo-auth.php b/data/web/sogo-auth.php index 07456a7d1..e2431bfe4 100644 --- a/data/web/sogo-auth.php +++ b/data/web/sogo-auth.php @@ -8,8 +8,12 @@ $ALLOW_ADMIN_EMAIL_LOGIN = (preg_match( $session_var_user_allowed = 'sogo-sso-user-allowed'; $session_var_pass = 'sogo-sso-pass'; +// only the internal nginx auth_request loopback (127.0.0.1:65510) sets this; +// external clients can never supply it (bare fastcgi param, not an HTTP header) +$is_internal_auth = (($_SERVER['SOGO_AUTH_INTERNAL'] ?? '') === '1'); + // validate credentials for basic auth requests -if (isset($_SERVER['PHP_AUTH_USER'])) { +if ($is_internal_auth && isset($_SERVER['PHP_AUTH_USER'])) { // load prerequisites only when required require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; @@ -80,7 +84,7 @@ elseif (isset($_GET['login'])) { exit; } // only check for admin-login on sogo GUI requests -elseif (isset($_SERVER['HTTP_X_ORIGINAL_URI']) && strcasecmp(substr($_SERVER['HTTP_X_ORIGINAL_URI'], 0, 9), "/SOGo/so/") === 0) { +elseif ($is_internal_auth && isset($_SERVER['HTTP_X_ORIGINAL_URI']) && strcasecmp(substr($_SERVER['HTTP_X_ORIGINAL_URI'], 0, 9), "/SOGo/so/") === 0) { // this is an nginx auth_request call, we check for existing sogo-sso session variables require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/vars.inc.php'; if (file_exists($_SERVER['DOCUMENT_ROOT'] . '/inc/vars.local.inc.php')) { From 8e72d22c5664f498c7a22f2639e3c622076ced75 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:17:55 +0200 Subject: [PATCH 45/68] [Web] Escape rspamd_history and rllog --- data/web/js/site/dashboard.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/data/web/js/site/dashboard.js b/data/web/js/site/dashboard.js index aee361710..198a06392 100644 --- a/data/web/js/site/dashboard.js +++ b/data/web/js/site/dashboard.js @@ -1089,6 +1089,8 @@ jQuery(function($){ return str; }).join('
\n'); item.subject = escapeHtml(item.subject); + if (item.sender_mime != null) item.sender_mime = escapeHtml(item.sender_mime); + if (item['message-id'] != null) item['message-id'] = escapeHtml(item['message-id']); var scan_time = item.time_real.toFixed(3); if (item.time_virtual) { scan_time += ' / ' + item.time_virtual.toFixed(3); @@ -1212,6 +1214,11 @@ jQuery(function($){ }); } else if (table == 'rllog') { $.each(data, function (i, item) { + if (item.header_from != null) item.header_from = escapeHtml(item.header_from); + if (item.header_subject != null) item.header_subject = escapeHtml(item.header_subject); + if (item.from != null) item.from = escapeHtml(item.from); + if (item.rcpt != null) item.rcpt = escapeHtml(item.rcpt); + if (item.message_id != null) item.message_id = escapeHtml(item.message_id); if (item.user == null) { item.user = "none"; } From 145745329e02a2275554583091d6accdb5f17ece Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:01:44 +0200 Subject: [PATCH 46/68] [Web] add dot stuffing for quarantine raw release --- data/web/inc/functions.quarantine.inc.php | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/data/web/inc/functions.quarantine.inc.php b/data/web/inc/functions.quarantine.inc.php index 5df86ef33..c89b3e408 100644 --- a/data/web/inc/functions.quarantine.inc.php +++ b/data/web/inc/functions.quarantine.inc.php @@ -22,7 +22,7 @@ function quarantine($_action, $_data = null) { return false; } $stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt` - WHERE `qhash` = :hash + WHERE `qhash` = :hash AND user_acl.quarantine = 1 AND rcpt IN (SELECT username FROM mailbox)'); $stmt->execute(array(':hash' => $hash)); @@ -65,7 +65,7 @@ function quarantine($_action, $_data = null) { return false; } $stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt` - WHERE `qhash` = :hash + WHERE `qhash` = :hash AND `user_acl`.`quarantine` = 1 AND `username` IN (SELECT `username` FROM `mailbox`)'); $stmt->execute(array(':hash' => $hash)); @@ -170,6 +170,8 @@ function quarantine($_action, $_data = null) { } elseif ($release_format == 'raw') { $detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $detail_row['msg']); + // dot-stuffing per RFC5321 4.5.2: escape leading dots + $detail_row['msg'] = preg_replace('~^\.~m', '..', $detail_row['msg']); $postfix_talk = array( array('220', 'HELO quarantine' . chr(10)), array('250', 'MAIL FROM: ' . $sender . chr(10)), @@ -180,7 +182,7 @@ function quarantine($_action, $_data = null) { array('221', '') ); // Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php - $smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1); + $smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1); if (!$smtp_connection) { logger(array('return' => array( array( @@ -192,7 +194,7 @@ function quarantine($_action, $_data = null) { return false; } for ($i=0; $i < count($postfix_talk); $i++) { - $smtp_resource = fgets($smtp_connection, 256); + $smtp_resource = fgets($smtp_connection, 256); if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) { $ret = substr($smtp_resource, 0, 3); $ret = (empty($ret)) ? '-' : $ret; @@ -465,17 +467,19 @@ function quarantine($_action, $_data = null) { } elseif ($release_format == 'raw') { $row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $row['msg']); + // dot-stuffing per RFC5321 4.5.2: escape leading dots + $row['msg'] = preg_replace('~^\.~m', '..', $row['msg']); $postfix_talk = array( array('220', 'HELO quarantine' . chr(10)), array('250', 'MAIL FROM: ' . $sender . chr(10)), array('250', 'RCPT TO: ' . $row['rcpt'] . chr(10)), array('250', 'DATA' . chr(10)), - array('354', str_replace("\n.", '', $row['msg']) . chr(10) . '.' . chr(10)), + array('354', $row['msg'] . chr(10) . '.' . chr(10)), array('250', 'QUIT' . chr(10)), array('221', '') ); // Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php - $smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1); + $smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1); if (!$smtp_connection) { $_SESSION['return'][] = array( 'type' => 'warning', @@ -485,7 +489,7 @@ function quarantine($_action, $_data = null) { return false; } for ($i=0; $i < count($postfix_talk); $i++) { - $smtp_resource = fgets($smtp_connection, 256); + $smtp_resource = fgets($smtp_connection, 256); if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) { $ret = substr($smtp_resource, 0, 3); $ret = (empty($ret)) ? '-' : $ret; @@ -833,7 +837,7 @@ function quarantine($_action, $_data = null) { ))); return false; } - $stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash'); + $stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash'); $stmt->execute(array(':hash' => $hash)); return $stmt->fetch(PDO::FETCH_ASSOC); break; From 92cc8bec90e97f2559ed99bb922ea1544ddd2e30 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Thu, 23 Jul 2026 15:29:41 +0200 Subject: [PATCH 47/68] [Web] Use parameterized LIKE for sogo_acl deletion --- data/web/inc/functions.mailbox.inc.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index a9f17705d..77dc0af6c 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -5647,8 +5647,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt->execute(array( ':username' => $username )); - $stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $username . "/%' OR `c_uid` = :username"); + // bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match) + $c_object_like = '%/' . addcslashes($username, '\\%_') . '/%'; + $stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username"); $stmt->execute(array( + ':c_object_like' => $c_object_like, ':username' => $username )); $stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)"); @@ -6044,8 +6047,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt->execute(array( ':username' => $username )); - $stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . str_replace('%', '\%', $username) . "/%' OR `c_uid` = :username"); + // bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match) + $c_object_like = '%/' . addcslashes($username, '\\%_') . '/%'; + $stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username"); $stmt->execute(array( + ':c_object_like' => $c_object_like, ':username' => $username )); $stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)"); @@ -6201,8 +6207,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt->execute(array( ':username' => $name )); - $stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $name . "/%' OR `c_uid` = :username"); + // bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match) + $c_object_like = '%/' . addcslashes($name, '\\%_') . '/%'; + $stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username"); $stmt->execute(array( + ':c_object_like' => $c_object_like, ':username' => $name )); $stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)"); From cc9af65852bcd6af5abdf052f4a5902f57dd101e Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Fri, 24 Jul 2026 10:26:23 +0200 Subject: [PATCH 48/68] [Web] remove domain admin sso token after use --- data/web/inc/functions.domain_admin.inc.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/data/web/inc/functions.domain_admin.inc.php b/data/web/inc/functions.domain_admin.inc.php index 46b651b85..17aa4ad6a 100644 --- a/data/web/inc/functions.domain_admin.inc.php +++ b/data/web/inc/functions.domain_admin.inc.php @@ -429,14 +429,25 @@ function domain_admin_sso($_action, $_data) { switch ($_action) { case 'check': - $token = $_data; + $token = preg_replace('/[^a-zA-Z0-9-]/', '', $_data); $stmt = $pdo->prepare("SELECT `t1`.`username` FROM `da_sso` AS `t1` JOIN `admin` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL '30' SECOND) AND `t2`.`active` = 1 AND `t2`.`superadmin` = 0;"); $stmt->execute(array( - ':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token) + ':token' => $token )); $return = $stmt->fetch(PDO::FETCH_ASSOC); - return empty($return['username']) ? false : $return['username']; + if (empty($return['username'])) { + return false; + } + // single-use: consume the token; if a concurrent request already used it, deny + $del = $pdo->prepare("DELETE FROM `da_sso` WHERE `token` = :token"); + $del->execute(array( + ':token' => $token + )); + if ($del->rowCount() < 1) { + return false; + } + return $return['username']; case 'issue': if ($_SESSION['mailcow_cc_role'] != "admin") { $_SESSION['return'][] = array( From fea38c8e1bdb5f68c351c5e2a02daba8e9a1cc76 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:16:39 +0200 Subject: [PATCH 49/68] [Web] escape mailbox name --- data/web/js/site/mailbox.js | 1 + 1 file changed, 1 insertion(+) diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index 0a0192798..5ad5cceab 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -996,6 +996,7 @@ jQuery(function($){ 'style="min-width:2em;width:' + item.percent_in_use + '%">' + item.percent_in_use + '%' + '
' }; item.username = escapeHtml(item.username); + item.name = escapeHtml(item.name); if (Array.isArray(item.tags)){ var tags = ''; From c877fdf0a56ca4d62f0de1446acb1e26ae446c81 Mon Sep 17 00:00:00 2001 From: oidipos <32098983+oidipos@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:57:27 +0200 Subject: [PATCH 50/68] fix: remove MIME decoding of JSON encoded subject Since moving to rspamd's multipart metadata_exporter, `subject` is a JSON encoded UTF-8 string and must not be MIME decoded. --- data/conf/rspamd/meta_exporter/pipe.php | 2 +- data/conf/rspamd/meta_exporter/pushover.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/conf/rspamd/meta_exporter/pipe.php b/data/conf/rspamd/meta_exporter/pipe.php index 0a9ed59e2..28b06d3d6 100644 --- a/data/conf/rspamd/meta_exporter/pipe.php +++ b/data/conf/rspamd/meta_exporter/pipe.php @@ -53,7 +53,7 @@ $raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8"); $raw_size = (int)$_FILES['message']['size']; $qid = $meta['qid'] ?? 'unknown'; -$subject = iconv_mime_decode($meta['subject'] ?? ''); +$subject = $meta['subject'] ?? ''; $score = $meta['score'] ?? 0; $rcpts = $meta['rcpt'] ?? array(); $user = $meta['user'] ?? 'unknown'; diff --git a/data/conf/rspamd/meta_exporter/pushover.php b/data/conf/rspamd/meta_exporter/pushover.php index a2cd0995b..188d0a57b 100644 --- a/data/conf/rspamd/meta_exporter/pushover.php +++ b/data/conf/rspamd/meta_exporter/pushover.php @@ -50,7 +50,7 @@ $qid = $meta['qid'] ?? 'unknown'; $rcpts = $meta['rcpt'] ?? array(); $sender = $meta['from'] ?? ''; $ip = $meta['ip'] ?? 'unknown'; -$subject = iconv_mime_decode($meta['subject'] ?? ''); +$subject = $meta['subject'] ?? ''; $messageid= $meta['message_id'] ?? ''; $priority = 0; From e245ac04d9a074291d91deb4584e3a2e7c7f0f35 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:21:29 +0200 Subject: [PATCH 51/68] [Web] enforce tenant boundary for SOGo SSO --- data/web/sogo-auth.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/data/web/sogo-auth.php b/data/web/sogo-auth.php index e2431bfe4..bc9694d8e 100644 --- a/data/web/sogo-auth.php +++ b/data/web/sogo-auth.php @@ -54,6 +54,11 @@ elseif (isset($_GET['login'])) { (($_SESSION['acl']['login_as'] == "1" && $ALLOW_ADMIN_EMAIL_LOGIN !== 0) || ($is_dual === false && $login == $_SESSION['mailcow_cc_username']))) { if (filter_var($login, FILTER_VALIDATE_EMAIL)) { if (user_get_alias_details($login) !== false) { + // enforce tenant boundary + if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $login)) { + header("Location: /"); + exit; + } // Block SOGo access if pending actions (2FA setup, password update) if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { header("Location: /"); From b4bb1a625b787691d5ffd8ad9ae978e0163aff87 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:07:20 +0200 Subject: [PATCH 52/68] Update actions/stale action to v11 (#7375) --- .github/workflows/close_old_issues_and_prs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/close_old_issues_and_prs.yml b/.github/workflows/close_old_issues_and_prs.yml index bba04b0d1..13dbac279 100644 --- a/.github/workflows/close_old_issues_and_prs.yml +++ b/.github/workflows/close_old_issues_and_prs.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - name: Mark/Close Stale Issues and Pull Requests 🗑️ - uses: actions/stale@v10.4.0 + uses: actions/stale@v11.0.0 with: repo-token: ${{ secrets.STALE_ACTION_PAT }} days-before-stale: 60 From 54170d075a4cccd624809ff2e52356e3fb09238c Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:32:18 +0200 Subject: [PATCH 53/68] [Web] Move mailcow update check to server side --- data/web/inc/ajax/update_check.php | 67 ++++++++++++++++++++++++++++++ data/web/js/site/dashboard.js | 51 ++++++++--------------- 2 files changed, 85 insertions(+), 33 deletions(-) create mode 100644 data/web/inc/ajax/update_check.php diff --git a/data/web/inc/ajax/update_check.php b/data/web/inc/ajax/update_check.php new file mode 100644 index 000000000..6bf016001 --- /dev/null +++ b/data/web/inc/ajax/update_check.php @@ -0,0 +1,67 @@ + 'error', 'message' => 'access denied')); + exit; +} + +$owner = $GLOBALS['MAILCOW_GIT_OWNER']; +$repo = $GLOBALS['MAILCOW_GIT_REPO']; +$current = $GLOBALS['MAILCOW_GIT_VERSION']; + +// Cache key is bound to the running version, so an update invalidates it naturally +$cache_key = 'MAILCOW_UPDATE_CHECK/' . $current; + +// Serve cached result if present (one GitHub call per TTL, not one per page load) +$cached = $redis->get($cache_key); +if ($cached !== false) { + echo $cached; + exit; +} + +// GitHub requires a User-Agent and rate-limits unauthenticated requests to 60/h per IP +function github_get($url) { + $ch = curl_init($url); + curl_setopt_array($ch, array( + CURLOPT_RETURNTRANSFER => true, + CURLOPT_USERAGENT => 'mailcow-update-check', + CURLOPT_TIMEOUT => 10, + CURLOPT_HTTPHEADER => array('Accept: application/vnd.github+json'), + )); + $body = curl_exec($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + if ($body === false || $code !== 200) return false; + $json = json_decode($body, true); + return is_array($json) ? $json : false; +} + +$latest = github_get("https://api.github.com/repos/{$owner}/{$repo}/releases/latest"); +$release = github_get("https://api.github.com/repos/{$owner}/{$repo}/releases/tags/{$current}"); + +// Any failure (rate limit, offline, unexpected payload) -> honest error, cached briefly +if ($latest === false || $release === false || + empty($latest['tag_name']) || empty($latest['created_at']) || empty($release['created_at'])) { + $result = json_encode(array('status' => 'error', 'message' => 'could not reach GitHub API')); + $redis->setex($cache_key, 300, $result); + echo $result; + exit; +} + +$date_latest = strtotime($latest['created_at']); +$date_current = strtotime($release['created_at']); + +if ($date_latest <= $date_current) { + $result = json_encode(array('status' => 'no_update')); +} else { + $result = json_encode(array('status' => 'update_available', 'latest_tag' => $latest['tag_name'])); +} + +// Cache the positive result for an hour +$redis->setex($cache_key, 3600, $result); +echo $result; diff --git a/data/web/js/site/dashboard.js b/data/web/js/site/dashboard.js index aee361710..eb50d698b 100644 --- a/data/web/js/site/dashboard.js +++ b/data/web/js/site/dashboard.js @@ -30,7 +30,7 @@ $(document).ready(function() { // create host cpu and mem charts createHostCpuAndMemChart(); // check for new version - if (mailcow_info.branch === "master"){ + if (mailcow_info.branch === "master" && mailcow_cc_role === "admin"){ check_update(mailcow_info.version_tag, mailcow_info.project_url); } $("#mailcow_version").click(function(){ @@ -1669,41 +1669,26 @@ function createHostCpuAndMemChart(){ function check_update(current_version, github_repo_url){ if (!current_version || !github_repo_url) return false; - var github_account = github_repo_url.split("/")[3]; - var github_repo_name = github_repo_url.split("/")[4]; - // get details about latest release - window.fetch("https://api.github.com/repos/"+github_account+"/"+github_repo_name+"/releases/latest", {method:'GET',cache:'no-cache'}).then(function(response) { + window.fetch("/inc/ajax/update_check.php", {method:'GET',cache:'no-cache'}).then(function(response) { + if (!response.ok) throw new Error("update check returned " + response.status); return response.json(); - }).then(function(latest_data) { - // get details about current release - window.fetch("https://api.github.com/repos/"+github_account+"/"+github_repo_name+"/releases/tags/"+current_version, {method:'GET',cache:'no-cache'}).then(function(response) { - return response.json(); - }).then(function(current_data) { - // compare releases - var date_current = new Date(current_data.created_at); - var date_latest = new Date(latest_data.created_at); - if (date_latest.getTime() <= date_current.getTime()){ - // no update available - $("#mailcow_update").removeClass("text-warning text-danger").addClass("text-success"); - $("#mailcow_update").html("" + lang_debug.no_update_available + ""); - } else { - // update available - $("#mailcow_update").removeClass("text-danger text-success").addClass("text-warning"); - $("#mailcow_update").html(lang_debug.update_available + ` `+latest_data.tag_name+``); - $("#mailcow_update_changelog").click(function(){ - if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin") - return; + }).then(function(data) { + if (data.status === "no_update") { + $("#mailcow_update").removeClass("text-warning text-danger").addClass("text-success"); + $("#mailcow_update").html("" + lang_debug.no_update_available + ""); + } else if (data.status === "update_available") { + $("#mailcow_update").removeClass("text-danger text-success").addClass("text-warning"); + $("#mailcow_update").html(lang_debug.update_available + ` `+data.latest_tag+``); + $("#mailcow_update_changelog").click(function(){ + if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin") + return; - showVersionModal("New Release " + latest_data.tag_name, latest_data.tag_name); - }) - } - }).catch(err => { - // err - console.log(err); - $("#mailcow_update").removeClass("text-success text-warning").addClass("text-danger"); - $("#mailcow_update").html(""+ lang_debug.update_failed +""); - }); + showVersionModal("New Release " + data.latest_tag, data.latest_tag); + }) + } else { + throw new Error(data.message || "update check failed"); + } }).catch(err => { // err console.log(err); From f44bd2f36a802d2a2d948c579fb34948e8181038 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:19:45 +0200 Subject: [PATCH 54/68] [Web] document sender_acl in get/mailbox API examples --- data/web/api/openapi.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index ee154c4da..093f9dd51 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -4920,6 +4920,7 @@ paths: quota: 3221225472 quota_used: 0 rl: false + sender_acl: ["otherbox@doman3.tld", "@aliasdomain.tld"] spam_aliases: 0 username: info@doman3.tld tags: ["tag1", "tag2"] @@ -5846,6 +5847,7 @@ paths: quota: 3221225472 quota_used: 0 rl: false + sender_acl: ["otherbox@domain3.tld", "@aliasdomain.tld"] spam_aliases: 0 username: info@domain3.tld tags: ["tag1", "tag2"] From 9cd16f00019f96d5e202db07a36e1e8161baf048 Mon Sep 17 00:00:00 2001 From: Stephen Ritz <127270018+smpaz7467@users.noreply.github.com> Date: Tue, 28 Jul 2026 07:12:17 -0700 Subject: [PATCH 55/68] [Web] show external sender addresses regardless of authsource The extended_sender_acl field sat inside the `{% if not result.authsource or result.authsource == 'mailcow' %}` block that hides the local password fields for mailboxes authenticating against an external identity provider. As a result the "External sender addresses" input was not rendered at all for keycloak / generic-oidc / ldap mailboxes, even for a full admin, so those addresses could neither be reviewed nor edited in the UI while the underlying sender_acl rows stayed active. Extended sender ACLs are unrelated to local password management. Move the field out of that block; it stays gated by acl.extend_sender_acl as intended. Fixes #7365 --- data/web/templates/edit/mailbox.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 %}
From 36c70db86cea54dcf810343c8d6b2cc10435ffe9 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:37:18 +0200 Subject: [PATCH 56/68] [Web] harden CORS origin matching and add Vary: Origin --- data/web/inc/functions.inc.php | 38 ++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index f9b0f6ab3..dfd860a9b 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -75,6 +75,17 @@ function valid_origin($origin) { $rebuilt = strtolower($parts['scheme']) . '://' . strtolower($host) . $port; return strtolower($origin) === $rebuilt; } +// Bring an origin into the exact form a browser sends it (scheme://host[:port], lowercase). +function normalize_cors_origin($origin) { + $origin = trim($origin); + if ($origin === '*') { + return '*'; + } + if ($origin !== '' && !preg_match('~^[a-z][a-z0-9+.-]*://~i', $origin)) { + $origin = 'https://' . $origin; + } + return valid_origin($origin) ? strtolower($origin) : false; +} // Thanks to https://stackoverflow.com/a/49373789 // Validates exact ip matches and ip-in-cidr, ipv4 and ipv6 function ip_acl($ip, $networks) { @@ -2370,26 +2381,35 @@ function cors($action, $data = null) { ); } - $cors_settings = !$cors_settings ? array('allowed_origins' => $_SERVER['SERVER_NAME'], 'allowed_methods' => 'GET, POST, PUT, DELETE') : $cors_settings; - $cors_settings['allowed_origins'] = empty($cors_settings['allowed_origins']) ? $_SERVER['SERVER_NAME'] : $cors_settings['allowed_origins']; + $cors_settings = !$cors_settings ? array('allowed_origins' => getBaseURL(), 'allowed_methods' => 'GET, POST, PUT, DELETE') : $cors_settings; + $cors_settings['allowed_origins'] = empty($cors_settings['allowed_origins']) ? getBaseURL() : $cors_settings['allowed_origins']; $cors_settings['allowed_methods'] = empty($cors_settings['allowed_methods']) ? 'GET, POST, PUT, DELETE, OPTION' : $cors_settings['allowed_methods']; return $cors_settings; break; case "set_headers": $cors_settings = cors('get'); + // normalize the stored list; it may still hold bare hostnames written before origins were validated as origins + $allowed_origins = array_filter(array_map('normalize_cors_origin', explode(',', $cors_settings['allowed_origins']))); + $origin = isset($_SERVER['HTTP_ORIGIN']) ? normalize_cors_origin($_SERVER['HTTP_ORIGIN']) : false; // check if requested origin is in allowed origins - $allowed_origins = explode(', ', $cors_settings['allowed_origins']); - $cors_settings['allowed_origins'] = $allowed_origins[0]; - if (in_array('*', $allowed_origins)){ - $cors_settings['allowed_origins'] = '*'; - } else if (array_key_exists('HTTP_ORIGIN', $_SERVER) && in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) { - $cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN']; + $allow_origin = null; + if (in_array('*', $allowed_origins, true)) { + $allow_origin = '*'; + } else if ($origin !== false && in_array($origin, $allowed_origins, true)) { + $allow_origin = $origin; } // always allow OPTIONS for preflight request $cors_settings["allowed_methods"] = empty($cors_settings["allowed_methods"]) ? 'OPTIONS' : $cors_settings["allowed_methods"] . ', ' . 'OPTIONS'; - header('Access-Control-Allow-Origin: ' . $cors_settings['allowed_origins']); + // a disallowed origin gets no Access-Control-Allow-Origin at all; echoing a different origin than the requesting one tells a browser nothing + if ($allow_origin !== null) { + header('Access-Control-Allow-Origin: ' . $allow_origin); + } + if ($allow_origin !== '*') { + // the response depends on the request origin, keep caches from mixing them up + header('Vary: Origin', false); + } header('Access-Control-Allow-Methods: '. $cors_settings['allowed_methods']); header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin'); From 7036dbf4f855ff238c9a8a57e957f7ee4178ca81 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:50:18 +0200 Subject: [PATCH 57/68] [Rspamd] update to 4.1.4 --- data/Dockerfiles/rspamd/Dockerfile | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index d37d1226a..13e56357b 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -2,7 +2,7 @@ FROM debian:trixie-slim LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ARG RSPAMD_VER=rspamd_4.1.0-1~e2b0b18 +ARG RSPAMD_VER=rspamd_4.1.4-1~beb659b ARG CODENAME=trixie ENV LC_ALL=C diff --git a/docker-compose.yml b/docker-compose.yml index dab078c88..8b24b23c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -84,7 +84,7 @@ services: - clamd rspamd-mailcow: - image: ghcr.io/mailcow/rspamd:4.1.0-1 + image: ghcr.io/mailcow/rspamd:4.1.4-1 stop_grace_period: 30s depends_on: - dovecot-mailcow From bd037d5644fa133e60909ef17d743019f67a244d Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Thu, 30 Jul 2026 08:51:21 +0200 Subject: [PATCH 58/68] [Nginx] Set image tag to 1.30.4-1 --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index dab078c88..a2bcae9db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -419,7 +419,7 @@ services: - php-fpm-mailcow - sogo-mailcow - rspamd-mailcow - image: ghcr.io/mailcow/nginx:1.30.4 + image: ghcr.io/mailcow/nginx:1.30.4-1 dns: - ${IPV4_NETWORK:-172.22.1}.254 environment: From d64c923aca1d80f8e2b73b0664500399a60542e2 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:33:55 +0200 Subject: [PATCH 59/68] [ACME] Skip mta-sts certificate request when MTA-STS is not active for a domain --- data/Dockerfiles/acme/acme.sh | 21 +++++++++++++++++++++ data/web/lang/lang.de-de.json | 1 + data/web/lang/lang.en-gb.json | 1 + data/web/templates/edit/domain.twig | 2 +- docker-compose.yml | 2 +- 5 files changed, 25 insertions(+), 2 deletions(-) diff --git a/data/Dockerfiles/acme/acme.sh b/data/Dockerfiles/acme/acme.sh index 271de4fc9..6b7b74300 100755 --- a/data/Dockerfiles/acme/acme.sh +++ b/data/Dockerfiles/acme/acme.sh @@ -249,12 +249,33 @@ while true; do done <<< "${SQL_DOMAINS}" if [[ ${ONLY_MAILCOW_HOSTNAME} != "y" ]]; then + # Fetch all domains with an active MTA-STS policy once. + unset MTA_STS_ACTIVE_DOMAINS + declare -A MTA_STS_ACTIVE_DOMAINS + if [[ ${AUTODISCOVER_SAN} == "y" ]]; then + SQL_MTA_STS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM mta_sts WHERE active = 1" -Bs) + if [[ $? -eq 0 ]]; then + while read mta_sts_domain; do + if [[ -z "${mta_sts_domain}" ]]; then + # ignore empty lines + continue + fi + MTA_STS_ACTIVE_DOMAINS["${mta_sts_domain}"]=1 + done <<< "${SQL_MTA_STS_DOMAINS}" + fi + fi for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do unset VALIDATED_CONFIG_DOMAINS_SUBDOMAINS declare -a VALIDATED_CONFIG_DOMAINS_SUBDOMAINS for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do FULL_SUBDOMAIN="${SUBDOMAIN}.${SQL_DOMAIN}" + # Skip mta-sts subdomain unless MTA-STS is enabled (active) for this domain + if [[ "${SUBDOMAIN}" == "mta-sts" && -z "${MTA_STS_ACTIVE_DOMAINS[${SQL_DOMAIN}]}" ]]; then + log_f "MTA-STS is not enabled for ${SQL_DOMAIN} - skipping mta-sts subdomain certificate" + continue + fi + # Skip if subdomain matches MAILCOW_HOSTNAME if [[ "${FULL_SUBDOMAIN}" == "${MAILCOW_HOSTNAME}" ]]; then continue diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index c19eb209b..28ad0d72b 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -728,6 +728,7 @@ "mta_sts_mx": "MX-Server", "mta_sts_mx_info": "Erlaubt das Senden nur an explizit aufgeführte Mailserver-Hostnamen; der sendende MTA überprüft, ob der DNS-MX-Hostname mit der Richtlinienliste übereinstimmt, und erlaubt die Zustellung nur mit einem gültigen TLS-Zertifikat (schützt vor MITM).", "mta_sts_mx_notice": "Es können mehrere MX-Server angegeben werden (durch Kommas getrennt).", + "mta_sts_active_info": "Wenn nicht angehakt, wird die MTA-STS-Richtlinie nicht veröffentlicht und für die mta-sts-Subdomain kein Zertifikat über ACME angefordert.", "multiple_bookings": "Mehrfaches Buchen", "nexthop": "Next Hop", "none_inherit": "Keine Auswahl / Erben", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 873d7b98d..b8a11e5a5 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -728,6 +728,7 @@ "mta_sts_mx": "MX server", "mta_sts_mx_info": "Allows sending only to explicitly listed mail server hostnames; the sending MTA checks if the DNS MX hostname matches the policy list, and only allows delivery with a valid TLS certificate (guards against MITM).", "mta_sts_mx_notice": "Multiple MX servers can be specified (separated by commas).", + "mta_sts_active_info": "If unchecked, the MTA-STS policy will not be published and no certificate will be requested for the mta-sts subdomain via ACME.", "multiple_bookings": "Multiple bookings", "none_inherit": "None / Inherit", "nexthop": "Next hop", diff --git a/data/web/templates/edit/domain.twig b/data/web/templates/edit/domain.twig index 83353079b..45ec654ce 100644 --- a/data/web/templates/edit/domain.twig +++ b/data/web/templates/edit/domain.twig @@ -340,7 +340,7 @@
- +
diff --git a/docker-compose.yml b/docker-compose.yml index 76359151b..4742fef17 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -465,7 +465,7 @@ services: condition: service_started unbound-mailcow: condition: service_healthy - image: ghcr.io/mailcow/acme:1.97 + image: ghcr.io/mailcow/acme:1.98 dns: - ${IPV4_NETWORK:-172.22.1}.254 environment: From 54da526d418f0bef3f4e9160847dff87cfab0bd6 Mon Sep 17 00:00:00 2001 From: MaximalBenedikt Date: Thu, 30 Jul 2026 14:11:32 +0200 Subject: [PATCH 60/68] update README.md sponsors --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8844c7482..63d0b6480 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ A big thank you to everyone supporting us on GitHub Sponsors—your contribution ### 50$/Month Sponsors ## Info, documentation and support From f7a536d63463236973898a2fde7fb686ca82683e Mon Sep 17 00:00:00 2001 From: milkmaker Date: Tue, 4 Aug 2026 18:35:48 +0200 Subject: [PATCH 61/68] update postscreen_access.cidr (#7394) --- data/conf/postfix/postscreen_access.cidr | 68 ++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/data/conf/postfix/postscreen_access.cidr b/data/conf/postfix/postscreen_access.cidr index 6fcedff95..3801fe31e 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 Wed Jul 1 00:51:20 UTC 2026 +# Whitelist generated by Postwhite v3.4 on Sat Aug 1 00:35:20 UTC 2026 # https://github.com/stevejenkins/postwhite/ -# 2251 total rules +# 2256 total rules 2a00:1450:4000::/36 permit 2a00:1450:4864::/56 permit 2a01:111:f400::/48 permit @@ -29,6 +29,7 @@ 2a01:b747:3004:200::/56 permit 2a01:b747:3005:200::/56 permit 2a01:b747:3006:200::/56 permit +2a01:b747:3007::/56 permit 2a02:a60:0:5::/64 permit 2a0f:f640::/56 permit 2c0f:fb50:4000::/36 permit @@ -56,7 +57,14 @@ 8.25.194.0/23 permit 8.25.196.0/23 permit 8.36.116.0/24 permit +8.39.54.0/23 permit +8.39.54.240/29 permit +8.39.54.250/31 permit 8.39.144.0/24 permit +8.40.222.0/23 permit +8.40.222.240/29 permit +8.40.222.250/31 permit +8.47.10.9 permit 12.130.86.238 permit 13.108.16.0/20 permit 13.110.208.0/21 permit @@ -67,6 +75,7 @@ 13.111.191.0/24 permit 13.216.7.111 permit 13.216.54.180 permit +13.247.164.219 permit 15.200.21.50 permit 15.200.44.248 permit 15.200.201.185 permit @@ -170,6 +179,7 @@ 34.215.104.144 permit 34.218.115.239 permit 34.225.212.172 permit +34.241.242.183 permit 35.83.148.184 permit 35.155.198.111 permit 35.158.23.94 permit @@ -193,6 +203,7 @@ 40.233.64.216 permit 40.233.83.78 permit 40.233.88.28 permit +43.239.212.33 permit 44.206.138.57 permit 44.210.169.44 permit 44.217.45.156 permit @@ -275,6 +286,7 @@ 51.83.17.38 permit 52.1.14.157 permit 52.5.230.59 permit +52.6.74.205 permit 52.12.53.23 permit 52.13.214.179 permit 52.26.1.71 permit @@ -331,6 +343,7 @@ 54.244.54.130 permit 54.244.242.0/24 permit 54.255.61.23 permit +56.124.6.228 permit 57.103.64.0/18 permit 57.129.93.249 permit 62.13.128.0/24 permit @@ -397,6 +410,7 @@ 65.110.161.77 permit 65.123.29.213 permit 65.123.29.220 permit +65.154.166.0/24 permit 65.212.180.36 permit 66.102.0.0/20 permit 66.119.150.192/26 permit @@ -1210,6 +1224,9 @@ 99.78.197.208/28 permit 103.9.96.0/22 permit 103.28.42.0/24 permit +103.84.217.15 permit +103.84.217.238 permit +103.89.75.238 permit 103.151.192.0/23 permit 103.168.172.128/27 permit 103.237.104.0/22 permit @@ -1370,6 +1387,10 @@ 117.120.16.0/21 permit 119.42.242.52/31 permit 119.42.242.156 permit +121.244.91.31 permit +121.244.91.48 permit +121.244.91.52 permit +122.15.156.182 permit 123.126.78.64/29 permit 124.108.96.24/31 permit 124.108.96.28/31 permit @@ -1452,7 +1473,21 @@ 134.170.141.64/26 permit 134.170.143.0/24 permit 134.170.174.0/24 permit +135.84.80.0/24 permit +135.84.81.0/24 permit +135.84.82.0/24 permit +135.84.83.0/24 permit 135.84.216.0/22 permit +136.143.160.0/24 permit +136.143.161.0/24 permit +136.143.162.0/24 permit +136.143.176.0/24 permit +136.143.177.0/24 permit +136.143.178.49 permit +136.143.182.0/23 permit +136.143.184.0/24 permit +136.143.188.0/24 permit +136.143.190.0/23 permit 136.146.128.0/20 permit 136.147.128.0/20 permit 136.147.135.0/24 permit @@ -1472,6 +1507,7 @@ 139.138.46.219 permit 139.138.57.55 permit 139.138.58.119 permit +139.167.79.86 permit 139.177.108.0/25 permit 139.180.17.0/24 permit 140.238.148.191 permit @@ -1528,7 +1564,7 @@ 149.97.173.180 permit 149.118.160.128/25 permit 150.136.21.199 permit -150.171.109.183 permit +150.171.109.72 permit 150.230.98.160 permit 151.145.38.14 permit 152.67.105.195 permit @@ -1643,6 +1679,19 @@ 168.245.12.252 permit 168.245.46.9 permit 168.245.127.231 permit +169.148.129.0/24 permit +169.148.131.0/24 permit +169.148.138.0/24 permit +169.148.142.10 permit +169.148.142.33 permit +169.148.144.0/25 permit +169.148.144.10 permit +169.148.146.0/23 permit +169.148.174.10 permit +169.148.175.3 permit +169.148.179.3 permit +169.148.188.0/24 permit +169.148.188.182 permit 170.9.232.254 permit 170.10.128.0/24 permit 170.10.129.0/24 permit @@ -1849,7 +1898,16 @@ 199.16.156.0/22 permit 199.33.145.1 permit 199.33.145.32 permit +199.34.22.36 permit 199.59.148.0/22 permit +199.67.80.2 permit +199.67.80.20 permit +199.67.82.2 permit +199.67.82.20 permit +199.67.84.0/24 permit +199.67.86.0/24 permit +199.67.88.0/24 permit +199.67.90.0/24 permit 199.101.161.130 permit 199.101.162.0/25 permit 199.122.120.0/21 permit @@ -1905,6 +1963,8 @@ 204.92.114.187 permit 204.92.114.203 permit 204.92.114.204/31 permit +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.168.0/21 permit @@ -2178,7 +2238,7 @@ 2001:748:400:3301::4 permit 2404:6800:4000::/36 permit 2404:6800:4864::/56 permit -2603:1061:14:1c2::1 permit +2603:1061:14:75::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 From d1a2f4e168ed1a0ba0b9019c3e6b6ae13eb3382f Mon Sep 17 00:00:00 2001 From: milkmaker Date: Tue, 4 Aug 2026 18:37:38 +0200 Subject: [PATCH 62/68] Translations update from Weblate (#7400) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Web] Updated lang.si-si.json Co-authored-by: Matjaž Tekavec Co-authored-by: milkmaker * [Web] Updated lang.pt-br.json Co-authored-by: André Glazastov --------- Co-authored-by: Matjaž Tekavec Co-authored-by: André Glazastov --- data/web/lang/lang.pt-br.json | 8 +++++--- data/web/lang/lang.si-si.json | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/data/web/lang/lang.pt-br.json b/data/web/lang/lang.pt-br.json index db3c8cac2..5645537c6 100644 --- a/data/web/lang/lang.pt-br.json +++ b/data/web/lang/lang.pt-br.json @@ -786,7 +786,8 @@ "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).", "sender_allowed": "Permitir enviar como este alias", - "sender_allowed_info": "Se desativado, este alias poderá apenas receber e-mails. Use a ACL de remetente para substituir essa configuração e conceder a caixas de correio específicas permissão para enviar." + "sender_allowed_info": "Se desativado, este alias poderá apenas receber e-mails. Use a ACL de remetente para substituir essa configuração e conceder a caixas de correio específicas permissão para enviar.", + "mta_sts_active_info": "Se esta opção estiver desmarcada, a política MTA-STS não será publicada e nenhum certificado será solicitado via ACME para o subdomínio mta-sts." }, "fido2": { "confirm": "Confirme", @@ -1034,7 +1035,8 @@ "weekly": "Semanalmente", "yes": "✓", "iam": "Provedor de Identidade", - "internal": "Interno" + "internal": "Interno", + "force_tfa": "A2F" }, "oauth2": { "access_denied": "Faça login como proprietário da mailbox para conceder acesso via OAuth2.", @@ -1086,7 +1088,7 @@ "rspamd_result": "Resultado do Rspamd", "sender": "Remetente (SMTP)", "sender_header": "Remetente (cabeçalho “De”)", - "settings_info": "Quantidade máxima de elementos a serem colocados em quarentena: %s
Tamanho máximo do e-mail: %s MiB", + "settings_info": "Número máximo de elementos em quarentena (por caixa de correio): %s
Tamanho máximo do e-mail: %s MiB", "show_item": "Mostrar item", "spam": "Spam", "spam_score": "Ponto", diff --git a/data/web/lang/lang.si-si.json b/data/web/lang/lang.si-si.json index 39d57eb31..2deb82712 100644 --- a/data/web/lang/lang.si-si.json +++ b/data/web/lang/lang.si-si.json @@ -1078,7 +1078,7 @@ "rspamd_result": "Rezultat Rspamd", "sender": "Pošiljatelj (SMTP)", "sender_header": "Pošiljatelj (glava »Od«)", - "settings_info": "Največje število elementov za karanteno: %s
Največja velikost e-pošte: %s MiB", + "settings_info": "Največje število elementov za karanteno (na poštni predal): %s
Največja velikost e-pošte: %s MiB", "show_item": "Prikaži element", "spam": "Neželena pošta", "subj": "Zadeva", From b5fa926bc043a36faabc1fba83f4829be7e009fb Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Thu, 13 Aug 2026 13:56:49 +0200 Subject: [PATCH 63/68] [Clamd] Update to 1.4.6 --- data/Dockerfiles/clamd/Dockerfile | 6 +++--- docker-compose.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/data/Dockerfiles/clamd/Dockerfile b/data/Dockerfiles/clamd/Dockerfile index e60e7eef1..5f444a8f4 100644 --- a/data/Dockerfiles/clamd/Dockerfile +++ b/data/Dockerfiles/clamd/Dockerfile @@ -1,7 +1,7 @@ -FROM alpine:3.21 AS builder +FROM alpine:3.24 AS builder WORKDIR /src -ENV CLAMD_VERSION=1.4.2 +ENV CLAMD_VERSION=1.4.6 RUN apk upgrade --no-cache \ && apk add --update --no-cache \ @@ -68,7 +68,7 @@ RUN wget -P /src https://www.clamav.net/downloads/production/clamav-${CLAMD_VERS "/clamav/etc/clamav/clamav-milter.conf.sample" > "/clamav/etc/clamav/clamav-milter.conf" || exit 1 -FROM alpine:3.21 +FROM alpine:3.24 LABEL maintainer = "The Infrastructure Company GmbH " diff --git a/docker-compose.yml b/docker-compose.yml index 4742fef17..07c738e4c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,7 +65,7 @@ services: - redis clamd-mailcow: - image: ghcr.io/mailcow/clamd:1.71 + image: ghcr.io/mailcow/clamd:1.4.6-1 restart: always depends_on: unbound-mailcow: From 8c85044781163a94a5af13410ed965002bd8eecd Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Mon, 17 Aug 2026 08:20:45 +0200 Subject: [PATCH 64/68] [SOGo] Update to 5.12.10 --- data/Dockerfiles/sogo/Dockerfile | 4 ++-- docker-compose.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index b4b401e8c..f65ec7ebd 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -12,8 +12,8 @@ FROM debian:bookworm LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ARG SOGO_VERSION=SOGo-5.12.9 -ARG SOPE_VERSION=SOPE-5.12.9 +ARG SOGO_VERSION=SOGo-5.12.10 +ARG SOPE_VERSION=SOPE-5.12.10 # 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/docker-compose.yml b/docker-compose.yml index 07c738e4c..66455c103 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -200,7 +200,7 @@ services: - phpfpm sogo-mailcow: - image: ghcr.io/mailcow/sogo:5.12.9-1 + image: ghcr.io/mailcow/sogo:5.12.10-1 environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} From feed5ad1831405ac2714a9e3c96eff6e91a53a41 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Mon, 17 Aug 2026 08:35:53 +0200 Subject: [PATCH 65/68] [Dovecot] Remove legacy DeltaChat auto-filing sieve rule --- data/conf/dovecot/global_sieve_before | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/data/conf/dovecot/global_sieve_before b/data/conf/dovecot/global_sieve_before index 3e79ca1d3..e71be5100 100644 --- a/data/conf/dovecot/global_sieve_before +++ b/data/conf/dovecot/global_sieve_before @@ -1,13 +1,3 @@ # global_sieve_before script # global_sieve_before -> user sieve_before (mailcow UI) -> user sieve_after (mailcow UI) -> global_sieve_after -require ["mailbox", "fileinto"]; - -if header :contains ["Chat-Version"] [""] { - if mailboxexists "DeltaChat" { - fileinto "DeltaChat"; - } else { - fileinto :create "DeltaChat"; - } - stop; -} From 555e7ed706aeb8863109e0b2cd2fe0df68502000 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:12:32 +0200 Subject: [PATCH 66/68] [Redis] Update to 7.4.10 --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 66455c103..7dce45691 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -42,7 +42,7 @@ services: - mysql redis-mailcow: - image: redis:7.4.6-alpine + image: redis:7.4.10-alpine entrypoint: ["/bin/sh","/redis-conf.sh"] volumes: - redis-vol-1:/data/ From 91d3b7246a53247feaa19350f8ec330423c6d909 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:08:17 +0200 Subject: [PATCH 67/68] [Web] Minor hardening across web UI and nginx --- data/conf/nginx/templates/sites-default.conf.j2 | 1 + data/web/inc/ajax/dns_diagnostics.php | 12 ++++++------ data/web/inc/functions.inc.php | 2 +- data/web/js/site/dashboard.js | 2 ++ data/web/oauth/authorize.php | 6 ++++++ 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/data/conf/nginx/templates/sites-default.conf.j2 b/data/conf/nginx/templates/sites-default.conf.j2 index 688f2baa6..842c50a54 100644 --- a/data/conf/nginx/templates/sites-default.conf.j2 +++ b/data/conf/nginx/templates/sites-default.conf.j2 @@ -116,6 +116,7 @@ location ~ \.php$ { include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param HTTP_X_REAL_IP $remote_addr; # trusted internal-auth marker; empty for external clients (see nginx.conf map) fastcgi_param SOGO_AUTH_INTERNAL $sogo_auth_internal; fastcgi_read_timeout 3600; diff --git a/data/web/inc/ajax/dns_diagnostics.php b/data/web/inc/ajax/dns_diagnostics.php index 95e34e886..7ed92db77 100644 --- a/data/web/inc/ajax/dns_diagnostics.php +++ b/data/web/inc/ajax/dns_diagnostics.php @@ -380,13 +380,13 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm else { $state = state_nomatch; } - $state .= '
' . $current[$data_field[$current['type']]]; + $state .= '
' . htmlspecialchars($current[$data_field[$current['type']]]); } if ($current['type'] == 'TXT' && stripos($current['txt'], 'v=dmarc') === 0 && $record[2] == $dmarc_link) { $current['txt'] = str_replace(' ', '', $current['txt']); - $state = $current[$data_field[$current['type']]] . state_optional; + $state = htmlspecialchars($current[$data_field[$current['type']]]) . state_optional; } elseif ($current['type'] == 'TXT' && stripos($current['txt'], 'v=spf') === 0 && @@ -396,7 +396,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm if (in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)) { $state = state_good; } - $state .= '
' . $current[$data_field[$current['type']]] . state_optional; + $state .= '
' . htmlspecialchars($current[$data_field[$current['type']]]) . state_optional; } elseif ($current['type'] == 'TXT' && stripos($current['txt'], 'v=dkim') === 0 && @@ -426,7 +426,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm if ($state == state_nomatch) { $state = array(); foreach ($currents as $current) { - $state[] = $current[$data_field[$current['type']]]; + $state[] = htmlspecialchars($current[$data_field[$current['type']]]); } $state = implode('
', $state); } @@ -436,7 +436,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm %s %s %s - ', $record[0], $record[1], $record[2], $state); + ', htmlspecialchars($record[0]), htmlspecialchars($record[1]), $record[2], $state); $record[3] = explode('
', $state); } @@ -477,7 +477,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm } ?> - .txt' type='text/csv'>Download + .txt' type='text/csv'>Download