Procházet zdrojové kódy

Merge pull request #5642 from mailcow/staging

2024-01
Niklas Meyer před 1 rokem
rodič
revize
b5db5dd0b4
47 změnil soubory, kde provedl 1333 přidání a 347 odebrání
  1. 3 2
      data/Dockerfiles/acme/Dockerfile
  2. 7 3
      data/Dockerfiles/clamd/Dockerfile
  3. 14 0
      data/Dockerfiles/clamd/clamdcheck.sh
  4. 6 4
      data/Dockerfiles/dockerapi/Dockerfile
  5. 50 49
      data/Dockerfiles/dockerapi/main.py
  6. 95 99
      data/Dockerfiles/dovecot/Dockerfile
  7. 4 0
      data/Dockerfiles/dovecot/docker-entrypoint.sh
  8. 2 3
      data/Dockerfiles/dovecot/quarantine_notify.py
  9. 1 1
      data/Dockerfiles/dovecot/quota_notify.py
  10. 4 0
      data/Dockerfiles/dovecot/supervisord.conf
  11. 5 5
      data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf
  12. 5 5
      data/Dockerfiles/dovecot/syslog-ng.conf
  13. 3 1
      data/Dockerfiles/netfilter/Dockerfile
  14. 2 1
      data/Dockerfiles/olefy/Dockerfile
  15. 4 4
      data/Dockerfiles/phpfpm/Dockerfile
  16. 1 1
      data/Dockerfiles/postfix/Dockerfile
  17. 2 2
      data/Dockerfiles/rspamd/Dockerfile
  18. 4 4
      data/Dockerfiles/sogo/Dockerfile
  19. 1 1
      data/Dockerfiles/solr/Dockerfile
  20. 5 3
      data/Dockerfiles/unbound/Dockerfile
  21. 85 8
      data/Dockerfiles/unbound/healthcheck.sh
  22. 2 2
      data/Dockerfiles/watchdog/Dockerfile
  23. 2 2
      data/Dockerfiles/watchdog/watchdog.sh
  24. 3 1
      data/conf/postfix/main.cf
  25. 17 3
      data/conf/postfix/postscreen_access.cidr
  26. 2 1
      data/conf/rspamd/dynmaps/footer.php
  27. 8 0
      data/conf/rspamd/lua/rspamd.local.lua
  28. 1 0
      data/conf/sogo/sogo.conf
  29. 1 1
      data/web/css/build/014-mailcow.css
  30. 3 0
      data/web/css/themes/mailcow-darkmode.css
  31. 18 0
      data/web/inc/functions.customize.inc.php
  32. 77 51
      data/web/inc/functions.mailbox.inc.php
  33. 2 1
      data/web/inc/init_db.inc.php
  34. 622 0
      data/web/inc/lib/ssp.class.php
  35. 9 0
      data/web/inc/vars.inc.php
  36. 42 28
      data/web/js/site/mailbox.js
  37. 95 15
      data/web/json_api.php
  38. 17 9
      data/web/lang/lang.de-de.json
  39. 4 0
      data/web/lang/lang.en-gb.json
  40. 3 1
      data/web/templates/base.twig
  41. 8 0
      data/web/templates/edit/domain.twig
  42. 4 4
      data/web/templates/modals/footer.twig
  43. 17 14
      docker-compose.yml
  44. 3 3
      generate_config.sh
  45. 29 1
      helper-scripts/_cold-standby.sh
  46. 36 9
      helper-scripts/backup_and_restore.sh
  47. 5 5
      update.sh

+ 3 - 2
data/Dockerfiles/acme/Dockerfile

@@ -1,7 +1,8 @@
-FROM alpine:3.17
+FROM alpine:3.19
 
-LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
+LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
 
+ARG PIP_BREAK_SYSTEM_PACKAGES=1
 RUN apk upgrade --no-cache \
   && apk add --update --no-cache \
   bash \

+ 7 - 3
data/Dockerfiles/clamd/Dockerfile

@@ -1,12 +1,14 @@
-FROM clamav/clamav:1.0.3_base
+FROM alpine:3.19
 
-LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
+LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
 
 RUN apk upgrade --no-cache \
   && apk add --update --no-cache \
   rsync \
+  clamav \
   bind-tools \
-  bash 
+  bash \
+  tini
 
 # init
 COPY clamd.sh /clamd.sh
@@ -14,7 +16,9 @@ RUN chmod +x /sbin/tini
 
 # healthcheck
 COPY healthcheck.sh /healthcheck.sh
+COPY clamdcheck.sh /usr/local/bin
 RUN chmod +x /healthcheck.sh
+RUN chmod +x /usr/local/bin/clamdcheck.sh
 HEALTHCHECK --start-period=6m CMD "/healthcheck.sh"
 
 ENTRYPOINT []

+ 14 - 0
data/Dockerfiles/clamd/clamdcheck.sh

@@ -0,0 +1,14 @@
+#!/bin/sh
+
+set -eu
+
+if [ "${CLAMAV_NO_CLAMD:-}" != "false" ]; then
+	if [ "$(echo "PING" | nc localhost 3310)" != "PONG" ]; then
+		echo "ERROR: Unable to contact server"
+		exit 1
+	fi
+
+	echo "Clamd is up"
+fi
+
+exit 0

+ 6 - 4
data/Dockerfiles/dockerapi/Dockerfile

@@ -1,7 +1,8 @@
-FROM alpine:3.17
+FROM alpine:3.19
 
-LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
+LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
 
+ARG PIP_BREAK_SYSTEM_PACKAGES=1
 WORKDIR /app
 
 RUN apk add --update --no-cache python3 \
@@ -9,12 +10,13 @@ RUN apk add --update --no-cache python3 \
   openssl \
   tzdata \
   py3-psutil \
+  py3-redis \
+  py3-async-timeout \
 && pip3 install --upgrade pip \
   fastapi \
   uvicorn \
   aiodocker \
-  docker \
-  aioredis 
+  docker
 RUN mkdir /app/modules
 
 COPY docker-entrypoint.sh /app/

+ 50 - 49
data/Dockerfiles/dockerapi/main.py

@@ -5,16 +5,63 @@ import json
 import uuid
 import async_timeout
 import asyncio
-import aioredis
 import aiodocker
 import docker
 import logging
 from logging.config import dictConfig
 from fastapi import FastAPI, Response, Request
 from modules.DockerApi import DockerApi
+from redis import asyncio as aioredis
+from contextlib import asynccontextmanager
 
 dockerapi = None
-app = FastAPI()
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+  global dockerapi
+
+  # Initialize a custom logger
+  logger = logging.getLogger("dockerapi")
+  logger.setLevel(logging.INFO)
+  # Configure the logger to output logs to the terminal
+  handler = logging.StreamHandler()
+  handler.setLevel(logging.INFO)
+  formatter = logging.Formatter("%(levelname)s:     %(message)s")
+  handler.setFormatter(formatter)
+  logger.addHandler(handler)
+
+  logger.info("Init APP")
+
+  # Init redis client
+  if os.environ['REDIS_SLAVEOF_IP'] != "":
+    redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0")
+  else:
+    redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0")
+
+  # Init docker clients
+  sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
+  async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
+
+  dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
+
+  logger.info("Subscribe to redis channel")
+  # Subscribe to redis channel
+  dockerapi.pubsub = redis.pubsub()
+  await dockerapi.pubsub.subscribe("MC_CHANNEL")
+  asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
+
+
+  yield
+
+  # Close docker connections
+  dockerapi.sync_docker_client.close()
+  await dockerapi.async_docker_client.close()
+
+  # Close redis
+  await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
+  await dockerapi.redis_client.close()
+
+app = FastAPI(lifespan=lifespan)
 
 # Define Routes
 @app.get("/host/stats")
@@ -144,53 +191,7 @@ async def post_container_update_stats(container_id : str):
 
   stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats'))
   return Response(content=json.dumps(stats, indent=4), media_type="application/json")
-
-# Events
-@app.on_event("startup")
-async def startup_event():
-  global dockerapi
-
-  # Initialize a custom logger
-  logger = logging.getLogger("dockerapi")
-  logger.setLevel(logging.INFO)
-  # Configure the logger to output logs to the terminal
-  handler = logging.StreamHandler()
-  handler.setLevel(logging.INFO)
-  formatter = logging.Formatter("%(levelname)s:     %(message)s")
-  handler.setFormatter(formatter)
-  logger.addHandler(handler)
-
-  logger.info("Init APP")
-
-  # Init redis client
-  if os.environ['REDIS_SLAVEOF_IP'] != "":
-    redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0")
-  else:
-    redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0")
-
-  # Init docker clients
-  sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
-  async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
-
-  dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
-
-  logger.info("Subscribe to redis channel")
-  # Subscribe to redis channel
-  dockerapi.pubsub = redis.pubsub()
-  await dockerapi.pubsub.subscribe("MC_CHANNEL")
-  asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
-
-@app.on_event("shutdown")
-async def shutdown_event():
-  global dockerapi
-
-  # Close docker connections
-  dockerapi.sync_docker_client.close()
-  await dockerapi.async_docker_client.close()
-
-  # Close redis
-  await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
-  await dockerapi.redis_client.close()
+  
 
 # PubSub Handler
 async def handle_pubsub_messages(channel: aioredis.client.PubSub):

+ 95 - 99
data/Dockerfiles/dovecot/Dockerfile

@@ -1,119 +1,115 @@
-FROM debian:bullseye-slim
-LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
+FROM alpine:3.19
+LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
 
-ARG DEBIAN_FRONTEND=noninteractive
-# renovate: datasource=github-tags depName=dovecot/core versioning=semver-coerced extractVersion=(?<version>.*)$
-ARG DOVECOT=2.3.21
-# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=(?<version>.*)$
+# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
 ARG GOSU_VERSION=1.16
-ENV LC_ALL C
 
+ENV LANG C.UTF-8
+ENV LC_ALL C.UTF-8
 
 # Add groups and users before installing Dovecot to not break compatibility
-RUN groupadd -g 5000 vmail \
-  && groupadd -g 401 dovecot \
-  && groupadd -g 402 dovenull \
-  && groupadd -g 999 sogo \
-  && usermod -a -G sogo nobody \
-  && useradd -g vmail -u 5000 vmail -d /var/vmail \
-  && useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \
-  && useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull \
-  && touch /etc/default/locale \
-  && apt-get update \
-  && apt-get -y --no-install-recommends install \
-  build-essential \
-  apt-transport-https \
+RUN addgroup -g 5000 vmail \
+  && addgroup -g 401 dovecot \
+  && addgroup -g 402 dovenull \
+  && sed -i "s/999/99/" /etc/group \
+  && addgroup -g 999 sogo \
+  && addgroup nobody sogo \
+  && adduser -D -u 5000 -G vmail -h /var/vmail vmail \
+  && adduser -D -G dovecot -u 401 -h /dev/null -s /sbin/nologin dovecot \
+  && adduser -D -G dovenull -u 402 -h /dev/null -s /sbin/nologin dovenull \
+  && apk add --no-cache --update \
+  bash \
+  bind-tools \
+  findutils \
+  envsubst \
   ca-certificates \
-  cpanminus \
   curl \
-  dnsutils \
-  dirmngr \
-  gettext \
-  gnupg2 \
   jq \
-  libauthen-ntlm-perl \
-  libcgi-pm-perl \
-  libcrypt-openssl-rsa-perl \
-  libcrypt-ssleay-perl \
-  libdata-uniqid-perl \
-  libdbd-mysql-perl \
-  libdbi-perl \
-  libdigest-hmac-perl \
-  libdist-checkconflicts-perl \
-  libencode-imaputf7-perl \
-  libfile-copy-recursive-perl \
-  libfile-tail-perl \
-  libhtml-parser-perl \
-  libio-compress-perl \
-  libio-socket-inet6-perl \
-  libio-socket-ssl-perl \
-  libio-tee-perl \
-  libipc-run-perl \
-  libjson-webtoken-perl \
-  liblockfile-simple-perl \
-  libmail-imapclient-perl \
-  libmodule-implementation-perl \
-  libmodule-scandeps-perl \
-  libnet-ssleay-perl \
-  libpackage-stash-perl \
-  libpackage-stash-xs-perl \
-  libpar-packer-perl \
-  libparse-recdescent-perl \
-  libproc-processtable-perl \
-  libreadonly-perl \
-  libregexp-common-perl \
-  libssl-dev \
-  libsys-meminfo-perl \
-  libterm-readkey-perl \
-  libtest-deep-perl \
-  libtest-fatal-perl \
-  libtest-mock-guard-perl \
-  libtest-mockobject-perl \
-  libtest-nowarnings-perl \
-  libtest-pod-perl \
-  libtest-requires-perl \
-  libtest-simple-perl \
-  libtest-warn-perl \
-  libtry-tiny-perl \
-  libunicode-string-perl \
-  liburi-perl \
-  libwww-perl \
-  lua-sql-mysql \
+  lua \
+  lua-cjson \
   lua-socket \
+  lua-sql-mysql \
+  lua5.3-sql-mysql \
+  icu-data-full \
+  mariadb-connector-c \
+  gcompat \
   mariadb-client \
+  perl \
+  perl-ntlm \
+  perl-cgi \
+  perl-crypt-openssl-rsa \
+  perl-utils \
+  perl-crypt-ssleay \
+  perl-data-uniqid \
+  perl-dbd-mysql \
+  perl-dbi \
+  perl-digest-hmac \
+  perl-dist-checkconflicts \
+  perl-encode-imaputf7 \
+  perl-file-copy-recursive \
+  perl-file-tail \
+  perl-io-socket-inet6 \
+  perl-io-gzip \
+  perl-io-socket-ssl \
+  perl-io-tee \
+  perl-ipc-run \
+  perl-json-webtoken \
+  perl-mail-imapclient \
+  perl-module-implementation \
+  perl-module-scandeps \
+  perl-net-ssleay \
+  perl-package-stash \
+  perl-package-stash-xs \
+  perl-par-packer \
+  perl-parse-recdescent \
+  perl-lockfile-simple --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/ \
+  libproc \
+  perl-readonly \
+  perl-regexp-common \
+  perl-sys-meminfo \
+  perl-term-readkey \
+  perl-test-deep \
+  perl-test-fatal \
+  perl-test-mockobject \
+  perl-test-mock-guard \
+  perl-test-pod \
+  perl-test-requires \
+  perl-test-simple \
+  perl-test-warn \
+  perl-try-tiny \
+  perl-unicode-string \
+  perl-proc-processtable \
+  perl-app-cpanminus \
   procps \
-  python3-pip \
-  redis-server \
-  supervisor \
+  python3 \
+  py3-mysqlclient \
+  py3-html2text \
+  py3-jinja2 \
+  py3-redis \
+  redis \
   syslog-ng \
-  syslog-ng-core \
-  syslog-ng-mod-redis \
+  syslog-ng-redis \
+  syslog-ng-json \
+  supervisor \
+  tzdata \
   wget \
-  && dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
-  && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
-  && chmod +x /usr/local/bin/gosu \
-  && gosu nobody true \
-  && apt-key adv --fetch-keys https://repo.dovecot.org/DOVECOT-REPO-GPG \
-  && echo "deb https://repo.dovecot.org/ce-${DOVECOT}/debian/bullseye bullseye main" > /etc/apt/sources.list.d/dovecot.list \
-  && apt-get update \
-  && apt-get -y --no-install-recommends install \
-  dovecot-lua \
-  dovecot-managesieved \
-  dovecot-sieve \
+  dovecot \
+  dovecot-dev \
   dovecot-lmtpd \
+  dovecot-lua \
   dovecot-ldap \
   dovecot-mysql \
-  dovecot-core \
+  dovecot-sql \
+  dovecot-submissiond \
+  dovecot-pigeonhole-plugin \
   dovecot-pop3d \
-  dovecot-imapd \
-  dovecot-solr \
-  && pip3 install mysql-connector-python html2text jinja2 redis \
-  && apt-get autoremove --purge -y \
-  && apt-get autoclean \
-  && rm -rf /var/lib/apt/lists/* \
-  && rm -rf /tmp/* /var/tmp/* /root/.cache/
-# imapsync dependencies
-RUN cpan Crypt::OpenSSL::PKCS12
+  dovecot-fts-solr \
+  && arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
+  && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$arch" \
+  && chmod +x /usr/local/bin/gosu \
+  && gosu nobody true
+
+# RUN cpan LockFile::Simple
 
 COPY trim_logs.sh /usr/local/bin/trim_logs.sh
 COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh

+ 4 - 0
data/Dockerfiles/dovecot/docker-entrypoint.sh

@@ -432,4 +432,8 @@ done
 # May be related to something inside Docker, I seriously don't know
 touch /etc/dovecot/lua/passwd-verify.lua
 
+if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
+  cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
+fi
+
 exec "$@"

+ 2 - 3
data/Dockerfiles/dovecot/quarantine_notify.py

@@ -3,11 +3,10 @@
 import smtplib
 import os
 import sys
-import mysql.connector
+import MySQLdb
 from email.mime.multipart import MIMEMultipart
 from email.mime.text import MIMEText
 from email.utils import COMMASPACE, formatdate
-import cgi
 import jinja2
 from jinja2 import Template
 import json
@@ -50,7 +49,7 @@ try:
   def query_mysql(query, headers = True, update = False):
     while True:
       try:
-        cnx = mysql.connector.connect(unix_socket = '/var/run/mysqld/mysqld.sock', user=os.environ.get('DBUSER'), passwd=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
+        cnx = MySQLdb.connect(user=os.environ.get('DBUSER'), password=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
       except Exception as ex:
         print('%s - trying again...'  % (ex))
         time.sleep(3)

+ 1 - 1
data/Dockerfiles/dovecot/quota_notify.py

@@ -55,7 +55,7 @@ try:
   msg.attach(text_part)
   msg.attach(html_part)
   msg['To'] = username
-  p = Popen(['/usr/lib/dovecot/dovecot-lda', '-d', username, '-o', '"plugin/quota=maildir:User quota:noenforcing"'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)
+  p = Popen(['/usr/libexec/dovecot/dovecot-lda', '-d', username, '-o', '"plugin/quota=maildir:User quota:noenforcing"'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)
   p.communicate(input=bytes(msg.as_string(), 'utf-8'))
 
   domain = username.split("@")[-1]

+ 4 - 0
data/Dockerfiles/dovecot/supervisord.conf

@@ -13,6 +13,10 @@ autostart=true
 
 [program:dovecot]
 command=/usr/sbin/dovecot -F
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/stderr
+stderr_logfile_maxbytes=0
 autorestart=true
 
 [eventlistener:processes]

+ 5 - 5
data/Dockerfiles/dovecot/syslog-ng-redis_slave.conf

@@ -1,4 +1,4 @@
-@version: 3.28
+@version: 4.5
 @include "scl.conf"
 options {
   chain_hostnames(off);
@@ -6,11 +6,11 @@ options {
   use_dns(no);
   use_fqdn(no);
   owner("root"); group("adm"); perm(0640);
-  stats_freq(0);
+  stats(freq(0));
   bad_hostname("^gconfd$");
 };
-source s_src {
-  unix-stream("/dev/log");
+source s_dgram {
+  unix-dgram("/dev/log");
   internal();
 };
 destination d_stdout { pipe("/dev/stdout"); };
@@ -36,7 +36,7 @@ filter f_replica {
   not match("Error: sync: Unknown user in remote" value("MESSAGE"));
 };
 log {
-  source(s_src);
+  source(s_dgram);
   filter(f_replica);
   destination(d_stdout);
   filter(f_mail);

+ 5 - 5
data/Dockerfiles/dovecot/syslog-ng.conf

@@ -1,4 +1,4 @@
-@version: 3.28
+@version: 4.5
 @include "scl.conf"
 options {
   chain_hostnames(off);
@@ -6,11 +6,11 @@ options {
   use_dns(no);
   use_fqdn(no);
   owner("root"); group("adm"); perm(0640);
-  stats_freq(0);
+  stats(freq(0));
   bad_hostname("^gconfd$");
 };
-source s_src {
-  unix-stream("/dev/log");
+source s_dgram {
+  unix-dgram("/dev/log");
   internal();
 };
 destination d_stdout { pipe("/dev/stdout"); };
@@ -36,7 +36,7 @@ filter f_replica {
   not match("Error: sync: Unknown user in remote" value("MESSAGE"));
 };
 log {
-  source(s_src);
+  source(s_dgram);
   filter(f_replica);
   destination(d_stdout);
   filter(f_mail);

+ 3 - 1
data/Dockerfiles/netfilter/Dockerfile

@@ -1,8 +1,9 @@
-FROM alpine:3.17
+FROM alpine:3.19
 LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
 
 WORKDIR /app
 
+ARG PIP_BREAK_SYSTEM_PACKAGES=1
 ENV XTABLES_LIBDIR /usr/lib/xtables
 ENV PYTHON_IPTABLES_XTABLES_VERSION 12
 ENV IPTABLES_LIBDIR /usr/lib
@@ -14,6 +15,7 @@ RUN apk add --virtual .build-deps \
   openssl-dev \
 && apk add -U python3 \
   iptables \
+  iptables-dev \
   ip6tables \
   xtables-addons \
   nftables \

+ 2 - 1
data/Dockerfiles/olefy/Dockerfile

@@ -1,6 +1,7 @@
-FROM alpine:3.17
+FROM alpine:3.19
 LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
 
+ARG PIP_BREAK_SYSTEM_PACKAGES=1
 WORKDIR /app
 
 #RUN addgroup -S olefy && adduser -S olefy -G olefy \

+ 4 - 4
data/Dockerfiles/phpfpm/Dockerfile

@@ -1,8 +1,8 @@
-FROM php:8.2-fpm-alpine3.17
+FROM php:8.2-fpm-alpine3.19
 LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
 
 # renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
-ARG APCU_PECL_VERSION=5.1.22
+ARG APCU_PECL_VERSION=5.1.23
 # renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
 ARG IMAGICK_PECL_VERSION=3.7.0
 # renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
@@ -10,9 +10,9 @@ ARG MAILPARSE_PECL_VERSION=3.1.6
 # renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
 ARG MEMCACHED_PECL_VERSION=3.2.0
 # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
-ARG REDIS_PECL_VERSION=6.0.1
+ARG REDIS_PECL_VERSION=6.0.2
 # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
-ARG COMPOSER_VERSION=2.6.5
+ARG COMPOSER_VERSION=2.6.6
 
 RUN apk add -U --no-cache autoconf \
   aspell-dev \

+ 1 - 1
data/Dockerfiles/postfix/Dockerfile

@@ -1,5 +1,5 @@
 FROM debian:bullseye-slim
-LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
+LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
 
 ARG DEBIAN_FRONTEND=noninteractive
 ENV LC_ALL C

+ 2 - 2
data/Dockerfiles/rspamd/Dockerfile

@@ -1,5 +1,5 @@
 FROM debian:bullseye-slim
-LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
+LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
 
 ARG DEBIAN_FRONTEND=noninteractive
 ARG CODENAME=bullseye
@@ -13,7 +13,7 @@ RUN apt-get update && apt-get install -y \
   dnsutils \
   netcat \
   && apt-key adv --fetch-keys https://rspamd.com/apt-stable/gpg.key \
-  && echo "deb [arch=amd64] https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \
+  && echo "deb https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \
   && apt-get update \
   && apt-get --no-install-recommends -y install rspamd redis-tools procps nano \
   && rm -rf /var/lib/apt/lists/* \

+ 4 - 4
data/Dockerfiles/sogo/Dockerfile

@@ -1,10 +1,10 @@
 FROM debian:bullseye-slim
-LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
+LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
 
 ARG DEBIAN_FRONTEND=noninteractive
-ARG SOGO_DEBIAN_REPOSITORY=http://packages.sogo.nu/nightly/5/debian/
+ARG SOGO_DEBIAN_REPOSITORY=http://www.axis.cz/linux/debian
 # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
-ARG GOSU_VERSION=1.16
+ARG GOSU_VERSION=1.17
 ENV LC_ALL C
 
 # Prerequisites
@@ -32,7 +32,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
   && mkdir /usr/share/doc/sogo \
   && touch /usr/share/doc/sogo/empty.sh \
   && apt-key adv --keyserver keys.openpgp.org --recv-key 74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 \
-  && echo "deb ${SOGO_DEBIAN_REPOSITORY} bullseye bullseye" > /etc/apt/sources.list.d/sogo.list \
+  && echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} bullseye sogo-v5" > /etc/apt/sources.list.d/sogo.list \
   && apt-get update && apt-get install -y --no-install-recommends \
     sogo \
     sogo-activesync \

+ 1 - 1
data/Dockerfiles/solr/Dockerfile

@@ -3,7 +3,7 @@ FROM solr:7.7-slim
 USER root
 
 # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=(?<version>.*)$
-ARG GOSU_VERSION=1.16
+ARG GOSU_VERSION=1.17
 
 COPY solr.sh /
 COPY solr-config-7.7.0.xml /

+ 5 - 3
data/Dockerfiles/unbound/Dockerfile

@@ -1,9 +1,11 @@
-FROM alpine:3.17
+FROM alpine:3.19
 
-LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
+LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
 
 RUN apk add --update --no-cache \
 	curl \
+	bind-tools \
+	netcat-openbsd \
 	unbound \
 	bash \
 	openssl \
@@ -21,7 +23,7 @@ COPY docker-entrypoint.sh /docker-entrypoint.sh
 # healthcheck (nslookup)
 COPY healthcheck.sh /healthcheck.sh
 RUN chmod +x /healthcheck.sh
-HEALTHCHECK --interval=30s --timeout=10s CMD [ "/healthcheck.sh" ]
+HEALTHCHECK --interval=5s --timeout=10s CMD [ "/healthcheck.sh" ]
 
 ENTRYPOINT ["/docker-entrypoint.sh"]
 

+ 85 - 8
data/Dockerfiles/unbound/healthcheck.sh

@@ -1,12 +1,89 @@
 #!/bin/bash
 
-nslookup mailcow.email 127.0.0.1 1> /dev/null
-
-if [ $? == 0 ]; then
-    echo "DNS resolution is working!"
-    exit 0
-else
-    echo "DNS resolution is not working correctly..."
-    echo "Maybe check your outbound firewall, as it needs to resolve DNS over TCP AND UDP!"
+# Declare log function for logfile inside container
+function log_to_file() {
+    echo "$(date +"%Y-%m-%d %H:%M:%S"): $1" > /var/log/healthcheck.log
+}
+
+# General Ping function to check general pingability
+function check_ping() {
+    declare -a ipstoping=("1.1.1.1" "8.8.8.8" "9.9.9.9")
+
+    for ip in "${ipstoping[@]}" ; do
+            ping -q -c 3 -w 5 "$ip"
+            if [ $? -ne 0 ]; then
+                log_to_file "Healthcheck: Couldn't ping $ip for 5 seconds... Gave up!"
+                log_to_file "Please check your internet connection or firewall rules to fix this error, because a simple ping test should always go through from the unbound container!"
+                return 1
+            fi
+    done
+
+    log_to_file "Healthcheck: Ping Checks WORKING properly!"
+    return 0
+}
+
+# General DNS Resolve Check against Unbound Resolver himself
+function check_dns() {
+    declare -a domains=("mailcow.email" "github.com" "hub.docker.com")
+
+    for domain in "${domains[@]}" ; do
+        for ((i=1; i<=3; i++)); do
+            dig +short +timeout=2 +tries=1 "$domain" @127.0.0.1 > /dev/null
+        if [ $? -ne 0 ]; then
+            log_to_file "Healthcheck: DNS Resolution Failed on $i attempt! Trying again..."
+            if [ $i -eq 3 ]; then
+                log_to_file "Healthcheck: DNS Resolution not possible after $i attempts... Gave up!"
+                log_to_file "Maybe check your outbound firewall, as it needs to resolve DNS over TCP AND UDP!"
+                return 1
+            fi
+        fi
+        done
+    done
+
+    log_to_file "Healthcheck: DNS Resolver WORKING properly!"
+    return 0
+    
+}
+
+# Simple Netcat Check to connect to common webports
+function check_netcat() {
+    declare -a domains=("mailcow.email" "github.com" "hub.docker.com")
+    declare -a ports=("80" "443")
+
+    for domain in "${domains[@]}" ; do
+        for port in "${ports[@]}" ; do
+            nc -z -w 2 $domain $port
+            if [ $? -ne 0 ]; then
+                log_to_file "Healthcheck: Could not reach $domain on Port $port... Gave up!"
+                log_to_file "Please check your internet connection or firewall rules to fix this error."
+                return 1
+            fi
+        done
+    done
+
+    log_to_file "Healthcheck: Netcat Checks WORKING properly!"
+    return 0
+
+}
+
+# run checks, if check is not returning 0 (return value if check is ok), healthcheck will exit with 1 (marked in docker as unhealthy)
+check_ping
+
+if [ $? -ne 0 ]; then
+    exit 1
+fi
+
+check_dns
+
+if [ $? -ne 0 ]; then
     exit 1
 fi
+
+check_netcat
+
+if [ $? -ne 0 ]; then
+    exit 1
+fi
+
+log_to_file "Healthcheck: ALL CHECKS WERE SUCCESSFUL! Unbound is healthy!"
+exit 0

+ 2 - 2
data/Dockerfiles/watchdog/Dockerfile

@@ -1,5 +1,5 @@
-FROM alpine:3.17
-LABEL maintainer "André Peters <andre.peters@servercow.de>"
+FROM alpine:3.19
+LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
 
 # Installation
 RUN apk add --update \

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

@@ -716,8 +716,8 @@ rspamd_checks() {
 From: watchdog@localhost
 
 Empty
-' | usr/bin/curl --max-time 10 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan | jq -rc .default.required_score)
-    if [[ ${SCORE} != "9999" ]]; then
+' | usr/bin/curl --max-time 10 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan | jq -rc .default.required_score | sed 's/\..*//' )
+    if [[ ${SCORE} -ne 9999 ]]; then
       echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2
       err_count=$(( ${err_count} + 1))
     else

+ 3 - 1
data/conf/postfix/main.cf

@@ -85,6 +85,7 @@ smtp_tls_security_level = dane
 smtpd_data_restrictions = reject_unauth_pipelining, permit
 smtpd_delay_reject = yes
 smtpd_error_sleep_time = 10s
+smtpd_forbid_bare_newline = yes
 smtpd_hard_error_limit = ${stress?1}${stress:5}
 smtpd_helo_required = yes
 smtpd_proxy_timeout = 600s
@@ -161,7 +162,8 @@ transport_maps = pcre:/opt/postfix/conf/custom_transport.pcre,
   proxy:mysql:/opt/postfix/conf/sql/mysql_relay_ne.cf,
   proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf
 smtp_sasl_auth_soft_bounce = no
-postscreen_discard_ehlo_keywords = silent-discard, dsn
+postscreen_discard_ehlo_keywords = silent-discard, dsn, chunking
+smtpd_discard_ehlo_keywords = chunking
 compatibility_level = 2
 smtputf8_enable = no
 # Define protocols for SMTPS and submission service

+ 17 - 3
data/conf/postfix/postscreen_access.cidr

@@ -1,6 +1,6 @@
-# Whitelist generated by Postwhite v3.4 on Fri Dec  1 00:15:18 UTC 2023
+# Whitelist generated by Postwhite v3.4 on Mon Jan  1 00:15:22 UTC 2024
 # https://github.com/stevejenkins/postwhite/
-# 2038 total rules
+# 2052 total rules
 2a00:1450:4000::/36	permit
 2a01:111:f400::/48	permit
 2a01:111:f403:8000::/50	permit
@@ -13,7 +13,7 @@
 3.70.123.177	permit
 3.93.157.0/24	permit
 3.129.120.190	permit
-3.137.78.75	permit
+3.137.16.58	permit
 3.210.190.0/24	permit
 8.20.114.31	permit
 8.25.194.0/23	permit
@@ -183,6 +183,8 @@
 50.18.125.237	permit
 50.18.126.162	permit
 50.31.32.0/19	permit
+50.56.130.220	permit
+50.56.130.221	permit
 51.137.58.21	permit
 51.140.75.55	permit
 51.144.100.179	permit
@@ -596,6 +598,7 @@
 74.208.5.64/26	permit
 74.208.122.0/26	permit
 74.209.250.0/24	permit
+75.2.70.75	permit
 76.223.128.0/19	permit
 76.223.176.0/20	permit
 77.238.176.0/22	permit
@@ -1186,6 +1189,7 @@
 98.139.245.208/30	permit
 98.139.245.212/31	permit
 99.78.197.208/28	permit
+99.83.190.102	permit
 103.2.140.0/22	permit
 103.9.96.0/22	permit
 103.28.42.0/24	permit
@@ -1460,6 +1464,8 @@
 144.178.38.0/24	permit
 145.253.228.160/29	permit
 145.253.239.128/29	permit
+146.20.14.105	permit
+146.20.14.107	permit
 146.20.112.0/26	permit
 146.20.113.0/24	permit
 146.20.191.0/24	permit
@@ -1534,6 +1540,10 @@
 163.47.180.0/23	permit
 163.114.130.16	permit
 163.114.132.120	permit
+164.177.132.168	permit
+164.177.132.169	permit
+164.177.132.170	permit
+164.177.132.171	permit
 165.173.128.0/24	permit
 166.78.68.0/22	permit
 166.78.68.221	permit
@@ -1726,6 +1736,7 @@
 199.34.22.36	permit
 199.59.148.0/22	permit
 199.67.80.2	permit
+199.67.82.2	permit
 199.67.84.0/24	permit
 199.67.86.0/24	permit
 199.67.88.0/24	permit
@@ -1789,6 +1800,7 @@
 204.92.114.187	permit
 204.92.114.203	permit
 204.92.114.204/31	permit
+204.132.224.66	permit
 204.141.32.0/23	permit
 204.141.42.0/23	permit
 204.220.160.0/20	permit
@@ -1832,6 +1844,8 @@
 207.67.98.192/27	permit
 207.68.176.0/26	permit
 207.68.176.96/27	permit
+207.97.204.96	permit
+207.97.204.97	permit
 207.126.144.0/20	permit
 207.171.160.0/19	permit
 207.211.30.64/26	permit

+ 2 - 1
data/conf/rspamd/dynmaps/footer.php

@@ -49,13 +49,14 @@ $from = $headers['From'];
 $empty_footer = json_encode(array(
   'html' => '',
   'plain' => '',
+  'skip_replies' => 0,
   'vars' => array()
 ));
 
 error_log("FOOTER: checking for domain " . $domain . ", user " . $username . " and address " . $from . PHP_EOL);
 
 try {
-  $stmt = $pdo->prepare("SELECT `plain`, `html`, `mbox_exclude` FROM `domain_wide_footer` 
+  $stmt = $pdo->prepare("SELECT `plain`, `html`, `mbox_exclude`, `skip_replies` FROM `domain_wide_footer` 
     WHERE `domain` = :domain");
   $stmt->execute(array(
     ':domain' => $domain

+ 8 - 0
data/conf/rspamd/lua/rspamd.local.lua

@@ -567,6 +567,14 @@ rspamd_config:register_symbol({
           if footer and type(footer) == "table" and (footer.html and footer.html ~= "" or footer.plain and footer.plain ~= "")  then
             rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s, vars=%s", uname, footer.html, footer.plain, footer.vars)
 
+            if footer.skip_replies ~= 0 then
+              in_reply_to = task:get_header_raw('in-reply-to')
+              if in_reply_to then
+                rspamd_logger.infox(rspamd_config, "mail is a reply - skip footer")
+                return
+              end
+            end
+
             local envfrom_mime = task:get_from(2)
             local from_name = ""
             if envfrom_mime and envfrom_mime[1].name then

+ 1 - 0
data/conf/sogo/sogo.conf

@@ -12,6 +12,7 @@
     SOGoJunkFolderName= "Junk";
     SOGoMailDomain = "sogo.local";
     SOGoEnableEMailAlarms = YES;
+    SOGoMailHideInlineAttachments = YES;
     SOGoFoldersSendEMailNotifications = YES;
     SOGoForwardEnabled = YES;
 

+ 1 - 1
data/web/css/build/014-mailcow.css

@@ -228,8 +228,8 @@ legend {
 	margin-top: 20px;
 }
 .slave-info {
-  padding: 15px 0px 15px 15px;
   font-weight: bold;
+  color: orange;
 }
 .alert-hr {
   margin:3px 0px;

+ 3 - 0
data/web/css/themes/mailcow-darkmode.css

@@ -175,6 +175,9 @@ pre {
     background-color: #282828;
     border: 1px solid #555;
 }
+.form-control {
+    background-color: transparent;
+}
 input.form-control, textarea.form-control {
     color: #e2e2e2 !important;
     background-color: #424242 !important;

+ 18 - 0
data/web/inc/functions.customize.inc.php

@@ -2,6 +2,7 @@
 function customize($_action, $_item, $_data = null) {
 	global $redis;
 	global $lang;
+  global $LOGO_LIMITS;
   
   switch ($_action) {
     case 'add':
@@ -35,6 +36,23 @@ function customize($_action, $_item, $_data = null) {
                 );
                 return false;
               }
+              if ($_data[$_item]['size'] > $LOGO_LIMITS['max_size']) {
+                $_SESSION['return'][] = array(
+                  'type' => 'danger',
+                  'log' => array(__FUNCTION__, $_action, $_item, $_data),
+                  'msg' => 'img_size_exceeded'
+                );
+                return false;
+              }
+              list($width, $height) = getimagesize($_data[$_item]['tmp_name']);
+              if ($width > $LOGO_LIMITS['max_width'] || $height > $LOGO_LIMITS['max_height']) {
+                $_SESSION['return'][] = array(
+                  'type' => 'danger',
+                  'log' => array(__FUNCTION__, $_action, $_item, $_data),
+                  'msg' => 'img_dimensions_exceeded'
+                );
+                return false;
+              }
               $image = new Imagick($_data[$_item]['tmp_name']);
               if ($image->valid() !== true) {
                 $_SESSION['return'][] = array(

+ 77 - 51
data/web/inc/functions.mailbox.inc.php

@@ -478,16 +478,24 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             return false;
           }
+          $DOMAIN_DEFAULT_ATTRIBUTES = null;
+          if ($_data['template']){
+            $DOMAIN_DEFAULT_ATTRIBUTES = mailbox('get', 'domain_templates', $_data['template'])['attributes'];
+          }
+          if (empty($DOMAIN_DEFAULT_ATTRIBUTES)) {
+            $DOMAIN_DEFAULT_ATTRIBUTES = mailbox('get', 'domain_templates')[0]['attributes'];
+          }
+
           $domain       = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
           $description  = $_data['description'];
           if (empty($description)) $description = $domain;
-          $tags         = (array)$_data['tags'];
-          $aliases      = (int)$_data['aliases'];
-          $mailboxes    = (int)$_data['mailboxes'];
-          $defquota     = (int)$_data['defquota'];
-          $maxquota     = (int)$_data['maxquota'];
+          $tags         = (isset($_data['tags'])) ? (array)$_data['tags'] : $DOMAIN_DEFAULT_ATTRIBUTES['tags'];
+          $aliases      = (isset($_data['aliases'])) ? (int)$_data['aliases'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_num_aliases_for_domain'];
+          $mailboxes    = (isset($_data['mailboxes'])) ? (int)$_data['mailboxes'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_num_mboxes_for_domain'];
+          $defquota     = (isset($_data['defquota'])) ? (int)$_data['defquota'] : $DOMAIN_DEFAULT_ATTRIBUTES['def_quota_for_mbox'] / 1024 ** 2;
+          $maxquota     = (isset($_data['maxquota'])) ? (int)$_data['maxquota'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_quota_for_mbox'] / 1024 ** 2;
           $restart_sogo = (int)$_data['restart_sogo'];
-          $quota        = (int)$_data['quota'];
+          $quota        = (isset($_data['quota'])) ? (int)$_data['quota'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_quota_for_domain'] / 1024 ** 2;
           if ($defquota > $maxquota) {
             $_SESSION['return'][] = array(
                 'type' => 'danger',
@@ -520,11 +528,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             return false;
           }
-          $active = intval($_data['active']);
-          $relay_all_recipients = intval($_data['relay_all_recipients']);
-          $relay_unknown_only = intval($_data['relay_unknown_only']);
-          $backupmx = intval($_data['backupmx']);
-          $gal = intval($_data['gal']);
+          $active = (isset($_data['active'])) ? intval($_data['active']) : $DOMAIN_DEFAULT_ATTRIBUTES['active'];
+          $relay_all_recipients = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_all_recipients'];
+          $relay_unknown_only = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_unknown_only'];
+          $backupmx = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : $DOMAIN_DEFAULT_ATTRIBUTES['backupmx'];
+          $gal = (isset($_data['gal'])) ? intval($_data['gal']) : $DOMAIN_DEFAULT_ATTRIBUTES['gal'];
           if ($relay_all_recipients == 1) {
             $backupmx = '1';
           }
@@ -625,9 +633,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             return false;
           }
-          if (!empty(intval($_data['rl_value']))) {
+          $_data['rl_value'] = (isset($_data['rl_value'])) ? intval($_data['rl_value']) : $DOMAIN_DEFAULT_ATTRIBUTES['rl_value'];
+          $_data['rl_frame'] = (isset($_data['rl_frame'])) ? $_data['rl_frame'] : $DOMAIN_DEFAULT_ATTRIBUTES['rl_frame'];
+          if (!empty($_data['rl_value']) && !empty($_data['rl_frame'])){
             ratelimit('edit', 'domain', array('rl_value' => $_data['rl_value'], 'rl_frame' => $_data['rl_frame'], 'object' => $domain));
           }
+          $_data['key_size'] = (isset($_data['key_size'])) ? intval($_data['key_size']) : $DOMAIN_DEFAULT_ATTRIBUTES['key_size'];
+          $_data['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : $DOMAIN_DEFAULT_ATTRIBUTES['dkim_selector'];
           if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) {
             if (!empty($redis->hGet('DKIM_SELECTORS', $domain))) {
               $_SESSION['return'][] = array(
@@ -1006,11 +1018,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             return false;
           }
+          if (empty($name)) {
+            $name = $local_part;
+          }
+          $template_attr = null;
+          if ($_data['template']){
+            $template_attr = mailbox('get', 'mailbox_templates', $_data['template'])['attributes'];
+          }
+          if (empty($template_attr)) {
+            $template_attr = mailbox('get', 'mailbox_templates')[0]['attributes'];
+          }
+          $MAILBOX_DEFAULT_ATTRIBUTES = array_merge($MAILBOX_DEFAULT_ATTRIBUTES, $template_attr);
+
           $password     = $_data['password'];
           $password2    = $_data['password2'];
           $name         = ltrim(rtrim($_data['name'], '>'), '<');
-          $tags         = $_data['tags'];
-          $quota_m      = intval($_data['quota']);
+          $tags         = (isset($_data['tags'])) ? $_data['tags'] : $MAILBOX_DEFAULT_ATTRIBUTES['tags'];
+          $quota_m      = (isset($_data['quota'])) ? intval($_data['quota']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['quota']) / 1024 ** 2;
           if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
@@ -1019,9 +1043,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             return false;
           }
-          if (empty($name)) {
-            $name = $local_part;
-          }
+
           if (isset($_data['protocol_access'])) {
             $_data['protocol_access'] = (array)$_data['protocol_access'];
             $_data['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
@@ -1029,7 +1051,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
             $_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
           }
-          $active = intval($_data['active']);
+          $active = (isset($_data['active'])) ? intval($_data['active']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['active']);
           $force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
           $tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
           $tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
@@ -1227,12 +1249,29 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             $_data['quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
             $_data['quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
             $_data['app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
+          } else {
+            $_data['spam_alias'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_alias']);
+            $_data['tls_policy'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_tls_policy']);
+            $_data['spam_score'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_score']);
+            $_data['spam_policy'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_policy']);
+            $_data['delimiter_action'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_delimiter_action']);
+            $_data['syncjobs'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_syncjobs']);
+            $_data['eas_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_eas_reset']);
+            $_data['sogo_profile_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_sogo_profile_reset']);
+            $_data['pushover'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pushover']);
+            $_data['quarantine'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine']);
+            $_data['quarantine_attachments'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_attachments']);
+            $_data['quarantine_notification'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_notification']);
+            $_data['quarantine_category'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_category']);
+            $_data['app_passwds'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_app_passwds']);     
+          }
 
+          try {
             $stmt = $pdo->prepare("INSERT INTO `user_acl` 
               (`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`,
-               `pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`) 
+                `pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`) 
               VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset,
-               :pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds) ");
+                :pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds) ");
             $stmt->execute(array(
               ':username' => $username,
               ':spam_alias' => $_data['spam_alias'],
@@ -1251,31 +1290,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               ':app_passwds' => $_data['app_passwds']
             ));
           }
-          else {
-            $stmt = $pdo->prepare("INSERT INTO `user_acl` 
-              (`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`,
-               `pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`) 
-              VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset,
-               :pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds) ");
-            $stmt->execute(array(
-              ':username' => $username,
-              ':spam_alias' => 0,
-              ':tls_policy' => 0,
-              ':spam_score' => 0,
-              ':spam_policy' => 0,
-              ':delimiter_action' => 0,
-              ':syncjobs' => 0,
-              ':eas_reset' => 0,
-              ':sogo_profile_reset' => 0,
-              ':pushover' => 0,
-              ':quarantine' => 0,
-              ':quarantine_attachments' => 0,
-              ':quarantine_notification' => 0,
-              ':quarantine_category' => 0,
-              ':app_passwds' => 0
-            ));
+          catch (PDOException $e) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => $e->getMessage()
+            );
+            return false;
           }
 
+          $_data['rl_frame'] = (isset($_data['rl_frame'])) ? $_data['rl_frame'] : $MAILBOX_DEFAULT_ATTRIBUTES['rl_frame'];
+          $_data['rl_value'] = (isset($_data['rl_value'])) ? $_data['rl_value'] : $MAILBOX_DEFAULT_ATTRIBUTES['rl_value'];
           if (isset($_data['rl_frame']) && isset($_data['rl_value'])){
             ratelimit('edit', 'mailbox', array(
               'object' => $username,
@@ -1524,17 +1549,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $attr["tls_enforce_out"]             = isset($_data['tls_enforce_out']) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
           if (isset($_data['protocol_access'])) {
             $_data['protocol_access'] = (array)$_data['protocol_access'];
-            $attr['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
-            $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
-            $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
-            $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
+            $attr['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
+            $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
+            $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
+            $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
           }   
           else {
             $attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
             $attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
             $attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
             $attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
-          }       
+          }
           if (isset($_data['acl'])) {
             $_data['acl'] = (array)$_data['acl'];
             $attr['acl_spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0;
@@ -3411,6 +3436,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $footers = array();
           $footers['html'] = isset($_data['html']) ? $_data['html'] : '';
           $footers['plain'] = isset($_data['plain']) ? $_data['plain'] : '';
+          $footers['skip_replies'] = isset($_data['skip_replies']) ? (int)$_data['skip_replies'] : 0;
           $footers['mbox_exclude'] = array();
           if (isset($_data["mbox_exclude"])){
             if (!is_array($_data["mbox_exclude"])) {
@@ -3460,12 +3486,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             try {
               $stmt = $pdo->prepare("DELETE FROM `domain_wide_footer` WHERE `domain`= :domain");
               $stmt->execute(array(':domain' => $domain));
-              $stmt = $pdo->prepare("INSERT INTO `domain_wide_footer` (`domain`, `html`, `plain`, `mbox_exclude`) VALUES (:domain, :html, :plain, :mbox_exclude)");
+              $stmt = $pdo->prepare("INSERT INTO `domain_wide_footer` (`domain`, `html`, `plain`, `mbox_exclude`, `skip_replies`) VALUES (:domain, :html, :plain, :mbox_exclude, :skip_replies)");
               $stmt->execute(array(
                 ':domain' => $domain,
                 ':html' => $footers['html'],
                 ':plain' => $footers['plain'],
                 ':mbox_exclude' => json_encode($footers['mbox_exclude']),
+                ':skip_replies' => $footers['skip_replies'],
               ));
             }
             catch (PDOException $e) {
@@ -4435,7 +4462,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $mailboxdata['active'] = $row['active'];
           $mailboxdata['active_int'] = $row['active'];
           $mailboxdata['domain'] = $row['domain'];
-          $mailboxdata['relayhost'] = $row['relayhost'];
           $mailboxdata['name'] = $row['name'];
           $mailboxdata['local_part'] = $row['local_part'];
           $mailboxdata['quota'] = $row['quota'];
@@ -4622,7 +4648,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           }
 
           try {
-            $stmt = $pdo->prepare("SELECT `html`, `plain`, `mbox_exclude` FROM `domain_wide_footer`
+            $stmt = $pdo->prepare("SELECT `html`, `plain`, `mbox_exclude`, `skip_replies` FROM `domain_wide_footer`
               WHERE `domain` = :domain");
             $stmt->execute(array(
               ':domain' => $domain

+ 2 - 1
data/web/inc/init_db.inc.php

@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
     global $pdo;
 
-    $db_version = "21112023_1644";
+    $db_version = "08012024_1442";
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -273,6 +273,7 @@ function init_db_schema() {
           "html" => "LONGTEXT",
           "plain" => "LONGTEXT",
           "mbox_exclude" => "JSON NOT NULL DEFAULT ('[]')",
+          "skip_replies" => "TINYINT(1) NOT NULL DEFAULT '0'"
         ),
         "keys" => array(
           "primary" => array(

+ 622 - 0
data/web/inc/lib/ssp.class.php

@@ -0,0 +1,622 @@
+<?php
+
+/*
+ * Helper functions for building a DataTables server-side processing SQL query
+ *
+ * The static functions in this class are just helper functions to help build
+ * the SQL used in the DataTables demo server-side processing scripts. These
+ * functions obviously do not represent all that can be done with server-side
+ * processing, they are intentionally simple to show how it works. More complex
+ * server-side processing operations will likely require a custom script.
+ *
+ * See https://datatables.net/usage/server-side for full details on the server-
+ * side processing requirements of DataTables.
+ *
+ * @license MIT - https://datatables.net/license_mit
+ */
+
+class SSP {
+	/**
+	 * Create the data output array for the DataTables rows
+	 *
+	 *  @param  array $columns Column information array
+	 *  @param  array $data    Data from the SQL get
+	 *  @return array          Formatted data in a row based format
+	 */
+	static function data_output ( $columns, $data )
+	{
+		$out = array();
+
+		for ( $i=0, $ien=count($data) ; $i<$ien ; $i++ ) {
+			$row = array();
+
+			for ( $j=0, $jen=count($columns) ; $j<$jen ; $j++ ) {
+				$column = $columns[$j];
+
+				// Is there a formatter?
+				if ( isset( $column['formatter'] ) ) {
+                    if(empty($column['db'])){
+                        $row[ $column['dt'] ] = $column['formatter']( $data[$i] );
+                    }
+                    else{
+                        $row[ $column['dt'] ] = $column['formatter']( $data[$i][ $column['db'] ], $data[$i] );
+                    }
+				}
+				else {
+                    if(!empty($column['db']) && (!isset($column['dummy']) || $column['dummy'] !== true)){
+                        $row[ $column['dt'] ] = $data[$i][ $columns[$j]['db'] ];
+                    }
+                    else{
+                        $row[ $column['dt'] ] = "";
+                    }
+				}
+			}
+
+			$out[] = $row;
+		}
+
+		return $out;
+	}
+
+
+	/**
+	 * Database connection
+	 *
+	 * Obtain an PHP PDO connection from a connection details array
+	 *
+	 *  @param  array $conn SQL connection details. The array should have
+	 *    the following properties
+	 *     * host - host name
+	 *     * db   - database name
+	 *     * user - user name
+	 *     * pass - user password
+	 *     * Optional: `'charset' => 'utf8'` - you might need this depending on your PHP / MySQL config
+	 *  @return resource PDO connection
+	 */
+	static function db ( $conn )
+	{
+		if ( is_array( $conn ) ) {
+			return self::sql_connect( $conn );
+		}
+
+		return $conn;
+	}
+
+
+	/**
+	 * Paging
+	 *
+	 * Construct the LIMIT clause for server-side processing SQL query
+	 *
+	 *  @param  array $request Data sent to server by DataTables
+	 *  @param  array $columns Column information array
+	 *  @return string SQL limit clause
+	 */
+	static function limit ( $request, $columns )
+	{
+		$limit = '';
+
+		if ( isset($request['start']) && $request['length'] != -1 ) {
+			$limit = "LIMIT ".intval($request['start']).", ".intval($request['length']);
+		}
+
+		return $limit;
+	}
+
+
+	/**
+	 * Ordering
+	 *
+	 * Construct the ORDER BY clause for server-side processing SQL query
+	 *
+	 *  @param  array $request Data sent to server by DataTables
+	 *  @param  array $columns Column information array
+	 *  @return string SQL order by clause
+	 */
+	static function order ( $tableAS, $request, $columns )
+	{
+    	$select = '';
+		$order = '';
+
+		if ( isset($request['order']) && count($request['order']) ) {
+    		$selects = [];
+			$orderBy = [];
+			$dtColumns = self::pluck( $columns, 'dt' );
+
+			for ( $i=0, $ien=count($request['order']) ; $i<$ien ; $i++ ) {
+				// Convert the column index into the column data property
+				$columnIdx = intval($request['order'][$i]['column']);
+				$requestColumn = $request['columns'][$columnIdx];
+
+				$columnIdx = array_search( $columnIdx, $dtColumns );
+				$column = $columns[ $columnIdx ];
+
+				if ( $requestColumn['orderable'] == 'true' ) {
+					$dir = $request['order'][$i]['dir'] === 'asc' ?
+						'ASC' :
+						'DESC';
+						
+                    if(isset($column['order_subquery'])) {
+        				$selects[] = '('.$column['order_subquery'].') AS `'.$column['db'].'_count`';
+        				$orderBy[] = '`'.$column['db'].'_count` '.$dir;
+    				} else {
+					    $orderBy[] = '`'.$tableAS.'`.`'.$column['db'].'` '.$dir;
+					}
+				}
+			}
+
+            if ( count( $selects ) ) {
+                $select = ', '.implode(', ', $selects);
+            }
+
+			if ( count( $orderBy ) ) {
+				$order = 'ORDER BY '.implode(', ', $orderBy);
+			}
+		}
+
+		return [$select, $order];
+	}
+
+
+	/**
+	 * Searching / Filtering
+	 *
+	 * Construct the WHERE clause for server-side processing SQL query.
+	 *
+	 * NOTE this does not match the built-in DataTables filtering which does it
+	 * word by word on any field. It's possible to do here performance on large
+	 * databases would be very poor
+	 *
+	 *  @param  array $request Data sent to server by DataTables
+	 *  @param  array $columns Column information array
+	 *  @param  array $bindings Array of values for PDO bindings, used in the
+	 *    sql_exec() function
+	 *  @return string SQL where clause
+	 */
+	static function filter ( $tablesAS, $request, $columns, &$bindings )
+	{
+		$globalSearch = array();
+		$columnSearch = array();
+		$joins = array();
+		$dtColumns = self::pluck( $columns, 'dt' );
+
+		if ( isset($request['search']) && $request['search']['value'] != '' ) {
+			$str = $request['search']['value'];
+
+			for ( $i=0, $ien=count($request['columns']) ; $i<$ien ; $i++ ) {
+				$requestColumn = $request['columns'][$i];
+				$columnIdx = array_search( $i, $dtColumns );
+				$column = $columns[ $columnIdx ];
+
+				if ( $requestColumn['searchable'] == 'true' ) {
+					if(!empty($column['db'])){
+    					$binding = self::bind( $bindings, '%'.$str.'%', PDO::PARAM_STR );
+    					
+    					if(isset($column['search']['join'])) {
+            				$joins[] = $column['search']['join'];
+            				$globalSearch[] = $column['search']['where_column'].' LIKE '.$binding;
+        				} else {
+						    $globalSearch[] = "`".$tablesAS."`.`".$column['db']."` LIKE ".$binding;
+						}
+					}
+				}
+			}
+		}
+
+		// Individual column filtering
+		if ( isset( $request['columns'] ) ) {
+			for ( $i=0, $ien=count($request['columns']) ; $i<$ien ; $i++ ) {
+				$requestColumn = $request['columns'][$i];
+				$columnIdx = array_search( $requestColumn['data'], $dtColumns );
+				$column = $columns[ $columnIdx ];
+
+				$str = $requestColumn['search']['value'];
+
+				if ( $requestColumn['searchable'] == 'true' &&
+				 $str != '' ) {
+					if(!empty($column['db'])){
+						$binding = self::bind( $bindings, '%'.$str.'%', PDO::PARAM_STR );
+						$columnSearch[] = "`".$tablesAS."`.`".$column['db']."` LIKE ".$binding;
+					}
+				}
+			}
+		}
+
+		// Combine the filters into a single string
+		$where = '';
+
+		if ( count( $globalSearch ) ) {
+			$where = '('.implode(' OR ', $globalSearch).')';
+		}
+
+		if ( count( $columnSearch ) ) {
+			$where = $where === '' ?
+				implode(' AND ', $columnSearch) :
+				$where .' AND '. implode(' AND ', $columnSearch);
+		}
+		
+		$join = '';
+		if( count($joins) ) {
+    		$join = implode(' ', $joins);
+		}
+
+		if ( $where !== '' ) {
+			$where = 'WHERE '.$where;
+		}
+
+		return [$join, $where];
+	}
+
+
+	/**
+	 * Perform the SQL queries needed for an server-side processing requested,
+	 * utilising the helper functions of this class, limit(), order() and
+	 * filter() among others. The returned array is ready to be encoded as JSON
+	 * in response to an SSP request, or can be modified if needed before
+	 * sending back to the client.
+	 *
+	 *  @param  array $request Data sent to server by DataTables
+	 *  @param  array|PDO $conn PDO connection resource or connection parameters array
+	 *  @param  string $table SQL table to query
+	 *  @param  string $primaryKey Primary key of the table
+	 *  @param  array $columns Column information array
+	 *  @return array          Server-side processing response array
+	 */
+	static function simple ( $request, $conn, $table, $primaryKey, $columns )
+	{
+		$bindings = array();
+		$db = self::db( $conn );
+
+		// Allow for a JSON string to be passed in
+		if (isset($request['json'])) {
+			$request = json_decode($request['json'], true);
+		}
+
+    // table AS
+    $tablesAS = null;
+    if(is_array($table)) {
+      $tablesAS = $table[1];
+      $table = $table[0];
+    }
+
+		// Build the SQL query string from the request
+		list($select, $order) = self::order( $tablesAS, $request, $columns );
+		$limit = self::limit( $request, $columns );
+		list($join, $where) = self::filter( $tablesAS, $request, $columns, $bindings );
+
+		// Main query to actually get the data
+		$data = self::sql_exec( $db, $bindings,
+			"SELECT `$tablesAS`.`".implode("`, `$tablesAS`.`", self::pluck($columns, 'db'))."`
+			 $select
+			 FROM `$table` AS `$tablesAS`
+			 $join
+			 $where
+			 GROUP BY `{$tablesAS}`.`{$primaryKey}`
+			 $order
+			 $limit"
+		);
+
+		// Data set length after filtering
+		$resFilterLength = self::sql_exec( $db, $bindings,
+			"SELECT COUNT(DISTINCT `{$tablesAS}`.`{$primaryKey}`)
+			 FROM   `$table` AS `$tablesAS`
+			 $join
+			 $where"
+		);
+		$recordsFiltered = $resFilterLength[0][0];
+
+		// Total data set length
+		$resTotalLength = self::sql_exec( $db,
+			"SELECT COUNT(`{$tablesAS}`.`{$primaryKey}`)
+			 FROM   `$table` AS `$tablesAS`"
+		);
+		$recordsTotal = $resTotalLength[0][0];
+
+		/*
+		 * Output
+		 */
+		return array(
+			"draw"            => isset ( $request['draw'] ) ?
+				intval( $request['draw'] ) :
+				0,
+			"recordsTotal"    => intval( $recordsTotal ),
+			"recordsFiltered" => intval( $recordsFiltered ),
+			"data"            => self::data_output( $columns, $data )
+		);
+	}
+
+
+	/**
+	 * The difference between this method and the `simple` one, is that you can
+	 * apply additional `where` conditions to the SQL queries. These can be in
+	 * one of two forms:
+	 *
+	 * * 'Result condition' - This is applied to the result set, but not the
+	 *   overall paging information query - i.e. it will not effect the number
+	 *   of records that a user sees they can have access to. This should be
+	 *   used when you want apply a filtering condition that the user has sent.
+	 * * 'All condition' - This is applied to all queries that are made and
+	 *   reduces the number of records that the user can access. This should be
+	 *   used in conditions where you don't want the user to ever have access to
+	 *   particular records (for example, restricting by a login id).
+	 *
+	 * In both cases the extra condition can be added as a simple string, or if
+	 * you are using external values, as an assoc. array with `condition` and
+	 * `bindings` parameters. The `condition` is a string with the SQL WHERE
+	 * condition and `bindings` is an assoc. array of the binding names and
+	 * values.
+	 *
+	 *  @param  array $request Data sent to server by DataTables
+	 *  @param  array|PDO $conn PDO connection resource or connection parameters array
+	 *  @param  string|array $table SQL table to query, if array second key is AS
+	 *  @param  string $primaryKey Primary key of the table
+	 *  @param  array $columns Column information array
+   *  @param  string $join JOIN sql string
+	 *  @param  string|array $whereResult WHERE condition to apply to the result set
+	 *  @return array          Server-side processing response array
+	 */
+	static function complex (
+		$request,
+		$conn,
+		$table,
+		$primaryKey,
+		$columns,
+    $join=null,
+		$whereResult=null
+	) {
+		$bindings = array();
+		$db = self::db( $conn );
+
+    // table AS
+    $tablesAS = null;
+    if(is_array($table)) {
+      $tablesAS = $table[1];
+      $table = $table[0];
+    }
+
+		// Build the SQL query string from the request
+		list($select, $order) = self::order( $tablesAS, $request, $columns );
+		$limit = self::limit( $request, $columns );
+		list($join_filter, $where) = self::filter( $tablesAS, $request, $columns, $bindings );
+
+		// whereResult can be a simple string, or an assoc. array with a
+		// condition and bindings
+		if ( $whereResult ) {
+			$str = $whereResult;
+
+			if ( is_array($whereResult) ) {
+				$str = $whereResult['condition'];
+
+				if ( isset($whereResult['bindings']) ) {
+					self::add_bindings($bindings, $whereResult);
+				}
+			}
+
+			$where = $where ?
+				$where .' AND '.$str :
+				'WHERE '.$str;
+		}
+
+		// Main query to actually get the data
+		$data = self::sql_exec( $db, $bindings,
+			"SELECT  `$tablesAS`.`".implode("`, `$tablesAS`.`", self::pluck($columns, 'db'))."`
+			 $select
+			 FROM `$table` AS `$tablesAS`
+			 $join
+			 $join_filter
+			 $where
+			 GROUP BY `{$tablesAS}`.`{$primaryKey}`
+			 $order
+			 $limit"
+		);
+
+		// Data set length after filtering
+		$resFilterLength = self::sql_exec( $db, $bindings,
+			"SELECT COUNT(DISTINCT `{$tablesAS}`.`{$primaryKey}`)
+			 FROM   `$table` AS `$tablesAS`
+			 $join
+			 $join_filter
+			 $where"
+		);
+		$recordsFiltered = (isset($resFilterLength[0])) ? $resFilterLength[0][0] : 0;
+
+		// Total data set length
+		$resTotalLength = self::sql_exec( $db, $bindings,
+			"SELECT COUNT(`{$tablesAS}`.`{$primaryKey}`)
+			 FROM   `$table` AS `$tablesAS`
+			 $join
+			 $join_filter
+			 $where"
+		);
+		$recordsTotal = (isset($resTotalLength[0])) ? $resTotalLength[0][0] : 0;
+
+		/*
+		 * Output
+		 */
+		return array(
+			"draw"            => isset ( $request['draw'] ) ?
+				intval( $request['draw'] ) :
+				0,
+			"recordsTotal"    => intval( $recordsTotal ),
+			"recordsFiltered" => intval( $recordsFiltered ),
+			"data"            => self::data_output( $columns, $data )
+		);
+	}
+
+
+	/**
+	 * Connect to the database
+	 *
+	 * @param  array $sql_details SQL server connection details array, with the
+	 *   properties:
+	 *     * host - host name
+	 *     * db   - database name
+	 *     * user - user name
+	 *     * pass - user password
+	 * @return resource Database connection handle
+	 */
+	static function sql_connect ( $sql_details )
+	{
+		try {
+			$db = @new PDO(
+				"mysql:host={$sql_details['host']};dbname={$sql_details['db']}",
+				$sql_details['user'],
+				$sql_details['pass'],
+				array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION )
+			);
+		}
+		catch (PDOException $e) {
+			self::fatal(
+				"An error occurred while connecting to the database. ".
+				"The error reported by the server was: ".$e->getMessage()
+			);
+		}
+
+		return $db;
+	}
+
+
+	/**
+	 * Execute an SQL query on the database
+	 *
+	 * @param  resource $db  Database handler
+	 * @param  array    $bindings Array of PDO binding values from bind() to be
+	 *   used for safely escaping strings. Note that this can be given as the
+	 *   SQL query string if no bindings are required.
+	 * @param  string   $sql SQL query to execute.
+	 * @return array         Result from the query (all rows)
+	 */
+	static function sql_exec ( $db, $bindings, $sql=null )
+	{
+		// Argument shifting
+		if ( $sql === null ) {
+			$sql = $bindings;
+		}
+
+		$stmt = $db->prepare( $sql );
+
+		// Bind parameters
+		if ( is_array( $bindings ) ) {
+			for ( $i=0, $ien=count($bindings) ; $i<$ien ; $i++ ) {
+				$binding = $bindings[$i];
+				$stmt->bindValue( $binding['key'], $binding['val'], $binding['type'] );
+			}
+		}
+
+		// Execute
+		try {
+			$stmt->execute();
+		}
+		catch (PDOException $e) {
+			self::fatal( "An SQL error occurred: ".$e->getMessage() );
+		}
+
+		// Return all
+		return $stmt->fetchAll( PDO::FETCH_BOTH );
+	}
+
+
+	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+	 * Internal methods
+	 */
+
+	/**
+	 * Throw a fatal error.
+	 *
+	 * This writes out an error message in a JSON string which DataTables will
+	 * see and show to the user in the browser.
+	 *
+	 * @param  string $msg Message to send to the client
+	 */
+	static function fatal ( $msg )
+	{
+		echo json_encode( array(
+			"error" => $msg
+		) );
+
+		exit(0);
+	}
+
+	/**
+	 * Create a PDO binding key which can be used for escaping variables safely
+	 * when executing a query with sql_exec()
+	 *
+	 * @param  array &$a    Array of bindings
+	 * @param  *      $val  Value to bind
+	 * @param  int    $type PDO field type
+	 * @return string       Bound key to be used in the SQL where this parameter
+	 *   would be used.
+	 */
+	static function bind ( &$a, $val, $type )
+	{
+		$key = ':binding_'.count( $a );
+
+		$a[] = array(
+			'key' => $key,
+			'val' => $val,
+			'type' => $type
+		);
+
+		return $key;
+	}
+
+	static function add_bindings(&$bindings, $vals)
+	{
+		foreach($vals['bindings'] as $key => $value) {
+			$bindings[] = array(
+				'key' => $key,
+				'val' => $value,
+				'type' => PDO::PARAM_STR
+			);
+		}
+	}
+
+
+	/**
+	 * Pull a particular property from each assoc. array in a numeric array,
+	 * returning and array of the property values from each item.
+	 *
+	 *  @param  array  $a    Array to get data from
+	 *  @param  string $prop Property to read
+	 *  @return array        Array of property values
+	 */
+	static function pluck ( $a, $prop )
+	{
+		$out = array();
+
+		for ( $i=0, $len=count($a) ; $i<$len ; $i++ ) {
+ 			if ( empty($a[$i][$prop]) && $a[$i][$prop] !== 0 ) {
+				continue;
+			}
+			if ( $prop == 'db' && isset($a[$i]['dummy']) && $a[$i]['dummy'] === true ) {
+    			continue;
+			}
+
+			//removing the $out array index confuses the filter method in doing proper binding,
+			//adding it ensures that the array data are mapped correctly
+			$out[$i] = $a[$i][$prop];
+		}
+
+		return $out;
+	}
+
+
+	/**
+	 * Return a string from an array or a string
+	 *
+	 * @param  array|string $a Array to join
+	 * @param  string $join Glue for the concatenation
+	 * @return string Joined string
+	 */
+	static function _flatten ( $a, $join = ' AND ' )
+	{
+		if ( ! $a ) {
+			return '';
+		}
+		else if ( $a && is_array($a) ) {
+			return implode( $join, $a );
+		}
+		return $a;
+	}
+}
+

+ 9 - 0
data/web/inc/vars.inc.php

@@ -126,6 +126,15 @@ $MAILCOW_APPS = array(
   )
 );
 
+// Logo max file size in bytes
+$LOGO_LIMITS['max_size'] = 15 * 1024 * 1024; // 15MB
+
+// Logo max width in pixels
+$LOGO_LIMITS['max_width'] = 1920;
+
+// Logo max height in pixels
+$LOGO_LIMITS['max_height'] = 1920;
+
 // Rows until pagination begins
 $PAGINATION_SIZE = 25;
 

+ 42 - 28
data/web/js/site/mailbox.js

@@ -435,7 +435,7 @@ jQuery(function($){
     var table = $('#domain_table').DataTable({
       responsive: true,
       processing: true,
-      serverSide: false,
+      serverSide: true,
       stateSave: true,
       pageLength: pagination_size,
       dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
@@ -447,9 +447,9 @@ jQuery(function($){
       },
       ajax: {
         type: "GET",
-        url: "/api/v1/get/domain/all",
+        url: "/api/v1/get/domain/datatables",
         dataSrc: function(json){
-          $.each(json, function(i, item) {
+          $.each(json.data, function(i, item) {
             item.domain_name = escapeHtml(item.domain_name);
 
             item.aliases = item.aliases_in_domain + " / " + item.max_num_aliases_for_domain;
@@ -498,7 +498,7 @@ jQuery(function($){
             }
           });
 
-          return json;
+          return json.data;
         }
       },
       columns: [
@@ -528,17 +528,20 @@ jQuery(function($){
         {
           title: lang.aliases,
           data: 'aliases',
+          searchable: false,
           defaultContent: ''
         },
         {
           title: lang.mailboxes,
           data: 'mailboxes',
+          searchable: false,
           responsivePriority: 4,
           defaultContent: ''
         },
         {
           title: lang.domain_quota,
           data: 'quota',
+          searchable: false,
           defaultContent: '',
           render: function (data, type) {
             data = data.split("/");
@@ -548,6 +551,7 @@ jQuery(function($){
         {
           title: lang.stats,
           data: 'stats',
+          searchable: false,
           defaultContent: '',
           render: function (data, type) {
             data = data.split("/");
@@ -557,53 +561,67 @@ jQuery(function($){
         {
           title: lang.mailbox_defquota,
           data: 'def_quota_for_mbox',
+          searchable: false,
           defaultContent: ''
         },
         {
           title: lang.mailbox_quota,
           data: 'max_quota_for_mbox',
+          searchable: false,
           defaultContent: ''
         },
         {
           title: 'RL',
           data: 'rl',
+          searchable: false,
+          orderable: false,
           defaultContent: ''
         },
         {
           title: lang.backup_mx,
           data: 'backupmx',
+          searchable: false,
           defaultContent: '',
-          redner: function (data, type){
-            return 1==value ? '<i class="bi bi-check-lg"></i>' : 0==value && '<i class="bi bi-x-lg"></i>';
+          render: function (data, type){
+            return 1==data ? '<i class="bi bi-check-lg"></i>' : 0==data && '<i class="bi bi-x-lg"></i>';
           }
         },
         {
           title: lang.domain_admins,
           data: 'domain_admins',
+          searchable: false,
+          orderable: false,
           defaultContent: '',
           className: 'none'
         },
         {
           title: lang.created_on,
           data: 'created',
+          searchable: false,
+          orderable: false,
           defaultContent: '',
           className: 'none'
         },
         {
           title: lang.last_modified,
           data: 'modified',
+          searchable: false,
+          orderable: false,
           defaultContent: '',
           className: 'none'
         },
         {
           title: 'Tags',
           data: 'tags',
+          searchable: true,
+          orderable: false,
           defaultContent: '',
           className: 'none'
         },
         {
           title: lang.active,
           data: 'active',
+          searchable: false,
           defaultContent: '',
           responsivePriority: 6,
           render: function (data, type) {
@@ -613,6 +631,8 @@ jQuery(function($){
         {
           title: lang.action,
           data: 'action',
+          searchable: false,
+          orderable: false,
           className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
           responsivePriority: 5,
           defaultContent: ''
@@ -844,7 +864,7 @@ jQuery(function($){
     var table = $('#mailbox_table').DataTable({
       responsive: true,
       processing: true,
-      serverSide: false,
+      serverSide: true,
       stateSave: true,
       pageLength: pagination_size,
       dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
@@ -853,13 +873,12 @@ jQuery(function($){
       language: lang_datatables,
       initComplete: function(settings, json){
         hideTableExpandCollapseBtn('#tab-mailboxes', '#mailbox_table');
-        filterByDomain(json, 8, table);
       },
       ajax: {
         type: "GET",
-        url: "/api/v1/get/mailbox/reduced",
+        url: "/api/v1/get/mailbox/datatables",
         dataSrc: function(json){
-          $.each(json, function (i, item) {
+          $.each(json.data, function (i, item) {
             item.quota = {
               sortBy: item.quota_used,
               value: item.quota
@@ -945,7 +964,7 @@ jQuery(function($){
             }
           });
 
-          return json;
+          return json.data;
         }
       },
       columns: [
@@ -975,13 +994,14 @@ jQuery(function($){
         {
           title: lang.domain_quota,
           data: 'quota.value',
+          searchable: false,
           responsivePriority: 8,
-          defaultContent: '',
-          orderData: 23
+          defaultContent: ''
         },
         {
           title: lang.last_mail_login,
           data: 'last_mail_login',
+          searchable: false,
           defaultContent: '',
           responsivePriority: 7,
           render: function (data, type) {
@@ -994,15 +1014,16 @@ jQuery(function($){
         {
           title: lang.last_pw_change,
           data: 'last_pw_change',
+          searchable: false,
           defaultContent: ''
         },
         {
           title: lang.in_use,
           data: 'in_use.value',
+          searchable: false,
           defaultContent: '',
           responsivePriority: 9,
-          className: 'dt-data-w100',
-          orderData: 24
+          className: 'dt-data-w100'
         },
         {
           title: lang.fname,
@@ -1067,6 +1088,7 @@ jQuery(function($){
         {
           title: lang.msg_num,
           data: 'messages',
+          searchable: false,
           defaultContent: '',
           responsivePriority: 5
         },
@@ -1085,12 +1107,14 @@ jQuery(function($){
         {
           title: 'Tags',
           data: 'tags',
+          searchable: true,
           defaultContent: '',
           className: 'none'
         },
         {
           title: lang.active,
           data: 'active',
+          searchable: false,
           defaultContent: '',
           responsivePriority: 4,
           render: function (data, type) {
@@ -1100,22 +1124,12 @@ jQuery(function($){
         {
           title: lang.action,
           data: 'action',
+          searchable: false,
+          orderable: false,
           className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
           responsivePriority: 6,
           defaultContent: ''
-        },
-        {
-          title: "",
-          data: 'quota.sortBy',
-          defaultContent: '',
-          className: "d-none"
-        },
-        {
-          title: "",
-          data: 'in_use.sortBy',
-          defaultContent: '',
-          className: "d-none"
-        },
+        }
       ]
     });
 

+ 95 - 15
data/web/json_api.php

@@ -15,7 +15,7 @@ function api_log($_data) {
       continue;
     }
 
-    $value = json_decode($value, true);     
+    $value = json_decode($value, true);
     if ($value) {
       if (is_array($value)) unset($value["csrf_token"]);
       foreach ($value as $key => &$val) {
@@ -23,7 +23,7 @@ function api_log($_data) {
           $val = '*';
         }
       }
-      $value = json_encode($value);  
+      $value = json_encode($value);
     }
     $data_var[] = $data . "='" . $value . "'";
   }
@@ -44,7 +44,7 @@ function api_log($_data) {
       'msg' => 'Redis: '.$e
     );
     return false;
-  }     
+  }
 }
 
 if (isset($_GET['query'])) {
@@ -178,12 +178,12 @@ if (isset($_GET['query'])) {
               // parse post data
               $post = trim(file_get_contents('php://input'));
               if ($post) $post = json_decode($post);
-              
+
               // process registration data from authenticator
               try {
                 // decode base64 strings
                 $clientDataJSON = base64_decode($post->clientDataJSON);
-                $attestationObject = base64_decode($post->attestationObject);   
+                $attestationObject = base64_decode($post->attestationObject);
 
                 // processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true)
                 $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $_SESSION['challenge'], false, true);
@@ -250,7 +250,7 @@ if (isset($_GET['query'])) {
             default:
               process_add_return(mailbox('add', 'domain', $attr));
             break;
-          }  
+          }
         break;
         case "resource":
           process_add_return(mailbox('add', 'resource', $attr));
@@ -470,7 +470,7 @@ if (isset($_GET['query'])) {
               //        false, if only internal is allowed
               //        null, if internal and cross-platform is allowed
               $createArgs = $WebAuthn->getCreateArgs($_SESSION["mailcow_cc_username"], $_SESSION["mailcow_cc_username"], $_SESSION["mailcow_cc_username"], 30, false, $GLOBALS['WEBAUTHN_UV_FLAG_REGISTER'], null, $excludeCredentialIds);
-              
+
               print(json_encode($createArgs));
               $_SESSION['challenge'] = $WebAuthn->getChallenge();
               return;
@@ -533,9 +533,50 @@ if (isset($_GET['query'])) {
 
           case "domain":
             switch ($object) {
+              case "datatables":
+                $table = ['domain', 'd'];
+                $primaryKey = 'domain';
+                $columns = [
+                  ['db' => 'domain', 'dt' => 2],
+                  ['db' => 'aliases', 'dt' => 3, 'order_subquery' => "SELECT COUNT(*) FROM `alias` WHERE (`domain`= `d`.`domain` OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = `d`.`domain`)) AND `address` NOT IN (SELECT `username` FROM `mailbox`)"],
+                  ['db' => 'mailboxes', 'dt' => 4, 'order_subquery' => "SELECT COUNT(*) FROM `mailbox` WHERE `mailbox`.`domain` = `d`.`domain` AND (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)"],
+                  ['db' => 'quota', 'dt' => 5, 'order_subquery' => "SELECT COALESCE(SUM(`mailbox`.`quota`), 0) FROM `mailbox` WHERE `mailbox`.`domain` = `d`.`domain` AND (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)"],
+                  ['db' => 'stats', 'dt' => 6, 'dummy' => true, 'order_subquery' => "SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` IN (SELECT `username` FROM `mailbox` WHERE `domain` = `d`.`domain`)"],
+                  ['db' => 'defquota', 'dt' => 7],
+                  ['db' => 'maxquota', 'dt' => 8],
+                  ['db' => 'backupmx', 'dt' => 10],
+                  ['db' => 'tags', 'dt' => 14, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_domain` AS `td` ON `td`.`domain` = `d`.`domain`', 'where_column' => '`td`.`tag_name`']],
+                  ['db' => 'active', 'dt' => 15],
+                ];
+
+                require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php';
+                global $pdo;
+                if($_SESSION['mailcow_cc_role'] === 'admin') {
+                  $data = SSP::simple($_GET, $pdo, $table, $primaryKey, $columns);
+                } elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
+                  $data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns,
+                    'INNER JOIN domain_admins as da ON da.domain = d.domain',
+                    [
+                      'condition' => 'da.active = 1 and da.username = :username',
+                      'bindings' => ['username' => $_SESSION['mailcow_cc_username']]
+                    ]);
+                }
+
+                if (!empty($data['data'])) {
+                  $domainsData = [];
+                  foreach ($data['data'] as $domain) {
+                    if ($details = mailbox('get', 'domain_details', $domain[2])) {
+                      $domainsData[] = $details;
+                    }
+                  }
+                  $data['data'] = $domainsData;
+                }
+
+                process_get_return($data);
+              break;
               case "all":
                 $tags = null;
-                if (isset($_GET['tags']) && $_GET['tags'] != '') 
+                if (isset($_GET['tags']) && $_GET['tags'] != '')
                   $tags = explode(',', $_GET['tags']);
 
                 $domains = mailbox('get', 'domains', null, $tags);
@@ -1021,10 +1062,49 @@ if (isset($_GET['query'])) {
           break;
           case "mailbox":
             switch ($object) {
+              case "datatables":
+                $table = ['mailbox', 'm'];
+                $primaryKey = 'username';
+                $columns = [
+                  ['db' => 'username', 'dt' => 2],
+                  ['db' => 'quota', 'dt' => 3],
+                  ['db' => 'last_mail_login', 'dt' => 4, 'dummy' => true, 'order_subquery' => "SELECT MAX(`datetime`) FROM `sasl_log` WHERE `service` != 'SSO' AND `username` = `m`.`username`"],
+                  ['db' => 'last_pw_change', 'dt' => 5, 'dummy' => true, 'order_subquery' => "JSON_EXTRACT(attributes, '$.passwd_update')"],
+                  ['db' => 'in_use', 'dt' => 6, 'dummy' => true, 'order_subquery' => "(SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`) / `m`.`quota`"],
+                  ['db' => 'messages', 'dt' => 17, 'dummy' => true, 'order_subquery' => "SELECT SUM(messages) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`"],
+                  ['db' => 'tags', 'dt' => 20, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_mailbox` AS `tm` ON `tm`.`username` = `m`.`username`', 'where_column' => '`tm`.`tag_name`']],
+                  ['db' => 'active', 'dt' => 21]
+                ];
+
+                require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php';
+                global $pdo;
+                if($_SESSION['mailcow_cc_role'] === 'admin') {
+                  $data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns, null, "(`m`.`kind` = '' OR `m`.`kind` = NULL)");
+                } elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
+                  $data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns,
+                    'INNER JOIN domain_admins as da ON da.domain = m.domain',
+                    [
+                      'condition' => "(`m`.`kind` = '' OR `m`.`kind` = NULL) AND `da`.`active` = 1 AND `da`.`username` = :username",
+                      'bindings' => ['username' => $_SESSION['mailcow_cc_username']]
+                    ]);
+                }
+
+                if (!empty($data['data'])) {
+                  $mailboxData = [];
+                  foreach ($data['data'] as $mailbox) {
+                    if ($details = mailbox('get', 'mailbox_details', $mailbox[2])) {
+                      $mailboxData[] = $details;
+                    }
+                  }
+                  $data['data'] = $mailboxData;
+                }
+
+                process_get_return($data);
+              break;
               case "all":
               case "reduced":
                 $tags = null;
-                if (isset($_GET['tags']) && $_GET['tags'] != '') 
+                if (isset($_GET['tags']) && $_GET['tags'] != '')
                   $tags = explode(',', $_GET['tags']);
 
                 if (empty($extra)) $domains = mailbox('get', 'domains');
@@ -1058,7 +1138,7 @@ if (isset($_GET['query'])) {
               break;
               default:
                 $tags = null;
-                if (isset($_GET['tags']) && $_GET['tags'] != '') 
+                if (isset($_GET['tags']) && $_GET['tags'] != '')
                   $tags = explode(',', $_GET['tags']);
 
                 if ($tags === null) {
@@ -1068,7 +1148,7 @@ if (isset($_GET['query'])) {
                   $mailboxes = mailbox('get', 'mailboxes', $object, $tags);
                   if (is_array($mailboxes)) {
                     foreach ($mailboxes as $mailbox) {
-                      if ($details = mailbox('get', 'mailbox_details', $mailbox)) 
+                      if ($details = mailbox('get', 'mailbox_details', $mailbox))
                         $data[] = $details;
                     }
                   }
@@ -1571,15 +1651,15 @@ if (isset($_GET['query'])) {
                     'solr_size' => $solr_size,
                     'solr_documents' => $solr_documents
                   ));
-                break;  
+                break;
                 case "host":
                   if (!$extra){
                     $stats = docker("host_stats");
                     echo json_encode($stats);
-                  } 
+                  }
                   else if ($extra == "ip") {
                     // get public ips
-                    
+
                     $curl = curl_init();
                     curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
                     curl_setopt($curl, CURLOPT_POST, 0);
@@ -2003,7 +2083,7 @@ if (isset($_GET['query'])) {
       exit();
   }
 }
-if ($_SESSION['mailcow_cc_api'] === true) {
+if (array_key_exists('mailcow_cc_api', $_SESSION) && $_SESSION['mailcow_cc_api'] === true) {
   if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) {
     unset($_SESSION['return']);
   }

+ 17 - 9
data/web/lang/lang.de-de.json

@@ -394,7 +394,9 @@
         "goto_invalid": "Ziel-Adresse %s ist ungültig",
         "ham_learn_error": "Ham Lernfehler: %s",
         "imagick_exception": "Fataler Bildverarbeitungsfehler",
+        "img_dimensions_exceeded": "Grafik überschreitet die maximale Bildgröße",
         "img_invalid": "Grafik konnte nicht validiert werden",
+        "img_size_exceeded": "Grafik überschreitet die maximale Dateigröße",
         "img_tmp_missing": "Grafik konnte nicht validiert werden: Erstellung temporärer Datei fehlgeschlagen.",
         "invalid_bcc_map_type": "Ungültiger BCC-Map-Typ",
         "invalid_destination": "Ziel-Format \"%s\" ist ungültig",
@@ -588,10 +590,19 @@
         "disable_login": "Login verbieten (Mails werden weiterhin angenommen)",
         "domain": "Domain bearbeiten",
         "domain_admin": "Domain-Administrator bearbeiten",
-        "domain_footer": "Domain wide footer",
-        "domain_footer_html": "HTML footer",
-        "domain_footer_info": "Domain wide footer werden allen ausgehenden E-Mails hinzugefügt, die einer Adresse innerhalb dieser Domain gehört.<br>Die folgenden Variablen können für den Footer benutzt werden:",
-        "domain_footer_plain": "PLAIN footer",
+        "domain_footer": "Domänenweite Fußzeile",
+        "domain_footer_html": "Fußzeile im HTML Format",
+        "domain_footer_info": "Domänenweite Footer (Domain wide footer) werden allen ausgehenden E-Mails hinzugefügt, die einer Adresse innerhalb dieser Domain gehört.<br>Die folgenden Variablen können für die Fußzeile benutzt werden:",
+        "domain_footer_info_vars": {
+            "auth_user": "{= auth_user =}   - Angemeldeter Benutzername vom MTA",
+            "from_user": "{= from_user =}   - Absender Teil der E-Mail z.B. für \"moo@mailcow.tld\" wird \"moo\" zurückgeben.",
+            "from_name": "{= from_name =}   - Namen des Absenders z.B. für \"Mailcow &lt;moo@mailcow.tld&gt;\", wird \"Mailcow\" zurückgegeben.",
+            "from_addr": "{= from_addr =}   - Adresse des Absenders.",
+            "from_domain": "{= from_domain =} - Domain des Absenders",
+            "custom": "{= foo =}         - Wenn die Mailbox das benutzerdefinierte Attribut \"foo\" mit dem Wert \"bar\" hat, wird \"bar\" zurückgegeben."
+        },
+        "domain_footer_plain": "Fußzeile im PLAIN Format",
+        "domain_footer_skip_replies": "Ignoriere Footer bei Antwort E-Mails",
         "domain_quota": "Domain Speicherplatz gesamt (MiB)",
         "domains": "Domains",
         "dont_check_sender_acl": "Absender für Domain %s u. Alias-Domain nicht prüfen",
@@ -680,11 +691,7 @@
         "unchanged_if_empty": "Unverändert, wenn leer",
         "username": "Benutzername",
         "validate_save": "Validieren und speichern",
-        "pushover_sound": "Ton",
-        "domain_footer_info_vars": {
-            "auth_user": "{= auth_user =}   - Angemeldeter Benutzername vom MTA",
-            "from_user": "{= from_user =}   - Von Teil des Benutzers z.B. \"moo@mailcow.tld\" wird \"moo\" zurückgeben."
-        }
+        "pushover_sound": "Ton"
     },
     "fido2": {
         "confirm": "Bestätigen",
@@ -1088,6 +1095,7 @@
         "verified_yotp_login": "Yubico-OTP-Anmeldung verifiziert"
     },
     "tfa": {
+        "authenticators": "Authentikatoren",
         "api_register": "%s verwendet die Yubico-Cloud-API. Ein API-Key für den Yubico-Stick kann <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">hier</a> bezogen werden.",
         "confirm": "Bestätigen",
         "confirm_totp_token": "Bitte bestätigen Sie die Änderung durch Eingabe eines generierten Tokens",

+ 4 - 0
data/web/lang/lang.en-gb.json

@@ -394,7 +394,9 @@
         "goto_invalid": "Goto address %s is invalid",
         "ham_learn_error": "Ham learn error: %s",
         "imagick_exception": "Error: Imagick exception while reading image",
+        "img_dimensions_exceeded": "Image exceeds the maximum image size",
         "img_invalid": "Cannot validate image file",
+        "img_size_exceeded": "Image exceeds the maximum file size",
         "img_tmp_missing": "Cannot validate image file: Temporary file not found",
         "invalid_bcc_map_type": "Invalid BCC map type",
         "invalid_destination": "Destination format \"%s\" is invalid",
@@ -600,6 +602,7 @@
             "custom": "{= foo =}         - If mailbox has the custom attribute \"foo\" with value \"bar\" it returns \"bar\""
         },
         "domain_footer_plain": "PLAIN footer",
+        "domain_footer_skip_replies": "Ignore footer on reply e-mails",
         "domain_quota": "Domain quota",
         "domains": "Domains",
         "dont_check_sender_acl": "Disable sender check for domain %s (+ alias domains)",
@@ -1099,6 +1102,7 @@
         "verified_yotp_login": "Verified Yubico OTP login"
     },
     "tfa": {
+        "authenticators": "Authenticators",
         "api_register": "%s uses the Yubico Cloud API. Please get an API key for your key <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">here</a>",
         "confirm": "Confirm",
         "confirm_totp_token": "Please confirm your changes by entering the generated token",

+ 3 - 1
data/web/templates/base.twig

@@ -114,7 +114,9 @@
         <li class="logged-in-as nav-item"><a href="#" onclick="logout.submit()" class="nav-link"><b class="username-lia">{{ mailcow_cc_username }} <span class="text-info">({{ dual_login.username }})</span> </b><i class="bi bi-power ms-2"></i></a></li>
         {% endif %}
         {% if not is_master %}
-        <li class="text-warning slave-info nav-item">[ slave ]</li>
+        <div class="nav-link form-check form-switch my-auto d-flex align-items-center">
+            <li class="slave-info">[ slave ]</li>
+        </div>
         {% endif %}
       </ul>
     </div><!--/.nav-collapse -->

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

@@ -305,6 +305,14 @@
                           </select>
                         </div>
                       </div>
+                      <div class="row mb-4">
+                        <label class="control-label col-sm-2" for="domain_footer_skip_replies">{{ lang.edit.domain_footer_skip_replies }}:</label>
+                        <div class="col-sm-10">
+                          <div class="form-check">
+                            <label><input type="checkbox" class="form-check-input" value="1" id="domain_footer_skip_replies" name="skip_replies"{% if domain_footer.skip_replies == '1' %} checked{% endif %}></label>
+                          </div>
+                        </div>
+                      </div>
                       <div class="row mb-2">
                         <label class="control-label col-sm-2" for="domain_footer_html">{{ lang.edit.domain_footer_html }}:</label>
                         <div class="col-sm-10">

+ 4 - 4
data/web/templates/modals/footer.twig

@@ -155,7 +155,7 @@
 
             {% if pending_tfa_authmechs["totp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
               <li class="nav-item">
-                <a class="nav-link {% if pending_tfa_authmechs["totp"] %}active{% endif %}" href="#tfa_tab_totp" data-bs-toggle="tab" id="pending_tfa_tab_totp"><i class="bi bi-clock-history"></i> Time based OTP</a>
+                <a class="nav-link {% if pending_tfa_authmechs["totp"] %}active{% endif %}" href="#tfa_tab_totp" data-bs-toggle="tab" id="pending_tfa_tab_totp"><i class="bi bi-clock-history"></i> Time-based OTP</a>
               </li>
             {% endif %}
 
@@ -173,7 +173,7 @@
                     <form role="form" method="post" id="webauthn_auth_form">
                       <legend class="mt-2 mb-2">
                           <i class="bi bi-shield-fill-check"></i>
-                          Authenticators
+                          {{ lang.tfa.authenticators }}
                           <hr />
                       </legend>
                       <div class="list-group">
@@ -216,7 +216,7 @@
                     <form role="form" method="post">
                       <legend class="mt-2 mb-2">
                           <i class="bi bi-shield-fill-check"></i>
-                          Authenticate
+                          {{ lang.tfa.authenticators }}
                           <hr />
                       </legend>
                       <div class="collapse show pending-tfa-collapse" id="collapseYubiTFA">
@@ -244,7 +244,7 @@
                     <form role="form" method="post">        
                       <legend class="mt-2 mb-2">
                           <i class="bi bi-shield-fill-check"></i>
-                          Authenticators
+                          {{ lang.tfa.authenticators }}
                           <hr />
                       </legend>
                       <div class="list-group">

+ 17 - 14
docker-compose.yml

@@ -2,7 +2,7 @@ version: '2.1'
 services:
 
     unbound-mailcow:
-      image: mailcow/unbound:1.18
+      image: mailcow/unbound:1.19
       environment:
         - TZ=${TZ}
       volumes:
@@ -58,7 +58,7 @@ services:
             - redis
 
     clamd-mailcow:
-      image: mailcow/clamd:1.63
+      image: mailcow/clamd:1.64
       restart: always
       depends_on:
         unbound-mailcow:
@@ -77,7 +77,7 @@ services:
             - clamd
 
     rspamd-mailcow:
-      image: mailcow/rspamd:1.94
+      image: mailcow/rspamd:1.95
       stop_grace_period: 30s
       depends_on:
         - dovecot-mailcow
@@ -107,7 +107,7 @@ services:
             - rspamd
 
     php-fpm-mailcow:
-      image: mailcow/phpfpm:1.85
+      image: mailcow/phpfpm:1.86
       command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
       depends_on:
         - redis-mailcow
@@ -171,7 +171,7 @@ services:
             - phpfpm
 
     sogo-mailcow:
-      image: mailcow/sogo:1.120
+      image: mailcow/sogo:1.121
       environment:
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
@@ -203,7 +203,7 @@ services:
       labels:
         ofelia.enabled: "true"
         ofelia.job-exec.sogo_sessions.schedule: "@every 1m"
-        ofelia.job-exec.sogo_sessions.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool expire-sessions $${SOGO_EXPIRE_SESSION} || exit 0\""
+        ofelia.job-exec.sogo_sessions.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool -v expire-sessions $${SOGO_EXPIRE_SESSION} || exit 0\""
         ofelia.job-exec.sogo_ealarms.schedule: "@every 1m"
         ofelia.job-exec.sogo_ealarms.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-ealarms-notify -p /etc/sogo/sieve.creds || exit 0\""
         ofelia.job-exec.sogo_eautoreply.schedule: "@every 5m"
@@ -218,7 +218,7 @@ services:
             - sogo
 
     dovecot-mailcow:
-      image: mailcow/dovecot:1.26
+      image: mailcow/dovecot:1.27
       depends_on:
         - mysql-mailcow
       dns:
@@ -298,7 +298,7 @@ services:
             - dovecot
 
     postfix-mailcow:
-      image: mailcow/postfix:1.73
+      image: mailcow/postfix:1.74
       depends_on:
         mysql-mailcow:
           condition: service_started
@@ -398,7 +398,7 @@ services:
           condition: service_started
         unbound-mailcow:
           condition: service_healthy
-      image: mailcow/acme:1.85
+      image: mailcow/acme:1.86
       dns:
         - ${IPV4_NETWORK:-172.22.1}.254
       environment:
@@ -434,7 +434,7 @@ services:
             - acme
 
     netfilter-mailcow:
-      image: mailcow/netfilter:1.54
+      image: mailcow/netfilter:1.55
       stop_grace_period: 30s
       depends_on:
         - dovecot-mailcow
@@ -457,7 +457,7 @@ services:
         - /lib/modules:/lib/modules:ro
 
     watchdog-mailcow:
-      image: mailcow/watchdog:2.00
+      image: mailcow/watchdog:2.01
       dns:
         - ${IPV4_NETWORK:-172.22.1}.254
       tmpfs:
@@ -529,7 +529,7 @@ services:
             - watchdog
 
     dockerapi-mailcow:
-      image: mailcow/dockerapi:2.06
+      image: mailcow/dockerapi:2.07
       security_opt:
         - label=disable
       restart: always
@@ -547,8 +547,10 @@ services:
           aliases:
             - dockerapi
 
+    
+    ##### Will be removed soon #####
     solr-mailcow:
-      image: mailcow/solr:1.8.1
+      image: mailcow/solr:1.8.2
       restart: always
       volumes:
         - solr-vol-1:/opt/solr/server/solr/dovecot-fts/data
@@ -562,9 +564,10 @@ services:
         mailcow-network:
           aliases:
             - solr
+    ################################
 
     olefy-mailcow:
-      image: mailcow/olefy:1.11
+      image: mailcow/olefy:1.12
       restart: always
       environment:
         - TZ=${TZ}

+ 3 - 3
generate_config.sh

@@ -34,7 +34,7 @@ if docker compose > /dev/null 2>&1; then
       echo -e "\e[33mNotice: You´ll have to update this Compose Version via your Package Manager manually!\e[0m"
     else
       echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" 
-      echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
+      echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
       exit 1
     fi
 elif docker-compose > /dev/null 2>&1; then
@@ -47,14 +47,14 @@ elif docker-compose > /dev/null 2>&1; then
       echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
     else
       echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" 
-      echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
+      echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
       exit 1
     fi
   fi
 
 else
   echo -e "\e[31mCannot find Docker Compose.\e[0m" 
-  echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
+  echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
   exit 1
 fi
 

+ 29 - 1
helper-scripts/_cold-standby.sh

@@ -2,6 +2,7 @@
 
 PATH=${PATH}:/opt/bin
 DATE=$(date +%Y-%m-%d_%H_%M_%S)
+LOCAL_ARCH=$(uname -m)
 export LC_ALL=C
 
 echo
@@ -148,6 +149,9 @@ else
   echo -e "\e[31mCannot find any Docker Compose on remote, exiting...\e[0m"
   exit 1
 fi
+
+ REMOTE_ARCH=$(ssh -o StrictHostKeyChecking=no -i "${REMOTE_SSH_KEY}" ${REMOTE_SSH_HOST} -p ${REMOTE_SSH_PORT} "uname -m") 
+
 }
 
 SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
@@ -164,6 +168,17 @@ echo -e "\033[1mFound compose project name ${CMPS_PRJ} for ${MAILCOW_HOSTNAME}\0
 echo -e "\033[1mFound SQL ${SQLIMAGE}\033[0m"
 echo
 
+# Print Message if Local Arch and Remote Arch is not the same
+if [[ $LOCAL_ARCH != $REMOTE_ARCH ]]; then
+  echo
+  echo -e "\e[1;33m!!!!!!!!!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!!!!!!!!!\e[0m"
+  echo -e "\e[3;33mDetected Architecture missmatch from source to destination...\e[0m"
+  echo -e "\e[3;33mYour backup is transferred but some volumes might be skipped!\e[0m"
+  echo -e "\e[1;33m!!!!!!!!!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!!!!!!!!!\e[0m"
+  echo
+  sleep 2
+fi
+
 # Make sure destination exists, rsync can fail under some circumstances
 echo -e "\033[1mPreparing remote...\033[0m"
 if ! ssh -o StrictHostKeyChecking=no \
@@ -248,8 +263,21 @@ for vol in $(docker volume ls -qf name="${CMPS_PRJ}"); do
     # Cleanup
     rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"
 
-  else
+  elif [[ "${vol}" =~ "rspamd-vol-1" ]]; then
+    # Exclude rspamd-vol-1 if the Architectures are not the same on source and destination due to compatibility issues.
+    if [[ $LOCAL_ARCH == $REMOTE_ARCH ]]; then
+      echo -e "\033[1mSynchronizing ${vol} from local ${mountpoint}...\033[0m"
+      rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \
+        -i \"${REMOTE_SSH_KEY}\" \
+        -p ${REMOTE_SSH_PORT}" \
+        "${mountpoint}/" root@${REMOTE_SSH_HOST}:"${mountpoint}"
+    else
+      echo -e "\e[1;31mSkipping ${vol} from local maschine due to incompatiblity between different architecture...\e[0m"
+      sleep 2
+      continue
+    fi
 
+  else
     echo -e "\033[1mSynchronizing ${vol} from local ${mountpoint}...\033[0m"
     rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \
       -i \"${REMOTE_SSH_KEY}\" \

+ 36 - 9
helper-scripts/backup_and_restore.sh

@@ -53,6 +53,7 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 COMPOSE_FILE=${SCRIPT_DIR}/../docker-compose.yml
 ENV_FILE=${SCRIPT_DIR}/../.env
 THREADS=$(echo ${THREADS:-1})
+ARCH=$(uname -m)
 
 if ! [[ "${THREADS}" =~ ^[1-9]+$ ]] ; then
   echo "Thread input is not a number!"
@@ -96,6 +97,7 @@ function backup() {
   mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}"
   chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}"
   cp "${SCRIPT_DIR}/../mailcow.conf" "${BACKUP_LOCATION}/mailcow-${DATE}"
+  touch "${BACKUP_LOCATION}/mailcow-${DATE}/.$ARCH"
   for bin in docker; do
   if [[ -z $(which ${bin}) ]]; then
     >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
@@ -231,12 +233,29 @@ function restore() {
       docker start $(docker ps -aqf name=dovecot-mailcow)
       ;;
     rspamd)
-      docker stop $(docker ps -qf name=rspamd-mailcow)
-      docker run -it --name mailcow-backup --rm \
-        -v ${RESTORE_LOCATION}:/backup:z \
-        -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \
-        ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz
-      docker start $(docker ps -aqf name=rspamd-mailcow)
+      if [[ $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then
+        echo -e "\e[33mCould not find a architecture signature of the loaded backup... Maybe the backup was done before the multiarch update?"
+        sleep 2
+        echo -e "Continuing anyhow. If rspamd is crashing opon boot try remove the rspamd volume with docker volume rm ${CMPS_PRJ}_rspamd-vol-1 after you've stopped the stack.\e[0m"
+        sleep 2
+        docker stop $(docker ps -qf name=rspamd-mailcow)
+        docker run -it --name mailcow-backup --rm \
+          -v ${RESTORE_LOCATION}:/backup:z \
+          -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \
+          ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz
+        docker start $(docker ps -aqf name=rspamd-mailcow)
+      elif [[ $ARCH != $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then
+        echo -e "\e[31mThe Architecture of the backed up mailcow OS is different then your restoring mailcow OS..."
+        sleep 2
+        echo -e "Skipping rspamd due to compatibility issues!\e[0m"
+      else
+        docker stop $(docker ps -qf name=rspamd-mailcow)
+        docker run -it --name mailcow-backup --rm \
+          -v ${RESTORE_LOCATION}:/backup:z \
+          -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \
+          ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz
+        docker start $(docker ps -aqf name=rspamd-mailcow)
+      fi
       ;;
     postfix)
       docker stop $(docker ps -qf name=postfix-mailcow)
@@ -360,9 +379,17 @@ elif [[ ${1} == "restore" ]]; then
       FILE_SELECTION[${i}]="redis"
       ((i++))
     elif [[ ${file} =~ rspamd ]]; then
-      echo "[ ${i} ] - Rspamd data"
-      FILE_SELECTION[${i}]="rspamd"
-      ((i++))
+      if [[ $(find "${FOLDER_SELECTION[${input_sel}]}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then
+        echo "[ ${i} ] - Rspamd data (unkown Arch detected, restore with caution!)"
+        FILE_SELECTION[${i}]="rspamd"
+        ((i++))
+      elif [[ $ARCH != $(find "${FOLDER_SELECTION[${input_sel}]}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then
+        echo -e "\e[31m[ NaN ] - Rspamd data (incompatible Arch, cannot restore it)\e[0m"
+      else
+        echo "[ ${i} ] - Rspamd data"
+        FILE_SELECTION[${i}]="rspamd"
+        ((i++))
+      fi
     elif [[ ${file} =~ postfix ]]; then
       echo "[ ${i} ] - Postfix data"
       FILE_SELECTION[${i}]="postfix"

+ 5 - 5
update.sh

@@ -181,7 +181,7 @@ if ! [[ "${DOCKER_COMPOSE_VERSION}" =~ ^(native|standalone)$ ]]; then
         echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
       else
         echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" 
-        echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
+        echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
         exit 1
       fi
   elif docker-compose > /dev/null 2>&1; then
@@ -196,14 +196,14 @@ if ! [[ "${DOCKER_COMPOSE_VERSION}" =~ ^(native|standalone)$ ]]; then
         echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
       else
         echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" 
-        echo -e "\e[31mPlease update/install regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
+        echo -e "\e[31mPlease update/install regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
         exit 1
       fi
     fi
 
   else
     echo -e "\e[31mCannot find Docker Compose.\e[0m" 
-    echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
+    echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
     exit 1
   fi
 
@@ -216,7 +216,7 @@ elif [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then
     if ! $COMPOSE_COMMAND > /dev/null 2>&1 || ! $COMPOSE_COMMAND --version | grep "^2." > /dev/null 2>&1; then
       # IF it cannot find Standalone in > 2.X, then script stops
       echo -e "\e[31mCannot find Docker Compose or the Version is lower then 2.X.X.\e[0m" 
-      echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
+      echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
       exit 1
     fi
       # If it finds the standalone Plugin it will use this instead and change the mailcow.conf Variable accordingly
@@ -236,7 +236,7 @@ elif [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then
     if ! $COMPOSE_COMMAND > /dev/null 2>&1; then
       # IF it cannot find Native in > 2.X, then script stops
       echo -e "\e[31mCannot find Docker Compose.\e[0m" 
-      echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
+      echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
       exit 1
     fi
       # If it finds the native Plugin it will use this instead and change the mailcow.conf Variable accordingly