Browse Source

Merge branch 'nightly' into feature/bootstrap5

DerLinkman 3 years ago
parent
commit
ecc16c69e6
40 changed files with 1452 additions and 774 deletions
  1. 0 120
      .drone.yml
  2. 2 1
      .github/workflows/close_old_issues_and_prs.yml
  3. 42 0
      .github/workflows/image_builds.yml
  4. 60 0
      .github/workflows/integration_tests.yml
  5. 17 0
      .github/workflows/tweet-trigger-publish-release.yml
  6. 0 16
      .travis.yml
  7. 2 1
      README.md
  8. 42 0
      SECURITY.md
  9. 8 2
      create_cold_standby.sh
  10. 9 0
      data/Dockerfiles/clamd/healthcheck.sh
  11. 1 1
      data/Dockerfiles/dovecot/Dockerfile
  12. 8 0
      data/Dockerfiles/dovecot/docker-entrypoint.sh
  13. 0 1
      data/conf/dovecot/dovecot.conf
  14. 3 0
      data/conf/rspamd/local.d/groups.conf
  15. 95 51
      data/web/api/openapi.yaml
  16. 1 78
      data/web/css/build/013-mailcow.css
  17. 1 1
      data/web/inc/ajax/destroy_tfa_auth.php
  18. 30 5
      data/web/inc/footer.inc.php
  19. 253 184
      data/web/inc/functions.inc.php
  20. 62 5
      data/web/inc/functions.mailbox.inc.php
  21. 14 6
      data/web/inc/init_db.inc.php
  22. 2 1
      data/web/inc/prerequisites.inc.php
  23. 16 15
      data/web/inc/triggers.inc.php
  24. 128 0
      data/web/inc/vars.inc.php
  25. 28 19
      data/web/json_api.php
  26. 2 1
      data/web/lang/lang.es.json
  27. 3 2
      data/web/lang/lang.ru.json
  28. 2 1
      data/web/lang/lang.uk.json
  29. 100 26
      data/web/templates/base.twig
  30. 1 1
      data/web/templates/domainadmin.twig
  31. 8 8
      data/web/templates/modals/footer.twig
  32. 2 2
      data/web/templates/modals/mailbox.twig
  33. 21 0
      data/web/templates/user/tab-user-auth.twig
  34. 2 2
      data/web/user.php
  35. 5 5
      docker-compose.yml
  36. 77 30
      generate_config.sh
  37. 29 55
      helper-scripts/_cold-standby.sh
  38. 25 22
      helper-scripts/backup_and_restore.sh
  39. 70 0
      helper-scripts/update_compose.sh
  40. 281 112
      update.sh

+ 0 - 120
.drone.yml

@@ -1,120 +0,0 @@
----
-kind: pipeline
-name: integration-testing
-
-platform:
-  os: linux
-  arch: amd64
-
-clone:
-  disable: true
-
-steps:
-- name: prepare-tests
-  pull: default
-  image: timovibritannia/ansible
-  commands:
-  - git clone https://github.com/mailcow/mailcow-integration-tests.git --branch $(curl -sL https://api.github.com/repos/mailcow/mailcow-integration-tests/releases/latest | jq -r '.tag_name') --single-branch .
-  - chmod +x ci.sh
-  - chmod +x ci-ssh.sh
-  - chmod +x ci-piprequierments.sh
-  - ./ci.sh
-  - wget -O group_vars/all/secrets.yml $SECRETS_DOWNLOAD_URL --quiet
-  environment:
-    SECRETS_DOWNLOAD_URL:
-      from_secret: SECRETS_DOWNLOAD_URL
-    VAULT_PW:
-      from_secret: VAULT_PW
-  when:
-    branch:
-    - master
-    - staging
-    event:
-    - push
-
-- name: lint
-  pull: default
-  image: timovibritannia/ansible
-  commands:
-  - ansible-lint ./
-  when:
-    branch:
-    - master
-    - staging
-    event:
-    - push
-
-- name: create-server
-  pull: default
-  image: timovibritannia/ansible
-  commands:
-  - ./ci-piprequierments.sh
-  - ansible-playbook mailcow-start-server.yml --diff
-  - ./ci-ssh.sh
-  environment:
-    ANSIBLE_HOST_KEY_CHECKING: false
-    ANSIBLE_FORCE_COLOR: true
-  when:
-    branch:
-    - master
-    - staging
-    event:
-    - push
-
-- name: setup-server
-  pull: default
-  image: timovibritannia/ansible
-  commands:
-  - sleep 120
-  - ./ci-piprequierments.sh
-  - ansible-playbook mailcow-setup-server.yml --private-key /drone/src/id_ssh_rsa --diff
-  environment:
-    ANSIBLE_HOST_KEY_CHECKING: false
-    ANSIBLE_FORCE_COLOR: true
-  when:
-    branch:
-    - master
-    - staging
-    event:
-    - push
-
-- name: run-tests
-  pull: default
-  image: timovibritannia/ansible
-  commands:
-  - ./ci-piprequierments.sh
-  - ansible-playbook mailcow-integration-tests.yml --private-key /drone/src/id_ssh_rsa --diff
-  environment:
-    ANSIBLE_HOST_KEY_CHECKING: false
-    ANSIBLE_FORCE_COLOR: true
-  when:
-    branch:
-    - master
-    - staging
-    event:
-    - push
-
-- name: delete-server
-  pull: default
-  image: timovibritannia/ansible
-  commands:
-  - ./ci-piprequierments.sh
-  - ansible-playbook mailcow-delete-server.yml --diff
-  environment:
-    ANSIBLE_HOST_KEY_CHECKING: false
-    ANSIBLE_FORCE_COLOR: true
-  when:
-    branch:
-    - master
-    - staging
-    event:
-    - push
-    status:
-    - failure
-    - success
-
----
-kind: signature
-hmac: f6619243fe2a27563291c9f2a46d93ffbc3b6dced9a05f23e64b555ce03a31e5
-
-...

+ 2 - 1
.github/workflows/close_old_issues_and_prs.yml

@@ -14,7 +14,7 @@ jobs:
       pull-requests: write
     steps:
       - name: Mark/Close Stale Issues and Pull Requests 🗑️
-        uses: actions/stale@v5.0.0
+        uses: actions/stale@v5.1.1
         with:
           repo-token: ${{ secrets.STALE_ACTION_PAT }}
           days-before-stale: 60
@@ -30,6 +30,7 @@ jobs:
           stale-issue-label: "stale"
           stale-pr-label: "stale"
           exempt-draft-pr: "true"
+          close-issue-reason: "not_planned"
           operations-per-run: "250"
           ascending: "true"
           #DRY-RUN

+ 42 - 0
.github/workflows/image_builds.yml

@@ -0,0 +1,42 @@
+name: Build mailcow Docker Images
+
+on:
+  push:
+    branches: [ "master", "staging" ]
+  workflow_dispatch:
+
+jobs:
+  docker_image_builds:
+    strategy:
+      matrix:
+        images:
+          - "acme-mailcow"
+          - "clamd-mailcow"
+          - "dockerapi-mailcow"
+          - "dovecot-mailcow"
+          - "netfilter-mailcow"
+          - "olefy-mailcow"
+          - "php-fpm-mailcow"
+          - "postfix-mailcow"
+          - "rspamd-mailcow"
+          - "sogo-mailcow"
+          - "solr-mailcow"
+          - "unbound-mailcow"
+          - "watchdog-mailcow"
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Setup Docker
+        run: |
+          curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
+          sudo service docker start
+          sudo curl -L https://github.com/docker/compose/releases/download/v$(curl -Ls https://www.servercow.de/docker-compose/latest.php)/docker-compose-$(uname -s)-$(uname -m) > /usr/local/bin/docker-compose
+          sudo chmod +x /usr/local/bin/docker-compose
+      - name: Prepair Image Builds
+        run: |
+          cp helper-scripts/docker-compose.override.yml.d/BUILD_FLAGS/docker-compose.override.yml docker-compose.override.yml
+      - name: Build Docker Images
+        run: |
+          docker-compose build ${image}
+        env:
+          image: ${{ matrix.images }}

+ 60 - 0
.github/workflows/integration_tests.yml

@@ -0,0 +1,60 @@
+name: mailcow Integration Tests
+
+on:
+  push:
+    branches: [ "master", "staging" ]
+  workflow_dispatch:
+
+jobs:
+  integration_tests:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Setup Ansible
+        run: |
+          export DEBIAN_FRONTEND=noninteractive
+          sudo apt-get update
+          sudo apt-get install python3 python3-pip git
+          sudo pip3 install ansible
+      - name: Prepair Test Environment
+        run: |
+          git clone https://github.com/mailcow/mailcow-integration-tests.git --branch $(curl -sL https://api.github.com/repos/mailcow/mailcow-integration-tests/releases/latest | jq -r '.tag_name') --single-branch .
+          ./fork_check.sh
+          ./ci.sh
+          ./ci-pip-requirements.sh
+        env:
+          VAULT_PW: ${{ secrets.MAILCOW_TESTS_VAULT_PW }}
+          VAULT_FILE: ${{ secrets.MAILCOW_TESTS_VAULT_FILE }}
+      - name: Start Integration Test Server
+        run: |
+          ./fork_check.sh
+          ansible-playbook mailcow-start-server.yml --diff
+        env:
+          PY_COLORS: '1'
+          ANSIBLE_FORCE_COLOR: '1'
+          ANSIBLE_HOST_KEY_CHECKING: 'false'
+      - name: Setup Integration Test Server
+        run: |
+          ./fork_check.sh
+          sleep 30
+          ansible-playbook mailcow-setup-server.yml --private-key id_ssh_rsa --diff
+        env:
+          PY_COLORS: '1'
+          ANSIBLE_FORCE_COLOR: '1'
+          ANSIBLE_HOST_KEY_CHECKING: 'false'
+      - name: Run Integration Tests
+        run: |
+          ./fork_check.sh
+          ansible-playbook mailcow-integration-tests.yml --private-key id_ssh_rsa --diff
+        env:
+          PY_COLORS: '1'
+          ANSIBLE_FORCE_COLOR: '1'
+          ANSIBLE_HOST_KEY_CHECKING: 'false'
+      - name: Delete Integration Test Server
+        if: always()
+        run: |
+          ./fork_check.sh
+          ansible-playbook mailcow-delete-server.yml --diff
+        env:
+          PY_COLORS: '1'
+          ANSIBLE_FORCE_COLOR: '1'
+          ANSIBLE_HOST_KEY_CHECKING: 'false'

+ 17 - 0
.github/workflows/tweet-trigger-publish-release.yml

@@ -0,0 +1,17 @@
+name: "Tweet trigger release"
+on:
+  release:
+    types: [published]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Tweet-trigger-publish-release
+        uses: mugi111/tweet-trigger-release@v1.1
+        with:
+          consumer_key: ${{ secrets.CONSUMER_KEY }}
+          consumer_secret: ${{ secrets.CONSUMER_SECRET }}
+          access_token_key: ${{ secrets.ACCESS_TOKEN_KEY }}
+          access_token_secret: ${{ secrets.ACCESS_TOKEN_SECRET }}
+          tweet_body: 'A new mailcow-dockerized Release has been Released on GitHub! Checkout our GitHub Page for the latest Release: github.com/mailcow/mailcow-dockerized/releases/latest'

+ 0 - 16
.travis.yml

@@ -1,16 +0,0 @@
-sudo: required
-services:
-- docker
-script:
-- echo 'Europe/Berlin' | MAILCOW_HOSTNAME=build.mailcow ./generate_config.sh
-- docker-compose pull --ignore-pull-failures --parallel
-- docker-compose build
-- docker login --username=$DOCKER_HUB_USERNAME --password=$DOCKER_HUB_PASSWORD
-- docker-compose push
-branches:
-  only:
-  - master_disabled
-env:
-  global:
-  - secure: MpxpTwD7f0CNEVLitSpVmocK7O9r+BwFE1deEHK4AlQo/oc9cOlhGe1EL3mx9zbglPmjlDg/8kMUGv6vSirIabfBo9Szjps76bHckFr9lr2Ykkg0e29oC8pgPpSXD1eY/1ZIN/FvIkxpUFLETo1okS/j9q/A0DCGFmti0n3EoMORsgRz9CpNAiEh0zpSd6+euPAGHuczuCrDuO84my9bIOCjA/+aPunHNeXiuM8yIM2SxCSyGtIKT0+jvquIvLF58VxivysXBlRfhDn8fhB09nXA2Ru/derYQACfcmNSn9Pd4bDpebPJW5B9H/XA8xjb58uKinUlncbAMB/QnxoT75j9YRWJZRSQ+34XNYP6ZgK9soZ2TC6djQyEKTUu45Kp/1s+poSn42m9jytJJTmmK0KxsZTRcC8JD5nrjIMZWPUNNTwC5L4+I7ZRWg2WooK3LNyq1Ng8Hn6W77wSgsvAJw2HD3Lx58AprGUhHuBeaIZRuSN9aKwZrl9vKQJLqPnOp/nF2EC6kot5HYYtcotGtETXPUDih21gWD5ZM2BqVqYfQQnJnNMgeYmMdj6QQuTFqhuNJf7hXRIRkTnD3j1gDOLKQZazW0+N2JE8XWDFwi6fKScDsxT85lJti9HmzHa7+k4RVHmUYuDgRoPuzUgjWHvPsiz3/Z8WQ9JYpH84S8w=
-  - secure: fWzZisT6nGDNL4lf6tXB07eFG2drgBakHxzdF/NFVvzuP861RFR6omuL+ED0PgXrEHDJBxaBLv52je8irmUXrAH1CNr7T8DWiZo/h5h609Uzr+38T1NnIu4krL0Wo6/CDwlLKnzqTq9yBIZLQSHVJmo8AOpo1JPIi2ajodqj9ZfmAxDQTQl+G6zvQjtqIkYHsHY7A44Rto0f14ykn7w2S82Jn6Ry89VNI5V1WEO3sMpM/XekNP/HokNcRIuntL/0+kuLvTJ5akGoTjBQxSnSW95opzPeGky74HRU2obExJYqKvF0VfVJRNAqejwjIiFIbbjqV0Sk5391kFuhuBErQQDM1bOHGdxZ41HsJH29qNWIl7C33Yl10qERoqecgsJ1N/bS2ZEmWqm/zQh5GClCXPvYmzEqMYsMGM3vjbKdjDlc1Wh2w/eFclsXN9LSXh1mc35rtj46frcT6e5Kof87AIfC9hTgDvk9kAsyjaHMkSHSZthbZXCIcsD8qriNm5UqfFBYD79mPIP1S2YMQ2jscCsjHOZgYVrcm0kzDF21J1w6H0Lo7d1jw37LYlegBdtLQ9gYgqY2D5m+nxWuVoD5FZmpR+5JGtK+ootyLFF8aiFoHXd4op1JCxRLjgkmnZKXzw3kTQSpE7oa7CgzchtQmK2nqcqla1b5Qk7ilVcjooo=

+ 2 - 1
README.md

@@ -2,7 +2,8 @@
 
 ## We stand with 🇺🇦
 
-[![master build status](https://img.shields.io/drone/build/mailcow/mailcow-dockerized/master?label=master%20build&server=https%3A%2F%2Fdrone.mailcow.email)](https://drone.mailcow.email/mailcow/mailcow-dockerized) [![staging build status](https://img.shields.io/drone/build/mailcow/mailcow-dockerized/staging?label=staging%20build&server=https%3A%2F%2Fdrone.mailcow.email)](https://drone.mailcow.email/mailcow/mailcow-dockerized) [![Translation status](https://translate.mailcow.email/widgets/mailcow-dockerized/-/translation/svg-badge.svg)](https://translate.mailcow.email/engage/mailcow-dockerized/)
+[![Mailcow Integration Tests](https://github.com/mailcow/mailcow-dockerized/actions/workflows/integration_tests.yml/badge.svg?branch=master)](https://github.com/mailcow/mailcow-dockerized/actions/workflows/integration_tests.yml)
+[![Translation status](https://translate.mailcow.email/widgets/mailcow-dockerized/-/translation/svg-badge.svg)](https://translate.mailcow.email/engage/mailcow-dockerized/)
 [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/mailcow_email.svg?style=social&label=Follow%20%40mailcow_email)](https://twitter.com/mailcow_email)
 
 ## Want to support mailcow?

+ 42 - 0
SECURITY.md

@@ -0,0 +1,42 @@
+# Security Policies and Procedures
+
+This document outlines security procedures and general policies for the _mailcow: dockerized_ project as found on [mailcow-dockerized](https://github.com/mailcow/mailcow-dockerized).
+
+  * [Reporting a Vulnerability](#reporting-a-vulnerability)
+  * [Disclosure Policy](#disclosure-policy)
+  * [Comments on this Policy](#comments-on-this-policy)
+
+## Reporting a Vulnerability 
+
+The mailcow team and community take all security vulnerabilities
+seriously. Thank you for improving the security of our open source 
+software. We appreciate your efforts and responsible disclosure and will
+make every effort to acknowledge your contributions.
+
+Report security vulnerabilities by emailing the mailcow team at:
+    
+    info at servercow.de
+
+mailcow team will acknowledge your email as soon as possible, and will
+send a more detailed response afterwards indicating the next steps in 
+handling your report. After the initial reply to your report, the mailcow
+team will endeavor to keep you informed of the progress towards a fix and
+full announcement, and may ask for additional information or guidance.
+
+Report security vulnerabilities in third-party modules to the person or 
+team maintaining the module.
+
+## Disclosure Policy
+
+When the mailcow team receives a security bug report, they will assign it
+to a primary handler. This person will coordinate the fix and release
+process, involving the following steps:
+
+  * Confirm the problem and determine the affected versions.
+  * Audit code to find any potential similar problems.
+  * Prepare fixes for all releases still under maintenance.
+
+## Comments on this Policy
+
+If you have suggestions on how this process could be improved please submit a
+pull request.

+ 8 - 2
create_cold_standby.sh

@@ -1,4 +1,4 @@
-FROM clamav/clamav:0.105.0_base
+FROM clamav/clamav:0.105.1_base
 
 LABEL maintainer "André Peters <andre.peters@servercow.de>"
 
@@ -8,8 +8,14 @@ RUN apk upgrade --no-cache \
   bind-tools \
   bash 
 
-COPY clamd.sh ./
+# init
+COPY clamd.sh /clamd.sh
 RUN chmod +x /sbin/tini
 
+# healthcheck
+COPY healthcheck.sh /healthcheck.sh
+RUN chmod +x /healthcheck.sh
+HEALTHCHECK --start-period=6m CMD "/healthcheck.sh"
+
 ENTRYPOINT []
 CMD ["/sbin/tini", "-g", "--", "/clamd.sh"]

+ 9 - 0
data/Dockerfiles/clamd/healthcheck.sh

@@ -0,0 +1,9 @@
+#!/bin/bash
+
+if [[ "${SKIP_CLAMD}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
+  echo "SKIP_CLAMD=y, skipping ClamAV..."
+  exit 0
+fi
+
+# run clamd healthcheck
+/usr/local/bin/clamdcheck.sh

+ 1 - 1
data/Dockerfiles/dovecot/Dockerfile

@@ -2,7 +2,7 @@ FROM debian:bullseye-slim
 LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 
 ARG DEBIAN_FRONTEND=noninteractive
-ARG DOVECOT=2.3.18
+ARG DOVECOT=2.3.19.1
 ENV LC_ALL C
 ENV GOSU_VERSION 1.14
 

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

@@ -349,6 +349,14 @@ sievec /var/vmail/sieve/global_sieve_after.sieve
 sievec /usr/lib/dovecot/sieve/report-spam.sieve
 sievec /usr/lib/dovecot/sieve/report-ham.sieve
 
+for file in /var/vmail/*/*/sieve/*.sieve ; do
+  if [[ "$file" == "/var/vmail/*/*/sieve/*.sieve" ]]; then
+    continue
+  fi
+  sievec "$file" "$(dirname "$file")/../.dovecot.svbin"
+  chown vmail:vmail "$(dirname "$file")/../.dovecot.svbin"
+done
+
 # 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

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

@@ -194,7 +194,6 @@ plugin {
   fts_solr = url=http://solr:8983/solr/dovecot-fts/
   quota = dict:Userquota::proxy::sqlquota
   quota_rule2 = Trash:storage=+100%%
-  sieve = /var/vmail/sieve/%u.sieve
   sieve_plugins = sieve_imapsieve sieve_extprograms
   sieve_vacation_send_from_recipient = yes
   sieve_redirect_envelope_from = recipient

+ 3 - 0
data/conf/rspamd/local.d/groups.conf

@@ -18,6 +18,9 @@ symbols {
   "ENCRYPTED_CHAT" {
     score = -20.0;
   }
+  "SOGO_CONTACT" {
+    score = -99.0;
+  }
 }
 
 group "MX" {

+ 95 - 51
data/web/api/openapi.yaml

@@ -518,21 +518,23 @@ paths:
                         - domain.tld
                       type: success
               schema:
-                properties:
-                  log:
-                    description: contains request object
-                    items: {}
-                    type: array
-                  msg:
-                    items: {}
-                    type: array
-                  type:
-                    enum:
-                      - success
-                      - danger
-                      - error
-                    type: string
-                type: object
+                type: array
+                items:
+                  type: object
+                  properties:
+                    log:
+                      description: contains request object
+                      items: {}
+                      type: array
+                    msg:
+                      items: {}
+                      type: array
+                    type:
+                      enum:
+                        - success
+                        - danger
+                        - error
+                      type: string
           description: OK
           headers: {}
       tags:
@@ -579,6 +581,11 @@ paths:
                 domain:
                   description: Fully qualified domain name
                   type: string
+                gal:
+                  description: >-
+                    is domain global address list active or not, it enables
+                    shared contacts accross domain in SOGo webmail
+                  type: boolean
                 mailboxes:
                   description: limit count of mailboxes associated with this domain
                   type: number
@@ -596,6 +603,9 @@ paths:
                     if not, them you have to create "dummy" mailbox for each
                     address to relay
                   type: boolean
+                relay_unknown_only:
+                  description: Relay non-existing mailboxes only. Existing mailboxes will be delivered locally.
+                  type: boolean
                 rl_frame:
                   enum:
                     - s
@@ -606,6 +616,11 @@ paths:
                 rl_value:
                   description: rate limit value
                   type: number
+                tags:
+                  description: tags for this Domain
+                  type: array
+                  items:
+                    type: string
               type: object
       summary: Create domain
   /api/v1/add/domain-admin:
@@ -1952,21 +1967,23 @@ paths:
                         - domain2.tld
                       type: success
               schema:
-                properties:
-                  log:
-                    description: contains request object
-                    items: {}
-                    type: array
-                  msg:
-                    items: {}
-                    type: array
-                  type:
-                    enum:
-                      - success
-                      - danger
-                      - error
-                    type: string
-                type: object
+                type: array
+                items:
+                  type: object
+                  properties:
+                    log:
+                      description: contains request object
+                      items: {}
+                      type: array
+                    msg:
+                      items: {}
+                      type: array
+                    type:
+                      enum:
+                        - success
+                        - danger
+                        - error
+                      type: string
           description: OK
           headers: {}
       tags:
@@ -1977,14 +1994,15 @@ paths:
         content:
           application/json:
             schema:
+              type: object
               example:
                 - domain.tld
                 - domain2.tld
               properties:
-                items:
-                  description: contains list of domains you want to delete
-                  type: object
-              type: object
+                items: 
+                  type: array
+                  items:
+                    type: string
       summary: Delete domain
   /api/v1/delete/domain-admin:
     post:
@@ -2972,23 +2990,25 @@ paths:
           $ref: "#/components/responses/Unauthorized"
         "200":
           content:
-            "*/*":
+            application/json:
               schema:
-                properties:
-                  log:
-                    description: contains request object
-                    items: {}
-                    type: array
-                  msg:
-                    items: {}
-                    type: array
-                  type:
-                    enum:
-                      - success
-                      - danger
-                      - error
-                    type: string
-                type: object
+                type: array
+                items: 
+                  type: object
+                  properties:
+                    log:
+                      type: array
+                      description: contains request object
+                      items: {}
+                    msg:
+                      type: array
+                      items: {}
+                    type:
+                      enum:
+                        - success
+                        - danger
+                        - error
+                      type: string
           description: OK
           headers: {}
       tags:
@@ -3056,13 +3076,33 @@ paths:
                         if not, them you have to create "dummy" mailbox for each
                         address to relay
                       type: boolean
+                    relay_unknown_only:
+                      description: Relay non-existing mailboxes only. Existing mailboxes will be delivered locally.
+                      type: boolean
                     relayhost:
                       description: id of relayhost
                       type: number
+                    rl_frame:
+                      enum:
+                        - s
+                        - m
+                        - h
+                        - d
+                      type: string
+                    rl_value:
+                      description: rate limit value
+                      type: number
+                    tags:
+                      description: tags for this Domain
+                      type: array
+                      items:
+                        type: string
                   type: object
                 items:
                   description: contains list of domain names you want update
-                  type: object
+                  type: array
+                  items:
+                    type: string
               type: object
       summary: Update domain
   /api/v1/edit/fail2ban:
@@ -3953,6 +3993,8 @@ paths:
           in: query
           name: tags
           required: false
+          schema:
+            type: string
         - description: e.g. api-key-string
           example: api-key-string
           in: header
@@ -4512,6 +4554,8 @@ paths:
           in: query
           name: tags
           required: false
+          schema:
+            type: string
         - description: e.g. api-key-string
           example: api-key-string
           in: header

+ 1 - 78
data/web/css/build/013-mailcow.css

@@ -270,27 +270,6 @@ code {
 .flag-icon {
   margin-right: 5px;
 }
-.dropdown-header {
-  font-weight: 600;
-}
-
-.dataTables_info {
-  margin: 15px 0 !important;
-  padding: 0px !important;
-}
-.dataTables_paginate, .dataTables_length, .dataTables_filter {
-  margin: 15px 0 !important;
-}
-.dtr-details {
-  width: 100%;
-}
-.dtr-title {
-  width: 20%;
-}
-table.dataTable>tbody>tr.child ul.dtr-details>li {
-  border-bottom: 1px solid rgba(239, 239, 239, 0.129);
-  padding: 0.5em 0;
-}
 
 .tag-box {
   display: flex;
@@ -328,6 +307,7 @@ table.dataTable>tbody>tr.child ul.dtr-details>li {
   align-items: center;
   display: inline-flex;
 }
+
 #dnstable {
   overflow-x: auto!important;
 }
@@ -335,61 +315,4 @@ table.dataTable>tbody>tr.child ul.dtr-details>li {
   border: 1px solid #dfdfdf;
   background-color: #f9f9f9;
   padding: 10px;
-}
-
-
-table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover, 
-table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
-  background-color: #5e5e5e;
-}
-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 {
-  background-color: #979797 !important;
-  border: 1.5px solid #616161 !important;
-  border-radius: 2px !important;
-  color: #fff;
-  height: 1em;
-  width: 1em;
-  line-height: 1.25em;
-  border-radius: 0px;
-  box-shadow: none;
-  font-size: 14px;
-  transition: 0.5s all;
-}
-table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before, 
-table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before,
-table.dataTable td.dt-control:before {
-  background-color: #979797 !important;
-}
-table.dataTable.dtr-inline.collapsed>tbody>tr>td.child, 
-table.dataTable.dtr-inline.collapsed>tbody>tr>th.child, 
-table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
-  background-color: #fbfbfb;
-}
-table.dataTable.table-striped>tbody>tr>td {
-  vertical-align: middle;
-}
-table.dataTable.table-striped>tbody>tr>td>input[type="checkbox"] {
-  margin-top: 7px;
-}
-
-
-.btn-check-label {
-  color: #555;
-}
-
-.caret {
-  transform: rotate(0deg);
-}
-a[aria-expanded='true'] > .caret, 
-button[aria-expanded='true'] > .caret {
-  transform: rotate(-180deg);
-}
-
-.list-group-details {
-  background: #fff;
-}
-.list-group-header {
-  background: #f7f7f7;
 }

+ 1 - 1
data/web/inc/ajax/destroy_tfa_auth.php

@@ -2,5 +2,5 @@
 session_start();
 unset($_SESSION['pending_mailcow_cc_username']);
 unset($_SESSION['pending_mailcow_cc_role']);
-unset($_SESSION['pending_tfa_method']);
+unset($_SESSION['pending_tfa_methods']);
 ?>

+ 30 - 5
data/web/inc/footer.inc.php

@@ -23,18 +23,43 @@ if (is_array($alertbox_log_parser)) {
   unset($_SESSION['return']);
 }
 
+// map tfa details for twig
+$pending_tfa_authmechs = [];
+foreach($_SESSION['pending_tfa_methods'] as $authdata){
+  $pending_tfa_authmechs[$authdata['authmech']] = false;
+}
+if (isset($pending_tfa_authmechs['webauthn'])) {
+  $pending_tfa_authmechs['webauthn'] = true;
+}
+if (!isset($pending_tfa_authmechs['webauthn']) 
+    && isset($pending_tfa_authmechs['yubi_otp'])) {
+  $pending_tfa_authmechs['yubi_otp'] = true;
+}
+if (!isset($pending_tfa_authmechs['webauthn']) 
+    && !isset($pending_tfa_authmechs['yubi_otp'])
+    && isset($pending_tfa_authmechs['totp'])) {
+  $pending_tfa_authmechs['totp'] = true;
+}
+if (isset($pending_tfa_authmechs['u2f'])) {
+  $pending_tfa_authmechs['u2f'] = true;
+}
+
 // globals
 $globalVariables = [
   'mailcow_info' => array(
     'version_tag' => $GLOBALS['MAILCOW_GIT_VERSION'],
     'last_version_tag' => $GLOBALS['MAILCOW_LAST_GIT_VERSION'],
-    'project_url' => $GLOBALS['MAILCOW_GIT_URL'],
-    'project_owner' => $GLOBALS['MAILCOW_GIT_OWNER'],
-    'project_repo' => $GLOBALS['MAILCOW_GIT_REPO'],
-    'updatedAt' => $GLOBALS['MAILCOW_UPDATEDAT']
+    'git_owner' => $GLOBALS['MAILCOW_GIT_OWNER'],
+    'git_repo' => $GLOBALS['MAILCOW_GIT_REPO'],
+    'git_project_url' => $GLOBALS['MAILCOW_GIT_URL'],
+    'git_commit' => $GLOBALS['MAILCOW_GIT_COMMIT'],
+    'git_commit_date' => $GLOBALS['MAILCOW_GIT_COMMIT_DATE'],
+    'mailcow_branch' => $GLOBALS['MAILCOW_BRANCH'],
+    'updated_at' => $GLOBALS['MAILCOW_UPDATEDAT']
   ),
   'js_path' => '/cache/'.basename($JSPath),
-  'pending_tfa_method' => @$_SESSION['pending_tfa_method'],
+  'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'],
+  'pending_tfa_authmechs' => $pending_tfa_authmechs,
   'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'],
   'lang_footer' => json_encode($lang['footer']),
   'lang_acl' => json_encode($lang['acl']),

+ 253 - 184
data/web/inc/functions.inc.php

@@ -833,11 +833,15 @@ function check_login($user, $pass, $app_passwd_data = false) {
   $stmt->execute(array(':user' => $user));
   $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
   foreach ($rows as $row) {
+    // verify password
     if (verify_hash($row['password'], $pass)) {
-      if (get_tfa($user)['name'] != "none") {
+      // 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_method'] = get_tfa($user)['name'];
+        $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
         unset($_SESSION['ldelay']);
         $_SESSION['return'][] =  array(
           'type' => 'info',
@@ -845,8 +849,7 @@ function check_login($user, $pass, $app_passwd_data = false) {
           'msg' => 'awaiting_tfa_confirmation'
         );
         return "pending";
-      }
-      else {
+      } 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");
@@ -869,11 +872,14 @@ function check_login($user, $pass, $app_passwd_data = false) {
   $stmt->execute(array(':user' => $user));
   $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
   foreach ($rows as $row) {
+    // verify password
     if (verify_hash($row['password'], $pass) !== false) {
-      if (get_tfa($user)['name'] != "none") {
+      // 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_method'] = get_tfa($user)['name'];
+        $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
         unset($_SESSION['ldelay']);
         $_SESSION['return'][] =  array(
           'type' => 'info',
@@ -933,24 +939,47 @@ function check_login($user, $pass, $app_passwd_data = false) {
     $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
   }
   foreach ($rows as $row) {
-    if (verify_hash($row['password'], $pass) !== false) {
-      unset($_SESSION['ldelay']);
-      $_SESSION['return'][] =  array(
-        'type' => 'success',
-        'log' => array(__FUNCTION__, $user, '*'),
-        'msg' => array('logged_in_as', $user)
-      );
-      if ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {
-        $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV';
-        $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' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'])
-        ));
+    // verify password
+    if ($app_passwd_data['eas'] !== true && $app_passwd_data['dav'] !== true){
+      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'] = "user";
+          $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
+          unset($_SESSION['ldelay']);
+          $_SESSION['return'][] =  array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $user, '*'),
+            'msg' => array('logged_in_as', $user)
+          );
+          return "pending";
+        } else {
+          // 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));
+
+          unset($_SESSION['ldelay']);
+          return "user";
+        }
+      }
+    } elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {
+      if (array_key_exists("app_passwd_id", $row)){
+        if (verify_hash($row['password'], $pass) !== false) {
+          $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV';
+          $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' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'])
+          ));
+
+          unset($_SESSION['ldelay']);
+          return "user";
+        }
       }
-      return "user";
     }
   }
 
@@ -1145,47 +1174,46 @@ function set_tfa($_data) {
   global $yubi;
   global $tfa;
   $_data_log = $_data;
+  $access_denied = null;
   !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
   $username = $_SESSION['mailcow_cc_username'];
-  if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
-      $_SESSION['return'][] =  array(
-        'type' => 'danger',
-        'log' => array(__FUNCTION__, $_data_log),
-        'msg' => 'access_denied'
-      );
-      return false;
-  }
-  $stmt = $pdo->prepare("SELECT `password` FROM `admin`
-      WHERE `username` = :username");
-  $stmt->execute(array(':username' => $username));
-  $row = $stmt->fetch(PDO::FETCH_ASSOC);
-  $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-  if (!empty($num_results)) {
-    if (!verify_hash($row['password'], $_data["confirm_password"])) {
-      $_SESSION['return'][] =  array(
-        'type' => 'danger',
-        'log' => array(__FUNCTION__, $_data_log),
-        'msg' => 'access_denied'
-      );
-      return false;
+
+  // check for empty user and role
+  if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
+
+  // check admin confirm password
+  if ($access_denied === null) {
+    $stmt = $pdo->prepare("SELECT `password` FROM `admin`
+        WHERE `username` = :username");
+    $stmt->execute(array(':username' => $username));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    if ($row) {
+      if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
+      else $access_denied = false;
     }
   }
-  $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
-      WHERE `username` = :username");
-  $stmt->execute(array(':username' => $username));
-  $row = $stmt->fetch(PDO::FETCH_ASSOC);
-  $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
-  if (!empty($num_results)) {
-    if (!verify_hash($row['password'], $_data["confirm_password"])) {
-      $_SESSION['return'][] =  array(
-        'type' => 'danger',
-        'log' => array(__FUNCTION__, $_data_log),
-        'msg' => 'access_denied'
-      );
-      return false;
+
+  // check mailbox confirm password
+  if ($access_denied === null) {
+    $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
+        WHERE `username` = :username");
+    $stmt->execute(array(':username' => $username));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+    if ($row) {
+      if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
+      else $access_denied = false;
     }
   }
 
+  // set access_denied error
+  if ($access_denied){
+    $_SESSION['return'][] =  array(
+      'type' => 'danger',
+      'log' => array(__FUNCTION__, $_data_log),
+      'msg' => 'access_denied'
+    );
+    return false;
+  }
 
   switch ($_data["tfa_method"]) {
     case "yubi_otp":
@@ -1223,8 +1251,7 @@ function set_tfa($_data) {
         $yubico_modhex_id = substr($_data["otp_token"], 0, 12);
         $stmt = $pdo->prepare("DELETE FROM `tfa`
           WHERE `username` = :username
-            AND (`authmech` != 'yubi_otp')
-            OR (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
+            AND (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
         $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
         $stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES
           (:key_id, :username, 'yubi_otp', '1', :secret)");
@@ -1268,9 +1295,6 @@ function set_tfa($_data) {
     case "webauthn":
         $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
 
-        $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `authmech` != 'webauthn'");
-        $stmt->execute(array(':username' => $username));
-
         $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`)
         VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')");
         $stmt->execute(array(
@@ -1442,25 +1466,27 @@ function unset_tfa_key($_data) {
   global $pdo;
   global $lang;
   $_data_log = $_data;
+  $access_denied = null;
   $id = intval($_data['unset_tfa_key']);
   $username = $_SESSION['mailcow_cc_username'];
-  if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
-    $_SESSION['return'][] =  array(
-      'type' => 'danger',
-      'log' => array(__FUNCTION__, $_data_log),
-      'msg' => 'access_denied'
-    );
-    return false;
-  }
+
+  // check for empty user and role
+  if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
+
   try {
-    if (!is_numeric($id)) {
-      $_SESSION['return'][] =  array(
+    if (!is_numeric($id)) $access_denied = true;
+    
+    // set access_denied error
+    if ($access_denied){
+      $_SESSION['return'][] = array(
         'type' => 'danger',
         'log' => array(__FUNCTION__, $_data_log),
         'msg' => 'access_denied'
       );
       return false;
-    }
+    } 
+
+    // check if it's last key
     $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
       WHERE `username` = :username AND `active` = '1'");
     $stmt->execute(array(':username' => $username));
@@ -1473,6 +1499,8 @@ function unset_tfa_key($_data) {
       );
       return false;
     }
+
+    // delete key
     $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id");
     $stmt->execute(array(':username' => $username, ':id' => $id));
     $_SESSION['return'][] =  array(
@@ -1490,7 +1518,7 @@ function unset_tfa_key($_data) {
     return false;
   }
 }
-function get_tfa($username = null) {
+function get_tfa($username = null, $id = null) {
   global $pdo;
   if (isset($_SESSION['mailcow_cc_username'])) {
     $username = $_SESSION['mailcow_cc_username'];
@@ -1498,95 +1526,119 @@ function get_tfa($username = null) {
   elseif (empty($username)) {
     return false;
   }
-  $stmt = $pdo->prepare("SELECT * FROM `tfa`
-      WHERE `username` = :username AND `active` = '1'");
-  $stmt->execute(array(':username' => $username));
-  $row = $stmt->fetch(PDO::FETCH_ASSOC);
 
-  if (isset($row["authmech"])) {
-    switch ($row["authmech"]) {
-      case "yubi_otp":
-        $data['name'] = "yubi_otp";
-        $data['pretty'] = "Yubico OTP";
-        $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username");
-        $stmt->execute(array(
-          ':username' => $username,
-        ));
-        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-        while($row = array_shift($rows)) {
-          $data['additional'][] = $row;
-        }
-        return $data;
-      break;
-      // u2f - deprecated, should be removed
-      case "u2f":
-        $data['name'] = "u2f";
-        $data['pretty'] = "Fido U2F";
-        $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
-        $stmt->execute(array(
-          ':username' => $username,
-        ));
-        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-        while($row = array_shift($rows)) {
-          $data['additional'][] = $row;
-        }
-        return $data;
-      break;
-      case "hotp":
-        $data['name'] = "hotp";
-        $data['pretty'] = "HMAC-based OTP";
-        return $data;
-      break;
-      case "totp":
-        $data['name'] = "totp";
-        $data['pretty'] = "Time-based OTP";
-        $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username");
-        $stmt->execute(array(
-          ':username' => $username,
-        ));
-        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-        while($row = array_shift($rows)) {
-          $data['additional'][] = $row;
-        }
+  if (!isset($id)){
+    // fetch all tfa methods - just get information about possible authenticators
+    $stmt = $pdo->prepare("SELECT `id`, `key_id`, `authmech` FROM `tfa`
+        WHERE `username` = :username AND `active` = '1'");
+    $stmt->execute(array(':username' => $username));
+    $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ 
+    // no tfa methods found
+    if (count($results) == 0) {
+        $data['name'] = 'none';
+        $data['pretty'] = "-";
+        $data['additional'] = array();
         return $data;
-      break;
-      case "webauthn":
-        $data['name'] = "webauthn";
-        $data['pretty'] = "WebAuthn";
-        $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username");
-        $stmt->execute(array(
-          ':username' => $username,
-        ));
-        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-        while($row = array_shift($rows)) {
-          $data['additional'][] = $row;
+    }
+
+    $data['additional'] = $results;
+    return $data;
+  } else {
+    // fetch specific authenticator details by id
+    $stmt = $pdo->prepare("SELECT * FROM `tfa`
+    WHERE `username` = :username AND `id` = :id AND `active` = '1'");
+    $stmt->execute(array(':username' => $username, ':id' => $id));
+    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+    if (isset($row["authmech"])) {
+        switch ($row["authmech"]) {
+          case "yubi_otp":
+            $data['name'] = "yubi_otp";
+            $data['pretty'] = "Yubico OTP";
+            $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username AND `id` = :id");
+            $stmt->execute(array(
+              ':username' => $username,
+              ':id' => $id
+            ));
+            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+            while($row = array_shift($rows)) {
+              $data['additional'][] = $row;
+            }
+            return $data;
+          break;
+          // u2f - deprecated, should be removed
+          case "u2f":
+            $data['name'] = "u2f";
+            $data['pretty'] = "Fido U2F";
+            $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username AND `id` = :id");
+            $stmt->execute(array(
+              ':username' => $username,
+              ':id' => $id
+            ));
+            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+            while($row = array_shift($rows)) {
+              $data['additional'][] = $row;
+            }
+            return $data;
+          break;
+          case "hotp":
+            $data['name'] = "hotp";
+            $data['pretty'] = "HMAC-based OTP";
+            return $data;
+          break;
+          case "totp":
+            $data['name'] = "totp";
+            $data['pretty'] = "Time-based OTP";
+            $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username AND `id` = :id");
+            $stmt->execute(array(
+              ':username' => $username,
+              ':id' => $id
+            ));
+            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+            while($row = array_shift($rows)) {
+              $data['additional'][] = $row;
+            }
+            return $data;
+          break;
+          case "webauthn":
+            $data['name'] = "webauthn";
+            $data['pretty'] = "WebAuthn";
+            $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username AND `id` = :id");
+            $stmt->execute(array(
+              ':username' => $username,
+              ':id' => $id
+            ));
+            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+            while($row = array_shift($rows)) {
+              $data['additional'][] = $row;
+            }
+            return $data;
+          break;
+          default:
+            $data['name'] = 'none';
+            $data['pretty'] = "-";
+            return $data;
+          break;
         }
-        return $data;
-      break;
-      default:
+      }
+      else {
         $data['name'] = 'none';
         $data['pretty'] = "-";
         return $data;
-      break;
+      }
     }
-  }
-  else {
-    $data['name'] = 'none';
-    $data['pretty'] = "-";
-    return $data;
-  }
 }
-function verify_tfa_login($username, $_data, $WebAuthn) {
-    global $pdo;
-    global $yubi;
-    global $u2f;
-    global $tfa;
-    $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`
-        WHERE `username` = :username AND `active` = '1'");
-    $stmt->execute(array(':username' => $username));
-    $row = $stmt->fetch(PDO::FETCH_ASSOC);
+function verify_tfa_login($username, $_data) {
+  global $pdo;
+  global $yubi;
+  global $u2f;
+  global $tfa;
+  global $WebAuthn;
+
+  if ($_data['tfa_method'] != 'u2f'){
 
-    switch ($row["authmech"]) {
+    switch ($_data["tfa_method"]) {
         case "yubi_otp":
             if (!ctype_alnum($_data['token']) || strlen($_data['token']) != 44) {
                 $_SESSION['return'][] =  array(
@@ -1600,7 +1652,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
             $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
                 WHERE `username` = :username
                 AND `authmech` = 'yubi_otp'
-                AND `active`='1'
+                AND `active` = '1'
                 AND `secret` LIKE :modhex");
             $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
             $row = $stmt->fetch(PDO::FETCH_ASSOC);
@@ -1635,15 +1687,16 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
             return false;
         break;
         case "totp":
-            try {
+          try {
             $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
                 WHERE `username` = :username
                 AND `authmech` = 'totp'
+                AND `id` = :id
                 AND `active`='1'");
-            $stmt->execute(array(':username' => $username));
+            $stmt->execute(array(':username' => $username, ':id' => $_data['id']));
             $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
             foreach ($rows as $row) {
-                if ($tfa->verifyCode($row['secret'], $_data['token']) === true) {
+              if ($tfa->verifyCode($row['secret'], $_data['token']) === true) {
                 $_SESSION['tfa_id'] = $row['id'];
                 $_SESSION['return'][] =  array(
                     'type' => 'success',
@@ -1651,7 +1704,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
                     'msg' => 'verified_totp_login'
                 );
                 return true;
-                }
+              }
             }
             $_SESSION['return'][] =  array(
                 'type' => 'danger',
@@ -1659,23 +1712,16 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
                 'msg' => 'totp_verification_failed'
             );
             return false;
-            }
-            catch (PDOException $e) {
+          }
+          catch (PDOException $e) {
             $_SESSION['return'][] =  array(
                 'type' => 'danger',
                 'log' => array(__FUNCTION__, $username, '*'),
                 'msg' => array('mysql_error', $e)
             );
             return false;
-            }
+          }
         break;
-        // u2f - deprecated, should be removed
-        case "u2f":
-            // delete old keys that used u2f
-            $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = :authmech AND `username` = :username");
-            $stmt->execute(array(':authmech' => 'u2f', ':username' => $username));
-
-            return true;
         case "webauthn":
             $tokenData = json_decode($_data['token']);
             $clientDataJSON = base64_decode($tokenData->clientDataJSON);
@@ -1684,13 +1730,20 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
             $id = base64_decode($tokenData->id);
             $challenge = $_SESSION['challenge'];
 
-            $stmt = $pdo->prepare("SELECT `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `keyHandle` = :tokenId");
-            $stmt->execute(array(':tokenId' => $tokenData->id));
+            $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `id` = :id AND `active`='1'");
+            $stmt->execute(array(':id' => $_data['id']));
             $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC);
 
-            if (empty($process_webauthn) || empty($process_webauthn['publicKey']) || empty($process_webauthn['username'])) return false;
+            if (empty($process_webauthn)){
+              $_SESSION['return'][] =  array(
+                  'type' => 'danger',
+                  'log' => array(__FUNCTION__, $username, '*'),
+                  'msg' => array('webauthn_verification_failed', 'authenticator not found')
+              );
+              return false;
+            } 
             
-            if ($process_webauthn['publicKey'] === false) {
+            if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) {
                 $_SESSION['return'][] =  array(
                     'type' => 'danger',
                     'log' => array(__FUNCTION__, $username, '*'),
@@ -1698,6 +1751,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
                 );
                 return false;
             }
+
             try {
                 $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
             }
@@ -1710,26 +1764,31 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
                 return false;
             }
 
-            
             $stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username");
             $stmt->execute(array(':username' => $process_webauthn['username']));
             $obj_props = $stmt->fetch(PDO::FETCH_ASSOC);
             if ($obj_props['superadmin'] === 1) {
-                $_SESSION["mailcow_cc_role"] = "admin";
+              $_SESSION["mailcow_cc_role"] = "admin";
             }
             elseif ($obj_props['superadmin'] === 0) {
-                $_SESSION["mailcow_cc_role"] = "domainadmin";
+              $_SESSION["mailcow_cc_role"] = "domainadmin";
             }
             else {
-                $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username");
-                $stmt->execute(array(':username' => $process_webauthn['username']));
-                $row = $stmt->fetch(PDO::FETCH_ASSOC);
-                if ($row['username'] == $process_webauthn['username']) {
+              $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username");
+              $stmt->execute(array(':username' => $process_webauthn['username']));
+              $row = $stmt->fetch(PDO::FETCH_ASSOC);
+              if (!empty($row['username'])) {
                 $_SESSION["mailcow_cc_role"] = "user";
-                }
+              } else {
+                $_SESSION['return'][] =  array(
+                  'type' => 'danger',
+                  'log' => array(__FUNCTION__, $username, '*'),
+                  'msg' => array('webauthn_verification_failed', 'could not determine user role')
+                );
+                return false;
+              }
             }
 
-        
             if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
                 $_SESSION['return'][] =  array(
                     'type' => 'danger',
@@ -1739,9 +1798,8 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
                 return false;
             }
 
-
             $_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
-            $_SESSION['tfa_id'] = $process_webauthn['key_id'];
+            $_SESSION['tfa_id'] = $process_webauthn['id'];
             $_SESSION['authReq'] = null;
             unset($_SESSION["challenge"]);
             $_SESSION['return'][] =  array(
@@ -1762,6 +1820,17 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
     }
 
     return false;
+  } else {
+    // delete old keys that used u2f
+    $stmt = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
+    $stmt->execute(array(':username' => $username));
+    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+    if (count($rows) == 0) return false;
+
+    $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
+    $stmt->execute(array(':username' => $username));
+    return true;
+  }
 }
 function admin_api($access, $action, $data = null) {
   global $pdo;

+ 62 - 5
data/web/inc/functions.mailbox.inc.php

@@ -336,9 +336,37 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
           $mins_interval        = $_data['mins_interval'];
           $enc1                 = $_data['enc1'];
           $custom_params        = (empty(trim($_data['custom_params']))) ? '' : trim($_data['custom_params']);
-          // Workaround, fixme
-          if (stripos($custom_params, 'pipemess') || stripos($custom_params, 'pipemes')) {
-            $custom_params = '';
+
+          // validate custom params
+          foreach (explode('-', $custom_params) as $param){
+            if(empty($param)) continue;
+
+            // extract option
+            if (str_contains($param, '=')) $param = explode('=', $param)[0];
+            else $param = rtrim($param, ' ');
+            // remove first char if first char is -
+            if ($param[0] == '-') $param = ltrim($param, $param[0]);
+
+            if (str_contains($param, ' ')) {
+              // bad char
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                'msg' => 'bad character SPACE'
+              );
+              return false;
+            }
+
+            // check if param is whitelisted
+            if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){
+              // bad option
+              $_SESSION['return'][] = array(
+                'type' => 'danger',
+                'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                'msg' => 'bad option '. $param
+              );
+              return false;
+            }
           }
           if (empty($subfolder2)) {
             $subfolder2 = "";
@@ -1764,8 +1792,37 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
               );
               continue;
             }
-            if (stripos($custom_params, 'pipemess') || stripos($custom_params, 'pipemes')) {
-              $custom_params = '';
+
+            // validate custom params
+            foreach (explode('-', $custom_params) as $param){
+              if(empty($param)) continue;
+
+              // extract option
+              if (str_contains($param, '=')) $param = explode('=', $param)[0];
+              else $param = rtrim($param, ' ');
+              // remove first char if first char is -
+              if ($param[0] == '-') $param = ltrim($param, $param[0]);
+
+              if (str_contains($param, ' ')) {
+                // bad char
+                $_SESSION['return'][] = array(
+                  'type' => 'danger',
+                  'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                  'msg' => 'bad character SPACE'
+                );
+                return false;
+              }
+  
+              // check if param is whitelisted
+              if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){
+                // bad option
+                $_SESSION['return'][] = array(
+                  'type' => 'danger',
+                  'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
+                  'msg' => 'bad option '. $param
+                );
+                return false;
+              }
             }
             if (empty($subfolder2)) {
               $subfolder2 = "";

+ 14 - 6
data/web/inc/init_db.inc.php

@@ -3,7 +3,7 @@ function init_db_schema() {
   try {
     global $pdo;
 
-    $db_version = "20052022_0938";
+    $db_version = "25072022_2300";
 
     $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
     $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -440,7 +440,7 @@ function init_db_schema() {
           "spam_score" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "delimiter_action" => "TINYINT(1) NOT NULL DEFAULT '1'",
-          "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'",
+          "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
           "sogo_profile_reset" => "TINYINT(1) NOT NULL DEFAULT '0'",
           "pushover" => "TINYINT(1) NOT NULL DEFAULT '1'",
@@ -738,8 +738,8 @@ function init_db_schema() {
           "username" => "VARCHAR(255) NOT NULL",
           "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')",
           "secret" => "VARCHAR(255) DEFAULT NULL",
-          "keyHandle" => "VARCHAR(255) DEFAULT NULL",
-          "publicKey" => "VARCHAR(255) DEFAULT NULL",
+          "keyHandle" => "VARCHAR(1023) DEFAULT NULL",
+          "publicKey" => "VARCHAR(4096) DEFAULT NULL",
           "counter" => "INT NOT NULL DEFAULT '0'",
           "certificate" => "TEXT",
           "active" => "TINYINT(1) NOT NULL DEFAULT '0'"
@@ -1227,8 +1227,16 @@ function init_db_schema() {
       $pdo->query($create);
     }
     
-    // Mitigate imapsync pipemess issue
-    $pdo->query("UPDATE `imapsync` SET `custom_params` = '' WHERE `custom_params` LIKE '%pipemess%' OR `custom_params` LIKE '%pipemes%';");
+    // Mitigate imapsync argument injection issue
+    $pdo->query("UPDATE `imapsync` SET `custom_params` = '' 
+      WHERE `custom_params` LIKE '%pipemess%' 
+        OR custom_params LIKE '%skipmess%' 
+        OR custom_params LIKE '%delete2foldersonly%' 
+        OR custom_params LIKE '%delete2foldersbutnot%' 
+        OR custom_params LIKE '%regexflag%' 
+        OR custom_params LIKE '%pipemess%' 
+        OR custom_params LIKE '%regextrans2%' 
+        OR custom_params LIKE '%maxlinelengthcmd%';");
     
     // Migrate webauthn tfa
     $stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')");

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

@@ -51,8 +51,9 @@ $qrprovider = new RobThree\Auth\Providers\Qr\QRServerProvider();
 $tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider);
 
 // FIDO2
+$server_name = parse_url('https://' . $_SERVER['HTTP_HOST'], PHP_URL_HOST);
 $formats = $GLOBALS['FIDO2_FORMATS'];
-$WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $_SERVER['HTTP_HOST'], $formats);
+$WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $server_name, $formats);
 // only include root ca's when needed
 if (getenv('WEBAUTHN_ONLY_TRUSTED_VENDORS') == 'y') $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates');
 

+ 16 - 15
data/web/inc/triggers.inc.php

@@ -1,24 +1,24 @@
 <?php
 if (isset($_POST["verify_tfa_login"])) {
-  if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST, $WebAuthn)) {
+  if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) {
     $_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];
     $_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role'];
     unset($_SESSION['pending_mailcow_cc_username']);
     unset($_SESSION['pending_mailcow_cc_role']);
-    unset($_SESSION['pending_tfa_method']);
+    unset($_SESSION['pending_tfa_methods']);
 	
     header("Location: /user");
   } else {
     unset($_SESSION['pending_mailcow_cc_username']);
     unset($_SESSION['pending_mailcow_cc_role']);
-    unset($_SESSION['pending_tfa_method']);
+    unset($_SESSION['pending_tfa_methods']);
   }
 }
 
 if (isset($_GET["cancel_tfa_login"])) {
     unset($_SESSION['pending_mailcow_cc_username']);
     unset($_SESSION['pending_mailcow_cc_role']);
-    unset($_SESSION['pending_tfa_method']);
+    unset($_SESSION['pending_tfa_methods']);
 
     header("Location: /");
 }
@@ -34,6 +34,7 @@ if (isset($_POST["quick_delete"])) {
 if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
 	$login_user = strtolower(trim($_POST["login_user"]));
 	$as = check_login($login_user, $_POST["pass_user"]);
+  
 	if ($as == "admin") {
 		$_SESSION['mailcow_cc_username'] = $login_user;
 		$_SESSION['mailcow_cc_role'] = "admin";
@@ -47,22 +48,22 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
 	elseif ($as == "user") {
 		$_SESSION['mailcow_cc_username'] = $login_user;
 		$_SESSION['mailcow_cc_role'] = "user";
-    $http_parameters = explode('&', $_SESSION['index_query_string']);
-    unset($_SESSION['index_query_string']);
-    if (in_array('mobileconfig', $http_parameters)) {
-      if (in_array('only_email', $http_parameters)) {
-        header("Location: /mobileconfig.php?email_only");
-        die();
-      }
-      header("Location: /mobileconfig.php");
-      die();
-    }
+        $http_parameters = explode('&', $_SESSION['index_query_string']);
+        unset($_SESSION['index_query_string']);
+        if (in_array('mobileconfig', $http_parameters)) {
+            if (in_array('only_email', $http_parameters)) {
+                header("Location: /mobileconfig.php?email_only");
+                die();
+            }
+            header("Location: /mobileconfig.php");
+            die();
+        }
 		header("Location: /user");
 	}
 	elseif ($as != "pending") {
     unset($_SESSION['pending_mailcow_cc_username']);
     unset($_SESSION['pending_mailcow_cc_role']);
-    unset($_SESSION['pending_tfa_method']);
+    unset($_SESSION['pending_tfa_methods']);
 		unset($_SESSION['mailcow_cc_username']);
 		unset($_SESSION['mailcow_cc_role']);
 	}

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

@@ -226,3 +226,131 @@ $RSPAMD_MAPS = array(
     'Monitoring Hosts' => 'monitoring_nolog.map'
   )
 );
+
+
+$IMAPSYNC_OPTIONS = array(
+  'whitelist' => array(
+    'authmech1',
+    'authmech2',
+    'authuser1', 
+    'authuser2', 
+    'debugcontent', 
+    'disarmreadreceipts', 
+    'logdir',
+    'debugcrossduplicates', 
+    'maxsize',
+    'minsize',
+    'minage',
+    'search', 
+    'noabletosearch', 
+    'pidfile', 
+    'pidfilelocking', 
+    'search1',
+    'search2', 
+    'sslargs1',
+    'sslargs2', 
+    'syncduplicates',
+    'usecache', 
+    'synclabels', 
+    'truncmess',  
+    'domino2',  
+    'expunge1',  
+    'filterbuggyflags',  
+    'justconnect',  
+    'justfolders',  
+    'maxlinelength',
+    'useheader',  
+    'noabletosearch1',  
+    'nolog',  
+    'prefix1',
+    'prefix2',
+    'sep1',
+    'sep2',
+    'nofoldersizesatend',
+    'justfoldersizes',  
+    'proxyauth1',  
+    'skipemptyfolders',
+    'include',
+    'subfolder1',
+    'subscribed',
+    'subscribe',   
+    'debug',   
+    'debugimap2',   
+    'domino1',   
+    'exchange1',   
+    'exchange2',   
+    'justlogin',   
+    'keepalive1',   
+    'keepalive2',   
+    'noabletosearch2',   
+    'noexpunge2',   
+    'noresyncflags',   
+    'nossl1',   
+    'nouidexpunge2',   
+    'syncinternaldates',
+    'idatefromheader',   
+    'useuid',    
+    'debugflags',    
+    'debugimap',    
+    'delete1emptyfolders',
+    'delete2folders',    
+    'gmail2',    
+    'office1',    
+    'testslive6',     
+    'debugimap1',     
+    'errorsmax',
+    'tests',     
+    'gmail1',     
+    'maxmessagespersecond',
+    'maxbytesafter',
+    'maxsleep',
+    'abort',     
+    'resyncflags',     
+    'resynclabels',     
+    'syncacls',
+    'nosyncacls',      
+    'nousecache',      
+    'office2',      
+    'testslive',       
+    'debugmemory',       
+    'exitwhenover',
+    'noid',       
+    'noexpunge1',        
+    'authmd51',        
+    'logfile',        
+    'proxyauth2',         
+    'domain1',
+    'domain2',
+    'oauthaccesstoken1',
+    'oauthaccesstoken2',
+    'oauthdirect1',
+    'oauthdirect2',
+    'folder',
+    'folderrec',
+    'folderfirst',
+    'folderlast',
+    'nomixfolders',          
+    'authmd52',           
+    'debugfolders',            
+    'nossl2',            
+    'ssl2',            
+    'tls2',             
+    'notls2',              
+    'debugssl',              
+    'notls1', 
+    'inet4',
+    'inet6',
+    'log',
+    'showpasswords'
+  ),
+  'blacklist' => array(
+    'skipmess',
+    'delete2foldersonly',
+    'delete2foldersbutnot',
+    'regexflag',
+    'regexmess',
+    'pipemess',
+    'regextrans2',
+    'maxlinelengthcmd'
+  )
+);

+ 28 - 19
data/web/json_api.php

@@ -178,15 +178,22 @@ if (isset($_GET['query'])) {
               // parse post data
               $post = trim(file_get_contents('php://input'));
               if ($post) $post = json_decode($post);
-
-              // decode base64 strings
-              $clientDataJSON = base64_decode($post->clientDataJSON);
-              $attestationObject = base64_decode($post->attestationObject);             
               
               // process registration data from authenticator
               try {
+                // decode base64 strings
+                $clientDataJSON = base64_decode($post->clientDataJSON);
+                $attestationObject = base64_decode($post->attestationObject);   
+
                 // processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true)
                 $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $_SESSION['challenge'], false, true);
+
+                // safe authenticator in mysql `tfa` table
+                $_data['tfa_method'] = $post->tfa_method;
+                $_data['key_id'] = $post->key_id;
+                $_data['confirm_password'] = $post->confirm_password;
+                $_data['registration'] = $data;
+                set_tfa($_data);
               }
               catch (Throwable $ex) {
                 // err
@@ -197,11 +204,6 @@ if (isset($_GET['query'])) {
                 exit;
               }
 
-              // safe authenticator in mysql `tfa` table
-              $_data['tfa_method'] = $post->tfa_method;
-              $_data['key_id'] = $post->key_id;
-              $_data['registration'] = $data;
-              set_tfa($_data);
 
               // send response
               $return = new stdClass();
@@ -419,7 +421,7 @@ if (isset($_GET['query'])) {
           // }
           $ids = NULL;
 
-          $getArgs = $WebAuthn->getGetArgs($ids, 30, true, true, true, true, $GLOBALS['FIDO2_UV_FLAG_LOGIN']);
+          $getArgs = $WebAuthn->getGetArgs($ids, 30, false, false, false, false, $GLOBALS['FIDO2_UV_FLAG_LOGIN']);
           print(json_encode($getArgs));
           $_SESSION['challenge'] = $WebAuthn->getChallenge();
           return;
@@ -428,8 +430,11 @@ if (isset($_GET['query'])) {
         case "webauthn-tfa-registration":
           if (isset($_SESSION["mailcow_cc_role"])) {
               // Exclude existing CredentialIds, if any
-              $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username");
-              $stmt->execute(array(':username' => $_SESSION['mailcow_cc_username']));
+              $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username AND authmech = :authmech");
+              $stmt->execute(array(
+                ':username' => $_SESSION['mailcow_cc_username'],
+                ':authmech' => 'webauthn'
+              ));
               $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
               while($row = array_shift($rows)) {
                 $excludeCredentialIds[] = base64_decode($row['keyHandle']);
@@ -450,20 +455,24 @@ if (isset($_GET['query'])) {
           }
         break;
         case "webauthn-tfa-get-args":
-          $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username");
-          $stmt->execute(array(':username' => $_SESSION['pending_mailcow_cc_username']));
+          $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username AND authmech = :authmech");
+          $stmt->execute(array(
+            ':username' => $_SESSION['pending_mailcow_cc_username'],
+            ':authmech' => 'webauthn'
+          ));
           $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
-          while($row = array_shift($rows)) {
-            $cids[] = base64_decode($row['keyHandle']);
-          }
-          if (count($cids) == 0) {
+          if (count($rows) == 0) {
             print(json_encode(array(
                 'type' => 'error',
                 'msg' => 'Cannot find matching credentialIds'
             )));
+            exit;
+          }
+          while($row = array_shift($rows)) {
+            $cids[] = base64_decode($row['keyHandle']);
           }
 
-          $getArgs = $WebAuthn->getGetArgs($cids, 30, true, true, true, true, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN']);
+          $getArgs = $WebAuthn->getGetArgs($cids, 30, false, false, false, false, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN']);
           $getArgs->publicKey->extensions = array('appid' => "https://".$getArgs->publicKey->rpId);
           print(json_encode($getArgs));
           $_SESSION['challenge'] = $WebAuthn->getChallenge();

+ 2 - 1
data/web/lang/lang.es.json

@@ -19,7 +19,8 @@
         "syncjobs": "Trabajos de sincronización",
         "tls_policy": "Póliza de TLS",
         "unlimited_quota": "Cuota ilimitada para buzones",
-        "app_passwds": "Gestionar las contraseñas de aplicaciones"
+        "app_passwds": "Gestionar las contraseñas de aplicaciones",
+        "domain_desc": "Cambiar descripción del dominio"
     },
     "add": {
         "activate_filter_warn": "Todos los demás filtros se desactivarán cuando este filtro se active.",

+ 3 - 2
data/web/lang/lang.ru.json

@@ -988,7 +988,7 @@
         "enter_qr_code": "Ваш код TOTP, если устройство не может отсканировать QR-код",
         "error_code": "Код ошибки",
         "init_webauthn": "Инициализация, пожалуйста, подождите...",
-        "key_id": "Идентификатор YubiKey ключа",
+        "key_id": "Идентификатор вашего устройства",
         "key_id_totp": "Идентификатор TOTP ключа",
         "none": "Отключить",
         "reload_retry": "- (перезагрузить страницу браузера или почистите кеш/cookies, если ошибка повторяется)",
@@ -1002,7 +1002,8 @@
         "webauthn": "WebAuthn аутентификация",
         "waiting_usb_auth": "<i>Ожидание устройства USB...</i><br><br>Пожалуйста, нажмите кнопку на USB устройстве сейчас.",
         "waiting_usb_register": "<i>Ожидание устройства USB...</i><br><br>Пожалуйста, введите пароль выше и подтвердите регистрацию, нажав кнопку на USB устройстве.",
-        "yubi_otp": "Yubico OTP аутентификация"
+        "yubi_otp": "Yubico OTP аутентификация",
+        "u2f_deprecated": "Похоже, что ваш ключ был зарегистрирован с использованием устаревшего метода U2F. Мы деактивируем для вас двухфакторную аутентификацию и удалим ваш ключ."
     },
     "user": {
         "action": "Действия",

+ 2 - 1
data/web/lang/lang.uk.json

@@ -980,7 +980,8 @@
         "resource_modified": "Зміни поштового акаунту %s збережено",
         "settings_map_added": "Правило додано",
         "tls_policy_map_entry_deleted": "Політику TLS ID %s видалено",
-        "verified_totp_login": "Авторизацію TOTP пройдено"
+        "verified_totp_login": "Авторизацію TOTP пройдено",
+        "domain_add_dkim_available": "Ключ DKIM вже існує"
     },
     "tfa": {
         "confirm": "Підтвердьте",

+ 100 - 26
data/web/templates/base.twig

@@ -208,9 +208,69 @@ function recursiveBase64StrToArrayBuffer(obj) {
       keyboard: false
     }).show();
 
+
+    // validate Time based OTP tfa
+    $("#pending_tfa_tab_totp").click(function(){
+      $(".webauthn-authenticator-selection").removeClass("active");
+      $("#collapseWebAuthnTFA").collapse('hide');
+
+      // select default if only one authenticator exists
+      if ($('.totp-authenticator-selection').length == 1){
+        $('.totp-authenticator-selection').addClass("active");
+        var id = $('.totp-authenticator-selection').children('input').first().val();
+        $("#totp_selected_id").val(id);
+        $("#collapseTotpTFA").collapse('show');
+      }
+    });
+    $(".totp-authenticator-selection").click(function(){
+      $(".totp-authenticator-selection").removeClass("active");
+      $(this).addClass("active");
+      
+      var id = $(this).children('input').first().val();
+      $("#totp_selected_id").val(id);
+
+      $("#collapseTotpTFA").collapse('show');
+    });
+    if ($('.totp-authenticator-selection').length == 1 &&
+        $('#pending_tfa_tab_yubi_otp').length == 0 &&
+        $('.webauthn-authenticator-selection').length == 0){
+      
+      // select default if only one authenticator exists
+      $('.totp-authenticator-selection').addClass("active");
+
+      var id = $('.totp-authenticator-selection').children('input').first().val();
+      $("#totp_selected_id").val(id);
+
+      $("#collapseTotpTFA").collapse('show');
+      setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 1000);
+    }
+    $('#pending_tfa_tab_totp').on('shown.bs.tab', function() {
+      // autofocus
+      setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 200);
+    });    
+    // validate Yubi OTP tfa
+    if ($('.webauthn-authenticator-selection').length == 0){
+      // autofocus
+      setTimeout(function() { $("#collapseYubiTFA").find('input[name="token"]').focus(); }, 1000);
+    }
+    $('#pending_tfa_tab_yubi_otp').on('shown.bs.tab', function() {
+      // autofocus
+      $("#collapseYubiTFA").find('input[name="token"]').focus();
+    });
     // validate WebAuthn tfa
-    $('#start_webauthn_confirmation').click(function(){
-      $('#webauthn_status_auth').html('<p><i class="bi bi-arrow-repeat icon-spin"></i> ' + lang_tfa.init_webauthn + '</p>');
+    $("#pending_tfa_tab_webauthn").click(function(){
+      $(".totp-authenticator-selection").removeClass("active");
+
+      $("#collapseTotpTFA").collapse('hide');
+    });
+    $(".webauthn-authenticator-selection").click(function(){
+      $(".webauthn-authenticator-selection").removeClass("active");
+      $(this).addClass("active");
+      
+      var id = $(this).children('input').first().val();
+      $("#webauthn_selected_id").val(id);
+      
+      $("#collapseWebAuthnTFA").collapse('show');
 
       $(this).find('input[name=token]').focus();
       if(document.getElementById("webauthn_auth_data") !== null) {
@@ -224,30 +284,32 @@ function recursiveBase64StrToArrayBuffer(obj) {
         window.fetch("/api/v1/get/webauthn-tfa-get-args", {method:'GET',cache:'no-cache'}).then(response => {
             return response.json();
         }).then(json => {
-            if (json.success === false) throw new Error();
+          console.log(json);
+          if (json.success === false) throw new Error();
+          if (json.type === "error") throw new Error(json.msg);
       
-            recursiveBase64StrToArrayBuffer(json);
-            return json;
+          recursiveBase64StrToArrayBuffer(json);
+          return json;
         }).then(getCredentialArgs => {
-            // get credentials
-            return navigator.credentials.get(getCredentialArgs);
+          // get credentials
+          return navigator.credentials.get(getCredentialArgs);
         }).then(cred => {
-            return {
-                id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
-                clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
-                authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
-                signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
-            };
+          return {
+            id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
+            clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
+            authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
+            signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
+          };
         }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
-            // send request by submit
-            var form = document.getElementById('webauthn_auth_form');
-            var auth = document.getElementById('webauthn_auth_data');
-            auth.value = AuthenticatorAttestationResponse;
-            form.submit();
+          // send request by submit
+          var form = document.getElementById('webauthn_auth_form');
+          var auth = document.getElementById('webauthn_auth_data');
+          auth.value = AuthenticatorAttestationResponse;
+          form.submit();
         }).catch(function(err) {
-            var webauthn_return_code = document.getElementById('webauthn_return_code');
-            webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null;
-            webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry;
+          var webauthn_return_code = document.getElementById('webauthn_return_code');
+          webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null;
+          webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry;
         });
       } 
     });
@@ -263,7 +325,9 @@ function recursiveBase64StrToArrayBuffer(obj) {
         }
       });
     });
-    {% endif %}
+  {% endif %}
+
+
     // Validate FIDO2
   $("#fido2-login").click(function(){
     $('#fido2-alerts').html();
@@ -384,11 +448,13 @@ function recursiveBase64StrToArrayBuffer(obj) {
 
         $("#start_webauthn_register").click(() => {
             var key_id = document.getElementsByName('key_id')[1].value;
+            var confirm_password = document.getElementsByName('confirm_password')[1].value;
 
             // fetch WebAuthn create args
             window.fetch("/api/v1/get/webauthn-tfa-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(response => {
                 return response.json();
             }).then(json => {
+                console.log(json);
                 if (json.success === false) throw new Error(json.msg);
                 recursiveBase64StrToArrayBuffer(json);
 
@@ -401,7 +467,8 @@ function recursiveBase64StrToArrayBuffer(obj) {
                     clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
                     attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null,
                     key_id: key_id,
-                    tfa_method: "webauthn"
+                    tfa_method: "webauthn",
+                    confirm_password: confirm_password
                 };
             }).then(JSON.stringify).then(AuthenticatorAttestationResponse => {
                 // send request
@@ -449,13 +516,20 @@ function recursiveBase64StrToArrayBuffer(obj) {
   {% if ui_texts.ui_footer %}
   <hr><span class="rot-enc">{{ ui_texts.ui_footer|rot13|raw }}</span>
   {% endif %}
-  {% if mailcow_cc_username and mailcow_info.version_tag|default %}
+  {% if mailcow_cc_username and mailcow_info.mailcow_branch|lower == "master" and mailcow_info.version_tag|default %}
   <span class="version">
     🐮 + 🐋 = 💕
-    <a href="{{ mailcow_info.project_url }}/releases/tag/{{ mailcow_info.version_tag }}" target="_blank">
-        Version: {{ mailcow_info.version_tag }}
+        Version: <a href="{{ mailcow_info.git_project_url }}/releases/tag/{{ mailcow_info.version_tag }}" target="_blank">{{ mailcow_info.version_tag }}
     </a>
   </span>
+  {% endif %}  
+  {% if mailcow_cc_username and mailcow_info.mailcow_branch|lower == "nightly" and mailcow_info.version_tag|default %}
+  <span class="version">
+    🛠️🐮 + 🐋 = 💕
+        Nightly: <a href="{{ mailcow_info.git_project_url }}/commit/{{ mailcow_info.git_commit }}" target="_blank">{{ mailcow_info.version_tag }}
+    </a><br>
+    <span style="text-align:right;display:block;">Build: {{ mailcow_info.git_commit_date }}</span>
+  </span>
   {% endif %}
 </div>
 </body>

+ 1 - 1
data/web/templates/domainadmin.twig

@@ -28,7 +28,7 @@
       <div class="col-sm-9 col-7">
         <select id="selectTFA" class="selectpicker" title="{{ lang.tfa.select }}">
           <option value="yubi_otp">{{ lang.tfa.yubi_otp }}</option>
-          <option value="u2f">{{ lang.tfa.u2f }}</option>
+          <option value="webauthn">{{ lang.tfa.webauthn }}</option>
           <option value="totp">{{ lang.tfa.totp }}</option>
           <option value="none">{{ lang.tfa.none }}</option>
         </select>

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

@@ -131,37 +131,37 @@
   </div>
 </div>
 {% endif %}
-{% if pending_tfa_method %}
+{% if pending_tfa_methods %}
 <div class="modal fade" id="ConfirmTFAModal" tabindex="-1" role="dialog" aria-labelledby="ConfirmTFAModalLabel">
   <div class="modal-dialog" role="document">
     <div class="modal-content">
       <div class="modal-header">
+        <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button>
         <h3 class="modal-title">{{ lang.tfa[pending_tfa_method] }}</h3>
-        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
       </div>
       <div class="modal-body">
         {% if pending_tfa_method == 'yubi_otp' %}
         <form role="form" method="post">
-          <div>
+          <div class="form-group">
             <div class="input-group">
-              <span class="input-group-text" id="yubi-addon"><img alt="Yubicon Icon" src="/img/yubi.ico"></span>
+              <span class="input-group-addon" id="yubi-addon"><img alt="Yubicon Icon" src="/img/yubi.ico"></span>
               <input type="text" name="token" class="form-control" autocomplete="off" placeholder="Touch Yubikey" aria-describedby="yubi-addon">
               <input type="hidden" name="tfa_method" value="yubi_otp">
             </div>
           </div>
-          <button class="btn btn-sm d-block d-sm-inline btn-sm btn-secondary" type="submit" name="verify_tfa_login">{{ lang.login.login }}</button>
+          <button class="btn btn-sm visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-sm btn-default" type="submit" name="verify_tfa_login">{{ lang.login.login }}</button>
         </form>
         {% endif %}
         {% if pending_tfa_method == 'totp' %}
         <form role="form" method="post">
-          <div>
+          <div class="form-group">
             <div class="input-group">
-              <span class="input-group-text" id="tfa-addon"><i class="bi bi-shield-lock-fill"></i></span>
+              <span class="input-group-addon" id="tfa-addon"><i class="bi bi-shield-lock-fill"></i></span>
               <input type="number" min="000000" max="999999" name="token" class="form-control" placeholder="123456" autocomplete="one-time-code" aria-describedby="tfa-addon">
               <input type="hidden" name="tfa_method" value="totp">
             </div>
           </div>
-          <button class="btn btn-sm d-block d-sm-inline btn-secondary" type="submit" name="verify_tfa_login">{{ lang.login.login }}</button>
+          <button class="btn btn-sm visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-default" type="submit" name="verify_tfa_login">{{ lang.login.login }}</button>
         </form>
         {% endif %}
         {% if pending_tfa_method == 'hotp' %}

+ 2 - 2
data/web/templates/modals/mailbox.twig

@@ -435,11 +435,11 @@
       </div>
       <div class="modal-body">
         <p class="text-muted">{{ lang.add.syncjob_hint }}</p>
-        <form class="form-horizontal" data-cached-form="true" role="form" data-id="add_syncjob">
+        <form class="form-horizontal" data-cached-form="false" role="form" data-id="add_syncjob">
           <div class="row mb-2">
             <label class="control-label col-sm-2 text-sm-end" for="username">{{ lang.add.username }}</label>
             <div class="col-sm-10">
-              <select data-live-search="true" name="username" required>
+              <select data-live-search="true" name="username" title="{{ lang.add.select }}" required>
                 {% for mailbox in mailboxes %}
                   <option>{{ mailbox }}</option>
                 {% endfor %}

+ 21 - 0
data/web/templates/user/tab-user-auth.twig

@@ -53,6 +53,27 @@
         </div>
       </div>
       <hr>
+      {# TFA #}
+      <div class="row">
+        <div class="col-sm-3 col-xs-5 text-right">{{ lang.tfa.tfa }}:</div>
+        <div class="col-sm-9 col-xs-7">
+          <p id="tfa_pretty">{{ tfa_data.pretty }}</p>
+          {% include 'tfa_keys.twig' %}
+          <br>
+        </div>
+      </div>
+      <div class="row">
+        <div class="col-sm-3 col-xs-5 text-right">{{ lang.tfa.set_tfa }}:</div>
+        <div class="col-sm-9 col-xs-7">
+          <select data-style="btn btn-sm dropdown-toggle bs-placeholder btn-default" data-width="fit" id="selectTFA" class="selectpicker" title="{{ lang.tfa.select }}">
+            <option value="yubi_otp">{{ lang.tfa.yubi_otp }}</option>
+            <option value="webauthn">{{ lang.tfa.webauthn }}</option>
+            <option value="totp">{{ lang.tfa.totp }}</option>
+            <option value="none">{{ lang.tfa.none }}</option>
+          </select>
+        </div>
+      </div>
+      <hr>
       {# FIDO2 #}
       <div class="row">
         <div class="col-sm-3 col-12 text-sm-end text-start">

+ 2 - 2
data/web/user.php

@@ -76,6 +76,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
     'acl_json' => json_encode($_SESSION['acl']),
     'user_spam_score' => mailbox('get', 'spam_score', $username),
     'tfa_data' => $tfa_data,
+    'tfa_id' => @$_SESSION['tfa_id'],
     'fido2_data' => $fido2_data,
     'mailboxdata' => $mailboxdata,
     'clientconfigstr' => $clientconfigstr,
@@ -91,8 +92,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
     'lang_datatables' => json_encode($lang['datatables']),
   ];
 }
-
-if (!isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
+else {
   header('Location: /');
   exit();
 }

+ 5 - 5
docker-compose.yml

@@ -58,7 +58,7 @@ services:
             - redis
 
     clamd-mailcow:
-      image: mailcow/clamd:1.52
+      image: mailcow/clamd:1.54
       restart: always
       depends_on:
         - unbound-mailcow
@@ -168,7 +168,7 @@ services:
             - phpfpm
 
     sogo-mailcow:
-      image: mailcow/sogo:1.108
+      image: mailcow/sogo:1.109
       environment:
         - DBNAME=${DBNAME}
         - DBUSER=${DBUSER}
@@ -215,7 +215,7 @@ services:
             - sogo
 
     dovecot-mailcow:
-      image: mailcow/dovecot:1.162
+      image: mailcow/dovecot:1.18
       depends_on:
         - mysql-mailcow
       dns:
@@ -377,8 +377,8 @@ services:
         - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z
         - sogo-web-vol-1:/usr/lib/GNUstep/SOGo/
       ports:
-        - "${HTTPS_BIND:-0.0.0.0}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}"
-        - "${HTTP_BIND:-0.0.0.0}:${HTTP_PORT:-80}:${HTTP_PORT:-80}"
+        - "${HTTPS_BIND:-}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}"
+        - "${HTTP_BIND:-}:${HTTP_PORT:-80}:${HTTP_PORT:-80}"
       restart: always
       networks:
         mailcow-network:

+ 77 - 30
generate_config.sh

@@ -16,37 +16,48 @@ if [[ "$(uname -r)" =~ ^4\.4\. ]]; then
   fi
 fi
 
-if grep --help 2>&1 | grep -q -i "busybox"; then
-  echo "BusyBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\""
-  exit 1
-fi
-if cp --help 2>&1 | grep -q -i "busybox"; then
-  echo "BusyBox cp detected, please install coreutils, \"apk add --no-cache --upgrade coreutils\""
-  exit 1
-fi
+if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\""; exit 1; fi
+# This will also cover sort
+if cp --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox cp detected, please install coreutils, \"apk add --no-cache --upgrade coreutils\""; exit 1; fi
+if sed --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox sed detected, please install gnu sed, \"apk add --no-cache --upgrade sed\""; exit 1; fi
 
 for bin in openssl curl docker git awk sha1sum; do
   if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
 done
 
-echo "checking docker compose version...";
-if docker compose >/dev/null 2>&1; then
-  echo -e "\e[32mFound Compose v2!\e[0m"
-elif docker-compose version --short | grep -m1 "^2" > /dev/null 2>&1; then
-  echo -e "\e[32mFound Compose v2!\e[0m"
-  COMPOSE_COMMAND="docker-compose"  
-elif docker-compose version --short | grep -m1 "^1" > /dev/null 2>&1; then
-  echo -e "\e[33mWARN: Your machine is using Docker-Compose v1!\e[0m"
-  echo -e "\e[33mmailcow will drop the Docker-Compose v1 Support in December 2022\e[0m"
-  echo -e "\e[33mPlease consider a upgrade to Docker-Compose v2.\e[0m"
-  echo
-  echo
-  echo -e "\e[33mContinuing...\e[0m"
-  sleep 3
+if docker compose > /dev/null 2>&1; then
+    if docker compose version --short | grep "^2." > /dev/null 2>&1; then
+      COMPOSE_VERSION=native
+      echo -e "\e[31mFound Docker Compose Plugin (native).\e[0m"
+      echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
+      sleep 2
+      echo -e "\e[33mNotice: You´ll have to update this Compose Version via your Package Manager manually!\e[0m"
+    else
+      echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" 
+      echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
+      exit 1
+    fi
+elif docker-compose > /dev/null 2>&1; then
+  if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
+    if docker-compose version --short | grep "^2." > /dev/null 2>&1; then
+      COMPOSE_VERSION=standalone
+      echo -e "\e[31mFound Docker Compose Standalone.\e[0m"
+      echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
+      sleep 2
+      echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
+    else
+      echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" 
+      echo -e "\e[31mPlease update/install manually regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
+      exit 1
+    fi
+  fi
+
 else
-  echo -e "\e[31mCannot find Docker-Compose v1 or v2 on your System. Please install Docker-Compose v2 and re-run the Script.\e[0m"
+  echo -e "\e[31mCannot find Docker Compose.\e[0m" 
+  echo -e "\e[31mPlease install it regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
   exit 1
 fi
+    
 
 if [ -f mailcow.conf ]; then
   read -r -p "A config file exists and will be overwritten, are you sure you want to continue? [y/N] " response
@@ -124,6 +135,25 @@ else
   SKIP_SOLR=n
 fi
 
+echo "Which branch of mailcow do you want to use?"
+echo ""
+echo "Available Branches:"
+echo "- master branch (stable updates) | default, recommended [1]"
+echo "- nightly branch (unstable updates, testing) | not-production ready [2]"
+sleep 1
+read -r -p  "Choose the Branch with it´s number [1/2] " branch
+  case $branch in
+    [2])
+      git_branch="nightly"
+      ;;
+    *)
+      git_branch="master"
+    ;;
+  esac
+
+git fetch --all
+git checkout -f $git_branch
+
 [ ! -f ./data/conf/rspamd/override.d/worker-controller-password.inc ] && echo '# Placeholder' > ./data/conf/rspamd/override.d/worker-controller-password.inc
 
 cat << EOF > mailcow.conf
@@ -202,6 +232,14 @@ TZ=${MAILCOW_TZ}
 
 COMPOSE_PROJECT_NAME=mailcowdockerized
 
+# Used Docker Compose version
+# Switch here between native (compose plugin) and standalone
+# For more informations take a look at the mailcow docs regarding the configuration options.
+# Normally this should be untouched but if you decided to use either of those you can switch it manually here.
+# Please be aware that at least one of those variants should be installed on your maschine or mailcow will fail.
+
+DOCKER_COMPOSE_VERSION=${COMPOSE_VERSION}
+
 # Set this to "allow" to enable the anyone pseudo user. Disabled by default.
 # When enabled, ACL can be created, that apply to "All authenticated users"
 # This should probably only be activated on mail hosts, that are used exclusivly by one organisation.
@@ -382,9 +420,18 @@ echo "Copying snake-oil certificate..."
 cp -n -d data/assets/ssl-example/*.pem data/assets/ssl/
 
 # Set app_info.inc.php
-mailcow_git_version=$(git describe --tags `git rev-list --tags --max-count=1`)
-mailcow_git_commit=$(git rev-parse HEAD)
-mailcow_git_commit_date=$(git show -s --format=%cd --date=format:'%Y-%m-%d %H:%M')
+if [ ${git_branch} == "master" ]; then
+  mailcow_git_version=$(git describe --tags `git rev-list --tags --max-count=1`)
+elif [ ${git_branch} == "nightly" ]; then
+  mailcow_git_version=$(git rev-parse --short $(git rev-parse @{upstream}))
+  mailcow_last_git_version=""
+else
+  mailcow_git_version=$(git rev-parse --short HEAD)
+  mailcow_last_git_version=""
+fi
+
+mailcow_git_commit=$(git rev-parse origin/${git_branch})
+mailcow_git_commit_date=$(git log -1 --format=%ci @{upstream} )
 
 if [ $? -eq 0 ]; then
   echo '<?php' > data/web/inc/app_info.inc.php
@@ -395,20 +442,20 @@ if [ $? -eq 0 ]; then
   echo '  $MAILCOW_GIT_URL="https://github.com/mailcow/mailcow-dockerized";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_COMMIT="'$mailcow_git_commit'";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_COMMIT_DATE="'$mailcow_git_commit_date'";' >> data/web/inc/app_info.inc.php
-  echo '  $MAILCOW_BUILD="'$BUILD'";' >> data/web/inc/app_info.inc.php
+  echo '  $MAILCOW_BRANCH="'$git_branch'";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_UPDATEDAT='$(date +%s)';' >> data/web/inc/app_info.inc.php
   echo '?>' >> data/web/inc/app_info.inc.php
 else
   echo '<?php' > data/web/inc/app_info.inc.php
-  echo '  $MAILCOW_GIT_VERSION="";' >> data/web/inc/app_info.inc.php
+  echo '  $MAILCOW_GIT_VERSION="'$mailcow_git_version'";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_LAST_GIT_VERSION="";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_OWNER="mailcow";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_REPO="mailcow-dockerized";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_URL="https://github.com/mailcow/mailcow-dockerized";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_COMMIT="";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_COMMIT_DATE="";' >> data/web/inc/app_info.inc.php
-  echo '  $MAILCOW_BUILD="'$BUILD'";' >> data/web/inc/app_info.inc.php
+  echo '  $MAILCOW_BRANCH="'$git_branch'";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_UPDATEDAT='$(date +%s)';' >> data/web/inc/app_info.inc.php
   echo '?>' >> data/web/inc/app_info.inc.php
   echo -e "\e[33mCannot determine current git repository version...\e[0m"
-fi
+fi

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

@@ -84,26 +84,6 @@ function preflight_local_checks() {
     fi
   done
 
-
-  echo "checking docker compose version...";
-  if docker compose >/dev/null 2>&1; then
-    echo -e "\e[32mFound Compose v2 on local machine!\e[0m"
-  elif docker-compose version --short | grep -m1 "^2" > /dev/null 2>&1; then
-  echo -e "\e[32mFound Compose v2!\e[0m"
-  COMPOSE_COMMAND="docker-compose"  
-  elif docker-compose version --short | grep -m1 "^1" > /dev/null 2>&1; then
-    echo -e "\e[33mWARN: Your machine is using Docker-Compose v1!\e[0m"
-    echo -e "\e[33mmailcow will drop the Docker-Compose v1 Support in December 2022\e[0m"
-    echo -e "\e[33mPlease consider a upgrade to Docker-Compose v2.\e[0m"
-    echo
-    echo
-    echo -e "\e[33mContinuing...\e[0m"
-    sleep 3
-  else
-    echo -e "\e[31mCannot find Docker-Compose v1 or v2 on your System. Please install Docker-Compose v2 and re-run the Script.\e[0m"
-    exit 1
-  fi
-
   if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then
     echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m"
     exit 1
@@ -142,49 +122,43 @@ function preflight_remote_checks() {
     fi
   done
 
-  echo "checking docker compose version on remote...";
-  if ssh -q -o StrictHostKeyChecking=no \
-      -i "${REMOTE_SSH_KEY}" \
-      ${REMOTE_SSH_HOST} \
-      -p ${REMOTE_SSH_PORT} \
-     -t 'docker compose' >/dev/null 2>&1; then
-    echo -e "\e[32mFound Compose v2 on remote!\e[0m"
-    COMPOSE_COMMAND="docker compose"
-  elif ssh -q -o StrictHostKeyChecking=no \
-      -i "${REMOTE_SSH_KEY}" \
-      ${REMOTE_SSH_HOST} \
-      -p ${REMOTE_SSH_PORT} \
-      -t 'docker-compose version --short' | grep -m1 "^2" > /dev/null 2>&1; then
-    echo -e "\e[32mFound Compose v2!\e[0m"
-    COMPOSE_COMMAND="docker-compose"
-  elif ssh -q -o StrictHostKeyChecking=no \
+  ssh -o StrictHostKeyChecking=no \
       -i "${REMOTE_SSH_KEY}" \
       ${REMOTE_SSH_HOST} \
       -p ${REMOTE_SSH_PORT} \
-      -t 'docker-compose version --short' | grep -m1 "^1" > /dev/null 2>&1; then
-    echo -e "\e[33mWARN: The remote is using Docker-Compose v1!\e[0m"
-    echo -e "\e[33mmailcow will drop the Docker-Compose v1 Support in December 2022\e[0m"
-    echo -e "\e[33mPlease consider a upgrade to Docker-Compose v2 on remote.\e[0m"
-    echo
-    echo
-    echo -e "\e[33mContinuing...\e[0m"
-    sleep 3
-    COMPOSE_COMMAND="docker-compose"
-  else
-    echo -e "\e[31mCannot find Docker-Compose v1 or v2 on the Remote Machine! Please install Docker-Compose v2 on that and re-run the script.\e[0m"
-    exit 1
-  fi
-}
+      "bash -s" << "EOF"
+if docker compose > /dev/null 2>&1; then
+	exit 0
+elif docker-compose version --short | grep "^2." > /dev/null 2>&1; then
+	exit 1
+else
+exit 2
+fi
+EOF
 
-preflight_local_checks
-preflight_remote_checks
+if [ $? = 0 ]; then
+  COMPOSE_COMMAND="docker compose"
+  echo "DEBUG: Using native docker compose on remote"
+
+elif [ $? = 1 ]; then
+  COMPOSE_COMMAND="docker-compose"
+  echo "DEBUG: Using standalone docker compose on remote"
+
+else
+  echo -e "\e[31mCannot find any Docker Compose on remote, exiting...\e[0m"
+  exit 1
+fi
+}
 
 SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
-COMPOSE_FILE="${SCRIPT_DIR}/../docker-compose.yml"
 source "${SCRIPT_DIR}/../mailcow.conf"
+COMPOSE_FILE="${SCRIPT_DIR}/../docker-compose.yml"
 CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd 'A-Za-z-_')
 SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' "${COMPOSE_FILE}")
 
+preflight_local_checks
+preflight_remote_checks
+
 echo
 echo -e "\033[1mFound compose project name ${CMPS_PRJ} for ${MAILCOW_HOSTNAME}\033[0m"
 echo -e "\033[1mFound SQL ${SQLIMAGE}\033[0m"
@@ -311,7 +285,7 @@ echo "OK"
     -i "${REMOTE_SSH_KEY}" \
     ${REMOTE_SSH_HOST} \
     -p ${REMOTE_SSH_PORT} \
-    $COMPOSE_COMMAND -f "${SCRIPT_DIR}/../docker-compose.yml" pull --no-parallel --quiet 2>&1 ; then
+    ${COMPOSE_COMMAND} -f "${SCRIPT_DIR}/../docker-compose.yml" pull --no-parallel --quiet 2>&1 ; then
       >&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote"
   fi
 
@@ -324,4 +298,4 @@ if ! ssh -o StrictHostKeyChecking=no \
     >&2 echo -e "\e[31m[ERR]\e[0m - Could not cleanup old images on remote"
 fi
 
-echo -e "\e[32mDone\e[0m"
+echo -e "\e[32mDone\e[0m"

+ 25 - 22
helper-scripts/backup_and_restore.sh

@@ -76,27 +76,6 @@ else
   CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]")
 fi
 
-echo "checking docker compose version...";
-if docker compose >/dev/null 2>&1; then
-  echo -e "\e[32mFound Compose v2!\e[0m"
-  COMPOSE_COMMAND="docker compose"
-elif docker-compose version --short | grep -m1 "^2" > /dev/null 2>&1; then
-  echo -e "\e[32mFound Compose v2!\e[0m"
-  COMPOSE_COMMAND="docker-compose"  
-elif docker-compose version --short | grep -m1 "^1" > /dev/null 2>&1; then
-  echo -e "\e[33mWARN: Your machine is using Docker-Compose v1!\e[0m"
-  echo -e "\e[33mmailcow will drop the Docker-Compose v1 Support in December 2022\e[0m"
-  echo -e "\e[33mPlease consider a upgrade to Docker-Compose v2.\e[0m"
-  echo
-  echo
-  echo -e "\e[33mContinuing...\e[0m"
-  sleep 3
-  COMPOSE_COMMAND="docker-compose"
-else
-  echo -e "\e[31mCannot find Docker-Compose v1 or v2 on your System. Please install Docker-Compose v2 and re-run the Script.\e[0m"
-  exit 1
-fi
-
 if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then
   >&2 echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m"
   exit 1
@@ -108,6 +87,12 @@ function backup() {
   mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}"
   chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}"
   cp "${SCRIPT_DIR}/../mailcow.conf" "${BACKUP_LOCATION}/mailcow-${DATE}"
+  for bin in docker; do
+  if [[ -z $(which ${bin}) ]]; then
+    >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
+    exit 1
+  fi
+  done
   while (( "$#" )); do
     case "$1" in
     vmail|all)
@@ -175,6 +160,24 @@ function backup() {
 }
 
 function restore() {
+  for bin in docker; do
+  if [[ -z $(which ${bin}) ]]; then
+    >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
+    exit 1
+  fi
+  done
+
+  if [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then
+  COMPOSE_COMMAND="docker compose"
+
+  elif [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then
+    COMPOSE_COMMAND="docker-compose"
+  
+  else
+    echo -e "\e[31mCan not read DOCKER_COMPOSE_VERSION variable from mailcow.conf! Is your mailcow up to date? Exiting...\e[0m"
+    exit 1
+  fi
+
   echo
   echo "Stopping watchdog-mailcow..."
   docker stop $(docker ps -qf name=watchdog-mailcow)
@@ -368,4 +371,4 @@ elif [[ ${1} == "restore" ]]; then
   done
   echo "Restoring ${FILE_SELECTION[${input_sel}]} from ${RESTORE_POINT}..."
   restore "${RESTORE_POINT}" ${FILE_SELECTION[${input_sel}]}
-fi
+fi

+ 70 - 0
helper-scripts/update_compose.sh

@@ -0,0 +1,70 @@
+#!/bin/bash
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+source ${SCRIPT_DIR}/../mailcow.conf
+
+if [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then
+LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php)
+COMPOSE_VERSION=$(docker-compose version --short)
+if [[ "$LATEST_COMPOSE" != "$COMPOSE_VERSION" ]]; then
+  echo -e "\e[33mA new docker-compose Version is available: $LATEST_COMPOSE\e[0m"
+  echo -e "\e[33mYour Version is: $COMPOSE_VERSION\e[0m"
+else
+  echo -e "\e[32mYour docker-compose Version is up to date! Not updating it...\e[0m"
+  exit 0 
+fi
+read -r -p "Do you want to update your docker-compose Version? It will automatic upgrade your docker-compose installation (recommended)? [y/N] " updatecomposeresponse 
+    if [[ ! "${updatecomposeresponse}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
+      echo "OK, not updating docker-compose."
+      exit 0
+    fi 
+echo -e "\e[32mFetching new docker-compose (standalone) version...\e[0m"
+echo -e "\e[32mTrying to determine GLIBC version...\e[0m"
+    if ldd --version > /dev/null; then
+        GLIBC_V=$(ldd --version | grep -E '(GLIBC|GNU libc)' | rev | cut -d ' ' -f1 | rev | cut -d '.' -f2)
+        if [ ! -z "${GLIBC_V}" ] && [ ${GLIBC_V} -gt 27 ]; then
+        DC_DL_SUFFIX=
+        else
+        DC_DL_SUFFIX=legacy
+        fi
+    else
+        DC_DL_SUFFIX=legacy
+    fi
+    sleep 1
+    if [[ $(command -v pip 2>&1) && $(pip list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 || $(command -v pip3 2>&1) && $(pip3 list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 ]]; then
+        echo -e "\e[33mFound a docker-compose Version installed with pip!\e[0m"
+        echo -e "\e[31mPlease uninstall the pip Version of docker-compose since it doesn´t support Versions higher than 1.29.2.\e[0m"
+        sleep 2
+        echo -e "\e[33mExiting...\e[0m"
+        exit 1
+        #prevent breaking a working docker-compose installed with pip
+    elif [[ $(curl -sL -w "%{http_code}" https://www.servercow.de/docker-compose/latest.php?vers=${DC_DL_SUFFIX} -o /dev/null) == "200" ]]; then
+        LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php)
+        COMPOSE_VERSION=$(docker-compose version --short)
+        if [[ "$LATEST_COMPOSE" != "$COMPOSE_VERSION" ]]; then
+        COMPOSE_PATH=$(command -v docker-compose)
+        if [[ -w ${COMPOSE_PATH} ]]; then
+            curl -#L https://github.com/docker/compose/releases/download/v${LATEST_COMPOSE}/docker-compose-$(uname -s)-$(uname -m) > $COMPOSE_PATH
+            chmod +x $COMPOSE_PATH
+            echo -e "\e[32mYour Docker Compose (standalone) has been updated to: $LATEST_COMPOSE\e[0m"
+            exit 0
+        else
+            echo -e "\e[33mWARNING: $COMPOSE_PATH is not writable, but new version $LATEST_COMPOSE is available (installed: $COMPOSE_VERSION)\e[0m"
+            return 1
+        fi
+        fi
+    else
+        echo -e "\e[33mCannot determine latest docker-compose version, skipping...\e[0m"
+        exit 1
+    fi
+
+elif [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then
+    echo -e "\e[31mYou are using the native Docker Compose Plugin. This Script is for the standalone Docker Compose Version only.\e[0m"
+    sleep 2
+    echo -e "\e[33mNotice: You´ll have to update this Compose Version via your Package Manager manually!\e[0m"
+    exit 1
+
+else
+    echo -e "\e[31mCan not read DOCKER_COMPOSE_VERSION variable from mailcow.conf! Is your mailcow up to date? Exiting...\e[0m"
+    exit 1
+fi

+ 281 - 112
update.sh

@@ -1,73 +1,6 @@
 #!/usr/bin/env bash
 
-# Check permissions
-if [ "$(id -u)" -ne "0" ]; then
-  echo "You need to be root"
-  exit 1
-fi
-
-SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
-
-# Run pre-update-hook
-if [ -f "${SCRIPT_DIR}/pre_update_hook.sh" ]; then
-  bash "${SCRIPT_DIR}/pre_update_hook.sh"
-fi
-
-if [[ "$(uname -r)" =~ ^4\.15\.0-60 ]]; then
-  echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!";
-  echo "Please update to 5.x or use another distribution."
-  exit 1
-fi
-
-if [[ "$(uname -r)" =~ ^4\.4\. ]]; then
-  if grep -q Ubuntu <<< $(uname -a); then
-    echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"
-    echo "Please update to linux-generic-hwe-16.04 by running \"apt-get install --install-recommends linux-generic-hwe-16.04\""
-    exit 1
-  fi
-  echo "mailcow on a 4.4.x kernel is not supported. It may or may not work, please upgrade your kernel or continue at your own risk."
-  read -p "Press any key to continue..." < /dev/tty
-fi
-
-# Exit on error and pipefail
-set -o pipefail
-
-# Setting high dc timeout
-export COMPOSE_HTTP_TIMEOUT=600
-
-# Add /opt/bin to PATH
-PATH=$PATH:/opt/bin
-
-umask 0022
-
-for bin in curl docker git awk sha1sum; do
-  if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
-done
-
-
-echo "checking docker compose version...";
-if docker compose >/dev/null 2>&1; then
-  echo -e "\e[32mFound Compose v2!\e[0m"
-  COMPOSE_COMMAND="docker compose"
-elif docker-compose version --short | grep -m1 "^2" > /dev/null 2>&1; then
-  echo -e "\e[32mFound Compose v2!\e[0m"
-  COMPOSE_COMMAND="docker-compose"
-elif docker-compose version --short | grep -m1 "^1" > /dev/null 2>&1; then
-  echo -e "\e[33mWARN: Your machine is using Docker-Compose v1!\e[0m"
-  echo -e "\e[33mmailcow will drop the Docker-Compose v1 Support in December 2022\e[0m"
-  echo -e "\e[33mPlease consider a upgrade to Docker-Compose v2.\e[0m"
-  echo
-  echo
-  echo -e "\e[33mContinuing...\e[0m"
-  sleep 3
-  COMPOSE_COMMAND="docker-compose"
-else
-  echo -e "\e[31mCannot find Docker-Compose v1 or v2 on your System. Please install Docker-Compose v2 and re-run the Script.\e[0m"
-  exit 1
-fi
-export LC_ALL=C
-DATE=$(date +%Y-%m-%d_%H_%M_%S)
-BRANCH=$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD)
+############## Begin Function Section ##############
 
 check_online_status() {
   CHECK_ONLINE_IPS=(1.1.1.1 9.9.9.9 8.8.8.8)
@@ -218,6 +151,132 @@ migrate_docker_nat() {
   fi
 }
 
+remove_obsolete_nginx_ports() {
+    # Removing obsolete docker-compose.override.yml
+    for override in docker-compose.override.yml docker-compose.override.yaml; do
+    if [ -s $override ] ; then
+        if cat $override | grep nginx-mailcow > /dev/null 2>&1; then
+          if cat $override | grep -E '(\[::])' > /dev/null 2>&1; then
+            if cat $override | grep -w 80:80 > /dev/null 2>&1 && cat $override | grep -w 443:443 > /dev/null 2>&1 ; then
+              echo -e "\e[33mBacking up ${override} to preserve custom changes...\e[0m"
+              echo -e "\e[33m!!! Manual Merge needed (if other overrides are set) !!!\e[0m"
+              sleep 3
+              cp $override ${override}_backup
+              sed -i '/nginx-mailcow:$/,/^$/d' $override
+              echo -e "\e[33mRemoved obsolete NGINX IPv6 Bind from original override File.\e[0m"
+                if [[ "$(cat $override | sed '/^\s*$/d' | wc -l)" == "2" ]]; then
+                  mv $override ${override}_empty
+                  echo -e "\e[31m${override} is empty. Renamed it to ensure mailcow is startable.\e[0m"
+                fi
+            fi
+          fi
+        fi
+    fi
+    done        
+}
+
+detect_docker_compose_command(){
+if ! [ "${DOCKER_COMPOSE_VERSION}" == "native" ] && ! [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then
+  if docker compose > /dev/null 2>&1; then
+      if docker compose version --short | grep "^2." > /dev/null 2>&1; then
+        DOCKER_COMPOSE_VERSION=native
+        COMPOSE_COMMAND="docker compose"
+        echo -e "\e[31mFound Docker Compose Plugin (native).\e[0m"
+        echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
+        sleep 2
+        echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
+      else
+        echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" 
+        echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
+        exit 1
+      fi
+  elif docker-compose > /dev/null 2>&1; then
+    if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
+      if docker-compose version --short | grep "^2." > /dev/null 2>&1; then
+        DOCKER_COMPOSE_VERSION=standalone
+        COMPOSE_COMMAND="docker-compose"
+        echo -e "\e[31mFound Docker Compose Standalone.\e[0m"
+        echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
+        sleep 2
+        echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.[0m"
+      else
+        echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" 
+        echo -e "\e[31mPlease update/install regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
+        exit 1
+      fi
+    fi
+
+  else
+    echo -e "\e[31mCannot find Docker Compose.\e[0m" 
+    echo -e "\e[31mPlease install it regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
+    exit 1
+  fi
+
+elif [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then
+  COMPOSE_COMMAND="docker compose"
+
+elif [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then
+  COMPOSE_COMMAND="docker-compose"
+fi
+}
+
+############## End Function Section ##############
+
+# Check permissions
+if [ "$(id -u)" -ne "0" ]; then
+  echo "You need to be root"
+  exit 1
+fi
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+# Run pre-update-hook
+if [ -f "${SCRIPT_DIR}/pre_update_hook.sh" ]; then
+  bash "${SCRIPT_DIR}/pre_update_hook.sh"
+fi
+
+if [[ "$(uname -r)" =~ ^4\.15\.0-60 ]]; then
+  echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!";
+  echo "Please update to 5.x or use another distribution."
+  exit 1
+fi
+
+if [[ "$(uname -r)" =~ ^4\.4\. ]]; then
+  if grep -q Ubuntu <<< $(uname -a); then
+    echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"
+    echo "Please update to linux-generic-hwe-16.04 by running \"apt-get install --install-recommends linux-generic-hwe-16.04\""
+    exit 1
+  fi
+  echo "mailcow on a 4.4.x kernel is not supported. It may or may not work, please upgrade your kernel or continue at your own risk."
+  read -p "Press any key to continue..." < /dev/tty
+fi
+
+# Exit on error and pipefail
+set -o pipefail
+
+# Setting high dc timeout
+export COMPOSE_HTTP_TIMEOUT=600
+
+# Add /opt/bin to PATH
+PATH=$PATH:/opt/bin
+
+umask 0022
+
+# Unset COMPOSE_COMMAND and DOCKER_COMPOSE_VERSION Variable to be on the newest state.
+unset COMPOSE_COMMAND
+unset DOCKER_COMPOSE_VERSION
+
+for bin in curl docker git awk sha1sum; do
+  if [[ -z $(command -v ${bin}) ]]; then 
+  echo "Cannot find ${bin}, exiting..." 
+  exit 1;
+  fi  
+done
+
+export LC_ALL=C
+DATE=$(date +%Y-%m-%d_%H_%M_%S)
+BRANCH=$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD)
+
 while (($#)); do
   case "${1}" in
     --check|-c)
@@ -242,11 +301,22 @@ while (($#)); do
     --skip-start)
       SKIP_START=y
     ;;
+    --skip-ping-check)
+      SKIP_PING_CHECK=y
+    ;;
+    --stable)
+      CURRENT_BRANCH="$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD)"
+      NEW_BRANCH="master"
+    ;;
     --gc)
       echo -e "\e[32mCollecting garbage...\e[0m"
       docker_garbage
       exit 0
     ;;
+    --nightly)
+      CURRENT_BRANCH="$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD)"
+      NEW_BRANCH="nightly"
+    ;;
     --prefetch)
       echo -e "\e[32mPrefetching images...\e[0m"
       prefetch_images
@@ -256,18 +326,17 @@ while (($#)); do
       echo -e "\e[32mRunning in forced mode...\e[0m"
       FORCE=y
     ;;
-    --skip-ping-check)
-      SKIP_PING_CHECK=y
-    ;;
     --help|-h)
-    echo './update.sh [-c|--check, --ours, --gc, --no-update-compose, --prefetch, --skip-start, --skip-ping-check, -f|--force, -h|--help]
+    echo './update.sh [-c|--check, --ours, --gc, --nightly, --prefetch, --skip-start, --skip-ping-check, --stable, -f|--force, -h|--help]
 
   -c|--check           -   Check for updates and exit (exit codes => 0: update available, 3: no updates)
   --ours               -   Use merge strategy option "ours" to solve conflicts in favor of non-mailcow code (local changes over remote changes), not recommended!
   --gc                 -   Run garbage collector to delete old image tags
+  --nightly            -   Switch your mailcow updates to the unstable (nightly) branch. FOR TESTING PURPOSES ONLY!!!!
   --prefetch           -   Only prefetch new images and exit (useful to prepare updates)
   --skip-start         -   Do not start mailcow after update
-  --skip-ping-check    -   Skip ICMP Check to public DNS resolvers (Use it only if you´ve blocked any ICMP Connections to your mailcow machine).
+  --skip-ping-check    -   Skip ICMP Check to public DNS resolvers (Use it only if you´ve blocked any ICMP Connections to your mailcow machine)
+  --stable             -   Switch your mailcow updates to the stable (master) branch. Default unless you changed it with --nightly.
   -f|--force           -   Force update, do not ask questions
 '
     exit 1
@@ -275,13 +344,16 @@ while (($#)); do
   shift
 done
 
-[[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing"; exit 1;}
 chmod 600 mailcow.conf
 source mailcow.conf
+
+detect_docker_compose_command
+
+[[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing! Is mailcow installed?"; exit 1;}
 DOTS=${MAILCOW_HOSTNAME//[^.]};
 if [ ${#DOTS} -lt 2 ]; then
   echo "MAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) is not a FQDN!"
-  echo "Please change it to a FQDN and run ${COMPOSE_COMMAND} down followed by ${COMPOSE_COMMAND} up -d"
+  echo "Please change it to a FQDN and run $COMPOSE_COMMAND down followed by $COMPOSE_COMMAND up -d"
   exit 1
 fi
 
@@ -290,6 +362,14 @@ if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox grep
 if cp --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox cp detected, please install coreutils, \"apk add --no-cache --upgrade coreutils\""; exit 1; fi
 if sed --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox sed detected, please install gnu sed, \"apk add --no-cache --upgrade sed\""; exit 1; fi
 
+# Check if Docker Compose is older then v2 before continuing
+if ! $COMPOSE_COMMAND version --short | grep "^2." > /dev/null 2>&1; then
+  echo -e "\e[33mYour Docker Compose Version is not up to date!\e[0m"
+  echo -e "\e[33mmailcow needs Docker Compose > 2.X.X!\e[0m"
+  echo -e "\e[33mYour current installed Version: $($COMPOSE_COMMAND version --short)\e[0m"
+  exit 1
+fi
+
 CONFIG_ARRAY=(
   "SKIP_LETS_ENCRYPT"
   "SKIP_SOGO"
@@ -308,6 +388,7 @@ CONFIG_ARRAY=(
   "SNAT_TO_SOURCE"
   "SNAT6_TO_SOURCE"
   "COMPOSE_PROJECT_NAME"
+  "DOCKER_COMPOSE_VERSION"
   "SQL_PORT"
   "API_KEY"
   "API_KEY_READ_ONLY"
@@ -343,6 +424,17 @@ for option in ${CONFIG_ARRAY[@]}; do
       echo "Adding new option \"${option}\" to mailcow.conf"
       echo "COMPOSE_PROJECT_NAME=mailcowdockerized" >> mailcow.conf
     fi
+  elif [[ ${option} == "DOCKER_COMPOSE_VERSION" ]]; then
+    if ! grep -q ${option} mailcow.conf; then
+      echo "Adding new option \"${option}\" to mailcow.conf"
+      echo "# Used Docker Compose version" >> mailcow.conf
+      echo "# Switch here between native (compose plugin) and standalone" >> mailcow.conf
+      echo "# For more informations take a look at the mailcow docs regarding the configuration options." >> mailcow.conf
+      echo "# Normally this should be untouched but if you decided to use either of those you can switch it manually here." >> mailcow.conf
+      echo "# Please be aware that at least one of those variants should be installed on your maschine or mailcow will fail." >> mailcow.conf
+      echo "" >> mailcow.conf
+      echo "DOCKER_COMPOSE_VERSION=${DOCKER_COMPOSE_VERSION}" >> mailcow.conf
+    fi
   elif [[ ${option} == "DOVEADM_PORT" ]]; then
     if ! grep -q ${option} mailcow.conf; then
       echo "Adding new option \"${option}\" to mailcow.conf"
@@ -567,6 +659,82 @@ else
    fi
 fi
 
+if ! [ $NEW_BRANCH ]; then
+  echo -e "\e[33mDetecting which build your mailcow runs on...\e[0m"
+  sleep 1
+  if [ ${BRANCH} == "master" ]; then
+    echo -e "\e[32mYou are receiving stable updates (master).\e[0m"
+    echo -e "\e[33mTo change that run the update.sh Script one time with the --nightly parameter to switch to nightly builds.\e[0m"
+
+  elif [ ${BRANCH} == "nightly" ]; then
+    echo -e "\e[31mYou are receiving unstable updates (nightly). These are for testing purposes only!!!\e[0m"
+    sleep 1
+    echo -e "\e[33mTo change that run the update.sh Script one time with the --stable parameter to switch to stable builds.\e[0m"
+
+  else
+    echo -e "\e[33mYou are receiving updates from a unsupported branch.\e[0m"
+    sleep 1
+    echo -e "\e[33mThe mailcow stack might still work but it is recommended to switch to the master branch (stable builds).\e[0m"
+    echo -e "\e[33mTo change that run the update.sh Script one time with the --stable parameter to switch to stable builds.\e[0m"
+  fi
+elif [ $FORCE ]; then
+  echo -e "\e[31mYou are running in forced mode!\e[0m"
+  echo -e "\e[31mA Branch Switch can only be performed manually (monitored).\e[0m"
+  echo -e "\e[31mPlease rerun the update.sh Script without the --force/-f parameter.\e[0m"
+  sleep 1
+elif [ $NEW_BRANCH == "master" ] && [ $CURRENT_BRANCH != "master" ]; then
+  echo -e "\e[33mYou are about to switch your mailcow Updates to the stable (master) branch.\e[0m"
+  sleep 1
+  echo -e "\e[33mBefore you do: Please take a backup of all components to ensure that no Data is lost...\e[0m"
+  sleep 1
+  echo -e "\e[31mWARNING: Please see on GitHub or ask in the communitys if a switch to master is stable or not.
+  In some rear cases a Update back to master can destroy your mailcow configuration in case of Database Upgrades etc.
+  Normally a upgrade back to master should be safe during each full release. 
+  Check GitHub for Database Changes and Update only if there similar to the full release!\e[0m"
+  read -r -p "Are you sure you that want to continue upgrading to the stable (master) branch? [y/N] " response
+  if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
+    echo "OK. If you prepared yourself for that please run the update.sh Script with the --stable parameter again to trigger this process here."
+    exit 0
+  fi
+  BRANCH=$NEW_BRANCH
+  DIFF_DIRECTORY=update_diffs
+  DIFF_FILE=${DIFF_DIRECTORY}/diff_before_upgrade_to_master_$(date +"%Y-%m-%d-%H-%M-%S")
+  mv diff_before_upgrade* ${DIFF_DIRECTORY}/ 2> /dev/null
+  if ! git diff-index --quiet HEAD; then
+    echo -e "\e[32mSaving diff to ${DIFF_FILE}...\e[0m"
+    mkdir -p ${DIFF_DIRECTORY}
+    git diff ${BRANCH} --stat > ${DIFF_FILE}
+    git diff ${BRANCH} >> ${DIFF_FILE}
+  fi
+  echo -e "\e[32mSwitching Branch to ${BRANCH}...\e[0m"
+  git fetch origin
+  git checkout -f ${BRANCH}
+
+elif [ $NEW_BRANCH == "nightly" ] && [ $CURRENT_BRANCH != "nightly" ]; then
+  echo -e "\e[33mYou are about to switch your mailcow Updates to the unstable (nightly) branch.\e[0m"
+  sleep 1
+  echo -e "\e[33mBefore you do: Please take a backup of all components to ensure that no Data is lost...\e[0m"
+  sleep 1
+  echo -e "\e[31mWARNING: A switch to nightly is possible any time. But a switch back (to master) isn't.\e[0m"
+  read -r -p "Are you sure you that want to continue upgrading to the unstable (nightly) branch? [y/N] " response
+  if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
+    echo "OK. If you prepared yourself for that please run the update.sh Script with the --nightly parameter again to trigger this process here."
+    exit 0
+  fi
+  BRANCH=$NEW_BRANCH
+  DIFF_DIRECTORY=update_diffs
+  DIFF_FILE=${DIFF_DIRECTORY}/diff_before_upgrade_to_nightly_$(date +"%Y-%m-%d-%H-%M-%S")
+  mv diff_before_upgrade* ${DIFF_DIRECTORY}/ 2> /dev/null
+  if ! git diff-index --quiet HEAD; then
+    echo -e "\e[32mSaving diff to ${DIFF_FILE}...\e[0m"
+    mkdir -p ${DIFF_DIRECTORY}
+    git diff ${BRANCH} --stat > ${DIFF_FILE}
+    git diff ${BRANCH} >> ${DIFF_FILE}
+  fi
+  git fetch origin
+  git checkout -f ${BRANCH}
+fi
+
 echo -e "\e[32mChecking for newer update script...\e[0m"
 SHA1_1=$(sha1sum update.sh)
 git fetch origin #${BRANCH}
@@ -578,13 +746,6 @@ if [[ ${SHA1_1} != ${SHA1_2} ]]; then
   exit 2
 fi
 
-if [[ -f mailcow.conf ]]; then
-  source mailcow.conf
-else
-  echo -e "\e[31mNo mailcow.conf - is mailcow installed?\e[0m"
-  exit 1
-fi
-
 if [ ! $FORCE ]; then
   read -r -p "Are you sure you want to update mailcow: dockerized? All containers will be stopped. [y/N] " response
   if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
@@ -594,14 +755,18 @@ if [ ! $FORCE ]; then
   migrate_docker_nat
 fi
 
+remove_obsolete_nginx_ports
+
 echo -e "\e[32mValidating docker-compose stack configuration...\e[0m"
-if ! ${COMPOSE_COMMAND} config -q; then
+sed -i 's/HTTPS_BIND:-:/HTTPS_BIND:-/g' docker-compose.yml
+sed -i 's/HTTP_BIND:-:/HTTP_BIND:-/g' docker-compose.yml
+if ! $COMPOSE_COMMAND config -q; then
   echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m"
   exit 1
 fi
 
 echo -e "\e[32mChecking for conflicting bridges...\e[0m"
-MAILCOW_BRIDGE=$(${COMPOSE_COMMAND} config | grep -i com.docker.network.bridge.name | cut -d':' -f2)
+MAILCOW_BRIDGE=$($COMPOSE_COMMAND config | grep -i com.docker.network.bridge.name | cut -d':' -f2)
 while read NAT_ID; do
   iptables -t nat -D POSTROUTING $NAT_ID
 done < <(iptables -L -vn -t nat --line-numbers | grep $IPV4_NETWORK | grep -E 'MASQUERADE.*all' | grep -v ${MAILCOW_BRIDGE} | cut -d' ' -f1)
@@ -621,8 +786,8 @@ prefetch_images
 
 echo -e "\e[32mStopping mailcow...\e[0m"
 sleep 2
-MAILCOW_CONTAINERS=($(${COMPOSE_COMMAND} ps -q))
-${COMPOSE_COMMAND} down
+MAILCOW_CONTAINERS=($($COMPOSE_COMMAND ps -q))
+$COMPOSE_COMMAND down
 echo -e "\e[32mChecking for remaining containers...\e[0m"
 sleep 2
 for container in "${MAILCOW_CONTAINERS[@]}"; do
@@ -633,8 +798,6 @@ done
 
 # Silently fixing remote url from andryyy to mailcow
 git remote set-url origin https://github.com/mailcow/mailcow-dockerized
-# get git mailcow version before updating...
-mailcow_last_git_version=$(git describe --tags `git rev-list --tags --max-count=1`)
 echo -e "\e[32mCommitting current status...\e[0m"
 [[ -z "$(git config user.name)" ]] && git config user.name moo
 [[ -z "$(git config user.email)" ]] && git config user.email moo@cow.moo
@@ -661,16 +824,13 @@ elif [[ ${MERGE_RETURN} == 1 ]]; then
 elif [[ ${MERGE_RETURN} != 0 ]]; then
   echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m"
   echo
-  echo "Run ${COMPOSE_COMMAND} up -d to restart your stack without updates or try again after fixing the mentioned errors."
+  echo "Run $COMPOSE_COMMAND up -d to restart your stack without updates or try again after fixing the mentioned errors."
   exit 1
 fi
 
-echo -e "\e[33mNot fetching latest docker-compose, please check for updates manually!\e[0m"
-sleep 3
-
 echo -e "\e[32mFetching new images, if any...\e[0m"
 sleep 2
-${COMPOSE_COMMAND} pull
+$COMPOSE_COMMAND pull
 
 # Fix missing SSL, does not overwrite existing files
 [[ ! -d data/assets/ssl ]] && mkdir -p data/assets/ssl
@@ -682,7 +842,7 @@ if grep -q 'SYSCTL_IPV6_DISABLED=1' mailcow.conf; then
   echo '!! IMPORTANT !!'
   echo
   echo 'SYSCTL_IPV6_DISABLED was removed due to complications. IPv6 can be disabled by editing "docker-compose.yml" and setting "enable_ipv6: true" to "enable_ipv6: false".'
-  echo 'This setting will only be active after a complete shutdown of mailcow by running "docker-compose down" followed by "docker-compose up -d".'
+  echo 'This setting will only be active after a complete shutdown of mailcow by running $COMPOSE_COMMAND down followed by $COMPOSE_COMMAND up -d".'
   echo
   echo '!! IMPORTANT !!'
   echo
@@ -710,46 +870,55 @@ if [ -f "data/conf/rspamd/local.d/metrics.conf" ]; then
 fi
 
 # Set app_info.inc.php
-mailcow_git_version=$(git describe --tags `git rev-list --tags --max-count=1`)
-mailcow_git_commit=$(git rev-parse HEAD)
-mailcow_git_commit_date=$(git show -s --format=%cd --date=format:'%Y-%m-%d %H:%M')
+if [ ${BRANCH} == "master" ]; then
+  mailcow_git_version=$(git describe --tags `git rev-list --tags --max-count=1`)
+elif [ ${BRANCH} == "nightly" ]; then
+  mailcow_git_version=$(git rev-parse --short $(git rev-parse @{upstream}))
+  mailcow_last_git_version=""
+else
+  mailcow_git_version=$(git rev-parse --short HEAD)
+  mailcow_last_git_version=""
+fi
+
+mailcow_git_commit=$(git rev-parse origin/${BRANCH})
+mailcow_git_commit_date=$(git log -1 --format=%ci @{upstream} )
 
 if [ $? -eq 0 ]; then
   echo '<?php' > data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_VERSION="'$mailcow_git_version'";' >> data/web/inc/app_info.inc.php
-
-  if [[ "$mailcow_git_version" != "$mailcow_last_git_version" ]]; then
-    echo '  $MAILCOW_LAST_GIT_VERSION="'$mailcow_last_git_version'";' >> data/web/inc/app_info.inc.php
-  fi
-
+  echo '  $MAILCOW_LAST_GIT_VERSION="";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_OWNER="mailcow";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_REPO="mailcow-dockerized";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_URL="https://github.com/mailcow/mailcow-dockerized";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_COMMIT="'$mailcow_git_commit'";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_COMMIT_DATE="'$mailcow_git_commit_date'";' >> data/web/inc/app_info.inc.php
-  echo '  $MAILCOW_BUILD="'$BUILD'";' >> data/web/inc/app_info.inc.php
+  echo '  $MAILCOW_BRANCH="'$BRANCH'";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_UPDATEDAT='$(date +%s)';' >> data/web/inc/app_info.inc.php
   echo '?>' >> data/web/inc/app_info.inc.php
 else
   echo '<?php' > data/web/inc/app_info.inc.php
-  echo '  $MAILCOW_GIT_VERSION="";' >> data/web/inc/app_info.inc.php
+  echo '  $MAILCOW_GIT_VERSION="'$mailcow_git_version'";' >> data/web/inc/app_info.inc.php
+  echo '  $MAILCOW_LAST_GIT_VERSION="";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_OWNER="mailcow";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_REPO="mailcow-dockerized";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_URL="https://github.com/mailcow/mailcow-dockerized";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_COMMIT="";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_GIT_COMMIT_DATE="";' >> data/web/inc/app_info.inc.php
-  echo '  $MAILCOW_BUILD="'$BUILD'";' >> data/web/inc/app_info.inc.php
+  echo '  $MAILCOW_BRANCH="'$BRANCH'";' >> data/web/inc/app_info.inc.php
   echo '  $MAILCOW_UPDATEDAT='$(date +%s)';' >> data/web/inc/app_info.inc.php
   echo '?>' >> data/web/inc/app_info.inc.php
   echo -e "\e[33mCannot determine current git repository version...\e[0m"
 fi
 
+# Set DOCKER_COMPOSE_VERSION
+sed -i 's/^DOCKER_COMPOSE_VERSION=$/DOCKER_COMPOSE_VERSION='$DOCKER_COMPOSE_VERSION'/g' mailcow.conf
+
 if [[ ${SKIP_START} == "y" ]]; then
-  echo -e "\e[33mNot starting mailcow, please run \"${COMPOSE_COMMAND} up -d --remove-orphans\" to start mailcow.\e[0m"
+  echo -e "\e[33mNot starting mailcow, please run \"$COMPOSE_COMMAND up -d --remove-orphans\" to start mailcow.\e[0m"
 else
   echo -e "\e[32mStarting mailcow...\e[0m"
   sleep 2
-  ${COMPOSE_COMMAND} up -d --remove-orphans
+  $COMPOSE_COMMAND up -d --remove-orphans
 fi
 
 echo -e "\e[32mCollecting garbage...\e[0m"
@@ -760,8 +929,8 @@ if [ -f "${SCRIPT_DIR}/post_update_hook.sh" ]; then
   bash "${SCRIPT_DIR}/post_update_hook.sh"
 fi
 
-#echo "In case you encounter any problem, hard-reset to a state before updating mailcow:"
-#echo
-#git reflog --color=always | grep "Before update on "
-#echo
-#echo "Use \"git reset --hard hash-on-the-left\" and run ${COMPOSE_COMMAND} up -d afterwards."
+# echo "In case you encounter any problem, hard-reset to a state before updating mailcow:"
+# echo
+# git reflog --color=always | grep "Before update on "
+# echo
+# echo "Use \"git reset --hard hash-on-the-left\" and run $COMPOSE_COMMAND up -d afterwards."