Bläddra i källkod

Merge branch 'staging' into nightly

FreddleSpl0it 5 månader sedan
förälder
incheckning
cf2d3c1b4e

+ 8 - 4
.github/workflows/rebuild_backup_image.yml

@@ -9,6 +9,8 @@ on:
 jobs:
   docker_image_build:
     runs-on: ubuntu-latest
+    permissions:
+      packages: write
     steps:
       - name: Checkout
         uses: actions/checkout@v4
@@ -19,11 +21,13 @@ jobs:
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v3
 
-      - name: Login to Docker Hub
+      - name: Login to GHCR
+        if: github.event_name != 'pull_request'
         uses: docker/login-action@v3
         with:
-          username: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_USERNAME }}
-          password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }}
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Build and push
         uses: docker/build-push-action@v6
@@ -32,4 +36,4 @@ jobs:
           platforms: linux/amd64,linux/arm64
           file: data/Dockerfiles/backup/Dockerfile
           push: true
-          tags: mailcow/backup:latest
+          tags: ghcr.io/mailcow/backup:latest

+ 2 - 3
data/Dockerfiles/acme/Dockerfile

@@ -1,8 +1,7 @@
-FROM alpine:3.20
+FROM alpine:3.21
 
 LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
 
-
 RUN apk upgrade --no-cache \
   && apk add --update --no-cache \
   bash \
@@ -15,7 +14,7 @@ RUN apk upgrade --no-cache \
   tini \
   tzdata \
   python3 \
-  acme-tiny --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/
+  acme-tiny
 
 COPY acme.sh /srv/acme.sh
 COPY functions.sh /srv/functions.sh

+ 2 - 2
data/Dockerfiles/acme/acme.sh

@@ -138,7 +138,7 @@ log_f "Resolver OK"
 log_f "Waiting for domain table..."
 while [[ -z ${DOMAIN_TABLE} ]]; do
   curl --silent http://nginx.${COMPOSE_PROJECT_NAME}_mailcow-network/ >/dev/null 2>&1
-  DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs)
+  DOMAIN_TABLE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs)
   [[ -z ${DOMAIN_TABLE} ]] && sleep 10
 done
 log_f "OK" no_date
@@ -231,7 +231,7 @@ while true; do
 
   #########################################
   # IP and webroot challenge verification #
-  SQL_DOMAINS=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0 and active=1" -Bs)
+  SQL_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0 and active=1" -Bs)
   if [[ ! $? -eq 0 ]]; then
     log_f "Failed to read SQL domains, retrying in 1 minute..."
     sleep 1m

+ 1 - 1
data/Dockerfiles/backup/Dockerfile

@@ -1,3 +1,3 @@
 FROM debian:bookworm-slim
 
-RUN apt update && apt install pigz
+RUN apt update && apt install pigz -y --no-install-recommends

+ 1 - 1
data/Dockerfiles/dockerapi/Dockerfile

@@ -1,4 +1,4 @@
-FROM alpine:3.20
+FROM alpine:3.21
 
 LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
 

+ 2 - 2
data/Dockerfiles/dovecot/Dockerfile

@@ -1,4 +1,4 @@
-FROM alpine:3.20
+FROM alpine:3.21
 
 LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
 
@@ -69,7 +69,7 @@ RUN addgroup -g 5000 vmail \
   perl-par-packer \
   perl-parse-recdescent \
   perl-lockfile-simple \
-  libproc \
+  libproc2 \
   perl-readonly \
   perl-regexp-common \
   perl-sys-meminfo \

+ 2 - 2
data/Dockerfiles/dovecot/clean_q_aged.sh

@@ -15,6 +15,6 @@ if ! [[ ${MAX_AGE} =~ ${NUM_REGEXP} ]] ; then
   exit 1
 fi
 
-TO_DELETE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT COUNT(id) FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY" -BN)
-mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY"
+TO_DELETE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT COUNT(id) FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY" -BN)
+mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY"
 echo "Deleted ${TO_DELETE} items from quarantine table (max age is ${MAX_AGE//[!0-9]/} days)"

+ 5 - 5
data/Dockerfiles/dovecot/docker-entrypoint.sh

@@ -297,15 +297,15 @@ printenv | sed 's/^\(.*\)$/export \1/g' > /source_env.sh
 
 # Clean stopped imapsync jobs
 rm -f /tmp/imapsync_busy.lock
-IMAPSYNC_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs)
-[[ ! -z ${IMAPSYNC_TABLE} ]] && mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'"
+IMAPSYNC_TABLE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs)
+[[ ! -z ${IMAPSYNC_TABLE} ]] && mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'"
 
 # Envsubst maildir_gc
 echo "$(envsubst < /usr/local/bin/maildir_gc.sh)" > /usr/local/bin/maildir_gc.sh
 
 # GUID generation
 while [[ ${VERSIONS_OK} != 'OK' ]]; do
-  if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = \"${DBNAME}\" AND TABLE_NAME = 'versions'") ]]; then
+  if [[ ! -z $(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = \"${DBNAME}\" AND TABLE_NAME = 'versions'") ]]; then
     VERSIONS_OK=OK
   else
     echo "Waiting for versions table to be created..."
@@ -316,11 +316,11 @@ PUBKEY_MCRYPT=$(doveconf -P 2> /dev/null | grep -i mail_crypt_global_public_key
 if [ -f ${PUBKEY_MCRYPT} ]; then
   GUID=$(cat <(echo ${MAILCOW_HOSTNAME}) /mail_crypt/ecpubkey.pem | sha256sum | cut -d ' ' -f1 | tr -cd "[a-fA-F0-9.:/] ")
   if [ ${#GUID} -eq 64 ]; then
-    mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
+    mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
 REPLACE INTO versions (application, version) VALUES ("GUID", "${GUID}");
 EOF
   else
-    mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
+    mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
 REPLACE INTO versions (application, version) VALUES ("GUID", "INVALID");
 EOF
   fi

+ 1 - 1
data/Dockerfiles/netfilter/Dockerfile

@@ -1,4 +1,4 @@
-FROM alpine:3.20
+FROM alpine:3.21
 
 LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
 

+ 1 - 1
data/Dockerfiles/olefy/Dockerfile

@@ -1,4 +1,4 @@
-FROM alpine:3.20
+FROM alpine:3.21
 
 LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
 

+ 2 - 2
data/Dockerfiles/phpfpm/Dockerfile

@@ -1,4 +1,4 @@
-FROM php:8.2-fpm-alpine3.20
+FROM php:8.2-fpm-alpine3.21
 
 LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
 
@@ -13,7 +13,7 @@ ARG MEMCACHED_PECL_VERSION=3.2.0
 # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
 ARG REDIS_PECL_VERSION=6.1.0
 # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
-ARG COMPOSER_VERSION=2.6.6
+ARG COMPOSER_VERSION=2.8.6
 
 RUN apk add -U --no-cache autoconf \
   aspell-dev \

+ 6 - 6
data/Dockerfiles/phpfpm/docker-entrypoint.sh

@@ -81,7 +81,7 @@ if [ ${SQL_CHANGED} -eq 1 ]; then
 fi
 
 # Check mysql tz import (master and slave)
-TZ_CHECK=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null)
+TZ_CHECK=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null)
 if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then
   SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json')
   echo "MySQL mysql_tzinfo_to_sql - debug output:"
@@ -120,11 +120,11 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
   while read line
   do
     DOMAIN_ARR+=("$line")
-  done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs)
+  done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs)
   while read line
   do
     DOMAIN_ARR+=("$line")
-  done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs)
+  done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs)
 
   if [[ ! -z ${DOMAIN_ARR} ]]; then
   for domain in "${DOMAIN_ARR[@]}"; do
@@ -146,13 +146,13 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
     VALIDATED_IPS=$(array_by_comma ${VALIDATED_API_ALLOW_FROM_ARR[*]})
     if [[ ! -z ${VALIDATED_IPS} ]]; then
       if [[ ${API_KEY} != "invalid" ]] && [[ ! -z ${API_KEY} ]]; then
-        mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
+        mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
 DELETE FROM api WHERE access = 'rw';
 INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY}", "1", "${VALIDATED_IPS}", "rw");
 EOF
       fi
       if [[ ${API_KEY_READ_ONLY} != "invalid" ]] && [[ ! -z ${API_KEY_READ_ONLY} ]]; then
-        mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
+        mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
 DELETE FROM api WHERE access = 'ro';
 INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY_READ_ONLY}", "1", "${VALIDATED_IPS}", "ro");
 EOF
@@ -161,7 +161,7 @@ EOF
   fi
 
   # Create events (master only, STATUS for event on slave will be SLAVESIDE_DISABLED)
-  mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
+  mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
 DROP EVENT IF EXISTS clean_spamalias;
 DELIMITER //
 CREATE EVENT clean_spamalias

+ 1 - 1
data/Dockerfiles/rspamd/Dockerfile

@@ -2,7 +2,7 @@ FROM debian:bookworm-slim
 LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
 
 ARG DEBIAN_FRONTEND=noninteractive
-ARG RSPAMD_VER=rspamd_3.11.0-2~90a175b45
+ARG RSPAMD_VER=rspamd_3.11.1-1~ab0b44951
 ARG CODENAME=bookworm
 ENV LC_ALL=C
 

+ 3 - 3
data/Dockerfiles/sogo/bootstrap-sogo.sh

@@ -14,11 +14,11 @@ do
 done
 
 # Wait for updated schema
-DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
+DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
 DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
 while [[ "${DBV_NOW}" != "${DBV_NEW}" ]]; do
   echo "Waiting for schema update..."
-  DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
+  DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
   DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
   sleep 5
 done
@@ -112,7 +112,7 @@ while read -r line gal
   /etc/sogo/plist_ldap.sh ${line} ${gal} >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
   echo "            </array>
         </dict>" >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
-done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain, CASE gal WHEN '1' THEN 'YES' ELSE 'NO' END AS gal FROM domain;" -B -N)
+done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain, CASE gal WHEN '1' THEN 'YES' ELSE 'NO' END AS gal FROM domain;" -B -N)
 
 # Generate footer
 echo '    </dict>

+ 1 - 1
data/Dockerfiles/unbound/Dockerfile

@@ -1,4 +1,4 @@
-FROM alpine:3.20
+FROM alpine:3.21
 
 LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
 

+ 1 - 1
data/Dockerfiles/watchdog/Dockerfile

@@ -1,4 +1,4 @@
-FROM alpine:3.20
+FROM alpine:3.21
 
 LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
 

+ 2 - 2
data/Dockerfiles/watchdog/check_mysql_slavestatus.sh

@@ -132,9 +132,9 @@ fi
 
 # Connect to the DB server and store output in vars
 if [[ -n $socket ]]; then 
-  ConnectionResult=$(mysql ${optfile} ${socket} ${user} -e "show slave ${connection} status\G" 2>&1)
+  ConnectionResult=$(mariadb --skip-ssl ${optfile} ${socket} ${user} -e "show slave ${connection} status\G" 2>&1)
 else
-  ConnectionResult=$(mysql ${optfile} ${host} ${port} ${user} -e "show slave ${connection} status\G" 2>&1)
+  ConnectionResult=$(mariadb --skip-ssl ${optfile} ${host} ${port} ${user} -e "show slave ${connection} status\G" 2>&1)
 fi
 
 if [ -z "`echo "${ConnectionResult}" |grep Slave_IO_State`" ]; then

+ 1 - 1
data/Dockerfiles/watchdog/watchdog.sh

@@ -234,7 +234,7 @@ external_checks() {
   diff_c=0
   THRESHOLD=${EXTERNAL_CHECKS_THRESHOLD}
   # Reduce error count by 2 after restarting an unhealthy container
-  GUID=$(mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'GUID'" -BN)
+  GUID=$(mariadb --skip-ssl -u${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'GUID'" -BN)
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   while [ ${err_count} -lt ${THRESHOLD} ]; do
     err_c_cur=${err_count}

+ 20 - 4
data/conf/postfix/postscreen_access.cidr

@@ -1,6 +1,6 @@
-# Whitelist generated by Postwhite v3.4 on Sat Feb  1 00:18:03 UTC 2025
+# Whitelist generated by Postwhite v3.4 on Sat Mar  1 00:19:29 UTC 2025
 # https://github.com/stevejenkins/postwhite/
-# 1984 total rules
+# 2000 total rules
 2a00:1450:4000::/36	permit
 2a01:111:f400::/48	permit
 2a01:111:f403:8000::/50	permit
@@ -8,6 +8,13 @@
 2a01:111:f403::/49	permit
 2a01:111:f403:c000::/51	permit
 2a01:111:f403:f000::/52	permit
+2a01:b747:3000:200::/56	permit
+2a01:b747:3001:200::/56	permit
+2a01:b747:3002:200::/56	permit
+2a01:b747:3003:200::/56	permit
+2a01:b747:3004:200::/56	permit
+2a01:b747:3005:200::/56	permit
+2a01:b747:3006:200::/56	permit
 2a02:a60:0:5::/64	permit
 2c0f:fb50:4000::/36	permit
 2.207.151.53	permit
@@ -19,7 +26,6 @@
 8.20.114.31	permit
 8.25.194.0/23	permit
 8.25.196.0/23	permit
-10.162.0.0/16	permit
 12.130.86.238	permit
 13.110.208.0/21	permit
 13.110.209.0/24	permit
@@ -35,7 +41,9 @@
 17.57.156.0/24	permit
 17.58.0.0/16	permit
 17.142.0.0/15	permit
-17.143.234.140/30	permit
+18.97.0.8/30	permit
+18.97.1.184/29	permit
+18.97.2.64/26	permit
 18.156.89.250	permit
 18.157.243.190	permit
 18.194.95.56	permit
@@ -283,6 +291,9 @@
 64.207.219.13	permit
 64.207.219.14	permit
 64.207.219.15	permit
+64.207.219.24	permit
+64.207.219.25	permit
+64.207.219.26	permit
 64.207.219.71	permit
 64.207.219.72	permit
 64.207.219.73	permit
@@ -292,6 +303,9 @@
 64.207.219.77	permit
 64.207.219.78	permit
 64.207.219.79	permit
+64.207.219.88	permit
+64.207.219.89	permit
+64.207.219.90	permit
 64.207.219.135	permit
 64.207.219.136	permit
 64.207.219.137	permit
@@ -1464,6 +1478,8 @@
 159.135.224.0/20	permit
 159.135.228.10	permit
 159.183.0.0/16	permit
+159.183.68.71	permit
+159.183.79.38	permit
 160.1.62.192	permit
 161.38.192.0/20	permit
 161.38.204.0/22	permit

+ 1 - 1
data/web/api/openapi.yaml

@@ -409,7 +409,7 @@ paths:
                   description: a list of domains for which a dkim key should be generated
                   type: string
                 key_size:
-                  description: the key size (1024 or 2048)
+                  description: the key size (1024, 2048, 3072 or 4096)
                   type: number
               type: object
       summary: Generate DKIM Key

+ 4 - 1
data/web/inc/functions.dkim.inc.php

@@ -240,9 +240,12 @@ function dkim($_action, $_data = null, $privkey = false) {
         if (strlen($dkimdata['pubkey']) < 391) {
           $dkimdata['length'] = "1024";
         }
-        elseif (strlen($dkimdata['pubkey']) < 736) {
+        elseif (strlen($dkimdata['pubkey']) < 564) {
           $dkimdata['length'] = "2048";
         }
+        elseif (strlen($dkimdata['pubkey']) < 736) {
+          $dkimdata['length'] = "3072";
+        }
         elseif (strlen($dkimdata['pubkey']) < 1416) {
           $dkimdata['length'] = "4096";
         }

+ 128 - 9
data/web/lang/lang.ko-kr.json

@@ -25,7 +25,11 @@
         "syncjobs": "동기화 작업",
         "tls_policy": "TLS 정책",
         "unlimited_quota": "메일에 무제한 할당",
-        "domain_desc": "도메인 설명 변경"
+        "domain_desc": "도메인 설명 변경",
+        "pw_reset": "mailcow 사용자 비밀번호 재설정 허용",
+        "domain_relayhost": "도메인의 릴레이 호스트 변경",
+        "mailbox_relayhost": "메일함의 릴레이 호스트 변경",
+        "quarantine_category": "검역소 알림 카테고리 변경"
     },
     "add": {
         "activate_filter_warn": "활성화가 체크되어 있으면 모든 다른 필터들은 비활성화됩니다.",
@@ -101,7 +105,9 @@
         "timeout2": "로컬 호스트 연결 시간 초과",
         "username": "사용자명",
         "validate": "확인하기",
-        "validation_success": "성공적으로 확인됨"
+        "validation_success": "성공적으로 확인됨",
+        "tags": "태그",
+        "app_passwd_protocols": "앱 비밀번호에 대해 허용되는 프로토콜"
     },
     "admin": {
         "access": "접근",
@@ -195,7 +201,7 @@
         "link": "Link",
         "loading": "잠시만 기다려주세요...",
         "logo_info": "이미지 크기는 상단 탐색 막대의 경우 40px, 시작 페이지의 경우 최대 너비 250px로 조정됩니다. 확장 가능한 그래픽을 권장합니다.",
-        "lookup_mx": "MX와 목적지 일치 (.outlook.com이 홉을 통해서 MX *.outlook.com을 대상으로 하는 모든 메일을 라우트한다.)",
+        "lookup_mx": "목적지가 MX 이름과 일치하는 정규 표현식입니다. (<code>.*\\.google\\.com</code>를 사용하여 이 홉을 통해 google.com으로 끝나는 모든 메일을 대상으로 하는 MX로 라우팅합니다.)",
         "main_name": "\"mailcow UI\" 이름",
         "merged_vars_hint": "회색으로 표시된 행은 <code>vars.(local.)php</code> 에서 병합되었고 이는 수정할 수 없습니다.",
         "message": "메세지",
@@ -301,7 +307,53 @@
         "username": "사용자 이름",
         "validate_license_now": "라이선스 서버와 GUID 확인",
         "verify": "확인",
-        "yes": "&#10003;"
+        "yes": "&#10003;",
+        "domain_admin": "도메인 관리자",
+        "f2b_filter": "정규식 필터",
+        "f2b_manage_external": "외부에서 Fail2Ban 관리",
+        "f2b_max_ban_time": "최대 차단 시간(초)",
+        "f2b_regex_info": "고려되는 로그: SOGo, Postfix, Dovecot, PHP-FPM.",
+        "html": "HTML",
+        "oauth2_apps": "OAuth2 앱",
+        "oauth2_add_client": "OAuth2 클라이언트 추가",
+        "optional": "선택 사항",
+        "options": "옵션",
+        "password_length": "비밀번호 길이",
+        "password_policy_chars": "하나 이상의 알파벳 문자를 포함해야 합니다.",
+        "password_policy_length": "최소 암호 길이가 %d입니다.",
+        "password_policy_numbers": "숫자 하나 이상을 포함해야 합니다.",
+        "password_policy_special_chars": "특수 문자를 포함해야 합니다.",
+        "password_reset_info": "복구 이메일이 제공되지 않으면 이 기능을 사용할 수 없습니다.",
+        "password_reset_settings": "비밀번호 복구 설정",
+        "password_reset_tmpl_html": "HTML 템플릿",
+        "password_reset_tmpl_text": "Text 템플릿",
+        "password_settings": "비밀번호 설정",
+        "queue_unban": "차단 해제",
+        "restore_template": "기본 템플릿을 복원하려면 비워둡니다.",
+        "service": "서비스",
+        "success": "성공",
+        "dkim_overwrite_key": "기존 DKIM 키 덮어쓰기",
+        "f2b_ban_time_increment": "차단 시간은 차단될 때마다 증가합니다.",
+        "password_policy": "비밀번호 정책",
+        "quarantine_max_score": "메일의 스팸 점수가 이 값보다 높으면 알림을 삭제합니다:<br><small>기본값: 9999.0</small>",
+        "f2b_manage_external_info": "Fail2ban은 차단 목록을 유지하지만 트래픽을 차단하는 규칙을 능동적으로 설정하지는 않습니다. 트래픽을 외부에서 차단하려면 아래 생성된 차단 목록을 사용하세요.",
+        "password_policy_lowerupper": "소문자 및 대문자를 포함해야 합니다.",
+        "transport_test_rcpt_info": "&#8226; null@hosted.mailcow.de 을 사용하여 해외 목적지로 릴레이를 테스트하세요.",
+        "ip_check_disabled": "IP 확인이 비활성화됩니다. 아래에서 활성화할 수 있습니다<br> <strong>시스템 > 구성 > 옵션 > 사용자 정의</strong>",
+        "logo_normal_label": "일반",
+        "logo_dark_label": "다크 모드의 경우 반전",
+        "convert_html_to_text": "HTML을 일반 텍스트로 변환",
+        "copy_to_clipboard": "클립보드에 텍스트가 복사되었습니다!",
+        "cors_settings": "CORS 설정",
+        "rsettings_preset_4": "도메인에 대해 Rspamd 비활성화",
+        "ip_check": "IP 확인",
+        "admins": "관리자",
+        "admins_ldap": "LDAP 관리자",
+        "api_read_only": "읽기 전용 액세스",
+        "api_read_write": "읽기-쓰기 액세스",
+        "is_mx_based": "MX 기반",
+        "login_time": "로그인 시간",
+        "ip_check_opt_in": "외부 IP 주소 확인을 위해 타사 서비스 <strong>ipv4.mailcow.email</strong> 및 <strong>ipv6.mailcow.email</strong>을 사용하도록 설정합니다."
     },
     "danger": {
         "access_denied": "접근이 거부되거나 잘못된 데이터 양식",
@@ -415,7 +467,30 @@
         "username_invalid": "%s는 사용지 이름으로 사용할 수 없습니다.",
         "validity_missing": "유효 기간을 지정해주세요.",
         "value_missing": "모든 값을 입력해주세요.",
-        "yotp_verification_failed": "Yubico OTP 검증 실패: %s"
+        "yotp_verification_failed": "Yubico OTP 검증 실패: %s",
+        "dkim_domain_or_sel_exists": "“%s\"에 대한 DKIM 키가 존재하며 덮어쓰지 않습니다.",
+        "img_size_exceeded": "이미지가 최대 파일 크기를 초과합니다.",
+        "invalid_reset_token": "잘못된 리셋 토큰",
+        "nginx_reload_failed": "Nginx 리로드 실패: %s",
+        "password_reset_na": "현재 비밀번호 복구를 사용할 수 없습니다. 관리자에게 문의하세요.",
+        "reset_f2b_regex": "정규식 필터를 제때 재설정하지 못했습니다. 다시 시도하거나 몇 초 더 기다렸다가 웹사이트를 다시 로드하세요.",
+        "template_exists": "템플릿 %s이(가) 이미 존재합니다.",
+        "template_id_invalid": "템플릿 ID %s가 잘못되었습니다.",
+        "template_name_invalid": "템플릿 이름이 잘못되었습니다.",
+        "tfa_token_invalid": "TFA 토큰이 유효하지 않습니다.",
+        "to_invalid": "수신자가 비어 있지 않아야 합니다.",
+        "webauthn_authenticator_failed": "선택한 인증기를 찾을 수 없습니다.",
+        "webauthn_username_failed": "선택한 인증기가 다른 계정에 속해 있습니다.",
+        "demo_mode_enabled": "데모 모드가 활성화됨",
+        "recovery_email_failed": "복구 이메일을 보낼 수 없습니다. 관리자에게 문의하세요.",
+        "password_reset_invalid_user": "사서함을 찾을 수 없거나 복구 이메일이 설정되어 있지 않습니다.",
+        "webauthn_publickey_failed": "선택한 인증기에 대한 공개 키가 저장되지 않았습니다.",
+        "fido2_verification_failed": "FIDO2 인증 실패: %s",
+        "extended_sender_acl_denied": "외부 발신자 주소를 설정하는 ACL 누락",
+        "img_dimensions_exceeded": "이미지가 최대 이미지 크기를 초과합니다.",
+        "reset_token_limit_exceeded": "토큰 재설정 한도를 초과했습니다. 나중에 다시 시도해 주세요.",
+        "cors_invalid_method": "잘못된 허용 메서드를 지정했습니다.",
+        "cors_invalid_origin": "잘못된 허용 원본을 지정했습니다."
     },
     "debug": {
         "chart_this_server": "Chart (this server)",
@@ -434,7 +509,24 @@
         "uptime": "Uptime",
         "started_on": "Started on",
         "static_logs": "Static logs",
-        "system_containers": "System & Containers"
+        "system_containers": "System & Containers",
+        "current_time": "시스템 시간",
+        "no_update_available": "시스템이 최신 버전입니다.",
+        "architecture": "아키텍처",
+        "container_running": "실행 중",
+        "container_disabled": "컨테이너 중지 또는 비활성화",
+        "container_stopped": "중지됨",
+        "online_users": "온라인 사용자",
+        "service": "서비스",
+        "success": "성공",
+        "show_ip": "공인 IP 표시",
+        "timezone": "시간대",
+        "update_available": "사용 가능한 업데이트가 있습니다.",
+        "update_failed": "업데이트를 확인할 수 없습니다",
+        "username": "사용자 이름",
+        "memory": "메모리",
+        "error_show_ip": "공인 IP 주소를 확인할 수 없습니다",
+        "login_time": "시간"
     },
     "diagnostics": {
         "cname_from_a": "Value derived from A/AAAA record. This is supported as long as the record points to the correct resource.",
@@ -542,7 +634,13 @@
         "title": "Edit object",
         "unchanged_if_empty": "If unchanged leave blank",
         "username": "Username",
-        "validate_save": "Validate and save"
+        "validate_save": "Validate and save",
+        "allow_from_smtp": "다음 IP만 <b>SMTP</b>를 사용하도록 허용합니다.",
+        "allow_from_smtp_info": "모든 발신자를 허용하려면 비워둡니다.<br>IPv4/IPv6 주소 및 네트워크.",
+        "allowed_protocols": "허용된 프로토콜",
+        "app_passwd_protocols": "앱 비밀번호에 대해 허용되는 프로토콜",
+        "acl": "ACL (권한)",
+        "admin": "관리자 수정"
     },
     "footer": {
         "cancel": "Cancel",
@@ -560,13 +658,14 @@
     "header": {
         "administration": "Configuration & Details",
         "apps": "Apps",
-        "debug": "System Information",
+        "debug": "정보",
         "email": "E-Mail",
         "mailcow_config": "Configuration",
         "quarantine": "Quarantine",
         "restart_netfilter": "Restart netfilter",
         "restart_sogo": "Restart SOGo",
-        "user_settings": "User Settings"
+        "user_settings": "User Settings",
+        "mailcow_system": "시스템"
     },
     "info": {
         "awaiting_tfa_confirmation": "Awaiting TFA confirmation",
@@ -1015,5 +1114,25 @@
         "quota_exceeded_scope": "Domain quota exceeded: Only unlimited mailboxes can be created in this domain scope.",
         "session_token": "Form token invalid: Token mismatch",
         "session_ua": "Form token invalid: User-Agent validation error"
+    },
+    "datatables": {
+        "collapse_all": "모두 접기",
+        "decimal": ".",
+        "emptyTable": "테이블에 사용 가능한 데이터가 없습니다.",
+        "expand_all": "모두 펼치기",
+        "infoEmpty": "0개 항목 중 0개부터 0개까지 표시",
+        "infoFiltered": "(_MAX_ 총 항목에서 필터링됨)",
+        "thousands": ",",
+        "lengthMenu": "_MENU_ 항목 표시",
+        "loadingRecords": "로딩 중...",
+        "processing": "잠시만 기다려 주세요...",
+        "search": "검색:",
+        "zeroRecords": "일치하는 레코드가 없습니다.",
+        "paginate": {
+            "first": "처음",
+            "last": "마지막",
+            "next": "다음",
+            "previous": "이전"
+        }
     }
 }

+ 2 - 0
data/web/templates/admin/tab-config-dkim.twig

@@ -117,6 +117,8 @@
             <select data-style="btn btn-light btn-sm" class="form-control" id="key_size" name="key_size" title="{{ lang.admin.dkim_key_length }}" required>
               <option data-subtext="bits">1024</option>
               <option data-subtext="bits">2048</option>
+              <option data-subtext="bits">3072</option>
+              <option data-subtext="bits">4096</option>
             </select>
           </div>
         </div>

+ 2 - 0
data/web/templates/edit/domain-templates.twig

@@ -103,6 +103,8 @@
         <select data-style="btn btn-light" class="form-control" id="key_size" name="key_size">
           <option value="1024" data-subtext="bits" {% if template.attributes.key_size == 1024 %} selected{% endif %}>1024</option>
           <option value="2048" data-subtext="bits" {% if template.attributes.key_size == 2048 %} selected{% endif %}>2048</option>
+          <option value="3072" data-subtext="bits" {% if template.attributes.key_size == 3072 %} selected{% endif %}>3072</option>
+          <option value="4096" data-subtext="bits" {% if template.attributes.key_size == 4096 %} selected{% endif %}>4096</option>
         </select>
       </div>
     </div>

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

@@ -493,6 +493,8 @@
               <select data-style="btn btn-light" class="form-control" id="key_size" name="key_size">
                 <option data-subtext="bits" value="1024">1024</option>
                 <option data-subtext="bits" value="2048" selected>2048</option>
+                <option data-subtext="bits" value="3072">3072</option>
+                <option data-subtext="bits" value="4096">4096</option>
               </select>
             </div>
           </div>
@@ -631,6 +633,8 @@
               <select data-style="btn btn-light" class="form-control" id="key_size" name="key_size">
                 <option data-subtext="bits">1024</option>
                 <option data-subtext="bits" selected>2048</option>
+                <option data-subtext="bits">3072</option>
+                <option data-subtext="bits">4096</option>
               </select>
             </div>
           </div>
@@ -846,6 +850,8 @@
               <select data-style="btn btn-light" class="form-control" id="key_size2" name="key_size">
                 <option data-subtext="bits">1024</option>
                 <option data-subtext="bits" selected>2048</option>
+                <option data-subtext="bits">3072</option>
+                <option data-subtext="bits">4096</option>
               </select>
             </div>
           </div>

+ 10 - 10
docker-compose.yml

@@ -1,7 +1,7 @@
 services:
 
     unbound-mailcow:
-      image: ghcr.io/mailcow/unbound:1.23
+      image: ghcr.io/mailcow/unbound:1.24
       environment:
         - TZ=${TZ}
         - SKIP_UNBOUND_HEALTHCHECK=${SKIP_UNBOUND_HEALTHCHECK:-n}
@@ -84,7 +84,7 @@ services:
             - clamd
 
     rspamd-mailcow:
-      image: ghcr.io/mailcow/rspamd:2.0
+      image: ghcr.io/mailcow/rspamd:2.1
       stop_grace_period: 30s
       depends_on:
         - dovecot-mailcow
@@ -117,7 +117,7 @@ services:
             - rspamd
 
     php-fpm-mailcow:
-      image: ghcr.io/mailcow/phpfpm:1.92
+      image: ghcr.io/mailcow/phpfpm:1.93
       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
       depends_on:
         - redis-mailcow
@@ -199,7 +199,7 @@ services:
             - phpfpm
 
     sogo-mailcow:
-      image: mailcow/sogo:nightly-20250224
+      image: ghcr.io/mailcow/sogo:1.131
       environment:
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
@@ -250,7 +250,7 @@ services:
             - sogo
 
     dovecot-mailcow:
-      image: mailcow/dovecot:nightly-20250224
+      image: ghcr.io/mailcow/dovecot:2.33
       depends_on:
         - mysql-mailcow
         - netfilter-mailcow
@@ -439,7 +439,7 @@ services:
           condition: service_started
         unbound-mailcow:
           condition: service_healthy
-      image: ghcr.io/mailcow/acme:1.91
+      image: ghcr.io/mailcow/acme:1.92
       dns:
         - ${IPV4_NETWORK:-172.22.1}.254
       environment:
@@ -477,7 +477,7 @@ services:
             - acme
 
     netfilter-mailcow:
-      image: ghcr.io/mailcow/netfilter:1.61
+      image: ghcr.io/mailcow/netfilter:1.62
       stop_grace_period: 30s
       restart: always
       privileged: true
@@ -497,7 +497,7 @@ services:
         - /lib/modules:/lib/modules:ro
 
     watchdog-mailcow:
-      image: ghcr.io/mailcow/watchdog:2.06
+      image: ghcr.io/mailcow/watchdog:2.07
       dns:
         - ${IPV4_NETWORK:-172.22.1}.254
       tmpfs:
@@ -569,7 +569,7 @@ services:
             - watchdog
 
     dockerapi-mailcow:
-      image: ghcr.io/mailcow/dockerapi:2.10
+      image: ghcr.io/mailcow/dockerapi:2.11
       security_opt:
         - label=disable
       restart: always
@@ -589,7 +589,7 @@ services:
             - dockerapi
 
     olefy-mailcow:
-      image: ghcr.io/mailcow/olefy:1.13
+      image: ghcr.io/mailcow/olefy:1.14
       restart: always
       environment:
         - TZ=${TZ}

+ 3 - 43
helper-scripts/_cold-standby.sh

@@ -10,46 +10,6 @@ echo "If this script is run automatically by cron or a timer AND you are using b
 echo "The snapshots of your backup destination should run AFTER the cold standby script finished to ensure consistent snapshots."
 echo
 
-function docker_garbage() {
-  IMGS_TO_DELETE=()
-
-  for container in $(grep -oP "image: \Kmailcow.+" docker-compose.yml); do
-
-    REPOSITORY=${container/:*}
-    TAG=${container/*:}
-    V_MAIN=${container/*.}
-    V_SUB=${container/*.}
-    EXISTING_TAGS=$(docker images | grep ${REPOSITORY} | awk '{ print $2 }')
-
-    for existing_tag in ${EXISTING_TAGS[@]}; do
-
-      V_MAIN_EXISTING=${existing_tag/*.}
-      V_SUB_EXISTING=${existing_tag/*.}
-
-      # Not an integer
-      [[ ! ${V_MAIN_EXISTING} =~ ^[0-9]+$ ]] && continue
-      [[ ! ${V_SUB_EXISTING} =~ ^[0-9]+$ ]] && continue
-
-      if [[ ${V_MAIN_EXISTING} == "latest" ]]; then
-        echo "Found deprecated label \"latest\" for repository ${REPOSITORY}, it should be deleted."
-        IMGS_TO_DELETE+=(${REPOSITORY}:${existing_tag})
-      elif [[ ${V_MAIN_EXISTING} -lt ${V_MAIN} ]]; then
-        echo "Found tag ${existing_tag} for ${REPOSITORY}, which is older than the current tag ${TAG} and should be deleted."
-        IMGS_TO_DELETE+=(${REPOSITORY}:${existing_tag})
-      elif [[ ${V_SUB_EXISTING} -lt ${V_SUB} ]]; then
-        echo "Found tag ${existing_tag} for ${REPOSITORY}, which is older than the current tag ${TAG} and should be deleted."
-        IMGS_TO_DELETE+=(${REPOSITORY}:${existing_tag})
-      fi
-
-    done
-
-  done
-
-  if [[ ! -z ${IMGS_TO_DELETE[*]} ]]; then
-    docker rmi ${IMGS_TO_DELETE[*]}
-  fi
-}
-
 function preflight_local_checks() {
   if [[ -z "${REMOTE_SSH_KEY}" ]]; then
     >&2 echo -e "\e[31mREMOTE_SSH_KEY is not set\e[0m"
@@ -139,11 +99,11 @@ EOF
 
 if [ $? = 0 ]; then
   COMPOSE_COMMAND="docker compose"
-  echo "DEBUG: Using native docker compose on remote"
+  echo "INFO: Using native docker compose on remote"
 
 elif [ $? = 1 ]; then
   COMPOSE_COMMAND="docker-compose"
-  echo "DEBUG: Using standalone docker compose on remote"
+  echo "INFO: Using standalone docker compose on remote"
 
 else
   echo -e "\e[31mCannot find any Docker Compose on remote, exiting...\e[0m"
@@ -324,7 +284,7 @@ echo "OK"
     -i "${REMOTE_SSH_KEY}" \
     ${REMOTE_SSH_HOST} \
     -p ${REMOTE_SSH_PORT} \
-    ${COMPOSE_COMMAND} -f "${SCRIPT_DIR}/../docker-compose.yml" pull --no-parallel --quiet 2>&1 ; then
+    ${COMPOSE_COMMAND} -f "${SCRIPT_DIR}/../docker-compose.yml" pull --quiet 2>&1 ; then
       >&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote"
   fi
 

+ 1 - 1
helper-scripts/backup_and_restore.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-DEBIAN_DOCKER_IMAGE="mailcow/backup:latest"
+DEBIAN_DOCKER_IMAGE="ghcr.io/mailcow/backup:latest"
 
 if [[ ! -z ${MAILCOW_BACKUP_LOCATION} ]]; then
   BACKUP_LOCATION="${MAILCOW_BACKUP_LOCATION}"

+ 18 - 6
update.sh

@@ -36,13 +36,19 @@ docker_garbage() {
   IMGS_TO_DELETE=()
 
   declare -A IMAGES_INFO
-  COMPOSE_IMAGES=($(grep -oP "image: \Kmailcow.+" "${SCRIPT_DIR}/docker-compose.yml"))
+  COMPOSE_IMAGES=($(grep -oP "image: \K(ghcr\.io/)?mailcow.+" "${SCRIPT_DIR}/docker-compose.yml"))
 
-  for existing_image in $(docker images --format "{{.ID}}:{{.Repository}}:{{.Tag}}" | grep 'mailcow/'); do
+  for existing_image in $(docker images --format "{{.ID}}:{{.Repository}}:{{.Tag}}" | grep -E '(mailcow/|ghcr\.io/mailcow/)'); do
       ID=$(echo "$existing_image" | cut -d ':' -f 1)
       REPOSITORY=$(echo "$existing_image" | cut -d ':' -f 2)
       TAG=$(echo "$existing_image" | cut -d ':' -f 3)
 
+      if [[ "$REPOSITORY" == "mailcow/backup" || "$REPOSITORY" == "ghcr.io/mailcow/backup" ]]; then
+          if [[ "$TAG" != "<none>" ]]; then
+              continue
+          fi
+      fi
+
       if [[ " ${COMPOSE_IMAGES[@]} " =~ " ${REPOSITORY}:${TAG} " ]]; then
           continue
       else
@@ -57,7 +63,7 @@ docker_garbage() {
           echo "    ${IMAGES_INFO[$id]} ($id)"
       done
 
-      if [ ! $FORCE ]; then
+      if [ -z "$FORCE" ]; then
           read -r -p "Do you want to delete them to free up some space? [y/N] " response
           if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
               docker rmi ${IMGS_TO_DELETE[*]}
@@ -716,9 +722,16 @@ detect_major_update() {
     # Add major versions here
     MAJOR_VERSIONS=(
       "2025-02"
+      "2025-03"
     )
 
-    current_version=$(git describe --tags $(git rev-list --tags --max-count=1))
+    current_version=""
+    if [[ -f "${SCRIPT_DIR}/data/web/inc/app_info.inc.php" ]]; then
+      current_version=$(grep 'MAILCOW_GIT_VERSION' ${SCRIPT_DIR}/data/web/inc/app_info.inc.php | sed -E 's/.*MAILCOW_GIT_VERSION="([^"]+)".*/\1/')
+    fi
+    if [[ -z "$current_version" ]]; then
+      return 1
+    fi
     release_url="https://github.com/mailcow/mailcow-dockerized/releases/tag"
 
     updates_to_apply=()
@@ -735,8 +748,7 @@ detect_major_update() {
         echo "$update - $release_url/$update"
       done
 
-      echo -e "\n⚠️  Please read the release notes before proceeding.\n"
-
+      echo -e "\nPlease read the release notes before proceeding."
       read -p "Do you want to proceed with the update? [y/n] " response
       if [[ "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
         echo "Proceeding with the update..."