Merge pull request #4680 from mailcow/staging
Mooly Update 2022 - TFA Flow Update
This commit is contained in:
		
							
								
								
									
										120
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										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
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
...
 | 
					 | 
				
			||||||
							
								
								
									
										16
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										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,7 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## We stand with 🇺🇦
 | 
					## We stand with 🇺🇦
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[](https://drone.mailcow.email/mailcow/mailcow-dockerized) [](https://drone.mailcow.email/mailcow/mailcow-dockerized) [](https://translate.mailcow.email/engage/mailcow-dockerized/)
 | 
					[](https://translate.mailcow.email/engage/mailcow-dockerized/)
 | 
				
			||||||
[](https://twitter.com/mailcow_email)
 | 
					[](https://twitter.com/mailcow_email)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Want to support mailcow?
 | 
					## Want to support mailcow?
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										0
									
								
								create_cold_standby.sh
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										0
									
								
								create_cold_standby.sh
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							@@ -8,8 +8,14 @@ RUN apk upgrade --no-cache \
 | 
				
			|||||||
  bind-tools \
 | 
					  bind-tools \
 | 
				
			||||||
  bash 
 | 
					  bash 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY clamd.sh ./
 | 
					# init
 | 
				
			||||||
 | 
					COPY clamd.sh /clamd.sh
 | 
				
			||||||
RUN chmod +x /sbin/tini
 | 
					RUN chmod +x /sbin/tini
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# healthcheck
 | 
				
			||||||
 | 
					COPY healthcheck.sh /healthcheck.sh
 | 
				
			||||||
 | 
					RUN chmod +x /healthcheck.sh
 | 
				
			||||||
 | 
					HEALTHCHECK --start-period=6m CMD "/healthcheck.sh"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENTRYPOINT []
 | 
					ENTRYPOINT []
 | 
				
			||||||
CMD ["/sbin/tini", "-g", "--", "/clamd.sh"]
 | 
					CMD ["/sbin/tini", "-g", "--", "/clamd.sh"]
 | 
				
			||||||
							
								
								
									
										9
									
								
								data/Dockerfiles/clamd/healthcheck.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										9
									
								
								data/Dockerfiles/clamd/healthcheck.sh
									
									
									
									
									
										Executable file
									
								
							@@ -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
 | 
				
			||||||
@@ -2,7 +2,7 @@ FROM debian:bullseye-slim
 | 
				
			|||||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 | 
					LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ARG DEBIAN_FRONTEND=noninteractive
 | 
					ARG DEBIAN_FRONTEND=noninteractive
 | 
				
			||||||
ARG DOVECOT=2.3.18
 | 
					ARG DOVECOT=2.3.19.1
 | 
				
			||||||
ENV LC_ALL C
 | 
					ENV LC_ALL C
 | 
				
			||||||
ENV GOSU_VERSION 1.14
 | 
					ENV GOSU_VERSION 1.14
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,9 @@ symbols {
 | 
				
			|||||||
  "ENCRYPTED_CHAT" {
 | 
					  "ENCRYPTED_CHAT" {
 | 
				
			||||||
    score = -20.0;
 | 
					    score = -20.0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  "SOGO_CONTACT" {
 | 
				
			||||||
 | 
					    score = -99.0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
group "MX" {
 | 
					group "MX" {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3953,6 +3953,8 @@ paths:
 | 
				
			|||||||
          in: query
 | 
					          in: query
 | 
				
			||||||
          name: tags
 | 
					          name: tags
 | 
				
			||||||
          required: false
 | 
					          required: false
 | 
				
			||||||
 | 
					          schema:
 | 
				
			||||||
 | 
					            type: string
 | 
				
			||||||
        - description: e.g. api-key-string
 | 
					        - description: e.g. api-key-string
 | 
				
			||||||
          example: api-key-string
 | 
					          example: api-key-string
 | 
				
			||||||
          in: header
 | 
					          in: header
 | 
				
			||||||
@@ -4512,6 +4514,8 @@ paths:
 | 
				
			|||||||
          in: query
 | 
					          in: query
 | 
				
			||||||
          name: tags
 | 
					          name: tags
 | 
				
			||||||
          required: false
 | 
					          required: false
 | 
				
			||||||
 | 
					          schema:
 | 
				
			||||||
 | 
					            type: string
 | 
				
			||||||
        - description: e.g. api-key-string
 | 
					        - description: e.g. api-key-string
 | 
				
			||||||
          example: api-key-string
 | 
					          example: api-key-string
 | 
				
			||||||
          in: header
 | 
					          in: header
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -260,6 +260,17 @@ code {
 | 
				
			|||||||
  margin-right: 5px;
 | 
					  margin-right: 5px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.list-group-item.webauthn-authenticator-selection,
 | 
				
			||||||
 | 
					.list-group-item.totp-authenticator-selection,
 | 
				
			||||||
 | 
					.list-group-item.yubi_otp-authenticator-selection {
 | 
				
			||||||
 | 
					  border-radius: 0px !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.pending-tfa-collapse {
 | 
				
			||||||
 | 
					  padding: 10px;
 | 
				
			||||||
 | 
					  background: #fbfbfb;
 | 
				
			||||||
 | 
					  border: 1px solid #ededed;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.tag-box {
 | 
					.tag-box {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  flex-wrap: wrap;
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
@@ -296,4 +307,3 @@ code {
 | 
				
			|||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  display: inline-flex;
 | 
					  display: inline-flex;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,5 +2,5 @@
 | 
				
			|||||||
session_start();
 | 
					session_start();
 | 
				
			||||||
unset($_SESSION['pending_mailcow_cc_username']);
 | 
					unset($_SESSION['pending_mailcow_cc_username']);
 | 
				
			||||||
unset($_SESSION['pending_mailcow_cc_role']);
 | 
					unset($_SESSION['pending_mailcow_cc_role']);
 | 
				
			||||||
unset($_SESSION['pending_tfa_method']);
 | 
					unset($_SESSION['pending_tfa_methods']);
 | 
				
			||||||
?>
 | 
					?>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,27 @@ if (is_array($alertbox_log_parser)) {
 | 
				
			|||||||
  unset($_SESSION['return']);
 | 
					  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
 | 
					// globals
 | 
				
			||||||
$globalVariables = [
 | 
					$globalVariables = [
 | 
				
			||||||
  'mailcow_info' => array(
 | 
					  'mailcow_info' => array(
 | 
				
			||||||
@@ -30,7 +51,8 @@ $globalVariables = [
 | 
				
			|||||||
    'git_project_url' => $GLOBALS['MAILCOW_GIT_URL']
 | 
					    'git_project_url' => $GLOBALS['MAILCOW_GIT_URL']
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  'js_path' => '/cache/'.basename($JSPath),
 | 
					  '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'],
 | 
					  'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'],
 | 
				
			||||||
  'lang_footer' => json_encode($lang['footer']),
 | 
					  'lang_footer' => json_encode($lang['footer']),
 | 
				
			||||||
  'lang_acl' => json_encode($lang['acl']),
 | 
					  'lang_acl' => json_encode($lang['acl']),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -830,11 +830,15 @@ function check_login($user, $pass, $app_passwd_data = false) {
 | 
				
			|||||||
  $stmt->execute(array(':user' => $user));
 | 
					  $stmt->execute(array(':user' => $user));
 | 
				
			||||||
  $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 | 
					  $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 | 
				
			||||||
  foreach ($rows as $row) {
 | 
					  foreach ($rows as $row) {
 | 
				
			||||||
 | 
					    // verify password
 | 
				
			||||||
    if (verify_hash($row['password'], $pass)) {
 | 
					    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_username'] = $user;
 | 
				
			||||||
        $_SESSION['pending_mailcow_cc_role'] = "admin";
 | 
					        $_SESSION['pending_mailcow_cc_role'] = "admin";
 | 
				
			||||||
        $_SESSION['pending_tfa_method'] = get_tfa($user)['name'];
 | 
					        $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
 | 
				
			||||||
        unset($_SESSION['ldelay']);
 | 
					        unset($_SESSION['ldelay']);
 | 
				
			||||||
        $_SESSION['return'][] =  array(
 | 
					        $_SESSION['return'][] =  array(
 | 
				
			||||||
          'type' => 'info',
 | 
					          'type' => 'info',
 | 
				
			||||||
@@ -842,8 +846,7 @@ function check_login($user, $pass, $app_passwd_data = false) {
 | 
				
			|||||||
          'msg' => 'awaiting_tfa_confirmation'
 | 
					          'msg' => 'awaiting_tfa_confirmation'
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        return "pending";
 | 
					        return "pending";
 | 
				
			||||||
      }
 | 
					      } else {
 | 
				
			||||||
      else {
 | 
					 | 
				
			||||||
        unset($_SESSION['ldelay']);
 | 
					        unset($_SESSION['ldelay']);
 | 
				
			||||||
        // Reactivate TFA if it was set to "deactivate TFA for next login"
 | 
					        // Reactivate TFA if it was set to "deactivate TFA for next login"
 | 
				
			||||||
        $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
 | 
					        $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
 | 
				
			||||||
@@ -866,11 +869,14 @@ function check_login($user, $pass, $app_passwd_data = false) {
 | 
				
			|||||||
  $stmt->execute(array(':user' => $user));
 | 
					  $stmt->execute(array(':user' => $user));
 | 
				
			||||||
  $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 | 
					  $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 | 
				
			||||||
  foreach ($rows as $row) {
 | 
					  foreach ($rows as $row) {
 | 
				
			||||||
 | 
					    // verify password
 | 
				
			||||||
    if (verify_hash($row['password'], $pass) !== false) {
 | 
					    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_username'] = $user;
 | 
				
			||||||
        $_SESSION['pending_mailcow_cc_role'] = "domainadmin";
 | 
					        $_SESSION['pending_mailcow_cc_role'] = "domainadmin";
 | 
				
			||||||
        $_SESSION['pending_tfa_method'] = get_tfa($user)['name'];
 | 
					        $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
 | 
				
			||||||
        unset($_SESSION['ldelay']);
 | 
					        unset($_SESSION['ldelay']);
 | 
				
			||||||
        $_SESSION['return'][] =  array(
 | 
					        $_SESSION['return'][] =  array(
 | 
				
			||||||
          'type' => 'info',
 | 
					          'type' => 'info',
 | 
				
			||||||
@@ -930,24 +936,39 @@ function check_login($user, $pass, $app_passwd_data = false) {
 | 
				
			|||||||
    $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
 | 
					    $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  foreach ($rows as $row) {
 | 
					  foreach ($rows as $row) {
 | 
				
			||||||
 | 
					    // verify password
 | 
				
			||||||
    if (verify_hash($row['password'], $pass) !== false) {
 | 
					    if (verify_hash($row['password'], $pass) !== false) {
 | 
				
			||||||
      unset($_SESSION['ldelay']);
 | 
					      // check for tfa authenticators
 | 
				
			||||||
      $_SESSION['return'][] =  array(
 | 
					      $authenticators = get_tfa($user);
 | 
				
			||||||
        'type' => 'success',
 | 
					      if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
 | 
				
			||||||
        'log' => array(__FUNCTION__, $user, '*'),
 | 
					        $_SESSION['pending_mailcow_cc_username'] = $user;
 | 
				
			||||||
        'msg' => array('logged_in_as', $user)
 | 
					        $_SESSION['pending_mailcow_cc_role'] = "user";
 | 
				
			||||||
      );
 | 
					        $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
 | 
				
			||||||
      if ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {
 | 
					        unset($_SESSION['ldelay']);
 | 
				
			||||||
        $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV';
 | 
					        $_SESSION['return'][] =  array(
 | 
				
			||||||
        $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)");
 | 
					          'type' => 'success',
 | 
				
			||||||
        $stmt->execute(array(
 | 
					          'log' => array(__FUNCTION__, $user, '*'),
 | 
				
			||||||
          ':service' => $service,
 | 
					          'msg' => array('logged_in_as', $user)
 | 
				
			||||||
          ':app_id' => $row['app_passwd_id'],
 | 
					        );
 | 
				
			||||||
          ':username' => $user,
 | 
					        return "pending";
 | 
				
			||||||
          ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'])
 | 
					      } else {
 | 
				
			||||||
        ));
 | 
					        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'])
 | 
				
			||||||
 | 
					          ));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        unset($_SESSION['ldelay']);
 | 
				
			||||||
 | 
					        // Reactivate TFA if it was set to "deactivate TFA for next login"
 | 
				
			||||||
 | 
					        $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
 | 
				
			||||||
 | 
					        $stmt->execute(array(':user' => $user));
 | 
				
			||||||
 | 
					        return "user";
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return "user";
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1142,47 +1163,46 @@ function set_tfa($_data) {
 | 
				
			|||||||
  global $yubi;
 | 
					  global $yubi;
 | 
				
			||||||
  global $tfa;
 | 
					  global $tfa;
 | 
				
			||||||
  $_data_log = $_data;
 | 
					  $_data_log = $_data;
 | 
				
			||||||
 | 
					  $access_denied = null;
 | 
				
			||||||
  !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
 | 
					  !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
 | 
				
			||||||
  $username = $_SESSION['mailcow_cc_username'];
 | 
					  $username = $_SESSION['mailcow_cc_username'];
 | 
				
			||||||
  if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
 | 
					
 | 
				
			||||||
      $_SESSION['return'][] =  array(
 | 
					  // check for empty user and role
 | 
				
			||||||
        'type' => 'danger',
 | 
					  if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
 | 
				
			||||||
        'log' => array(__FUNCTION__, $_data_log),
 | 
					
 | 
				
			||||||
        'msg' => 'access_denied'
 | 
					  // check admin confirm password
 | 
				
			||||||
      );
 | 
					  if ($access_denied === null) {
 | 
				
			||||||
      return false;
 | 
					    $stmt = $pdo->prepare("SELECT `password` FROM `admin`
 | 
				
			||||||
  }
 | 
					        WHERE `username` = :username");
 | 
				
			||||||
  $stmt = $pdo->prepare("SELECT `password` FROM `admin`
 | 
					    $stmt->execute(array(':username' => $username));
 | 
				
			||||||
      WHERE `username` = :username");
 | 
					    $row = $stmt->fetch(PDO::FETCH_ASSOC);
 | 
				
			||||||
  $stmt->execute(array(':username' => $username));
 | 
					    if ($row) {
 | 
				
			||||||
  $row = $stmt->fetch(PDO::FETCH_ASSOC);
 | 
					      if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
 | 
				
			||||||
  $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
 | 
					      else $access_denied = false;
 | 
				
			||||||
  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;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  $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"]) {
 | 
					  switch ($_data["tfa_method"]) {
 | 
				
			||||||
    case "yubi_otp":
 | 
					    case "yubi_otp":
 | 
				
			||||||
@@ -1220,8 +1240,7 @@ function set_tfa($_data) {
 | 
				
			|||||||
        $yubico_modhex_id = substr($_data["otp_token"], 0, 12);
 | 
					        $yubico_modhex_id = substr($_data["otp_token"], 0, 12);
 | 
				
			||||||
        $stmt = $pdo->prepare("DELETE FROM `tfa`
 | 
					        $stmt = $pdo->prepare("DELETE FROM `tfa`
 | 
				
			||||||
          WHERE `username` = :username
 | 
					          WHERE `username` = :username
 | 
				
			||||||
            AND (`authmech` != 'yubi_otp')
 | 
					            AND (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
 | 
				
			||||||
            OR (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
 | 
					 | 
				
			||||||
        $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
 | 
					        $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
 | 
				
			||||||
        $stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES
 | 
					        $stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES
 | 
				
			||||||
          (:key_id, :username, 'yubi_otp', '1', :secret)");
 | 
					          (:key_id, :username, 'yubi_otp', '1', :secret)");
 | 
				
			||||||
@@ -1265,9 +1284,6 @@ function set_tfa($_data) {
 | 
				
			|||||||
    case "webauthn":
 | 
					    case "webauthn":
 | 
				
			||||||
        $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
 | 
					        $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`)
 | 
					        $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`)
 | 
				
			||||||
        VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')");
 | 
					        VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')");
 | 
				
			||||||
        $stmt->execute(array(
 | 
					        $stmt->execute(array(
 | 
				
			||||||
@@ -1439,25 +1455,27 @@ function unset_tfa_key($_data) {
 | 
				
			|||||||
  global $pdo;
 | 
					  global $pdo;
 | 
				
			||||||
  global $lang;
 | 
					  global $lang;
 | 
				
			||||||
  $_data_log = $_data;
 | 
					  $_data_log = $_data;
 | 
				
			||||||
 | 
					  $access_denied = null;
 | 
				
			||||||
  $id = intval($_data['unset_tfa_key']);
 | 
					  $id = intval($_data['unset_tfa_key']);
 | 
				
			||||||
  $username = $_SESSION['mailcow_cc_username'];
 | 
					  $username = $_SESSION['mailcow_cc_username'];
 | 
				
			||||||
  if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
 | 
					
 | 
				
			||||||
    $_SESSION['return'][] =  array(
 | 
					  // check for empty user and role
 | 
				
			||||||
      'type' => 'danger',
 | 
					  if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
 | 
				
			||||||
      'log' => array(__FUNCTION__, $_data_log),
 | 
					
 | 
				
			||||||
      'msg' => 'access_denied'
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    return false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    if (!is_numeric($id)) {
 | 
					    if (!is_numeric($id)) $access_denied = true;
 | 
				
			||||||
      $_SESSION['return'][] =  array(
 | 
					    
 | 
				
			||||||
 | 
					    // set access_denied error
 | 
				
			||||||
 | 
					    if ($access_denied){
 | 
				
			||||||
 | 
					      $_SESSION['return'][] = array(
 | 
				
			||||||
        'type' => 'danger',
 | 
					        'type' => 'danger',
 | 
				
			||||||
        'log' => array(__FUNCTION__, $_data_log),
 | 
					        'log' => array(__FUNCTION__, $_data_log),
 | 
				
			||||||
        'msg' => 'access_denied'
 | 
					        'msg' => 'access_denied'
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    } 
 | 
					    } 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // check if it's last key
 | 
				
			||||||
    $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
 | 
					    $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
 | 
				
			||||||
      WHERE `username` = :username AND `active` = '1'");
 | 
					      WHERE `username` = :username AND `active` = '1'");
 | 
				
			||||||
    $stmt->execute(array(':username' => $username));
 | 
					    $stmt->execute(array(':username' => $username));
 | 
				
			||||||
@@ -1470,6 +1488,8 @@ function unset_tfa_key($_data) {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // delete key
 | 
				
			||||||
    $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id");
 | 
					    $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id");
 | 
				
			||||||
    $stmt->execute(array(':username' => $username, ':id' => $id));
 | 
					    $stmt->execute(array(':username' => $username, ':id' => $id));
 | 
				
			||||||
    $_SESSION['return'][] =  array(
 | 
					    $_SESSION['return'][] =  array(
 | 
				
			||||||
@@ -1487,7 +1507,7 @@ function unset_tfa_key($_data) {
 | 
				
			|||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
function get_tfa($username = null) {
 | 
					function get_tfa($username = null, $id = null) {
 | 
				
			||||||
  global $pdo;
 | 
					  global $pdo;
 | 
				
			||||||
  if (isset($_SESSION['mailcow_cc_username'])) {
 | 
					  if (isset($_SESSION['mailcow_cc_username'])) {
 | 
				
			||||||
    $username = $_SESSION['mailcow_cc_username'];
 | 
					    $username = $_SESSION['mailcow_cc_username'];
 | 
				
			||||||
@@ -1495,92 +1515,120 @@ function get_tfa($username = null) {
 | 
				
			|||||||
  elseif (empty($username)) {
 | 
					  elseif (empty($username)) {
 | 
				
			||||||
    return false;
 | 
					    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"])) {
 | 
					  if (!isset($id)){
 | 
				
			||||||
    switch ($row["authmech"]) {
 | 
					    // fetch all tfa methods - just get information about possible authenticators
 | 
				
			||||||
      case "yubi_otp":
 | 
					    $stmt = $pdo->prepare("SELECT `id`, `key_id`, `authmech` FROM `tfa`
 | 
				
			||||||
        $data['name'] = "yubi_otp";
 | 
					        WHERE `username` = :username AND `active` = '1'");
 | 
				
			||||||
        $data['pretty'] = "Yubico OTP";
 | 
					    $stmt->execute(array(':username' => $username));
 | 
				
			||||||
        $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username");
 | 
					    $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
 | 
				
			||||||
        $stmt->execute(array(
 | 
					 
 | 
				
			||||||
          ':username' => $username,
 | 
					    // no tfa methods found
 | 
				
			||||||
        ));
 | 
					    if (count($results) == 0) {
 | 
				
			||||||
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 | 
					        $data['name'] = 'none';
 | 
				
			||||||
        while($row = array_shift($rows)) {
 | 
					        $data['pretty'] = "-";
 | 
				
			||||||
          $data['additional'][] = $row;
 | 
					        $data['additional'] = array();
 | 
				
			||||||
 | 
					        return $data;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $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;
 | 
					      else {
 | 
				
			||||||
      // 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;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        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;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return $data;
 | 
					 | 
				
			||||||
      break;
 | 
					 | 
				
			||||||
      default:
 | 
					 | 
				
			||||||
        $data['name'] = 'none';
 | 
					        $data['name'] = 'none';
 | 
				
			||||||
        $data['pretty'] = "-";
 | 
					        $data['pretty'] = "-";
 | 
				
			||||||
        return $data;
 | 
					        return $data;
 | 
				
			||||||
      break;
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  else {
 | 
					 | 
				
			||||||
    $data['name'] = 'none';
 | 
					 | 
				
			||||||
    $data['pretty'] = "-";
 | 
					 | 
				
			||||||
    return $data;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
function verify_tfa_login($username, $_data, $WebAuthn) {
 | 
					function verify_tfa_login($username, $_data) {
 | 
				
			||||||
    global $pdo;
 | 
					  global $pdo;
 | 
				
			||||||
    global $yubi;
 | 
					  global $yubi;
 | 
				
			||||||
    global $u2f;
 | 
					  global $u2f;
 | 
				
			||||||
    global $tfa;
 | 
					  global $tfa;
 | 
				
			||||||
 | 
					  global $WebAuthn;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if ($_data['tfa_method'] != 'u2f'){
 | 
				
			||||||
    $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`
 | 
					    $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`
 | 
				
			||||||
        WHERE `username` = :username AND `active` = '1'");
 | 
					        WHERE `username` = :username AND `id` = :id AND `active` = '1'");
 | 
				
			||||||
    $stmt->execute(array(':username' => $username));
 | 
					    $stmt->execute(array(':username' => $username, ':id' => $_data['id']));
 | 
				
			||||||
    $row = $stmt->fetch(PDO::FETCH_ASSOC);
 | 
					    $row = $stmt->fetch(PDO::FETCH_ASSOC);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    switch ($row["authmech"]) {
 | 
					    switch ($row["authmech"]) {
 | 
				
			||||||
@@ -1597,9 +1645,10 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
 | 
				
			|||||||
            $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
 | 
					            $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
 | 
				
			||||||
                WHERE `username` = :username
 | 
					                WHERE `username` = :username
 | 
				
			||||||
                AND `authmech` = 'yubi_otp'
 | 
					                AND `authmech` = 'yubi_otp'
 | 
				
			||||||
                AND `active`='1'
 | 
					                AND `id` = :id
 | 
				
			||||||
 | 
					                AND `active` = '1'
 | 
				
			||||||
                AND `secret` LIKE :modhex");
 | 
					                AND `secret` LIKE :modhex");
 | 
				
			||||||
            $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
 | 
					            $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id, ':id' => $_data['id']));
 | 
				
			||||||
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
 | 
					            $row = $stmt->fetch(PDO::FETCH_ASSOC);
 | 
				
			||||||
            $yubico_auth = explode(':', $row['secret']);
 | 
					            $yubico_auth = explode(':', $row['secret']);
 | 
				
			||||||
            $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);
 | 
					            $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);
 | 
				
			||||||
@@ -1632,15 +1681,16 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
 | 
				
			|||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
        case "totp":
 | 
					        case "totp":
 | 
				
			||||||
            try {
 | 
					          try {
 | 
				
			||||||
            $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
 | 
					            $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
 | 
				
			||||||
                WHERE `username` = :username
 | 
					                WHERE `username` = :username
 | 
				
			||||||
                AND `authmech` = 'totp'
 | 
					                AND `authmech` = 'totp'
 | 
				
			||||||
 | 
					                AND `id` = :id
 | 
				
			||||||
                AND `active`='1'");
 | 
					                AND `active`='1'");
 | 
				
			||||||
            $stmt->execute(array(':username' => $username));
 | 
					            $stmt->execute(array(':username' => $username, ':id' => $_data['id']));
 | 
				
			||||||
            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 | 
					            $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 | 
				
			||||||
            foreach ($rows as $row) {
 | 
					            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['tfa_id'] = $row['id'];
 | 
				
			||||||
                $_SESSION['return'][] =  array(
 | 
					                $_SESSION['return'][] =  array(
 | 
				
			||||||
                    'type' => 'success',
 | 
					                    'type' => 'success',
 | 
				
			||||||
@@ -1648,7 +1698,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
 | 
				
			|||||||
                    'msg' => 'verified_totp_login'
 | 
					                    'msg' => 'verified_totp_login'
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
                return true;
 | 
					                return true;
 | 
				
			||||||
                }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            $_SESSION['return'][] =  array(
 | 
					            $_SESSION['return'][] =  array(
 | 
				
			||||||
                'type' => 'danger',
 | 
					                'type' => 'danger',
 | 
				
			||||||
@@ -1656,23 +1706,16 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
 | 
				
			|||||||
                'msg' => 'totp_verification_failed'
 | 
					                'msg' => 'totp_verification_failed'
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
            }
 | 
					          }
 | 
				
			||||||
            catch (PDOException $e) {
 | 
					          catch (PDOException $e) {
 | 
				
			||||||
            $_SESSION['return'][] =  array(
 | 
					            $_SESSION['return'][] =  array(
 | 
				
			||||||
                'type' => 'danger',
 | 
					                'type' => 'danger',
 | 
				
			||||||
                'log' => array(__FUNCTION__, $username, '*'),
 | 
					                'log' => array(__FUNCTION__, $username, '*'),
 | 
				
			||||||
                'msg' => array('mysql_error', $e)
 | 
					                'msg' => array('mysql_error', $e)
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
            }
 | 
					          }
 | 
				
			||||||
        break;
 | 
					        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":
 | 
					        case "webauthn":
 | 
				
			||||||
            $tokenData = json_decode($_data['token']);
 | 
					            $tokenData = json_decode($_data['token']);
 | 
				
			||||||
            $clientDataJSON = base64_decode($tokenData->clientDataJSON);
 | 
					            $clientDataJSON = base64_decode($tokenData->clientDataJSON);
 | 
				
			||||||
@@ -1681,13 +1724,20 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
 | 
				
			|||||||
            $id = base64_decode($tokenData->id);
 | 
					            $id = base64_decode($tokenData->id);
 | 
				
			||||||
            $challenge = $_SESSION['challenge'];
 | 
					            $challenge = $_SESSION['challenge'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $stmt = $pdo->prepare("SELECT `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `keyHandle` = :tokenId");
 | 
					            $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `id` = :id AND `active`='1'");
 | 
				
			||||||
            $stmt->execute(array(':tokenId' => $tokenData->id));
 | 
					            $stmt->execute(array(':id' => $_data['id']));
 | 
				
			||||||
            $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC);
 | 
					            $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(
 | 
					                $_SESSION['return'][] =  array(
 | 
				
			||||||
                    'type' => 'danger',
 | 
					                    'type' => 'danger',
 | 
				
			||||||
                    'log' => array(__FUNCTION__, $username, '*'),
 | 
					                    'log' => array(__FUNCTION__, $username, '*'),
 | 
				
			||||||
@@ -1695,6 +1745,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
 | 
				
			|||||||
                );
 | 
					                );
 | 
				
			||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
 | 
					                $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -1707,26 +1758,31 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
 | 
				
			|||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            $stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username");
 | 
					            $stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username");
 | 
				
			||||||
            $stmt->execute(array(':username' => $process_webauthn['username']));
 | 
					            $stmt->execute(array(':username' => $process_webauthn['username']));
 | 
				
			||||||
            $obj_props = $stmt->fetch(PDO::FETCH_ASSOC);
 | 
					            $obj_props = $stmt->fetch(PDO::FETCH_ASSOC);
 | 
				
			||||||
            if ($obj_props['superadmin'] === 1) {
 | 
					            if ($obj_props['superadmin'] === 1) {
 | 
				
			||||||
                $_SESSION["mailcow_cc_role"] = "admin";
 | 
					              $_SESSION["mailcow_cc_role"] = "admin";
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            elseif ($obj_props['superadmin'] === 0) {
 | 
					            elseif ($obj_props['superadmin'] === 0) {
 | 
				
			||||||
                $_SESSION["mailcow_cc_role"] = "domainadmin";
 | 
					              $_SESSION["mailcow_cc_role"] = "domainadmin";
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            else {
 | 
					            else {
 | 
				
			||||||
                $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username");
 | 
					              $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username");
 | 
				
			||||||
                $stmt->execute(array(':username' => $process_webauthn['username']));
 | 
					              $stmt->execute(array(':username' => $process_webauthn['username']));
 | 
				
			||||||
                $row = $stmt->fetch(PDO::FETCH_ASSOC);
 | 
					              $row = $stmt->fetch(PDO::FETCH_ASSOC);
 | 
				
			||||||
                if ($row['username'] == $process_webauthn['username']) {
 | 
					              if (!empty($row['username'])) {
 | 
				
			||||||
                $_SESSION["mailcow_cc_role"] = "user";
 | 
					                $_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']){
 | 
					            if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
 | 
				
			||||||
                $_SESSION['return'][] =  array(
 | 
					                $_SESSION['return'][] =  array(
 | 
				
			||||||
                    'type' => 'danger',
 | 
					                    'type' => 'danger',
 | 
				
			||||||
@@ -1736,9 +1792,8 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
 | 
				
			|||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
            $_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
 | 
					            $_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
 | 
				
			||||||
            $_SESSION['tfa_id'] = $process_webauthn['key_id'];
 | 
					            $_SESSION['tfa_id'] = $process_webauthn['id'];
 | 
				
			||||||
            $_SESSION['authReq'] = null;
 | 
					            $_SESSION['authReq'] = null;
 | 
				
			||||||
            unset($_SESSION["challenge"]);
 | 
					            unset($_SESSION["challenge"]);
 | 
				
			||||||
            $_SESSION['return'][] =  array(
 | 
					            $_SESSION['return'][] =  array(
 | 
				
			||||||
@@ -1759,6 +1814,17 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return false;
 | 
					    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) {
 | 
					function admin_api($access, $action, $data = null) {
 | 
				
			||||||
  global $pdo;
 | 
					  global $pdo;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -338,9 +338,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
 | 
				
			|||||||
          $custom_params        = (empty(trim($_data['custom_params']))) ? '' : trim($_data['custom_params']);
 | 
					          $custom_params        = (empty(trim($_data['custom_params']))) ? '' : trim($_data['custom_params']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          // validate custom params
 | 
					          // validate custom params
 | 
				
			||||||
          foreach (explode(' -', $custom_params) as $param){
 | 
					          foreach (explode('-', $custom_params) as $param){
 | 
				
			||||||
            if(empty($param)) continue;
 | 
					            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, ' ')) {
 | 
					            if (str_contains($param, ' ')) {
 | 
				
			||||||
              // bad char
 | 
					              // bad char
 | 
				
			||||||
              $_SESSION['return'][] = array(
 | 
					              $_SESSION['return'][] = array(
 | 
				
			||||||
@@ -351,11 +357,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
 | 
				
			|||||||
              return false;
 | 
					              return false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // extract option
 | 
					 | 
				
			||||||
            if (str_contains($param, '=')) $param = explode('=', $param)[0];
 | 
					 | 
				
			||||||
            // remove first char if first char is -
 | 
					 | 
				
			||||||
            if ($param[0] == '-') $param = ltrim($param, $param[0]);
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            // check if param is whitelisted
 | 
					            // check if param is whitelisted
 | 
				
			||||||
            if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){
 | 
					            if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){
 | 
				
			||||||
              // bad option
 | 
					              // bad option
 | 
				
			||||||
@@ -1793,9 +1794,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // validate custom params
 | 
					            // validate custom params
 | 
				
			||||||
            foreach (explode(' -', $custom_params) as $param){
 | 
					            foreach (explode('-', $custom_params) as $param){
 | 
				
			||||||
              if(empty($param)) continue;
 | 
					              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, ' ')) {
 | 
					              if (str_contains($param, ' ')) {
 | 
				
			||||||
                // bad char
 | 
					                // bad char
 | 
				
			||||||
                $_SESSION['return'][] = array(
 | 
					                $_SESSION['return'][] = array(
 | 
				
			||||||
@@ -1806,11 +1813,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
 | 
				
			|||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
              // extract option
 | 
					 | 
				
			||||||
              if (str_contains($param, '=')) $param = explode('=', $param)[0];
 | 
					 | 
				
			||||||
              // remove first char if first char is -
 | 
					 | 
				
			||||||
              if ($param[0] == '-') $param = ltrim($param, $param[0]);
 | 
					 | 
				
			||||||
              
 | 
					 | 
				
			||||||
              // check if param is whitelisted
 | 
					              // check if param is whitelisted
 | 
				
			||||||
              if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){
 | 
					              if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){
 | 
				
			||||||
                // bad option
 | 
					                // bad option
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@ function init_db_schema() {
 | 
				
			|||||||
  try {
 | 
					  try {
 | 
				
			||||||
    global $pdo;
 | 
					    global $pdo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $db_version = "18062022_1153";
 | 
					    $db_version = "13072022_1700";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
 | 
					    $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
 | 
				
			||||||
    $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
 | 
					    $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
 | 
				
			||||||
@@ -739,7 +739,7 @@ function init_db_schema() {
 | 
				
			|||||||
          "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')",
 | 
					          "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')",
 | 
				
			||||||
          "secret" => "VARCHAR(255) DEFAULT NULL",
 | 
					          "secret" => "VARCHAR(255) DEFAULT NULL",
 | 
				
			||||||
          "keyHandle" => "VARCHAR(255) DEFAULT NULL",
 | 
					          "keyHandle" => "VARCHAR(255) DEFAULT NULL",
 | 
				
			||||||
          "publicKey" => "VARCHAR(255) DEFAULT NULL",
 | 
					          "publicKey" => "VARCHAR(4096) DEFAULT NULL",
 | 
				
			||||||
          "counter" => "INT NOT NULL DEFAULT '0'",
 | 
					          "counter" => "INT NOT NULL DEFAULT '0'",
 | 
				
			||||||
          "certificate" => "TEXT",
 | 
					          "certificate" => "TEXT",
 | 
				
			||||||
          "active" => "TINYINT(1) NOT NULL DEFAULT '0'"
 | 
					          "active" => "TINYINT(1) NOT NULL DEFAULT '0'"
 | 
				
			||||||
@@ -1227,7 +1227,7 @@ function init_db_schema() {
 | 
				
			|||||||
      $pdo->query($create);
 | 
					      $pdo->query($create);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Mitigate imapsync pipemess issue
 | 
					    // Mitigate imapsync argument injection issue
 | 
				
			||||||
    $pdo->query("UPDATE `imapsync` SET `custom_params` = '' 
 | 
					    $pdo->query("UPDATE `imapsync` SET `custom_params` = '' 
 | 
				
			||||||
      WHERE `custom_params` LIKE '%pipemess%' 
 | 
					      WHERE `custom_params` LIKE '%pipemess%' 
 | 
				
			||||||
        OR custom_params LIKE '%skipmess%' 
 | 
					        OR custom_params LIKE '%skipmess%' 
 | 
				
			||||||
@@ -1238,7 +1238,6 @@ function init_db_schema() {
 | 
				
			|||||||
        OR custom_params LIKE '%regextrans2%' 
 | 
					        OR custom_params LIKE '%regextrans2%' 
 | 
				
			||||||
        OR custom_params LIKE '%maxlinelengthcmd%';");
 | 
					        OR custom_params LIKE '%maxlinelengthcmd%';");
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Migrate webauthn tfa
 | 
					    // Migrate webauthn tfa
 | 
				
			||||||
    $stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')");
 | 
					    $stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -66,8 +66,9 @@ $qrprovider = new RobThree\Auth\Providers\Qr\QRServerProvider();
 | 
				
			|||||||
$tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider);
 | 
					$tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// FIDO2
 | 
					// FIDO2
 | 
				
			||||||
 | 
					$server_name = parse_url('https://' . $_SERVER['HTTP_HOST'], PHP_URL_HOST);
 | 
				
			||||||
$formats = $GLOBALS['FIDO2_FORMATS'];
 | 
					$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
 | 
					// only include root ca's when needed
 | 
				
			||||||
if (getenv('WEBAUTHN_ONLY_TRUSTED_VENDORS') == 'y') $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates');
 | 
					if (getenv('WEBAUTHN_ONLY_TRUSTED_VENDORS') == 'y') $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,24 +1,24 @@
 | 
				
			|||||||
<?php
 | 
					<?php
 | 
				
			||||||
if (isset($_POST["verify_tfa_login"])) {
 | 
					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_username'] = $_SESSION['pending_mailcow_cc_username'];
 | 
				
			||||||
    $_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role'];
 | 
					    $_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role'];
 | 
				
			||||||
    unset($_SESSION['pending_mailcow_cc_username']);
 | 
					    unset($_SESSION['pending_mailcow_cc_username']);
 | 
				
			||||||
    unset($_SESSION['pending_mailcow_cc_role']);
 | 
					    unset($_SESSION['pending_mailcow_cc_role']);
 | 
				
			||||||
    unset($_SESSION['pending_tfa_method']);
 | 
					    unset($_SESSION['pending_tfa_methods']);
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
    header("Location: /user");
 | 
					    header("Location: /user");
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    unset($_SESSION['pending_mailcow_cc_username']);
 | 
					    unset($_SESSION['pending_mailcow_cc_username']);
 | 
				
			||||||
    unset($_SESSION['pending_mailcow_cc_role']);
 | 
					    unset($_SESSION['pending_mailcow_cc_role']);
 | 
				
			||||||
    unset($_SESSION['pending_tfa_method']);
 | 
					    unset($_SESSION['pending_tfa_methods']);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (isset($_GET["cancel_tfa_login"])) {
 | 
					if (isset($_GET["cancel_tfa_login"])) {
 | 
				
			||||||
    unset($_SESSION['pending_mailcow_cc_username']);
 | 
					    unset($_SESSION['pending_mailcow_cc_username']);
 | 
				
			||||||
    unset($_SESSION['pending_mailcow_cc_role']);
 | 
					    unset($_SESSION['pending_mailcow_cc_role']);
 | 
				
			||||||
    unset($_SESSION['pending_tfa_method']);
 | 
					    unset($_SESSION['pending_tfa_methods']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    header("Location: /");
 | 
					    header("Location: /");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -34,6 +34,7 @@ if (isset($_POST["quick_delete"])) {
 | 
				
			|||||||
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
 | 
					if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
 | 
				
			||||||
	$login_user = strtolower(trim($_POST["login_user"]));
 | 
						$login_user = strtolower(trim($_POST["login_user"]));
 | 
				
			||||||
	$as = check_login($login_user, $_POST["pass_user"]);
 | 
						$as = check_login($login_user, $_POST["pass_user"]);
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
	if ($as == "admin") {
 | 
						if ($as == "admin") {
 | 
				
			||||||
		$_SESSION['mailcow_cc_username'] = $login_user;
 | 
							$_SESSION['mailcow_cc_username'] = $login_user;
 | 
				
			||||||
		$_SESSION['mailcow_cc_role'] = "admin";
 | 
							$_SESSION['mailcow_cc_role'] = "admin";
 | 
				
			||||||
@@ -47,22 +48,22 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
 | 
				
			|||||||
	elseif ($as == "user") {
 | 
						elseif ($as == "user") {
 | 
				
			||||||
		$_SESSION['mailcow_cc_username'] = $login_user;
 | 
							$_SESSION['mailcow_cc_username'] = $login_user;
 | 
				
			||||||
		$_SESSION['mailcow_cc_role'] = "user";
 | 
							$_SESSION['mailcow_cc_role'] = "user";
 | 
				
			||||||
    $http_parameters = explode('&', $_SESSION['index_query_string']);
 | 
					        $http_parameters = explode('&', $_SESSION['index_query_string']);
 | 
				
			||||||
    unset($_SESSION['index_query_string']);
 | 
					        unset($_SESSION['index_query_string']);
 | 
				
			||||||
    if (in_array('mobileconfig', $http_parameters)) {
 | 
					        if (in_array('mobileconfig', $http_parameters)) {
 | 
				
			||||||
      if (in_array('only_email', $http_parameters)) {
 | 
					            if (in_array('only_email', $http_parameters)) {
 | 
				
			||||||
        header("Location: /mobileconfig.php?email_only");
 | 
					                header("Location: /mobileconfig.php?email_only");
 | 
				
			||||||
        die();
 | 
					                die();
 | 
				
			||||||
      }
 | 
					            }
 | 
				
			||||||
      header("Location: /mobileconfig.php");
 | 
					            header("Location: /mobileconfig.php");
 | 
				
			||||||
      die();
 | 
					            die();
 | 
				
			||||||
    }
 | 
					        }
 | 
				
			||||||
		header("Location: /user");
 | 
							header("Location: /user");
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	elseif ($as != "pending") {
 | 
						elseif ($as != "pending") {
 | 
				
			||||||
    unset($_SESSION['pending_mailcow_cc_username']);
 | 
					    unset($_SESSION['pending_mailcow_cc_username']);
 | 
				
			||||||
    unset($_SESSION['pending_mailcow_cc_role']);
 | 
					    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_username']);
 | 
				
			||||||
		unset($_SESSION['mailcow_cc_role']);
 | 
							unset($_SESSION['mailcow_cc_role']);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -232,131 +232,127 @@ $RSPAMD_MAPS = array(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
$IMAPSYNC_OPTIONS = array(
 | 
					$IMAPSYNC_OPTIONS = array(
 | 
				
			||||||
  'whitelist' => array(
 | 
					  'whitelist' => array(
 | 
				
			||||||
      'log',
 | 
					    'authmech1',
 | 
				
			||||||
      'showpasswords',   
 | 
					    'authmech2',
 | 
				
			||||||
      'nossl1',            
 | 
					    'authuser1', 
 | 
				
			||||||
      'nossl2',            
 | 
					    'authuser2', 
 | 
				
			||||||
      'ssl2',              
 | 
					    'debugcontent', 
 | 
				
			||||||
      'notls1',             
 | 
					    'disarmreadreceipts', 
 | 
				
			||||||
      'notls2',            
 | 
					    'logdir',
 | 
				
			||||||
      'tls2',              
 | 
					    'debugcrossduplicates', 
 | 
				
			||||||
      'debugssl', 
 | 
					    'maxsize',
 | 
				
			||||||
      'sslargs1',
 | 
					    'minsize',
 | 
				
			||||||
      'sslargs2', 
 | 
					    'minage',
 | 
				
			||||||
      'authmech1',
 | 
					    'search', 
 | 
				
			||||||
      'authmech2',
 | 
					    'noabletosearch', 
 | 
				
			||||||
      'authuser1', 
 | 
					    'pidfile', 
 | 
				
			||||||
      'authuser2',  
 | 
					    'pidfilelocking', 
 | 
				
			||||||
      'proxyauth1',        
 | 
					    'search1',
 | 
				
			||||||
      'proxyauth2',        
 | 
					    'search2', 
 | 
				
			||||||
      'authmd51',          
 | 
					    'sslargs1',
 | 
				
			||||||
      'authmd52',         
 | 
					    'sslargs2', 
 | 
				
			||||||
      'domain1',
 | 
					    'syncduplicates',
 | 
				
			||||||
      'domain2',
 | 
					    'usecache', 
 | 
				
			||||||
      'oauthaccesstoken1',
 | 
					    'synclabels', 
 | 
				
			||||||
      'oauthaccesstoken2',
 | 
					    'truncmess',  
 | 
				
			||||||
      'oauthdirect1',
 | 
					    'domino2',  
 | 
				
			||||||
      'oauthdirect2',
 | 
					    'expunge1',  
 | 
				
			||||||
      'folder',
 | 
					    'filterbuggyflags',  
 | 
				
			||||||
      'folder', 
 | 
					    'justconnect',  
 | 
				
			||||||
      'folderrec',
 | 
					    'justfolders',  
 | 
				
			||||||
      'folderrec', 
 | 
					    'maxlinelength',
 | 
				
			||||||
      'folderfirst',
 | 
					    'useheader',  
 | 
				
			||||||
      'folderfirst', 
 | 
					    'noabletosearch1',  
 | 
				
			||||||
      'folderlast',
 | 
					    'nolog',  
 | 
				
			||||||
      'folderlast',
 | 
					    'prefix1',
 | 
				
			||||||
      'nomixfolders',  
 | 
					    'prefix2',
 | 
				
			||||||
      'skipemptyfolders',
 | 
					    'sep1',
 | 
				
			||||||
      'include',
 | 
					    'sep2',
 | 
				
			||||||
      'include',
 | 
					    'nofoldersizesatend',
 | 
				
			||||||
      'subfolder1',
 | 
					    'justfoldersizes',  
 | 
				
			||||||
      'subscribed',
 | 
					    'proxyauth1',  
 | 
				
			||||||
      'subscribe',  
 | 
					    'skipemptyfolders',
 | 
				
			||||||
      'prefix1',
 | 
					    'include',
 | 
				
			||||||
      'prefix2',
 | 
					    'subfolder1',
 | 
				
			||||||
      'sep1',
 | 
					    'subscribed',
 | 
				
			||||||
      'sep2',
 | 
					    'subscribe',   
 | 
				
			||||||
      'nofoldersizesatend',
 | 
					    'debug',   
 | 
				
			||||||
      'justfoldersizes', 
 | 
					    'debugimap2',   
 | 
				
			||||||
      'pidfile', 
 | 
					    'domino1',   
 | 
				
			||||||
      'pidfilelocking',  
 | 
					    'exchange1',   
 | 
				
			||||||
      'nolog',        
 | 
					    'exchange2',   
 | 
				
			||||||
      'logfile', 
 | 
					    'justlogin',   
 | 
				
			||||||
      'logdir',
 | 
					    'keepalive1',   
 | 
				
			||||||
      'debugcrossduplicates', 
 | 
					    'keepalive2',   
 | 
				
			||||||
      'disarmreadreceipts', 
 | 
					    'noabletosearch2',   
 | 
				
			||||||
      'truncmess', 
 | 
					    'noexpunge2',   
 | 
				
			||||||
      'synclabels',     
 | 
					    'noresyncflags',   
 | 
				
			||||||
      'resynclabels',     
 | 
					    'nossl1',   
 | 
				
			||||||
      'resyncflags',   
 | 
					    'nouidexpunge2',   
 | 
				
			||||||
      'noresyncflags',  
 | 
					    'syncinternaldates',
 | 
				
			||||||
      'filterbuggyflags',  
 | 
					    'idatefromheader',   
 | 
				
			||||||
      'expunge1',       
 | 
					    'useuid',    
 | 
				
			||||||
      'noexpunge1',    
 | 
					    'debugflags',    
 | 
				
			||||||
      'delete1emptyfolders',
 | 
					    'debugimap',    
 | 
				
			||||||
      'delete2folders',   
 | 
					    'delete1emptyfolders',
 | 
				
			||||||
      'noexpunge2',   
 | 
					    'delete2folders',    
 | 
				
			||||||
      'nouidexpunge2',   
 | 
					    'gmail2',    
 | 
				
			||||||
      'syncinternaldates',
 | 
					    'office1',    
 | 
				
			||||||
      'idatefromheader', 
 | 
					    'testslive6',     
 | 
				
			||||||
      'maxsize',
 | 
					    'debugimap1',     
 | 
				
			||||||
      'minsize',
 | 
					    'errorsmax',
 | 
				
			||||||
      'minage',
 | 
					    'tests',     
 | 
				
			||||||
      'search', 
 | 
					    'gmail1',     
 | 
				
			||||||
      'search1',
 | 
					    'maxmessagespersecond',
 | 
				
			||||||
      'search2', 
 | 
					    'maxbytesafter',
 | 
				
			||||||
      'noabletosearch',  
 | 
					    'maxsleep',
 | 
				
			||||||
      'noabletosearch1',   
 | 
					    'abort',     
 | 
				
			||||||
      'noabletosearch2',  
 | 
					    'resyncflags',     
 | 
				
			||||||
      'maxlinelength',
 | 
					    'resynclabels',     
 | 
				
			||||||
      'useheader',
 | 
					    'syncacls',
 | 
				
			||||||
      'useheader',   
 | 
					    'nosyncacls',      
 | 
				
			||||||
      'syncduplicates',
 | 
					    'nousecache',      
 | 
				
			||||||
      'usecache',      
 | 
					    'office2',      
 | 
				
			||||||
      'nousecache',   
 | 
					    'testslive',       
 | 
				
			||||||
      'useuid',     
 | 
					    'debugmemory',       
 | 
				
			||||||
      'syncacls',
 | 
					    'exitwhenover',
 | 
				
			||||||
      'nosyncacls',   
 | 
					    'noid',       
 | 
				
			||||||
      'debug',           
 | 
					    'noexpunge1',        
 | 
				
			||||||
      'debugfolders', 
 | 
					    'authmd51',        
 | 
				
			||||||
      'debugcontent',    
 | 
					    'logfile',        
 | 
				
			||||||
      'debugflags',     
 | 
					    'proxyauth2',         
 | 
				
			||||||
      'debugimap1',   
 | 
					    'domain1',
 | 
				
			||||||
      'debugimap2',    
 | 
					    'domain2',
 | 
				
			||||||
      'debugimap',       
 | 
					    'oauthaccesstoken1',
 | 
				
			||||||
      'debugmemory',     
 | 
					    'oauthaccesstoken2',
 | 
				
			||||||
      'errorsmax',
 | 
					    'oauthdirect1',
 | 
				
			||||||
      'tests',      
 | 
					    'oauthdirect2',
 | 
				
			||||||
      'testslive',    
 | 
					    'folder',
 | 
				
			||||||
      'testslive6',     
 | 
					    'folderrec',
 | 
				
			||||||
      'gmail1',    
 | 
					    'folderfirst',
 | 
				
			||||||
      'gmail2',    
 | 
					    'folderlast',
 | 
				
			||||||
      'office1',      
 | 
					    'nomixfolders',          
 | 
				
			||||||
      'office2',   
 | 
					    'authmd52',           
 | 
				
			||||||
      'exchange1',   
 | 
					    'debugfolders',            
 | 
				
			||||||
      'exchange2',   
 | 
					    'nossl2',            
 | 
				
			||||||
      'domino1',  
 | 
					    'ssl2',            
 | 
				
			||||||
      'domino2',   
 | 
					    'tls2',             
 | 
				
			||||||
      'keepalive1',   
 | 
					    'notls2',              
 | 
				
			||||||
      'keepalive2',     
 | 
					    'debugssl',              
 | 
				
			||||||
      'maxmessagespersecond',
 | 
					    'notls1', 
 | 
				
			||||||
      'maxbytesafter',
 | 
					    'inet4',
 | 
				
			||||||
      'maxsleep',
 | 
					    'inet6',
 | 
				
			||||||
      'abort',       
 | 
					    'log',
 | 
				
			||||||
      'exitwhenover',
 | 
					    'showpasswords'
 | 
				
			||||||
      'noid',  
 | 
					 | 
				
			||||||
      'justconnect',   
 | 
					 | 
				
			||||||
      'justlogin',  
 | 
					 | 
				
			||||||
      'justfolders'
 | 
					 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
  'blacklist' => array(
 | 
					  'blacklist' => array(
 | 
				
			||||||
      'skipmess',
 | 
					    'skipmess',
 | 
				
			||||||
      'delete2foldersonly',
 | 
					    'delete2foldersonly',
 | 
				
			||||||
      'delete2foldersbutnot',
 | 
					    'delete2foldersbutnot',
 | 
				
			||||||
      'regexflag',
 | 
					    'regexflag',
 | 
				
			||||||
      'regexmess',
 | 
					    'regexmess',
 | 
				
			||||||
      'pipemess',
 | 
					    'pipemess',
 | 
				
			||||||
      'regextrans2',
 | 
					    'regextrans2',
 | 
				
			||||||
      'maxlinelengthcmd'
 | 
					    'maxlinelengthcmd'
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -179,14 +179,21 @@ if (isset($_GET['query'])) {
 | 
				
			|||||||
              $post = trim(file_get_contents('php://input'));
 | 
					              $post = trim(file_get_contents('php://input'));
 | 
				
			||||||
              if ($post) $post = json_decode($post);
 | 
					              if ($post) $post = json_decode($post);
 | 
				
			||||||
              
 | 
					              
 | 
				
			||||||
              // decode base64 strings
 | 
					 | 
				
			||||||
              $clientDataJSON = base64_decode($post->clientDataJSON);
 | 
					 | 
				
			||||||
              $attestationObject = base64_decode($post->attestationObject);             
 | 
					 | 
				
			||||||
              
 | 
					 | 
				
			||||||
              // process registration data from authenticator
 | 
					              // process registration data from authenticator
 | 
				
			||||||
              try {
 | 
					              try {
 | 
				
			||||||
 | 
					                // decode base64 strings
 | 
				
			||||||
 | 
					                $clientDataJSON = base64_decode($post->clientDataJSON);
 | 
				
			||||||
 | 
					                $attestationObject = base64_decode($post->attestationObject);   
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                // processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true)
 | 
					                // processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true)
 | 
				
			||||||
                $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $_SESSION['challenge'], false, 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) {
 | 
					              catch (Throwable $ex) {
 | 
				
			||||||
                // err
 | 
					                // err
 | 
				
			||||||
@@ -197,11 +204,6 @@ if (isset($_GET['query'])) {
 | 
				
			|||||||
                exit;
 | 
					                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
 | 
					              // send response
 | 
				
			||||||
              $return = new stdClass();
 | 
					              $return = new stdClass();
 | 
				
			||||||
@@ -419,7 +421,7 @@ if (isset($_GET['query'])) {
 | 
				
			|||||||
          // }
 | 
					          // }
 | 
				
			||||||
          $ids = NULL;
 | 
					          $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));
 | 
					          print(json_encode($getArgs));
 | 
				
			||||||
          $_SESSION['challenge'] = $WebAuthn->getChallenge();
 | 
					          $_SESSION['challenge'] = $WebAuthn->getChallenge();
 | 
				
			||||||
          return;
 | 
					          return;
 | 
				
			||||||
@@ -428,8 +430,11 @@ if (isset($_GET['query'])) {
 | 
				
			|||||||
        case "webauthn-tfa-registration":
 | 
					        case "webauthn-tfa-registration":
 | 
				
			||||||
          if (isset($_SESSION["mailcow_cc_role"])) {
 | 
					          if (isset($_SESSION["mailcow_cc_role"])) {
 | 
				
			||||||
              // Exclude existing CredentialIds, if any
 | 
					              // Exclude existing CredentialIds, if any
 | 
				
			||||||
              $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username");
 | 
					              $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username AND authmech = :authmech");
 | 
				
			||||||
              $stmt->execute(array(':username' => $_SESSION['mailcow_cc_username']));
 | 
					              $stmt->execute(array(
 | 
				
			||||||
 | 
					                ':username' => $_SESSION['mailcow_cc_username'],
 | 
				
			||||||
 | 
					                ':authmech' => 'webauthn'
 | 
				
			||||||
 | 
					              ));
 | 
				
			||||||
              $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 | 
					              $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 | 
				
			||||||
              while($row = array_shift($rows)) {
 | 
					              while($row = array_shift($rows)) {
 | 
				
			||||||
                $excludeCredentialIds[] = base64_decode($row['keyHandle']);
 | 
					                $excludeCredentialIds[] = base64_decode($row['keyHandle']);
 | 
				
			||||||
@@ -450,20 +455,24 @@ if (isset($_GET['query'])) {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
        case "webauthn-tfa-get-args":
 | 
					        case "webauthn-tfa-get-args":
 | 
				
			||||||
          $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username");
 | 
					          $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username AND authmech = :authmech");
 | 
				
			||||||
          $stmt->execute(array(':username' => $_SESSION['pending_mailcow_cc_username']));
 | 
					          $stmt->execute(array(
 | 
				
			||||||
 | 
					            ':username' => $_SESSION['pending_mailcow_cc_username'],
 | 
				
			||||||
 | 
					            ':authmech' => 'webauthn'
 | 
				
			||||||
 | 
					          ));
 | 
				
			||||||
          $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 | 
					          $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 | 
				
			||||||
          while($row = array_shift($rows)) {
 | 
					          if (count($rows) == 0) {
 | 
				
			||||||
            $cids[] = base64_decode($row['keyHandle']);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          if (count($cids) == 0) {
 | 
					 | 
				
			||||||
            print(json_encode(array(
 | 
					            print(json_encode(array(
 | 
				
			||||||
                'type' => 'error',
 | 
					                'type' => 'error',
 | 
				
			||||||
                'msg' => 'Cannot find matching credentialIds'
 | 
					                '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);
 | 
					          $getArgs->publicKey->extensions = array('appid' => "https://".$getArgs->publicKey->rpId);
 | 
				
			||||||
          print(json_encode($getArgs));
 | 
					          print(json_encode($getArgs));
 | 
				
			||||||
          $_SESSION['challenge'] = $WebAuthn->getChallenge();
 | 
					          $_SESSION['challenge'] = $WebAuthn->getChallenge();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -988,7 +988,7 @@
 | 
				
			|||||||
        "enter_qr_code": "Ваш код TOTP, если устройство не может отсканировать QR-код",
 | 
					        "enter_qr_code": "Ваш код TOTP, если устройство не может отсканировать QR-код",
 | 
				
			||||||
        "error_code": "Код ошибки",
 | 
					        "error_code": "Код ошибки",
 | 
				
			||||||
        "init_webauthn": "Инициализация, пожалуйста, подождите...",
 | 
					        "init_webauthn": "Инициализация, пожалуйста, подождите...",
 | 
				
			||||||
        "key_id": "Идентификатор YubiKey ключа",
 | 
					        "key_id": "Идентификатор вашего устройства",
 | 
				
			||||||
        "key_id_totp": "Идентификатор TOTP ключа",
 | 
					        "key_id_totp": "Идентификатор TOTP ключа",
 | 
				
			||||||
        "none": "Отключить",
 | 
					        "none": "Отключить",
 | 
				
			||||||
        "reload_retry": "- (перезагрузить страницу браузера или почистите кеш/cookies, если ошибка повторяется)",
 | 
					        "reload_retry": "- (перезагрузить страницу браузера или почистите кеш/cookies, если ошибка повторяется)",
 | 
				
			||||||
@@ -1002,7 +1002,8 @@
 | 
				
			|||||||
        "webauthn": "WebAuthn аутентификация",
 | 
					        "webauthn": "WebAuthn аутентификация",
 | 
				
			||||||
        "waiting_usb_auth": "<i>Ожидание устройства USB...</i><br><br>Пожалуйста, нажмите кнопку на USB устройстве сейчас.",
 | 
					        "waiting_usb_auth": "<i>Ожидание устройства USB...</i><br><br>Пожалуйста, нажмите кнопку на USB устройстве сейчас.",
 | 
				
			||||||
        "waiting_usb_register": "<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": {
 | 
					    "user": {
 | 
				
			||||||
        "action": "Действия",
 | 
					        "action": "Действия",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -980,7 +980,8 @@
 | 
				
			|||||||
        "resource_modified": "Зміни поштового акаунту %s збережено",
 | 
					        "resource_modified": "Зміни поштового акаунту %s збережено",
 | 
				
			||||||
        "settings_map_added": "Правило додано",
 | 
					        "settings_map_added": "Правило додано",
 | 
				
			||||||
        "tls_policy_map_entry_deleted": "Політику TLS ID %s видалено",
 | 
					        "tls_policy_map_entry_deleted": "Політику TLS ID %s видалено",
 | 
				
			||||||
        "verified_totp_login": "Авторизацію TOTP пройдено"
 | 
					        "verified_totp_login": "Авторизацію TOTP пройдено",
 | 
				
			||||||
 | 
					        "domain_add_dkim_available": "Ключ DKIM вже існує"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "tfa": {
 | 
					    "tfa": {
 | 
				
			||||||
        "confirm": "Підтвердьте",
 | 
					        "confirm": "Підтвердьте",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -176,15 +176,62 @@ function recursiveBase64StrToArrayBuffer(obj) {
 | 
				
			|||||||
    {% endfor %}
 | 
					    {% endfor %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Confirm TFA modal
 | 
					    // Confirm TFA modal
 | 
				
			||||||
  {% if pending_tfa_method %}
 | 
					  {% if pending_tfa_methods %}
 | 
				
			||||||
    $('#ConfirmTFAModal').modal({
 | 
					    $('#ConfirmTFAModal').modal({
 | 
				
			||||||
      backdrop: 'static',
 | 
					      backdrop: 'static',
 | 
				
			||||||
      keyboard: false
 | 
					      keyboard: false
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // validate Yubi OTP tfa
 | 
				
			||||||
 | 
					    $("#pending_tfa_tab_yubi_otp").click(function(){
 | 
				
			||||||
 | 
					      $(".totp-authenticator-selection").removeClass("active");
 | 
				
			||||||
 | 
					      $(".webauthn-authenticator-selection").removeClass("active");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      $("#collapseTotpTFA").collapse('hide');
 | 
				
			||||||
 | 
					      $("#collapseWebAuthnTFA").collapse('hide');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    $(".yubi-authenticator-selection").click(function(){
 | 
				
			||||||
 | 
					      $(".yubi-authenticator-selection").removeClass("active");
 | 
				
			||||||
 | 
					      $(this).addClass("active");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      var id = $(this).children('input').first().val();
 | 
				
			||||||
 | 
					      $("#yubi_selected_id").val(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      $("#collapseYubiTFA").collapse('show');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    // validate Time based OTP tfa
 | 
				
			||||||
 | 
					    $("#pending_tfa_tab_totp").click(function(){
 | 
				
			||||||
 | 
					      $(".yubi-authenticator-selection").removeClass("active");
 | 
				
			||||||
 | 
					      $(".webauthn-authenticator-selection").removeClass("active");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      $("#collapseYubiTFA").collapse('hide');
 | 
				
			||||||
 | 
					      $("#collapseWebAuthnTFA").collapse('hide');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    $(".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');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
    // validate WebAuthn tfa
 | 
					    // validate WebAuthn tfa
 | 
				
			||||||
    $('#start_webauthn_confirmation').click(function(){
 | 
					    $("#pending_tfa_tab_webauthn").click(function(){
 | 
				
			||||||
      $('#webauthn_status_auth').html('<p><i class="bi bi-arrow-repeat icon-spin"></i> ' + lang_tfa.init_webauthn + '</p>');
 | 
					      $(".totp-authenticator-selection").removeClass("active");
 | 
				
			||||||
 | 
					      $(".yubi-authenticator-selection").removeClass("active");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      $("#collapseTotpTFA").collapse('hide');
 | 
				
			||||||
 | 
					      $("#collapseYubiTFA").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();
 | 
					      $(this).find('input[name=token]').focus();
 | 
				
			||||||
      if(document.getElementById("webauthn_auth_data") !== null) {
 | 
					      if(document.getElementById("webauthn_auth_data") !== null) {
 | 
				
			||||||
@@ -198,30 +245,32 @@ function recursiveBase64StrToArrayBuffer(obj) {
 | 
				
			|||||||
        window.fetch("/api/v1/get/webauthn-tfa-get-args", {method:'GET',cache:'no-cache'}).then(response => {
 | 
					        window.fetch("/api/v1/get/webauthn-tfa-get-args", {method:'GET',cache:'no-cache'}).then(response => {
 | 
				
			||||||
            return response.json();
 | 
					            return response.json();
 | 
				
			||||||
        }).then(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);
 | 
					          recursiveBase64StrToArrayBuffer(json);
 | 
				
			||||||
            return json;
 | 
					          return json;
 | 
				
			||||||
        }).then(getCredentialArgs => {
 | 
					        }).then(getCredentialArgs => {
 | 
				
			||||||
            // get credentials
 | 
					          // get credentials
 | 
				
			||||||
            return navigator.credentials.get(getCredentialArgs);
 | 
					          return navigator.credentials.get(getCredentialArgs);
 | 
				
			||||||
        }).then(cred => {
 | 
					        }).then(cred => {
 | 
				
			||||||
            return {
 | 
					          return {
 | 
				
			||||||
                id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
 | 
					            id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
 | 
				
			||||||
                clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
 | 
					            clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
 | 
				
			||||||
                authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
 | 
					            authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
 | 
				
			||||||
                signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
 | 
					            signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
 | 
				
			||||||
            };
 | 
					          };
 | 
				
			||||||
        }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
 | 
					        }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
 | 
				
			||||||
            // send request by submit
 | 
					          // send request by submit
 | 
				
			||||||
            var form = document.getElementById('webauthn_auth_form');
 | 
					          var form = document.getElementById('webauthn_auth_form');
 | 
				
			||||||
            var auth = document.getElementById('webauthn_auth_data');
 | 
					          var auth = document.getElementById('webauthn_auth_data');
 | 
				
			||||||
            auth.value = AuthenticatorAttestationResponse;
 | 
					          auth.value = AuthenticatorAttestationResponse;
 | 
				
			||||||
            form.submit();
 | 
					          form.submit();
 | 
				
			||||||
        }).catch(function(err) {
 | 
					        }).catch(function(err) {
 | 
				
			||||||
            var webauthn_return_code = document.getElementById('webauthn_return_code');
 | 
					          var webauthn_return_code = document.getElementById('webauthn_return_code');
 | 
				
			||||||
            webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null;
 | 
					          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;
 | 
					          webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
      } 
 | 
					      } 
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@@ -237,7 +286,9 @@ function recursiveBase64StrToArrayBuffer(obj) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    {% endif %}
 | 
					  {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Validate FIDO2
 | 
					    // Validate FIDO2
 | 
				
			||||||
  $("#fido2-login").click(function(){
 | 
					  $("#fido2-login").click(function(){
 | 
				
			||||||
    $('#fido2-alerts').html();
 | 
					    $('#fido2-alerts').html();
 | 
				
			||||||
@@ -358,11 +409,13 @@ function recursiveBase64StrToArrayBuffer(obj) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $("#start_webauthn_register").click(() => {
 | 
					        $("#start_webauthn_register").click(() => {
 | 
				
			||||||
            var key_id = document.getElementsByName('key_id')[1].value;
 | 
					            var key_id = document.getElementsByName('key_id')[1].value;
 | 
				
			||||||
 | 
					            var confirm_password = document.getElementsByName('confirm_password')[1].value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // fetch WebAuthn create args
 | 
					            // 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 => {
 | 
					            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();
 | 
					                return response.json();
 | 
				
			||||||
            }).then(json => {
 | 
					            }).then(json => {
 | 
				
			||||||
 | 
					                console.log(json);
 | 
				
			||||||
                if (json.success === false) throw new Error(json.msg);
 | 
					                if (json.success === false) throw new Error(json.msg);
 | 
				
			||||||
                recursiveBase64StrToArrayBuffer(json);
 | 
					                recursiveBase64StrToArrayBuffer(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -375,7 +428,8 @@ function recursiveBase64StrToArrayBuffer(obj) {
 | 
				
			|||||||
                    clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
 | 
					                    clientDataJSON: cred.response.clientDataJSON  ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
 | 
				
			||||||
                    attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null,
 | 
					                    attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null,
 | 
				
			||||||
                    key_id: key_id,
 | 
					                    key_id: key_id,
 | 
				
			||||||
                    tfa_method: "webauthn"
 | 
					                    tfa_method: "webauthn",
 | 
				
			||||||
 | 
					                    confirm_password: confirm_password
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
            }).then(JSON.stringify).then(AuthenticatorAttestationResponse => {
 | 
					            }).then(JSON.stringify).then(AuthenticatorAttestationResponse => {
 | 
				
			||||||
                // send request
 | 
					                // send request
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,7 +28,7 @@
 | 
				
			|||||||
      <div class="col-sm-9 col-xs-7">
 | 
					      <div class="col-sm-9 col-xs-7">
 | 
				
			||||||
        <select id="selectTFA" class="selectpicker" title="{{ lang.tfa.select }}">
 | 
					        <select id="selectTFA" class="selectpicker" title="{{ lang.tfa.select }}">
 | 
				
			||||||
          <option value="yubi_otp">{{ lang.tfa.yubi_otp }}</option>
 | 
					          <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="totp">{{ lang.tfa.totp }}</option>
 | 
				
			||||||
          <option value="none">{{ lang.tfa.none }}</option>
 | 
					          <option value="none">{{ lang.tfa.none }}</option>
 | 
				
			||||||
        </select>
 | 
					        </select>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -133,73 +133,174 @@
 | 
				
			|||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
{% endif %}
 | 
					{% 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 fade" id="ConfirmTFAModal" tabindex="-1" role="dialog" aria-labelledby="ConfirmTFAModalLabel">
 | 
				
			||||||
  <div class="modal-dialog" role="document">
 | 
					  <div class="modal-dialog" role="document">
 | 
				
			||||||
    <div class="modal-content">
 | 
					    <div class="modal-content">
 | 
				
			||||||
      <div class="modal-header">
 | 
					      <div class="modal-header">
 | 
				
			||||||
        <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button>
 | 
					        <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button>
 | 
				
			||||||
        <h3 class="modal-title">{{ lang.tfa[pending_tfa_method] }}</h3>
 | 
					        <h3 class="modal-title">{{ lang.tfa.tfa }}</h3>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="modal-body">
 | 
					 | 
				
			||||||
        {% if pending_tfa_method == 'yubi_otp' %}
 | 
					 | 
				
			||||||
        <form role="form" method="post">
 | 
					 | 
				
			||||||
          <div class="form-group">
 | 
					 | 
				
			||||||
            <div class="input-group">
 | 
					 | 
				
			||||||
              <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 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 class="form-group">
 | 
					 | 
				
			||||||
            <div class="input-group">
 | 
					 | 
				
			||||||
              <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 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' %}
 | 
					 | 
				
			||||||
        <div class="empty"></div>
 | 
					 | 
				
			||||||
        {% endif %}
 | 
					 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
        {% if pending_tfa_method == 'webauthn' %}
 | 
					        <ul class="nav nav-tabs" id="tabContent">
 | 
				
			||||||
        <form role="form" method="post" id="webauthn_auth_form">
 | 
					            {% if pending_tfa_authmechs["webauthn"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
 | 
				
			||||||
          <center>
 | 
					              <li class="active"><a href="#tfa_tab_webauthn" data-toggle="tab" id="pending_tfa_tab_webauthn"><i class="bi bi-fingerprint"></i> WebAuthn</a></li>
 | 
				
			||||||
            <div style="cursor:pointer" id="start_webauthn_confirmation">
 | 
					            {% endif %}
 | 
				
			||||||
              <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24">
 | 
					
 | 
				
			||||||
                <path d="M17.81 4.47c-.08 0-.16-.02-.23-.06C15.66 3.42 14 3 12.01 3c-1.98 0-3.86.47-5.57 1.41-.24.13-.54.04-.68-.2-.13-.24-.04-.55.2-.68C7.82 2.52 9.86 2 12.01 2c2.13 0 3.99.47 6.03 1.52.25.13.34.43.21.67-.09.18-.26.28-.44.28zM3.5 9.72c-.1 0-.2-.03-.29-.09-.23-.16-.28-.47-.12-.7.99-1.4 2.25-2.5 3.75-3.27C9.98 4.04 14 4.03 17.15 5.65c1.5.77 2.76 1.86 3.75 3.25.16.22.11.54-.12.7-.23.16-.54.11-.7-.12-.9-1.26-2.04-2.25-3.39-2.94-2.87-1.47-6.54-1.47-9.4.01-1.36.7-2.5 1.7-3.4 2.96-.08.14-.23.21-.39.21zm6.25 12.07c-.13 0-.26-.05-.35-.15-.87-.87-1.34-1.43-2.01-2.64-.69-1.23-1.05-2.73-1.05-4.34 0-2.97 2.54-5.39 5.66-5.39s5.66 2.42 5.66 5.39c0 .28-.22.5-.5.5s-.5-.22-.5-.5c0-2.42-2.09-4.39-4.66-4.39-2.57 0-4.66 1.97-4.66 4.39 0 1.44.32 2.77.93 3.85.64 1.15 1.08 1.64 1.85 2.42.19.2.19.51 0 .71-.11.1-.24.15-.37.15zm7.17-1.85c-1.19 0-2.24-.3-3.1-.89-1.49-1.01-2.38-2.65-2.38-4.39 0-.28.22-.5.5-.5s.5.22.5.5c0 1.41.72 2.74 1.94 3.56.71.48 1.54.71 2.54.71.24 0 .64-.03 1.04-.1.27-.05.53.13.58.41.05.27-.13.53-.41.58-.57.11-1.07.12-1.21.12zM14.91 22c-.04 0-.09-.01-.13-.02-1.59-.44-2.63-1.03-3.72-2.1-1.4-1.39-2.17-3.24-2.17-5.22 0-1.62 1.38-2.94 3.08-2.94 1.7 0 3.08 1.32 3.08 2.94 0 1.07.93 1.94 2.08 1.94s2.08-.87 2.08-1.94c0-3.77-3.25-6.83-7.25-6.83-2.84 0-5.44 1.58-6.61 4.03-.39.81-.59 1.76-.59 2.8 0 .78.07 2.01.67 3.61.1.26-.03.55-.29.64-.26.1-.55-.04-.64-.29-.49-1.31-.73-2.61-.73-3.96 0-1.2.23-2.29.68-3.24 1.33-2.79 4.28-4.6 7.51-4.6 4.55 0 8.25 3.51 8.25 7.83 0 1.62-1.38 2.94-3.08 2.94s-3.08-1.32-3.08-2.94c0-1.07-.93-1.94-2.08-1.94s-2.08.87-2.08 1.94c0 1.71.66 3.31 1.87 4.51.95.94 1.86 1.46 3.27 1.85.27.07.42.35.35.61-.05.23-.26.38-.47.38z"></path>
 | 
					            {% if pending_tfa_authmechs["yubi_otp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
 | 
				
			||||||
              </svg>
 | 
					              <li class="tab-pane {% if pending_tfa_authmechs["yubi_otp"] %}active{% endif %}">
 | 
				
			||||||
              <p>{{ lang.tfa.start_webauthn_validation }}</p>
 | 
					                <a href="#tfa_tab_yubi_otp" data-toggle="tab" id="pending_tfa_tab_yubi_otp"><i class="bi bi-usb-drive"></i> Yubi OTP</a>
 | 
				
			||||||
              <hr>
 | 
					              </li>
 | 
				
			||||||
 | 
					            {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {% if pending_tfa_authmechs["totp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
 | 
				
			||||||
 | 
					              <li class="tab-pane {% if pending_tfa_authmechs["totp"] %}active{% endif %}">
 | 
				
			||||||
 | 
					                <a href="#tfa_tab_totp" data-toggle="tab" id="pending_tfa_tab_totp"><i class="bi bi-clock-history"></i> Time based OTP</a>
 | 
				
			||||||
 | 
					              </li>
 | 
				
			||||||
 | 
					            {% endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <!-- <li><a href="#tfa_tab_hotp" data-toggle="tab">HOTP</a></li> -->
 | 
				
			||||||
 | 
					            {% if pending_tfa_authmechs["u2f"] is defined %}
 | 
				
			||||||
 | 
					              <li class="active"><a href="#tfa_tab_u2f" data-toggle="tab"><i class="bi bi-x-octagon"></i> U2F</a></li>
 | 
				
			||||||
 | 
					            {% endif %}
 | 
				
			||||||
 | 
					        </ul>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="tab-content">
 | 
				
			||||||
 | 
					          {% if pending_tfa_authmechs["webauthn"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
 | 
				
			||||||
 | 
					            <div role="tabpanel" class="tab-pane active" id="tfa_tab_webauthn">
 | 
				
			||||||
 | 
					              <div class="panel panel-default" style="margin-bottom: 0px;">
 | 
				
			||||||
 | 
					                  <div class="panel-body">
 | 
				
			||||||
 | 
					                    <form role="form" method="post" id="webauthn_auth_form">
 | 
				
			||||||
 | 
					                      <legend>
 | 
				
			||||||
 | 
					                          <i class="bi bi-shield-fill-check"></i>
 | 
				
			||||||
 | 
					                          Authenticators
 | 
				
			||||||
 | 
					                      </legend>
 | 
				
			||||||
 | 
					                      <div class="list-group">
 | 
				
			||||||
 | 
					                        {% for authenticator in pending_tfa_methods %}
 | 
				
			||||||
 | 
					                          {% if authenticator["authmech"] == "webauthn" %}
 | 
				
			||||||
 | 
					                            <a href="#" class="list-group-item webauthn-authenticator-selection">
 | 
				
			||||||
 | 
					                              <i class="bi bi-key-fill" style="margin-right: 5px"></i>
 | 
				
			||||||
 | 
					                              <span>{{ authenticator["key_id"] }}</span>
 | 
				
			||||||
 | 
					                              <input type="hidden" value="{{ authenticator["id"] }}" /><br/>
 | 
				
			||||||
 | 
					                            </a>
 | 
				
			||||||
 | 
					                          {% endif %}
 | 
				
			||||||
 | 
					                        {% endfor %}
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      <div class="collapse pending-tfa-collapse" id="collapseWebAuthnTFA">
 | 
				
			||||||
 | 
					                        <p id="webauthn_status_auth"><p><i class="bi bi-arrow-repeat icon-spin"></i> {{ lang.tfa.init_webauthn }}</p></p>
 | 
				
			||||||
 | 
					                        <div class="alert alert-danger" style="display:none" id="webauthn_return_code"></div>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      <input type="hidden" name="token" id="webauthn_auth_data"/>
 | 
				
			||||||
 | 
					                      <input type="hidden" name="tfa_method" value="webauthn">
 | 
				
			||||||
 | 
					                      <input type="hidden" name="verify_tfa_login"/><br/>
 | 
				
			||||||
 | 
					                      <input type="hidden" name="id" id="webauthn_selected_id" /><br/>
 | 
				
			||||||
 | 
					                    </form>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </center>
 | 
					          {% endif %}
 | 
				
			||||||
          <p id="webauthn_status_auth"></p>
 | 
					          {% if pending_tfa_authmechs["yubi_otp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
 | 
				
			||||||
          <div class="alert alert-danger" style="display:none" id="webauthn_return_code"></div>
 | 
					            <div role="tabpanel" class="tab-pane {% if pending_tfa_authmechs["yubi_otp"] %}active{% endif %}" id="tfa_tab_yubi_otp">
 | 
				
			||||||
          <input type="hidden" name="token" id="webauthn_auth_data"/>
 | 
					              <div class="panel panel-default" style="margin-bottom: 0px;">
 | 
				
			||||||
          <input type="hidden" name="tfa_method" value="webauthn">
 | 
					                  <div class="panel-body">
 | 
				
			||||||
          <input type="hidden" name="verify_tfa_login"/><br/>
 | 
					                    <form role="form" method="post">
 | 
				
			||||||
        </form>
 | 
					                      <legend>
 | 
				
			||||||
        {% endif %}
 | 
					                          <i class="bi bi-shield-fill-check"></i>
 | 
				
			||||||
        {# leave this here to inform users that u2f is deprecated #}
 | 
					                          Authenticators
 | 
				
			||||||
        {% if pending_tfa_method == 'u2f' %}
 | 
					                      </legend>
 | 
				
			||||||
        <form role="form" method="post" id="u2f_auth_form">
 | 
					                      <div class="list-group">
 | 
				
			||||||
          <p>{{ lang.tfa.u2f_deprecated }}</p>
 | 
					                        {% for authenticator in pending_tfa_methods %}
 | 
				
			||||||
          <p><b>{{ lang.tfa.u2f_deprecated_important }}</b></p>
 | 
					                          {% if authenticator["authmech"] == "yubi_otp" %}
 | 
				
			||||||
          <input type="hidden" name="token" value="destroy" />
 | 
					                            <a href="#" class="list-group-item yubi-authenticator-selection">
 | 
				
			||||||
          <input type="hidden" name="tfa_method" value="u2f">
 | 
					                              <i class="bi bi-key-fill" style="margin-right: 5px"></i>
 | 
				
			||||||
          <input type="hidden" name="verify_tfa_login"/><br/>
 | 
					                              <span>{{ authenticator["key_id"] }}</span>
 | 
				
			||||||
          <button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button>
 | 
					                              <input type="hidden" value="{{ authenticator["id"] }}" />
 | 
				
			||||||
        </form>
 | 
					                            </a>
 | 
				
			||||||
        {% endif %}
 | 
					                          {% endif %}
 | 
				
			||||||
      </div>
 | 
					                        {% endfor %}
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      <div class="collapse pending-tfa-collapse" id="collapseYubiTFA">
 | 
				
			||||||
 | 
					                        <div class="form-group">
 | 
				
			||||||
 | 
					                          <div class="input-group">
 | 
				
			||||||
 | 
					                            <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">
 | 
				
			||||||
 | 
					                            <input type="hidden" name="id" id="yubi_selected_id" />
 | 
				
			||||||
 | 
					                          </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <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>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    </form>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          {% endif %}
 | 
				
			||||||
 | 
					          {% if pending_tfa_authmechs["totp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
 | 
				
			||||||
 | 
					            <div role="tabpanel" class="tab-pane {% if pending_tfa_authmechs["totp"] %}active{% endif %}" id="tfa_tab_totp">
 | 
				
			||||||
 | 
					              <div class="panel panel-default" style="margin-bottom: 0px;">
 | 
				
			||||||
 | 
					                  <div class="panel-body">
 | 
				
			||||||
 | 
					                    <form role="form" method="post">        
 | 
				
			||||||
 | 
					                      <legend>
 | 
				
			||||||
 | 
					                          <i class="bi bi-shield-fill-check"></i>
 | 
				
			||||||
 | 
					                          Authenticators
 | 
				
			||||||
 | 
					                      </legend>
 | 
				
			||||||
 | 
					                      <div class="list-group">
 | 
				
			||||||
 | 
					                        {% for authenticator in pending_tfa_methods %}
 | 
				
			||||||
 | 
					                          {% if authenticator["authmech"] == "totp" %}
 | 
				
			||||||
 | 
					                            <a href="#" class="list-group-item totp-authenticator-selection">
 | 
				
			||||||
 | 
					                              <i class="bi bi-key-fill" style="margin-right: 5px"></i>
 | 
				
			||||||
 | 
					                              <span>{{ authenticator["key_id"] }}</span>
 | 
				
			||||||
 | 
					                              <input type="hidden" value="{{ authenticator["id"] }}" />
 | 
				
			||||||
 | 
					                            </a>
 | 
				
			||||||
 | 
					                          {% endif %}
 | 
				
			||||||
 | 
					                        {% endfor %}
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                      <div class="collapse pending-tfa-collapse" id="collapseTotpTFA">
 | 
				
			||||||
 | 
					                        <div class="form-group">
 | 
				
			||||||
 | 
					                          <div class="input-group">
 | 
				
			||||||
 | 
					                            <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">
 | 
				
			||||||
 | 
					                            <input type="hidden" name="id" id="totp_selected_id" /><br/>
 | 
				
			||||||
 | 
					                          </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <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>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    </form>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          {% endif %}
 | 
				
			||||||
 | 
					            <!--
 | 
				
			||||||
 | 
					            <div role="tabpanel" class="tab-pane" id="tfa_tab_hotp">
 | 
				
			||||||
 | 
					              <div class="panel panel-default" style="margin-bottom: 0px;">
 | 
				
			||||||
 | 
					                  <div class="panel-body">
 | 
				
			||||||
 | 
					                      <div class="empty"></div>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            -->
 | 
				
			||||||
 | 
					          {% if pending_tfa_authmechs["u2f"] is defined %}
 | 
				
			||||||
 | 
					            <div role="tabpanel" class="tab-pane active" id="tfa_tab_u2f">
 | 
				
			||||||
 | 
					              <div class="panel panel-default" style="margin-bottom: 0px;">
 | 
				
			||||||
 | 
					                  <div class="panel-body">
 | 
				
			||||||
 | 
					                    {# leave this here to inform users that u2f is deprecated #}
 | 
				
			||||||
 | 
					                    <form role="form" method="post" id="u2f_auth_form">
 | 
				
			||||||
 | 
					                      <div>
 | 
				
			||||||
 | 
					                        <p>{{ lang.tfa.u2f_deprecated }}</p>
 | 
				
			||||||
 | 
					                        <p><b>{{ lang.tfa.u2f_deprecated_important }}</b></p>
 | 
				
			||||||
 | 
					                        <input type="hidden" name="token" value="destroy" />
 | 
				
			||||||
 | 
					                        <input type="hidden" name="tfa_method" value="u2f">
 | 
				
			||||||
 | 
					                        <input type="hidden" name="verify_tfa_login"/><br/>
 | 
				
			||||||
 | 
					                        <button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button>
 | 
				
			||||||
 | 
					                      </div>
 | 
				
			||||||
 | 
					                    </form>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          {% endif %}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -435,11 +435,11 @@
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="modal-body">
 | 
					      <div class="modal-body">
 | 
				
			||||||
        <p class="help-block">{{ lang.add.syncjob_hint }}</p>
 | 
					        <p class="help-block">{{ 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="form-group">
 | 
					          <div class="form-group">
 | 
				
			||||||
            <label class="control-label col-sm-2" for="username">{{ lang.add.username }}</label>
 | 
					            <label class="control-label col-sm-2" for="username">{{ lang.add.username }}</label>
 | 
				
			||||||
            <div class="col-sm-10">
 | 
					            <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 %}
 | 
					                {% for mailbox in mailboxes %}
 | 
				
			||||||
                  <option>{{ mailbox }}</option>
 | 
					                  <option>{{ mailbox }}</option>
 | 
				
			||||||
                {% endfor %}
 | 
					                {% endfor %}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,10 @@
 | 
				
			|||||||
              <i class="bi bi-inbox-fill"></i> {{ lang.user.open_webmail_sso }}
 | 
					              <i class="bi bi-inbox-fill"></i> {{ lang.user.open_webmail_sso }}
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
          {% endif %}
 | 
					          {% endif %}
 | 
				
			||||||
 | 
					          <div>
 | 
				
			||||||
 | 
					            <hr>
 | 
				
			||||||
 | 
					            <p><a href="#pwChangeModal" data-toggle="modal"><i class="bi bi-pencil-fill"></i> {{ lang.user.change_password }}</a></p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <hr>
 | 
					      <hr>
 | 
				
			||||||
@@ -43,8 +47,27 @@
 | 
				
			|||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <p>{{ mailboxdata.quota_used|formatBytes(2) }} / {% if mailboxdata.quota == 0 %}∞{% else %}{{ mailboxdata.quota|formatBytes(2) }}{% endif %}<br>{{ mailboxdata.messages }} {{ lang.user.messages }}</p>
 | 
					          <p>{{ mailboxdata.quota_used|formatBytes(2) }} / {% if mailboxdata.quota == 0 %}∞{% else %}{{ mailboxdata.quota|formatBytes(2) }}{% endif %}<br>{{ mailboxdata.messages }} {{ lang.user.messages }}</p>
 | 
				
			||||||
          <hr>
 | 
					        </div>
 | 
				
			||||||
          <p><a href="#pwChangeModal" data-toggle="modal"><i class="bi bi-pencil-fill"></i> {{ lang.user.change_password }}</a></p>
 | 
					      </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>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <hr>
 | 
					      <hr>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -76,6 +76,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
 | 
				
			|||||||
    'acl_json' => json_encode($_SESSION['acl']),
 | 
					    'acl_json' => json_encode($_SESSION['acl']),
 | 
				
			||||||
    'user_spam_score' => mailbox('get', 'spam_score', $username),
 | 
					    'user_spam_score' => mailbox('get', 'spam_score', $username),
 | 
				
			||||||
    'tfa_data' => $tfa_data,
 | 
					    'tfa_data' => $tfa_data,
 | 
				
			||||||
 | 
					    'tfa_id' => @$_SESSION['tfa_id'],
 | 
				
			||||||
    'fido2_data' => $fido2_data,
 | 
					    'fido2_data' => $fido2_data,
 | 
				
			||||||
    'mailboxdata' => $mailboxdata,
 | 
					    'mailboxdata' => $mailboxdata,
 | 
				
			||||||
    'clientconfigstr' => $clientconfigstr,
 | 
					    'clientconfigstr' => $clientconfigstr,
 | 
				
			||||||
@@ -90,8 +91,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
 | 
				
			|||||||
    'number_of_app_passwords' => $number_of_app_passwords,
 | 
					    'number_of_app_passwords' => $number_of_app_passwords,
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					else {
 | 
				
			||||||
if (!isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
 | 
					 | 
				
			||||||
  header('Location: /');
 | 
					  header('Location: /');
 | 
				
			||||||
  exit();
 | 
					  exit();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -58,7 +58,7 @@ services:
 | 
				
			|||||||
            - redis
 | 
					            - redis
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    clamd-mailcow:
 | 
					    clamd-mailcow:
 | 
				
			||||||
      image: mailcow/clamd:1.52
 | 
					      image: mailcow/clamd:1.53
 | 
				
			||||||
      restart: always
 | 
					      restart: always
 | 
				
			||||||
      depends_on:
 | 
					      depends_on:
 | 
				
			||||||
        - unbound-mailcow
 | 
					        - unbound-mailcow
 | 
				
			||||||
@@ -168,7 +168,7 @@ services:
 | 
				
			|||||||
            - phpfpm
 | 
					            - phpfpm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sogo-mailcow:
 | 
					    sogo-mailcow:
 | 
				
			||||||
      image: mailcow/sogo:1.108
 | 
					      image: mailcow/sogo:1.109
 | 
				
			||||||
      environment:
 | 
					      environment:
 | 
				
			||||||
        - DBNAME=${DBNAME}
 | 
					        - DBNAME=${DBNAME}
 | 
				
			||||||
        - DBUSER=${DBUSER}
 | 
					        - DBUSER=${DBUSER}
 | 
				
			||||||
@@ -215,7 +215,7 @@ services:
 | 
				
			|||||||
            - sogo
 | 
					            - sogo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dovecot-mailcow:
 | 
					    dovecot-mailcow:
 | 
				
			||||||
      image: mailcow/dovecot:1.162
 | 
					      image: mailcow/dovecot:1.17
 | 
				
			||||||
      depends_on:
 | 
					      depends_on:
 | 
				
			||||||
        - mysql-mailcow
 | 
					        - mysql-mailcow
 | 
				
			||||||
      dns:
 | 
					      dns:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -271,4 +271,13 @@ if ! ssh -o StrictHostKeyChecking=no \
 | 
				
			|||||||
    >&2 echo -e "\e[31m[ERR]\e[0m - Could not cleanup old images on remote"
 | 
					    >&2 echo -e "\e[31m[ERR]\e[0m - Could not cleanup old images on remote"
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo -e "\033[1mExecuting update script and checking for new docker-compose Version on remote...\033[0m"
 | 
				
			||||||
 | 
					if ! ssh -o StrictHostKeyChecking=no \
 | 
				
			||||||
 | 
					  -i "${REMOTE_SSH_KEY}" \
 | 
				
			||||||
 | 
					  ${REMOTE_SSH_HOST} \
 | 
				
			||||||
 | 
					  -p ${REMOTE_SSH_PORT} \
 | 
				
			||||||
 | 
					  ${SCRIPT_DIR}/../update.sh -f --update-compose ; then
 | 
				
			||||||
 | 
					    >&2 echo -e "\e[31m[ERR]\e[0m - Could not fetch docker-compose on remote"
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
echo -e "\e[32mDone\e[0m"
 | 
					echo -e "\e[32mDone\e[0m"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -76,13 +76,6 @@ else
 | 
				
			|||||||
  CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]")
 | 
					  CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]")
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
for bin in docker docker-compose; do
 | 
					 | 
				
			||||||
  if [[ -z $(which ${bin}) ]]; then
 | 
					 | 
				
			||||||
    >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
 | 
					 | 
				
			||||||
    exit 1
 | 
					 | 
				
			||||||
  fi
 | 
					 | 
				
			||||||
done
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then
 | 
					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"
 | 
					  >&2 echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m"
 | 
				
			||||||
  exit 1
 | 
					  exit 1
 | 
				
			||||||
@@ -94,6 +87,12 @@ function backup() {
 | 
				
			|||||||
  mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}"
 | 
					  mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}"
 | 
				
			||||||
  chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}"
 | 
					  chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}"
 | 
				
			||||||
  cp "${SCRIPT_DIR}/../mailcow.conf" "${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
 | 
					  while (( "$#" )); do
 | 
				
			||||||
    case "$1" in
 | 
					    case "$1" in
 | 
				
			||||||
    vmail|all)
 | 
					    vmail|all)
 | 
				
			||||||
@@ -161,6 +160,12 @@ function backup() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function restore() {
 | 
					function restore() {
 | 
				
			||||||
 | 
					  for bin in docker docker-compose; do
 | 
				
			||||||
 | 
					  if [[ -z $(which ${bin}) ]]; then
 | 
				
			||||||
 | 
					    >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
 | 
				
			||||||
 | 
					    exit 1
 | 
				
			||||||
 | 
					  fi
 | 
				
			||||||
 | 
					  done  
 | 
				
			||||||
  echo
 | 
					  echo
 | 
				
			||||||
  echo "Stopping watchdog-mailcow..."
 | 
					  echo "Stopping watchdog-mailcow..."
 | 
				
			||||||
  docker stop $(docker ps -qf name=watchdog-mailcow)
 | 
					  docker stop $(docker ps -qf name=watchdog-mailcow)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										180
									
								
								update.sh
									
									
									
									
									
								
							
							
						
						
									
										180
									
								
								update.sh
									
									
									
									
									
								
							@@ -1,64 +1,6 @@
 | 
				
			|||||||
#!/usr/bin/env bash
 | 
					#!/usr/bin/env bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Check permissions
 | 
					############## Begin Function Section ##############
 | 
				
			||||||
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;
 | 
					 | 
				
			||||||
  elif [[ -z $(which docker-compose) ]]; then
 | 
					 | 
				
			||||||
  echo "Cannot find docker-compose Standalone. Installing..."
 | 
					 | 
				
			||||||
  sleep 3
 | 
					 | 
				
			||||||
   if [[ -e /etc/alpine-release ]]; then
 | 
					 | 
				
			||||||
    echo -e "\e[33mNot installing latest docker-compose, because you are using Alpine Linux without glibc support. Install docker-compose via apk!\e[0m"
 | 
					 | 
				
			||||||
    exit 1
 | 
					 | 
				
			||||||
   fi 
 | 
					 | 
				
			||||||
  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
 | 
					 | 
				
			||||||
  chmod +x /usr/local/bin/docker-compose  
 | 
					 | 
				
			||||||
  fi
 | 
					 | 
				
			||||||
done
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export LC_ALL=C
 | 
					 | 
				
			||||||
DATE=$(date +%Y-%m-%d_%H_%M_%S)
 | 
					 | 
				
			||||||
BRANCH=$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
check_online_status() {
 | 
					check_online_status() {
 | 
				
			||||||
  CHECK_ONLINE_IPS=(1.1.1.1 9.9.9.9 8.8.8.8)
 | 
					  CHECK_ONLINE_IPS=(1.1.1.1 9.9.9.9 8.8.8.8)
 | 
				
			||||||
@@ -223,7 +165,7 @@ remove_obsolete_nginx_ports() {
 | 
				
			|||||||
              sed -i '/nginx-mailcow:$/,/^$/d' $override
 | 
					              sed -i '/nginx-mailcow:$/,/^$/d' $override
 | 
				
			||||||
              echo -e "\e[33mRemoved obsolete NGINX IPv6 Bind from original override File.\e[0m"
 | 
					              echo -e "\e[33mRemoved obsolete NGINX IPv6 Bind from original override File.\e[0m"
 | 
				
			||||||
                if [[ "$(cat $override | sed '/^\s*$/d' | wc -l)" == "2" ]]; then
 | 
					                if [[ "$(cat $override | sed '/^\s*$/d' | wc -l)" == "2" ]]; then
 | 
				
			||||||
                  mv $override ${override}_backup
 | 
					                  mv $override ${override}_empty
 | 
				
			||||||
                  echo -e "\e[31m${override} is empty. Renamed it to ensure mailcow is startable.\e[0m"
 | 
					                  echo -e "\e[31m${override} is empty. Renamed it to ensure mailcow is startable.\e[0m"
 | 
				
			||||||
                fi
 | 
					                fi
 | 
				
			||||||
            fi
 | 
					            fi
 | 
				
			||||||
@@ -241,6 +183,13 @@ elif [[ -e /etc/alpine-release ]]; then
 | 
				
			|||||||
  echo -e "\e[33mNot fetching latest docker-compose, because you are using Alpine Linux without glibc support. Please update docker-compose via apk!\e[0m"
 | 
					  echo -e "\e[33mNot fetching latest docker-compose, because you are using Alpine Linux without glibc support. Please update docker-compose via apk!\e[0m"
 | 
				
			||||||
  return 0
 | 
					  return 0
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
 | 
					  if [ ! $FORCE ]; then
 | 
				
			||||||
 | 
					    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."
 | 
				
			||||||
 | 
					      return 0
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					  fi 
 | 
				
			||||||
  echo -e "\e[32mFetching new docker-compose version...\e[0m"
 | 
					  echo -e "\e[32mFetching new docker-compose version...\e[0m"
 | 
				
			||||||
  echo -e "\e[32mTrying to determine GLIBC version...\e[0m"
 | 
					  echo -e "\e[32mTrying to determine GLIBC version...\e[0m"
 | 
				
			||||||
  if ldd --version > /dev/null; then
 | 
					  if ldd --version > /dev/null; then
 | 
				
			||||||
@@ -254,8 +203,12 @@ else
 | 
				
			|||||||
    DC_DL_SUFFIX=legacy
 | 
					    DC_DL_SUFFIX=legacy
 | 
				
			||||||
  fi
 | 
					  fi
 | 
				
			||||||
  sleep 1
 | 
					  sleep 1
 | 
				
			||||||
  if [[ ! -z $(which pip) && $(pip list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 ]]; then
 | 
					  if [[ $(which pip 2>&1) && $(pip list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 || $(which pip3 2>&1) && $(pip3 list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 ]]; then
 | 
				
			||||||
    true
 | 
					    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
 | 
					    #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
 | 
					  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)
 | 
					    LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php)
 | 
				
			||||||
@@ -277,6 +230,79 @@ else
 | 
				
			|||||||
fi
 | 
					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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					for bin in curl docker git awk sha1sum; do
 | 
				
			||||||
 | 
					  if [[ -z $(which ${bin}) ]]; then 
 | 
				
			||||||
 | 
					  echo "Cannot find ${bin}, exiting..." 
 | 
				
			||||||
 | 
					  exit 1;
 | 
				
			||||||
 | 
					  elif [[ -z $(which docker-compose) ]]; then
 | 
				
			||||||
 | 
					  echo -e "\e[31mCannot find docker-compose Standalone.\e[0m" 
 | 
				
			||||||
 | 
					  echo -e "\e[31mPlease install it manually regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
 | 
				
			||||||
 | 
					  sleep 3
 | 
				
			||||||
 | 
					  exit 1;
 | 
				
			||||||
 | 
					  fi  
 | 
				
			||||||
 | 
					done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Check if docker-compose >= v2
 | 
				
			||||||
 | 
					if ! docker-compose 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: $(docker-compose version --short)\e[0m"
 | 
				
			||||||
 | 
					  sleep 3
 | 
				
			||||||
 | 
					  update_compose
 | 
				
			||||||
 | 
					  if [[ ! "${updatecomposeresponse}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
 | 
				
			||||||
 | 
					     echo -e "\e[31mmailcow does not work with docker-compose < 2.X.X anymore!\e[0m"
 | 
				
			||||||
 | 
					     echo -e "\e[31mPlease update your docker-compose manually, to run mailcow.\e[0m"
 | 
				
			||||||
 | 
					     echo -e "\e[31mExiting...\e[0m"
 | 
				
			||||||
 | 
					     exit 1
 | 
				
			||||||
 | 
					  fi    
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export LC_ALL=C
 | 
				
			||||||
 | 
					DATE=$(date +%Y-%m-%d_%H_%M_%S)
 | 
				
			||||||
 | 
					BRANCH=$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
while (($#)); do
 | 
					while (($#)); do
 | 
				
			||||||
  case "${1}" in
 | 
					  case "${1}" in
 | 
				
			||||||
    --check|-c)
 | 
					    --check|-c)
 | 
				
			||||||
@@ -318,19 +344,33 @@ while (($#)); do
 | 
				
			|||||||
    --no-update-compose)
 | 
					    --no-update-compose)
 | 
				
			||||||
      NO_UPDATE_COMPOSE=y
 | 
					      NO_UPDATE_COMPOSE=y
 | 
				
			||||||
    ;;
 | 
					    ;;
 | 
				
			||||||
 | 
					    --update-compose)
 | 
				
			||||||
 | 
					      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"
 | 
				
			||||||
 | 
					        update_compose
 | 
				
			||||||
 | 
					        echo -e "\e[32mYour docker-compose Version is now up to date!\e[0m" 
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					      echo -e "\e[32mYour docker-compose Version is up to date! Not updating it...\e[0m" 
 | 
				
			||||||
 | 
					      fi
 | 
				
			||||||
 | 
					      exit 0
 | 
				
			||||||
 | 
					    ;;
 | 
				
			||||||
    --skip-ping-check)
 | 
					    --skip-ping-check)
 | 
				
			||||||
      SKIP_PING_CHECK=y
 | 
					      SKIP_PING_CHECK=y
 | 
				
			||||||
    ;;
 | 
					    ;;
 | 
				
			||||||
    --help|-h)
 | 
					    --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, --no-update-compose, --update-compose, --prefetch, --skip-start, --skip-ping-check, -f|--force, -h|--help]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  -c|--check           -   Check for updates and exit (exit codes => 0: update available, 3: no updates)
 | 
					  -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!
 | 
					  --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
 | 
					  --gc                 -   Run garbage collector to delete old image tags
 | 
				
			||||||
  --no-update-compose  -   Do not update docker-compose  
 | 
					  --no-update-compose  -   Skip the docker-compose Updates during the mailcow Update process
 | 
				
			||||||
 | 
					  --update-compose     -   Only run the docker-compose Update process (don´t updates your mailcow itself)
 | 
				
			||||||
  --prefetch           -   Only prefetch new images and exit (useful to prepare updates)
 | 
					  --prefetch           -   Only prefetch new images and exit (useful to prepare updates)
 | 
				
			||||||
  --skip-start         -   Do not start mailcow after update
 | 
					  --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)
 | 
				
			||||||
  -f|--force           -   Force update, do not ask questions
 | 
					  -f|--force           -   Force update, do not ask questions
 | 
				
			||||||
'
 | 
					'
 | 
				
			||||||
    exit 1
 | 
					    exit 1
 | 
				
			||||||
@@ -657,7 +697,15 @@ if [ ! $FORCE ]; then
 | 
				
			|||||||
  migrate_docker_nat
 | 
					  migrate_docker_nat
 | 
				
			||||||
fi
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
update_compose
 | 
					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"
 | 
				
			||||||
 | 
					  update_compose
 | 
				
			||||||
 | 
					else
 | 
				
			||||||
 | 
					  echo -e "\e[32mYour docker-compose Version is up to date! Not updating it...\e[0m" 
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
remove_obsolete_nginx_ports
 | 
					remove_obsolete_nginx_ports
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user