ソースを参照

Merge pull request #6391 from mailcow/staging

Update 2025-03
FreddleSpl0it 5 ヶ月 前
コミット
c3c68360dc
100 ファイル変更5631 行追加1142 行削除
  1. 1 1
      .github/workflows/rebuild_backup_image.yml
  2. 2 0
      .gitignore
  3. 2 3
      data/Dockerfiles/acme/Dockerfile
  4. 2 2
      data/Dockerfiles/acme/acme.sh
  5. 1 1
      data/Dockerfiles/clamd/clamdcheck.sh
  6. 1 1
      data/Dockerfiles/dockerapi/Dockerfile
  7. 6 2
      data/Dockerfiles/dovecot/Dockerfile
  8. 2 2
      data/Dockerfiles/dovecot/clean_q_aged.sh
  9. 9 126
      data/Dockerfiles/dovecot/docker-entrypoint.sh
  10. 1 0
      data/Dockerfiles/dovecot/trim_logs.sh
  11. 1 1
      data/Dockerfiles/netfilter/Dockerfile
  12. 1 1
      data/Dockerfiles/olefy/Dockerfile
  13. 2 2
      data/Dockerfiles/phpfpm/Dockerfile
  14. 6 6
      data/Dockerfiles/phpfpm/docker-entrypoint.sh
  15. 2 2
      data/Dockerfiles/rspamd/Dockerfile
  16. 1 0
      data/Dockerfiles/sogo/Dockerfile
  17. 8 108
      data/Dockerfiles/sogo/bootstrap-sogo.sh
  18. 20 0
      data/Dockerfiles/sogo/navMailcowBtns.diff
  19. 1 1
      data/Dockerfiles/unbound/Dockerfile
  20. 1 1
      data/Dockerfiles/watchdog/Dockerfile
  21. 2 2
      data/Dockerfiles/watchdog/check_mysql_slavestatus.sh
  22. 1 1
      data/Dockerfiles/watchdog/watchdog.sh
  23. 108 0
      data/conf/dovecot/auth/mailcowauth.php
  24. 42 0
      data/conf/dovecot/auth/passwd-verify.lua
  25. 9 8
      data/conf/dovecot/dovecot.conf
  26. 23 0
      data/conf/nginx/templates/nginx.conf.j2
  27. 231 0
      data/conf/phpfpm/crons/keycloak-sync.php
  28. 198 0
      data/conf/phpfpm/crons/ldap-sync.php
  29. 20 4
      data/conf/postfix/postscreen_access.cidr
  30. 0 2
      data/conf/rspamd/local.d/redis.conf
  31. 9 0
      data/conf/sogo/custom-sogo.js
  32. 34 28
      data/conf/sogo/plist_ldap.sh
  33. 13 4
      data/web/admin/dashboard.php
  34. 29 0
      data/web/admin/index.php
  35. 13 3
      data/web/admin/mailbox.php
  36. 12 3
      data/web/admin/queue.php
  37. 15 2
      data/web/admin/system.php
  38. 233 6
      data/web/api/openapi.yaml
  39. 10 2
      data/web/autodiscover.php
  40. 0 3
      data/web/css/build/013-datatables.css
  41. 60 17
      data/web/css/build/014-mailcow.css
  42. 5 27
      data/web/css/build/015-responsive.css
  43. 0 3
      data/web/css/site/admin.css
  44. 28 0
      data/web/domainadmin/index.php
  45. 58 0
      data/web/domainadmin/mailbox.php
  46. 44 0
      data/web/domainadmin/user.php
  47. 2 1
      data/web/edit.php
  48. 2 0
      data/web/inc/footer.inc.php
  49. 3 3
      data/web/inc/functions.acl.inc.php
  50. 680 0
      data/web/inc/functions.auth.inc.php
  51. 26 4
      data/web/inc/functions.customize.inc.php
  52. 4 1
      data/web/inc/functions.dkim.inc.php
  53. 804 228
      data/web/inc/functions.inc.php
  54. 247 64
      data/web/inc/functions.mailbox.inc.php
  55. 3 3
      data/web/inc/functions.ratelimit.inc.php
  56. 38 1
      data/web/inc/header.inc.php
  57. 21 4
      data/web/inc/init_db.inc.php
  58. 4 3
      data/web/inc/lib/composer.json
  59. 883 28
      data/web/inc/lib/composer.lock
  60. 34 12
      data/web/inc/lib/vendor/bin/carbon
  61. 34 12
      data/web/inc/lib/vendor/bin/var-dump-server
  62. 21 0
      data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/LICENSE
  63. 14 0
      data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/README.md
  64. 36 0
      data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/composer.json
  65. 16 0
      data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonDoctrineType.php
  66. 9 0
      data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonImmutableType.php
  67. 9 0
      data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonType.php
  68. 50 42
      data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonTypeConverter.php
  69. 1 8
      data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeDefaultPrecision.php
  70. 12 4
      data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeImmutableType.php
  71. 24 0
      data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeType.php
  72. 0 5
      data/web/inc/lib/vendor/composer/autoload_classmap.php
  73. 4 0
      data/web/inc/lib/vendor/composer/autoload_files.php
  74. 10 0
      data/web/inc/lib/vendor/composer/autoload_psr4.php
  75. 95 39
      data/web/inc/lib/vendor/composer/autoload_static.php
  76. 857 30
      data/web/inc/lib/vendor/composer/installed.json
  77. 159 36
      data/web/inc/lib/vendor/composer/installed.php
  78. 4 1
      data/web/inc/lib/vendor/directorytree/ldaprecord/.github/ISSUE_TEMPLATE/bug_report.md
  79. 4 1
      data/web/inc/lib/vendor/directorytree/ldaprecord/.github/ISSUE_TEMPLATE/support---help-request.md
  80. 62 0
      data/web/inc/lib/vendor/directorytree/ldaprecord/.github/workflows/run-integration-tests.yml
  81. 5 42
      data/web/inc/lib/vendor/directorytree/ldaprecord/.github/workflows/run-tests.yml
  82. 0 7
      data/web/inc/lib/vendor/directorytree/ldaprecord/.styleci.yml
  83. 4 3
      data/web/inc/lib/vendor/directorytree/ldaprecord/composer.json
  84. 35 0
      data/web/inc/lib/vendor/directorytree/ldaprecord/docker-compose.yml
  85. 5 2
      data/web/inc/lib/vendor/directorytree/ldaprecord/phpunit.xml
  86. 0 15
      data/web/inc/lib/vendor/directorytree/ldaprecord/psalm.xml
  87. 2 2
      data/web/inc/lib/vendor/directorytree/ldaprecord/readme.md
  88. 3 3
      data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Event.php
  89. 18 25
      data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Guard.php
  90. 9 13
      data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/DomainConfiguration.php
  91. 2 2
      data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/Validator.php
  92. 49 30
      data/web/inc/lib/vendor/directorytree/ldaprecord/src/Connection.php
  93. 10 18
      data/web/inc/lib/vendor/directorytree/ldaprecord/src/ConnectionManager.php
  94. 5 8
      data/web/inc/lib/vendor/directorytree/ldaprecord/src/Container.php
  95. 3 3
      data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetailedError.php
  96. 7 12
      data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetectsErrors.php
  97. 3 4
      data/web/inc/lib/vendor/directorytree/ldaprecord/src/EscapesValues.php
  98. 1 1
      data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/ConnectionEvent.php
  99. 18 27
      data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Dispatcher.php
  100. 13 20
      data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/DispatcherInterface.php

+ 1 - 1
.github/workflows/rebuild_backup_image.yml

@@ -30,7 +30,7 @@ jobs:
           password: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Build and push
-        uses: docker/build-push-action@v5
+        uses: docker/build-push-action@v6
         with:
           context: .
           platforms: linux/amd64,linux/arm64

+ 2 - 0
.gitignore

@@ -45,6 +45,7 @@ data/conf/rspamd/local.d/*
 data/conf/rspamd/override.d/*
 data/conf/sogo/custom-theme.js
 data/conf/sogo/plist_ldap
+data/conf/sogo/plist_ldap.sh
 data/conf/sogo/sieve.creds
 data/conf/sogo/cron.creds
 data/conf/sogo/custom-fulllogo.svg
@@ -73,3 +74,4 @@ rebuild-images.sh
 refresh_images.sh
 update_diffs/
 create_cold_standby.sh
+!data/conf/nginx/mailcow_auth.conf

+ 2 - 3
data/Dockerfiles/acme/Dockerfile

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

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

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

+ 1 - 1
data/Dockerfiles/clamd/clamdcheck.sh

@@ -11,4 +11,4 @@ if [ "${CLAMAV_NO_CLAMD:-}" != "false" ]; then
 	echo "Clamd is up"
 fi
 
-exit 0
+exit 0

+ 1 - 1
data/Dockerfiles/dockerapi/Dockerfile

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

+ 6 - 2
data/Dockerfiles/dovecot/Dockerfile

@@ -1,4 +1,4 @@
-FROM alpine:3.20
+FROM alpine:3.21
 
 LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
 
@@ -34,9 +34,13 @@ RUN addgroup -g 5000 vmail \
   lua5.3-sql-mysql \
   icu-data-full \
   mariadb-connector-c \
+  lua-sec \
+  mariadb-dev \
+  glib-dev \
   gcompat \
   mariadb-client \
   perl \
+  perl-dev \
   perl-ntlm \
   perl-cgi \
   perl-crypt-openssl-rsa \
@@ -65,7 +69,7 @@ RUN addgroup -g 5000 vmail \
   perl-par-packer \
   perl-parse-recdescent \
   perl-lockfile-simple \
-  libproc \
+  libproc2 \
   perl-readonly \
   perl-regexp-common \
   perl-sys-meminfo \

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

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

+ 9 - 126
data/Dockerfiles/dovecot/docker-entrypoint.sh

@@ -28,7 +28,7 @@ ${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
 
 # Create missing directories
 [[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
-[[ ! -d /etc/dovecot/lua/ ]] && mkdir -p /etc/dovecot/lua/
+[[ ! -d /etc/dovecot/auth/ ]] && mkdir -p /etc/dovecot/auth/
 [[ ! -d /etc/dovecot/conf.d/ ]] && mkdir -p /etc/dovecot/conf.d/
 [[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage
 [[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
@@ -131,123 +131,6 @@ user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format
 iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
 EOF
 
-cat <<EOF > /etc/dovecot/lua/passwd-verify.lua
-function auth_password_verify(req, pass)
-
-  if req.domain == nil then
-    return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
-  end
-
-  if cur == nil then
-    script_init()
-  end
-
-  if req.user == nil then
-    req.user = ''
-  end
-
-  respbody = {}
-
-  -- check against mailbox passwds
-  local cur,errorString = con:execute(string.format([[SELECT password FROM mailbox
-    WHERE username = '%s'
-      AND active = '1'
-      AND domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')
-      AND IFNULL(JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.force_pw_update')), 0) != '1'
-      AND IFNULL(JSON_UNQUOTE(JSON_VALUE(attributes, '$.%s_access')), 1) = '1']], con:escape(req.user), con:escape(req.domain), con:escape(req.service)))
-  local row = cur:fetch ({}, "a")
-  while row do
-    if req.password_verify(req, row.password, pass) == 1 then
-      con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
-        VALUES ("%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip)))
-      cur:close()
-      con:close()
-      return dovecot.auth.PASSDB_RESULT_OK, ""
-    end
-    row = cur:fetch (row, "a")
-  end
-
-  -- check against app passwds for imap and smtp
-  -- app passwords are only available for imap, smtp, sieve and pop3 when using sasl
-  if req.service == "smtp" or req.service == "imap" or req.service == "sieve" or req.service == "pop3" then
-    local cur,errorString = con:execute(string.format([[SELECT app_passwd.id, %s_access AS has_prot_access, app_passwd.password FROM app_passwd
-      INNER JOIN mailbox ON mailbox.username = app_passwd.mailbox
-      WHERE mailbox = '%s'
-        AND app_passwd.active = '1'
-        AND mailbox.active = '1'
-        AND app_passwd.domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.service), con:escape(req.user), con:escape(req.domain)))
-    local row = cur:fetch ({}, "a")
-    while row do
-      if req.password_verify(req, row.password, pass) == 1 then
-        -- if password is valid and protocol access is 1 OR real_rip matches SOGo, proceed
-        if tostring(req.real_rip) == "__IPV4_SOGO__" then
-          cur:close()
-          con:close()
-          return dovecot.auth.PASSDB_RESULT_OK, ""
-        elseif row.has_prot_access == "1" then
-          con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
-            VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
-          cur:close()
-          con:close()
-          return dovecot.auth.PASSDB_RESULT_OK, ""
-        end
-      end
-      row = cur:fetch (row, "a")
-    end
-  end
-
-  cur:close()
-  con:close()
-
-  return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
-
-  -- PoC
-  -- local reqbody = string.format([[{
-  --   "success":0,
-  --   "service":"%s",
-  --   "app_password":false,
-  --   "username":"%s",
-  --   "real_rip":"%s"
-  -- }]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip))
-  -- http.request {
-  --   method = "POST",
-  --   url = "http://nginx:8081/sasl_log.php",
-  --   source = ltn12.source.string(reqbody),
-  --   headers = {
-  --     ["content-type"] = "application/json",
-  --     ["content-length"] = tostring(#reqbody)
-  --   },
-  --   sink = ltn12.sink.table(respbody)
-  -- }
-
-end
-
-function auth_passdb_lookup(req)
-   return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
-end
-
-function script_init()
-  mysql = require "luasql.mysql"
-  http = require "socket.http"
-  http.TIMEOUT = 5
-  ltn12 = require "ltn12"
-  env  = mysql.mysql()
-  con = env:connect("__DBNAME__","__DBUSER__","__DBPASS__","localhost")
-  return 0
-end
-
-function script_deinit()
-  con:close()
-  env:close()
-end
-EOF
-
-# Replace patterns in app-passdb.lua
-sed -i "s/__DBUSER__/${DBUSER}/g" /etc/dovecot/lua/passwd-verify.lua
-sed -i "s/__DBPASS__/${DBPASS}/g" /etc/dovecot/lua/passwd-verify.lua
-sed -i "s/__DBNAME__/${DBNAME}/g" /etc/dovecot/lua/passwd-verify.lua
-sed -i "s/__IPV4_SOGO__/${IPV4_NETWORK}.248/g" /etc/dovecot/lua/passwd-verify.lua
-
 
 # Migrate old sieve_after file
 [[ -f /etc/dovecot/sieve_after ]] && mv /etc/dovecot/sieve_after /etc/dovecot/global_sieve_after
@@ -385,8 +268,8 @@ sievec /usr/lib/dovecot/sieve/report-ham.sieve
 
 # Fix permissions
 chown root:root /etc/dovecot/sql/*.conf
-chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua
-chmod 640 /etc/dovecot/sql/*.conf /etc/dovecot/lua/passwd-verify.lua
+chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/auth/passwd-verify.lua
+chmod 640 /etc/dovecot/sql/*.conf /etc/dovecot/auth/passwd-verify.lua
 chown -R vmail:vmail /var/vmail/sieve
 chown -R vmail:vmail /var/volatile
 chown -R vmail:vmail /var/vmail_index
@@ -414,15 +297,15 @@ printenv | sed 's/^\(.*\)$/export \1/g' > /source_env.sh
 
 # Clean stopped imapsync jobs
 rm -f /tmp/imapsync_busy.lock
-IMAPSYNC_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs)
-[[ ! -z ${IMAPSYNC_TABLE} ]] && mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'"
+IMAPSYNC_TABLE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs)
+[[ ! -z ${IMAPSYNC_TABLE} ]] && mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'"
 
 # Envsubst maildir_gc
 echo "$(envsubst < /usr/local/bin/maildir_gc.sh)" > /usr/local/bin/maildir_gc.sh
 
 # GUID generation
 while [[ ${VERSIONS_OK} != 'OK' ]]; do
-  if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = \"${DBNAME}\" AND TABLE_NAME = 'versions'") ]]; then
+  if [[ ! -z $(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = \"${DBNAME}\" AND TABLE_NAME = 'versions'") ]]; then
     VERSIONS_OK=OK
   else
     echo "Waiting for versions table to be created..."
@@ -433,11 +316,11 @@ PUBKEY_MCRYPT=$(doveconf -P 2> /dev/null | grep -i mail_crypt_global_public_key
 if [ -f ${PUBKEY_MCRYPT} ]; then
   GUID=$(cat <(echo ${MAILCOW_HOSTNAME}) /mail_crypt/ecpubkey.pem | sha256sum | cut -d ' ' -f1 | tr -cd "[a-fA-F0-9.:/] ")
   if [ ${#GUID} -eq 64 ]; then
-    mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
+    mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
 REPLACE INTO versions (application, version) VALUES ("GUID", "${GUID}");
 EOF
   else
-    mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
+    mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
 REPLACE INTO versions (application, version) VALUES ("GUID", "INVALID");
 EOF
   fi
@@ -456,7 +339,7 @@ done
 
 # For some strange, unknown and stupid reason, Dovecot may run into a race condition, when this file is not touched before it is read by dovecot/auth
 # May be related to something inside Docker, I seriously don't know
-touch /etc/dovecot/lua/passwd-verify.lua
+touch /etc/dovecot/auth/passwd-verify.lua
 
 if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
   cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf

+ 1 - 0
data/Dockerfiles/dovecot/trim_logs.sh

@@ -23,3 +23,4 @@ catch_non_zero "${REDIS_CMDLINE} LTRIM AUTODISCOVER_LOG 0 ${LOG_LINES}"
 catch_non_zero "${REDIS_CMDLINE} LTRIM API_LOG 0 ${LOG_LINES}"
 catch_non_zero "${REDIS_CMDLINE} LTRIM RL_LOG 0 ${LOG_LINES}"
 catch_non_zero "${REDIS_CMDLINE} LTRIM WATCHDOG_LOG 0 ${LOG_LINES}"
+catch_non_zero "${REDIS_CMDLINE} LTRIM CRON_LOG 0 ${LOG_LINES}"

+ 1 - 1
data/Dockerfiles/netfilter/Dockerfile

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

+ 1 - 1
data/Dockerfiles/olefy/Dockerfile

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

+ 2 - 2
data/Dockerfiles/phpfpm/Dockerfile

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

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

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

+ 2 - 2
data/Dockerfiles/rspamd/Dockerfile

@@ -2,11 +2,11 @@ FROM debian:bookworm-slim
 LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
 
 ARG DEBIAN_FRONTEND=noninteractive
-ARG RSPAMD_VER=rspamd_3.11.0-2~90a175b45
+ARG RSPAMD_VER=rspamd_3.11.1-1~ab0b44951
 ARG CODENAME=bookworm
 ENV LC_ALL=C
 
-RUN apt-get update && apt-get install -y \
+RUN apt-get update && apt-get install -y --no-install-recommends \
   tzdata \
   ca-certificates \
   gnupg2 \

+ 1 - 0
data/Dockerfiles/sogo/Dockerfile

@@ -47,6 +47,7 @@ COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
 COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
 COPY supervisord.conf /etc/supervisor/supervisord.conf
 COPY acl.diff /acl.diff
+COPY navMailcowBtns.diff /navMailcowBtns.diff
 COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
 COPY docker-entrypoint.sh /
 

+ 8 - 108
data/Dockerfiles/sogo/bootstrap-sogo.sh

@@ -14,120 +14,16 @@ do
 done
 
 # Wait for updated schema
-DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
+DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
 DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
 while [[ "${DBV_NOW}" != "${DBV_NEW}" ]]; do
   echo "Waiting for schema update..."
-  DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
+  DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
   DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
   sleep 5
 done
 echo "DB schema is ${DBV_NOW}"
 
-# Recreate view
-if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
-  echo "We are master, preparing sogo_view..."
-  mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS sogo_view"
-  while [[ ${VIEW_OK} != 'OK' ]]; do
-    mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
-CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings) AS
-SELECT
-   mailbox.username,
-   mailbox.domain,
-   mailbox.username,
-   IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.force_pw_update')) = '0', IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.sogo_access')) = 1, password, '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'), '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'),
-   mailbox.name,
-   mailbox.username,
-   IFNULL(GROUP_CONCAT(ga.aliases ORDER BY ga.aliases SEPARATOR ' '), ''),
-   IFNULL(gda.ad_alias, ''),
-   IFNULL(external_acl.send_as_acl, ''),
-   mailbox.kind,
-   mailbox.multiple_bookings
-FROM
-   mailbox
-   LEFT OUTER JOIN
-      grouped_mail_aliases ga
-      ON ga.username REGEXP CONCAT('(^|,)', mailbox.username, '($|,)')
-   LEFT OUTER JOIN
-      grouped_domain_alias_address gda
-      ON gda.username = mailbox.username
-   LEFT OUTER JOIN
-      grouped_sender_acl_external external_acl
-      ON external_acl.username = mailbox.username
-WHERE
-   mailbox.active = '1'
-GROUP BY
-   mailbox.username;
-EOF
-    if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view'") ]]; then
-      VIEW_OK=OK
-    else
-      echo "Will retry to setup SOGo view in 3s..."
-      sleep 3
-    fi
-  done
-else
-  while [[ ${VIEW_OK} != 'OK' ]]; do
-    if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view'") ]]; then
-      VIEW_OK=OK
-    else
-      echo "Waiting for SOGo view to be created by master..."
-      sleep 3
-    fi
-  done
-fi
-
-# Wait for static view table if missing after update and update content
-if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
-  echo "We are master, preparing _sogo_static_view..."
-  while [[ ${STATIC_VIEW_OK} != 'OK' ]]; do
-    if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then
-      STATIC_VIEW_OK=OK
-      echo "Updating _sogo_static_view content..."
-      # If changed, also update init_db.inc.php
-      mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "REPLACE INTO _sogo_static_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings) SELECT c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings from sogo_view;"
-      mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "DELETE FROM _sogo_static_view WHERE c_uid NOT IN (SELECT username FROM mailbox WHERE active = '1')"
-    else
-      echo "Waiting for database initialization..."
-      sleep 3
-    fi
-  done
-else
-  while [[ ${STATIC_VIEW_OK} != 'OK' ]]; do
-    if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then
-      STATIC_VIEW_OK=OK
-    else
-      echo "Waiting for database initialization by master..."
-      sleep 3
-    fi
-  done
-fi
-
-
-# Recreate password update trigger
-if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
-  echo "We are master, preparing update trigger..."
-  mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TRIGGER IF EXISTS sogo_update_password"
-  while [[ ${TRIGGER_OK} != 'OK' ]]; do
-  mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
-DELIMITER -
-CREATE TRIGGER sogo_update_password AFTER UPDATE ON _sogo_static_view
-FOR EACH ROW
-BEGIN
-UPDATE mailbox SET password = NEW.c_password WHERE NEW.c_uid = username;
-END;
--
-DELIMITER ;
-EOF
-    if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_NAME = 'sogo_update_password'") ]]; then
-      TRIGGER_OK=OK
-    else
-      echo "Will retry to setup SOGo password update trigger in 3s"
-      sleep 3
-    fi
-  done
-fi
-
 # cat /dev/urandom seems to hang here occasionally and is not recommended anyway, better use openssl
 RAND_PASS=$(openssl rand -base64 16 | tr -dc _A-Z-a-z-0-9)
 
@@ -213,10 +109,10 @@ while read -r line gal
                 </dict>" >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
   # Generate alternative LDAP authentication dict, when SQL authentication fails
   # This will nevertheless read attributes from LDAP
-  line=${line} envsubst < /etc/sogo/plist_ldap >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
+  /etc/sogo/plist_ldap.sh ${line} ${gal} >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
   echo "            </array>
         </dict>" >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
-done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain, CASE gal WHEN '1' THEN 'YES' ELSE 'NO' END AS gal FROM domain;" -B -N)
+done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain, CASE gal WHEN '1' THEN 'YES' ELSE 'NO' END AS gal FROM domain;" -B -N)
 
 # Generate footer
 echo '    </dict>
@@ -240,6 +136,10 @@ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
 #  fi
 #fi
 
+if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then
+  patch -R /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
+fi
+
 # Rename custom logo, if any
 [[ -f /etc/sogo/sogo-full.svg ]] && mv /etc/sogo/sogo-full.svg /etc/sogo/custom-fulllogo.svg
 

+ 20 - 0
data/Dockerfiles/sogo/navMailcowBtns.diff

@@ -0,0 +1,20 @@
+59,65d58
+<                ng-show="::!activeUser.isSuperUser"
+<                var:ng-click="navButtonClick"
+<                ng-href="/user">
+<       <md-icon>build</md-icon>
+<       <md-tooltip><var:string label:value="mailcow"/></md-tooltip>
+<     </md-button>
+<     <md-button class="md-icon-button"
+83c76
+<                onclick="document.getElementById('mc_logout').setAttribute('action', '/'); document.getElementById('mc_logout').submit();"
+---
+>                ng-show="::activeUser.path.logoff.length"
+85c78
+<                ng-href="#">
+---
+>                ng-href="{{::activeUser.path.logoff}}">
+89,91d81
+<     <form method="POST" id="mc_logout" action="user">
+<       <input type="hidden" name="logout" value="1">
+<     </form>

+ 1 - 1
data/Dockerfiles/unbound/Dockerfile

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

+ 1 - 1
data/Dockerfiles/watchdog/Dockerfile

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

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

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

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

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

+ 108 - 0
data/conf/dovecot/auth/mailcowauth.php

@@ -0,0 +1,108 @@
+<?php
+ini_set('error_reporting', 0);
+header('Content-Type: application/json');
+
+$post = trim(file_get_contents('php://input'));
+if ($post) {
+  $post = json_decode($post, true);
+}
+
+
+$return = array("success" => false);
+if(!isset($post['username']) || !isset($post['password']) || !isset($post['real_rip'])){
+  error_log("MAILCOWAUTH: Bad Request");
+  http_response_code(400); // Bad Request
+  echo json_encode($return);
+  exit();
+}
+
+require_once('../../../web/inc/vars.inc.php');
+if (file_exists('../../../web/inc/vars.local.inc.php')) {
+  include_once('../../../web/inc/vars.local.inc.php');
+}
+require_once '../../../web/inc/lib/vendor/autoload.php';
+
+
+// Init Redis
+$redis = new Redis();
+try {
+  if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
+    $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
+  }
+  else {
+    $redis->connect('redis-mailcow', 6379);
+  }
+  $redis->auth(getenv("REDISPASS"));
+}
+catch (Exception $e) {
+  error_log("MAILCOWAUTH: " . $e . PHP_EOL);
+  http_response_code(500); // Internal Server Error
+  echo json_encode($return);
+  exit;
+}
+
+// Init database
+$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
+$opt = [
+    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
+    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+    PDO::ATTR_EMULATE_PREPARES   => false,
+];
+try {
+  $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
+}
+catch (PDOException $e) {
+  error_log("MAILCOWAUTH: " . $e . PHP_EOL);
+  http_response_code(500); // Internal Server Error
+  echo json_encode($return);
+  exit;
+}
+
+// Load core functions first
+require_once 'functions.inc.php';
+require_once 'functions.auth.inc.php';
+require_once 'sessions.inc.php';
+require_once 'functions.mailbox.inc.php';
+require_once 'functions.ratelimit.inc.php';
+require_once 'functions.acl.inc.php';
+
+
+$isSOGoRequest = $post['real_rip'] == getenv('IPV4_NETWORK') . '.248';
+$result = false;
+$protocol = $post['protocol'];
+if ($isSOGoRequest) {
+  $protocol = null;
+  // This is a SOGo Auth request. First check for SSO password.
+  $sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
+  if ($sogo_sso_pass === $post['password']){
+    error_log('MAILCOWAUTH: SOGo SSO auth for user ' . $post['username']);
+    $result = true;
+  }
+}
+if ($result === false){
+  $result = apppass_login($post['username'], $post['password'], $protocol, array(
+    'is_internal' => true,
+    'remote_addr' => $post['real_rip']
+  ));
+  if ($result) error_log('MAILCOWAUTH: App auth for user ' . $post['username']);
+}
+if ($result === false){
+  // Init Identity Provider
+  $iam_provider = identity_provider('init');
+  $iam_settings = identity_provider('get');
+  $result = user_login($post['username'], $post['password'], array('is_internal' => true));
+  if ($result) error_log('MAILCOWAUTH: User auth for user ' . $post['username']);
+}
+
+if ($result) {
+  http_response_code(200); // OK
+  $return['success'] = true;
+} else {
+  error_log("MAILCOWAUTH: Login failed for user " . $post['username']);
+  http_response_code(401); // Unauthorized
+}
+
+
+echo json_encode($return);
+session_destroy();
+exit;

+ 42 - 0
data/conf/dovecot/auth/passwd-verify.lua

@@ -0,0 +1,42 @@
+function auth_password_verify(request, password)
+  if request.domain == nil then
+    return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
+  end
+
+  json = require "cjson"
+  ltn12 = require "ltn12"
+  https = require "ssl.https"
+  https.TIMEOUT = 5
+
+  local req = {
+    username = request.user,
+    password = password,
+    real_rip = request.real_rip,
+    protocol = {}
+  }
+  req.protocol[request.service] = true
+  local req_json = json.encode(req)
+  local res = {} 
+  
+  local b, c = https.request {
+    method = "POST",
+    url = "https://nginx:9082",
+    source = ltn12.source.string(req_json),
+    headers = {
+      ["content-type"] = "application/json",
+      ["content-length"] = tostring(#req_json)
+    },
+    sink = ltn12.sink.table(res),
+    insecure = true
+  }
+  local api_response = json.decode(table.concat(res))
+  if api_response.success == true then
+    return dovecot.auth.PASSDB_RESULT_OK, ""
+  end
+  
+  return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
+end
+
+function auth_passdb_lookup(req)
+   return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
+end

+ 9 - 8
data/conf/dovecot/dovecot.conf

@@ -53,7 +53,7 @@ mail_shared_explicit_inbox = yes
 mail_prefetch_count = 30
 passdb {
   driver = lua
-  args = file=/etc/dovecot/lua/passwd-verify.lua blocking=yes
+  args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes cache_key=%u:%w
   result_success = return-ok
   result_failure = continue
   result_internalfail = continue
@@ -69,7 +69,7 @@ passdb {
 # a return of the following passdb is mandatory
 passdb {
   driver = lua
-  args = file=/etc/dovecot/lua/passwd-verify.lua blocking=yes
+  args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes
 }
 # Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
 service doveadm {
@@ -125,6 +125,7 @@ service managesieve-login {
 }
 service imap-login {
   service_count = 1
+  process_min_avail = 2
   process_limit = 10000
   vsz_limit = 1G
   user = dovenull
@@ -140,6 +141,7 @@ service imap-login {
 }
 service pop3-login {
   service_count = 1
+  process_min_avail = 1
   vsz_limit = 1G
   inet_listener pop3_haproxy {
     port = 10110
@@ -239,7 +241,7 @@ plugin {
   mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem
   mail_crypt_save_version = 2
 
-  # Enable compression while saving, lz4 Dovecot v2.2.11+
+  # Enable compression while saving, lz4 Dovecot v2.3.17+
   zlib_save = lz4
 
   mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
@@ -274,10 +276,10 @@ service stats {
   }
 }
 imap_max_line_length = 2 M
-#auth_cache_verify_password_with_worker = yes
-#auth_cache_negative_ttl = 0
-#auth_cache_ttl = 30 s
-#auth_cache_size = 2 M
+auth_cache_verify_password_with_worker = yes
+auth_cache_negative_ttl = 60s
+auth_cache_ttl = 300s
+auth_cache_size = 10M
 auth_verbose_passwords = sha1:6
 service replicator {
   process_min_avail = 1
@@ -302,7 +304,6 @@ replication_dsync_parameters = -d -l 30 -U -n INBOX
 !include_try /etc/dovecot/sni.conf
 !include_try /etc/dovecot/sogo_trusted_ip.conf
 !include_try /etc/dovecot/extra.conf
-!include_try /etc/dovecot/sogo-sso.conf
 !include_try /etc/dovecot/shared_namespace.conf
 !include_try /etc/dovecot/conf.d/fts.conf
 # </Includes>

+ 23 - 0
data/conf/nginx/templates/nginx.conf.j2

@@ -159,6 +159,29 @@ http {
         }
     }
 
+    server {
+        listen 9082 ssl http2;
+
+        ssl_certificate /etc/ssl/mail/cert.pem;
+        ssl_certificate_key /etc/ssl/mail/key.pem;
+
+        index mailcowauth.php;
+        server_name _;
+        error_log  /var/log/nginx/error.log;
+        access_log /var/log/nginx/access.log;
+        root /mailcowauth;
+        client_max_body_size 10M;
+        location ~ \.php$ {
+            client_max_body_size 10M;
+            try_files $uri =404;
+            fastcgi_split_path_info ^(.+\.php)(/.+)$;
+            fastcgi_pass phpfpm:9001;
+            include fastcgi_params;
+            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+            fastcgi_param PATH_INFO $fastcgi_path_info;
+        }
+    }
+
     {% for cert in valid_cert_dirs %}
     server {
         {% if not HTTP_REDIRECT %}

+ 231 - 0
data/conf/phpfpm/crons/keycloak-sync.php

@@ -0,0 +1,231 @@
+<?php
+
+require_once(__DIR__ . '/../web/inc/vars.inc.php');
+if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
+  include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
+}
+require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
+
+// Init database
+//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
+$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
+$opt = [
+    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
+    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+    PDO::ATTR_EMULATE_PREPARES   => false,
+];
+try {
+  $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
+}
+catch (PDOException $e) {
+  logMsg("err", $e->getMessage());
+  session_destroy();
+  exit;
+}
+
+// Init Redis
+$redis = new Redis();
+try {
+  if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
+    $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
+  }
+  else {
+    $redis->connect('redis-mailcow', 6379);
+  }
+  $redis->auth(getenv("REDISPASS"));
+}
+catch (Exception $e) {
+  echo "Exiting: " . $e->getMessage();
+  session_destroy();
+  exit;
+}
+
+function logMsg($priority, $message, $task = "Keycloak Sync") {
+  global $redis;
+
+  $finalMsg = array(
+    "time" => time(),
+    "priority" => $priority,
+    "task" => $task,
+    "message" => $message
+  );
+  $redis->lPush('CRON_LOG', json_encode($finalMsg));
+}
+
+// Load core functions first
+require_once __DIR__ . '/../web/inc/functions.inc.php';
+require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
+require_once __DIR__ . '/../web/inc/sessions.inc.php';
+require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php';
+require_once __DIR__ . '/../web/inc/functions.ratelimit.inc.php';
+require_once __DIR__ . '/../web/inc/functions.acl.inc.php';
+
+$_SESSION['mailcow_cc_username'] = "admin";
+$_SESSION['mailcow_cc_role'] = "admin";
+$_SESSION['acl']['tls_policy'] = "1";
+$_SESSION['acl']['quarantine_notification'] = "1";
+$_SESSION['acl']['quarantine_category'] = "1";
+$_SESSION['acl']['ratelimit'] = "1";
+$_SESSION['acl']['sogo_access'] = "1";
+$_SESSION['acl']['protocol_access'] = "1";
+$_SESSION['acl']['mailbox_relayhost'] = "1";
+$_SESSION['acl']['unlimited_quota'] = "1";
+
+$iam_settings = identity_provider('get');
+if ($iam_settings['authsource'] != "keycloak" || (intval($iam_settings['periodic_sync']) != 1 && intval($iam_settings['import_users']) != 1)) {
+  session_destroy();
+  exit;
+}
+
+// Set pagination variables
+$start = 0;
+$max = 100;
+
+// lock sync if already running
+$lock_file = '/tmp/iam-sync.lock';
+if (file_exists($lock_file)) {
+  $lock_file_parts = explode("\n", file_get_contents($lock_file));
+  $pid = $lock_file_parts[0];
+  if (count($lock_file_parts) > 1){
+    $last_execution = $lock_file_parts[1];
+    $elapsed_time = (time() - $last_execution) / 60;
+    if ($elapsed_time < intval($iam_settings['sync_interval'])) {
+      logMsg("warning", "Sync not ready (".number_format((float)$elapsed_time, 2, '.', '')."min / ".$iam_settings['sync_interval']."min)");
+      session_destroy();
+      exit;
+    }
+  }
+
+  if (posix_kill($pid, 0)) {
+    logMsg("warning", "Sync is already running");
+    session_destroy();
+    exit;
+  } else {
+    unlink($lock_file);
+  }
+}
+$lock_file_handle = fopen($lock_file, 'w');
+fwrite($lock_file_handle, getmypid());
+fclose($lock_file_handle);
+
+// Init Keycloak Provider
+$iam_provider = identity_provider('init');
+
+// Loop until all users have been retrieved
+while (true) {
+  // Get admin access token
+  $admin_token = identity_provider("get-keycloak-admin-token");
+
+  // Make the API request to retrieve the users
+  $url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users?first=$start&max=$max";
+  $ch = curl_init();
+  curl_setopt($ch, CURLOPT_URL, $url);
+  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+  curl_setopt($ch, CURLOPT_HTTPHEADER, [
+    "Content-Type: application/json",
+    "Authorization: Bearer " . $admin_token
+  ]);
+  $response = curl_exec($ch);
+  $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+  curl_close($ch);
+
+  if ($code != 200){
+    logMsg("err", "Recieved HTTP {$code}");
+    session_destroy();
+    exit;
+  }
+  try {
+    $response = json_decode($response, true);
+  } catch (Exception $e) {
+    logMsg("err", $e->getMessage());
+    break;
+  }
+  if (!is_array($response)){
+    logMsg("err", "Recieved malformed response from keycloak api");
+    break;
+  }
+  if (count($response) == 0) {
+    break;
+  }
+
+  // Process the batch of users
+  foreach ($response as $user) {
+    if (empty($user['email'])){
+      logMsg("warning", "No email address in keycloak found for user " . $user['name']);
+      continue;
+    }
+
+    // try get mailbox user
+    $stmt = $pdo->prepare("SELECT
+      mailbox.*,
+      domain.active AS d_active
+      FROM `mailbox`
+      INNER JOIN domain on mailbox.domain = domain.domain
+      WHERE `kind` NOT REGEXP 'location|thing|group'
+        AND `username` = :user");
+    $stmt->execute(array(':user' => $user['email']));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+    // check if matching attribute mapping exists
+    $user_template = $user['attributes']['mailcow_template'][0];
+    $mapper_key = array_search($user_template, $iam_settings['mappers']);
+
+    $_SESSION['access_all_exception'] = '1';
+    if (!$row && intval($iam_settings['import_users']) == 1){
+      if ($mapper_key === false){
+        if (!empty($iam_settings['default_template'])) {
+          $mbox_template = $iam_settings['default_template'];
+          logMsg("warning", "Using default template for user " . $user['email']);
+        } else {
+          logMsg("warning", "No matching attribute mapping found for user " . $user['email']);
+          continue;
+        }
+      } else {
+        $mbox_template = $iam_settings['templates'][$mapper_key];
+      }
+      // mailbox user does not exist, create...
+      logMsg("info", "Creating user " . $user['email']);
+      $create_res = mailbox('add', 'mailbox_from_template', array(
+        'domain' => explode('@', $user['email'])[1],
+        'local_part' => explode('@', $user['email'])[0],
+        'name' => $user['firstName'] . " " . $user['lastName'],
+        'authsource' => 'keycloak',
+        'template' => $mbox_template
+      ));
+      if (!$create_res){
+        logMsg("err", "Could not create user " . $user['email']);
+        continue;
+      }
+    } else if ($row && intval($iam_settings['periodic_sync']) == 1) {
+      if ($mapper_key === false){
+        logMsg("warning", "No matching attribute mapping found for user " . $user['email']);
+        continue;
+      }
+      $mbox_template = $iam_settings['templates'][$mapper_key];
+      // mailbox user does exist, sync attribtues...
+      logMsg("info", "Syncing attributes for user " . $user['email']);
+      mailbox('edit', 'mailbox_from_template', array(
+        'username' => $user['email'],
+        'name' => $user['firstName'] . " " . $user['lastName'],
+        'template' => $mbox_template
+      ));
+    } else {
+      // skip mailbox user
+      logMsg("info", "Skipping user " . $user['email']);
+    }
+    $_SESSION['access_all_exception'] = '0';
+
+    sleep(0.025);
+  }
+
+  // Update the pagination variables for the next batch
+  $start += $max;
+  sleep(1);
+}
+
+logMsg("info", "DONE!");
+// add last execution time to lock file
+$lock_file_handle = fopen($lock_file, 'w');
+fwrite($lock_file_handle, getmypid() . "\n" . time());
+fclose($lock_file_handle);
+session_destroy();

+ 198 - 0
data/conf/phpfpm/crons/ldap-sync.php

@@ -0,0 +1,198 @@
+<?php
+
+require_once(__DIR__ . '/../web/inc/vars.inc.php');
+if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
+  include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
+}
+require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
+
+// Init database
+//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
+$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
+$opt = [
+    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
+    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+    PDO::ATTR_EMULATE_PREPARES   => false,
+];
+try {
+  $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
+}
+catch (PDOException $e) {
+  logMsg("err", $e->getMessage());
+  session_destroy();
+  exit;
+}
+
+// Init Redis
+$redis = new Redis();
+try {
+  if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
+    $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
+  }
+  else {
+    $redis->connect('redis-mailcow', 6379);
+  }
+  $redis->auth(getenv("REDISPASS"));
+}
+catch (Exception $e) {
+  echo "Exiting: " . $e->getMessage();
+  session_destroy();
+  exit;
+}
+
+function logMsg($priority, $message, $task = "LDAP Sync") {
+  global $redis;
+
+  $finalMsg = array(
+    "time" => time(),
+    "priority" => $priority,
+    "task" => $task,
+    "message" => $message
+  );
+  $redis->lPush('CRON_LOG', json_encode($finalMsg));
+}
+
+// Load core functions first
+require_once __DIR__ . '/../web/inc/functions.inc.php';
+require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
+require_once __DIR__ . '/../web/inc/sessions.inc.php';
+require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php';
+require_once __DIR__ . '/../web/inc/functions.ratelimit.inc.php';
+require_once __DIR__ . '/../web/inc/functions.acl.inc.php';
+
+$_SESSION['mailcow_cc_username'] = "admin";
+$_SESSION['mailcow_cc_role'] = "admin";
+$_SESSION['acl']['tls_policy'] = "1";
+$_SESSION['acl']['quarantine_notification'] = "1";
+$_SESSION['acl']['quarantine_category'] = "1";
+$_SESSION['acl']['ratelimit'] = "1";
+$_SESSION['acl']['sogo_access'] = "1";
+$_SESSION['acl']['protocol_access'] = "1";
+$_SESSION['acl']['mailbox_relayhost'] = "1";
+$_SESSION['acl']['unlimited_quota'] = "1";
+
+$iam_settings = identity_provider('get');
+if ($iam_settings['authsource'] != "ldap" || (intval($iam_settings['periodic_sync']) != 1 && intval($iam_settings['import_users']) != 1)) {
+  session_destroy();
+  exit;
+}
+
+// Set pagination variables
+$start = 0;
+$max = 100;
+
+// lock sync if already running
+$lock_file = '/tmp/iam-sync.lock';
+if (file_exists($lock_file)) {
+  $lock_file_parts = explode("\n", file_get_contents($lock_file));
+  $pid = $lock_file_parts[0];
+  if (count($lock_file_parts) > 1){
+    $last_execution = $lock_file_parts[1];
+    $elapsed_time = (time() - $last_execution) / 60;
+    if ($elapsed_time < intval($iam_settings['sync_interval'])) {
+      logMsg("warning", "Sync not ready (".number_format((float)$elapsed_time, 2, '.', '')."min / ".$iam_settings['sync_interval']."min)");
+      session_destroy();
+      exit;
+    }
+  }
+
+  if (posix_kill($pid, 0)) {
+    logMsg("warning", "Sync is already running");
+    session_destroy();
+    exit;
+  } else {
+    unlink($lock_file);
+  }
+}
+$lock_file_handle = fopen($lock_file, 'w');
+fwrite($lock_file_handle, getmypid());
+fclose($lock_file_handle);
+
+// Init Provider
+$iam_provider = identity_provider('init');
+
+// Get ldap users
+$ldap_query = $iam_provider->query();
+if (!empty($iam_settings['filter'])) {
+  $ldap_query = $ldap_query->rawFilter($iam_settings['filter']);
+}
+$response = $ldap_query->where($iam_settings['username_field'], "*")
+  ->where($iam_settings['attribute_field'], "*")
+  ->select([$iam_settings['username_field'], $iam_settings['attribute_field'], 'displayname'])
+  ->paginate($max);
+
+// Process the users
+foreach ($response as $user) {
+  // try get mailbox user
+  $stmt = $pdo->prepare("SELECT
+    mailbox.*,
+    domain.active AS d_active
+    FROM `mailbox`
+    INNER JOIN domain on mailbox.domain = domain.domain
+    WHERE `kind` NOT REGEXP 'location|thing|group'
+      AND `username` = :user");
+  $stmt->execute(array(':user' => $user[$iam_settings['username_field']][0]));
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+  // check if matching attribute mapping exists
+  $user_template = $user[$iam_settings['attribute_field']][0];
+  $mapper_key = array_search($user_template, $iam_settings['mappers']);
+
+  if (empty($user[$iam_settings['username_field']][0])){
+    logMsg("warning", "Skipping user " . $user['displayname'][0] . " due to empty LDAP ". $iam_settings['username_field'] . " property.");
+    continue;
+  }
+
+  $_SESSION['access_all_exception'] = '1';
+  if (!$row && intval($iam_settings['import_users']) == 1){
+    if ($mapper_key === false){
+      if (!empty($iam_settings['default_template'])) {
+        $mbox_template = $iam_settings['default_template'];
+      } else {
+        logMsg("warning", "No matching attribute mapping found for user " . $user[$iam_settings['username_field']][0]);
+        continue;
+      }
+    } else {
+      $mbox_template = $iam_settings['templates'][$mapper_key];
+    }
+    // mailbox user does not exist, create...
+    logMsg("info", "Creating user " .  $user[$iam_settings['username_field']][0]);
+    $create_res = mailbox('add', 'mailbox_from_template', array(
+      'domain' => explode('@',  $user[$iam_settings['username_field']][0])[1],
+      'local_part' => explode('@',  $user[$iam_settings['username_field']][0])[0],
+      'name' => $user['displayname'][0],
+      'authsource' => 'ldap',
+      'template' => $mbox_template
+    ));
+    if (!$create_res){
+      logMsg("err", "Could not create user " . $user[$iam_settings['username_field']][0]);
+      continue;
+    }
+  } else if ($row && intval($iam_settings['periodic_sync']) == 1) {
+    if ($mapper_key === false){
+      logMsg("warning", "No matching attribute mapping found for user " . $user[$iam_settings['username_field']][0]);
+      continue;
+    }
+    $mbox_template = $iam_settings['templates'][$mapper_key];
+    // mailbox user does exist, sync attribtues...
+    logMsg("info", "Syncing attributes for user " . $user[$iam_settings['username_field']][0]);
+    mailbox('edit', 'mailbox_from_template', array(
+      'username' =>  $user[$iam_settings['username_field']][0],
+      'name' => $user['displayname'][0],
+      'template' => $mbox_template
+    ));
+  } else {
+    // skip mailbox user
+    logMsg("info", "Skipping user " .  $user[$iam_settings['username_field']][0]);
+  }
+  $_SESSION['access_all_exception'] = '0';
+
+  sleep(0.025);
+}
+
+logMsg("info", "DONE!");
+// add last execution time to lock file
+$lock_file_handle = fopen($lock_file, 'w');
+fwrite($lock_file_handle, getmypid() . "\n" . time());
+fclose($lock_file_handle);
+session_destroy();

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

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

+ 0 - 2
data/conf/rspamd/local.d/redis.conf

@@ -1,2 +0,0 @@
-servers = "redis:6379";
-timeout = 10;

+ 9 - 0
data/conf/sogo/custom-sogo.js

@@ -1,3 +1,11 @@
+// redirect to mailcow login form
+document.addEventListener('DOMContentLoaded', function () {
+    var loginForm = document.forms.namedItem("loginForm");
+    if (loginForm) {
+        window.location.href = '/user';
+    }
+});
+
 // Custom SOGo JS
 
 // Change the visible font-size in the editor, this does not change the font of a html message by default
@@ -5,3 +13,4 @@ CKEDITOR.addCss("body {font-size: 16px !important}");
 
 // Enable scayt by default
 //CKEDITOR.config.scayt_autoStartup = true;
+

+ 34 - 28
data/conf/sogo/plist_ldap → data/conf/sogo/plist_ldap.sh

@@ -1,28 +1,34 @@
-                <!--
-                <example>
-                    <key>canAuthenticate</key>
-                    <string>YES</string>
-                    <key>id</key>
-                    <string>${line}_ldap</string>
-                    <key>isAddressBook</key>
-                    <string>NO</string>
-                    <key>IDFieldName</key>
-                    <string>mail</string>
-                    <key>UIDFieldName</key>
-                    <string>uid</string>
-                    <key>bindFields</key>
-                    <array>
-                        <string>mail</string>
-                    </array>
-                    <key>type</key>
-                    <string>ldap</string>
-                    <key>bindDN</key>
-                    <string>cn=admin,dc=example,dc=local</string>
-                    <key>bindPassword</key>
-                    <string>password</string>
-                    <key>baseDN</key>
-                    <string>ou=People,dc=example,dc=local</string>
-                    <key>hostname</key>
-                    <string>ldap://1.2.3.4:389</string>
-                </example>
-                -->
+#!/bin/bash
+
+domain="$1"
+gal_status="$2"
+
+echo "
+                <!--
+                <example>
+                    <key>canAuthenticate</key>
+                    <string>YES</string>
+                    <key>id</key>
+                    <string>"${domain}"_ldap</string>
+                    <key>isAddressBook</key>
+                    <string>"${gal_status}"</string>
+                    <key>IDFieldName</key>
+                    <string>mail</string>
+                    <key>UIDFieldName</key>
+                    <string>uid</string>
+                    <key>bindFields</key>
+                    <array>
+                        <string>mail</string>
+                    </array>
+                    <key>type</key>
+                    <string>ldap</string>
+                    <key>bindDN</key>
+                    <string>cn=admin,dc=example,dc=local</string>
+                    <key>bindPassword</key>
+                    <string>password</string>
+                    <key>baseDN</key>
+                    <string>ou=People,dc=example,dc=local</string>
+                    <key>hostname</key>
+                    <string>ldap://1.2.3.4:389</string>
+                </example>
+                -->"

+ 13 - 4
data/web/debug.php → data/web/admin/dashboard.php

@@ -1,8 +1,17 @@
 <?php
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
 
-if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
-  header('Location: /');
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
+  header('Location: /domainadmin/mailbox');
+  exit();
+}
+elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
+  header('Location: /user');
+  exit();
+}
+elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
+  header('Location: /admin');
   exit();
 }
 
@@ -15,7 +24,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
   $_SESSION['gal'] = json_decode($license_cache, true);
 }
 
-$js_minifier->add('/web/js/site/debug.js');
+$js_minifier->add('/web/js/site/dashboard.js');
 
 // vmail df
 $exec_fields = array('cmd' => 'system', 'task' => 'df', 'dir' => '/var/vmail');
@@ -59,7 +68,7 @@ foreach ($containers_info as $container => $container_info) {
 $hostname = getenv('MAILCOW_HOSTNAME');
 $timezone = getenv('TZ');
 
-$template = 'debug.twig';
+$template = 'dashboard.twig';
 $template_data = [
   'log_lines' => getenv('LOG_LINES'),
   'vmail_df' => $vmail_df,

+ 29 - 0
data/web/admin/index.php

@@ -0,0 +1,29 @@
+<?php
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
+
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
+  header('Location: /admin/dashboard');
+  exit();
+}
+elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
+  header('Location: /domainadmin/mailbox');
+  exit();
+}
+elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
+  header('Location: /user');
+  exit();
+}
+
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
+$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
+$_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
+
+
+$template = 'admin_index.twig';
+$template_data = [
+  'login_delay' => @$_SESSION['ldelay']
+];
+
+$js_minifier->add('/web/js/site/index.js');
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';

+ 13 - 3
data/web/mailbox.php → data/web/admin/mailbox.php

@@ -1,10 +1,20 @@
 <?php
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
 
-if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
-  header('Location: /');
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
+  header('Location: /domainadmin/mailbox');
   exit();
 }
+elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
+  header('Location: /user');
+  exit();
+}
+elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
+  header('Location: /admin');
+  exit();
+}
+
 require_once $_SERVER['DOCUMENT_ROOT'] .  '/inc/header.inc.php';
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 
@@ -14,7 +24,7 @@ $js_minifier->add('/web/js/site/mailbox.js');
 $js_minifier->add('/web/js/presets/sieveMailbox.js');
 $js_minifier->add('/web/js/site/pwgen.js');
 
-$role = ($_SESSION['mailcow_cc_role'] == "admin") ? 'admin' : 'domainadmin';
+$role = "admin";
 $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? 'true' : 'false';
 $allow_admin_email_login = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"])) ? 'true' : 'false';
 

+ 12 - 3
data/web/queue.php → data/web/admin/queue.php

@@ -1,8 +1,17 @@
 <?php
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
 
-if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
-  header('Location: /');
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
+  header('Location: /domainadmin/mailbox');
+  exit();
+}
+elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
+  header('Location: /user');
+  exit();
+}
+elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
+  header('Location: /admin');
   exit();
 }
 
@@ -11,7 +20,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
 $js_minifier->add('/web/js/site/queue.js');
 $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
 
-$role = ($_SESSION['mailcow_cc_role'] == "admin") ? 'admin' : 'domainadmin';
+$role = "admin";
 
 $template = 'queue.twig';
 $template_data = [

+ 15 - 2
data/web/admin.php → data/web/admin/system.php

@@ -1,8 +1,17 @@
 <?php
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
 
-if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
-  header('Location: /');
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
+  header('Location: /domainadmin/mailbox');
+  exit();
+}
+elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
+  header('Location: /user');
+  exit();
+}
+elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
+  header('Location: /admin');
   exit();
 }
 
@@ -86,6 +95,8 @@ $cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allo
 $cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
 
 $f2b_data = fail2ban('get');
+// mbox templates
+$mbox_templates = mailbox('get', 'mailbox_templates');
 
 $template = 'admin.twig';
 $template_data = [
@@ -118,6 +129,8 @@ $template_data = [
   'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
   'cors_settings' => $cors_settings,
   'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
+  'iam_settings' => $iam_settings,
+  'mbox_templates' => $mbox_templates,
   'lang_admin' => json_encode($lang['admin']),
   'lang_datatables' => json_encode($lang['datatables'])
 ];

+ 233 - 6
data/web/api/openapi.yaml

@@ -409,7 +409,7 @@ paths:
                   description: a list of domains for which a dkim key should be generated
                   type: string
                 key_size:
-                  description: the key size (1024 or 2048)
+                  description: the key size (1024, 2048, 3072 or 4096)
                   type: number
               type: object
       summary: Generate DKIM Key
@@ -1112,6 +1112,7 @@ paths:
                 domain: domain.tld
                 local_part: info
                 name: Full name
+                authsource: mailcow
                 password: atedismonsin
                 password2: atedismonsin
                 quota: "3072"
@@ -1132,11 +1133,16 @@ paths:
                 name:
                   description: Full name of the mailbox user
                   type: string
+                authsource:
+                  description: Specifies the authentication source for the mailbox.
+                  type: string
+                  enum: [mailcow, ldap, keycloak, generic-oidc]
+                  default: mailcow
                 password2:
                   description: mailbox password for confirmation
                   type: string
                 password:
-                  description: mailbox password
+                  description: mailbox password when using `mailcow` as the authentication source.
                   type: string
                 quota:
                   description: mailbox quota
@@ -3374,6 +3380,7 @@ paths:
                   active: "1"
                   force_pw_update: "0"
                   name: Full name
+                  authsource: mailcow
                   password: ""
                   password2: ""
                   quota: "3072"
@@ -3398,11 +3405,15 @@ paths:
                     name:
                       description: Full name of the mailbox user
                       type: string
+                    authsource:
+                      description: Specifies the authentication source for the mailbox.
+                      type: string
+                      enum: [mailcow, ldap, keycloak, generic-oidc]
                     password2:
                       description: new mailbox password for confirmation
                       type: string
                     password:
-                      description: new mailbox password
+                      description: new mailbox password when using `mailcow` as the authentication source.
                       type: string
                     quota:
                       description: mailbox quota
@@ -5687,7 +5698,7 @@ paths:
         - description: name of domain
           in: path
           name: domain
-          required: false
+          required: true
           schema:
             type: string
         - description: e.g. api-key-string
@@ -5755,8 +5766,8 @@ paths:
       tags:
         - Cross-Origin Resource Sharing (CORS)
       description: >-
-        This endpoint allows you to manage Cross-Origin Resource Sharing (CORS) settings for the API. 
-        CORS is a security feature implemented by web browsers to prevent unauthorized cross-origin requests. 
+        This endpoint allows you to manage Cross-Origin Resource Sharing (CORS) settings for the API.
+        CORS is a security feature implemented by web browsers to prevent unauthorized cross-origin requests.
         By editing the CORS settings, you can specify which domains and which methods are permitted to access the API resources from outside the mailcow domain.
       operationId: Edit Cross-Origin Resource Sharing (CORS) settings
       requestBody:
@@ -5814,6 +5825,220 @@ paths:
         Using this endpoint you can get the global spam filter score or the spam filter score of a certain mailbox.
       operationId: Get mailbox or global spam filter score
       summary: Get mailbox or global spam filter score
+  /api/v1/edit/identity-provider:
+    post:
+      responses:
+        "401":
+          $ref: "#/components/responses/Unauthorized"
+        "200":
+          content:
+            application/json:
+              examples:
+                response:
+                  value:
+                    - type: "success"
+                      log:
+                        - "identity_provider"
+                        - "edit"
+                        - authsource: "keycloak"
+                          server_url: "https://auth.mailcow.tld"
+                          realm: "mailcow"
+                          client_id: "mailcow_client"
+                          client_secret: "*"
+                          redirect_url: "https://mail.mailcow.tld"
+                          version: "26.1.3"
+                          default_template: "Default"
+                          mappers:
+                            - "small_mbox"
+                            - "medium_mbox"
+                          templates:
+                            - "small"
+                            - "medium"
+                          ignore_ssl_error: true
+                          mailpassword_flow: true
+                          periodic_sync: true
+                          import_users: true
+                          sync_interval: 30
+                      msg:
+                        - "object_modified"
+                        - ""
+          description: OK
+          headers: { }
+      tags:
+        - Identity Provider
+      description: >-
+        Configure an external Identity Provider to use as user authentication
+      operationId: Edit external Identity Provider settings
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                items:
+                  type: array
+                  default: ["identity-provider"]
+                attr:
+                  type: object
+                  properties:
+                    authsource:
+                      description: Specifies the type of the Identity Provider
+                      type: string
+                      enum: [ldap, keycloak, generic-oidc]
+                    server_url:
+                      description: The base URL of your Keycloak server. Required if `authsource` is keycloak.
+                      type: string
+                    realm:
+                      description: The Keycloak realm where the mailcow client is configured. Required if `authsource` is keycloak.
+                      type: string
+                    client_id:
+                      description: The Client ID assigned to mailcow Client in OIDC Provider. Required if `authsource` is keycloak or generic-oidc.
+                      type: string
+                    client_secret:
+                      description: The Client Secret assigned to mailcow Client in OIDC Provider. Required if `authsource` is keycloak or generic-oidc.
+                      type: string
+                    redirect_url:
+                      description: The redirect URL that OIDC Provider will use after authentication. Required if `authsource` is keycloak or generic-oidc.
+                      type: string
+                    version:
+                      description: Specifies the Keycloak version. Required if `authsource` is keycloak.
+                      type: string
+                    default_template:
+                      description: (Optional) If no matching Attribute Mapping exists for a User, the default template will be used for creating the mailbox, but not for updating the mailbox.
+                      type: string
+                    mappers:
+                      description: (Optional) Attribute values used to match a mailbox template. Each element corresponds to the respective index in the templates array (i.e., the first element matches the first element of templates, the second matches the second, and so on).
+                      type: array
+                    templates:
+                      description: (Optional) Defines the mailbox templates to be assigned. Each element corresponds to the respective index in the `mappers` array.
+                      type: array
+                    ignore_ssl_error:
+                      description: If enabled, SSL certificate validation is bypassed
+                      type: boolean
+                      default: false
+                    mailpassword_flow:
+                      description: If enabled, mailcow will attempt to validate user credentials using the Keycloak Admin REST API instead of relying solely on the Authorization Code Flow.
+                      type: boolean
+                      default: false
+                    periodic_sync:
+                      description: If enabled, mailcow periodically performs a full sync of all users from Keycloak or LDAP.
+                      type: boolean
+                      default: false
+                    import_users:
+                      description: If enabled, new users are automatically imported from Keycloak or LDAP into mailcow.
+                      type: boolean
+                      default: false
+                    sync_interval:
+                      description: Defines the time interval (in minutes) for periodic synchronization and user imports.
+                      type: number
+                      default: 15
+                    host:
+                      description: The address of your LDAP server. You can provide a single hostname or a comma-separated list of hosts for fallback in case the primary server is unreachable. Required if `authsource` is ldap.
+                      type: string
+                    port:
+                      description: The port used to connect to the LDAP server. Required if `authsource` is ldap.
+                      type: string
+                    use_ssl:
+                      description: enable LDAPS connection. If Port is set to 389 it will be overriden to 636.
+                      type: boolean
+                      default: false
+                    use_tls:
+                      description: enable TLS connection. TLS is recommended over SSL. SSL Ports cannot be used.
+                      type: boolean
+                      default: false
+                    basedn:
+                      description: The Distinguished Name (DN) from which searches will be performed. Required if `authsource` is ldap.
+                      type: string
+                    username_field:
+                      description: The LDAP attribute used to identify users during authentication. Required if `authsource` is ldap.
+                      type: string
+                      default: mail
+                    filter:
+                      description: An optional LDAP search filter to refine which users can authenticate.
+                      type: string
+                    attribute_field:
+                      description: Specifies an LDAP attribute that holds a specific value which can be mapped to a mailbox template using the Attribute Mapping section. Required if `authsource` is ldap.
+                      type: string
+                    binddn:
+                      description: The Distinguished Name (DN) of the LDAP user that will be used to authenticate and perform LDAP searches. This account should have sufficient permissions to read the required attributes. Required if `authsource` is ldap.
+                      type: string
+                    bindpass:
+                      description: The password for the Bind DN user. It is required for authentication when connecting to the LDAP server. Required if `authsource` is ldap.
+                      type: string
+                    authorize_url:
+                      description: The OIDC provider's authorization server URL. Required if `authsource` is generic-oidc.
+                      type: string
+                    token_url:
+                      description: The OIDC provider's token server URL. Required if `authsource` is generic-oidc.
+                      type: string
+                    userinfo_url:
+                      description: The OIDC provider's user info server URL. Required if `authsource` is generic-oidc.
+                      type: string
+                    client_scopes:
+                      description: Specifies the OIDC scopes requested during authentication.
+                      type: string
+                      default: "openid profile email mailcow_template"
+            examples:
+              keycloak:
+                value:
+                  items:
+                    - "identity-provider"
+                  attr:
+                    authsource: "keycloak"
+                    server_url: "https://auth.mailcow.tld"
+                    realm: "mailcow"
+                    client_id: "mailcow_client"
+                    client_secret: "Xy7GdPqvJ9m3R8sT2LkVZ5W1oNbCaYQf"
+                    redirect_url: "https://mail.mailcow.tld"
+                    version: "26.1.3"
+                    default_template: "Default"
+                    mappers: ["small_mbox", "medium_mbox"]
+                    templates: ["small", "medium"]
+                    ignore_ssl_error: true
+                    mailpassword_flow: true
+                    periodic_sync: true
+                    import_users: true
+                    sync_interval: 30
+              ldap:
+                value:
+                  items:
+                    - "identity-provider"
+                  attr:
+                    authsource: "ldap"
+                    host: "127.0.0.1"
+                    port: "389"
+                    use_ssl: false
+                    use_tls: false
+                    ignore_ssl_error: false
+                    basedn: "DC=mailcow,DC=local"
+                    username_field: "mail"
+                    filter: "(memberOf:1.2.840.113556.1.4.1941:=DC=mailcow,DC=local)"
+                    attribute_field: "othermailbox"
+                    binddn: "CN=LDAP Read Only,CN=Users,DC=mailcow,DC=local"
+                    bindpass: "moohoo"
+                    default_template: "Default"
+                    mappers: ["small_mbox", "medium_mbox"]
+                    templates: ["small", "medium"]
+                    periodic_sync: true
+                    import_users: true
+                    sync_interval: 30
+              generic-oidc:
+                value:
+                  items:
+                    - "identity-provider"
+                  attr:
+                    authsource: "generic-oidc"
+                    authorize_url: "https://auth.mailcow.tld/application/o/authorize/"
+                    token_url: "https://auth.mailcow.tld/application/o/token/"
+                    userinfo_url: "https://auth.mailcow.tld/application/o/userinfo/"
+                    client_id: "mailcow_client"
+                    client_secret: "Xy7GdPqvJ9m3R8sT2LkVZ5W1oNbCaYQf"
+                    redirect_url: "https://mail.mailcow.tld"
+                    client_scopes: "openid profile email mailcow_template"
+                    default_template: "Default"
+                    mappers: ["small_mbox", "medium_mbox"]
+                    templates: ["small", "medium"]
+                    ignore_ssl_error: true
+      summary: Edit external Identity Provider
 
 tags:
   - name: Domains
@@ -5860,3 +6085,5 @@ tags:
     description: Edit domain ratelimits
   - name: Cross-Origin Resource Sharing (CORS)
     description: Manage Cross-Origin Resource Sharing (CORS) settings
+  - name: Identity Provider
+    description: Manage external Identity Provider settings

+ 10 - 2
data/web/autodiscover.php

@@ -1,10 +1,13 @@
 <?php
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/vendor/autoload.php';
 require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/vars.inc.php';
-require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
-$default_autodiscover_config = $autodiscover_config;
 if(file_exists('inc/vars.local.inc.php')) {
   include_once 'inc/vars.local.inc.php';
 }
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
+$default_autodiscover_config = $autodiscover_config;
 $autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
 
 // Redis
@@ -50,6 +53,11 @@ $opt = [
   PDO::ATTR_EMULATE_PREPARES   => false,
 ];
 $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
+
+// Init Identity Provider
+$iam_provider = identity_provider('init');
+$iam_settings = identity_provider('get');
+
 $login_user = strtolower(trim($_SERVER['PHP_AUTH_USER']));
 $login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW']));
 

+ 0 - 3
data/web/css/build/013-datatables.css

@@ -8,9 +8,6 @@
 .dtr-details {
     width: 100%;
 }
-.table-striped>tbody>tr:nth-of-type(odd) {
-    background-color: #F2F2F2;
-}
 td.child>ul>li {
     display: flex;
 }

+ 60 - 17
data/web/css/build/014-mailcow.css

@@ -33,6 +33,13 @@
        url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff2') format('woff2'),
        url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff') format('woff');
 }
+
+body {
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background-color: #fbfbfb;
+}
 #maxmsgsize { min-width: 80px; }
 #slider1 .slider-selection {
 	background: #FFD700;
@@ -74,10 +81,23 @@
   align-items: center;
   padding: 0 10px !important;
 }
-.navbar-fixed-bottom .navbar-collapse, 
+.navbar-fixed-bottom .navbar-collapse,
 .navbar-fixed-top .navbar-collapse {
   max-height: 1000px
 }
+.nav-tabs .nav-link, .nav-tabs .nav-link.disabled, .nav-tabs .nav-link.disabled:hover, .nav-tabs .nav-link.disabled:focus {
+  border-color: #dfdfdf;
+}
+.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
+  border-color: #dfdfdf;
+  border-bottom: 1px solid #ffffff;
+}
+.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {
+  border-color: #dfdfdf;
+}
+.nav-tabs {
+  border-bottom: 1px solid #dfdfdf;
+}
 .bi {
   display: inline-block;
   font-size: 12pt;
@@ -123,18 +143,18 @@
   }
 }
 @keyframes blink {
-  50% { 
-    color: transparent 
+  50% {
+    color: transparent
   }
 }
-.loader-dot { 
-  animation: 1s blink infinite 
+.loader-dot {
+  animation: 1s blink infinite
 }
-.loader-dot:nth-child(2) { 
-  animation-delay: 250ms 
+.loader-dot:nth-child(2) {
+  animation-delay: 250ms
 }
-.loader-dot:nth-child(3) { 
-  animation-delay: 500ms 
+.loader-dot:nth-child(3) {
+  animation-delay: 500ms
 }
 
 pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;}
@@ -200,13 +220,13 @@ legend {
 }
 .haveibeenpwned {
   cursor: pointer;
-  -webkit-user-select: none;  
-  -moz-user-select: none;    
-  -ms-user-select: none;      
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
   user-select: none;
 }
 .full-width-select {
-  width: 100%!important;  
+  width: 100%!important;
 }
 .tooltip {
   font-family: inherit;
@@ -330,7 +350,7 @@ code {
 .caret {
   transform: rotate(0deg);
 }
-a[aria-expanded='true'] > .caret, 
+a[aria-expanded='true'] > .caret,
 button[aria-expanded='true'] > .caret {
   transform: rotate(-180deg);
 }
@@ -340,7 +360,7 @@ button[aria-expanded='true'] > .caret {
 }
 .list-group-header {
   background: #f7f7f7;
-} 
+}
 
 
 .bg-primary, .alert-primary, .btn-primary {
@@ -366,12 +386,13 @@ button[aria-expanded='true'] > .caret {
     background-color: #f0f0f0;
 }
 .btn.btn-outline-secondary {
-  border-color: #cfcfcf !important;  
+  color: #000000 !important;
+  border-color: #cfcfcf !important;
 }
 .btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
     background-color: #f0f0f0 !important;
 }
-.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {    
+.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
     color: #fff;
     background-color: #555;
     background-image: none;
@@ -389,4 +410,26 @@ button[aria-expanded='true'] > .caret {
 .badge.bg-danger > a {
     color: #fff !important;
     text-decoration: none;
+}
+
+.hr-title {
+  display: flex;
+  align-items: center;
+  text-align: center;
+  margin: 20px 0;
+}
+
+.hr-title::before,
+.hr-title::after {
+  content: "";
+  flex: 1;
+  border-bottom: 1px solid #ccc;
+}
+
+.hr-title:not(:empty)::before {
+  margin-right: 10px;
+}
+
+.hr-title:not(:empty)::after {
+  margin-left: 10px;
 }

+ 5 - 27
data/web/css/build/015-responsive.css

@@ -6,15 +6,9 @@
   max-width: 350px;
 }
 
-.card-login .apps .btn {
-  width: auto;
-  float: left;
-  margin-right: 10px;
-  margin-top: auto;
-}
-.card-login .apps .btn:hover {
-  margin-top: 1px !important;
-  border-bottom-width: 3px;
+.card .apps {
+  display: flex;
+  flex-wrap: wrap;
 }
 
 .responsive-tabs .nav-tabs {
@@ -43,16 +37,6 @@
       opacity: 1;
   }
 
-  .card-login .apps .btn {
-    width: 100%;
-    float: none;
-    margin-bottom: 10px;
-  }
-
-  .card-login .apps .btn {
-    border-bottom-width: 4px;
-  }
-
   .xs-show {
     display: block !important;
   }
@@ -113,9 +97,6 @@
   .btn-group.nowrap .dropdown-menu {
     width: 100%;
   }
-  .card-login .btn-group {
-    display: block;
-  }
   .mass-actions-user .btn-group {
     float: none;
   }
@@ -191,9 +172,6 @@
   .btn-group .btn i {
     margin-right: 5px;
   }
-  .card-login .btn-group .btn {
-    display: block !important;
-  }
 
   .dt-sm-head-hidden .dtr-title {
     display: none !important;
@@ -206,7 +184,7 @@
   .senders-mw220 {
     max-width: 100% !important;
   }
-  
+
   table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
   table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before,
   table.dataTable td.dt-control:before {
@@ -215,7 +193,7 @@
       line-height: 2rem;
       margin-top: -15px;
   }
-  
+
   li .dtr-data {
       padding: 0;
   }

+ 0 - 3
data/web/css/site/admin.css

@@ -59,9 +59,6 @@ body.modal-open {
 .table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td {
   padding: 3px;
 }
-table tbody tr {
-  cursor: pointer;
-}
 table tbody tr td input[type="checkbox"] {
   cursor: pointer;
 }

+ 28 - 0
data/web/domainadmin/index.php

@@ -0,0 +1,28 @@
+<?php
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
+
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
+  header('Location: /domainadmin/mailbox');
+  exit();
+}
+elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
+  header('Location: /admin/dashboard');
+  exit();
+}
+elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
+  header('Location: /user');
+  exit();
+}
+
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
+$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
+$_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
+
+$template = 'domainadmin_index.twig';
+$template_data = [
+  'login_delay' => @$_SESSION['ldelay'],
+];
+
+$js_minifier->add('/web/js/site/index.js');
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';

+ 58 - 0
data/web/domainadmin/mailbox.php

@@ -0,0 +1,58 @@
+<?php
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
+
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
+  header('Location: /admin/dashboard');
+  exit();
+}
+elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
+  header('Location: /user');
+  exit();
+}
+elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "domainadmin") {
+  header('Location: /domainadmin');
+  exit();
+}
+
+require_once $_SERVER['DOCUMENT_ROOT'] .  '/inc/header.inc.php';
+$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
+
+
+
+$js_minifier->add('/web/js/site/mailbox.js');
+$js_minifier->add('/web/js/presets/sieveMailbox.js');
+$js_minifier->add('/web/js/site/pwgen.js');
+
+$role = "domainadmin";
+$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? 'true' : 'false';
+$allow_admin_email_login = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"])) ? 'true' : 'false';
+
+// domains
+$domains = mailbox('get', 'domains');
+
+// mailboxes
+$mailboxes = [];
+foreach ($domains as $domain) {
+  foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) {
+    $mailboxes[] = $mailbox;
+  }
+}
+
+$template = 'mailbox.twig';
+$template_data = [
+  'acl' => $_SESSION['acl'],
+  'acl_json' => json_encode($_SESSION['acl']),
+  'role' => $role,
+  'is_dual' => $is_dual,
+  'allow_admin_email_login' => $allow_admin_email_login,
+  'global_filters' => mailbox('get', 'global_filter_details'),
+  'domains' => $domains,
+  'mailboxes' => $mailboxes,
+  'lang_mailbox' => json_encode($lang['mailbox']),
+  'lang_rl' => json_encode($lang['ratelimit']),
+  'lang_edit' => json_encode($lang['edit']),
+  'lang_datatables' => json_encode($lang['datatables']),
+];
+
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';

+ 44 - 0
data/web/domainadmin/user.php

@@ -0,0 +1,44 @@
+<?php
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
+
+if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
+
+  /*
+  / DOMAIN ADMIN
+  */
+
+  require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
+  $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
+  $tfa_data = get_tfa();
+  $fido2_data = fido2(array("action" => "get_friendly_names"));
+  $username = $_SESSION['mailcow_cc_username'];
+
+  $template = 'domainadmin.twig';
+  $template_data = [
+    'acl' => $_SESSION['acl'],
+    'acl_json' => json_encode($_SESSION['acl']),
+    'user_spam_score' => mailbox('get', 'spam_score', $username),
+    'tfa_data' => $tfa_data,
+    'fido2_data' => $fido2_data,
+    'lang_user' => json_encode($lang['user']),
+    'lang_datatables' => json_encode($lang['datatables']),
+  ];
+}
+elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
+  header('Location: /admin/dashboard');
+  exit();
+}
+elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
+  header('Location: /user');
+  exit();
+}
+else {
+  header('Location: /domainadmin');
+  exit();
+}
+
+$js_minifier->add('/web/js/site/user.js');
+$js_minifier->add('/web/js/site/pwgen.js');
+
+require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';

+ 2 - 1
data/web/edit.php

@@ -131,7 +131,8 @@ if (isset($_SESSION['mailcow_cc_role'])) {
           'rlyhosts' => $rlyhosts,
           'sender_acl_handles' => mailbox('get', 'sender_acl_handles', $mailbox),
           'user_acls' => acl('get', 'user', $mailbox),
-          'mailbox_details' => $result
+          'mailbox_details' => $result,
+          'iam_settings' => $iam_settings,
         ];
       }
     }

+ 2 - 0
data/web/inc/footer.inc.php

@@ -66,6 +66,8 @@ $globalVariables = [
   'lang_acl' => json_encode($lang['acl']),
   'lang_tfa' => json_encode($lang['tfa']),
   'lang_fido2' => json_encode($lang['fido2']),
+  'lang_success' => json_encode($lang['success']),
+  'lang_danger' => json_encode($lang['danger']),
   'docker_timeout' => $DOCKER_TIMEOUT,
   'session_lifetime' => (int)$SESSION_LIFETIME,
   'csrf_token' => $_SESSION['CSRF']['TOKEN'],

+ 3 - 3
data/web/inc/functions.acl.inc.php

@@ -1,5 +1,5 @@
 <?php
-function acl($_action, $_scope = null, $_data = null) {
+function acl($_action, $_scope = null, $_data = null, $_extra = null) {
   global $pdo;
   global $lang;
   $_data_log = $_data;
@@ -24,7 +24,7 @@ function acl($_action, $_scope = null, $_data = null) {
             }
             // Users cannot change their own ACL
             if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)
-              || ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin')) {
+              || ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin' && $_SESSION['access_all_exception'] != '1')) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
@@ -34,7 +34,7 @@ function acl($_action, $_scope = null, $_data = null) {
             }
             // Read all available acl options by calling acl(get)
             // Set all available acl options we cannot find in the post data to 0, else 1
-            $is_now = acl('get', 'user', $username);
+            $is_now = acl('get', 'user', $username, $_extra);
             if (!empty($is_now)) {
               foreach ($is_now as $acl_now_name => $acl_now_val) {
                 $set_acls[$acl_now_name] = (isset($acl_post[$acl_now_name])) ? 1 : 0;

+ 680 - 0
data/web/inc/functions.auth.inc.php

@@ -0,0 +1,680 @@
+<?php
+function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
+  global $pdo;
+  global $redis;
+
+  $is_internal = $extra['is_internal'];
+  $role = $extra['role'];
+
+  // Try validate admin
+  if (!isset($role) || $role == "admin") {
+    $result = admin_login($user, $pass);
+    if ($result !== false) return $result;
+  }
+
+  // Try validate domain admin
+  if (!isset($role) || $role == "domain_admin") {
+    $result = domainadmin_login($user, $pass);
+    if ($result !== false) return $result;
+  }
+
+  // Try validate user
+  if (!isset($role) || $role == "user") {
+    $result = user_login($user, $pass);
+    if ($result !== false) return $result;
+  }
+
+  // Try validate app password
+  if (!isset($role) || $role == "app") {
+    $result = apppass_login($user, $pass, $app_passwd_data);
+    if ($result !== false) return $result;
+  }
+
+  // skip log and only return false if it's an internal request
+  if ($is_internal == true) return false;
+
+  if (!isset($_SESSION['ldelay'])) {
+    $_SESSION['ldelay'] = "0";
+    $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
+    error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
+  }
+  elseif (!isset($_SESSION['mailcow_cc_username'])) {
+    $_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
+    $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
+    error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
+  }
+  $_SESSION['return'][] =  array(
+    'type' => 'danger',
+    'log' => array(__FUNCTION__, $user, '*'),
+    'msg' => 'login_failed'
+  );
+
+  sleep($_SESSION['ldelay']);
+  return false;
+}
+
+function admin_login($user, $pass){
+  global $pdo;
+
+  if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
+    if (!$is_internal){
+      $_SESSION['return'][] =  array(
+        'type' => 'danger',
+        'log' => array(__FUNCTION__, $user, '*'),
+        'msg' => 'malformed_username'
+      );
+    }
+    return false;
+  }
+
+  $user = strtolower(trim($user));
+  $stmt = $pdo->prepare("SELECT `password` FROM `admin`
+      WHERE `superadmin` = '1'
+      AND `active` = '1'
+      AND `username` = :user");
+  $stmt->execute(array(':user' => $user));
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+  // verify password
+  if (verify_hash($row['password'], $pass)) {
+    // check for tfa authenticators
+    $authenticators = get_tfa($user);
+    if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
+      // active tfa authenticators found, set pending user login
+      $_SESSION['pending_mailcow_cc_username'] = $user;
+      $_SESSION['pending_mailcow_cc_role'] = "admin";
+      $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
+      unset($_SESSION['ldelay']);
+      $_SESSION['return'][] =  array(
+        'type' => 'info',
+        'log' => array(__FUNCTION__, $user, '*'),
+        'msg' => 'awaiting_tfa_confirmation'
+      );
+      return "pending";
+    } else {
+      unset($_SESSION['ldelay']);
+      // Reactivate TFA if it was set to "deactivate TFA for next login"
+      $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
+      $stmt->execute(array(':user' => $user));
+      $_SESSION['return'][] =  array(
+        'type' => 'success',
+        'log' => array(__FUNCTION__, $user, '*'),
+        'msg' => array('logged_in_as', $user)
+      );
+      return "admin";
+    }
+  }
+
+  return false;
+}
+function domainadmin_login($user, $pass){
+  global $pdo;
+
+  if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
+    if (!$is_internal){
+      $_SESSION['return'][] =  array(
+        'type' => 'danger',
+        'log' => array(__FUNCTION__, $user, '*'),
+        'msg' => 'malformed_username'
+      );
+    }
+    return false;
+  }
+
+  $stmt = $pdo->prepare("SELECT `password` FROM `admin`
+      WHERE `superadmin` = '0'
+      AND `active`='1'
+      AND `username` = :user");
+  $stmt->execute(array(':user' => $user));
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+  // verify password
+  if (verify_hash($row['password'], $pass) !== false) {
+    // check for tfa authenticators
+    $authenticators = get_tfa($user);
+    if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
+      $_SESSION['pending_mailcow_cc_username'] = $user;
+      $_SESSION['pending_mailcow_cc_role'] = "domainadmin";
+      $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
+      unset($_SESSION['ldelay']);
+      $_SESSION['return'][] =  array(
+        'type' => 'info',
+        'log' => array(__FUNCTION__, $user, '*'),
+        'msg' => 'awaiting_tfa_confirmation'
+      );
+      return "pending";
+    }
+    else {
+      unset($_SESSION['ldelay']);
+      // Reactivate TFA if it was set to "deactivate TFA for next login"
+      $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
+      $stmt->execute(array(':user' => $user));
+      $_SESSION['return'][] =  array(
+        'type' => 'success',
+        'log' => array(__FUNCTION__, $user, '*'),
+        'msg' => array('logged_in_as', $user)
+      );
+      return "domainadmin";
+    }
+  }
+
+  return false;
+}
+function user_login($user, $pass, $extra = null){
+  global $pdo;
+  global $iam_provider;
+  global $iam_settings;
+
+  $is_internal = $extra['is_internal'];
+
+  if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
+    if (!$is_internal){
+      $_SESSION['return'][] =  array(
+        'type' => 'danger',
+        'log' => array(__FUNCTION__, $user, '*'),
+        'msg' => 'malformed_username'
+      );
+    }
+    return false;
+  }
+
+  $stmt = $pdo->prepare("SELECT
+      mailbox.*,
+      domain.active AS d_active
+      FROM `mailbox`
+      INNER JOIN domain on mailbox.domain = domain.domain
+      WHERE `kind` NOT REGEXP 'location|thing|group'
+        AND `username` = :user");
+  $stmt->execute(array(':user' => $user));
+  $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+  // user does not exist, try call idp login and create user if possible via rest flow
+  if (!$row){
+    $result = false;
+    if ($iam_settings['authsource'] == 'keycloak' && intval($iam_settings['mailpassword_flow']) == 1){
+      $result = keycloak_mbox_login_rest($user, $pass, array('is_internal' => $is_internal, 'create' => true));
+    } else if ($iam_settings['authsource'] == 'ldap') {
+      $result = ldap_mbox_login($user, $pass, array('is_internal' => $is_internal, 'create' => true));
+    }
+    if ($result !== false){
+      // double check if mailbox is active
+      $stmt = $pdo->prepare("SELECT * FROM `mailbox`
+      INNER JOIN domain on mailbox.domain = domain.domain
+      WHERE `kind` NOT REGEXP 'location|thing|group'
+        AND `mailbox`.`active`='1'
+        AND `domain`.`active`='1'
+        AND `username` = :user");
+      $stmt->execute(array(':user' => $user));
+      $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+      if (!empty($row)) {
+        return true;
+      }
+    }
+    clear_session();
+    return false;
+  }
+
+  switch ($row['authsource']) {
+    case 'keycloak':
+      // user authsource is keycloak, try using via rest flow
+      if (intval($iam_settings['mailpassword_flow']) == 1){
+        $result = keycloak_mbox_login_rest($user, $pass, array('is_internal' => $is_internal));
+        if ($result !== false) {
+          // double check if mailbox and domain is active
+          $stmt = $pdo->prepare("SELECT * FROM `mailbox`
+          INNER JOIN domain on mailbox.domain = domain.domain
+          WHERE `kind` NOT REGEXP 'location|thing|group'
+            AND `mailbox`.`active`='1'
+            AND `domain`.`active`='1'
+            AND `username` = :user");
+          $stmt->execute(array(':user' => $user));
+          $row = $stmt->fetch(PDO::FETCH_ASSOC);
+          if (empty($row)) {
+            return false;
+          }
+
+          // check for tfa authenticators
+          $authenticators = get_tfa($user);
+          if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
+            // authenticators found, init TFA flow
+            $_SESSION['pending_mailcow_cc_username'] = $user;
+            $_SESSION['pending_mailcow_cc_role'] = "user";
+            $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
+            unset($_SESSION['ldelay']);
+            $_SESSION['return'][] =  array(
+              'type' => 'success',
+              'log' => array(__FUNCTION__, $user, '*', 'Provider: Keycloak'),
+              'msg' => array('logged_in_as', $user)
+            );
+            return "pending";
+          } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
+            // no authenticators found, login successfull
+            if (!$is_internal){
+              unset($_SESSION['ldelay']);
+              // Reactivate TFA if it was set to "deactivate TFA for next login"
+              $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
+              $stmt->execute(array(':user' => $user));
+              $_SESSION['return'][] =  array(
+                'type' => 'success',
+                'log' => array(__FUNCTION__, $user, '*', 'Provider: Keycloak'),
+                'msg' => array('logged_in_as', $user)
+              );
+            }
+            return "user";
+          }
+        }
+        return $result;
+      } else {
+        return false;
+      }
+    break;
+    case 'ldap':
+      // user authsource is ldap
+      $result = ldap_mbox_login($user, $pass, array('is_internal' => $is_internal));
+      if ($result !== false) {
+        // double check if mailbox and domain is active
+        $stmt = $pdo->prepare("SELECT * FROM `mailbox`
+        INNER JOIN domain on mailbox.domain = domain.domain
+        WHERE `kind` NOT REGEXP 'location|thing|group'
+          AND `mailbox`.`active`='1'
+          AND `domain`.`active`='1'
+          AND `username` = :user");
+        $stmt->execute(array(':user' => $user));
+        $row = $stmt->fetch(PDO::FETCH_ASSOC);
+        if (empty($row)) {
+          return false;
+        }
+
+        // check for tfa authenticators
+        $authenticators = get_tfa($user);
+        if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
+          // authenticators found, init TFA flow
+          $_SESSION['pending_mailcow_cc_username'] = $user;
+          $_SESSION['pending_mailcow_cc_role'] = "user";
+          $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
+          unset($_SESSION['ldelay']);
+          $_SESSION['return'][] =  array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $user, '*', 'Provider: LDAP'),
+            'msg' => array('logged_in_as', $user)
+          );
+          return "pending";
+        } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
+          // no authenticators found, login successfull
+          if (!$is_internal){
+            unset($_SESSION['ldelay']);
+            // Reactivate TFA if it was set to "deactivate TFA for next login"
+            $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
+            $stmt->execute(array(':user' => $user));
+            $_SESSION['return'][] =  array(
+              'type' => 'success',
+              'log' => array(__FUNCTION__, $user, '*', 'Provider: LDAP'),
+              'msg' => array('logged_in_as', $user)
+            );
+          }
+          return "user";
+        }
+      }
+      return $result;
+    break;
+    case 'mailcow':
+      if ($row['active'] != 1 || $row['d_active'] != 1) {
+        return false;
+      }
+      // verify password
+      if (verify_hash($row['password'], $pass) !== false) {
+        // check for tfa authenticators
+        $authenticators = get_tfa($user);
+        if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
+          // authenticators found, init TFA flow
+          $_SESSION['pending_mailcow_cc_username'] = $user;
+          $_SESSION['pending_mailcow_cc_role'] = "user";
+          $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
+          unset($_SESSION['ldelay']);
+          $_SESSION['return'][] =  array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $user, '*', 'Provider: mailcow'),
+            'msg' => array('logged_in_as', $user)
+          );
+          return "pending";
+        } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
+          // no authenticators found, login successfull
+          if (!$is_internal){
+            unset($_SESSION['ldelay']);
+            // Reactivate TFA if it was set to "deactivate TFA for next login"
+            $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
+            $stmt->execute(array(':user' => $user));
+            $_SESSION['return'][] =  array(
+              'type' => 'success',
+              'log' => array(__FUNCTION__, $user, '*', 'Provider: mailcow'),
+              'msg' => array('logged_in_as', $user)
+            );
+          }
+          return "user";
+        }
+      }
+    break;
+  }
+
+  return false;
+}
+function apppass_login($user, $pass, $app_passwd_data, $extra = null){
+  global $pdo;
+
+  $is_internal = $extra['is_internal'];
+
+  if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
+    if (!$is_internal){
+      $_SESSION['return'][] =  array(
+        'type' => 'danger',
+        'log' => array(__FUNCTION__, $user, '*'),
+        'msg' => 'malformed_username'
+      );
+    }
+    return false;
+  }
+
+  $protocol = false;
+  if ($app_passwd_data['eas']){
+    $protocol = 'eas';
+  } else if ($app_passwd_data['dav']){
+    $protocol = 'dav';
+  } else if ($app_passwd_data['smtp']){
+    $protocol = 'smtp';
+  } else if ($app_passwd_data['imap']){
+    $protocol = 'imap';
+  } else if ($app_passwd_data['sieve']){
+    $protocol = 'sieve';
+  } else if ($app_passwd_data['pop3']){
+    $protocol = 'pop3';
+  } else if (!$is_internal) {
+    return false;
+  }
+
+  // fetch app password data
+  $stmt = $pdo->prepare("SELECT `app_passwd`.*, `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
+    INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
+    INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
+    WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
+      AND `mailbox`.`active` = '1'
+      AND `domain`.`active` = '1'
+      AND `app_passwd`.`active` = '1'
+      AND `app_passwd`.`mailbox` = :user"
+  );
+  // fetch password data
+  $stmt->execute(array(
+    ':user' => $user,
+  ));
+  $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+  foreach ($rows as $row) {
+    if ($protocol && $row[$protocol . '_access'] != '1'){
+      continue;
+    }
+
+    // verify password
+    if (verify_hash($row['password'], $pass) !== false) {
+      if ($is_internal){
+        $remote_addr = $extra['remote_addr'];
+      } else {
+        $remote_addr = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
+      }
+
+      $service = strtoupper($is_app_passwd);
+      $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)");
+      $stmt->execute(array(
+        ':service' => $service,
+        ':app_id' => $row['app_passwd_id'],
+        ':username' => $user,
+        ':remote_addr' => $remote_addr
+      ));
+
+      unset($_SESSION['ldelay']);
+      return "user";
+    }
+  }
+
+  return false;
+}
+// Keycloak REST Api Flow - auth user by mailcow_password attribute
+// This password will be used for direct UI, IMAP and SMTP Auth
+// To use direct user credentials, only Authorization Code Flow is valid
+function keycloak_mbox_login_rest($user, $pass, $extra = null){
+  global $pdo;
+  global $iam_provider;
+  global $iam_settings;
+
+  $is_internal = $extra['is_internal'];
+  $create = $extra['create'];
+
+  if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
+    if (!$is_internal){
+      $_SESSION['return'][] =  array(
+        'type' => 'danger',
+        'log' => array(__FUNCTION__, $user, '*'),
+        'msg' => 'malformed_username'
+      );
+    }
+    return false;
+  }
+
+  // get access_token for service account of mailcow client
+  $admin_token = identity_provider("get-keycloak-admin-token");
+
+  // get the mailcow_password attribute from keycloak user
+  $url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users";
+  $queryParams = array('email' => $user, 'exact' => true);
+  $queryString = http_build_query($queryParams);
+  $curl = curl_init();
+  curl_setopt($curl, CURLOPT_TIMEOUT, 7);
+  curl_setopt($curl, CURLOPT_URL, $url . '?' . $queryString);
+  curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
+  curl_setopt($curl, CURLOPT_HTTPHEADER, array(
+      'Authorization: Bearer ' . $admin_token,
+      'Content-Type: application/json'
+  ));
+  $user_res = json_decode(curl_exec($curl), true)[0];
+  $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
+  curl_close($curl);
+  if ($code != 200) {
+    $_SESSION['return'][] =  array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $user, '*', 'Identity Provider returned HTTP ' . $code),
+      'msg' => 'generic_server_error'
+    );
+    return false;
+  }
+  if (!isset($user_res['attributes']['mailcow_password']) || !is_array($user_res['attributes']['mailcow_password'])){
+    $_SESSION['return'][] =  array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $user, '*', 'User has no mailcow_password attribute'),
+      'msg' => 'generic_server_error'
+    );
+    return false;
+  }
+  if (empty($user_res['attributes']['mailcow_password'][0])){
+    $_SESSION['return'][] =  array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $user, '*', "User's mailcow_password attribute is empty"),
+      'msg' => 'generic_server_error'
+    );
+    return false;
+  }
+
+  // validate mailcow_password
+  $mailcow_password = $user_res['attributes']['mailcow_password'][0];
+  if (!verify_hash($mailcow_password, $pass)) {
+    return false;
+  }
+
+  // get mapped template
+  $user_template = $user_res['attributes']['mailcow_template'][0];
+  $mapper_key = array_search($user_template, $iam_settings['mappers']);
+
+  if (!$create) {
+    // login success
+    if ($mapper_key !== false) {
+      // update user
+      $_SESSION['access_all_exception'] = '1';
+      mailbox('edit', 'mailbox_from_template', array(
+        'username' => $user,
+        'name' => $user_res['name'],
+        'template' => $iam_settings['templates'][$mapper_key]
+      ));
+      $_SESSION['access_all_exception'] = '0';
+    }
+    return 'user';
+  }
+
+  // check if matching attribute exist
+  if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) {
+    if (!empty($iam_settings['default_template'])) {
+      $mbox_template = $iam_settings['default_template'];
+    } else {
+      $_SESSION['return'][] =  array(
+        'type' => 'danger',
+        'log' => array(__FUNCTION__, $user, '*', 'No matching attribute mapping was found'),
+        'msg' => 'generic_server_error'
+      );
+      return false;
+    }
+  } else {
+    $mbox_template = $iam_settings['templates'][$mapper_key];
+  }
+
+  // create mailbox
+  $_SESSION['access_all_exception'] = '1';
+  $create_res = mailbox('add', 'mailbox_from_template', array(
+    'domain' => explode('@', $user)[1],
+    'local_part' => explode('@', $user)[0],
+    'name' => $user_res['name'],
+    'authsource' => 'keycloak',
+    'template' => $mbox_template
+  ));
+  $_SESSION['access_all_exception'] = '0';
+  if (!$create_res){
+    clear_session();
+    $_SESSION['return'][] =  array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $user, '*', 'Could not create mailbox on login'),
+      'msg' => 'generic_server_error'
+    );
+    return false;
+  }
+
+  return 'user';
+}
+function ldap_mbox_login($user, $pass, $extra = null){
+  global $pdo;
+  global $iam_provider;
+  global $iam_settings;
+
+  $is_internal = $extra['is_internal'];
+  $create = $extra['create'];
+
+  if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
+    if (!$is_internal){
+      $_SESSION['return'][] =  array(
+        'type' => 'danger',
+        'log' => array(__FUNCTION__, $user, '*'),
+        'msg' => 'malformed_username'
+      );
+    }
+    return false;
+  }
+  if (!$iam_provider) {
+    return false;
+  }
+
+  try {
+    $ldap_query = $iam_provider->query();
+    if (!empty($iam_settings['filter'])) {
+      $ldap_query = $ldap_query->rawFilter($iam_settings['filter']);
+    }
+    $ldap_query = $ldap_query->where($iam_settings['username_field'], '=', $user)
+      ->select([$iam_settings['username_field'], $iam_settings['attribute_field'], 'displayname', 'distinguishedname', 'dn']);
+
+    $user_res = $ldap_query->firstOrFail();
+  } catch (Exception $e) {
+    // clear $_SESSION['return'] to not leak data
+    $_SESSION['return'] = array();
+    $_SESSION['return'][] =  array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
+      'msg' => 'generic_server_error'
+    );
+    return false;
+  }
+  try {
+    if (!$iam_provider->auth()->attempt($user_res['dn'], $pass)) {
+      return false;
+    }
+  } catch (Exception $e) {
+    // clear $_SESSION['return'] to not leak data
+    $_SESSION['return'] = array();
+    $_SESSION['return'][] =  array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
+      'msg' => 'generic_server_error'
+    );
+    return false;
+  }
+
+  // get mapped template
+  $user_template = $user_res[$iam_settings['attribute_field']][0];
+  $mapper_key = array_search($user_template, $iam_settings['mappers']);
+
+  if (!$create) {
+    // login success
+    if ($mapper_key !== false) {
+      // update user
+      $_SESSION['access_all_exception'] = '1';
+      mailbox('edit', 'mailbox_from_template', array(
+        'username' => $user,
+        'name' => $user_res['displayname'][0],
+        'template' => $iam_settings['templates'][$mapper_key]
+      ));
+      $_SESSION['access_all_exception'] = '0';
+    }
+    return 'user';
+  }
+
+  // check if matching attribute exist
+  if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) {
+    if (!empty($iam_settings['default_tempalte'])) {
+      $mbox_template = $iam_settings['default_tempalte'];
+    } else {
+      $_SESSION['return'][] =  array(
+        'type' => 'danger',
+        'log' => array(__FUNCTION__, $user, '*', 'No matching attribute mapping was found'),
+        'msg' => 'generic_server_error'
+      );
+      return false;
+    }
+  } else {
+    $mbox_template = $iam_settings['templates'][$mapper_key];
+  }
+
+  // create mailbox
+  $_SESSION['access_all_exception'] = '1';
+  $create_res = mailbox('add', 'mailbox_from_template', array(
+    'domain' => explode('@', $user)[1],
+    'local_part' => explode('@', $user)[0],
+    'name' => $user_res['displayname'][0],
+    'authsource' => 'ldap',
+    'template' => $mbox_template
+  ));
+  $_SESSION['access_all_exception'] = '0';
+  if (!$create_res){
+    clear_session();
+    $_SESSION['return'][] =  array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $user, '*', 'Could not create mailbox on login'),
+      'msg' => 'generic_server_error'
+    );
+    return false;
+  }
+
+  return 'user';
+}

+ 26 - 4
data/web/inc/functions.customize.inc.php

@@ -3,7 +3,7 @@ function customize($_action, $_item, $_data = null) {
 	global $redis;
 	global $lang;
   global $LOGO_LIMITS;
-  
+
   switch ($_action) {
     case 'add':
       // disable functionality when demo mode is enabled
@@ -122,10 +122,16 @@ function customize($_action, $_item, $_data = null) {
         case 'app_links':
           $apps = (array)$_data['app'];
           $links = (array)$_data['href'];
+          $user_links = (array)$_data['user_href'];
+          $hide = (array)$_data['hide'];
           $out = array();
-          if (count($apps) == count($links)) {
+          if (count($apps) == count($links) && count($apps) == count($user_links) && count($apps) == count($hide)) {
             for ($i = 0; $i < count($apps); $i++) {
-              $out[] = array($apps[$i] => $links[$i]);
+              $out[] = array($apps[$i] => array(
+                'link' => $links[$i],
+                'user_link' => $user_links[$i],
+                'hide' => ($hide[$i] === '0' || $hide[$i] === 0) ? false : true
+              ));
             }
             try {
               $redis->set('APP_LINKS', json_encode($out));
@@ -256,7 +262,23 @@ function customize($_action, $_item, $_data = null) {
             );
             return false;
           }
-          return ($app_links) ? $app_links : false;
+
+          if (empty($app_links)){
+            return false;
+          }
+
+          // convert from old style
+          foreach($app_links as $i => $entry){
+            foreach($entry as $app => $link){
+              if (empty($link['link']) && empty($link['user_link'])){
+                $app_links[$i][$app] = array();
+                $app_links[$i][$app]['link'] = $link;
+                $app_links[$i][$app]['user_link'] = $link;
+              }
+            }
+          }
+
+          return $app_links;
         break;
         case 'main_logo':
         case 'main_logo_dark':

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

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

ファイルの差分が大きいため隠しています
+ 804 - 228
data/web/inc/functions.inc.php


+ 247 - 64
data/web/inc/functions.mailbox.inc.php

@@ -4,6 +4,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
   global $redis;
   global $lang;
   global $MAILBOX_DEFAULT_ATTRIBUTES;
+  global $iam_settings;
+
   $_data_log = $_data;
   !isset($_data_log['password']) ?: $_data_log['password'] = '*';
   !isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
@@ -1005,6 +1007,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $local_part   = strtolower(trim($_data['local_part']));
           $domain       = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
           $username     = $local_part . '@' . $domain;
+          $authsource   = 'mailcow';
           if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
@@ -1021,15 +1024,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             return false;
           }
+          if ($_data['authsource'] == "mailcow" ||
+              in_array($_data['authsource'], array('keycloak', 'generic-oidc', 'ldap')) && $iam_settings['authsource'] == $_data['authsource']){
+            $authsource = $_data['authsource'];
+          }
           if (empty($name)) {
             $name = $local_part;
           }
           $template_attr = null;
           if ($_data['template']){
-            $template_attr = mailbox('get', 'mailbox_templates', $_data['template'])['attributes'];
+            $template_attr = mailbox('get', 'mailbox_templates', $_data['template'], $_extra)['attributes'];
           }
           if (empty($template_attr)) {
-            $template_attr = mailbox('get', 'mailbox_templates')[0]['attributes'];
+            $template_attr = mailbox('get', 'mailbox_templates', null, $_extra)[0]['attributes'];
           }
           $MAILBOX_DEFAULT_ATTRIBUTES = array_merge($MAILBOX_DEFAULT_ATTRIBUTES, $template_attr);
 
@@ -1038,7 +1045,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $name         = ltrim(rtrim($_data['name'], '>'), '<');
           $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) {
+          if ($authsource != 'mailcow'){
+            $password = '';
+            $password2 = '';
+            $password_hashed = '';
+          }
+          if (!hasACLAccess("unlimited_quota") && $quota_m === 0) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1067,6 +1079,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
           $quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
           $quota_b    = ($quota_m * 1048576);
+          $attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
+          if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
+            $force_pw_update = 0;
+          }
           $mailbox_attrs = json_encode(
             array(
               'force_pw_update' => strval($force_pw_update),
@@ -1081,7 +1097,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               'passwd_update' => time(),
               'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']),
               'quarantine_notification' => strval($quarantine_notification),
-              'quarantine_category' => strval($quarantine_category)
+              'quarantine_category' => strval($quarantine_category),
+              'attribute_hash' => $attribute_hash
             )
           );
           if (!is_valid_domain_name($domain)) {
@@ -1156,10 +1173,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             return false;
           }
-          if (password_check($password, $password2) !== true) {
-            return false;
+          if ($authsource == 'mailcow'){
+            if (password_check($password, $password2) !== true) {
+              return false;
+            }
+            $password_hashed = hash_password($password);
           }
-          $password_hashed = hash_password($password);
           if ($MailboxData['count'] >= $DomainData['mailboxes']) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
@@ -1185,8 +1204,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             );
             return false;
           }
-          $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `quota`, `local_part`, `domain`, `attributes`, `active`)
-            VALUES (:username, :password_hashed, :name, :quota_b, :local_part, :domain, :mailbox_attrs, :active)");
+          $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `quota`, `local_part`, `domain`, `attributes`, `authsource`, `active`)
+            VALUES (:username, :password_hashed, :name, :quota_b, :local_part, :domain, :mailbox_attrs, :authsource, :active)");
           $stmt->execute(array(
             ':username' => $username,
             ':password_hashed' => $password_hashed,
@@ -1195,6 +1214,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             ':local_part' => $local_part,
             ':domain' => $domain,
             ':mailbox_attrs' => $mailbox_attrs,
+            ':authsource' => $authsource,
             ':active' => $active
           ));
           $stmt = $pdo->prepare("UPDATE `mailbox` SET
@@ -1214,11 +1234,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               break;
             }
-            $stmt = $pdo->prepare("INSERT INTO `tags_mailbox` (`username`, `tag_name`) VALUES (:username, :tag_name)");
-            $stmt->execute(array(
-              ':username' => $username,
-              ':tag_name' => $tag,
-            ));
+            try {
+              $stmt = $pdo->prepare("INSERT INTO `tags_mailbox` (`username`, `tag_name`) VALUES (:username, :tag_name)");
+              $stmt->execute(array(
+                ':username' => $username,
+                ':tag_name' => $tag,
+              ));
+            } catch (Exception $e) {
+            }
           }
           $stmt = $pdo->prepare("INSERT INTO `quota2` (`username`, `bytes`, `messages`)
             VALUES (:username, '0', '0') ON DUPLICATE KEY UPDATE `bytes` = '0', `messages` = '0';");
@@ -1312,16 +1335,62 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               'object' => $username,
               'rl_frame' => $_data['rl_frame'],
               'rl_value' => $_data['rl_value']
-            ));
+            ), $_extra);
           }
 
-          update_sogo_static_view($username);
+          try {
+            update_sogo_static_view($username);
+          } catch (PDOException $e) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => $e->getMessage()
+            );
+          }
           $_SESSION['return'][] = array(
             'type' => 'success',
             'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
             'msg' => array('mailbox_added', htmlspecialchars($username))
           );
-          return true;
+        break;
+        case 'mailbox_from_template':
+          $stmt = $pdo->prepare("SELECT * FROM `templates`
+          WHERE `template` = :template AND type = 'mailbox'");
+          $stmt->execute(array(
+            ":template" => $_data['template']
+          ));
+          $mbox_template_data = $stmt->fetch(PDO::FETCH_ASSOC);
+          if (empty($mbox_template_data)){
+            $_SESSION['return'][] =  array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => 'template_missing'
+            );
+            return false;
+          }
+
+          $attribute_hash = sha1(json_encode($mbox_template_data["attributes"]));
+          $mbox_template_data = json_decode($mbox_template_data["attributes"], true);
+          $mbox_template_data['domain'] = $_data['domain'];
+          $mbox_template_data['name'] = $_data['name'];
+          $mbox_template_data['local_part'] = $_data['local_part'];
+          $mbox_template_data['authsource'] = $_data['authsource'];
+          $mbox_template_data['attribute_hash'] = $attribute_hash;
+          $mbox_template_data['quota'] = intval($mbox_template_data['quota'] / 1048576);
+
+          $mailbox_attributes = array('acl' => array());
+          foreach ($mbox_template_data as $key => $value){
+            switch (true) {
+              case (strpos($key, 'acl_') === 0 && $value != 0):
+                array_push($mailbox_attributes['acl'], str_replace('acl_' , '', $key));
+              break;
+              default:
+                $mailbox_attributes[$key] = $value;
+              break;
+            }
+          }
+
+          return mailbox('add', 'mailbox', $mailbox_attributes);
         break;
         case 'resource':
           $domain             = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
@@ -1689,7 +1758,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           else {
             $usernames = $_data['username'];
           }
-          if (!isset($_SESSION['acl']['tls_policy']) || $_SESSION['acl']['tls_policy'] != "1" ) {
+          if (!hasACLAccess("tls_policy")) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1698,7 +1767,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             return false;
           }
           foreach ($usernames as $username) {
-            if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+            if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1706,7 +1775,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            $is_now = mailbox('get', 'tls_policy', $username);
+            $is_now = mailbox('get', 'tls_policy', $username, $_extra);
             if (!empty($is_now)) {
               $tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in'];
               $tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : $is_now['tls_enforce_out'];
@@ -1743,7 +1812,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           else {
             $usernames = $_data['username'];
           }
-          if (!isset($_SESSION['acl']['quarantine_notification']) || $_SESSION['acl']['quarantine_notification'] != "1" ) {
+          if (!hasACLAccess("quarantine_notification")) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1752,7 +1821,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             return false;
           }
           foreach ($usernames as $username) {
-            if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+            if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1760,7 +1829,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            $is_now = mailbox('get', 'quarantine_notification', $username);
+            $is_now = mailbox('get', 'quarantine_notification', $username, $_extra);
             if (!empty($is_now)) {
               $quarantine_notification = (isset($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification'];
             }
@@ -1802,7 +1871,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           else {
             $usernames = $_data['username'];
           }
-          if (!isset($_SESSION['acl']['quarantine_category']) || $_SESSION['acl']['quarantine_category'] != "1" ) {
+          if (!hasACLAccess("quarantine_category")) {
             $_SESSION['return'][] = array(
               'type' => 'danger',
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1811,7 +1880,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             return false;
           }
           foreach ($usernames as $username) {
-            if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
+            if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1819,7 +1888,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            $is_now = mailbox('get', 'quarantine_category', $username);
+            $is_now = mailbox('get', 'quarantine_category', $username, $_extra);
             if (!empty($is_now)) {
               $quarantine_category = (isset($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category'];
             }
@@ -2863,7 +2932,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            $is_now = mailbox('get', 'mailbox_details', $username);
+            $is_now = mailbox('get', 'mailbox_details', $username, $_extra);
             if (isset($_data['protocol_access'])) {
               $_data['protocol_access'] = (array)$_data['protocol_access'];
               $_data['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
@@ -2874,20 +2943,29 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             if (!empty($is_now)) {
               $active               = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
               (int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
-              (int)$sogo_access     = (isset($_data['sogo_access']) && isset($_SESSION['acl']['sogo_access']) && $_SESSION['acl']['sogo_access'] == "1") ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
-              (int)$imap_access     = (isset($_data['imap_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
-              (int)$pop3_access     = (isset($_data['pop3_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
-              (int)$smtp_access     = (isset($_data['smtp_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
-              (int)$sieve_access    = (isset($_data['sieve_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
-              (int)$relayhost       = (isset($_data['relayhost']) && isset($_SESSION['acl']['mailbox_relayhost']) && $_SESSION['acl']['mailbox_relayhost'] == "1") ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
+              (int)$sogo_access     = (isset($_data['sogo_access']) && hasACLAccess("sogo_access")) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
+              (int)$imap_access     = (isset($_data['imap_access']) && hasACLAccess("protocol_access")) ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
+              (int)$pop3_access     = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
+              (int)$smtp_access     = (isset($_data['smtp_access']) && hasACLAccess("protocol_access")) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
+              (int)$sieve_access    = (isset($_data['sieve_access']) && hasACLAccess("protocol_access")) ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
+              (int)$relayhost       = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
               (int)$quota_m         = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
               $name                 = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
               $domain               = $is_now['domain'];
               $quota_b              = $quota_m * 1048576;
               $password             = (!empty($_data['password'])) ? $_data['password'] : null;
               $password2            = (!empty($_data['password2'])) ? $_data['password2'] : null;
-              $pw_recovery_email     = (isset($_data['pw_recovery_email'])) ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
               $tags                 = (is_array($_data['tags']) ? $_data['tags'] : array());
+              $attribute_hash       = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
+              $authsource           = $is_now['authsource'];
+              if ($_data['authsource'] == "mailcow" ||
+                  in_array($_data['authsource'], array('keycloak', 'generic-oidc', 'ldap')) && $iam_settings['authsource'] == $_data['authsource']){
+                $authsource = $_data['authsource'];
+              }
+              if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
+                $force_pw_update = 0;
+              }
+              $pw_recovery_email    = (isset($_data['pw_recovery_email']) && $authsource == 'mailcow') ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
             }
             else {
               $_SESSION['return'][] = array(
@@ -2898,7 +2976,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               continue;
             }
             // if already 0 == ok
-            if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && ($quota_m == 0 && $is_now['quota'] != 0)) {
+            if (!hasACLAccess("unlimited_quota") && ($quota_m == 0 && $is_now['quota'] != 0)) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -2914,7 +2992,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            $DomainData = mailbox('get', 'domain_details', $domain);
+            $DomainData = mailbox('get', 'domain_details', $domain, $_extra);
             if ($quota_m > ($is_now['max_new_quota'] / 1048576)) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
@@ -2933,7 +3011,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             }
             $extra_acls = array();
             if (isset($_data['extended_sender_acl'])) {
-              if (!isset($_SESSION['acl']['extend_sender_acl']) || $_SESSION['acl']['extend_sender_acl'] != "1" ) {
+              if (!hasACLAccess("extend_sender_acl")) {
                 $_SESSION['return'][] = array(
                   'type' => 'danger',
                   'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -3126,7 +3204,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               $stmt = $pdo->prepare("UPDATE `mailbox` SET
                   `password` = :password_hashed,
                   `attributes` = JSON_SET(`attributes`, '$.passwd_update', NOW())
-                    WHERE `username` = :username");
+                    WHERE `username` = :username AND authsource = 'mailcow'");
               $stmt->execute(array(
                 ':password_hashed' => $password_hashed,
                 ':username' => $username
@@ -3145,6 +3223,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                   `active` = :active,
                   `name`= :name,
                   `quota` = :quota_b,
+                  `authsource` = :authsource,
                   `attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update),
                   `attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
                   `attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
@@ -3152,22 +3231,25 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                   `attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
                   `attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
                   `attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
-                  `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email)
+                  `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email),
+                  `attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash)
                     WHERE `username` = :username");
-                $stmt->execute(array(
-                  ':active' => $active,
-                  ':name' => $name,
-                  ':quota_b' => $quota_b,
-                  ':force_pw_update' => $force_pw_update,
-                  ':sogo_access' => $sogo_access,
-                  ':imap_access' => $imap_access,
-                  ':pop3_access' => $pop3_access,
-                  ':sieve_access' => $sieve_access,
-                  ':smtp_access' => $smtp_access,
-                  ':recovery_email' => $pw_recovery_email,
-                  ':relayhost' => $relayhost,
-                  ':username' => $username
-                ));
+              $stmt->execute(array(
+                ':active' => $active,
+                ':name' => $name,
+                ':quota_b' => $quota_b,
+                ':attribute_hash' => $attribute_hash,
+                ':force_pw_update' => $force_pw_update,
+                ':sogo_access' => $sogo_access,
+                ':imap_access' => $imap_access,
+                ':pop3_access' => $pop3_access,
+                ':sieve_access' => $sieve_access,
+                ':smtp_access' => $smtp_access,
+                ':recovery_email' => $pw_recovery_email,
+                ':relayhost' => $relayhost,
+                ':username' => $username,
+                ':authsource' => $authsource
+              ));
             }
             catch (PDOException $e) {
               $_SESSION['return'][] = array(
@@ -3188,11 +3270,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
                 );
                 break;
               }
-              $stmt = $pdo->prepare("INSERT INTO `tags_mailbox` (`username`, `tag_name`) VALUES (:username, :tag_name)");
-              $stmt->execute(array(
-                ':username' => $username,
-                ':tag_name' => $tag,
-              ));
+              try {
+                $stmt = $pdo->prepare("INSERT INTO `tags_mailbox` (`username`, `tag_name`) VALUES (:username, :tag_name)");
+                $stmt->execute(array(
+                  ':username' => $username,
+                  ':tag_name' => $tag,
+                ));
+              } catch (Exception $e) {
+              }
             }
 
             $_SESSION['return'][] = array(
@@ -3201,7 +3286,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               'msg' => array('mailbox_modified', $username)
             );
 
-            update_sogo_static_view($username);
+            try {
+              update_sogo_static_view($username);
+            } catch (PDOException $e) {
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                'msg' => $e->getMessage()
+              );
+            }
           }
           return true;
         break;
@@ -3401,6 +3494,75 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             'msg' => array('mailbox_renamed', $old_username, $new_username)
           );
         break;
+        case 'mailbox_from_template':
+          $stmt = $pdo->prepare("SELECT * FROM `templates`
+          WHERE `template` = :template AND type = 'mailbox'");
+          $stmt->execute(array(
+            ":template" => $_data['template']
+          ));
+          $mbox_template_data = $stmt->fetch(PDO::FETCH_ASSOC);
+          if (empty($mbox_template_data)){
+            $_SESSION['return'][] =  array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+              'msg' => 'template_missing'
+            );
+            return false;
+          }
+
+          $attribute_hash = sha1(json_encode($mbox_template_data["attributes"]));
+          $is_now = mailbox('get', 'mailbox_details', $_data['username']);
+          $name = ltrim(rtrim($_data['name'], '>'), '<');
+          if ($is_now['attributes']['attribute_hash'] == $attribute_hash && $is_now['name'] == $name)
+            return true;
+
+          $mbox_template_data = json_decode($mbox_template_data["attributes"], true);
+          $mbox_template_data['attribute_hash'] = $attribute_hash;
+          $mbox_template_data['name'] = $name;
+          $quarantine_attributes = array('username' => $_data['username']);
+          $tls_attributes = array('username' => $_data['username']);
+          $ratelimit_attributes = array('object' => $_data['username']);
+          $acl_attributes = array('username' => $_data['username'], 'user_acl' => array());
+          $mailbox_attributes = array('username' => $_data['username']);
+          foreach ($mbox_template_data as $key => $value){
+            switch (true) {
+              case (strpos($key, 'quarantine_') === 0):
+                $quarantine_attributes[$key] = $value;
+              break;
+              case (strpos($key, 'tls_') === 0):
+                if ($value == null)
+                  $value = 0;
+                $tls_attributes[$key] = $value;
+              break;
+              case (strpos($key, 'rl_') === 0):
+                $ratelimit_attributes[$key] = $value;
+              break;
+              case (strpos($key, 'acl_') === 0 && $value != 0):
+                array_push($acl_attributes['user_acl'], str_replace('acl_' , '', $key));
+              break;
+              default:
+                $mailbox_attributes[$key] = $value;
+              break;
+            }
+          }
+
+          $mailbox_attributes['quota'] = intval($mailbox_attributes['quota'] / 1048576);
+          $result = mailbox('edit', 'mailbox', $mailbox_attributes);
+          if ($result === false) return $result;
+          $result = mailbox('edit', 'tls_policy', $tls_attributes);
+          if ($result === false) return $result;
+          $result = mailbox('edit', 'quarantine_notification', $quarantine_attributes);
+          if ($result === false) return $result;
+          $result = mailbox('edit', 'quarantine_category', $quarantine_attributes);
+          if ($result === false) return $result;
+          $result = ratelimit('edit', 'mailbox', $ratelimit_attributes);
+          if ($result === false) return $result;
+          $result = acl('edit', 'user', $acl_attributes);
+          if ($result === false) return $result;
+
+          $_SESSION['return'] = array();
+          return true;
+        break;
         case 'mailbox_templates':
           if ($_SESSION['mailcow_cc_role'] != "admin") {
             $_SESSION['return'][] = array(
@@ -4666,6 +4828,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               `mailbox`.`quota`,
               `mailbox`.`created`,
               `mailbox`.`modified`,
+              `mailbox`.`authsource`,
               `quota2`.`bytes`,
               `attributes`,
               `custom_attributes`,
@@ -4687,6 +4850,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               `mailbox`.`quota`,
               `mailbox`.`created`,
               `mailbox`.`modified`,
+              `mailbox`.`authsource`,
               `quota2replica`.`bytes`,
               `attributes`,
               `custom_attributes`,
@@ -4716,6 +4880,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
           $mailboxdata['created'] = $row['created'];
           $mailboxdata['modified'] = $row['modified'];
+          $mailboxdata['authsource'] = ($row['authsource']) ? $row['authsource'] : 'mailcow';
 
           if ($mailboxdata['percent_in_use'] === '- ') {
             $mailboxdata['percent_class'] = "info";
@@ -4746,7 +4911,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
             else if ($SaslLogs['service'] == 'pop3') {
               $last_pop3_login = strtotime($SaslLogs['datetime']);
             }
-			else if ($SaslLogs['service'] == 'SSO') {
+			      else if ($SaslLogs['service'] == 'SSO') {
               $last_sso_login = strtotime($SaslLogs['datetime']);
             }
           }
@@ -4759,7 +4924,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           if (!isset($last_pop3_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
             $last_pop3_login = 0;
           }
-		  if (!isset($last_sso_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
+		      if (!isset($last_sso_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
             $last_sso_login = 0;
           }
           $mailboxdata['last_imap_login'] = $last_imap_login;
@@ -4811,7 +4976,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           return $mailboxdata;
         break;
         case 'mailbox_templates':
-          if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
+          if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin" && $_SESSION['access_all_exception'] != "1") {
             return false;
           }
           $_data = (isset($_data)) ? intval($_data) : null;
@@ -5565,7 +5730,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               continue;
             }
 
-            update_sogo_static_view($username);
+            try {
+              update_sogo_static_view($username);
+            }catch (PDOException $e) {
+              $_SESSION['return'][] = array(
+                'type' => 'success',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                'msg' => $e->getMessage()
+              );
+            }
             $_SESSION['return'][] = array(
               'type' => 'success',
               'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -5779,6 +5952,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
     break;
   }
   if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'resource')) && getenv('SKIP_SOGO') != "y") {
-    update_sogo_static_view();
+    try {
+      update_sogo_static_view();
+    }catch (PDOException $e) {
+      $_SESSION['return'][] = array(
+        'type' => 'success',
+        'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+        'msg' => $e->getMessage()
+      );
+    }
   }
+
+  return true;
 }

+ 3 - 3
data/web/inc/functions.ratelimit.inc.php

@@ -1,10 +1,10 @@
 <?php
-function ratelimit($_action, $_scope, $_data = null) {
+function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
   global $redis;
   $_data_log = $_data;
   switch ($_action) {
     case 'edit':
-      if (!isset($_SESSION['acl']['ratelimit']) || $_SESSION['acl']['ratelimit'] != "1" ) {
+      if (!hasACLAccess("ratelimit")) {
         $_SESSION['return'][] = array(
           'type' => 'danger',
           'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -93,7 +93,7 @@ function ratelimit($_action, $_scope, $_data = null) {
               continue;
             }
             if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)
-              || ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin')) {
+                || ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin' && $_SESSION['access_all_exception'] != '1')) {
               $_SESSION['return'][] = array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),

+ 38 - 1
data/web/inc/header.inc.php

@@ -30,6 +30,40 @@ if(!file_exists($CSSPath)) {
   cleanupCSS($hash);
 }
 
+$mailcow_apps_processed = $MAILCOW_APPS;
+$app_links = customize('get', 'app_links');
+$app_links_processed = $app_links;
+$hide_mailcow_apps = true;
+for ($i = 0; $i < count($mailcow_apps_processed); $i++) {
+  if ($hide_mailcow_apps && !$mailcow_apps_processed[$i]['hide']){
+    $hide_mailcow_apps = false;
+  }
+  if (!empty($_SESSION['mailcow_cc_username'])){
+    if ($app_links_processed[$i]['user_link']) {
+      $mailcow_apps_processed[$i]['user_link'] = str_replace('%u', $_SESSION['mailcow_cc_username'], $mailcow_apps_processed[$i]['user_link']);
+    } else {
+      $mailcow_apps_processed[$i]['user_link'] = $mailcow_apps_processed[$i]['link'];
+    }
+  }
+}
+if ($app_links_processed){
+  for ($i = 0; $i < count($app_links_processed); $i++) {
+    $key = array_key_first($app_links_processed[$i]);
+    if ($hide_mailcow_apps && !$app_links_processed[$i][$key]['hide']){
+      $hide_mailcow_apps = false;
+    }
+    if (!empty($_SESSION['mailcow_cc_username'])){
+      if ($app_links_processed[$i][$key]['user_link']) {
+        $app_links_processed[$i][$key]['user_link'] = str_replace('%u', $_SESSION['mailcow_cc_username'], $app_links_processed[$i][$key]['user_link']);
+      } else {
+        $app_links_processed[$i][$key]['user_link'] = $app_links_processed[$i][$key]['link'];
+      }
+    }
+  }
+}
+
+
+
 $globalVariables = [
   'mailcow_hostname' => getenv('MAILCOW_HOSTNAME'),
   'mailcow_locale' => @$_SESSION['mailcow_locale'],
@@ -45,8 +79,11 @@ $globalVariables = [
   'lang' => $lang,
   'skip_sogo' => (getenv('SKIP_SOGO') == 'y'),
   'allow_admin_email_login' => (getenv('ALLOW_ADMIN_EMAIL_LOGIN') == 'n'),
+  'hide_mailcow_apps' => $hide_mailcow_apps,
   'mailcow_apps' => $MAILCOW_APPS,
-  'app_links' => customize('get', 'app_links'),
+  'mailcow_apps_processed' => $mailcow_apps_processed,
+  'app_links' => $app_links,
+  'app_links_processed' => $app_links_processed,
   'is_root_uri' => (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) == '/'),
   'uri' => $_SERVER['REQUEST_URI'],
 ];

+ 21 - 4
data/web/inc/init_db.inc.php

@@ -4,7 +4,7 @@ function init_db_schema()
   try {
     global $pdo;
 
-    $db_version = "20112024_1105";
+    $db_version = "27012025_1555";
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -368,6 +368,7 @@ function init_db_schema()
           "custom_attributes" => "JSON NOT NULL DEFAULT ('{}')",
           "kind" => "VARCHAR(100) NOT NULL DEFAULT ''",
           "multiple_bookings" => "INT NOT NULL DEFAULT -1",
+          "authsource" => "ENUM('mailcow', 'keycloak', 'generic-oidc', 'ldap') DEFAULT 'mailcow'",
           "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
           "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
           "active" => "TINYINT(1) NOT NULL DEFAULT '1'"
@@ -575,6 +576,20 @@ function init_db_schema()
         ),
         "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
       ),
+      "identity_provider" => array(
+        "cols" => array(
+          "key" => "VARCHAR(255) NOT NULL",
+          "value" => "TEXT NOT NULL",
+          "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
+          "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
+        ),
+        "keys" => array(
+          "primary" => array(
+            "" => array("key")
+          )
+        ),
+        "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
+      ),
       "logs" => array(
         "cols" => array(
           "id" => "INT NOT NULL AUTO_INCREMENT",
@@ -1455,6 +1470,9 @@ function init_db_schema()
       ));
     }
 
+    // remove old sogo views and triggers
+    $pdo->query("DROP TRIGGER IF EXISTS sogo_update_password");
+
     if (php_sapi_name() == "cli") {
       echo "DB initialization completed" . PHP_EOL;
     } else {
@@ -1478,6 +1496,7 @@ function init_db_schema()
 }
 if (php_sapi_name() == "cli") {
   include '/web/inc/vars.inc.php';
+  include '/web/inc/functions.inc.php';
   include '/web/inc/functions.docker.inc.php';
   // $now = new DateTime();
   // $mins = $now->getOffset() / 60;
@@ -1499,9 +1518,7 @@ if (php_sapi_name() == "cli") {
   if (intval($res['OK_C']) === 2) {
     // Be more precise when replacing into _sogo_static_view, col orders may change
     try {
-      $stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`)
-        SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view");
-      $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
+      update_sogo_static_view();
       echo "Fixed _sogo_static_view" . PHP_EOL;
     } catch (Exception $e) {
       // Dunno

+ 4 - 3
data/web/inc/lib/composer.json

@@ -1,7 +1,6 @@
 {
     "require": {
         "robthree/twofactorauth": "^1.6",
-        "yubico/u2flib-server": "^1.0",
         "phpmailer/phpmailer": "^6.1",
         "php-mime-mail-parser/php-mime-mail-parser": "^7",
         "soundasleep/html2text": "^0.5.0",
@@ -9,7 +8,9 @@
         "matthiasmullie/minify": "^1.3",
         "bshaffer/oauth2-server-php": "^1.11",
         "mustangostang/spyc": "^0.6.3",
-        "directorytree/ldaprecord": "^2.4",
-        "twig/twig": "^3.0"
+        "directorytree/ldaprecord": "^3.3",
+        "twig/twig": "^3.0",
+        "stevenmaguire/oauth2-keycloak": "^4.0",
+        "league/oauth2-client": "^2.7"
     }
 }

ファイルの差分が大きいため隠しています
+ 883 - 28
data/web/inc/lib/composer.lock


+ 34 - 12
data/web/inc/lib/vendor/bin/carbon

@@ -12,6 +12,7 @@
 
 namespace Composer;
 
+$GLOBALS['_composer_bin_dir'] = __DIR__;
 $GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
 
 if (PHP_VERSION_ID < 80000) {
@@ -23,18 +24,17 @@ if (PHP_VERSION_ID < 80000) {
         {
             private $handle;
             private $position;
+            private $realpath;
 
             public function stream_open($path, $mode, $options, &$opened_path)
             {
-                // get rid of composer-bin-proxy:// prefix for __FILE__ & __DIR__ resolution
-                $opened_path = substr($path, 21);
-                $opened_path = realpath($opened_path) ?: $opened_path;
-                $this->handle = fopen($opened_path, $mode);
+                // get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
+                $opened_path = substr($path, 17);
+                $this->realpath = realpath($opened_path) ?: $opened_path;
+                $opened_path = $this->realpath;
+                $this->handle = fopen($this->realpath, $mode);
                 $this->position = 0;
 
-                // remove all traces of this stream wrapper once it has been used
-                stream_wrapper_unregister('composer-bin-proxy');
-
                 return (bool) $this->handle;
             }
 
@@ -66,6 +66,16 @@ if (PHP_VERSION_ID < 80000) {
                 return $operation ? flock($this->handle, $operation) : true;
             }
 
+            public function stream_seek($offset, $whence)
+            {
+                if (0 === fseek($this->handle, $offset, $whence)) {
+                    $this->position = ftell($this->handle);
+                    return true;
+                }
+
+                return false;
+            }
+
             public function stream_tell()
             {
                 return $this->position;
@@ -78,20 +88,32 @@ if (PHP_VERSION_ID < 80000) {
 
             public function stream_stat()
             {
-                return fstat($this->handle);
+                return array();
             }
 
             public function stream_set_option($option, $arg1, $arg2)
             {
                 return true;
             }
+
+            public function url_stat($path, $flags)
+            {
+                $path = substr($path, 17);
+                if (file_exists($path)) {
+                    return stat($path);
+                }
+
+                return false;
+            }
         }
     }
 
-    if (function_exists('stream_wrapper_register') && stream_wrapper_register('composer-bin-proxy', 'Composer\BinProxyWrapper')) {
-        include("composer-bin-proxy://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon');
-        exit(0);
+    if (
+        (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
+        || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
+    ) {
+        return include("phpvfscomposer://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon');
     }
 }
 
-include __DIR__ . '/..'.'/nesbot/carbon/bin/carbon';
+return include __DIR__ . '/..'.'/nesbot/carbon/bin/carbon';

+ 34 - 12
data/web/inc/lib/vendor/bin/var-dump-server

@@ -12,6 +12,7 @@
 
 namespace Composer;
 
+$GLOBALS['_composer_bin_dir'] = __DIR__;
 $GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
 
 if (PHP_VERSION_ID < 80000) {
@@ -23,18 +24,17 @@ if (PHP_VERSION_ID < 80000) {
         {
             private $handle;
             private $position;
+            private $realpath;
 
             public function stream_open($path, $mode, $options, &$opened_path)
             {
-                // get rid of composer-bin-proxy:// prefix for __FILE__ & __DIR__ resolution
-                $opened_path = substr($path, 21);
-                $opened_path = realpath($opened_path) ?: $opened_path;
-                $this->handle = fopen($opened_path, $mode);
+                // get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
+                $opened_path = substr($path, 17);
+                $this->realpath = realpath($opened_path) ?: $opened_path;
+                $opened_path = $this->realpath;
+                $this->handle = fopen($this->realpath, $mode);
                 $this->position = 0;
 
-                // remove all traces of this stream wrapper once it has been used
-                stream_wrapper_unregister('composer-bin-proxy');
-
                 return (bool) $this->handle;
             }
 
@@ -66,6 +66,16 @@ if (PHP_VERSION_ID < 80000) {
                 return $operation ? flock($this->handle, $operation) : true;
             }
 
+            public function stream_seek($offset, $whence)
+            {
+                if (0 === fseek($this->handle, $offset, $whence)) {
+                    $this->position = ftell($this->handle);
+                    return true;
+                }
+
+                return false;
+            }
+
             public function stream_tell()
             {
                 return $this->position;
@@ -78,20 +88,32 @@ if (PHP_VERSION_ID < 80000) {
 
             public function stream_stat()
             {
-                return fstat($this->handle);
+                return array();
             }
 
             public function stream_set_option($option, $arg1, $arg2)
             {
                 return true;
             }
+
+            public function url_stat($path, $flags)
+            {
+                $path = substr($path, 17);
+                if (file_exists($path)) {
+                    return stat($path);
+                }
+
+                return false;
+            }
         }
     }
 
-    if (function_exists('stream_wrapper_register') && stream_wrapper_register('composer-bin-proxy', 'Composer\BinProxyWrapper')) {
-        include("composer-bin-proxy://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
-        exit(0);
+    if (
+        (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
+        || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
+    ) {
+        return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
     }
 }
 
-include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';
+return include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';

+ 21 - 0
data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Carbon
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 14 - 0
data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/README.md

@@ -0,0 +1,14 @@
+# carbonphp/carbon-doctrine-types
+
+Types to use Carbon in Doctrine
+
+## Documentation
+
+[Check how to use in the official Carbon documentation](https://carbon.nesbot.com/symfony/)
+
+This package is an externalization of [src/Carbon/Doctrine](https://github.com/briannesbitt/Carbon/tree/2.71.0/src/Carbon/Doctrine)
+from `nestbot/carbon` package.
+
+Externalization allows to better deal with different versions of dbal. With
+version 4.0 of dbal, it no longer sustainable to be compatible with all version
+using a single code.

+ 36 - 0
data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/composer.json

@@ -0,0 +1,36 @@
+{
+    "name": "carbonphp/carbon-doctrine-types",
+    "description": "Types to use Carbon in Doctrine",
+    "type": "library",
+    "keywords": [
+        "date",
+        "time",
+        "DateTime",
+        "Carbon",
+        "Doctrine"
+    ],
+    "require": {
+        "php": "^8.1"
+    },
+    "require-dev": {
+        "doctrine/dbal": "^4.0.0",
+        "nesbot/carbon": "^2.71.0 || ^3.0.0",
+        "phpunit/phpunit": "^10.3"
+    },
+    "conflict": {
+        "doctrine/dbal": "<4.0.0 || >=5.0.0"
+    },
+    "license": "MIT",
+    "autoload": {
+        "psr-4": {
+            "Carbon\\Doctrine\\": "src/Carbon/Doctrine/"
+        }
+    },
+    "authors": [
+        {
+            "name": "KyleKatarn",
+            "email": "kylekatarnls@gmail.com"
+        }
+    ],
+    "minimum-stability": "dev"
+}

+ 16 - 0
data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonDoctrineType.php

@@ -0,0 +1,16 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Carbon\Doctrine;
+
+use Doctrine\DBAL\Platforms\AbstractPlatform;
+
+interface CarbonDoctrineType
+{
+    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform);
+
+    public function convertToPHPValue(mixed $value, AbstractPlatform $platform);
+
+    public function convertToDatabaseValue($value, AbstractPlatform $platform);
+}

+ 9 - 0
data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonImmutableType.php

@@ -0,0 +1,9 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Carbon\Doctrine;
+
+class CarbonImmutableType extends DateTimeImmutableType implements CarbonDoctrineType
+{
+}

+ 9 - 0
data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonType.php

@@ -0,0 +1,9 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Carbon\Doctrine;
+
+class CarbonType extends DateTimeType implements CarbonDoctrineType
+{
+}

+ 50 - 42
data/web/inc/lib/vendor/nesbot/carbon/src/Carbon/Doctrine/CarbonTypeConverter.php → data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/CarbonTypeConverter.php

@@ -1,13 +1,6 @@
 <?php
 
-/**
- * This file is part of the Carbon package.
- *
- * (c) Brian Nesbitt <brian@nesbot.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
+declare(strict_types=1);
 
 namespace Carbon\Doctrine;
 
@@ -15,7 +8,12 @@ use Carbon\Carbon;
 use Carbon\CarbonInterface;
 use DateTimeInterface;
 use Doctrine\DBAL\Platforms\AbstractPlatform;
-use Doctrine\DBAL\Types\ConversionException;
+use Doctrine\DBAL\Platforms\DB2Platform;
+use Doctrine\DBAL\Platforms\OraclePlatform;
+use Doctrine\DBAL\Platforms\SQLitePlatform;
+use Doctrine\DBAL\Platforms\SQLServerPlatform;
+use Doctrine\DBAL\Types\Exception\InvalidType;
+use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
 use Exception;
 
 /**
@@ -23,6 +21,14 @@ use Exception;
  */
 trait CarbonTypeConverter
 {
+    /**
+     * This property differentiates types installed by carbonphp/carbon-doctrine-types
+     * from the ones embedded previously in nesbot/carbon source directly.
+     *
+     * @readonly
+     */
+    public bool $external = true;
+
     /**
      * @return class-string<T>
      */
@@ -31,20 +37,12 @@ trait CarbonTypeConverter
         return Carbon::class;
     }
 
-    /**
-     * @return string
-     */
-    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
+    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
     {
-        $precision = $fieldDeclaration['precision'] ?: 10;
-
-        if ($fieldDeclaration['secondPrecision'] ?? false) {
-            $precision = 0;
-        }
-
-        if ($precision === 10) {
-            $precision = DateTimeDefaultPrecision::get();
-        }
+        $precision = min(
+            $fieldDeclaration['precision'] ?? DateTimeDefaultPrecision::get(),
+            $this->getMaximumPrecision($platform),
+        );
 
         $type = parent::getSQLDeclaration($fieldDeclaration, $platform);
 
@@ -63,10 +61,25 @@ trait CarbonTypeConverter
 
     /**
      * @SuppressWarnings(PHPMD.UnusedFormalParameter)
-     *
-     * @return T|null
      */
-    public function convertToPHPValue($value, AbstractPlatform $platform)
+    public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
+    {
+        if ($value === null) {
+            return $value;
+        }
+
+        if ($value instanceof DateTimeInterface) {
+            return $value->format('Y-m-d H:i:s.u');
+        }
+
+        throw InvalidType::new(
+            $value,
+            static::class,
+            ['null', 'DateTime', 'Carbon']
+        );
+    }
+
+    private function doConvertToPHPValue(mixed $value)
     {
         $class = $this->getCarbonClassName();
 
@@ -88,9 +101,9 @@ trait CarbonTypeConverter
         }
 
         if (!$date) {
-            throw ConversionException::conversionFailedFormat(
+            throw ValueNotConvertible::new(
                 $value,
-                $this->getName(),
+                static::class,
                 'Y-m-d H:i:s.u or any format supported by '.$class.'::parse()',
                 $error
             );
@@ -99,25 +112,20 @@ trait CarbonTypeConverter
         return $date;
     }
 
-    /**
-     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
-     *
-     * @return string|null
-     */
-    public function convertToDatabaseValue($value, AbstractPlatform $platform)
+    private function getMaximumPrecision(AbstractPlatform $platform): int
     {
-        if ($value === null) {
-            return $value;
+        if ($platform instanceof DB2Platform) {
+            return 12;
         }
 
-        if ($value instanceof DateTimeInterface) {
-            return $value->format('Y-m-d H:i:s.u');
+        if ($platform instanceof OraclePlatform) {
+            return 9;
         }
 
-        throw ConversionException::conversionFailedInvalidType(
-            $value,
-            $this->getName(),
-            ['null', 'DateTime', 'Carbon']
-        );
+        if ($platform instanceof SQLServerPlatform || $platform instanceof SQLitePlatform) {
+            return 3;
+        }
+
+        return 6;
     }
 }

+ 1 - 8
data/web/inc/lib/vendor/nesbot/carbon/src/Carbon/Doctrine/DateTimeDefaultPrecision.php → data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeDefaultPrecision.php

@@ -1,13 +1,6 @@
 <?php
 
-/**
- * This file is part of the Carbon package.
- *
- * (c) Brian Nesbitt <brian@nesbot.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
+declare(strict_types=1);
 
 namespace Carbon\Doctrine;
 

+ 12 - 4
data/web/inc/lib/vendor/nesbot/carbon/src/Carbon/Doctrine/DateTimeImmutableType.php → data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeImmutableType.php

@@ -1,12 +1,12 @@
 <?php
 
-/**
- * Thanks to https://github.com/flaushi for his suggestion:
- * https://github.com/doctrine/dbal/issues/2873#issuecomment-534956358
- */
+declare(strict_types=1);
+
 namespace Carbon\Doctrine;
 
 use Carbon\CarbonImmutable;
+use DateTimeImmutable;
+use Doctrine\DBAL\Platforms\AbstractPlatform;
 use Doctrine\DBAL\Types\VarDateTimeImmutableType;
 
 class DateTimeImmutableType extends VarDateTimeImmutableType implements CarbonDoctrineType
@@ -14,6 +14,14 @@ class DateTimeImmutableType extends VarDateTimeImmutableType implements CarbonDo
     /** @use CarbonTypeConverter<CarbonImmutable> */
     use CarbonTypeConverter;
 
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?CarbonImmutable
+    {
+        return $this->doConvertToPHPValue($value);
+    }
+
     /**
      * @return class-string<CarbonImmutable>
      */

+ 24 - 0
data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine/DateTimeType.php

@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Carbon\Doctrine;
+
+use Carbon\Carbon;
+use DateTime;
+use Doctrine\DBAL\Platforms\AbstractPlatform;
+use Doctrine\DBAL\Types\VarDateTimeType;
+
+class DateTimeType extends VarDateTimeType implements CarbonDoctrineType
+{
+    /** @use CarbonTypeConverter<Carbon> */
+    use CarbonTypeConverter;
+
+    /**
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?Carbon
+    {
+        return $this->doConvertToPHPValue($value);
+    }
+}

+ 0 - 5
data/web/inc/lib/vendor/composer/autoload_classmap.php

@@ -13,9 +13,4 @@ return array(
     'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
     'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
     'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
-    'u2flib_server\\Error' => $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php',
-    'u2flib_server\\RegisterRequest' => $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php',
-    'u2flib_server\\Registration' => $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php',
-    'u2flib_server\\SignRequest' => $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php',
-    'u2flib_server\\U2F' => $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php',
 );

+ 4 - 0
data/web/inc/lib/vendor/composer/autoload_files.php

@@ -6,8 +6,12 @@ $vendorDir = dirname(__DIR__);
 $baseDir = dirname($vendorDir);
 
 return array(
+    '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
     '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
+    '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
+    'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
     'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
+    '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
     'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
     '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
     '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',

+ 10 - 0
data/web/inc/lib/vendor/composer/autoload_psr4.php

@@ -15,17 +15,27 @@ return array(
     'Symfony\\Contracts\\Translation\\' => array($vendorDir . '/symfony/translation-contracts'),
     'Symfony\\Component\\VarDumper\\' => array($vendorDir . '/symfony/var-dumper'),
     'Symfony\\Component\\Translation\\' => array($vendorDir . '/symfony/translation'),
+    'Stevenmaguire\\OAuth2\\Client\\' => array($vendorDir . '/stevenmaguire/oauth2-keycloak/src'),
     'RobThree\\Auth\\' => array($vendorDir . '/robthree/twofactorauth/lib'),
     'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
     'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
+    'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
+    'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
     'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
+    'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'),
     'PhpMimeMailParser\\' => array($vendorDir . '/php-mime-mail-parser/php-mime-mail-parser/src'),
     'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
     'MatthiasMullie\\PathConverter\\' => array($vendorDir . '/matthiasmullie/path-converter/src'),
     'MatthiasMullie\\Minify\\' => array($vendorDir . '/matthiasmullie/minify/src'),
+    'League\\OAuth2\\Client\\' => array($vendorDir . '/league/oauth2-client/src'),
     'LdapRecord\\' => array($vendorDir . '/directorytree/ldaprecord/src'),
     'Illuminate\\Contracts\\' => array($vendorDir . '/illuminate/contracts'),
     'Html2Text\\' => array($vendorDir . '/soundasleep/html2text/src'),
+    'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
+    'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
+    'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
+    'Firebase\\JWT\\' => array($vendorDir . '/firebase/php-jwt/src'),
     'Ddeboer\\Imap\\' => array($vendorDir . '/ddeboer/imap/src'),
+    'Carbon\\Doctrine\\' => array($vendorDir . '/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine'),
     'Carbon\\' => array($vendorDir . '/nesbot/carbon/src/Carbon'),
 );

+ 95 - 39
data/web/inc/lib/vendor/composer/autoload_static.php

@@ -7,8 +7,12 @@ namespace Composer\Autoload;
 class ComposerStaticInit873464e4bd965a3168f133248b1b218b
 {
     public static $files = array (
+        '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
         '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
+        '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
+        'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
         'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
+        '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
         'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php',
         '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
         '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
@@ -24,12 +28,12 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
     );
 
     public static $prefixLengthsPsr4 = array (
-        'T' => 
+        'T' =>
         array (
             'Twig\\' => 5,
             'Tightenco\\Collect\\' => 18,
         ),
-        'S' => 
+        'S' =>
         array (
             'Symfony\\Polyfill\\Php81\\' => 23,
             'Symfony\\Polyfill\\Php80\\' => 23,
@@ -38,141 +42,198 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
             'Symfony\\Contracts\\Translation\\' => 30,
             'Symfony\\Component\\VarDumper\\' => 28,
             'Symfony\\Component\\Translation\\' => 30,
+            'Stevenmaguire\\OAuth2\\Client\\' => 28,
         ),
-        'R' => 
+        'R' =>
         array (
             'RobThree\\Auth\\' => 14,
         ),
-        'P' => 
+        'P' =>
         array (
             'Psr\\SimpleCache\\' => 16,
             'Psr\\Log\\' => 8,
+            'Psr\\Http\\Message\\' => 17,
+            'Psr\\Http\\Client\\' => 16,
             'Psr\\Container\\' => 14,
+            'Psr\\Clock\\' => 10,
             'PhpMimeMailParser\\' => 18,
             'PHPMailer\\PHPMailer\\' => 20,
         ),
-        'M' => 
+        'M' =>
         array (
             'MatthiasMullie\\PathConverter\\' => 29,
             'MatthiasMullie\\Minify\\' => 22,
         ),
-        'L' => 
+        'L' =>
         array (
+            'League\\OAuth2\\Client\\' => 21,
             'LdapRecord\\' => 11,
         ),
-        'I' => 
+        'I' =>
         array (
             'Illuminate\\Contracts\\' => 21,
         ),
-        'H' => 
+        'H' =>
         array (
             'Html2Text\\' => 10,
         ),
-        'D' => 
+        'G' =>
+        array (
+            'GuzzleHttp\\Psr7\\' => 16,
+            'GuzzleHttp\\Promise\\' => 19,
+            'GuzzleHttp\\' => 11,
+        ),
+        'F' =>
+        array (
+            'Firebase\\JWT\\' => 13,
+        ),
+        'D' =>
         array (
             'Ddeboer\\Imap\\' => 13,
         ),
-        'C' => 
+        'C' =>
         array (
+            'Carbon\\Doctrine\\' => 16,
             'Carbon\\' => 7,
         ),
     );
 
     public static $prefixDirsPsr4 = array (
-        'Twig\\' => 
+        'Twig\\' =>
         array (
             0 => __DIR__ . '/..' . '/twig/twig/src',
         ),
-        'Tightenco\\Collect\\' => 
+        'Tightenco\\Collect\\' =>
         array (
             0 => __DIR__ . '/..' . '/tightenco/collect/src/Collect',
         ),
-        'Symfony\\Polyfill\\Php81\\' => 
+        'Symfony\\Polyfill\\Php81\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-php81',
         ),
-        'Symfony\\Polyfill\\Php80\\' => 
+        'Symfony\\Polyfill\\Php80\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
         ),
-        'Symfony\\Polyfill\\Mbstring\\' => 
+        'Symfony\\Polyfill\\Mbstring\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
         ),
-        'Symfony\\Polyfill\\Ctype\\' => 
+        'Symfony\\Polyfill\\Ctype\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/polyfill-ctype',
         ),
-        'Symfony\\Contracts\\Translation\\' => 
+        'Symfony\\Contracts\\Translation\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/translation-contracts',
         ),
-        'Symfony\\Component\\VarDumper\\' => 
+        'Symfony\\Component\\VarDumper\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/var-dumper',
         ),
-        'Symfony\\Component\\Translation\\' => 
+        'Symfony\\Component\\Translation\\' =>
         array (
             0 => __DIR__ . '/..' . '/symfony/translation',
         ),
-        'RobThree\\Auth\\' => 
+        'Stevenmaguire\\OAuth2\\Client\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/stevenmaguire/oauth2-keycloak/src',
+        ),
+        'RobThree\\Auth\\' =>
         array (
             0 => __DIR__ . '/..' . '/robthree/twofactorauth/lib',
         ),
-        'Psr\\SimpleCache\\' => 
+        'Psr\\SimpleCache\\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/simple-cache/src',
         ),
-        'Psr\\Log\\' => 
+        'Psr\\Log\\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/log/src',
         ),
-        'Psr\\Container\\' => 
+        'Psr\\Http\\Message\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/psr/http-factory/src',
+            1 => __DIR__ . '/..' . '/psr/http-message/src',
+        ),
+        'Psr\\Http\\Client\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/psr/http-client/src',
+        ),
+        'Psr\\Container\\' =>
         array (
             0 => __DIR__ . '/..' . '/psr/container/src',
         ),
-        'PhpMimeMailParser\\' => 
+        'Psr\\Clock\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/psr/clock/src',
+        ),
+        'PhpMimeMailParser\\' =>
         array (
             0 => __DIR__ . '/..' . '/php-mime-mail-parser/php-mime-mail-parser/src',
         ),
-        'PHPMailer\\PHPMailer\\' => 
+        'PHPMailer\\PHPMailer\\' =>
         array (
             0 => __DIR__ . '/..' . '/phpmailer/phpmailer/src',
         ),
-        'MatthiasMullie\\PathConverter\\' => 
+        'MatthiasMullie\\PathConverter\\' =>
         array (
             0 => __DIR__ . '/..' . '/matthiasmullie/path-converter/src',
         ),
-        'MatthiasMullie\\Minify\\' => 
+        'MatthiasMullie\\Minify\\' =>
         array (
             0 => __DIR__ . '/..' . '/matthiasmullie/minify/src',
         ),
-        'LdapRecord\\' => 
+        'League\\OAuth2\\Client\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/league/oauth2-client/src',
+        ),
+        'LdapRecord\\' =>
         array (
             0 => __DIR__ . '/..' . '/directorytree/ldaprecord/src',
         ),
-        'Illuminate\\Contracts\\' => 
+        'Illuminate\\Contracts\\' =>
         array (
             0 => __DIR__ . '/..' . '/illuminate/contracts',
         ),
-        'Html2Text\\' => 
+        'Html2Text\\' =>
         array (
             0 => __DIR__ . '/..' . '/soundasleep/html2text/src',
         ),
-        'Ddeboer\\Imap\\' => 
+        'GuzzleHttp\\Psr7\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
+        ),
+        'GuzzleHttp\\Promise\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/guzzlehttp/promises/src',
+        ),
+        'GuzzleHttp\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src',
+        ),
+        'Firebase\\JWT\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/firebase/php-jwt/src',
+        ),
+        'Ddeboer\\Imap\\' =>
         array (
             0 => __DIR__ . '/..' . '/ddeboer/imap/src',
         ),
-        'Carbon\\' => 
+        'Carbon\\Doctrine\\' =>
+        array (
+            0 => __DIR__ . '/..' . '/carbonphp/carbon-doctrine-types/src/Carbon/Doctrine',
+        ),
+        'Carbon\\' =>
         array (
             0 => __DIR__ . '/..' . '/nesbot/carbon/src/Carbon',
         ),
     );
 
     public static $prefixesPsr0 = array (
-        'O' => 
+        'O' =>
         array (
-            'OAuth2' => 
+            'OAuth2' =>
             array (
                 0 => __DIR__ . '/..' . '/bshaffer/oauth2-server-php/src',
             ),
@@ -187,11 +248,6 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
         'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
         'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
         'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
-        'u2flib_server\\Error' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php',
-        'u2flib_server\\RegisterRequest' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php',
-        'u2flib_server\\Registration' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php',
-        'u2flib_server\\SignRequest' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php',
-        'u2flib_server\\U2F' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php',
     );
 
     public static function getInitializer(ClassLoader $loader)

ファイルの差分が大きいため隠しています
+ 857 - 30
data/web/inc/lib/vendor/composer/installed.json


+ 159 - 36
data/web/inc/lib/vendor/composer/installed.php

@@ -28,6 +28,15 @@
             'aliases' => array(),
             'dev_requirement' => false,
         ),
+        'carbonphp/carbon-doctrine-types' => array(
+            'pretty_version' => '3.2.0',
+            'version' => '3.2.0.0',
+            'reference' => '18ba5ddfec8976260ead6e866180bd5d2f71aa1d',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../carbonphp/carbon-doctrine-types',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
         'ddeboer/imap' => array(
             'pretty_version' => '1.13.1',
             'version' => '1.13.1.0',
@@ -38,9 +47,9 @@
             'dev_requirement' => false,
         ),
         'directorytree/ldaprecord' => array(
-            'pretty_version' => 'v2.10.1',
-            'version' => '2.10.1.0',
-            'reference' => 'bf512d9af7a7b0e2ed7a666ab29cefdd027bee88',
+            'pretty_version' => 'v2.20.5',
+            'version' => '2.20.5.0',
+            'reference' => '5bd0a5a9d257cf1049ae83055dbba4c3479ddf16',
             'type' => 'library',
             'install_path' => __DIR__ . '/../directorytree/ldaprecord',
             'aliases' => array(),
@@ -52,15 +61,60 @@
                 0 => '*',
             ),
         ),
+        'firebase/php-jwt' => array(
+            'pretty_version' => 'v6.5.0',
+            'version' => '6.5.0.0',
+            'reference' => 'e94e7353302b0c11ec3cfff7180cd0b1743975d2',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../firebase/php-jwt',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/guzzle' => array(
+            'pretty_version' => '7.5.0',
+            'version' => '7.5.0.0',
+            'reference' => 'b50a2a1251152e43f6a37f0fa053e730a67d25ba',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/promises' => array(
+            'pretty_version' => '1.5.2',
+            'version' => '1.5.2.0',
+            'reference' => 'b94b2807d85443f9719887892882d0329d1e2598',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/promises',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'guzzlehttp/psr7' => array(
+            'pretty_version' => '2.4.5',
+            'version' => '2.4.5.0',
+            'reference' => '0454e12ef0cd597ccd2adb036f7bda4e7fface66',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../guzzlehttp/psr7',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
         'illuminate/contracts' => array(
-            'pretty_version' => 'v9.3.0',
-            'version' => '9.3.0.0',
-            'reference' => 'bf4b3c254c49d28157645d01e4883b5951b1e1d0',
+            'pretty_version' => 'v10.44.0',
+            'version' => '10.44.0.0',
+            'reference' => '8d7152c4a1f5d9cf7da3e8b71f23e4556f6138ac',
             'type' => 'library',
             'install_path' => __DIR__ . '/../illuminate/contracts',
             'aliases' => array(),
             'dev_requirement' => false,
         ),
+        'league/oauth2-client' => array(
+            'pretty_version' => '2.7.0',
+            'version' => '2.7.0.0',
+            'reference' => '160d6274b03562ebeb55ed18399281d8118b76c8',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../league/oauth2-client',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
         'matthiasmullie/minify' => array(
             'pretty_version' => '1.3.66',
             'version' => '1.3.66.0',
@@ -95,9 +149,9 @@
             'dev_requirement' => false,
         ),
         'nesbot/carbon' => array(
-            'pretty_version' => '2.57.0',
-            'version' => '2.57.0.0',
-            'reference' => '4a54375c21eea4811dbd1149fe6b246517554e78',
+            'pretty_version' => '2.72.3',
+            'version' => '2.72.3.0',
+            'reference' => '0c6fd108360c562f6e4fd1dedb8233b423e91c83',
             'type' => 'library',
             'install_path' => __DIR__ . '/../nesbot/carbon',
             'aliases' => array(),
@@ -130,6 +184,21 @@
             'aliases' => array(),
             'dev_requirement' => false,
         ),
+        'psr/clock' => array(
+            'pretty_version' => '1.0.0',
+            'version' => '1.0.0.0',
+            'reference' => 'e41a24703d4560fd0acb709162f73b8adfc3aa0d',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/clock',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'psr/clock-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
         'psr/container' => array(
             'pretty_version' => '2.0.2',
             'version' => '2.0.2.0',
@@ -139,6 +208,51 @@
             'aliases' => array(),
             'dev_requirement' => false,
         ),
+        'psr/http-client' => array(
+            'pretty_version' => '1.0.1',
+            'version' => '1.0.1.0',
+            'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-client',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'psr/http-client-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/http-factory' => array(
+            'pretty_version' => '1.0.1',
+            'version' => '1.0.1.0',
+            'reference' => '12ac7fcd07e5b077433f5f2bee95b3a771bf61be',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-factory',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'psr/http-factory-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
+        'psr/http-message' => array(
+            'pretty_version' => '1.0.1',
+            'version' => '1.0.1.0',
+            'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../psr/http-message',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
+        'psr/http-message-implementation' => array(
+            'dev_requirement' => false,
+            'provided' => array(
+                0 => '1.0',
+            ),
+        ),
         'psr/log' => array(
             'pretty_version' => '3.0.0',
             'version' => '3.0.0.0',
@@ -157,6 +271,15 @@
             'aliases' => array(),
             'dev_requirement' => false,
         ),
+        'ralouphie/getallheaders' => array(
+            'pretty_version' => '3.0.3',
+            'version' => '3.0.3.0',
+            'reference' => '120b605dfeb996808c31b6477290a714d356e822',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../ralouphie/getallheaders',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
         'robthree/twofactorauth' => array(
             'pretty_version' => '1.8.1',
             'version' => '1.8.1.0',
@@ -175,6 +298,15 @@
             'aliases' => array(),
             'dev_requirement' => false,
         ),
+        'stevenmaguire/oauth2-keycloak' => array(
+            'pretty_version' => '4.0.0',
+            'version' => '4.0.0.0',
+            'reference' => '05ead6bb6bcd2b6f96dfae87c769dcd3e5f6129d',
+            'type' => 'library',
+            'install_path' => __DIR__ . '/../stevenmaguire/oauth2-keycloak',
+            'aliases' => array(),
+            'dev_requirement' => false,
+        ),
         'symfony/deprecation-contracts' => array(
             'pretty_version' => 'v3.5.0',
             'version' => '3.5.0.0',
@@ -194,18 +326,18 @@
             'dev_requirement' => false,
         ),
         'symfony/polyfill-mbstring' => array(
-            'pretty_version' => 'v1.24.0',
-            'version' => '1.24.0.0',
-            'reference' => '0abb51d2f102e00a4eefcf46ba7fec406d245825',
+            'pretty_version' => 'v1.29.0',
+            'version' => '1.29.0.0',
+            'reference' => '9773676c8a1bb1f8d4340a62efe641cf76eda7ec',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
             'aliases' => array(),
             'dev_requirement' => false,
         ),
         'symfony/polyfill-php80' => array(
-            'pretty_version' => 'v1.24.0',
-            'version' => '1.24.0.0',
-            'reference' => '57b712b08eddb97c762a8caa32c84e037892d2e9',
+            'pretty_version' => 'v1.29.0',
+            'version' => '1.29.0.0',
+            'reference' => '87b68208d5c1188808dd7839ee1e6c8ec3b02f1b',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/polyfill-php80',
             'aliases' => array(),
@@ -221,18 +353,18 @@
             'dev_requirement' => false,
         ),
         'symfony/translation' => array(
-            'pretty_version' => 'v6.0.5',
-            'version' => '6.0.5.0',
-            'reference' => 'e69501c71107cc3146b32aaa45f4edd0c3427875',
+            'pretty_version' => 'v6.4.3',
+            'version' => '6.4.3.0',
+            'reference' => '637c51191b6b184184bbf98937702bcf554f7d04',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/translation',
             'aliases' => array(),
             'dev_requirement' => false,
         ),
         'symfony/translation-contracts' => array(
-            'pretty_version' => 'v3.0.0',
-            'version' => '3.0.0.0',
-            'reference' => '1b6ea5a7442af5a12dba3dbd6d71034b5b234e77',
+            'pretty_version' => 'v3.4.1',
+            'version' => '3.4.1.0',
+            'reference' => '06450585bf65e978026bda220cdebca3f867fde7',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/translation-contracts',
             'aliases' => array(),
@@ -245,18 +377,18 @@
             ),
         ),
         'symfony/var-dumper' => array(
-            'pretty_version' => 'v6.0.5',
-            'version' => '6.0.5.0',
-            'reference' => '60d6a756d5f485df5e6e40b337334848f79f61ce',
+            'pretty_version' => 'v6.4.3',
+            'version' => '6.4.3.0',
+            'reference' => '0435a08f69125535336177c29d56af3abc1f69da',
             'type' => 'library',
             'install_path' => __DIR__ . '/../symfony/var-dumper',
             'aliases' => array(),
             'dev_requirement' => false,
         ),
         'tightenco/collect' => array(
-            'pretty_version' => 'v8.83.2',
-            'version' => '8.83.2.0',
-            'reference' => 'd9c66d586ec2d216d8a31283d73f8df1400cc722',
+            'pretty_version' => 'v9.52.7',
+            'version' => '9.52.7.0',
+            'reference' => 'b15143cd11fe01a700fcc449df61adc64452fa6d',
             'type' => 'library',
             'install_path' => __DIR__ . '/../tightenco/collect',
             'aliases' => array(),
@@ -271,14 +403,5 @@
             'aliases' => array(),
             'dev_requirement' => false,
         ),
-        'yubico/u2flib-server' => array(
-            'pretty_version' => '1.0.2',
-            'version' => '1.0.2.0',
-            'reference' => '55d813acf68212ad2cadecde07551600d6971939',
-            'type' => 'library',
-            'install_path' => __DIR__ . '/../yubico/u2flib-server',
-            'aliases' => array(),
-            'dev_requirement' => false,
-        ),
     ),
 );

+ 4 - 1
data/web/inc/lib/vendor/directorytree/ldaprecord/.github/ISSUE_TEMPLATE/bug_report.md

@@ -7,7 +7,10 @@ assignees: ''
 
 ---
 
-<!-- Please update the below information with your environment. -->
+<!--
+  Please update the below information with your environment.
+  Issues filed without the below information will be closed.
+-->
 **Environment:**
  - LDAP Server Type: [e.g. ActiveDirectory / OpenLDAP / FreeIPA]
  - PHP Version: [e.g. 7.3 / 7.4 / 8.0]

+ 4 - 1
data/web/inc/lib/vendor/directorytree/ldaprecord/.github/ISSUE_TEMPLATE/support---help-request.md

@@ -11,7 +11,10 @@ assignees: ''
 <!-- https://github.com/sponsors/stevebauman -->
 <!-- Thank you for your understanding. -->
 
-<!-- Please update the below information with your environment. -->
+<!--
+  Please update the below information with your environment.
+  Issues filed without the below information will be closed.
+-->
 **Environment:**
  - LDAP Server Type: [e.g. ActiveDirectory / OpenLDAP / FreeIPA]
  - PHP Version: [e.g. 7.3 / 7.4 / 8.0]

+ 62 - 0
data/web/inc/lib/vendor/directorytree/ldaprecord/.github/workflows/run-integration-tests.yml

@@ -0,0 +1,62 @@
+name: run-integration-tests
+
+on:
+  push:
+  pull_request:
+  schedule:
+    - cron: "0 0 * * *"
+
+jobs:
+  run-tests:
+    runs-on: ${{ matrix.os }}
+
+    services:
+      ldap:
+        image: osixia/openldap:1.4.0
+        env:
+          LDAP_TLS_VERIFY_CLIENT: try
+          LDAP_OPENLDAP_UID: 1000
+          LDAP_OPENLDAP_GID: 1000
+          LDAP_ORGANISATION: Local
+          LDAP_DOMAIN: local.com
+          LDAP_ADMIN_PASSWORD: secret
+        ports:
+          - 389:389
+          - 636:636
+
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest]
+        php: [8.1, 8.0, 7.4]
+
+    name: ${{ matrix.os }} - P${{ matrix.php }}
+
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v2
+
+      - name: Cache dependencies
+        uses: actions/cache@v2
+        with:
+          path: ~/.composer/cache/files
+          key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
+
+      - name: Set ldap.conf file permissions
+        run: sudo chown -R $USER:$USER /etc/ldap/ldap.conf
+
+      - name: Create ldap.conf file disabling TLS verification
+        run: sudo echo "TLS_REQCERT never" > "/etc/ldap/ldap.conf"
+
+      - name: Setup PHP
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: ${{ matrix.php }}
+          extensions: ldap, json
+          coverage: none
+
+      - name: Install dependencies
+        run: composer update --prefer-dist --no-interaction
+
+      - name: Execute tests
+        run: vendor/bin/phpunit --testsuite Integration

+ 5 - 42
data/web/inc/lib/vendor/directorytree/ldaprecord/.github/workflows/run-tests.yml

@@ -9,20 +9,20 @@ on:
 jobs:
   run-tests:
     runs-on: ${{ matrix.os }}
+    name: ${{ matrix.os }} - P${{ matrix.php }}
+
     strategy:
       fail-fast: false
       matrix:
         os: [ubuntu-latest, windows-latest]
         php: [8.1, 8.0, 7.4, 7.3]
 
-    name: ${{ matrix.os }} - P${{ matrix.php }}
-
     steps:
       - name: Checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v2
 
       - name: Cache dependencies
-        uses: actions/cache@v3
+        uses: actions/cache@v2
         with:
           path: ~/.composer/cache/files
           key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
@@ -38,41 +38,4 @@ jobs:
         run: composer update --prefer-dist --no-interaction
 
       - name: Execute tests
-        run: vendor/bin/phpunit
-
-  run-analysis:
-    runs-on: ${{ matrix.os }}
-    name: Static code analysis (PHP ${{ matrix.php }})
-
-    strategy:
-      fail-fast: false
-      matrix:
-        os: [ubuntu-latest]
-        php: [8.0]
-
-    steps:
-      - name: Checkout code
-        uses: actions/checkout@v4
-
-      - name: Cache dependencies
-        uses: actions/cache@v3
-        with:
-          path: ~/.composer/cache/files
-          key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
-
-      - name: Setup PHP
-        uses: shivammathur/setup-php@v2
-        with:
-          php-version: ${{ matrix.php }}
-          extensions: ldap, json
-          coverage: none
-          tools: psalm
-
-      - name: Validate composer.json
-        run: composer validate
-
-      - name: Install dependencies
-        run: composer update --prefer-dist --no-interaction
-
-      - name: Run Psalm
-        run: psalm
+        run: vendor/bin/phpunit --testsuite Unit

+ 0 - 7
data/web/inc/lib/vendor/directorytree/ldaprecord/.styleci.yml

@@ -1,8 +1 @@
 preset: laravel
-enabled:
-  - phpdoc_align
-  - phpdoc_separation
-  - unalign_double_arrow
-disabled:
-  - laravel_phpdoc_alignment
-  - laravel_phpdoc_separation

+ 4 - 3
data/web/inc/lib/vendor/directorytree/ldaprecord/composer.json

@@ -32,11 +32,12 @@
         "php": ">=7.3",
         "ext-ldap": "*",
         "ext-json": "*",
-        "psr/log": "*",
+        "psr/log": "^1.0|^2.0|^3.0",
         "psr/simple-cache": "^1.0|^2.0",
         "nesbot/carbon": "^1.0|^2.0",
-        "tightenco/collect": "^5.6|^6.0|^7.0|^8.0",
-        "illuminate/contracts": "^5.0|^6.0|^7.0|^8.0|^9.0"
+        "tightenco/collect": "^5.6|^6.0|^7.0|^8.0|^9.0",
+        "illuminate/contracts": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0",
+        "symfony/polyfill-php80": "^1.25"
     },
     "require-dev": {
         "phpunit/phpunit": "^9.0",

+ 35 - 0
data/web/inc/lib/vendor/directorytree/ldaprecord/docker-compose.yml

@@ -0,0 +1,35 @@
+version: '3'
+
+services:
+    ldap:
+        image: osixia/openldap:1.4.0
+        container_name: ldap
+        restart: always
+        hostname: local.com
+        environment:
+            LDAP_TLS_VERIFY_CLIENT: try
+            LDAP_OPENLDAP_UID: 1000
+            LDAP_OPENLDAP_GID: 1000
+            LDAP_ORGANISATION: Local
+            LDAP_DOMAIN : local.com
+            LDAP_ADMIN_PASSWORD: secret
+        ports:
+            - "389:389"
+            - "636:636"
+        networks:
+            - local
+  
+    ldapadmin:
+        image: osixia/phpldapadmin:0.9.0
+        container_name: ldapadmin
+        environment:
+            PHPLDAPADMIN_LDAP_HOSTS: ldap
+        restart: always
+        ports:
+            - "6443:443"
+        networks:
+            - local
+
+networks:
+    local:
+        driver: bridge

+ 5 - 2
data/web/inc/lib/vendor/directorytree/ldaprecord/phpunit.xml

@@ -10,8 +10,11 @@
          stopOnFailure="false"
         >
     <testsuites>
-        <testsuite name="LdapRecord Test Suite">
-            <directory suffix="Test.php">./tests/</directory>
+        <testsuite name="Unit">
+            <directory suffix="Test.php">./tests/Unit</directory>
+        </testsuite>
+        <testsuite name="Integration">
+            <directory suffix="Test.php">./tests/Integration</directory>
         </testsuite>
     </testsuites>
 </phpunit>

+ 0 - 15
data/web/inc/lib/vendor/directorytree/ldaprecord/psalm.xml

@@ -1,15 +0,0 @@
-<?xml version="1.0"?>
-<psalm
-    errorLevel="7"
-    resolveFromConfigFile="true"
-    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xmlns="https://getpsalm.org/schema/config"
-    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
->
-    <projectFiles>
-        <directory name="src" />
-        <ignoreFiles>
-            <directory name="vendor" />
-        </ignoreFiles>
-    </projectFiles>
-</psalm>

+ 2 - 2
data/web/inc/lib/vendor/directorytree/ldaprecord/readme.md

@@ -6,7 +6,7 @@
 
 <p align="center">
     <a href="https://github.com/DirectoryTree/LdapRecord/actions">
-        <img src="https://img.shields.io/github/workflow/status/directorytree/ldaprecord/run-tests.svg?style=flat-square">
+        <img src="https://img.shields.io/github/actions/workflow/status/directorytree/ldaprecord/run-tests.yml?branch=master&style=flat-square">
     </a>
     <a href="https://scrutinizer-ci.com/g/DirectoryTree/LdapRecord/?branch=master">
         <img src="https://img.shields.io/scrutinizer/g/DirectoryTree/LdapRecord/master.svg?style=flat-square"/>
@@ -45,7 +45,7 @@
 
 ⏲ **Up and Running Fast**
 
-Connect to your LDAP servers and start running queries at lightning speed.
+Connect to your LDAP servers and start running queries in a matter of minutes.
 
 💡 **Fluent Filter Builder**
 

+ 3 - 3
data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Events/Event.php

@@ -30,9 +30,9 @@ abstract class Event
     /**
      * Constructor.
      *
-     * @param LdapInterface $connection
-     * @param string        $username
-     * @param string        $password
+     * @param  LdapInterface  $connection
+     * @param  string  $username
+     * @param  string  $password
      */
     public function __construct(LdapInterface $connection, $username, $password)
     {

+ 18 - 25
data/web/inc/lib/vendor/directorytree/ldaprecord/src/Auth/Guard.php

@@ -38,8 +38,8 @@ class Guard
     /**
      * Constructor.
      *
-     * @param LdapInterface       $connection
-     * @param DomainConfiguration $configuration
+     * @param  LdapInterface  $connection
+     * @param  DomainConfiguration  $configuration
      */
     public function __construct(LdapInterface $connection, DomainConfiguration $configuration)
     {
@@ -50,10 +50,9 @@ class Guard
     /**
      * Attempt binding a user to the LDAP server.
      *
-     * @param string $username
-     * @param string $password
-     * @param bool   $stayBound
-     *
+     * @param  string  $username
+     * @param  string  $password
+     * @param  bool  $stayBound
      * @return bool
      *
      * @throws UsernameRequiredException
@@ -90,8 +89,8 @@ class Guard
     /**
      * Attempt binding a user to the LDAP server. Supports anonymous binding.
      *
-     * @param string|null $username
-     * @param string|null $password
+     * @param  string|null  $username
+     * @param  string|null  $password
      *
      * @throws BindException
      * @throws \LdapRecord\ConnectionException
@@ -148,8 +147,7 @@ class Guard
     /**
      * Set the event dispatcher instance.
      *
-     * @param DispatcherInterface $dispatcher
-     *
+     * @param  DispatcherInterface  $dispatcher
      * @return void
      */
     public function setDispatcher(DispatcherInterface $dispatcher)
@@ -160,9 +158,8 @@ class Guard
     /**
      * Fire the attempting event.
      *
-     * @param string $username
-     * @param string $password
-     *
+     * @param  string  $username
+     * @param  string  $password
      * @return void
      */
     protected function fireAttemptingEvent($username, $password)
@@ -175,9 +172,8 @@ class Guard
     /**
      * Fire the passed event.
      *
-     * @param string $username
-     * @param string $password
-     *
+     * @param  string  $username
+     * @param  string  $password
      * @return void
      */
     protected function firePassedEvent($username, $password)
@@ -190,9 +186,8 @@ class Guard
     /**
      * Fire the failed event.
      *
-     * @param string $username
-     * @param string $password
-     *
+     * @param  string  $username
+     * @param  string  $password
      * @return void
      */
     protected function fireFailedEvent($username, $password)
@@ -205,9 +200,8 @@ class Guard
     /**
      * Fire the binding event.
      *
-     * @param string $username
-     * @param string $password
-     *
+     * @param  string  $username
+     * @param  string  $password
      * @return void
      */
     protected function fireBindingEvent($username, $password)
@@ -220,9 +214,8 @@ class Guard
     /**
      * Fire the bound event.
      *
-     * @param string $username
-     * @param string $password
-     *
+     * @param  string  $username
+     * @param  string  $password
      * @return void
      */
     protected function fireBoundEvent($username, $password)

+ 9 - 13
data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/DomainConfiguration.php

@@ -58,7 +58,7 @@ class DomainConfiguration
     /**
      * Constructor.
      *
-     * @param array $options
+     * @param  array  $options
      *
      * @throws ConfigurationException When an option value given is an invalid type.
      */
@@ -74,9 +74,8 @@ class DomainConfiguration
     /**
      * Extend the configuration with a custom option, or override an existing.
      *
-     * @param string $option
-     * @param mixed  $default
-     *
+     * @param  string  $option
+     * @param  mixed  $default
      * @return void
      */
     public static function extend($option, $default = null)
@@ -107,8 +106,8 @@ class DomainConfiguration
     /**
      * Set a configuration option.
      *
-     * @param string $key
-     * @param mixed  $value
+     * @param  string  $key
+     * @param  mixed  $value
      *
      * @throws ConfigurationException When an option value given is an invalid type.
      */
@@ -122,8 +121,7 @@ class DomainConfiguration
     /**
      * Returns the value for the specified configuration options.
      *
-     * @param string $key
-     *
+     * @param  string  $key
      * @return mixed
      *
      * @throws ConfigurationException When the option specified does not exist.
@@ -140,8 +138,7 @@ class DomainConfiguration
     /**
      * Checks if a configuration option exists.
      *
-     * @param string $key
-     *
+     * @param  string  $key
      * @return bool
      */
     public function has($key)
@@ -152,9 +149,8 @@ class DomainConfiguration
     /**
      * Validate the configuration option.
      *
-     * @param string $key
-     * @param mixed  $value
-     *
+     * @param  string  $key
+     * @param  mixed  $value
      * @return bool
      *
      * @throws ConfigurationException When an option value given is an invalid type.

+ 2 - 2
data/web/inc/lib/vendor/directorytree/ldaprecord/src/Configuration/Validators/Validator.php

@@ -30,8 +30,8 @@ abstract class Validator
     /**
      * Constructor.
      *
-     * @param string $key
-     * @param mixed  $value
+     * @param  string  $key
+     * @param  mixed  $value
      */
     public function __construct($key, $value)
     {

+ 49 - 30
data/web/inc/lib/vendor/directorytree/ldaprecord/src/Connection.php

@@ -88,8 +88,8 @@ class Connection
     /**
      * Constructor.
      *
-     * @param array              $config
-     * @param LdapInterface|null $ldap
+     * @param  array|DomainConfiguration  $config
+     * @param  LdapInterface|null  $ldap
      */
     public function __construct($config = [], LdapInterface $ldap = null)
     {
@@ -109,15 +109,18 @@ class Connection
     /**
      * Set the connection configuration.
      *
-     * @param array $config
-     *
+     * @param  array|DomainConfiguration  $config
      * @return $this
      *
      * @throws Configuration\ConfigurationException
      */
     public function setConfiguration($config = [])
     {
-        $this->configuration = new DomainConfiguration($config);
+        if (! $config instanceof DomainConfiguration) {
+            $config = new DomainConfiguration($config);
+        }
+
+        $this->configuration = $config;
 
         $this->hosts = $this->configuration->get('hosts');
 
@@ -129,8 +132,7 @@ class Connection
     /**
      * Set the LDAP connection.
      *
-     * @param LdapInterface $ldap
-     *
+     * @param  LdapInterface  $ldap
      * @return $this
      */
     public function setLdapConnection(LdapInterface $ldap)
@@ -143,8 +145,7 @@ class Connection
     /**
      * Set the event dispatcher.
      *
-     * @param DispatcherInterface $dispatcher
-     *
+     * @param  DispatcherInterface  $dispatcher
      * @return $this
      */
     public function setDispatcher(DispatcherInterface $dispatcher)
@@ -192,8 +193,7 @@ class Connection
     /**
      * Set the cache store.
      *
-     * @param CacheInterface $store
-     *
+     * @param  CacheInterface  $store
      * @return $this
      */
     public function setCache(CacheInterface $store)
@@ -238,9 +238,8 @@ class Connection
      *
      * If no username or password is specified, then the configured credentials are used.
      *
-     * @param string|null $username
-     * @param string|null $password
-     *
+     * @param  string|null  $username
+     * @param  string|null  $password
      * @return Connection
      *
      * @throws Auth\BindException
@@ -298,6 +297,16 @@ class Connection
         $this->initialize();
     }
 
+    /**
+     * Clone the connection.
+     *
+     * @return static
+     */
+    public function replicate()
+    {
+        return new static($this->configuration, new $this->ldap);
+    }
+
     /**
      * Disconnect from the LDAP server.
      *
@@ -311,8 +320,7 @@ class Connection
     /**
      * Dispatch an event.
      *
-     * @param object $event
-     *
+     * @param  object  $event
      * @return void
      */
     public function dispatch($event)
@@ -335,8 +343,7 @@ class Connection
     /**
      * Perform the operation on the LDAP connection.
      *
-     * @param Closure $operation
-     *
+     * @param  Closure  $operation
      * @return mixed
      */
     public function run(Closure $operation)
@@ -360,10 +367,26 @@ class Connection
     }
 
     /**
-     * Attempt to get an exception for the cause of failure.
+     * Perform the operation on an isolated LDAP connection.
      *
-     * @param LdapRecordException $e
+     * @param  Closure  $operation
+     * @return mixed
+     */
+    public function isolate(Closure $operation)
+    {
+        $connection = $this->replicate();
+
+        try {
+            return $operation($connection);
+        } finally {
+            $connection->disconnect();
+        }
+    }
+
+    /**
+     * Attempt to get an exception for the cause of failure.
      *
+     * @param  LdapRecordException  $e
      * @return mixed
      */
     protected function getExceptionForCauseOfFailure(LdapRecordException $e)
@@ -383,8 +406,7 @@ class Connection
     /**
      * Run the operation callback on the current LDAP connection.
      *
-     * @param Closure $operation
-     *
+     * @param  Closure  $operation
      * @return mixed
      *
      * @throws LdapRecordException
@@ -439,9 +461,8 @@ class Connection
     /**
      * Attempt to retry an LDAP operation if due to a lost connection.
      *
-     * @param LdapRecordException $e
-     * @param Closure             $operation
-     *
+     * @param  LdapRecordException  $e
+     * @param  Closure  $operation
      * @return mixed
      *
      * @throws LdapRecordException
@@ -461,8 +482,7 @@ class Connection
     /**
      * Retry the operation on the current host.
      *
-     * @param Closure $operation
-     *
+     * @param  Closure  $operation
      * @return mixed
      *
      * @throws LdapRecordException
@@ -483,9 +503,8 @@ class Connection
     /**
      * Attempt the operation again on the next host.
      *
-     * @param LdapRecordException $e
-     * @param Closure             $operation
-     *
+     * @param  LdapRecordException  $e
+     * @param  Closure  $operation
      * @return mixed
      *
      * @throws LdapRecordException

+ 10 - 18
data/web/inc/lib/vendor/directorytree/ldaprecord/src/ConnectionManager.php

@@ -81,9 +81,8 @@ class ConnectionManager
     /**
      * Forward missing method calls onto the instance.
      *
-     * @param string $method
-     * @param mixed  $args
-     *
+     * @param  string  $method
+     * @param  mixed  $args
      * @return mixed
      */
     public function __call($method, $args)
@@ -104,9 +103,8 @@ class ConnectionManager
     /**
      * Add a new connection.
      *
-     * @param Connection  $connection
-     * @param string|null $name
-     *
+     * @param  Connection  $connection
+     * @param  string|null  $name
      * @return $this
      */
     public function add(Connection $connection, $name = null)
@@ -123,8 +121,7 @@ class ConnectionManager
     /**
      * Remove a connection.
      *
-     * @param $name
-     *
+     * @param  $name
      * @return $this
      */
     public function remove($name)
@@ -147,8 +144,7 @@ class ConnectionManager
     /**
      * Get a connection by name or return the default.
      *
-     * @param string|null $name
-     *
+     * @param  string|null  $name
      * @return Connection
      *
      * @throws ContainerException If the given connection does not exist.
@@ -185,8 +181,7 @@ class ConnectionManager
     /**
      * Checks if the connection exists.
      *
-     * @param string $name
-     *
+     * @param  string  $name
      * @return bool
      */
     public function exists($name)
@@ -197,8 +192,7 @@ class ConnectionManager
     /**
      * Set the default connection name.
      *
-     * @param string $name
-     *
+     * @param  string  $name
      * @return $this
      */
     public function setDefault($name = null)
@@ -237,8 +231,7 @@ class ConnectionManager
     /**
      * Set the event logger to use.
      *
-     * @param LoggerInterface $logger
-     *
+     * @param  LoggerInterface  $logger
      * @return void
      */
     public function setLogger(LoggerInterface $logger)
@@ -299,8 +292,7 @@ class ConnectionManager
     /**
      * Set the event dispatcher.
      *
-     * @param DispatcherInterface $dispatcher
-     *
+     * @param  DispatcherInterface  $dispatcher
      * @return void
      */
     public function setDispatcher(DispatcherInterface $dispatcher)

+ 5 - 8
data/web/inc/lib/vendor/directorytree/ldaprecord/src/Container.php

@@ -48,9 +48,8 @@ class Container
     /**
      * Forward missing static calls onto the current instance.
      *
-     * @param string $method
-     * @param mixed  $args
-     *
+     * @param  string  $method
+     * @param  mixed  $args
      * @return mixed
      */
     public static function __callStatic($method, $args)
@@ -71,8 +70,7 @@ class Container
     /**
      * Set the container instance.
      *
-     * @param Container|null $container
-     *
+     * @param  Container|null  $container
      * @return Container|null
      */
     public static function setInstance(self $container = null)
@@ -103,9 +101,8 @@ class Container
     /**
      * Forward missing method calls onto the connection manager.
      *
-     * @param string $method
-     * @param mixed  $args
-     *
+     * @param  string  $method
+     * @param  mixed  $args
      * @return mixed
      */
     public function __call($method, $args)

+ 3 - 3
data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetailedError.php

@@ -28,9 +28,9 @@ class DetailedError
     /**
      * Constructor.
      *
-     * @param int    $errorCode
-     * @param string $errorMessage
-     * @param string $diagnosticMessage
+     * @param  int  $errorCode
+     * @param  string  $errorMessage
+     * @param  string  $diagnosticMessage
      */
     public function __construct($errorCode, $errorMessage, $diagnosticMessage)
     {

+ 7 - 12
data/web/inc/lib/vendor/directorytree/ldaprecord/src/DetectsErrors.php

@@ -7,8 +7,7 @@ trait DetectsErrors
     /**
      * Determine if the error was caused by a lost connection.
      *
-     * @param string $error
-     *
+     * @param  string  $error
      * @return bool
      */
     protected function causedByLostConnection($error)
@@ -19,8 +18,7 @@ trait DetectsErrors
     /**
      * Determine if the error was caused by lack of pagination support.
      *
-     * @param string $error
-     *
+     * @param  string  $error
      * @return bool
      */
     protected function causedByPaginationSupport($error)
@@ -31,8 +29,7 @@ trait DetectsErrors
     /**
      * Determine if the error was caused by a size limit warning.
      *
-     * @param $error
-     *
+     * @param  $error
      * @return bool
      */
     protected function causedBySizeLimit($error)
@@ -43,8 +40,7 @@ trait DetectsErrors
     /**
      * Determine if the error was caused by a "No such object" warning.
      *
-     * @param string $error
-     *
+     * @param  string  $error
      * @return bool
      */
     protected function causedByNoSuchObject($error)
@@ -55,15 +51,14 @@ trait DetectsErrors
     /**
      * Determine if the error contains the any of the messages.
      *
-     * @param string       $error
-     * @param string|array $messages
-     *
+     * @param  string  $error
+     * @param  string|array  $messages
      * @return bool
      */
     protected function errorContainsMessage($error, $messages = [])
     {
         foreach ((array) $messages as $message) {
-            if (strpos($error, $message) !== false) {
+            if (str_contains((string) $error, $message)) {
                 return true;
             }
         }

+ 3 - 4
data/web/inc/lib/vendor/directorytree/ldaprecord/src/EscapesValues.php

@@ -9,10 +9,9 @@ trait EscapesValues
     /**
      * Prepare a value to be escaped.
      *
-     * @param string $value
-     * @param string $ignore
-     * @param int    $flags
-     *
+     * @param  string  $value
+     * @param  string  $ignore
+     * @param  int  $flags
      * @return EscapedValue
      */
     public function escape($value, $ignore = '', $flags = 0)

+ 1 - 1
data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/ConnectionEvent.php

@@ -16,7 +16,7 @@ abstract class ConnectionEvent
     /**
      * Constructor.
      *
-     * @param Connection $connection
+     * @param  Connection  $connection
      */
     public function __construct(Connection $connection)
     {

+ 18 - 27
data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/Dispatcher.php

@@ -46,7 +46,7 @@ class Dispatcher implements DispatcherInterface
     public function listen($events, $listener)
     {
         foreach ((array) $events as $event) {
-            if (strpos($event, '*') !== false) {
+            if (str_contains((string) $event, '*')) {
                 $this->setupWildcardListen($event, $listener);
             } else {
                 $this->listeners[$event][] = $this->makeListener($listener);
@@ -57,9 +57,8 @@ class Dispatcher implements DispatcherInterface
     /**
      * Setup a wildcard listener callback.
      *
-     * @param string $event
-     * @param mixed  $listener
-     *
+     * @param  string  $event
+     * @param  mixed  $listener
      * @return void
      */
     protected function setupWildcardListen($event, $listener)
@@ -134,9 +133,8 @@ class Dispatcher implements DispatcherInterface
     /**
      * Parse the given event and payload and prepare them for dispatching.
      *
-     * @param mixed $event
-     * @param mixed $payload
-     *
+     * @param  mixed  $event
+     * @param  mixed  $payload
      * @return array
      */
     protected function parseEventAndPayload($event, $payload)
@@ -168,8 +166,7 @@ class Dispatcher implements DispatcherInterface
     /**
      * Get the wildcard listeners for the event.
      *
-     * @param string $eventName
-     *
+     * @param  string  $eventName
      * @return array
      */
     protected function getWildcardListeners($eventName)
@@ -190,9 +187,8 @@ class Dispatcher implements DispatcherInterface
      *
      * This function is a direct excerpt from Laravel's Str::is().
      *
-     * @param string $wildcard
-     * @param string $eventName
-     *
+     * @param  string  $wildcard
+     * @param  string  $eventName
      * @return bool
      */
     protected function wildcardContainsEvent($wildcard, $eventName)
@@ -229,9 +225,8 @@ class Dispatcher implements DispatcherInterface
     /**
      * Add the listeners for the event's interfaces to the given array.
      *
-     * @param string $eventName
-     * @param array  $listeners
-     *
+     * @param  string  $eventName
+     * @param  array  $listeners
      * @return array
      */
     protected function addInterfaceListeners($eventName, array $listeners = [])
@@ -250,9 +245,8 @@ class Dispatcher implements DispatcherInterface
     /**
      * Register an event listener with the dispatcher.
      *
-     * @param \Closure|string $listener
-     * @param bool            $wildcard
-     *
+     * @param  \Closure|string  $listener
+     * @param  bool  $wildcard
      * @return \Closure
      */
     public function makeListener($listener, $wildcard = false)
@@ -273,9 +267,8 @@ class Dispatcher implements DispatcherInterface
     /**
      * Create a class based listener.
      *
-     * @param string $listener
-     * @param bool   $wildcard
-     *
+     * @param  string  $listener
+     * @param  bool  $wildcard
      * @return \Closure
      */
     protected function createClassListener($listener, $wildcard = false)
@@ -295,8 +288,7 @@ class Dispatcher implements DispatcherInterface
     /**
      * Create the class based event callable.
      *
-     * @param string $listener
-     *
+     * @param  string  $listener
      * @return callable
      */
     protected function createClassCallable($listener)
@@ -309,13 +301,12 @@ class Dispatcher implements DispatcherInterface
     /**
      * Parse the class listener into class and method.
      *
-     * @param string $listener
-     *
+     * @param  string  $listener
      * @return array
      */
     protected function parseListenerCallback($listener)
     {
-        return strpos($listener, '@') !== false
+        return str_contains((string) $listener, '@')
             ? explode('@', $listener, 2)
             : [$listener, 'handle'];
     }
@@ -325,7 +316,7 @@ class Dispatcher implements DispatcherInterface
      */
     public function forget($event)
     {
-        if (strpos($event, '*') !== false) {
+        if (str_contains((string) $event, '*')) {
             unset($this->wildcards[$event]);
         } else {
             unset($this->listeners[$event]);

+ 13 - 20
data/web/inc/lib/vendor/directorytree/ldaprecord/src/Events/DispatcherInterface.php

@@ -7,9 +7,8 @@ interface DispatcherInterface
     /**
      * Register an event listener with the dispatcher.
      *
-     * @param string|array $events
-     * @param mixed        $listener
-     *
+     * @param  string|array  $events
+     * @param  mixed  $listener
      * @return void
      */
     public function listen($events, $listener);
@@ -17,8 +16,7 @@ interface DispatcherInterface
     /**
      * Determine if a given event has listeners.
      *
-     * @param string $eventName
-     *
+     * @param  string  $eventName
      * @return bool
      */
     public function hasListeners($eventName);
@@ -26,9 +24,8 @@ interface DispatcherInterface
     /**
      * Fire an event until the first non-null response is returned.
      *
-     * @param string|object $event
-     * @param mixed         $payload
-     *
+     * @param  string|object  $event
+     * @param  mixed  $payload
      * @return array|null
      */
     public function until($event, $payload = []);
@@ -36,10 +33,9 @@ interface DispatcherInterface
     /**
      * Fire an event and call the listeners.
      *
-     * @param string|object $event
-     * @param mixed         $payload
-     * @param bool          $halt
-     *
+     * @param  string|object  $event
+     * @param  mixed  $payload
+     * @param  bool  $halt
      * @return mixed
      */
     public function fire($event, $payload = [], $halt = false);
@@ -47,10 +43,9 @@ interface DispatcherInterface
     /**
      * Fire an event and call the listeners.
      *
-     * @param string|object $event
-     * @param mixed         $payload
-     * @param bool          $halt
-     *
+     * @param  string|object  $event
+     * @param  mixed  $payload
+     * @param  bool  $halt
      * @return array|null
      */
     public function dispatch($event, $payload = [], $halt = false);
@@ -58,8 +53,7 @@ interface DispatcherInterface
     /**
      * Get all of the listeners for a given event name.
      *
-     * @param string $eventName
-     *
+     * @param  string  $eventName
      * @return array
      */
     public function getListeners($eventName);
@@ -67,8 +61,7 @@ interface DispatcherInterface
     /**
      * Remove a set of listeners from the dispatcher.
      *
-     * @param string $event
-     *
+     * @param  string  $event
      * @return void
      */
     public function forget($event);

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません