Merge from upstream branch 'staging'

# Conflicts:
#	data/web/inc/vars.inc.php
This commit is contained in:
Tomy Hsieh 2022-09-30 16:03:49 +08:00
commit 7d46de33d8
No known key found for this signature in database
GPG Key ID: 9E47A53D54F34479
65 changed files with 1721 additions and 889 deletions

View File

@ -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
...

View File

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

45
.github/workflows/image_builds.yml vendored Normal file
View File

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

63
.github/workflows/integration_tests.yml vendored Normal file
View File

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

View File

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

View File

@ -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=

View File

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

42
SECURITY.md Normal file
View File

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

0
create_cold_standby.sh Normal file → Executable file
View File

View File

@ -1,4 +1,4 @@
FROM clamav/clamav:0.105.0_base FROM clamav/clamav:0.105.1_base
LABEL maintainer "André Peters <andre.peters@servercow.de>" LABEL maintainer "André Peters <andre.peters@servercow.de>"
@ -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"]

View 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

View File

@ -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

View File

@ -307,6 +307,7 @@ namespace {
} }
EOF EOF
cat <<EOF > /etc/dovecot/sogo_trusted_ip.conf cat <<EOF > /etc/dovecot/sogo_trusted_ip.conf
# Autogenerated by mailcow # Autogenerated by mailcow
remote ${IPV4_NETWORK}.248 { remote ${IPV4_NETWORK}.248 {
@ -349,6 +350,14 @@ sievec /var/vmail/sieve/global_sieve_after.sieve
sievec /usr/lib/dovecot/sieve/report-spam.sieve sievec /usr/lib/dovecot/sieve/report-spam.sieve
sievec /usr/lib/dovecot/sieve/report-ham.sieve sievec /usr/lib/dovecot/sieve/report-ham.sieve
for file in /var/vmail/*/*/sieve/*.sieve ; do
if [[ "$file" == "/var/vmail/*/*/sieve/*.sieve" ]]; then
continue
fi
sievec "$file" "$(dirname "$file")/../.dovecot.svbin"
chown vmail:vmail "$(dirname "$file")/../.dovecot.svbin"
done
# Fix permissions # Fix permissions
chown root:root /etc/dovecot/sql/*.conf chown root:root /etc/dovecot/sql/*.conf
chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua

View File

@ -50,7 +50,7 @@ try:
def query_mysql(query, headers = True, update = False): def query_mysql(query, headers = True, update = False):
while True: while True:
try: try:
cnx = mysql.connector.connect(unix_socket = '/var/run/mysqld/mysqld.sock', user=os.environ.get('DBUSER'), passwd=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8") cnx = mysql.connector.connect(unix_socket = '/var/run/mysqld/mysqld.sock', user=os.environ.get('DBUSER'), passwd=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
except Exception as ex: except Exception as ex:
print('%s - trying again...' % (ex)) print('%s - trying again...' % (ex))
time.sleep(3) time.sleep(3)
@ -166,4 +166,4 @@ try:
notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl'], attrs['quarantine_category']) notify_rcpt(record['rcpt'], record['counter'], record['quarantine_acl'], attrs['quarantine_category'])
finally: finally:
os.unlink(pidfile) os.unlink(pidfile)

View File

@ -323,7 +323,19 @@ hosts = unix:/var/run/mysqld/mysqld.sock
dbname = ${DBNAME} dbname = ${DBNAME}
# First select queries domain and alias_domain to determine if domains are active. # First select queries domain and alias_domain to determine if domains are active.
query = SELECT goto FROM alias query = SELECT goto FROM alias
WHERE address='%s' WHERE id IN (
SELECT COALESCE (
(
SELECT id FROM alias
WHERE address='%s'
AND (active='1' OR active='2')
), (
SELECT id FROM alias
WHERE address='@%d'
AND (active='1' OR active='2')
)
)
)
AND active='1' AND active='1'
AND (domain IN AND (domain IN
(SELECT domain FROM domain (SELECT domain FROM domain
@ -354,7 +366,7 @@ query = SELECT goto FROM alias
WHERE alias_domain.alias_domain = '%d' WHERE alias_domain.alias_domain = '%d'
AND mailbox.username = CONCAT('%u','@',alias_domain.target_domain) AND mailbox.username = CONCAT('%u','@',alias_domain.target_domain)
AND (mailbox.active = '1' OR mailbox.active ='2') AND (mailbox.active = '1' OR mailbox.active ='2')
AND alias_domain.active='1' AND alias_domain.active='1';
EOF EOF
# MX based routing # MX based routing

View File

@ -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 SOGO_DEBIAN_REPOSITORY=http://packages.inverse.ca/SOGo/nightly/5/debian/ ARG SOGO_DEBIAN_REPOSITORY=http://packages.sogo.nu/nightly/5/debian/
ENV LC_ALL C ENV LC_ALL C
ENV GOSU_VERSION 1.14 ENV GOSU_VERSION 1.14
@ -30,7 +30,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
&& gosu nobody true \ && gosu nobody true \
&& mkdir /usr/share/doc/sogo \ && mkdir /usr/share/doc/sogo \
&& touch /usr/share/doc/sogo/empty.sh \ && touch /usr/share/doc/sogo/empty.sh \
&& apt-key adv --keyserver keyserver.ubuntu.com --recv-key 0x810273C4 \ && apt-key adv --keyserver keys.openpgp.org --recv-key 74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 \
&& echo "deb ${SOGO_DEBIAN_REPOSITORY} bullseye bullseye" > /etc/apt/sources.list.d/sogo.list \ && echo "deb ${SOGO_DEBIAN_REPOSITORY} bullseye bullseye" > /etc/apt/sources.list.d/sogo.list \
&& apt-get update && apt-get install -y --no-install-recommends \ && apt-get update && apt-get install -y --no-install-recommends \
sogo \ sogo \
@ -52,4 +52,4 @@ RUN chmod +x /bootstrap-sogo.sh \
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf

View File

@ -142,6 +142,10 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_acl</string> <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_acl</string>
<key>SOGoIMAPServer</key> <key>SOGoIMAPServer</key>
<string>imap://${IPV4_NETWORK}.250:143/?TLS=YES&amp;tlsVerifyMode=none</string> <string>imap://${IPV4_NETWORK}.250:143/?TLS=YES&amp;tlsVerifyMode=none</string>
<key>SOGoSieveServer</key>
<string>sieve://${IPV4_NETWORK}.250:4190/?TLS=YES&amp;tlsVerifyMode=none</string>
<key>SOGoSMTPServer</key>
<string>smtp://${IPV4_NETWORK}.253:588/?TLS=YES&amp;tlsVerifyMode=none</string>
<key>SOGoTrustProxyAuthentication</key> <key>SOGoTrustProxyAuthentication</key>
<string>YES</string> <string>YES</string>
<key>SOGoEncryptionKey</key> <key>SOGoEncryptionKey</key>

View File

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

View File

@ -32,8 +32,6 @@
// ); // );
// self-signed is not trusted anymore // self-signed is not trusted anymore
SOGoSieveServer = "sieve://dovecot:4190/?TLS=YES&tlsVerifyMode=none";
SOGoSMTPServer = "smtp://postfix:588/?TLS=YES&tlsVerifyMode=none";
WOPort = "0.0.0.0:20000"; WOPort = "0.0.0.0:20000";
SOGoMemcachedHost = "memcached"; SOGoMemcachedHost = "memcached";

16
data/web/api/index.css Normal file
View File

@ -0,0 +1,16 @@
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}

View File

@ -5,56 +5,15 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Swagger UI</title> <title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" /> <link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
<link rel="stylesheet" type="text/css" href="index.css" />
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" /> <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" /> <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head> </head>
<body> <body>
<div id="swagger-ui"></div> <div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script> <script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script> <script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script> <script src="./swagger-initializer.js" charset="UTF-8"> </script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
urls: [{url: "/api/openapi.yaml", name: "mailcow API"}],
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
// End Swagger UI call region
window.ui = ui;
};
</script>
</body> </body>
</html> </html>

View File

@ -13,7 +13,7 @@
var isValid, qp, arr; var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) { if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1); qp = window.location.hash.substring(1).replace('?', '&');
} else { } else {
qp = location.search.substring(1); qp = location.search.substring(1);
} }
@ -38,7 +38,7 @@
authId: oauth2.auth.name, authId: oauth2.auth.name,
source: "auth", source: "auth",
level: "warning", level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server" message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
}); });
} }
@ -58,7 +58,7 @@
authId: oauth2.auth.name, authId: oauth2.auth.name,
source: "auth", source: "auth",
level: "error", level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server" message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
}); });
} }
} else { } else {
@ -67,9 +67,13 @@
window.close(); window.close();
} }
window.addEventListener('DOMContentLoaded', function () { if (document.readyState !== 'loading') {
run(); run();
}); } else {
document.addEventListener('DOMContentLoaded', function () {
run();
});
}
</script> </script>
</body> </body>
</html> </html>

View File

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

View File

@ -0,0 +1,19 @@
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
urls: [{url: "/api/openapi.yaml", name: "mailcow API"}],
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
// End Swagger UI call region
window.ui = ui;
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -260,6 +260,18 @@ 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;
min-height: 110px;
}
.tag-box { .tag-box {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -296,4 +308,3 @@ code {
align-items: center; align-items: center;
display: inline-flex; display: inline-flex;
} }

View File

@ -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']);
?> ?>

View File

@ -23,14 +23,43 @@ 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(
'version_tag' => $GLOBALS['MAILCOW_GIT_VERSION'], 'version_tag' => $GLOBALS['MAILCOW_GIT_VERSION'],
'git_project_url' => $GLOBALS['MAILCOW_GIT_URL'] 'last_version_tag' => $GLOBALS['MAILCOW_LAST_GIT_VERSION'],
'git_owner' => $GLOBALS['MAILCOW_GIT_OWNER'],
'git_repo' => $GLOBALS['MAILCOW_GIT_REPO'],
'git_project_url' => $GLOBALS['MAILCOW_GIT_URL'],
'git_commit' => $GLOBALS['MAILCOW_GIT_COMMIT'],
'git_commit_date' => $GLOBALS['MAILCOW_GIT_COMMIT_DATE'],
'mailcow_branch' => $GLOBALS['MAILCOW_BRANCH'],
'updated_at' => $GLOBALS['MAILCOW_UPDATEDAT']
), ),
'js_path' => '/cache/'.basename($JSPath), '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']),

View File

@ -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',
@ -929,15 +935,37 @@ function check_login($user, $pass, $app_passwd_data = false) {
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
$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']); if (!array_key_exists("app_passwd_id", $row)){
$_SESSION['return'][] = array( // password is not a app password
'type' => 'success', // check for tfa authenticators
'log' => array(__FUNCTION__, $user, '*'), $authenticators = get_tfa($user);
'msg' => array('logged_in_as', $user) if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 &&
); $app_passwd_data['eas'] !== true && $app_passwd_data['dav'] !== true) {
if ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { // authenticators found, init TFA flow
$_SESSION['pending_mailcow_cc_username'] = $user;
$_SESSION['pending_mailcow_cc_role'] = "user";
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
unset($_SESSION['ldelay']);
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
return "pending";
} else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
// no authenticators found, login successfull
// Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user));
unset($_SESSION['ldelay']);
return "user";
}
} elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {
// password is a app password
$service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV'; $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 = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)");
$stmt->execute(array( $stmt->execute(array(
@ -946,8 +974,10 @@ function check_login($user, $pass, $app_passwd_data = false) {
':username' => $user, ':username' => $user,
':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'])
)); ));
unset($_SESSION['ldelay']);
return "user";
} }
return "user";
} }
} }
@ -1142,47 +1172,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 +1249,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 +1293,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 +1464,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 +1497,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 +1516,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,95 +1524,119 @@ 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;
$stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` global $WebAuthn;
WHERE `username` = :username AND `active` = '1'");
$stmt->execute(array(':username' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
switch ($row["authmech"]) { if ($_data['tfa_method'] != 'u2f'){
switch ($_data["tfa_method"]) {
case "yubi_otp": case "yubi_otp":
if (!ctype_alnum($_data['token']) || strlen($_data['token']) != 44) { if (!ctype_alnum($_data['token']) || strlen($_data['token']) != 44) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@ -1597,7 +1650,7 @@ 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 `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));
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
@ -1632,15 +1685,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 +1702,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 +1710,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 +1728,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 +1749,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 +1762,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 +1796,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 +1818,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;

View File

@ -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

View File

@ -3,7 +3,7 @@ function init_db_schema() {
try { try {
global $pdo; global $pdo;
$db_version = "18062022_1153"; $db_version = "25072022_2300";
$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));
@ -738,8 +738,8 @@ function init_db_schema() {
"username" => "VARCHAR(255) NOT NULL", "username" => "VARCHAR(255) NOT NULL",
"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(1023) 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%'
@ -1237,8 +1237,7 @@ function init_db_schema() {
OR custom_params LIKE '%pipemess%' OR custom_params LIKE '%pipemess%'
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')");

View File

@ -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');

View File

@ -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']);
} }

View File

@ -101,6 +101,7 @@ $AVAILABLE_LANGUAGES = array(
'ru-ru' => 'Pусский (Russian)', 'ru-ru' => 'Pусский (Russian)',
'sk-sk' => 'Slovenčina (Slovak)', 'sk-sk' => 'Slovenčina (Slovak)',
'sv-se' => 'Svenska (Swedish)', 'sv-se' => 'Svenska (Swedish)',
'tr-tr' => 'Türkçe (Turkish)',
'uk-ua' => 'Українська (Ukrainian)', 'uk-ua' => 'Українська (Ukrainian)',
'zh-cn' => '简体中文 (Simplified Chinese)', 'zh-cn' => '简体中文 (Simplified Chinese)',
'zh-tw' => '繁體中文 (Traditional Chinese)', 'zh-tw' => '繁體中文 (Traditional Chinese)',
@ -234,131 +235,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'
) )
); );

View File

@ -178,15 +178,22 @@ if (isset($_GET['query'])) {
// parse post data // parse post data
$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();

View File

@ -839,7 +839,7 @@
"confirm_delete": "Potvrdit smazání prvku.", "confirm_delete": "Potvrdit smazání prvku.",
"danger": "Nebezpečí", "danger": "Nebezpečí",
"deliver_inbox": "Doručit do schránky", "deliver_inbox": "Doručit do schránky",
"disabled_by_config": "Funkce karanténa je momentálně vypnuta v nastavení systému.", "disabled_by_config": "Funkce karanténa je momentálně vypnuta v nastavení systému. Nastavte, prosím, prvkům karantény hodnoty \"počet zadržených zpráv\" a \"maximální velikost\".",
"download_eml": "Stáhnout (.eml)", "download_eml": "Stáhnout (.eml)",
"empty": "Žádné výsledky", "empty": "Žádné výsledky",
"high_danger": "Vysoké nebezpečí", "high_danger": "Vysoké nebezpečí",

View File

@ -102,7 +102,8 @@
"timeout2": "Délai d'expiration pour la connexion à l'hôte local", "timeout2": "Délai d'expiration pour la connexion à l'hôte local",
"username": "Nom d'utilisateur", "username": "Nom d'utilisateur",
"validate": "Valider", "validate": "Valider",
"validation_success": "Validation réussie" "validation_success": "Validation réussie",
"bcc_dest_format": "La destination Cci doit être une seule adresse e-mail valide.<br>Si vous avez besoin d'envoyer une copie à plusieurs adresses, créez un alias et utilisez-le ici."
}, },
"admin": { "admin": {
"access": "Accès", "access": "Accès",
@ -322,7 +323,9 @@
"yes": "&#10003;", "yes": "&#10003;",
"api_read_write": "Accès Lecture-Écriture", "api_read_write": "Accès Lecture-Écriture",
"oauth2_add_client": "Ajouter un client OAuth2", "oauth2_add_client": "Ajouter un client OAuth2",
"password_policy": "Politique de mots de passe" "password_policy": "Politique de mots de passe",
"admins": "Administrateurs",
"api_read_only": "Accès lecture-seule"
}, },
"danger": { "danger": {
"access_denied": "Accès refusé ou données de formulaire non valides", "access_denied": "Accès refusé ou données de formulaire non valides",

View File

@ -973,7 +973,8 @@
"verified_fido2_login": "Verified FIDO2 login", "verified_fido2_login": "Verified FIDO2 login",
"verified_totp_login": "Verified TOTP login", "verified_totp_login": "Verified TOTP login",
"verified_webauthn_login": "Verified WebAuthn login", "verified_webauthn_login": "Verified WebAuthn login",
"verified_yotp_login": "Verified Yubico OTP login" "verified_yotp_login": "Verified Yubico OTP login",
"domain_add_dkim_available": "Esisteva già una chiave DKIM"
}, },
"tfa": { "tfa": {
"api_register": "%s usa le API Yubico Cloud. Richiedi una chiave API <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">qui</a>", "api_register": "%s usa le API Yubico Cloud. Richiedi una chiave API <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">qui</a>",

View File

@ -979,7 +979,8 @@
"verified_totp_login": "Autentificarea TOTP verificată", "verified_totp_login": "Autentificarea TOTP verificată",
"verified_webauthn_login": "Autentificarea WebAuthn verificată", "verified_webauthn_login": "Autentificarea WebAuthn verificată",
"verified_fido2_login": "Conectare FIDO2 verificată", "verified_fido2_login": "Conectare FIDO2 verificată",
"verified_yotp_login": "Autentificarea Yubico OTP verificată" "verified_yotp_login": "Autentificarea Yubico OTP verificată",
"domain_add_dkim_available": "O cheie DKIM deja a existat"
}, },
"tfa": { "tfa": {
"api_register": "%s utilizează API-ul Yubico Cloud. Obțineți o cheie API pentru cheia dvs. de <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">aici</a>", "api_register": "%s utilizează API-ul Yubico Cloud. Obțineți o cheie API pentru cheia dvs. de <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">aici</a>",
@ -990,7 +991,7 @@
"enter_qr_code": "Codul tău TOTP dacă dispozitivul tău nu poate scana codurile QR", "enter_qr_code": "Codul tău TOTP dacă dispozitivul tău nu poate scana codurile QR",
"error_code": "Cod de eroare", "error_code": "Cod de eroare",
"init_webauthn": "Inițializare, vă rugăm așteptați...", "init_webauthn": "Inițializare, vă rugăm așteptați...",
"key_id": "Un identificator pentru YubiKey", "key_id": "Un identificator pentru dispozitiv",
"key_id_totp": "Un identificator pentru cheia ta", "key_id_totp": "Un identificator pentru cheia ta",
"none": "Dezactivează", "none": "Dezactivează",
"reload_retry": "- (reîncărcați browserul dacă eroarea persistă)", "reload_retry": "- (reîncărcați browserul dacă eroarea persistă)",

View File

@ -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": "Действия",

View File

@ -0,0 +1,86 @@
{
"acl": {
"alias_domains": "Takma alan adı ekle",
"app_passwds": "Uygulama şifrelerini yönet",
"delimiter_action": "Sınırlama işlemi",
"domain_relayhost": "Bir alan adı için relayhost sunucusunu değiştir",
"eas_reset": "EAS cihazlarını sıfırla",
"mailbox_relayhost": "Bir posta kutusunun relayhost sunucularını değiştir",
"pushover": "Bildirim",
"quarantine": "Karantina işlemleri",
"quarantine_attachments": "Ekleri karantinaya al",
"quarantine_notification": "Karantina bildirimlerini değiştir",
"smtp_ip_access": "SMTP sunucularının değiştirilmesine izin ver",
"sogo_access": "SOGo erişiminin yönetilmesine izin ver",
"domain_desc": "Alan adııklamasını değiştir",
"extend_sender_acl": "Gönderenin acl'sini harici adreslere göre genişletmeye izin ver",
"spam_policy": "Engellenenler / İzin verilenler",
"filters": "Fitreler"
},
"add": {
"activate_filter_warn": "Aktif edilirse diğer tüm filtreler devre dışı bırakılacak.",
"add_domain_only": "Sadece alan adı ekle",
"alias_address": "Takma ad adres(leri)",
"alias_domain": "Takma alan adı",
"alias_domain_info": "<small>Sadece geçerli alan adları (virgülle ayırın).</small>",
"backup_mx_options": "İletme ayarları",
"delete2": "Kaynakta olmayan hedefteki mesajları sil",
"delete2duplicates": "Hedefteki kopyaları sil",
"disable_login": "Giriş yapmaya izin verme ( Gelen mailler yine de kabul edilir)",
"domain": "Alan adı",
"domain_matches_hostname": "Alan adı %s ana bilgisayar adıyla eşleşiyor",
"add_domain_restart": "Alan adı ekleyin ve SOGo'yu yeniden başlatın",
"alias_address_info": "<small>Bir alan adına ilişkin tüm iletileri yakalamak için tam e-posta adresi veya @example.com olacak şeklinde girin (virgülle ayırın).<b>sadece mailcow alan adları</b>.</small>",
"domain_quota_m": "Toplam alan adı kotası (MiB)",
"generate": "oluştur",
"goto_ham": "Ham olarak<span class=\"text-success\"><b>işaretle</b></span>",
"goto_null": "Postaları sessizce çöpe at",
"goto_spam": "Spam olarak<span class=\"text-danger\"><b>işaretle</b></span>",
"hostname": "Ana sunucu",
"kind": "Tür",
"mailbox_quota_m": "Posta kutusu başına maksimum kota (MiB)",
"max_aliases": "Maksimum olası takma adı",
"max_mailboxes": "Maksimum olası posta kutusu",
"nexthop": "Sonraki atlama",
"port": "Port",
"public_comment": "Genel yorum",
"relay_all": "Tüm alıcılara ilet",
"relay_all_info": "Eğer <b>hiçbir</b> alıcıya iletilmemesini seçerseniz, aktarılması gereken her alıcı için bir (\"kör\") posta kutusu eklemeniz gerekecektir.",
"relay_domain": "Bu alan adını ilet",
"relay_transport_info": "<div class=\"label label-info\">Bilgi</div> Bu etki alanı için özel bir hedef için aktarım eşlemeleri tanımlayabilirsiniz. Ayarlanmazsa, bir MX araması yapılacaktır.",
"relay_unknown_only": "Yalnızca mevcut olmayan posta kutularını ilet. Mevcut posta kutuları yerel olarak teslim edilecektir.",
"relayhost_wrapped_tls_info": "Lütfen TLS ile örtülmüş portları <b> kullanmayın</b> (çoğu 465 portunda çalışır).<br>\nÖrtülmemiş port kullan ve STARTTLS üzerinden yayınla. TLS'yi zorlamak için bir TLS ilkesi \"TLS ilke eşlemeleri\" sayfası içinde oluşturulabilir.",
"skipcrossduplicates": "Klasörler arasında yinelenen mesajları atlayın (ilk mesaj seçilir)",
"target_address": "Adreslere git",
"target_address_info": "<small>Tam e-posta adres(leri) girin ( virgülle ayırın).</small>",
"target_domain": "Hedef alan adı",
"timeout1": "Uzak ana bilgisayara bağlantısı zaman aşımına uğradı",
"timeout2": "Yerel ana bilgisayara bağlantı zaman aşımına uğradı"
},
"admin": {
"action": "İşlem",
"add_forwarding_host": "Yönlendirme sunucusu ekle",
"add_transport": "İletim ekle",
"admin_details": "Yönetici detaylarını düzenle",
"admin_domains": "Alan adı atamaları",
"add_domain_admin": "Alan adı yöneticisi ekle",
"api_info": "API üzerinde çalışmalar devam etmektedir. Belgeler <a href=\"/api\">/api</a>adresinde bulunabilir",
"apps_name": "\"mailcow Uygulamaları\" adı",
"authed_user": "Yetkili kullanıcı",
"ban_list_info": "Aşağıdaki yasaklı IP'lerin listesine bakın: <b>ağ (kalan yasak süresi) - [işlemler]</b>.<br />Yasağı kaldırılmak üzere sıraya alınan IP'ler birkaç saniye içinde aktif yasak listesinden kaldırılacaktır.<br />Kırmızı etiketler, kara listeye alınarak aktif kalıcı yasakları gösterir.",
"configuration": "Yapılandırma",
"dkim_from_title": "Verilerin kopyalanacağı kaynak alan adı",
"dkim_to": "Kime",
"dkim_to_title": "Hedef alan ad(ları) üzerinde yazılacak",
"dkim_domains_wo_keys": "Eksik anahtarları olan alan adlarını seçin",
"domain": "Alan adı",
"domain_admin": "Alan adı yöneticisi",
"domain_admins": "Alan adı yöneticileri",
"domain_s": "Alan ad(ları)",
"duplicate": "Çift",
"duplicate_dkim": "Çift DKIM kayıtları",
"f2b_ban_time": "Yasaklama süresi (saniye)",
"f2b_max_attempts": "Maksimum giriş denemesi",
"f2b_retry_window": "Maksimum girişim için deneme pencere(leri)"
}
}

View File

@ -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": "Підтвердьте",

View File

@ -176,15 +176,75 @@ 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 Time based OTP tfa
$("#pending_tfa_tab_totp").click(function(){
$(".webauthn-authenticator-selection").removeClass("active");
$("#collapseWebAuthnTFA").collapse('hide');
// select default if only one authenticator exists
if ($('.totp-authenticator-selection').length == 1){
$('.totp-authenticator-selection').addClass("active");
var id = $('.totp-authenticator-selection').children('input').first().val();
$("#totp_selected_id").val(id);
$("#collapseTotpTFA").collapse('show');
}
});
$(".totp-authenticator-selection").click(function(){
$(".totp-authenticator-selection").removeClass("active");
$(this).addClass("active");
var id = $(this).children('input').first().val();
$("#totp_selected_id").val(id);
$("#collapseTotpTFA").collapse('show');
});
if ($('.totp-authenticator-selection').length == 1 &&
$('#pending_tfa_tab_yubi_otp').length == 0 &&
$('.webauthn-authenticator-selection').length == 0){
// select default if only one authenticator exists
$('.totp-authenticator-selection').addClass("active");
var id = $('.totp-authenticator-selection').children('input').first().val();
$("#totp_selected_id").val(id);
$("#collapseTotpTFA").collapse('show');
setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 1000);
}
$('#pending_tfa_tab_totp').on('shown.bs.tab', function() {
// autofocus
setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 200);
});
// validate Yubi OTP tfa
if ($('.webauthn-authenticator-selection').length == 0){
// autofocus
setTimeout(function() { $("#collapseYubiTFA").find('input[name="token"]').focus(); }, 1000);
}
$('#pending_tfa_tab_yubi_otp').on('shown.bs.tab', function() {
// autofocus
$("#collapseYubiTFA").find('input[name="token"]').focus();
});
// validate WebAuthn tfa // 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");
$("#collapseTotpTFA").collapse('hide');
});
$(".webauthn-authenticator-selection").click(function(){
$(".webauthn-authenticator-selection").removeClass("active");
$(this).addClass("active");
var id = $(this).children('input').first().val();
$("#webauthn_selected_id").val(id);
$("#collapseWebAuthnTFA").collapse('show');
$(this).find('input[name=token]').focus(); $(this).find('input[name=token]').focus();
if(document.getElementById("webauthn_auth_data") !== null) { if(document.getElementById("webauthn_auth_data") !== null) {
@ -198,30 +258,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 +299,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 +422,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 +441,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
@ -423,13 +490,20 @@ function recursiveBase64StrToArrayBuffer(obj) {
{% if ui_texts.ui_footer %} {% if ui_texts.ui_footer %}
<hr><span class="rot-enc">{{ ui_texts.ui_footer|rot13|raw }}</span> <hr><span class="rot-enc">{{ ui_texts.ui_footer|rot13|raw }}</span>
{% endif %} {% endif %}
{% if mailcow_cc_username and mailcow_info.version_tag|default %} {% if mailcow_cc_username and mailcow_info.mailcow_branch|lower == "master" and mailcow_info.version_tag|default %}
<span class="version"> <span class="version">
🐮 + 🐋 = 💕 🐮 + 🐋 = 💕
<a href="{{ mailcow_info.git_project_url }}/releases/tag/{{ mailcow_info.version_tag }}" target="_blank"> Version: <a href="{{ mailcow_info.git_project_url }}/releases/tag/{{ mailcow_info.version_tag }}" target="_blank">{{ mailcow_info.version_tag }}
Version: {{ mailcow_info.version_tag }}
</a> </a>
</span> </span>
{% endif %}
{% if mailcow_cc_username and mailcow_info.mailcow_branch|lower == "nightly" and mailcow_info.version_tag|default %}
<span class="version">
🛠️🐮 + 🐋 = 💕
Nightly: <a href="{{ mailcow_info.git_project_url }}/commit/{{ mailcow_info.git_commit }}" target="_blank">{{ mailcow_info.version_tag }}
</a><br>
<span style="text-align:right;display:block;">Build: {{ mailcow_info.git_commit_date }}</span>
</span>
{% endif %} {% endif %}
</div> </div>
</body> </body>

View File

@ -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>

View File

@ -133,73 +133,163 @@
</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' %} <ul class="nav nav-tabs" id="tabContent">
<form role="form" method="post"> {% if pending_tfa_authmechs["webauthn"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
<div class="form-group"> <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 class="input-group"> {% endif %}
<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' %} {% if pending_tfa_authmechs["yubi_otp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
<form role="form" method="post" id="webauthn_auth_form"> <li class="tab-pane {% if pending_tfa_authmechs["yubi_otp"] %}active{% endif %}">
<center> <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>
<div style="cursor:pointer" id="start_webauthn_confirmation"> </li>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24"> {% endif %}
<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>
</svg> {% if pending_tfa_authmechs["totp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
<p>{{ lang.tfa.start_webauthn_validation }}</p> <li class="tab-pane {% if pending_tfa_authmechs["totp"] %}active{% endif %}">
<hr> <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 #} Authenticate
{% if pending_tfa_method == 'u2f' %} </legend>
<form role="form" method="post" id="u2f_auth_form"> <div class="collapse in pending-tfa-collapse" id="collapseYubiTFA">
<p>{{ lang.tfa.u2f_deprecated }}</p> <div class="form-group">
<p><b>{{ lang.tfa.u2f_deprecated_important }}</b></p> <div class="input-group">
<input type="hidden" name="token" value="destroy" /> <span class="input-group-addon" id="yubi-addon"><img alt="Yubicon Icon" src="/img/yubi.ico"></span>
<input type="hidden" name="tfa_method" value="u2f"> <input type="text" name="token" class="form-control" autocomplete="off" placeholder="Touch Yubikey" aria-describedby="yubi-addon">
<input type="hidden" name="verify_tfa_login"/><br/> <input type="hidden" name="tfa_method" value="yubi_otp">
<button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button> <input type="hidden" name="id" id="yubi_selected_id" />
</form> </div>
{% endif %} </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>

View File

@ -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 %}

View File

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

View File

@ -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();
} }

View File

@ -58,7 +58,7 @@ services:
- redis - redis
clamd-mailcow: clamd-mailcow:
image: mailcow/clamd:1.52 image: mailcow/clamd:1.54
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.111
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.20
depends_on: depends_on:
- mysql-mailcow - mysql-mailcow
dns: dns:
@ -295,7 +295,7 @@ services:
- dovecot - dovecot
postfix-mailcow: postfix-mailcow:
image: mailcow/postfix:1.67 image: mailcow/postfix:1.68
depends_on: depends_on:
- mysql-mailcow - mysql-mailcow
volumes: volumes:

View File

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

View File

@ -77,7 +77,7 @@ function preflight_local_checks() {
exit 1 exit 1
fi fi
for bin in rsync docker docker-compose grep cut; do for bin in rsync docker grep cut; do
if [[ -z $(which ${bin}) ]]; then if [[ -z $(which ${bin}) ]]; then
>&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m" >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
exit 1 exit 1
@ -111,7 +111,7 @@ function preflight_remote_checks() {
exit 1 exit 1
fi fi
for bin in rsync docker docker-compose; do for bin in rsync docker; do
if ! ssh -o StrictHostKeyChecking=no \ if ! ssh -o StrictHostKeyChecking=no \
-i "${REMOTE_SSH_KEY}" \ -i "${REMOTE_SSH_KEY}" \
${REMOTE_SSH_HOST} \ ${REMOTE_SSH_HOST} \
@ -121,17 +121,44 @@ function preflight_remote_checks() {
exit 1 exit 1
fi fi
done done
ssh -o StrictHostKeyChecking=no \
-i "${REMOTE_SSH_KEY}" \
${REMOTE_SSH_HOST} \
-p ${REMOTE_SSH_PORT} \
"bash -s" << "EOF"
if docker compose > /dev/null 2>&1; then
exit 0
elif docker-compose version --short | grep "^2." > /dev/null 2>&1; then
exit 1
else
exit 2
fi
EOF
if [ $? = 0 ]; then
COMPOSE_COMMAND="docker compose"
echo "DEBUG: Using native docker compose on remote"
elif [ $? = 1 ]; then
COMPOSE_COMMAND="docker-compose"
echo "DEBUG: Using standalone docker compose on remote"
else
echo -e "\e[31mCannot find any Docker Compose on remote, exiting...\e[0m"
exit 1
fi
} }
SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
source "${SCRIPT_DIR}/../mailcow.conf"
COMPOSE_FILE="${SCRIPT_DIR}/../docker-compose.yml"
CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd 'A-Za-z-_')
SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' "${COMPOSE_FILE}")
preflight_local_checks preflight_local_checks
preflight_remote_checks preflight_remote_checks
SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
COMPOSE_FILE="${SCRIPT_DIR}/../docker-compose.yml"
source "${SCRIPT_DIR}/../mailcow.conf"
CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd 'A-Za-z-_')
SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' "${COMPOSE_FILE}")
echo echo
echo -e "\033[1mFound compose project name ${CMPS_PRJ} for ${MAILCOW_HOSTNAME}\033[0m" echo -e "\033[1mFound compose project name ${CMPS_PRJ} for ${MAILCOW_HOSTNAME}\033[0m"
echo -e "\033[1mFound SQL ${SQLIMAGE}\033[0m" echo -e "\033[1mFound SQL ${SQLIMAGE}\033[0m"
@ -258,7 +285,7 @@ echo "OK"
-i "${REMOTE_SSH_KEY}" \ -i "${REMOTE_SSH_KEY}" \
${REMOTE_SSH_HOST} \ ${REMOTE_SSH_HOST} \
-p ${REMOTE_SSH_PORT} \ -p ${REMOTE_SSH_PORT} \
docker-compose -f "${SCRIPT_DIR}/../docker-compose.yml" pull --no-parallel --quiet 2>&1 ; then ${COMPOSE_COMMAND} -f "${SCRIPT_DIR}/../docker-compose.yml" pull --no-parallel --quiet 2>&1 ; then
>&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote" >&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote"
fi fi
@ -271,4 +298,4 @@ 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 "\e[32mDone\e[0m" echo -e "\e[32mDone\e[0m"

View File

@ -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,24 @@ function backup() {
} }
function restore() { function restore() {
for bin in docker; do
if [[ -z $(which ${bin}) ]]; then
>&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
exit 1
fi
done
if [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then
COMPOSE_COMMAND="docker compose"
elif [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then
COMPOSE_COMMAND="docker-compose"
else
echo -e "\e[31mCan not read DOCKER_COMPOSE_VERSION variable from mailcow.conf! Is your mailcow up to date? Exiting...\e[0m"
exit 1
fi
echo echo
echo "Stopping watchdog-mailcow..." echo "Stopping watchdog-mailcow..."
docker stop $(docker ps -qf name=watchdog-mailcow) docker stop $(docker ps -qf name=watchdog-mailcow)
@ -239,7 +256,7 @@ function restore() {
continue continue
else else
echo "Stopping mailcow..." echo "Stopping mailcow..."
docker-compose -f ${COMPOSE_FILE} --env-file ${ENV_FILE} down ${COMPOSE_COMMAND} -f ${COMPOSE_FILE} --env-file ${ENV_FILE} down
fi fi
#docker stop $(docker ps -qf name=mysql-mailcow) #docker stop $(docker ps -qf name=mysql-mailcow)
if [[ -d "${RESTORE_LOCATION}/mysql" ]]; then if [[ -d "${RESTORE_LOCATION}/mysql" ]]; then
@ -277,7 +294,7 @@ function restore() {
sed -i --follow-symlinks "/DBROOT/c\DBROOT=${DBROOT}" ${SCRIPT_DIR}/../mailcow.conf sed -i --follow-symlinks "/DBROOT/c\DBROOT=${DBROOT}" ${SCRIPT_DIR}/../mailcow.conf
source ${SCRIPT_DIR}/../mailcow.conf source ${SCRIPT_DIR}/../mailcow.conf
echo "Starting mailcow..." echo "Starting mailcow..."
docker-compose -f ${COMPOSE_FILE} --env-file ${ENV_FILE} up -d ${COMPOSE_COMMAND} -f ${COMPOSE_FILE} --env-file ${ENV_FILE} up -d
#docker start $(docker ps -aqf name=mysql-mailcow) #docker start $(docker ps -aqf name=mysql-mailcow)
fi fi
;; ;;
@ -354,4 +371,4 @@ elif [[ ${1} == "restore" ]]; then
done done
echo "Restoring ${FILE_SELECTION[${input_sel}]} from ${RESTORE_POINT}..." echo "Restoring ${FILE_SELECTION[${input_sel}]} from ${RESTORE_POINT}..."
restore "${RESTORE_POINT}" ${FILE_SELECTION[${input_sel}]} restore "${RESTORE_POINT}" ${FILE_SELECTION[${input_sel}]}
fi fi

View File

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

375
update.sh
View File

@ -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
@ -233,50 +175,108 @@ remove_obsolete_nginx_ports() {
done done
} }
update_compose(){ detect_docker_compose_command(){
if [[ ${NO_UPDATE_COMPOSE} == "y" ]]; then if ! [ "${DOCKER_COMPOSE_VERSION}" == "native" ] && ! [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then
echo -e "\e[33mNot fetching latest docker-compose, please check for updates manually!\e[0m" if docker compose > /dev/null 2>&1; then
return 0 if docker compose version --short | grep "2." > /dev/null 2>&1; then
elif [[ -e /etc/alpine-release ]]; then DOCKER_COMPOSE_VERSION=native
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" COMPOSE_COMMAND="docker compose"
return 0 echo -e "\e[31mFound Docker Compose Plugin (native).\e[0m"
else echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
echo -e "\e[32mFetching new docker-compose version...\e[0m" sleep 2
echo -e "\e[32mTrying to determine GLIBC version...\e[0m" echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
if ldd --version > /dev/null; then
GLIBC_V=$(ldd --version | grep -E '(GLIBC|GNU libc)' | rev | cut -d ' ' -f1 | rev | cut -d '.' -f2)
if [ ! -z "${GLIBC_V}" ] && [ ${GLIBC_V} -gt 27 ]; then
DC_DL_SUFFIX=
else
DC_DL_SUFFIX=legacy
fi
else
DC_DL_SUFFIX=legacy
fi
sleep 1
if [[ ! -z $(which pip) && $(pip list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 ]]; then
true
#prevent breaking a working docker-compose installed with pip
elif [[ $(curl -sL -w "%{http_code}" https://www.servercow.de/docker-compose/latest.php?vers=${DC_DL_SUFFIX} -o /dev/null) == "200" ]]; then
LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php)
COMPOSE_VERSION=$(docker-compose version --short)
if [[ "$LATEST_COMPOSE" != "$COMPOSE_VERSION" ]]; then
COMPOSE_PATH=$(which docker-compose)
if [[ -w ${COMPOSE_PATH} ]]; then
curl -#L https://github.com/docker/compose/releases/download/v${LATEST_COMPOSE}/docker-compose-$(uname -s)-$(uname -m) > $COMPOSE_PATH
chmod +x $COMPOSE_PATH
else else
echo -e "\e[33mWARNING: $COMPOSE_PATH is not writable, but new version $LATEST_COMPOSE is available (installed: $COMPOSE_VERSION)\e[0m" echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
return 1 echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
exit 1
fi
elif docker-compose > /dev/null 2>&1; then
if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
if docker-compose version --short | grep "^2." > /dev/null 2>&1; then
DOCKER_COMPOSE_VERSION=standalone
COMPOSE_COMMAND="docker-compose"
echo -e "\e[31mFound Docker Compose Standalone.\e[0m"
echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
sleep 2
echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
exit 1
fi fi
fi fi
else else
echo -e "\e[33mCannot determine latest docker-compose version, skipping...\e[0m" echo -e "\e[31mCannot find Docker Compose.\e[0m"
return 1 echo -e "\e[31mPlease install it regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
exit 1
fi fi
elif [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then
COMPOSE_COMMAND="docker compose"
elif [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then
COMPOSE_COMMAND="docker-compose"
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
# Unset COMPOSE_COMMAND and DOCKER_COMPOSE_VERSION Variable to be on the newest state.
unset COMPOSE_COMMAND
unset DOCKER_COMPOSE_VERSION
for bin in curl docker git awk sha1sum; do
if [[ -z $(command -v ${bin}) ]]; then
echo "Cannot find ${bin}, exiting..."
exit 1;
fi
done
export LC_ALL=C
DATE=$(date +%Y-%m-%d_%H_%M_%S)
BRANCH=$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD)
while (($#)); do while (($#)); do
case "${1}" in case "${1}" in
--check|-c) --check|-c)
@ -301,11 +301,22 @@ while (($#)); do
--skip-start) --skip-start)
SKIP_START=y SKIP_START=y
;; ;;
--skip-ping-check)
SKIP_PING_CHECK=y
;;
--stable)
CURRENT_BRANCH="$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD)"
NEW_BRANCH="master"
;;
--gc) --gc)
echo -e "\e[32mCollecting garbage...\e[0m" echo -e "\e[32mCollecting garbage...\e[0m"
docker_garbage docker_garbage
exit 0 exit 0
;; ;;
--nightly)
CURRENT_BRANCH="$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD)"
NEW_BRANCH="nightly"
;;
--prefetch) --prefetch)
echo -e "\e[32mPrefetching images...\e[0m" echo -e "\e[32mPrefetching images...\e[0m"
prefetch_images prefetch_images
@ -315,22 +326,17 @@ while (($#)); do
echo -e "\e[32mRunning in forced mode...\e[0m" echo -e "\e[32mRunning in forced mode...\e[0m"
FORCE=y FORCE=y
;; ;;
--no-update-compose)
NO_UPDATE_COMPOSE=y
;;
--skip-ping-check)
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, --nightly, --prefetch, --skip-start, --skip-ping-check, --stable, -f|--force, -h|--help]
-c|--check - Check for updates and exit (exit codes => 0: update available, 3: no updates) -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 --nightly - Switch your mailcow updates to the unstable (nightly) branch. FOR TESTING PURPOSES ONLY!!!!
--prefetch - Only prefetch new images and exit (useful to prepare updates) --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)
--stable - Switch your mailcow updates to the stable (master) branch. Default unless you changed it with --nightly.
-f|--force - Force update, do not ask questions -f|--force - Force update, do not ask questions
' '
exit 1 exit 1
@ -338,13 +344,16 @@ while (($#)); do
shift shift
done done
[[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing"; exit 1;}
chmod 600 mailcow.conf chmod 600 mailcow.conf
source mailcow.conf source mailcow.conf
detect_docker_compose_command
[[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing! Is mailcow installed?"; exit 1;}
DOTS=${MAILCOW_HOSTNAME//[^.]}; DOTS=${MAILCOW_HOSTNAME//[^.]};
if [ ${#DOTS} -lt 2 ]; then if [ ${#DOTS} -lt 2 ]; then
echo "MAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) is not a FQDN!" echo "MAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) is not a FQDN!"
echo "Please change it to a FQDN and run docker-compose down followed by docker-compose up -d" echo "Please change it to a FQDN and run $COMPOSE_COMMAND down followed by $COMPOSE_COMMAND up -d"
exit 1 exit 1
fi fi
@ -371,6 +380,7 @@ CONFIG_ARRAY=(
"SNAT_TO_SOURCE" "SNAT_TO_SOURCE"
"SNAT6_TO_SOURCE" "SNAT6_TO_SOURCE"
"COMPOSE_PROJECT_NAME" "COMPOSE_PROJECT_NAME"
"DOCKER_COMPOSE_VERSION"
"SQL_PORT" "SQL_PORT"
"API_KEY" "API_KEY"
"API_KEY_READ_ONLY" "API_KEY_READ_ONLY"
@ -406,6 +416,17 @@ for option in ${CONFIG_ARRAY[@]}; do
echo "Adding new option \"${option}\" to mailcow.conf" echo "Adding new option \"${option}\" to mailcow.conf"
echo "COMPOSE_PROJECT_NAME=mailcowdockerized" >> mailcow.conf echo "COMPOSE_PROJECT_NAME=mailcowdockerized" >> mailcow.conf
fi fi
elif [[ ${option} == "DOCKER_COMPOSE_VERSION" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo "# Used Docker Compose version" >> mailcow.conf
echo "# Switch here between native (compose plugin) and standalone" >> mailcow.conf
echo "# For more informations take a look at the mailcow docs regarding the configuration options." >> mailcow.conf
echo "# Normally this should be untouched but if you decided to use either of those you can switch it manually here." >> mailcow.conf
echo "# Please be aware that at least one of those variants should be installed on your maschine or mailcow will fail." >> mailcow.conf
echo "" >> mailcow.conf
echo "DOCKER_COMPOSE_VERSION=${DOCKER_COMPOSE_VERSION}" >> mailcow.conf
fi
elif [[ ${option} == "DOVEADM_PORT" ]]; then elif [[ ${option} == "DOVEADM_PORT" ]]; then
if ! grep -q ${option} mailcow.conf; then if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf" echo "Adding new option \"${option}\" to mailcow.conf"
@ -630,6 +651,82 @@ else
fi fi
fi fi
if ! [ $NEW_BRANCH ]; then
echo -e "\e[33mDetecting which build your mailcow runs on...\e[0m"
sleep 1
if [ ${BRANCH} == "master" ]; then
echo -e "\e[32mYou are receiving stable updates (master).\e[0m"
echo -e "\e[33mTo change that run the update.sh Script one time with the --nightly parameter to switch to nightly builds.\e[0m"
elif [ ${BRANCH} == "nightly" ]; then
echo -e "\e[31mYou are receiving unstable updates (nightly). These are for testing purposes only!!!\e[0m"
sleep 1
echo -e "\e[33mTo change that run the update.sh Script one time with the --stable parameter to switch to stable builds.\e[0m"
else
echo -e "\e[33mYou are receiving updates from a unsupported branch.\e[0m"
sleep 1
echo -e "\e[33mThe mailcow stack might still work but it is recommended to switch to the master branch (stable builds).\e[0m"
echo -e "\e[33mTo change that run the update.sh Script one time with the --stable parameter to switch to stable builds.\e[0m"
fi
elif [ $FORCE ]; then
echo -e "\e[31mYou are running in forced mode!\e[0m"
echo -e "\e[31mA Branch Switch can only be performed manually (monitored).\e[0m"
echo -e "\e[31mPlease rerun the update.sh Script without the --force/-f parameter.\e[0m"
sleep 1
elif [ $NEW_BRANCH == "master" ] && [ $CURRENT_BRANCH != "master" ]; then
echo -e "\e[33mYou are about to switch your mailcow Updates to the stable (master) branch.\e[0m"
sleep 1
echo -e "\e[33mBefore you do: Please take a backup of all components to ensure that no Data is lost...\e[0m"
sleep 1
echo -e "\e[31mWARNING: Please see on GitHub or ask in the communitys if a switch to master is stable or not.
In some rear cases a Update back to master can destroy your mailcow configuration in case of Database Upgrades etc.
Normally a upgrade back to master should be safe during each full release.
Check GitHub for Database Changes and Update only if there similar to the full release!\e[0m"
read -r -p "Are you sure you that want to continue upgrading to the stable (master) branch? [y/N] " response
if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "OK. If you prepared yourself for that please run the update.sh Script with the --stable parameter again to trigger this process here."
exit 0
fi
BRANCH=$NEW_BRANCH
DIFF_DIRECTORY=update_diffs
DIFF_FILE=${DIFF_DIRECTORY}/diff_before_upgrade_to_master_$(date +"%Y-%m-%d-%H-%M-%S")
mv diff_before_upgrade* ${DIFF_DIRECTORY}/ 2> /dev/null
if ! git diff-index --quiet HEAD; then
echo -e "\e[32mSaving diff to ${DIFF_FILE}...\e[0m"
mkdir -p ${DIFF_DIRECTORY}
git diff ${BRANCH} --stat > ${DIFF_FILE}
git diff ${BRANCH} >> ${DIFF_FILE}
fi
echo -e "\e[32mSwitching Branch to ${BRANCH}...\e[0m"
git fetch origin
git checkout -f ${BRANCH}
elif [ $NEW_BRANCH == "nightly" ] && [ $CURRENT_BRANCH != "nightly" ]; then
echo -e "\e[33mYou are about to switch your mailcow Updates to the unstable (nightly) branch.\e[0m"
sleep 1
echo -e "\e[33mBefore you do: Please take a backup of all components to ensure that no Data is lost...\e[0m"
sleep 1
echo -e "\e[31mWARNING: A switch to nightly is possible any time. But a switch back (to master) isn't.\e[0m"
read -r -p "Are you sure you that want to continue upgrading to the unstable (nightly) branch? [y/N] " response
if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "OK. If you prepared yourself for that please run the update.sh Script with the --nightly parameter again to trigger this process here."
exit 0
fi
BRANCH=$NEW_BRANCH
DIFF_DIRECTORY=update_diffs
DIFF_FILE=${DIFF_DIRECTORY}/diff_before_upgrade_to_nightly_$(date +"%Y-%m-%d-%H-%M-%S")
mv diff_before_upgrade* ${DIFF_DIRECTORY}/ 2> /dev/null
if ! git diff-index --quiet HEAD; then
echo -e "\e[32mSaving diff to ${DIFF_FILE}...\e[0m"
mkdir -p ${DIFF_DIRECTORY}
git diff ${BRANCH} --stat > ${DIFF_FILE}
git diff ${BRANCH} >> ${DIFF_FILE}
fi
git fetch origin
git checkout -f ${BRANCH}
fi
echo -e "\e[32mChecking for newer update script...\e[0m" echo -e "\e[32mChecking for newer update script...\e[0m"
SHA1_1=$(sha1sum update.sh) SHA1_1=$(sha1sum update.sh)
git fetch origin #${BRANCH} git fetch origin #${BRANCH}
@ -641,13 +738,6 @@ if [[ ${SHA1_1} != ${SHA1_2} ]]; then
exit 2 exit 2
fi fi
if [[ -f mailcow.conf ]]; then
source mailcow.conf
else
echo -e "\e[31mNo mailcow.conf - is mailcow installed?\e[0m"
exit 1
fi
if [ ! $FORCE ]; then if [ ! $FORCE ]; then
read -r -p "Are you sure you want to update mailcow: dockerized? All containers will be stopped. [y/N] " response read -r -p "Are you sure you want to update mailcow: dockerized? All containers will be stopped. [y/N] " response
if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
@ -657,20 +747,18 @@ if [ ! $FORCE ]; then
migrate_docker_nat migrate_docker_nat
fi fi
update_compose
remove_obsolete_nginx_ports remove_obsolete_nginx_ports
echo -e "\e[32mValidating docker-compose stack configuration...\e[0m" echo -e "\e[32mValidating docker-compose stack configuration...\e[0m"
sed -i 's/HTTPS_BIND:-:/HTTPS_BIND:-/g' docker-compose.yml sed -i 's/HTTPS_BIND:-:/HTTPS_BIND:-/g' docker-compose.yml
sed -i 's/HTTP_BIND:-:/HTTP_BIND:-/g' docker-compose.yml sed -i 's/HTTP_BIND:-:/HTTP_BIND:-/g' docker-compose.yml
if ! docker-compose config -q; then if ! $COMPOSE_COMMAND config -q; then
echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m" echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m"
exit 1 exit 1
fi fi
echo -e "\e[32mChecking for conflicting bridges...\e[0m" echo -e "\e[32mChecking for conflicting bridges...\e[0m"
MAILCOW_BRIDGE=$(docker-compose config | grep -i com.docker.network.bridge.name | cut -d':' -f2) MAILCOW_BRIDGE=$($COMPOSE_COMMAND config | grep -i com.docker.network.bridge.name | cut -d':' -f2)
while read NAT_ID; do while read NAT_ID; do
iptables -t nat -D POSTROUTING $NAT_ID iptables -t nat -D POSTROUTING $NAT_ID
done < <(iptables -L -vn -t nat --line-numbers | grep $IPV4_NETWORK | grep -E 'MASQUERADE.*all' | grep -v ${MAILCOW_BRIDGE} | cut -d' ' -f1) done < <(iptables -L -vn -t nat --line-numbers | grep $IPV4_NETWORK | grep -E 'MASQUERADE.*all' | grep -v ${MAILCOW_BRIDGE} | cut -d' ' -f1)
@ -690,8 +778,8 @@ prefetch_images
echo -e "\e[32mStopping mailcow...\e[0m" echo -e "\e[32mStopping mailcow...\e[0m"
sleep 2 sleep 2
MAILCOW_CONTAINERS=($(docker-compose ps -q)) MAILCOW_CONTAINERS=($($COMPOSE_COMMAND ps -q))
docker-compose down $COMPOSE_COMMAND down
echo -e "\e[32mChecking for remaining containers...\e[0m" echo -e "\e[32mChecking for remaining containers...\e[0m"
sleep 2 sleep 2
for container in "${MAILCOW_CONTAINERS[@]}"; do for container in "${MAILCOW_CONTAINERS[@]}"; do
@ -728,13 +816,13 @@ elif [[ ${MERGE_RETURN} == 1 ]]; then
elif [[ ${MERGE_RETURN} != 0 ]]; then elif [[ ${MERGE_RETURN} != 0 ]]; then
echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m" echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m"
echo echo
echo "Run docker-compose up -d to restart your stack without updates or try again after fixing the mentioned errors." echo "Run $COMPOSE_COMMAND up -d to restart your stack without updates or try again after fixing the mentioned errors."
exit 1 exit 1
fi fi
echo -e "\e[32mFetching new images, if any...\e[0m" echo -e "\e[32mFetching new images, if any...\e[0m"
sleep 2 sleep 2
docker-compose pull $COMPOSE_COMMAND pull
# Fix missing SSL, does not overwrite existing files # Fix missing SSL, does not overwrite existing files
[[ ! -d data/assets/ssl ]] && mkdir -p data/assets/ssl [[ ! -d data/assets/ssl ]] && mkdir -p data/assets/ssl
@ -746,7 +834,7 @@ if grep -q 'SYSCTL_IPV6_DISABLED=1' mailcow.conf; then
echo '!! IMPORTANT !!' echo '!! IMPORTANT !!'
echo echo
echo 'SYSCTL_IPV6_DISABLED was removed due to complications. IPv6 can be disabled by editing "docker-compose.yml" and setting "enable_ipv6: true" to "enable_ipv6: false".' echo 'SYSCTL_IPV6_DISABLED was removed due to complications. IPv6 can be disabled by editing "docker-compose.yml" and setting "enable_ipv6: true" to "enable_ipv6: false".'
echo 'This setting will only be active after a complete shutdown of mailcow by running "docker-compose down" followed by "docker-compose up -d".' echo 'This setting will only be active after a complete shutdown of mailcow by running $COMPOSE_COMMAND down followed by $COMPOSE_COMMAND up -d".'
echo echo
echo '!! IMPORTANT !!' echo '!! IMPORTANT !!'
echo echo
@ -774,26 +862,55 @@ if [ -f "data/conf/rspamd/local.d/metrics.conf" ]; then
fi fi
# Set app_info.inc.php # Set app_info.inc.php
mailcow_git_version=$(git describe --tags `git rev-list --tags --max-count=1`) if [ ${BRANCH} == "master" ]; then
mailcow_git_version=$(git describe --tags `git rev-list --tags --max-count=1`)
elif [ ${BRANCH} == "nightly" ]; then
mailcow_git_version=$(git rev-parse --short $(git rev-parse @{upstream}))
mailcow_last_git_version=""
else
mailcow_git_version=$(git rev-parse --short HEAD)
mailcow_last_git_version=""
fi
mailcow_git_commit=$(git rev-parse origin/${BRANCH})
mailcow_git_commit_date=$(git log -1 --format=%ci @{upstream} )
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo '<?php' > data/web/inc/app_info.inc.php echo '<?php' > data/web/inc/app_info.inc.php
echo ' $MAILCOW_GIT_VERSION="'$mailcow_git_version'";' >> data/web/inc/app_info.inc.php echo ' $MAILCOW_GIT_VERSION="'$mailcow_git_version'";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_LAST_GIT_VERSION="";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_GIT_OWNER="mailcow";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_GIT_REPO="mailcow-dockerized";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_GIT_URL="https://github.com/mailcow/mailcow-dockerized";' >> data/web/inc/app_info.inc.php echo ' $MAILCOW_GIT_URL="https://github.com/mailcow/mailcow-dockerized";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_GIT_COMMIT="'$mailcow_git_commit'";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_GIT_COMMIT_DATE="'$mailcow_git_commit_date'";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_BRANCH="'$BRANCH'";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_UPDATEDAT='$(date +%s)';' >> data/web/inc/app_info.inc.php
echo '?>' >> data/web/inc/app_info.inc.php echo '?>' >> data/web/inc/app_info.inc.php
else else
echo '<?php' > data/web/inc/app_info.inc.php echo '<?php' > data/web/inc/app_info.inc.php
echo ' $MAILCOW_GIT_VERSION="";' >> data/web/inc/app_info.inc.php echo ' $MAILCOW_GIT_VERSION="'$mailcow_git_version'";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_GIT_URL="";' >> data/web/inc/app_info.inc.php echo ' $MAILCOW_LAST_GIT_VERSION="";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_GIT_OWNER="mailcow";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_GIT_REPO="mailcow-dockerized";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_GIT_URL="https://github.com/mailcow/mailcow-dockerized";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_GIT_COMMIT="";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_GIT_COMMIT_DATE="";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_BRANCH="'$BRANCH'";' >> data/web/inc/app_info.inc.php
echo ' $MAILCOW_UPDATEDAT='$(date +%s)';' >> data/web/inc/app_info.inc.php
echo '?>' >> data/web/inc/app_info.inc.php echo '?>' >> data/web/inc/app_info.inc.php
echo -e "\e[33mCannot determine current git repository version...\e[0m" echo -e "\e[33mCannot determine current git repository version...\e[0m"
fi fi
# Set DOCKER_COMPOSE_VERSION
sed -i 's/^DOCKER_COMPOSE_VERSION=$/DOCKER_COMPOSE_VERSION='$DOCKER_COMPOSE_VERSION'/g' mailcow.conf
if [[ ${SKIP_START} == "y" ]]; then if [[ ${SKIP_START} == "y" ]]; then
echo -e "\e[33mNot starting mailcow, please run \"docker-compose up -d --remove-orphans\" to start mailcow.\e[0m" echo -e "\e[33mNot starting mailcow, please run \"$COMPOSE_COMMAND up -d --remove-orphans\" to start mailcow.\e[0m"
else else
echo -e "\e[32mStarting mailcow...\e[0m" echo -e "\e[32mStarting mailcow...\e[0m"
sleep 2 sleep 2
docker-compose up -d --remove-orphans $COMPOSE_COMMAND up -d --remove-orphans
fi fi
echo -e "\e[32mCollecting garbage...\e[0m" echo -e "\e[32mCollecting garbage...\e[0m"
@ -808,4 +925,4 @@ fi
# echo # echo
# git reflog --color=always | grep "Before update on " # git reflog --color=always | grep "Before update on "
# echo # echo
# echo "Use \"git reset --hard hash-on-the-left\" and run docker-compose up -d afterwards." # echo "Use \"git reset --hard hash-on-the-left\" and run $COMPOSE_COMMAND up -d afterwards."