Browse Source

Merge branch 'staging' into nightly

FreddleSpl0it 5 months ago
parent
commit
cf2d3c1b4e

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

@@ -9,6 +9,8 @@ on:
 jobs:
 jobs:
   docker_image_build:
   docker_image_build:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
+    permissions:
+      packages: write
     steps:
     steps:
       - name: Checkout
       - name: Checkout
         uses: actions/checkout@v4
         uses: actions/checkout@v4
@@ -19,11 +21,13 @@ jobs:
       - name: Set up Docker Buildx
       - name: Set up Docker Buildx
         uses: docker/setup-buildx-action@v3
         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
         uses: docker/login-action@v3
         with:
         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
       - name: Build and push
         uses: docker/build-push-action@v6
         uses: docker/build-push-action@v6
@@ -32,4 +36,4 @@ jobs:
           platforms: linux/amd64,linux/arm64
           platforms: linux/amd64,linux/arm64
           file: data/Dockerfiles/backup/Dockerfile
           file: data/Dockerfiles/backup/Dockerfile
           push: true
           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>"
 LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
 
 
-
 RUN apk upgrade --no-cache \
 RUN apk upgrade --no-cache \
   && apk add --update --no-cache \
   && apk add --update --no-cache \
   bash \
   bash \
@@ -15,7 +14,7 @@ RUN apk upgrade --no-cache \
   tini \
   tini \
   tzdata \
   tzdata \
   python3 \
   python3 \
-  acme-tiny --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/
+  acme-tiny
 
 
 COPY acme.sh /srv/acme.sh
 COPY acme.sh /srv/acme.sh
 COPY functions.sh /srv/functions.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..."
 log_f "Waiting for domain table..."
 while [[ -z ${DOMAIN_TABLE} ]]; do
 while [[ -z ${DOMAIN_TABLE} ]]; do
   curl --silent http://nginx.${COMPOSE_PROJECT_NAME}_mailcow-network/ >/dev/null 2>&1
   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
   [[ -z ${DOMAIN_TABLE} ]] && sleep 10
 done
 done
 log_f "OK" no_date
 log_f "OK" no_date
@@ -231,7 +231,7 @@ while true; do
 
 
   #########################################
   #########################################
   # IP and webroot challenge verification #
   # 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
   if [[ ! $? -eq 0 ]]; then
     log_f "Failed to read SQL domains, retrying in 1 minute..."
     log_f "Failed to read SQL domains, retrying in 1 minute..."
     sleep 1m
     sleep 1m

+ 1 - 1
data/Dockerfiles/backup/Dockerfile

@@ -1,3 +1,3 @@
 FROM debian:bookworm-slim
 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>"
 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>"
 LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
 
 
@@ -69,7 +69,7 @@ RUN addgroup -g 5000 vmail \
   perl-par-packer \
   perl-par-packer \
   perl-parse-recdescent \
   perl-parse-recdescent \
   perl-lockfile-simple \
   perl-lockfile-simple \
-  libproc \
+  libproc2 \
   perl-readonly \
   perl-readonly \
   perl-regexp-common \
   perl-regexp-common \
   perl-sys-meminfo \
   perl-sys-meminfo \

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

@@ -15,6 +15,6 @@ if ! [[ ${MAX_AGE} =~ ${NUM_REGEXP} ]] ; then
   exit 1
   exit 1
 fi
 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)"
 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
 # Clean stopped imapsync jobs
 rm -f /tmp/imapsync_busy.lock
 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
 # Envsubst maildir_gc
 echo "$(envsubst < /usr/local/bin/maildir_gc.sh)" > /usr/local/bin/maildir_gc.sh
 echo "$(envsubst < /usr/local/bin/maildir_gc.sh)" > /usr/local/bin/maildir_gc.sh
 
 
 # GUID generation
 # GUID generation
 while [[ ${VERSIONS_OK} != 'OK' ]]; do
 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
     VERSIONS_OK=OK
   else
   else
     echo "Waiting for versions table to be created..."
     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
 if [ -f ${PUBKEY_MCRYPT} ]; then
   GUID=$(cat <(echo ${MAILCOW_HOSTNAME}) /mail_crypt/ecpubkey.pem | sha256sum | cut -d ' ' -f1 | tr -cd "[a-fA-F0-9.:/] ")
   GUID=$(cat <(echo ${MAILCOW_HOSTNAME}) /mail_crypt/ecpubkey.pem | sha256sum | cut -d ' ' -f1 | tr -cd "[a-fA-F0-9.:/] ")
   if [ ${#GUID} -eq 64 ]; then
   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}");
 REPLACE INTO versions (application, version) VALUES ("GUID", "${GUID}");
 EOF
 EOF
   else
   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");
 REPLACE INTO versions (application, version) VALUES ("GUID", "INVALID");
 EOF
 EOF
   fi
   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>"
 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>"
 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>"
 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>.*)$
 # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
 ARG REDIS_PECL_VERSION=6.1.0
 ARG REDIS_PECL_VERSION=6.1.0
 # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
 # 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 \
 RUN apk add -U --no-cache autoconf \
   aspell-dev \
   aspell-dev \

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

@@ -81,7 +81,7 @@ if [ ${SQL_CHANGED} -eq 1 ]; then
 fi
 fi
 
 
 # Check mysql tz import (master and slave)
 # 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
 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')
   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:"
   echo "MySQL mysql_tzinfo_to_sql - debug output:"
@@ -120,11 +120,11 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
   while read line
   while read line
   do
   do
     DOMAIN_ARR+=("$line")
     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
   while read line
   do
   do
     DOMAIN_ARR+=("$line")
     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
   if [[ ! -z ${DOMAIN_ARR} ]]; then
   for domain in "${DOMAIN_ARR[@]}"; do
   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[*]})
     VALIDATED_IPS=$(array_by_comma ${VALIDATED_API_ALLOW_FROM_ARR[*]})
     if [[ ! -z ${VALIDATED_IPS} ]]; then
     if [[ ! -z ${VALIDATED_IPS} ]]; then
       if [[ ${API_KEY} != "invalid" ]] && [[ ! -z ${API_KEY} ]]; 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';
 DELETE FROM api WHERE access = 'rw';
 INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY}", "1", "${VALIDATED_IPS}", "rw");
 INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY}", "1", "${VALIDATED_IPS}", "rw");
 EOF
 EOF
       fi
       fi
       if [[ ${API_KEY_READ_ONLY} != "invalid" ]] && [[ ! -z ${API_KEY_READ_ONLY} ]]; then
       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';
 DELETE FROM api WHERE access = 'ro';
 INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY_READ_ONLY}", "1", "${VALIDATED_IPS}", "ro");
 INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY_READ_ONLY}", "1", "${VALIDATED_IPS}", "ro");
 EOF
 EOF
@@ -161,7 +161,7 @@ EOF
   fi
   fi
 
 
   # Create events (master only, STATUS for event on slave will be SLAVESIDE_DISABLED)
   # 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;
 DROP EVENT IF EXISTS clean_spamalias;
 DELIMITER //
 DELIMITER //
 CREATE EVENT clean_spamalias
 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>"
 LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
 
 
 ARG DEBIAN_FRONTEND=noninteractive
 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
 ARG CODENAME=bookworm
 ENV LC_ALL=C
 ENV LC_ALL=C
 
 

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

@@ -14,11 +14,11 @@ do
 done
 done
 
 
 # Wait for updated schema
 # 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)
 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
 while [[ "${DBV_NOW}" != "${DBV_NEW}" ]]; do
   echo "Waiting for schema update..."
   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)
   DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
   sleep 5
   sleep 5
 done
 done
@@ -112,7 +112,7 @@ while read -r line gal
   /etc/sogo/plist_ldap.sh ${line} ${gal} >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
   /etc/sogo/plist_ldap.sh ${line} ${gal} >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
   echo "            </array>
   echo "            </array>
         </dict>" >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
         </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
 # Generate footer
 echo '    </dict>
 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>"
 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>"
 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
 # Connect to the DB server and store output in vars
 if [[ -n $socket ]]; then 
 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
 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
 fi
 
 
 if [ -z "`echo "${ConnectionResult}" |grep Slave_IO_State`" ]; then
 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
   diff_c=0
   THRESHOLD=${EXTERNAL_CHECKS_THRESHOLD}
   THRESHOLD=${EXTERNAL_CHECKS_THRESHOLD}
   # Reduce error count by 2 after restarting an unhealthy container
   # 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
   trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
   while [ ${err_count} -lt ${THRESHOLD} ]; do
   while [ ${err_count} -lt ${THRESHOLD} ]; do
     err_c_cur=${err_count}
     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/
 # https://github.com/stevejenkins/postwhite/
-# 1984 total rules
+# 2000 total rules
 2a00:1450:4000::/36	permit
 2a00:1450:4000::/36	permit
 2a01:111:f400::/48	permit
 2a01:111:f400::/48	permit
 2a01:111:f403:8000::/50	permit
 2a01:111:f403:8000::/50	permit
@@ -8,6 +8,13 @@
 2a01:111:f403::/49	permit
 2a01:111:f403::/49	permit
 2a01:111:f403:c000::/51	permit
 2a01:111:f403:c000::/51	permit
 2a01:111:f403:f000::/52	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
 2a02:a60:0:5::/64	permit
 2c0f:fb50:4000::/36	permit
 2c0f:fb50:4000::/36	permit
 2.207.151.53	permit
 2.207.151.53	permit
@@ -19,7 +26,6 @@
 8.20.114.31	permit
 8.20.114.31	permit
 8.25.194.0/23	permit
 8.25.194.0/23	permit
 8.25.196.0/23	permit
 8.25.196.0/23	permit
-10.162.0.0/16	permit
 12.130.86.238	permit
 12.130.86.238	permit
 13.110.208.0/21	permit
 13.110.208.0/21	permit
 13.110.209.0/24	permit
 13.110.209.0/24	permit
@@ -35,7 +41,9 @@
 17.57.156.0/24	permit
 17.57.156.0/24	permit
 17.58.0.0/16	permit
 17.58.0.0/16	permit
 17.142.0.0/15	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.156.89.250	permit
 18.157.243.190	permit
 18.157.243.190	permit
 18.194.95.56	permit
 18.194.95.56	permit
@@ -283,6 +291,9 @@
 64.207.219.13	permit
 64.207.219.13	permit
 64.207.219.14	permit
 64.207.219.14	permit
 64.207.219.15	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.71	permit
 64.207.219.72	permit
 64.207.219.72	permit
 64.207.219.73	permit
 64.207.219.73	permit
@@ -292,6 +303,9 @@
 64.207.219.77	permit
 64.207.219.77	permit
 64.207.219.78	permit
 64.207.219.78	permit
 64.207.219.79	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.135	permit
 64.207.219.136	permit
 64.207.219.136	permit
 64.207.219.137	permit
 64.207.219.137	permit
@@ -1464,6 +1478,8 @@
 159.135.224.0/20	permit
 159.135.224.0/20	permit
 159.135.228.10	permit
 159.135.228.10	permit
 159.183.0.0/16	permit
 159.183.0.0/16	permit
+159.183.68.71	permit
+159.183.79.38	permit
 160.1.62.192	permit
 160.1.62.192	permit
 161.38.192.0/20	permit
 161.38.192.0/20	permit
 161.38.204.0/22	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
                   description: a list of domains for which a dkim key should be generated
                   type: string
                   type: string
                 key_size:
                 key_size:
-                  description: the key size (1024 or 2048)
+                  description: the key size (1024, 2048, 3072 or 4096)
                   type: number
                   type: number
               type: object
               type: object
       summary: Generate DKIM Key
       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) {
         if (strlen($dkimdata['pubkey']) < 391) {
           $dkimdata['length'] = "1024";
           $dkimdata['length'] = "1024";
         }
         }
-        elseif (strlen($dkimdata['pubkey']) < 736) {
+        elseif (strlen($dkimdata['pubkey']) < 564) {
           $dkimdata['length'] = "2048";
           $dkimdata['length'] = "2048";
         }
         }
+        elseif (strlen($dkimdata['pubkey']) < 736) {
+          $dkimdata['length'] = "3072";
+        }
         elseif (strlen($dkimdata['pubkey']) < 1416) {
         elseif (strlen($dkimdata['pubkey']) < 1416) {
           $dkimdata['length'] = "4096";
           $dkimdata['length'] = "4096";
         }
         }

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

@@ -25,7 +25,11 @@
         "syncjobs": "동기화 작업",
         "syncjobs": "동기화 작업",
         "tls_policy": "TLS 정책",
         "tls_policy": "TLS 정책",
         "unlimited_quota": "메일에 무제한 할당",
         "unlimited_quota": "메일에 무제한 할당",
-        "domain_desc": "도메인 설명 변경"
+        "domain_desc": "도메인 설명 변경",
+        "pw_reset": "mailcow 사용자 비밀번호 재설정 허용",
+        "domain_relayhost": "도메인의 릴레이 호스트 변경",
+        "mailbox_relayhost": "메일함의 릴레이 호스트 변경",
+        "quarantine_category": "검역소 알림 카테고리 변경"
     },
     },
     "add": {
     "add": {
         "activate_filter_warn": "활성화가 체크되어 있으면 모든 다른 필터들은 비활성화됩니다.",
         "activate_filter_warn": "활성화가 체크되어 있으면 모든 다른 필터들은 비활성화됩니다.",
@@ -101,7 +105,9 @@
         "timeout2": "로컬 호스트 연결 시간 초과",
         "timeout2": "로컬 호스트 연결 시간 초과",
         "username": "사용자명",
         "username": "사용자명",
         "validate": "확인하기",
         "validate": "확인하기",
-        "validation_success": "성공적으로 확인됨"
+        "validation_success": "성공적으로 확인됨",
+        "tags": "태그",
+        "app_passwd_protocols": "앱 비밀번호에 대해 허용되는 프로토콜"
     },
     },
     "admin": {
     "admin": {
         "access": "접근",
         "access": "접근",
@@ -195,7 +201,7 @@
         "link": "Link",
         "link": "Link",
         "loading": "잠시만 기다려주세요...",
         "loading": "잠시만 기다려주세요...",
         "logo_info": "이미지 크기는 상단 탐색 막대의 경우 40px, 시작 페이지의 경우 최대 너비 250px로 조정됩니다. 확장 가능한 그래픽을 권장합니다.",
         "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\" 이름",
         "main_name": "\"mailcow UI\" 이름",
         "merged_vars_hint": "회색으로 표시된 행은 <code>vars.(local.)php</code> 에서 병합되었고 이는 수정할 수 없습니다.",
         "merged_vars_hint": "회색으로 표시된 행은 <code>vars.(local.)php</code> 에서 병합되었고 이는 수정할 수 없습니다.",
         "message": "메세지",
         "message": "메세지",
@@ -301,7 +307,53 @@
         "username": "사용자 이름",
         "username": "사용자 이름",
         "validate_license_now": "라이선스 서버와 GUID 확인",
         "validate_license_now": "라이선스 서버와 GUID 확인",
         "verify": "확인",
         "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": {
     "danger": {
         "access_denied": "접근이 거부되거나 잘못된 데이터 양식",
         "access_denied": "접근이 거부되거나 잘못된 데이터 양식",
@@ -415,7 +467,30 @@
         "username_invalid": "%s는 사용지 이름으로 사용할 수 없습니다.",
         "username_invalid": "%s는 사용지 이름으로 사용할 수 없습니다.",
         "validity_missing": "유효 기간을 지정해주세요.",
         "validity_missing": "유효 기간을 지정해주세요.",
         "value_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": {
     "debug": {
         "chart_this_server": "Chart (this server)",
         "chart_this_server": "Chart (this server)",
@@ -434,7 +509,24 @@
         "uptime": "Uptime",
         "uptime": "Uptime",
         "started_on": "Started on",
         "started_on": "Started on",
         "static_logs": "Static logs",
         "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": {
     "diagnostics": {
         "cname_from_a": "Value derived from A/AAAA record. This is supported as long as the record points to the correct resource.",
         "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",
         "title": "Edit object",
         "unchanged_if_empty": "If unchanged leave blank",
         "unchanged_if_empty": "If unchanged leave blank",
         "username": "Username",
         "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": {
     "footer": {
         "cancel": "Cancel",
         "cancel": "Cancel",
@@ -560,13 +658,14 @@
     "header": {
     "header": {
         "administration": "Configuration & Details",
         "administration": "Configuration & Details",
         "apps": "Apps",
         "apps": "Apps",
-        "debug": "System Information",
+        "debug": "정보",
         "email": "E-Mail",
         "email": "E-Mail",
         "mailcow_config": "Configuration",
         "mailcow_config": "Configuration",
         "quarantine": "Quarantine",
         "quarantine": "Quarantine",
         "restart_netfilter": "Restart netfilter",
         "restart_netfilter": "Restart netfilter",
         "restart_sogo": "Restart SOGo",
         "restart_sogo": "Restart SOGo",
-        "user_settings": "User Settings"
+        "user_settings": "User Settings",
+        "mailcow_system": "시스템"
     },
     },
     "info": {
     "info": {
         "awaiting_tfa_confirmation": "Awaiting TFA confirmation",
         "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.",
         "quota_exceeded_scope": "Domain quota exceeded: Only unlimited mailboxes can be created in this domain scope.",
         "session_token": "Form token invalid: Token mismatch",
         "session_token": "Form token invalid: Token mismatch",
         "session_ua": "Form token invalid: User-Agent validation error"
         "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>
             <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">1024</option>
               <option data-subtext="bits">2048</option>
               <option data-subtext="bits">2048</option>
+              <option data-subtext="bits">3072</option>
+              <option data-subtext="bits">4096</option>
             </select>
             </select>
           </div>
           </div>
         </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">
         <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="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="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>
         </select>
       </div>
       </div>
     </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">
               <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="1024">1024</option>
                 <option data-subtext="bits" value="2048" selected>2048</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>
               </select>
             </div>
             </div>
           </div>
           </div>
@@ -631,6 +633,8 @@
               <select data-style="btn btn-light" class="form-control" id="key_size" name="key_size">
               <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">1024</option>
                 <option data-subtext="bits" selected>2048</option>
                 <option data-subtext="bits" selected>2048</option>
+                <option data-subtext="bits">3072</option>
+                <option data-subtext="bits">4096</option>
               </select>
               </select>
             </div>
             </div>
           </div>
           </div>
@@ -846,6 +850,8 @@
               <select data-style="btn btn-light" class="form-control" id="key_size2" name="key_size">
               <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">1024</option>
                 <option data-subtext="bits" selected>2048</option>
                 <option data-subtext="bits" selected>2048</option>
+                <option data-subtext="bits">3072</option>
+                <option data-subtext="bits">4096</option>
               </select>
               </select>
             </div>
             </div>
           </div>
           </div>

+ 10 - 10
docker-compose.yml

@@ -1,7 +1,7 @@
 services:
 services:
 
 
     unbound-mailcow:
     unbound-mailcow:
-      image: ghcr.io/mailcow/unbound:1.23
+      image: ghcr.io/mailcow/unbound:1.24
       environment:
       environment:
         - TZ=${TZ}
         - TZ=${TZ}
         - SKIP_UNBOUND_HEALTHCHECK=${SKIP_UNBOUND_HEALTHCHECK:-n}
         - SKIP_UNBOUND_HEALTHCHECK=${SKIP_UNBOUND_HEALTHCHECK:-n}
@@ -84,7 +84,7 @@ services:
             - clamd
             - clamd
 
 
     rspamd-mailcow:
     rspamd-mailcow:
-      image: ghcr.io/mailcow/rspamd:2.0
+      image: ghcr.io/mailcow/rspamd:2.1
       stop_grace_period: 30s
       stop_grace_period: 30s
       depends_on:
       depends_on:
         - dovecot-mailcow
         - dovecot-mailcow
@@ -117,7 +117,7 @@ services:
             - rspamd
             - rspamd
 
 
     php-fpm-mailcow:
     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"
       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
       depends_on:
       depends_on:
         - redis-mailcow
         - redis-mailcow
@@ -199,7 +199,7 @@ services:
             - phpfpm
             - phpfpm
 
 
     sogo-mailcow:
     sogo-mailcow:
-      image: mailcow/sogo:nightly-20250224
+      image: ghcr.io/mailcow/sogo:1.131
       environment:
       environment:
         - DBNAME=${DBNAME}
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
         - DBUSER=${DBUSER}
@@ -250,7 +250,7 @@ services:
             - sogo
             - sogo
 
 
     dovecot-mailcow:
     dovecot-mailcow:
-      image: mailcow/dovecot:nightly-20250224
+      image: ghcr.io/mailcow/dovecot:2.33
       depends_on:
       depends_on:
         - mysql-mailcow
         - mysql-mailcow
         - netfilter-mailcow
         - netfilter-mailcow
@@ -439,7 +439,7 @@ services:
           condition: service_started
           condition: service_started
         unbound-mailcow:
         unbound-mailcow:
           condition: service_healthy
           condition: service_healthy
-      image: ghcr.io/mailcow/acme:1.91
+      image: ghcr.io/mailcow/acme:1.92
       dns:
       dns:
         - ${IPV4_NETWORK:-172.22.1}.254
         - ${IPV4_NETWORK:-172.22.1}.254
       environment:
       environment:
@@ -477,7 +477,7 @@ services:
             - acme
             - acme
 
 
     netfilter-mailcow:
     netfilter-mailcow:
-      image: ghcr.io/mailcow/netfilter:1.61
+      image: ghcr.io/mailcow/netfilter:1.62
       stop_grace_period: 30s
       stop_grace_period: 30s
       restart: always
       restart: always
       privileged: true
       privileged: true
@@ -497,7 +497,7 @@ services:
         - /lib/modules:/lib/modules:ro
         - /lib/modules:/lib/modules:ro
 
 
     watchdog-mailcow:
     watchdog-mailcow:
-      image: ghcr.io/mailcow/watchdog:2.06
+      image: ghcr.io/mailcow/watchdog:2.07
       dns:
       dns:
         - ${IPV4_NETWORK:-172.22.1}.254
         - ${IPV4_NETWORK:-172.22.1}.254
       tmpfs:
       tmpfs:
@@ -569,7 +569,7 @@ services:
             - watchdog
             - watchdog
 
 
     dockerapi-mailcow:
     dockerapi-mailcow:
-      image: ghcr.io/mailcow/dockerapi:2.10
+      image: ghcr.io/mailcow/dockerapi:2.11
       security_opt:
       security_opt:
         - label=disable
         - label=disable
       restart: always
       restart: always
@@ -589,7 +589,7 @@ services:
             - dockerapi
             - dockerapi
 
 
     olefy-mailcow:
     olefy-mailcow:
-      image: ghcr.io/mailcow/olefy:1.13
+      image: ghcr.io/mailcow/olefy:1.14
       restart: always
       restart: always
       environment:
       environment:
         - TZ=${TZ}
         - 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 "The snapshots of your backup destination should run AFTER the cold standby script finished to ensure consistent snapshots."
 echo
 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() {
 function preflight_local_checks() {
   if [[ -z "${REMOTE_SSH_KEY}" ]]; then
   if [[ -z "${REMOTE_SSH_KEY}" ]]; then
     >&2 echo -e "\e[31mREMOTE_SSH_KEY is not set\e[0m"
     >&2 echo -e "\e[31mREMOTE_SSH_KEY is not set\e[0m"
@@ -139,11 +99,11 @@ EOF
 
 
 if [ $? = 0 ]; then
 if [ $? = 0 ]; then
   COMPOSE_COMMAND="docker compose"
   COMPOSE_COMMAND="docker compose"
-  echo "DEBUG: Using native docker compose on remote"
+  echo "INFO: Using native docker compose on remote"
 
 
 elif [ $? = 1 ]; then
 elif [ $? = 1 ]; then
   COMPOSE_COMMAND="docker-compose"
   COMPOSE_COMMAND="docker-compose"
-  echo "DEBUG: Using standalone docker compose on remote"
+  echo "INFO: Using standalone docker compose on remote"
 
 
 else
 else
   echo -e "\e[31mCannot find any Docker Compose on remote, exiting...\e[0m"
   echo -e "\e[31mCannot find any Docker Compose on remote, exiting...\e[0m"
@@ -324,7 +284,7 @@ echo "OK"
     -i "${REMOTE_SSH_KEY}" \
     -i "${REMOTE_SSH_KEY}" \
     ${REMOTE_SSH_HOST} \
     ${REMOTE_SSH_HOST} \
     -p ${REMOTE_SSH_PORT} \
     -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"
       >&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote"
   fi
   fi
 
 

+ 1 - 1
helper-scripts/backup_and_restore.sh

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

+ 18 - 6
update.sh

@@ -36,13 +36,19 @@ docker_garbage() {
   IMGS_TO_DELETE=()
   IMGS_TO_DELETE=()
 
 
   declare -A IMAGES_INFO
   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)
       ID=$(echo "$existing_image" | cut -d ':' -f 1)
       REPOSITORY=$(echo "$existing_image" | cut -d ':' -f 2)
       REPOSITORY=$(echo "$existing_image" | cut -d ':' -f 2)
       TAG=$(echo "$existing_image" | cut -d ':' -f 3)
       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
       if [[ " ${COMPOSE_IMAGES[@]} " =~ " ${REPOSITORY}:${TAG} " ]]; then
           continue
           continue
       else
       else
@@ -57,7 +63,7 @@ docker_garbage() {
           echo "    ${IMAGES_INFO[$id]} ($id)"
           echo "    ${IMAGES_INFO[$id]} ($id)"
       done
       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
           read -r -p "Do you want to delete them to free up some space? [y/N] " response
           if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
           if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
               docker rmi ${IMGS_TO_DELETE[*]}
               docker rmi ${IMGS_TO_DELETE[*]}
@@ -716,9 +722,16 @@ detect_major_update() {
     # Add major versions here
     # Add major versions here
     MAJOR_VERSIONS=(
     MAJOR_VERSIONS=(
       "2025-02"
       "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"
     release_url="https://github.com/mailcow/mailcow-dockerized/releases/tag"
 
 
     updates_to_apply=()
     updates_to_apply=()
@@ -735,8 +748,7 @@ detect_major_update() {
         echo "$update - $release_url/$update"
         echo "$update - $release_url/$update"
       done
       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
       read -p "Do you want to proceed with the update? [y/n] " response
       if [[ "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
       if [[ "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
         echo "Proceeding with the update..."
         echo "Proceeding with the update..."