diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index befe7db7..11402129 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: https://mailcow.github.io/mailcow-dockerized-docs/#help-mailcow +custom: ["https://www.servercow.de/mailcow?lang=en#sal"] diff --git a/.github/ISSUE_TEMPLATE/Bug_report.yml b/.github/ISSUE_TEMPLATE/Bug_report.yml index 6134a9ad..3cfbbe0d 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.yml +++ b/.github/ISSUE_TEMPLATE/Bug_report.yml @@ -7,8 +7,8 @@ body: label: Contribution guidelines description: Please read the contribution guidelines before proceeding. options: - - label: I've read the [contribution guidelines](https://github.com/mailcow/mailcow-dockerized/blob/master/CONTRIBUTING.md) and wholeheartedly agree - required: true + - label: I've read the [contribution guidelines](https://github.com/mailcow/mailcow-dockerized/blob/master/CONTRIBUTING.md) and wholeheartedly agree + required: true - type: checkboxes attributes: label: I've found a bug and checked that ... @@ -26,70 +26,132 @@ body: attributes: label: Description description: Please provide a brief description of the bug in 1-2 sentences. If applicable, add screenshots to help explain your problem. Very useful for bugs in mailcow UI. + render: plain text validations: required: true - type: textarea attributes: - label: Logs - description: Please take a look at the [official documentation](https://mailcow.github.io/mailcow-dockerized-docs/debug-logs/) and post the last few lines of logs, when the error occurs. For example, docker container logs of affected containers. This will be automatically formatted into code, so no need for backticks. - render: bash + label: "Logs:" + description: "Please take a look at the [official documentation](https://docs.mailcow.email/troubleshooting/debug-logs/) and post the last few lines of logs, when the error occurs. For example, docker container logs of affected containers. This will be automatically formatted into code, so no need for backticks." + render: plain text validations: required: true - type: textarea attributes: - label: Steps to reproduce - description: Please describe the steps to reproduce the bug. Screenshots can be added, if helpful. + label: "Steps to reproduce:" + description: "Please describe the steps to reproduce the bug. Screenshots can be added, if helpful." + render: plain text placeholder: |- 1. ... 2. ... 3. ... validations: required: true - - type: textarea + - type: markdown attributes: - label: System information - description: In this stage we would kindly ask you to attach general system information about your setup. - value: |- - | Question | Answer | - | --- | --- | - | My operating system | I_DO_REPLY_HERE | - | Is Apparmor, SELinux or similar active? | I_DO_REPLY_HERE | - | Virtualization technology (KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported** | I_DO_REPLY_HERE | - | Server/VM specifications (Memory, CPU Cores) | I_DO_REPLY_HERE | - | Docker version (`docker version`) | I_DO_REPLY_HERE | - | docker-compose version (`docker-compose version`) | I_DO_REPLY_HERE | - | mailcow version (```git describe --tags `git rev-list --tags --max-count=1` ```) | I_DO_REPLY_HERE | - | Reverse proxy (custom solution) | I_DO_REPLY_HERE | - - Output of `git diff origin/master`, any other changes to the code? If so, **please post them**: - ``` - YOUR OUTPUT GOES HERE - ``` - - All third-party firewalls and custom iptables rules are unsupported. **Please check the Docker docs about how to use Docker with your own ruleset**. Nevertheless, iptabels output can help us to help you: - iptables -L -vn: - ``` - YOUR OUTPUT GOES HERE - ``` - - ip6tables -L -vn: - ``` - YOUR OUTPUT GOES HERE - ``` - - iptables -L -vn -t nat: - ``` - YOUR OUTPUT GOES HERE - ``` - - ip6tables -L -vn -t nat: - ``` - YOUR OUTPUT GOES HERE - ``` - - DNS problems? Please run `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @172.22.1.254` (set the IP accordingly, if you changed the internal mailcow network) and post the output: - ``` - YOUR OUTPUT GOES HERE - ``` + value: | + ## System information + ### In this stage we would kindly ask you to attach general system information about your setup. + - type: dropdown + attributes: + label: "Which branch are you using?" + description: "#### `git rev-parse --abbrev-ref HEAD`" + multiple: false + options: + - master + - nightly + validations: + required: true + - type: input + attributes: + label: "Operating System:" + placeholder: "e.g. Ubuntu 22.04 LTS" + validations: + required: true + - type: input + attributes: + label: "Server/VM specifications:" + placeholder: "Memory, CPU Cores" + validations: + required: true + - type: input + attributes: + label: "Is Apparmor, SELinux or similar active?" + placeholder: "yes/no" + validations: + required: true + - type: input + attributes: + label: "Virtualization technology:" + placeholder: "KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported**" + validations: + required: true + - type: input + attributes: + label: "Docker version:" + description: "#### `docker version`" + placeholder: "20.10.21" + validations: + required: true + - type: input + attributes: + label: "docker-compose version or docker compose version:" + description: "#### `docker-compose version` or `docker compose version`" + placeholder: "v2.12.2" + validations: + required: true + - type: input + attributes: + label: "mailcow version:" + description: "#### ```git describe --tags `git rev-list --tags --max-count=1` ```" + placeholder: "2022-08" + validations: + required: true + - type: input + attributes: + label: "Reverse proxy:" + placeholder: "e.g. Nginx/Traefik" + validations: + required: true + - type: textarea + attributes: + label: "Logs of git diff:" + description: "#### Output of `git diff origin/master`, any other changes to the code? If so, **please post them**:" + render: plain text + validations: + required: true + - type: textarea + attributes: + label: "Logs of iptables -L -vn:" + description: "#### Output of `iptables -L -vn`" + render: plain text + validations: + required: true + - type: textarea + attributes: + label: "Logs of ip6tables -L -vn:" + description: "#### Output of `ip6tables -L -vn`" + render: plain text + validations: + required: true + - type: textarea + attributes: + label: "Logs of iptables -L -vn -t nat:" + description: "#### Output of `iptables -L -vn -t nat`" + render: plain text + validations: + required: true + - type: textarea + attributes: + label: "Logs of ip6tables -L -vn -t nat:" + description: "#### Output of `ip6tables -L -vn -t nat`" + render: plain text + validations: + required: true + - type: textarea + attributes: + label: "DNS check:" + description: "#### Output of `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @172.22.1.254` (set the IP accordingly, if you changed the internal mailcow network)" + render: plain text validations: required: true diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 00000000..36b4aec5 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,31 @@ +{ + "enabled": true, + "timezone": "Europe/Berlin", + "dependencyDashboard": true, + "dependencyDashboardTitle": "Renovate Dashboard", + "commitBody": "Signed-off-by: milkmaker ", + "rebaseWhen": "auto", + "labels": ["renovate"], + "assignees": [ + "@magiccc" + ], + "baseBranches": ["staging"], + "enabledManagers": ["github-actions", "regex", "docker-compose"], + "ignorePaths": [ + "data\/web\/inc\/lib\/vendor\/matthiasmullie\/minify\/**" + ], + "regexManagers": [ + { + "fileMatch": ["^helper-scripts\/nextcloud.sh$"], + "matchStrings": [ + "#\\srenovate:\\sdatasource=(?.*?) depName=(?.*?)( versioning=(?.*?))?( extractVersion=(?.*?))?\\s.*?_VERSION=(?.*)" + ] + }, + { + "fileMatch": ["(^|/)Dockerfile[^/]*$"], + "matchStrings": [ + "#\\srenovate:\\sdatasource=(?.*?) depName=(?.*?)( versioning=(?.*?))?\\s(ENV|ARG) .*?_VERSION=(?.*)\\s" + ] + } + ] +} diff --git a/.github/workflows/assets/check_prs_if_on_staging.png b/.github/workflows/assets/check_prs_if_on_staging.png new file mode 100644 index 00000000..2e0fc7ff Binary files /dev/null and b/.github/workflows/assets/check_prs_if_on_staging.png differ diff --git a/.github/workflows/check_prs_if_on_staging.yml b/.github/workflows/check_prs_if_on_staging.yml new file mode 100644 index 00000000..485dc26e --- /dev/null +++ b/.github/workflows/check_prs_if_on_staging.yml @@ -0,0 +1,33 @@ +name: Check PRs if on staging +on: + pull_request_target: + types: [opened, edited] +permissions: {} + +jobs: + is_not_staging: + runs-on: ubuntu-latest + if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging + steps: + - name: Send message + uses: thollander/actions-comment-pull-request@v2.3.1 + with: + GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }} + message: | + Thanks for contributing! + + I noticed that you didn't select `staging` as your base branch. Please change the base branch to `staging`. + See the attached picture on how to change the base branch to `staging`: + + ![check_prs_if_on_staging.png](https://raw.githubusercontent.com/mailcow/mailcow-dockerized/master/.github/workflows/assets/check_prs_if_on_staging.png) + + - name: Fail #we want to see failed checks in the PR + if: ${{ success() }} #set exit code to 1 even if commenting somehow failed + run: exit 1 + + is_staging: + runs-on: ubuntu-latest + if: github.event.pull_request.base.ref == 'staging' #check if the target branch is staging + steps: + - name: Success + run: exit 0 diff --git a/.github/workflows/close_old_issues_and_prs.yml b/.github/workflows/close_old_issues_and_prs.yml index 83a75d25..64002617 100644 --- a/.github/workflows/close_old_issues_and_prs.yml +++ b/.github/workflows/close_old_issues_and_prs.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - name: Mark/Close Stale Issues and Pull Requests 🗑️ - uses: actions/stale@v6.0.1 + uses: actions/stale@v7.0.0 with: repo-token: ${{ secrets.STALE_ACTION_PAT }} days-before-stale: 60 diff --git a/.github/workflows/image_builds.yml b/.github/workflows/image_builds.yml index fe660754..65678dff 100644 --- a/.github/workflows/image_builds.yml +++ b/.github/workflows/image_builds.yml @@ -33,13 +33,11 @@ jobs: 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} + docker compose build ${image} env: image: ${{ matrix.images }} diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml deleted file mode 100644 index ee083bf4..00000000 --- a/.github/workflows/integration_tests.yml +++ /dev/null @@ -1,63 +0,0 @@ -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' diff --git a/.github/workflows/pr_to_nightly.yml b/.github/workflows/pr_to_nightly.yml index fd9e4946..57aac781 100644 --- a/.github/workflows/pr_to_nightly.yml +++ b/.github/workflows/pr_to_nightly.yml @@ -12,7 +12,7 @@ jobs: with: fetch-depth: 0 - name: Run the Action - uses: devops-infra/action-pull-request@v0.5.1 + uses: devops-infra/action-pull-request@v0.5.5 with: github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }} title: Automatic PR to nightly from ${{ github.event.repository.updated_at}} diff --git a/.github/workflows/rebuild_backup_image.yml b/.github/workflows/rebuild_backup_image.yml new file mode 100644 index 00000000..21c218a8 --- /dev/null +++ b/.github/workflows/rebuild_backup_image.yml @@ -0,0 +1,34 @@ +name: Build mailcow backup image + +on: + schedule: + # At 00:00 on Sunday + - cron: "0 0 * * 0" + workflow_dispatch: # Allow to run workflow manually + +jobs: + docker_image_build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_USERNAME }} + password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + file: data/Dockerfiles/backup/Dockerfile + push: true + tags: mailcow/backup:latest diff --git a/.github/workflows/tweet-trigger-publish-release.yml b/.github/workflows/tweet-trigger-publish-release.yml deleted file mode 100644 index 82f1dc3a..00000000 --- a/.github/workflows/tweet-trigger-publish-release.yml +++ /dev/null @@ -1,17 +0,0 @@ -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' diff --git a/README.md b/README.md index 313fa13b..c15b8ef0 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,5 @@ # mailcow: dockerized - 🐮 + 🐋 = 💕 -## We stand with 🇺🇦 - -[![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) @@ -36,3 +33,9 @@ Telegram desktop clients are available for [multiple platforms](https://desktop. **Important**: mailcow makes use of various open-source software. Please assure you agree with their license before using mailcow. Any part of mailcow itself is released under **GNU General Public License, Version 3**. + +mailcow is a registered word mark of The Infrastructure Company GmbH, Parkstr. 42, 47877 Willich, Germany. + +The project is managed and maintained by The Infrastructure Company GmbH. + +Originated from @andryyy (André) \ No newline at end of file diff --git a/data/Dockerfiles/acme/Dockerfile b/data/Dockerfiles/acme/Dockerfile index f5b7b56c..571c3d08 100644 --- a/data/Dockerfiles/acme/Dockerfile +++ b/data/Dockerfiles/acme/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.17 LABEL maintainer "Andre Peters " diff --git a/data/Dockerfiles/acme/acme.sh b/data/Dockerfiles/acme/acme.sh index 4f5cb803..1cd456a4 100755 --- a/data/Dockerfiles/acme/acme.sh +++ b/data/Dockerfiles/acme/acme.sh @@ -213,11 +213,13 @@ while true; do done ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig') + if [[ ${SKIP_IP_CHECK} != "y" ]]; then # Start IP detection log_f "Detecting IP addresses..." IPV4=$(get_ipv4) IPV6=$(get_ipv6) log_f "OK: ${IPV4}, ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}" + fi ######################################### # IP and webroot challenge verification # diff --git a/data/Dockerfiles/clamd/Dockerfile b/data/Dockerfiles/clamd/Dockerfile index efbc6a4d..f381e0ef 100644 --- a/data/Dockerfiles/clamd/Dockerfile +++ b/data/Dockerfiles/clamd/Dockerfile @@ -1,4 +1,4 @@ -FROM clamav/clamav:0.105.1_base +FROM clamav/clamav:1.0.1-1_base LABEL maintainer "André Peters " diff --git a/data/Dockerfiles/dockerapi/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile index f021b73e..aa4a3858 100644 --- a/data/Dockerfiles/dockerapi/Dockerfile +++ b/data/Dockerfiles/dockerapi/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.17 LABEL maintainer "Andre Peters " @@ -13,6 +13,7 @@ RUN apk add --update --no-cache python3 \ fastapi \ uvicorn \ aiodocker \ + docker \ redis COPY docker-entrypoint.sh /app/ diff --git a/data/Dockerfiles/dockerapi/dockerapi.py b/data/Dockerfiles/dockerapi/dockerapi.py index 304c1781..9e699c22 100644 --- a/data/Dockerfiles/dockerapi/dockerapi.py +++ b/data/Dockerfiles/dockerapi/dockerapi.py @@ -1,5 +1,6 @@ from fastapi import FastAPI, Response, Request import aiodocker +import docker import psutil import sys import re @@ -9,11 +10,38 @@ import json import asyncio import redis from datetime import datetime +import logging +from logging.config import dictConfig +log_config = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "default": { + "()": "uvicorn.logging.DefaultFormatter", + "fmt": "%(levelprefix)s %(asctime)s %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", + + }, + }, + "handlers": { + "default": { + "formatter": "default", + "class": "logging.StreamHandler", + "stream": "ext://sys.stderr", + }, + }, + "loggers": { + "api-logger": {"handlers": ["default"], "level": "INFO"}, + }, +} +dictConfig(log_config) + containerIds_to_update = [] host_stats_isUpdating = False app = FastAPI() +logger = logging.getLogger('api-logger') @app.get("/host/stats") @@ -21,18 +49,15 @@ async def get_host_update_stats(): global host_stats_isUpdating if host_stats_isUpdating == False: - print("start host stats task") asyncio.create_task(get_host_stats()) host_stats_isUpdating = True while True: if redis_client.exists('host_stats'): break - print("wait for host_stats results") await asyncio.sleep(1.5) - print("host stats pulled") stats = json.loads(redis_client.get('host_stats')) return Response(content=json.dumps(stats, indent=4), media_type="application/json") @@ -106,14 +131,14 @@ async def post_containers(container_id : str, post_action : str, request: Reques else: api_call_method_name = '__'.join(['container_post', str(post_action) ]) - docker_utils = DockerUtils(async_docker_client) + docker_utils = DockerUtils(sync_docker_client) api_call_method = getattr(docker_utils, api_call_method_name, lambda container_id: Response(content=json.dumps({'type': 'danger', 'msg':'container_post - unknown api call' }, indent=4), media_type="application/json")) - print("api call: %s, container_id: %s" % (api_call_method_name, container_id)) - return await api_call_method(container_id, request_json) + logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id)) + return api_call_method(container_id, request_json) except Exception as e: - print("error - container_post: %s" % str(e)) + logger.error("error - container_post: %s" % str(e)) res = { "type": "danger", "msg": str(e) @@ -152,398 +177,289 @@ class DockerUtils: self.docker_client = docker_client # api call: container_post - post_action: stop - async def container_post__stop(self, container_id, request_json): - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - await container.stop() - res = { - 'type': 'success', - 'msg': 'command completed successfully' - } - return Response(content=json.dumps(res, indent=4), media_type="application/json") + def container_post__stop(self, container_id, request_json): + for container in self.docker_client.containers.list(all=True, filters={"id": container_id}): + container.stop() + res = { 'type': 'success', 'msg': 'command completed successfully'} + return Response(content=json.dumps(res, indent=4), media_type="application/json") # api call: container_post - post_action: start - async def container_post__start(self, container_id, request_json): - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - await container.start() - res = { - 'type': 'success', - 'msg': 'command completed successfully' - } + def container_post__start(self, container_id, request_json): + for container in self.docker_client.containers.list(all=True, filters={"id": container_id}): + container.start() + + res = { 'type': 'success', 'msg': 'command completed successfully'} return Response(content=json.dumps(res, indent=4), media_type="application/json") - - # api call: container_post - post_action: restart - async def container_post__restart(self, container_id, request_json): - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - await container.restart() - res = { - 'type': 'success', - 'msg': 'command completed successfully' - } + def container_post__restart(self, container_id, request_json): + for container in self.docker_client.containers.list(all=True, filters={"id": container_id}): + container.restart() + + res = { 'type': 'success', 'msg': 'command completed successfully'} return Response(content=json.dumps(res, indent=4), media_type="application/json") - - # api call: container_post - post_action: top - async def container_post__top(self, container_id, request_json): - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - ps_exec = await container.exec("ps") - async with ps_exec.start(detach=False) as stream: - ps_return = await stream.read_out() - - exec_details = await ps_exec.inspect() - if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0: - res = { - 'type': 'success', - 'msg': ps_return.data.decode('utf-8') - } - return Response(content=json.dumps(res, indent=4), media_type="application/json") - else: - res = { - 'type': 'danger', - 'msg': '' - } - return Response(content=json.dumps(res, indent=4), media_type="application/json") - + def container_post__top(self, container_id, request_json): + for container in self.docker_client.containers.list(all=True, filters={"id": container_id}): + res = { 'type': 'success', 'msg': container.top()} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: stats + def container_post__stats(self, container_id, request_json): + for container in self.docker_client.containers.list(all=True, filters={"id": container_id}): + for stat in container.stats(decode=True, stream=True): + res = { 'type': 'success', 'msg': stat} + return Response(content=json.dumps(res, indent=4), media_type="application/json") # api call: container_post - post_action: exec - cmd: mailq - task: delete - async def container_post__exec__mailq__delete(self, container_id, request_json): + def container_post__exec__mailq__delete(self, container_id, request_json): if 'items' in request_json: r = re.compile("^[0-9a-fA-F]+$") filtered_qids = filter(r.match, request_json['items']) if filtered_qids: flagged_qids = ['-d %s' % i for i in filtered_qids] - sanitized_string = str(' '.join(flagged_qids)) + sanitized_string = str(' '.join(flagged_qids)); + for container in self.docker_client.containers.list(filters={"id": container_id}): + postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) + return exec_run_handler('generic', postsuper_r) - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) - return await exec_run_handler('generic', postsuper_r_exec) # api call: container_post - post_action: exec - cmd: mailq - task: hold - async def container_post__exec__mailq__hold(self, container_id, request_json): + def container_post__exec__mailq__hold(self, container_id, request_json): if 'items' in request_json: r = re.compile("^[0-9a-fA-F]+$") filtered_qids = filter(r.match, request_json['items']) if filtered_qids: flagged_qids = ['-h %s' % i for i in filtered_qids] - sanitized_string = str(' '.join(flagged_qids)) - - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) - return await exec_run_handler('generic', postsuper_r_exec) + sanitized_string = str(' '.join(flagged_qids)); + for container in self.docker_client.containers.list(filters={"id": container_id}): + postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) + return exec_run_handler('generic', postsuper_r) # api call: container_post - post_action: exec - cmd: mailq - task: cat - async def container_post__exec__mailq__cat(self, container_id, request_json): + def container_post__exec__mailq__cat(self, container_id, request_json): if 'items' in request_json: r = re.compile("^[0-9a-fA-F]+$") filtered_qids = filter(r.match, request_json['items']) if filtered_qids: - sanitized_string = str(' '.join(filtered_qids)) + sanitized_string = str(' '.join(filtered_qids)); - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - postcat_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix') - return await exec_run_handler('utf8_text_only', postcat_exec) + for container in self.docker_client.containers.list(filters={"id": container_id}): + postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix') + if not postcat_return: + postcat_return = 'err: invalid' + return exec_run_handler('utf8_text_only', postcat_return) # api call: container_post - post_action: exec - cmd: mailq - task: unhold - async def container_post__exec__mailq__unhold(self, container_id, request_json): + def container_post__exec__mailq__unhold(self, container_id, request_json): if 'items' in request_json: r = re.compile("^[0-9a-fA-F]+$") filtered_qids = filter(r.match, request_json['items']) if filtered_qids: flagged_qids = ['-H %s' % i for i in filtered_qids] - sanitized_string = str(' '.join(flagged_qids)) - - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) - return await exec_run_handler('generic', postsuper_r_exec) - + sanitized_string = str(' '.join(flagged_qids)); + for container in self.docker_client.containers.list(filters={"id": container_id}): + postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string]) + return exec_run_handler('generic', postsuper_r) # api call: container_post - post_action: exec - cmd: mailq - task: deliver - async def container_post__exec__mailq__deliver(self, container_id, request_json): + def container_post__exec__mailq__deliver(self, container_id, request_json): if 'items' in request_json: r = re.compile("^[0-9a-fA-F]+$") filtered_qids = filter(r.match, request_json['items']) if filtered_qids: flagged_qids = ['-i %s' % i for i in filtered_qids] - - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - for i in flagged_qids: - postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix') - async with postsuper_r_exec.start(detach=False) as stream: - postsuper_r_return = await stream.read_out() - # todo: check each exit code - res = { - 'type': 'success', - 'msg': 'Scheduled immediate delivery' - } - return Response(content=json.dumps(res, indent=4), media_type="application/json") - - - # api call: container_post - post_action: exec - cmd: mailq - task: list - async def container_post__exec__mailq__list(self, container_id, request_json): - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - mailq_exec = await container.exec(["/usr/sbin/postqueue", "-j"], user='postfix') - return await exec_run_handler('utf8_text_only', mailq_exec) - - - # api call: container_post - post_action: exec - cmd: mailq - task: flush - async def container_post__exec__mailq__flush(self, container_id, request_json): - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - postsuper_r_exec = await container.exec(["/usr/sbin/postqueue", "-f"], user='postfix') - return await exec_run_handler('generic', postsuper_r_exec) - - - # api call: container_post - post_action: exec - cmd: mailq - task: super_delete - async def container_post__exec__mailq__super_delete(self, container_id, request_json): - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - postsuper_r_exec = await container.exec(["/usr/sbin/postsuper", "-d", "ALL"]) - return await exec_run_handler('generic', postsuper_r_exec) - - - # api call: container_post - post_action: exec - cmd: system - task: fts_rescan - async def container_post__exec__system__fts_rescan(self, container_id, request_json): - if 'username' in request_json: - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - rescan_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail') - async with rescan_exec.start(detach=False) as stream: - rescan_return = await stream.read_out() - - exec_details = await rescan_exec.inspect() - if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0: - res = { - 'type': 'success', - 'msg': 'fts_rescan: rescan triggered' - } - return Response(content=json.dumps(res, indent=4), media_type="application/json") - else: - res = { - 'type': 'warning', - 'msg': 'fts_rescan error' - } - return Response(content=json.dumps(res, indent=4), media_type="application/json") - - if 'all' in request_json: - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - rescan_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail') - async with rescan_exec.start(detach=False) as stream: - rescan_return = await stream.read_out() - - exec_details = await rescan_exec.inspect() - if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0: - res = { - 'type': 'success', - 'msg': 'fts_rescan: rescan triggered' - } - return Response(content=json.dumps(res, indent=4), media_type="application/json") - else: - res = { - 'type': 'warning', - 'msg': 'fts_rescan error' - } - return Response(content=json.dumps(res, indent=4), media_type="application/json") - - - # api call: container_post - post_action: exec - cmd: system - task: df - async def container_post__exec__system__df(self, container_id, request_json): - if 'dir' in request_json: - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - df_exec = await container.exec(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody') - async with df_exec.start(detach=False) as stream: - df_return = await stream.read_out() - - print(df_return) - print(await df_exec.inspect()) - exec_details = await df_exec.inspect() - if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0: - return df_return.data.decode('utf-8').rstrip() - else: - return "0,0,0,0,0,0" - - - # api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade - async def container_post__exec__system__mysql_upgrade(self, container_id, request_json): - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - sql_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql') - async with sql_exec.start(detach=False) as stream: - sql_return = await stream.read_out() - - exec_details = await sql_exec.inspect() - if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0: - matched = False - for line in sql_return.data.decode('utf-8').split("\n"): - if 'is already upgraded to' in line: - matched = True - if matched: - res = { - 'type': 'success', - 'msg': 'mysql_upgrade: already upgraded', - 'text': sql_return.data.decode('utf-8') - } - return Response(content=json.dumps(res, indent=4), media_type="application/json") - else: - await container.restart() - res = { - 'type': 'warning', - 'msg': 'mysql_upgrade: upgrade was applied', - 'text': sql_return.data.decode('utf-8') - } - return Response(content=json.dumps(res, indent=4), media_type="application/json") - else: - res = { - 'type': 'error', - 'msg': 'mysql_upgrade: error running command', - 'text': sql_return.data.decode('utf-8') - } + for container in self.docker_client.containers.list(filters={"id": container_id}): + for i in flagged_qids: + postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix') + # todo: check each exit code + res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'} return Response(content=json.dumps(res, indent=4), media_type="application/json") - - # api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql - async def container_post__exec__system__mysql_tzinfo_to_sql(self, container_id, request_json): - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - sql_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql') - async with sql_exec.start(detach=False) as stream: - sql_return = await stream.read_out() - - exec_details = await sql_exec.inspect() - if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0: - res = { - 'type': 'info', - 'msg': 'mysql_tzinfo_to_sql: command completed successfully', - 'text': sql_return.data.decode('utf-8') - } - return Response(content=json.dumps(res, indent=4), media_type="application/json") - else: - res = { - 'type': 'error', - 'msg': 'mysql_tzinfo_to_sql: error running command', - 'text': sql_return.data.decode('utf-8') - } - return Response(content=json.dumps(res, indent=4), media_type="application/json") - - # api call: container_post - post_action: exec - cmd: reload - task: dovecot - async def container_post__exec__reload__dovecot(self, container_id, request_json): - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - reload_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/dovecot reload"]) - return await exec_run_handler('generic', reload_exec) - - - # api call: container_post - post_action: exec - cmd: reload - task: postfix - async def container_post__exec__reload__postfix(self, container_id, request_json): - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - reload_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postfix reload"]) - return await exec_run_handler('generic', reload_exec) - - - # api call: container_post - post_action: exec - cmd: reload - task: nginx - async def container_post__exec__reload__nginx(self, container_id, request_json): - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - reload_exec = await container.exec(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"]) - return await exec_run_handler('generic', reload_exec) - - - # api call: container_post - post_action: exec - cmd: sieve - task: list - async def container_post__exec__sieve__list(self, container_id, request_json): - if 'username' in request_json: - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - sieve_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"]) - return await exec_run_handler('utf8_text_only', sieve_exec) - - - # api call: container_post - post_action: exec - cmd: sieve - task: print - async def container_post__exec__sieve__print(self, container_id, request_json): - if 'username' in request_json and 'script_name' in request_json: - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"] - sieve_exec = await container.exec(cmd) - return await exec_run_handler('utf8_text_only', sieve_exec) - - - # api call: container_post - post_action: exec - cmd: maildir - task: cleanup - async def container_post__exec__maildir__cleanup(self, container_id, request_json): - if 'maildir' in request_json: - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - sane_name = re.sub(r'\W+', '', request_json['maildir']) - cmd = ["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request_json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request_json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"] - maildir_cleanup_exec = await container.exec(cmd, user='vmail') - return await exec_run_handler('generic', maildir_cleanup_exec) - - # api call: container_post - post_action: exec - cmd: rspamd - task: worker_password - async def container_post__exec__rspamd__worker_password(self, container_id, request_json): - if 'raw' in request_json: - for container in (await self.docker_client.containers.list()): - if container._id == container_id: - cmd = "./set_worker_password.sh '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null" - rspamd_password_exec = await container.exec(cmd, user='_rspamd') - async with rspamd_password_exec.start(detach=False) as stream: - rspamd_password_return = await stream.read_out() - - matched = False - if "OK" in rspamd_password_return.data.decode('utf-8'): + # api call: container_post - post_action: exec - cmd: mailq - task: list + def container_post__exec__mailq__list(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix') + return exec_run_handler('utf8_text_only', mailq_return) + # api call: container_post - post_action: exec - cmd: mailq - task: flush + def container_post__exec__mailq__flush(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix') + return exec_run_handler('generic', postqueue_r) + # api call: container_post - post_action: exec - cmd: mailq - task: super_delete + def container_post__exec__mailq__super_delete(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"]) + return exec_run_handler('generic', postsuper_r) + # api call: container_post - post_action: exec - cmd: system - task: fts_rescan + def container_post__exec__system__fts_rescan(self, container_id, request_json): + if 'username' in request_json: + for container in self.docker_client.containers.list(filters={"id": container_id}): + rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail') + if rescan_return.exit_code == 0: + res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + else: + res = { 'type': 'warning', 'msg': 'fts_rescan error'} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + if 'all' in request_json: + for container in self.docker_client.containers.list(filters={"id": container_id}): + rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail') + if rescan_return.exit_code == 0: + res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + else: + res = { 'type': 'warning', 'msg': 'fts_rescan error'} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: exec - cmd: system - task: df + def container_post__exec__system__df(self, container_id, request_json): + if 'dir' in request_json: + for container in self.docker_client.containers.list(filters={"id": container_id}): + df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody') + if df_return.exit_code == 0: + return df_return.output.decode('utf-8').rstrip() + else: + return "0,0,0,0,0,0" + # api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade + def container_post__exec__system__mysql_upgrade(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql') + if sql_return.exit_code == 0: + matched = False + for line in sql_return.output.decode('utf-8').split("\n"): + if 'is already upgraded to' in line: matched = True - await container.restart() + if matched: + res = { 'type': 'success', 'msg':'mysql_upgrade: already upgraded', 'text': sql_return.output.decode('utf-8')} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + else: + container.restart() + res = { 'type': 'warning', 'msg':'mysql_upgrade: upgrade was applied', 'text': sql_return.output.decode('utf-8')} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + else: + res = { 'type': 'error', 'msg': 'mysql_upgrade: error running command', 'text': sql_return.output.decode('utf-8')} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql + def container_post__exec__system__mysql_tzinfo_to_sql(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql') + if sql_return.exit_code == 0: + res = { 'type': 'info', 'msg': 'mysql_tzinfo_to_sql: command completed successfully', 'text': sql_return.output.decode('utf-8')} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + else: + res = { 'type': 'error', 'msg': 'mysql_tzinfo_to_sql: error running command', 'text': sql_return.output.decode('utf-8')} + return Response(content=json.dumps(res, indent=4), media_type="application/json") + # api call: container_post - post_action: exec - cmd: reload - task: dovecot + def container_post__exec__reload__dovecot(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"]) + return exec_run_handler('generic', reload_return) + # api call: container_post - post_action: exec - cmd: reload - task: postfix + def container_post__exec__reload__postfix(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"]) + return exec_run_handler('generic', reload_return) + # api call: container_post - post_action: exec - cmd: reload - task: nginx + def container_post__exec__reload__nginx(self, container_id, request_json): + for container in self.docker_client.containers.list(filters={"id": container_id}): + reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"]) + return exec_run_handler('generic', reload_return) + # api call: container_post - post_action: exec - cmd: sieve - task: list + def container_post__exec__sieve__list(self, container_id, request_json): + if 'username' in request_json: + for container in self.docker_client.containers.list(filters={"id": container_id}): + sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"]) + return exec_run_handler('utf8_text_only', sieve_return) + # api call: container_post - post_action: exec - cmd: sieve - task: print + def container_post__exec__sieve__print(self, container_id, request_json): + if 'username' in request.json and 'script_name' in request_json: + for container in self.docker_client.containers.list(filters={"id": container_id}): + cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"] + sieve_return = container.exec_run(cmd) + return exec_run_handler('utf8_text_only', sieve_return) + # api call: container_post - post_action: exec - cmd: maildir - task: cleanup + def container_post__exec__maildir__cleanup(self, container_id, request_json): + if 'maildir' in request_json: + for container in self.docker_client.containers.list(filters={"id": container_id}): + sane_name = re.sub(r'\W+', '', request_json['maildir']) + cmd = ["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request_json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request_json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"] + maildir_cleanup = container.exec_run(cmd, user='vmail') + return exec_run_handler('generic', maildir_cleanup) + # api call: container_post - post_action: exec - cmd: rspamd - task: worker_password + def container_post__exec__rspamd__worker_password(self, container_id, request_json): + if 'raw' in request_json: + for container in self.docker_client.containers.list(filters={"id": container_id}): + cmd = "/usr/bin/rspamadm pw -e -p '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null" + cmd_response = exec_cmd_container(container, cmd, user="_rspamd") - if matched: - res = { - 'type': 'success', - 'msg': 'command completed successfully' - } - return Response(content=json.dumps(res, indent=4), media_type="application/json") - else: - res = { - 'type': 'danger', - 'msg': 'command did not complete' - } - return Response(content=json.dumps(res, indent=4), media_type="application/json") + matched = False + for line in cmd_response.split("\n"): + if '$2$' in line: + hash = line.strip() + hash_out = re.search('\$2\$.+$', hash).group(0) + rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip()) + rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc" + cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename) + cmd_response = exec_cmd_container(container, cmd, user="_rspamd") + if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response: + container.restart() + matched = True + if matched: + res = { 'type': 'success', 'msg': 'command completed successfully' } + logger.info('success changing Rspamd password') + return Response(content=json.dumps(res, indent=4), media_type="application/json") + else: + logger.error('failed changing Rspamd password') + res = { 'type': 'danger', 'msg': 'command did not complete' } + return Response(content=json.dumps(res, indent=4), media_type="application/json") +def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"): -async def exec_run_handler(type, exec_obj): - async with exec_obj.start(detach=False) as stream: - exec_return = await stream.read_out() + def recv_socket_data(c_socket, timeout): + c_socket.setblocking(0) + total_data=[] + data='' + begin=time.time() + while True: + if total_data and time.time()-begin > timeout: + break + elif time.time()-begin > timeout*2: + break + try: + data = c_socket.recv(8192) + if data: + total_data.append(data.decode('utf-8')) + #change the beginning time for measurement + begin=time.time() + else: + #sleep for sometime to indicate a gap + time.sleep(0.1) + break + except: + pass + return ''.join(total_data) + - if exec_return == None: - exec_return = "" - else: - exec_return = exec_return.data.decode('utf-8') - - if type == 'generic': - exec_details = await exec_obj.inspect() - if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0: - res = { - "type": "success", - "msg": "command completed successfully" - } + try : + socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock + if not cmd.endswith("\n"): + cmd = cmd + "\n" + socket.send(cmd.encode('utf-8')) + data = recv_socket_data(socket, timeout) + socket.close() + return data + except Exception as e: + logger.error("error - exec_cmd_container: %s" % str(e)) + traceback.print_exc(file=sys.stdout) +def exec_run_handler(type, output): + if type == 'generic': + if output.exit_code == 0: + res = { 'type': 'success', 'msg': 'command completed successfully' } return Response(content=json.dumps(res, indent=4), media_type="application/json") else: - res = { - "type": "success", - "msg": "'command failed: " + exec_return - } + res = { 'type': 'danger', 'msg': 'command failed: ' + output.output.decode('utf-8') } return Response(content=json.dumps(res, indent=4), media_type="application/json") if type == 'utf8_text_only': - return Response(content=exec_return, media_type="text/plain") + return Response(content=output.output.decode('utf-8'), media_type="text/plain") async def get_host_stats(wait=5): global host_stats_isUpdating @@ -570,12 +486,10 @@ async def get_host_stats(wait=5): "type": "danger", "msg": str(e) } - print(json.dumps(res, indent=4)) await asyncio.sleep(wait) host_stats_isUpdating = False - async def get_container_stats(container_id, wait=5, stop=False): global containerIds_to_update @@ -598,13 +512,11 @@ async def get_container_stats(container_id, wait=5, stop=False): "type": "danger", "msg": str(e) } - print(json.dumps(res, indent=4)) else: res = { "type": "danger", "msg": "no or invalid id defined" } - print(json.dumps(res, indent=4)) await asyncio.sleep(wait) if stop == True: @@ -615,9 +527,13 @@ async def get_container_stats(container_id, wait=5, stop=False): await get_container_stats(container_id, wait=0, stop=True) + if os.environ['REDIS_SLAVEOF_IP'] != "": redis_client = redis.Redis(host=os.environ['REDIS_SLAVEOF_IP'], port=os.environ['REDIS_SLAVEOF_PORT'], db=0) else: redis_client = redis.Redis(host='redis-mailcow', port=6379, db=0) +sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto') async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock') + +logger.info('DockerApi started') diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 1fa97641..2460cb04 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -6,7 +6,7 @@ ARG DOVECOT=2.3.19.1 ARG FLATCURVE=v0.3.2 ARG XAPIAN=1.4.21 ENV LC_ALL C -ENV GOSU_VERSION 1.14 + # Add groups and users before installing Dovecot to not break compatibility RUN touch /etc/default/locale \ diff --git a/data/Dockerfiles/dovecot/imapsync_runner.pl b/data/Dockerfiles/dovecot/imapsync_runner.pl index d3aed97b..5b297abd 100644 --- a/data/Dockerfiles/dovecot/imapsync_runner.pl +++ b/data/Dockerfiles/dovecot/imapsync_runner.pl @@ -51,8 +51,8 @@ sub sig_handler { die "sig_handler received signal, preparing to exit...\n"; }; -open my $file, '<', "/etc/sogo/sieve.creds"; -my $creds = <$file>; +open my $file, '<', "/etc/sogo/sieve.creds"; +my $creds = <$file>; close $file; my ($master_user, $master_pass) = split /:/, $creds; my $sth = $dbh->prepare("SELECT id, @@ -166,17 +166,11 @@ while ($row = $sth->fetchrow_arrayref()) { $success = 1; } - $keep_job_active = 1; - if (defined $exit_status && $exit_status eq "EXIT_AUTHENTICATION_FAILURE_USER1") { - $keep_job_active = 0; - } - - $update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, success = ?, exit_status = ?, active = ? WHERE id = ?"); + $update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, success = ?, exit_status = ? WHERE id = ?"); $update->bind_param( 1, ${stdout} ); $update->bind_param( 2, ${success} ); $update->bind_param( 3, ${exit_status} ); - $update->bind_param( 4, ${keep_job_active} ); - $update->bind_param( 5, ${id} ); + $update->bind_param( 4, ${id} ); $update->execute(); } catch { $update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync', success = 0 WHERE id = ?"); diff --git a/data/Dockerfiles/netfilter/Dockerfile b/data/Dockerfiles/netfilter/Dockerfile index 621da149..bc707391 100644 --- a/data/Dockerfiles/netfilter/Dockerfile +++ b/data/Dockerfiles/netfilter/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.17 LABEL maintainer "Andre Peters " ENV XTABLES_LIBDIR /usr/lib/xtables diff --git a/data/Dockerfiles/netfilter/server.py b/data/Dockerfiles/netfilter/server.py index 382a3f78..0b0e2a41 100644 --- a/data/Dockerfiles/netfilter/server.py +++ b/data/Dockerfiles/netfilter/server.py @@ -97,9 +97,9 @@ def refreshF2bregex(): f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+' f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+' f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+' - f2bregex[6] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),' - f2bregex[7] = '-login: Aborted login \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' - f2bregex[8] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' + f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),' + f2bregex[7] = '-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' + f2bregex[8] = '-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked' f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+' r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False)) @@ -359,21 +359,28 @@ def snat4(snat_target): chain = iptc.Chain(table, 'POSTROUTING') table.autocommit = False new_rule = get_snat4_rule() - for position, rule in enumerate(chain.rules): - match = all(( - new_rule.get_src() == rule.get_src(), - new_rule.get_dst() == rule.get_dst(), - new_rule.target.parameters == rule.target.parameters, - new_rule.target.name == rule.target.name - )) - if position == 0: - if not match: - logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}') - chain.insert_rule(new_rule) - else: - if match: - logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}') - chain.delete_rule(rule) + + if not chain.rules: + # if there are no rules in the chain, insert the new rule directly + logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}') + chain.insert_rule(new_rule) + else: + for position, rule in enumerate(chain.rules): + match = all(( + new_rule.get_src() == rule.get_src(), + new_rule.get_dst() == rule.get_dst(), + new_rule.target.parameters == rule.target.parameters, + new_rule.target.name == rule.target.name + )) + if position == 0: + if not match: + logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}') + chain.insert_rule(new_rule) + else: + if match: + logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}') + chain.delete_rule(rule) + table.commit() table.autocommit = True except: diff --git a/data/Dockerfiles/olefy/Dockerfile b/data/Dockerfiles/olefy/Dockerfile index 889f84b4..10d63d02 100644 --- a/data/Dockerfiles/olefy/Dockerfile +++ b/data/Dockerfiles/olefy/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.17 LABEL maintainer "Andre Peters " WORKDIR /app diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index 74035c02..c8713e04 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -1,12 +1,18 @@ -FROM php:8.0-fpm-alpine3.16 +FROM php:8.1-fpm-alpine3.17 LABEL maintainer "Andre Peters " -ENV APCU_PECL 5.1.21 -ENV IMAGICK_PECL 3.7.0 -# Mailparse is pulled from master branch -#ENV MAILPARSE_PECL 3.0.2 -ENV MEMCACHED_PECL 3.2.0 -ENV REDIS_PECL 5.3.7 +# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced +ARG APCU_PECL_VERSION=5.1.22 +# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced +ARG IMAGICK_PECL_VERSION=3.7.0 +# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced +ARG MAILPARSE_PECL_VERSION=3.1.4 +# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced +ARG MEMCACHED_PECL_VERSION=3.2.0 +# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced +ARG REDIS_PECL_VERSION=5.3.7 +# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced +ARG COMPOSER_VERSION=2.5.4 RUN apk add -U --no-cache autoconf \ aspell-dev \ @@ -18,6 +24,7 @@ RUN apk add -U --no-cache autoconf \ freetype-dev \ g++ \ git \ + gettext \ gettext-dev \ gmp-dev \ gnupg \ @@ -27,8 +34,11 @@ RUN apk add -U --no-cache autoconf \ imagemagick-dev \ imap-dev \ jq \ + libavif \ + libavif-dev \ libjpeg-turbo \ libjpeg-turbo-dev \ + libmemcached \ libmemcached-dev \ libpng \ libpng-dev \ @@ -38,7 +48,9 @@ RUN apk add -U --no-cache autoconf \ libtool \ libwebp-dev \ libxml2-dev \ + libxpm \ libxpm-dev \ + libzip \ libzip-dev \ make \ mysql-client \ @@ -49,22 +61,24 @@ RUN apk add -U --no-cache autoconf \ samba-client \ zlib-dev \ tzdata \ - && git clone https://github.com/php/pecl-mail-mailparse \ - && cd pecl-mail-mailparse \ - && pecl install package.xml \ - && cd .. \ - && rm -r pecl-mail-mailparse \ - && pecl install redis-${REDIS_PECL} memcached-${MEMCACHED_PECL} APCu-${APCU_PECL} imagick-${IMAGICK_PECL} \ + && pecl install APCu-${APCU_PECL_VERSION} \ + && pecl install imagick-${IMAGICK_PECL_VERSION} \ + && pecl install mailparse-${MAILPARSE_PECL_VERSION} \ + && pecl install memcached-${MEMCACHED_PECL_VERSION} \ + && pecl install redis-${REDIS_PECL_VERSION} \ && docker-php-ext-enable apcu imagick memcached mailparse redis \ && pecl clear-cache \ && docker-php-ext-configure intl \ && docker-php-ext-configure exif \ && docker-php-ext-configure gd --with-freetype=/usr/include/ \ --with-jpeg=/usr/include/ \ + --with-webp \ + --with-xpm \ + --with-avif \ && docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql pspell soap sockets zip bcmath gmp \ && docker-php-ext-configure imap --with-imap --with-imap-ssl \ && docker-php-ext-install -j 4 imap \ - && curl --silent --show-error https://getcomposer.org/installer | php \ + && curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \ && mv composer.phar /usr/local/bin/composer \ && chmod +x /usr/local/bin/composer \ && apk del --purge autoconf \ @@ -72,15 +86,21 @@ RUN apk add -U --no-cache autoconf \ cyrus-sasl-dev \ freetype-dev \ g++ \ + gettext-dev \ icu-dev \ imagemagick-dev \ imap-dev \ + libavif-dev \ libjpeg-turbo-dev \ + libmemcached-dev \ libpng-dev \ libressl-dev \ libwebp-dev \ libxml2-dev \ + libxpm-dev \ + libzip-dev \ make \ + openldap-dev \ pcre-dev \ zlib-dev @@ -88,4 +108,4 @@ COPY ./docker-entrypoint.sh / ENTRYPOINT ["/docker-entrypoint.sh"] -CMD ["php-fpm"] +CMD ["php-fpm"] \ No newline at end of file diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index f08600ac..da8f23be 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -3,8 +3,9 @@ LABEL maintainer "Andre Peters " ARG DEBIAN_FRONTEND=noninteractive ARG SOGO_DEBIAN_REPOSITORY=http://packages.sogo.nu/nightly/5/debian/ +# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced +ARG GOSU_VERSION=1.16 ENV LC_ALL C -ENV GOSU_VERSION 1.14 # Prerequisites RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \ diff --git a/data/Dockerfiles/solr/Dockerfile b/data/Dockerfiles/solr/Dockerfile index 06299257..0c5af1af 100644 --- a/data/Dockerfiles/solr/Dockerfile +++ b/data/Dockerfiles/solr/Dockerfile @@ -2,7 +2,8 @@ FROM solr:7.7-slim USER root -ENV GOSU_VERSION 1.11 +# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced +ARG GOSU_VERSION=1.16 COPY solr.sh / COPY solr-config-7.7.0.xml / diff --git a/data/Dockerfiles/unbound/Dockerfile b/data/Dockerfiles/unbound/Dockerfile index 0b1cefe9..d9756d04 100644 --- a/data/Dockerfiles/unbound/Dockerfile +++ b/data/Dockerfiles/unbound/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.17 LABEL maintainer "Andre Peters " diff --git a/data/Dockerfiles/watchdog/Dockerfile b/data/Dockerfiles/watchdog/Dockerfile index 637c4680..654dea08 100644 --- a/data/Dockerfiles/watchdog/Dockerfile +++ b/data/Dockerfiles/watchdog/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.17 LABEL maintainer "André Peters " # Installation diff --git a/data/conf/rspamd/custom/bulk_header.map b/data/conf/rspamd/custom/bulk_header.map index 39aa7fea..69a20af8 100644 --- a/data/conf/rspamd/custom/bulk_header.map +++ b/data/conf/rspamd/custom/bulk_header.map @@ -3,7 +3,6 @@ /.*episerver.*/i /.*supergewinne.*/i /List-Unsubscribe.*nbps\.eu/i -/X-Mailer: AWeber.*/i /.*regiofinder.*/i /.*EmailSocket.*/i /List-Unsubscribe:.*respread.*/i diff --git a/data/conf/rspamd/local.d/metadata_exporter.conf b/data/conf/rspamd/local.d/metadata_exporter.conf index 47373d99..daaa79b4 100644 --- a/data/conf/rspamd/local.d/metadata_exporter.conf +++ b/data/conf/rspamd/local.d/metadata_exporter.conf @@ -16,8 +16,7 @@ rules { backend = "http"; url = "http://nginx:9081/pushover.php"; selector = "mailcow_rcpt"; - # Only return msgid, do not parse the full message - formatter = "msgid"; + formatter = "json"; meta_headers = true; } } diff --git a/data/conf/rspamd/local.d/multimap.conf b/data/conf/rspamd/local.d/multimap.conf index 17ada99e..3f554c5b 100644 --- a/data/conf/rspamd/local.d/multimap.conf +++ b/data/conf/rspamd/local.d/multimap.conf @@ -175,7 +175,7 @@ BAD_SUBJECT_00 { type = "header"; header = "subject"; regexp = true; - map = "http://nullnull.org/bad-subject-regex.txt"; + map = "http://fuzzy.mailcow.email/bad-subject-regex.txt"; score = 6.0; symbols_set = ["BAD_SUBJECT_00"]; } diff --git a/data/conf/rspamd/meta_exporter/pushover.php b/data/conf/rspamd/meta_exporter/pushover.php index a5e83343..10265d15 100644 --- a/data/conf/rspamd/meta_exporter/pushover.php +++ b/data/conf/rspamd/meta_exporter/pushover.php @@ -47,12 +47,14 @@ if (!function_exists('getallheaders')) { } $headers = getallheaders(); +$json_body = json_decode(file_get_contents('php://input')); $qid = $headers['X-Rspamd-Qid']; $rcpts = $headers['X-Rspamd-Rcpt']; $sender = $headers['X-Rspamd-From']; $ip = $headers['X-Rspamd-Ip']; $subject = $headers['X-Rspamd-Subject']; +$messageid= $json_body->message_id; $priority = 0; $symbols_array = json_decode($headers['X-Rspamd-Symbols'], true); @@ -65,6 +67,20 @@ if (is_array($symbols_array)) { } } +$sender_address = $json_body->header_from[0]; +$sender_name = '-'; +if (preg_match('/(?.*?)<(?
.*?)>/i', $sender_address, $matches)) { + $sender_address = $matches['address']; + $sender_name = trim($matches['name'], '"\' '); +} + +$to_address = $json_body->header_to[0]; +$to_name = '-'; +if (preg_match('/(?.*?)<(?
.*?)>/i', $to_address, $matches)) { + $to_address = $matches['address']; + $to_name = trim($matches['name'], '"\' '); +} + $rcpt_final_mailboxes = array(); // Loop through all rcpts @@ -229,9 +245,16 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) { $post_fields = array( "token" => $api_data['token'], "user" => $api_data['key'], - "title" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}'), array($subject, $sender), $title)), + "title" => sprintf("%s", str_replace( + array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO_NAME}', '{TO_ADDRESS}', '{MSG_ID}'), + array($subject, $sender, $sender_name, $sender_address, $to_name, $to_address, $messageid), $title) + ), "priority" => $priority, - "message" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}'), array($subject, $sender), $text)) + "message" => sprintf("%s", str_replace( + array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO_NAME}', '{TO_ADDRESS}', '{MSG_ID}', '\n'), + array($subject, $sender, $sender_name, $sender_address, $to_name, $to_address, $messageid, PHP_EOL), $text) + ), + "sound" => $attributes['sound'] ?? "pushover" ); if ($attributes['evaluate_x_prio'] == "1" && $priority == 1) { $post_fields['expire'] = 600; diff --git a/data/web/_status.502.html b/data/web/_status.502.html index efbc0e8b..35a66ba9 100644 --- a/data/web/_status.502.html +++ b/data/web/_status.502.html @@ -13,12 +13,12 @@ Please check the logs or contact support if the error persists.

Quick debugging

Check Nginx and PHP logs:

-
docker-compose logs --tail=200 php-fpm-mailcow nginx-mailcow
+
docker compose logs --tail=200 php-fpm-mailcow nginx-mailcow

Make sure your SQL credentials in mailcow.conf (a link to .env) do fit your initialized SQL volume. If you see an access denied, you might have the wrong mailcow.conf:

-
source mailcow.conf ; docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME}
+
source mailcow.conf ; docker compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME}

In case of a previous failed installation, create a backup of your existing data, followed by removing all volumes and starting over (NEVER do this with a production system, it will remove ALL data):

BACKUP_LOCATION=/tmp/ ./helper-scripts/backup_and_restore.sh backup all
-
docker-compose down --volumes ; docker-compose up -d
+
docker compose down --volumes ; docker compose up -d

Make sure your timezone is correct. Use "America/New_York" for example, do not use spaces. Check here for a list.


Click to learn more about getting support. diff --git a/data/web/admin.php b/data/web/admin.php index e53d18fd..cd3eb890 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -10,9 +10,6 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $tfa_data = get_tfa(); $fido2_data = fido2(array("action" => "get_friendly_names")); -if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) { - $_SESSION['gal'] = json_decode($license_cache, true); -} $js_minifier->add('/web/js/site/admin.js'); $js_minifier->add('/web/js/presets/rspamd.js'); @@ -89,7 +86,6 @@ $template_data = [ 'tfa_id' => @$_SESSION['tfa_id'], 'fido2_cid' => @$_SESSION['fido2_cid'], 'fido2_data' => $fido2_data, - 'gal' => @$_SESSION['gal'], 'api' => [ 'ro' => admin_api('ro', 'get'), 'rw' => admin_api('rw', 'get'), @@ -107,6 +103,7 @@ $template_data = [ 'rsettings' => $rsettings, 'rspamd_regex_maps' => $rspamd_regex_maps, 'logo_specs' => customize('get', 'main_logo_specs'), + 'ip_check' => customize('get', 'ip_check'), 'password_complexity' => password_complexity('get'), 'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'], 'lang_admin' => json_encode($lang['admin']), diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index 8fb6245c..5e07c4b3 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -699,6 +699,38 @@ paths: type: string type: object summary: Create Domain Admin user + /api/v1/add/sso/domain-admin: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + token: "591F6D-5C3DD2-7455CD-DAF1C1-AA4FCC" + description: OK + headers: { } + tags: + - Single Sign-On + description: >- + Using this endpoint you can issue a token for Domain Admin user. This token can be used for + autologin Domain Admin user by using query_string var sso_token={token}. Token expiration time is 30s + operationId: Issue Domain Admin SSO token + requestBody: + content: + application/json: + schema: + example: + username: testadmin + properties: + username: + description: the username for the admin user + type: object + type: object + summary: Issue Domain Admin SSO token /api/v1/edit/da-acl: post: responses: @@ -1999,7 +2031,7 @@ paths: - domain.tld - domain2.tld properties: - items: + items: type: array items: type: string @@ -2993,7 +3025,7 @@ paths: application/json: schema: type: array - items: + items: type: object properties: log: @@ -3349,6 +3381,7 @@ paths: evaluate_x_prio: "0" key: 21e8918e1jksdjcpis712 only_x_prio: "0" + sound: "pushover" senders: "" senders_regex: "" text: "" @@ -3392,6 +3425,7 @@ paths: evaluate_x_prio: "0" key: 21e8918e1jksdjcpis712 only_x_prio: "0" + sound: "pushover" senders: "" senders_regex: "" text: "" @@ -3413,6 +3447,9 @@ paths: only_x_prio: description: Only send push for prio mails type: number + sound: + description: Set notification sound + type: string senders: description: Only send push for emails from these senders type: string @@ -5501,6 +5538,60 @@ paths: attr: spam_score: "8,15" summary: Edit mailbox spam filter score + "/api/v1/get/mailbox/all/{domain}": + get: + parameters: + - description: name of domain + in: path + name: domain + required: false + schema: + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - active: "1" + attributes: + force_pw_update: "0" + mailbox_format: "maildir:" + quarantine_notification: never + sogo_access: "1" + tls_enforce_in: "0" + tls_enforce_out: "0" + domain: domain3.tld + is_relayed: 0 + local_part: info + max_new_quota: 10737418240 + messages: 0 + name: Full name + percent_class: success + percent_in_use: 0 + quota: 3221225472 + quota_used: 0 + rl: false + spam_aliases: 0 + username: info@domain3.tld + tags: ["tag1", "tag2"] + description: OK + headers: {} + tags: + - Mailboxes + description: You can list all mailboxes existing in system for a specific domain. + operationId: Get mailboxes of a domain + summary: Get mailboxes of a domain tags: - name: Domains @@ -5527,6 +5618,8 @@ tags: description: Manage DKIM keys - name: Domain admin description: Create or udpdate domain admin users + - name: Single Sign-On + description: Issue tokens for users - name: Address Rewriting description: Create BCC maps or recipient maps - name: Outgoing TLS Policy Map Overrides diff --git a/data/web/css/build/011-datatables.css b/data/web/css/build/011-datatables.css index e0512bdf..d03514ff 100644 --- a/data/web/css/build/011-datatables.css +++ b/data/web/css/build/011-datatables.css @@ -4,10 +4,10 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-1.12.0/r-2.3.0/sl-1.4.0 + * https://datatables.net/download/#bs5/dt-1.13.1/r-2.4.0/sl-1.5.0 * * Included libraries: - * DataTables 1.12.0, Responsive 2.3.0, Select 1.4.0 + * DataTables 1.13.1, Responsive 2.4.0, Select 1.5.0 */ @charset "UTF-8"; @@ -63,7 +63,7 @@ table.dataTable thead > tr > td.sorting_desc_disabled:after { opacity: 0.125; right: 10px; line-height: 9px; - font-size: 0.9em; + font-size: 0.8em; } table.dataTable thead > tr > th.sorting:before, table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:before, table.dataTable thead > tr > th.sorting_asc_disabled:before, table.dataTable thead > tr > th.sorting_desc_disabled:before, table.dataTable thead > tr > td.sorting:before, @@ -72,7 +72,7 @@ table.dataTable thead > tr > td.sorting_desc:before, table.dataTable thead > tr > td.sorting_asc_disabled:before, table.dataTable thead > tr > td.sorting_desc_disabled:before { bottom: 50%; - content: "▴"; + content: "▲"; } table.dataTable thead > tr > th.sorting:after, table.dataTable thead > tr > th.sorting_asc:after, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > th.sorting_asc_disabled:after, table.dataTable thead > tr > th.sorting_desc_disabled:after, table.dataTable thead > tr > td.sorting:after, @@ -81,7 +81,7 @@ table.dataTable thead > tr > td.sorting_desc:after, table.dataTable thead > tr > td.sorting_asc_disabled:after, table.dataTable thead > tr > td.sorting_desc_disabled:after { top: 50%; - content: "▾"; + content: "▼"; } table.dataTable thead > tr > th.sorting_asc:before, table.dataTable thead > tr > th.sorting_desc:after, table.dataTable thead > tr > td.sorting_asc:before, @@ -287,6 +287,9 @@ table.dataTable > tbody > tr.selected > * { box-shadow: inset 0 0 0 9999px rgba(13, 110, 253, 0.9); color: white; } +table.dataTable > tbody > tr.selected a { + color: #090a0b; +} table.dataTable.table-striped > tbody > tr.odd > * { box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.05); } @@ -335,6 +338,9 @@ div.dataTables_wrapper div.dataTables_paginate ul.pagination { white-space: nowrap; justify-content: flex-end; } +div.dataTables_wrapper div.dt-row { + position: relative; +} div.dataTables_scrollHead table.dataTable { margin-bottom: 0 !important; @@ -380,17 +386,6 @@ div.dataTables_wrapper div.dataTables_paginate { table.dataTable.table-sm > thead > tr > th:not(.sorting_disabled) { padding-right: 20px; } -table.dataTable.table-sm .sorting:before, -table.dataTable.table-sm .sorting_asc:before, -table.dataTable.table-sm .sorting_desc:before { - top: 5px; - right: 0.85em; -} -table.dataTable.table-sm .sorting:after, -table.dataTable.table-sm .sorting_asc:after, -table.dataTable.table-sm .sorting_desc:after { - top: 5px; -} table.table-bordered.dataTable { border-right-width: 0; @@ -629,13 +624,13 @@ table.dataTable > tbody > tr > .selected { background-color: rgba(13, 110, 253, 0.9); color: white; } -table.dataTable tbody td.select-checkbox, -table.dataTable tbody th.select-checkbox { +table.dataTable > tbody > tr > td.select-checkbox, +table.dataTable > tbody > tr > th.select-checkbox { position: relative; } -table.dataTable tbody td.select-checkbox:before, table.dataTable tbody td.select-checkbox:after, -table.dataTable tbody th.select-checkbox:before, -table.dataTable tbody th.select-checkbox:after { +table.dataTable > tbody > tr > td.select-checkbox:before, table.dataTable > tbody > tr > td.select-checkbox:after, +table.dataTable > tbody > tr > th.select-checkbox:before, +table.dataTable > tbody > tr > th.select-checkbox:after { display: block; position: absolute; top: 1.2em; @@ -644,20 +639,20 @@ table.dataTable tbody th.select-checkbox:after { height: 12px; box-sizing: border-box; } -table.dataTable tbody td.select-checkbox:before, -table.dataTable tbody th.select-checkbox:before { +table.dataTable > tbody > tr > td.select-checkbox:before, +table.dataTable > tbody > tr > th.select-checkbox:before { content: " "; margin-top: -5px; margin-left: -6px; border: 1px solid black; border-radius: 3px; } -table.dataTable tr.selected td.select-checkbox:before, -table.dataTable tr.selected th.select-checkbox:before { +table.dataTable > tbody > tr.selected > td.select-checkbox:before, +table.dataTable > tbody > tr.selected > th.select-checkbox:before { border: 1px solid white; } -table.dataTable tr.selected td.select-checkbox:after, -table.dataTable tr.selected th.select-checkbox:after { +table.dataTable > tbody > tr.selected > td.select-checkbox:after, +table.dataTable > tbody > tr.selected > th.select-checkbox:after { content: "✓"; font-size: 20px; margin-top: -19px; @@ -665,12 +660,12 @@ table.dataTable tr.selected th.select-checkbox:after { text-align: center; text-shadow: 1px 1px #B0BED9, -1px -1px #B0BED9, 1px -1px #B0BED9, -1px 1px #B0BED9; } -table.dataTable.compact tbody td.select-checkbox:before, -table.dataTable.compact tbody th.select-checkbox:before { +table.dataTable.compact > tbody > tr > td.select-checkbox:before, +table.dataTable.compact > tbody > tr > th.select-checkbox:before { margin-top: -12px; } -table.dataTable.compact tr.selected td.select-checkbox:after, -table.dataTable.compact tr.selected th.select-checkbox:after { +table.dataTable.compact > tbody > tr.selected > td.select-checkbox:after, +table.dataTable.compact > tbody > tr.selected > th.select-checkbox:after { margin-top: -16px; } @@ -690,4 +685,3 @@ table.dataTable.table-sm tbody td.select-checkbox::before { margin-top: -9px; } - diff --git a/data/web/css/build/015-datatables.css b/data/web/css/build/013-datatables.css similarity index 86% rename from data/web/css/build/015-datatables.css rename to data/web/css/build/013-datatables.css index e5518ff8..13378460 100644 --- a/data/web/css/build/015-datatables.css +++ b/data/web/css/build/013-datatables.css @@ -77,4 +77,22 @@ li .dtr-data { table.dataTable>tbody>tr.child span.dtr-title { width: 30%; max-width: 250px; -} \ No newline at end of file +} + + +div.dataTables_wrapper div.dataTables_filter { + text-align: left; +} +div.dataTables_wrapper div.dataTables_length { + text-align: right; +} +.dataTables_paginate, .dataTables_length, .dataTables_filter { + margin: 10px 0!important; +} + +td.dt-text-right { + text-align: end !important; +} +th.dt-text-right { + text-align: end !important; +} diff --git a/data/web/css/build/013-mailcow.css b/data/web/css/build/014-mailcow.css similarity index 100% rename from data/web/css/build/013-mailcow.css rename to data/web/css/build/014-mailcow.css diff --git a/data/web/css/build/014-responsive.css b/data/web/css/build/015-responsive.css similarity index 96% rename from data/web/css/build/014-responsive.css rename to data/web/css/build/015-responsive.css index a9877271..a626a384 100644 --- a/data/web/css/build/014-responsive.css +++ b/data/web/css/build/015-responsive.css @@ -199,6 +199,13 @@ display: none !important; } + div.dataTables_wrapper div.dataTables_length { + text-align: left; + } + + .senders-mw220 { + max-width: 100% !important; + } } @media (max-width: 350px) { diff --git a/data/web/css/site/quarantine.css b/data/web/css/site/quarantine.css index 98a74d66..0455b7c1 100644 --- a/data/web/css/site/quarantine.css +++ b/data/web/css/site/quarantine.css @@ -1,102 +1,104 @@ -.pagination a { - text-decoration: none !important; -} - -.panel.panel-default { - overflow: visible !important; -} - -.table-responsive { - overflow: visible !important; -} - -.table-responsive { - overflow-x: scroll !important; -} - -.footer-add-item { - display: block; - text-align: center; - font-style: italic; - padding: 10px; - background: #F5F5F5; -} - -@media (min-width: 992px) { - .container { - width: 100%; - } -} -@media (min-width: 1920px) { - .container { - width: 80%; - } -} - -.mass-actions-quarantine { - user-select: none; -} - -.inputMissingAttr { - border-color: #FF4136; -} - -.modal#qidDetailModal p { - word-break: break-all; -} - -span#qid_detail_score { - font-weight: 700; - margin-left: 5px; -} - -span.rspamd-symbol { - display: inline-block; - margin: 2px 6px 2px 0; - border-radius: 4px; - padding: 0 7px; -} - -span.rspamd-symbol.positive { - background: #4CAF50; - border: 1px solid #4CAF50; - color: white; -} - -span.rspamd-symbol.negative { - background: #ff4136; - border: 1px solid #ff4136; - color: white; -} - -span.rspamd-symbol.neutral { - background: #f5f5f5; - color: #333; - border: 1px solid #ccc; -} - -span.rspamd-symbol span.score { - font-weight: 700; -} - -span.mail-address-item { - background-color: #f5f5f5; - border-radius: 4px; - border: 1px solid #ccc; - padding: 2px 7px; - display: inline-block; - margin: 2px 6px 2px 0; -} - -table tbody tr { - cursor: pointer; -} - -table tbody tr td input[type="checkbox"] { - cursor: pointer; -} -.label-rspamd-action { - font-size:110%; - margin:20px; -} - +.pagination a { + text-decoration: none !important; +} + +.panel.panel-default { + overflow: visible !important; +} + +.table-responsive { + overflow: visible !important; +} + +.table-responsive { + overflow-x: scroll !important; +} + +.footer-add-item { + display: block; + text-align: center; + font-style: italic; + padding: 10px; + background: #F5F5F5; +} + +@media (min-width: 992px) { + .container { + width: 100%; + } +} +@media (min-width: 1920px) { + .container { + width: 80%; + } +} + +.mass-actions-quarantine { + user-select: none; +} + +.inputMissingAttr { + border-color: #FF4136; +} + +.modal#qidDetailModal p { + word-break: break-all; +} + +span#qid_detail_score { + font-weight: 700; + margin-left: 5px; +} + +span.rspamd-symbol { + display: inline-block; + margin: 2px 6px 2px 0; + border-radius: 4px; + padding: 0 7px; +} + +span.rspamd-symbol.positive { + background: #4CAF50; + border: 1px solid #4CAF50; + color: white; +} + +span.rspamd-symbol.negative { + background: #ff4136; + border: 1px solid #ff4136; + color: white; +} + +span.rspamd-symbol.neutral { + background: #f5f5f5; + color: #333; + border: 1px solid #ccc; +} + +span.rspamd-symbol span.score { + font-weight: 700; +} + +span.mail-address-item { + background-color: #f5f5f5; + border-radius: 4px; + border: 1px solid #ccc; + padding: 2px 7px; + display: inline-block; + margin: 2px 6px 2px 0; +} + +table tbody tr { + cursor: pointer; +} + +table tbody tr td input[type="checkbox"] { + cursor: pointer; +} +.label-rspamd-action { + font-size:110%; + margin:20px; +} +.senders-mw220 { + max-width: 220px; +} diff --git a/data/web/css/themes/lumen-bootstrap.css b/data/web/css/themes/lumen-bootstrap.css index a7582237..bcf62683 100644 --- a/data/web/css/themes/lumen-bootstrap.css +++ b/data/web/css/themes/lumen-bootstrap.css @@ -11,7 +11,86 @@ * Copyright 2011-2021 Twitter, Inc. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ -@import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,300;0,400;0,700;1,400&display=swap"); + +/* source-sans-pro-300 - latin */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 300; + src: url('/fonts/source-sans-pro-v21-latin-300.eot'); /* IE9 Compat Modes */ + src: local(''), + url('/fonts/source-sans-pro-v21-latin-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('/fonts/source-sans-pro-v21-latin-300.woff2') format('woff2'), /* Super Modern Browsers */ + url('/fonts/source-sans-pro-v21-latin-300.woff') format('woff'), /* Modern Browsers */ + url('/fonts/source-sans-pro-v21-latin-300.ttf') format('truetype'), /* Safari, Android, iOS */ + url('/fonts/source-sans-pro-v21-latin-300.svg#SourceSansPro') format('svg'); /* Legacy iOS */ +} +/* source-sans-pro-300italic - latin */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 300; + src: url('/fonts/source-sans-pro-v21-latin-300italic.eot'); /* IE9 Compat Modes */ + src: local(''), + url('/fonts/source-sans-pro-v21-latin-300italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('/fonts/source-sans-pro-v21-latin-300italic.woff2') format('woff2'), /* Super Modern Browsers */ + url('/fonts/source-sans-pro-v21-latin-300italic.woff') format('woff'), /* Modern Browsers */ + url('/fonts/source-sans-pro-v21-latin-300italic.ttf') format('truetype'), /* Safari, Android, iOS */ + url('/fonts/source-sans-pro-v21-latin-300italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */ +} +/* source-sans-pro-regular - latin */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 400; + src: url('/fonts/source-sans-pro-v21-latin-regular.eot'); /* IE9 Compat Modes */ + src: local(''), + url('/fonts/source-sans-pro-v21-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('/fonts/source-sans-pro-v21-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ + url('/fonts/source-sans-pro-v21-latin-regular.woff') format('woff'), /* Modern Browsers */ + url('/fonts/source-sans-pro-v21-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */ + url('/fonts/source-sans-pro-v21-latin-regular.svg#SourceSansPro') format('svg'); /* Legacy iOS */ +} +/* source-sans-pro-italic - latin */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 400; + src: url('/fonts/source-sans-pro-v21-latin-italic.eot'); /* IE9 Compat Modes */ + src: local(''), + url('/fonts/source-sans-pro-v21-latin-italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('/fonts/source-sans-pro-v21-latin-italic.woff2') format('woff2'), /* Super Modern Browsers */ + url('/fonts/source-sans-pro-v21-latin-italic.woff') format('woff'), /* Modern Browsers */ + url('/fonts/source-sans-pro-v21-latin-italic.ttf') format('truetype'), /* Safari, Android, iOS */ + url('/fonts/source-sans-pro-v21-latin-italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */ +} +/* source-sans-pro-700 - latin */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: normal; + font-weight: 700; + src: url('/fonts/source-sans-pro-v21-latin-700.eot'); /* IE9 Compat Modes */ + src: local(''), + url('/fonts/source-sans-pro-v21-latin-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('/fonts/source-sans-pro-v21-latin-700.woff2') format('woff2'), /* Super Modern Browsers */ + url('/fonts/source-sans-pro-v21-latin-700.woff') format('woff'), /* Modern Browsers */ + url('/fonts/source-sans-pro-v21-latin-700.ttf') format('truetype'), /* Safari, Android, iOS */ + url('/fonts/source-sans-pro-v21-latin-700.svg#SourceSansPro') format('svg'); /* Legacy iOS */ +} +/* source-sans-pro-700italic - latin */ +@font-face { + font-family: 'Source Sans Pro'; + font-style: italic; + font-weight: 700; + src: url('/fonts/source-sans-pro-v21-latin-700italic.eot'); /* IE9 Compat Modes */ + src: local(''), + url('/fonts/source-sans-pro-v21-latin-700italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ + url('/fonts/source-sans-pro-v21-latin-700italic.woff2') format('woff2'), /* Super Modern Browsers */ + url('/fonts/source-sans-pro-v21-latin-700italic.woff') format('woff'), /* Modern Browsers */ + url('/fonts/source-sans-pro-v21-latin-700italic.ttf') format('truetype'), /* Safari, Android, iOS */ + url('/fonts/source-sans-pro-v21-latin-700italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */ +} + :root { --bs-blue: #158cba; --bs-indigo: #6610f2; diff --git a/data/web/css/themes/mailcow-darkmode.css b/data/web/css/themes/mailcow-darkmode.css index e1824420..6e0db0e9 100644 --- a/data/web/css/themes/mailcow-darkmode.css +++ b/data/web/css/themes/mailcow-darkmode.css @@ -358,3 +358,11 @@ table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty { background: #333; } +span.mail-address-item { + background-color: #333; + border-radius: 4px; + border: 1px solid #555; + padding: 2px 7px; + display: inline-block; + margin: 2px 6px 2px 0; +} diff --git a/data/web/debug.php b/data/web/debug.php index 5618ec00..52052f68 100644 --- a/data/web/debug.php +++ b/data/web/debug.php @@ -11,6 +11,11 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $solr_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_SOLR"])) ? false : solr_status(); $clamd_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_CLAMD"])) ? false : true; + +if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) { + $_SESSION['gal'] = json_decode($license_cache, true); +} + $js_minifier->add('/web/js/site/debug.js'); // vmail df @@ -54,11 +59,13 @@ $template_data = [ 'vmail_df' => $vmail_df, 'hostname' => $hostname, 'timezone' => $timezone, + 'gal' => @$_SESSION['gal'], 'license_guid' => license('guid'), 'solr_status' => $solr_status, 'solr_uptime' => round($solr_status['status']['dovecot-fts']['uptime'] / 1000 / 60 / 60), 'clamd_status' => $clamd_status, 'containers' => $containers, + 'ip_check' => customize('get', 'ip_check'), 'lang_admin' => json_encode($lang['admin']), 'lang_debug' => json_encode($lang['debug']), 'lang_datatables' => json_encode($lang['datatables']), diff --git a/data/web/fonts/source-sans-pro-v21-latin-300.woff b/data/web/fonts/source-sans-pro-v21-latin-300.woff new file mode 100644 index 00000000..e966494d Binary files /dev/null and b/data/web/fonts/source-sans-pro-v21-latin-300.woff differ diff --git a/data/web/fonts/source-sans-pro-v21-latin-300.woff2 b/data/web/fonts/source-sans-pro-v21-latin-300.woff2 new file mode 100644 index 00000000..fed32a93 Binary files /dev/null and b/data/web/fonts/source-sans-pro-v21-latin-300.woff2 differ diff --git a/data/web/fonts/source-sans-pro-v21-latin-300italic.woff b/data/web/fonts/source-sans-pro-v21-latin-300italic.woff new file mode 100644 index 00000000..c0dca072 Binary files /dev/null and b/data/web/fonts/source-sans-pro-v21-latin-300italic.woff differ diff --git a/data/web/fonts/source-sans-pro-v21-latin-300italic.woff2 b/data/web/fonts/source-sans-pro-v21-latin-300italic.woff2 new file mode 100644 index 00000000..a8e1fff2 Binary files /dev/null and b/data/web/fonts/source-sans-pro-v21-latin-300italic.woff2 differ diff --git a/data/web/fonts/source-sans-pro-v21-latin-700.woff b/data/web/fonts/source-sans-pro-v21-latin-700.woff new file mode 100644 index 00000000..a6786d1f Binary files /dev/null and b/data/web/fonts/source-sans-pro-v21-latin-700.woff differ diff --git a/data/web/fonts/source-sans-pro-v21-latin-700.woff2 b/data/web/fonts/source-sans-pro-v21-latin-700.woff2 new file mode 100644 index 00000000..cd6bfd0f Binary files /dev/null and b/data/web/fonts/source-sans-pro-v21-latin-700.woff2 differ diff --git a/data/web/fonts/source-sans-pro-v21-latin-700italic.woff b/data/web/fonts/source-sans-pro-v21-latin-700italic.woff new file mode 100644 index 00000000..729bdee9 Binary files /dev/null and b/data/web/fonts/source-sans-pro-v21-latin-700italic.woff differ diff --git a/data/web/fonts/source-sans-pro-v21-latin-700italic.woff2 b/data/web/fonts/source-sans-pro-v21-latin-700italic.woff2 new file mode 100644 index 00000000..b413356f Binary files /dev/null and b/data/web/fonts/source-sans-pro-v21-latin-700italic.woff2 differ diff --git a/data/web/fonts/source-sans-pro-v21-latin-italic.woff b/data/web/fonts/source-sans-pro-v21-latin-italic.woff new file mode 100644 index 00000000..f927419c Binary files /dev/null and b/data/web/fonts/source-sans-pro-v21-latin-italic.woff differ diff --git a/data/web/fonts/source-sans-pro-v21-latin-italic.woff2 b/data/web/fonts/source-sans-pro-v21-latin-italic.woff2 new file mode 100644 index 00000000..9448cd52 Binary files /dev/null and b/data/web/fonts/source-sans-pro-v21-latin-italic.woff2 differ diff --git a/data/web/fonts/source-sans-pro-v21-latin-regular.woff b/data/web/fonts/source-sans-pro-v21-latin-regular.woff new file mode 100644 index 00000000..db90a83e Binary files /dev/null and b/data/web/fonts/source-sans-pro-v21-latin-regular.woff differ diff --git a/data/web/fonts/source-sans-pro-v21-latin-regular.woff2 b/data/web/fonts/source-sans-pro-v21-latin-regular.woff2 new file mode 100644 index 00000000..e49928e8 Binary files /dev/null and b/data/web/fonts/source-sans-pro-v21-latin-regular.woff2 differ diff --git a/data/web/inc/functions.customize.inc.php b/data/web/inc/functions.customize.inc.php index 16c5c036..6025d97d 100644 --- a/data/web/inc/functions.customize.inc.php +++ b/data/web/inc/functions.customize.inc.php @@ -160,6 +160,25 @@ function customize($_action, $_item, $_data = null) { 'msg' => 'ui_texts' ); break; + case 'ip_check': + $ip_check = ($_data['ip_check_opt_in'] == "1") ? 1 : 0; + try { + $redis->set('IP_CHECK', $ip_check); + } + catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => array('redis_error', $e) + ); + return false; + } + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => 'ip_check_opt_in_modified' + ); + break; } break; case 'delete': @@ -276,6 +295,20 @@ function customize($_action, $_item, $_data = null) { return false; } break; + case 'ip_check': + try { + $ip_check = ($ip_check = $redis->get('IP_CHECK')) ? $ip_check : 0; + return $ip_check; + } + catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => array('redis_error', $e) + ); + return false; + } + break; } break; } diff --git a/data/web/inc/functions.domain_admin.inc.php b/data/web/inc/functions.domain_admin.inc.php index 804c0f83..bb88ea34 100644 --- a/data/web/inc/functions.domain_admin.inc.php +++ b/data/web/inc/functions.domain_admin.inc.php @@ -1,407 +1,468 @@ - 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - if (empty($domains)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'domain_invalid' - ); - return false; - } - if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username) || $username == 'API') { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('username_invalid', $username) - ); - return false; - } - - $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - - $stmt = $pdo->prepare("SELECT `username` FROM `admin` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - - $stmt = $pdo->prepare("SELECT `username` FROM `domain_admins` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - - foreach ($num_results as $num_results_each) { - if ($num_results_each != 0) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('object_exists', htmlspecialchars($username)) - ); - return false; - } - } - if (password_check($password, $password2) !== true) { - continue; - } - $password_hashed = hash_password($password); - $valid_domains = 0; - foreach ($domains as $domain) { - if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('domain_invalid', htmlspecialchars($domain)) - ); - continue; - } - $valid_domains++; - $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) - VALUES (:username, :domain, :created, :active)"); - $stmt->execute(array( - ':username' => $username, - ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':active' => $active - )); - } - if ($valid_domains != 0) { - $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`) - VALUES (:username, :password_hashed, '0', :active)"); - $stmt->execute(array( - ':username' => $username, - ':password_hashed' => $password_hashed, - ':active' => $active - )); - } - $stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)"); - $stmt->execute(array( - ':username' => $username - )); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('domain_admin_added', htmlspecialchars($username)) - ); - break; - case 'edit': - if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - // Administrator - if ($_SESSION['mailcow_cc_role'] == "admin") { - if (!is_array($_data['username'])) { - $usernames = array(); - $usernames[] = $_data['username']; - } - else { - $usernames = $_data['username']; - } - foreach ($usernames as $username) { - $is_now = domain_admin('details', $username); - $domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null; - if (!empty($is_now)) { - $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; - $domains = (!empty($domains)) ? $domains : $is_now['selected_domains']; - $username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username']; - } - else { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'access_denied' - ); - continue; - } - $password = $_data['password']; - $password2 = $_data['password2']; - if (!empty($domains)) { - foreach ($domains as $domain) { - if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('domain_invalid', htmlspecialchars($domain)) - ); - continue 2; - } - } - } - if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('username_invalid', $username_new) - ); - continue; - } - if ($username_new != $username) { - if (!empty(domain_admin('details', $username_new)['username'])) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('username_invalid', $username_new) - ); - continue; - } - } - $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $stmt = $pdo->prepare("UPDATE `da_acl` SET `username` = :username_new WHERE `username` = :username"); - $stmt->execute(array( - ':username_new' => $username_new, - ':username' => $username - )); - if (!empty($domains)) { - foreach ($domains as $domain) { - $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) - VALUES (:username_new, :domain, :created, :active)"); - $stmt->execute(array( - ':username_new' => $username_new, - ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':active' => $active - )); - } - } - if (!empty($password)) { - if (password_check($password, $password2) !== true) { - return false; - } - $password_hashed = hash_password($password); - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); - $stmt->execute(array( - ':password_hashed' => $password_hashed, - ':username_new' => $username_new, - ':username' => $username, - ':active' => $active - )); - if (isset($_data['disable_tfa'])) { - $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - } - else { - $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); - $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); - } - } - else { - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username"); - $stmt->execute(array( - ':username_new' => $username_new, - ':username' => $username, - ':active' => $active - )); - if (isset($_data['disable_tfa'])) { - $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - } - else { - $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); - $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); - } - } - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('domain_admin_modified', htmlspecialchars($username)) - ); - } - return true; - } - // Domain administrator - // Can only edit itself - elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") { - $username = $_SESSION['mailcow_cc_username']; - $password_old = $_data['user_old_pass']; - $password_new = $_data['user_new_pass']; - $password_new2 = $_data['user_new_pass2']; - - $stmt = $pdo->prepare("SELECT `password` FROM `admin` - WHERE `username` = :user"); - $stmt->execute(array(':user' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (!verify_hash($row['password'], $password_old)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - if (password_check($password_new, $password_new2) !== true) { - return false; - } - $password_hashed = hash_password($password_new); - $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username"); - $stmt->execute(array( - ':password_hashed' => $password_hashed, - ':username' => $username - )); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('domain_admin_modified', htmlspecialchars($username)) - ); - } - break; - case 'delete': - if ($_SESSION['mailcow_cc_role'] != "admin") { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - $usernames = (array)$_data['username']; - foreach ($usernames as $username) { - if (empty(domain_admin('details', $username))) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('username_invalid', $username) - ); - continue; - } - $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $stmt = $pdo->prepare("DELETE FROM `da_acl` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('domain_admin_removed', htmlspecialchars($username)) - ); - } - break; - case 'get': - $domainadmins = array(); - if ($_SESSION['mailcow_cc_role'] != "admin") { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - $stmt = $pdo->query("SELECT DISTINCT - `username` - FROM `domain_admins` - WHERE `username` IN ( - SELECT `username` FROM `admin` - WHERE `superadmin`!='1' - )"); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($row = array_shift($rows)) { - $domainadmins[] = $row['username']; - } - return $domainadmins; - break; - case 'details': - $domainadmindata = array(); - if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) { - return false; - } - elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) { - return false; - } - if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) { - return false; - } - $stmt = $pdo->prepare("SELECT - `tfa`.`active` AS `tfa_active`, - `domain_admins`.`username`, - `domain_admins`.`created`, - `domain_admins`.`active` AS `active` - FROM `domain_admins` - LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username` - WHERE `domain_admins`.`username`= :domain_admin"); - $stmt->execute(array( - ':domain_admin' => $_data - )); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (empty($row)) { - return false; - } - $domainadmindata['username'] = $row['username']; - $domainadmindata['tfa_active'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active']; - $domainadmindata['tfa_active_int'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active']; - $domainadmindata['active'] = $row['active']; - $domainadmindata['active_int'] = $row['active']; - $domainadmindata['created'] = $row['created']; - // GET SELECTED - $stmt = $pdo->prepare("SELECT `domain` FROM `domain` - WHERE `domain` IN ( - SELECT `domain` FROM `domain_admins` - WHERE `username`= :domain_admin)"); - $stmt->execute(array(':domain_admin' => $_data)); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $domainadmindata['selected_domains'][] = $row['domain']; - } - // GET UNSELECTED - $stmt = $pdo->prepare("SELECT `domain` FROM `domain` - WHERE `domain` NOT IN ( - SELECT `domain` FROM `domain_admins` - WHERE `username`= :domain_admin)"); - $stmt->execute(array(':domain_admin' => $_data)); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $domainadmindata['unselected_domains'][] = $row['domain']; - } - if (!isset($domainadmindata['unselected_domains'])) { - $domainadmindata['unselected_domains'] = ""; - } - - return $domainadmindata; - break; - } -} + 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + if (empty($domains)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'domain_invalid' + ); + return false; + } + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username) || $username == 'API') { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('username_invalid', $username) + ); + return false; + } + + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + + $stmt = $pdo->prepare("SELECT `username` FROM `admin` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + + $stmt = $pdo->prepare("SELECT `username` FROM `domain_admins` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + + foreach ($num_results as $num_results_each) { + if ($num_results_each != 0) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('object_exists', htmlspecialchars($username)) + ); + return false; + } + } + if (password_check($password, $password2) !== true) { + continue; + } + $password_hashed = hash_password($password); + $valid_domains = 0; + foreach ($domains as $domain) { + if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('domain_invalid', htmlspecialchars($domain)) + ); + continue; + } + $valid_domains++; + $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) + VALUES (:username, :domain, :created, :active)"); + $stmt->execute(array( + ':username' => $username, + ':domain' => $domain, + ':created' => date('Y-m-d H:i:s'), + ':active' => $active + )); + } + if ($valid_domains != 0) { + $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`) + VALUES (:username, :password_hashed, '0', :active)"); + $stmt->execute(array( + ':username' => $username, + ':password_hashed' => $password_hashed, + ':active' => $active + )); + } + $stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)"); + $stmt->execute(array( + ':username' => $username + )); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('domain_admin_added', htmlspecialchars($username)) + ); + break; + case 'edit': + if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + // Administrator + if ($_SESSION['mailcow_cc_role'] == "admin") { + if (!is_array($_data['username'])) { + $usernames = array(); + $usernames[] = $_data['username']; + } + else { + $usernames = $_data['username']; + } + foreach ($usernames as $username) { + $is_now = domain_admin('details', $username); + $domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null; + if (!empty($is_now)) { + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; + $domains = (!empty($domains)) ? $domains : $is_now['selected_domains']; + $username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username']; + } + else { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'access_denied' + ); + continue; + } + $password = $_data['password']; + $password2 = $_data['password2']; + if (!empty($domains)) { + foreach ($domains as $domain) { + if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('domain_invalid', htmlspecialchars($domain)) + ); + continue 2; + } + } + } + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('username_invalid', $username_new) + ); + continue; + } + if ($username_new != $username) { + if (!empty(domain_admin('details', $username_new)['username'])) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('username_invalid', $username_new) + ); + continue; + } + } + $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $stmt = $pdo->prepare("UPDATE `da_acl` SET `username` = :username_new WHERE `username` = :username"); + $stmt->execute(array( + ':username_new' => $username_new, + ':username' => $username + )); + if (!empty($domains)) { + foreach ($domains as $domain) { + $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) + VALUES (:username_new, :domain, :created, :active)"); + $stmt->execute(array( + ':username_new' => $username_new, + ':domain' => $domain, + ':created' => date('Y-m-d H:i:s'), + ':active' => $active + )); + } + } + if (!empty($password)) { + if (password_check($password, $password2) !== true) { + return false; + } + $password_hashed = hash_password($password); + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); + $stmt->execute(array( + ':password_hashed' => $password_hashed, + ':username_new' => $username_new, + ':username' => $username, + ':active' => $active + )); + if (isset($_data['disable_tfa'])) { + $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + } + else { + $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); + $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); + } + } + else { + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username"); + $stmt->execute(array( + ':username_new' => $username_new, + ':username' => $username, + ':active' => $active + )); + if (isset($_data['disable_tfa'])) { + $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + } + else { + $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); + $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); + } + } + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('domain_admin_modified', htmlspecialchars($username)) + ); + } + return true; + } + // Domain administrator + // Can only edit itself + elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") { + $username = $_SESSION['mailcow_cc_username']; + $password_old = $_data['user_old_pass']; + $password_new = $_data['user_new_pass']; + $password_new2 = $_data['user_new_pass2']; + + $stmt = $pdo->prepare("SELECT `password` FROM `admin` + WHERE `username` = :user"); + $stmt->execute(array(':user' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (!verify_hash($row['password'], $password_old)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + if (password_check($password_new, $password_new2) !== true) { + return false; + } + $password_hashed = hash_password($password_new); + $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username"); + $stmt->execute(array( + ':password_hashed' => $password_hashed, + ':username' => $username + )); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('domain_admin_modified', htmlspecialchars($username)) + ); + } + break; + case 'delete': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + $usernames = (array)$_data['username']; + foreach ($usernames as $username) { + if (empty(domain_admin('details', $username))) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('username_invalid', $username) + ); + continue; + } + $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $stmt = $pdo->prepare("DELETE FROM `da_acl` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('domain_admin_removed', htmlspecialchars($username)) + ); + } + break; + case 'get': + $domainadmins = array(); + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + $stmt = $pdo->query("SELECT DISTINCT + `username` + FROM `domain_admins` + WHERE `username` IN ( + SELECT `username` FROM `admin` + WHERE `superadmin`!='1' + )"); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($rows)) { + $domainadmins[] = $row['username']; + } + return $domainadmins; + break; + case 'details': + $domainadmindata = array(); + if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) { + return false; + } + elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) { + return false; + } + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) { + return false; + } + $stmt = $pdo->prepare("SELECT + `tfa`.`active` AS `tfa_active`, + `domain_admins`.`username`, + `domain_admins`.`created`, + `domain_admins`.`active` AS `active` + FROM `domain_admins` + LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username` + WHERE `domain_admins`.`username`= :domain_admin"); + $stmt->execute(array( + ':domain_admin' => $_data + )); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (empty($row)) { + return false; + } + $domainadmindata['username'] = $row['username']; + $domainadmindata['tfa_active'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active']; + $domainadmindata['tfa_active_int'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active']; + $domainadmindata['active'] = $row['active']; + $domainadmindata['active_int'] = $row['active']; + $domainadmindata['created'] = $row['created']; + // GET SELECTED + $stmt = $pdo->prepare("SELECT `domain` FROM `domain` + WHERE `domain` IN ( + SELECT `domain` FROM `domain_admins` + WHERE `username`= :domain_admin)"); + $stmt->execute(array(':domain_admin' => $_data)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $domainadmindata['selected_domains'][] = $row['domain']; + } + // GET UNSELECTED + $stmt = $pdo->prepare("SELECT `domain` FROM `domain` + WHERE `domain` NOT IN ( + SELECT `domain` FROM `domain_admins` + WHERE `username`= :domain_admin)"); + $stmt->execute(array(':domain_admin' => $_data)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $domainadmindata['unselected_domains'][] = $row['domain']; + } + if (!isset($domainadmindata['unselected_domains'])) { + $domainadmindata['unselected_domains'] = ""; + } + + return $domainadmindata; + break; + } +} +function domain_admin_sso($_action, $_data) { + global $pdo; + + switch ($_action) { + case 'check': + $token = $_data; + + $stmt = $pdo->prepare("SELECT `t1`.`username` FROM `da_sso` AS `t1` JOIN `admin` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL '30' SECOND) AND `t2`.`active` = 1 AND `t2`.`superadmin` = 0;"); + $stmt->execute(array( + ':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token) + )); + $return = $stmt->fetch(PDO::FETCH_ASSOC); + return empty($return['username']) ? false : $return['username']; + case 'issue': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data), + 'msg' => 'access_denied' + ); + return false; + } + + $username = $_data['username']; + + $stmt = $pdo->prepare("SELECT `username` FROM `domain_admins` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + + if ($num_results < 1) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data), + 'msg' => array('object_doesnt_exist', htmlspecialchars($username)) + ); + return false; + } + + $token = implode('-', array( + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))) + )); + + $stmt = $pdo->prepare("INSERT INTO `da_sso` (`username`, `token`) + VALUES (:username, :token)"); + $stmt->execute(array( + ':username' => $username, + ':token' => $token + )); + + // perform cleanup + $pdo->query("DELETE FROM `da_sso` WHERE created < DATE_SUB(NOW(), INTERVAL '30' SECOND);"); + + return ['token' => $token]; + break; + } +} diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 3bab56bb..de1855fa 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1739,7 +1739,7 @@ function verify_tfa_login($username, $_data) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $username, '*'), - 'msg' => array('webauthn_verification_failed', 'authenticator not found') + 'msg' => array('webauthn_authenticator_failed') ); return false; } @@ -1748,11 +1748,20 @@ function verify_tfa_login($username, $_data) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $username, '*'), - 'msg' => array('webauthn_verification_failed', 'publicKey not found') + 'msg' => array('webauthn_publickey_failed') ); return false; } + if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){ + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $username, '*'), + 'msg' => array('webauthn_username_failed') + ); + return false; + } + try { $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']); } @@ -1784,21 +1793,12 @@ function verify_tfa_login($username, $_data) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $username, '*'), - 'msg' => array('webauthn_verification_failed', 'could not determine user role') + 'msg' => array('webauthn_role_failed') ); return false; } } - if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){ - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $username, '*'), - 'msg' => array('webauthn_verification_failed', 'user who requests does not match with sql entry') - ); - return false; - } - $_SESSION["mailcow_cc_username"] = $process_webauthn['username']; $_SESSION['tfa_id'] = $process_webauthn['id']; $_SESSION['authReq'] = null; diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 55c8d6bc..4529ee7b 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -1420,11 +1420,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // check attributes $attr = array(); $attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array(); - $attr['max_num_aliases_for_domain'] = (isset($_data['max_num_aliases_for_domain'])) ? intval($_data['max_num_aliases_for_domain']) : 0; - $attr['max_num_mboxes_for_domain'] = (isset($_data['max_num_mboxes_for_domain'])) ? intval($_data['max_num_mboxes_for_domain']) : 0; - $attr['def_quota_for_mbox'] = (isset($_data['def_quota_for_mbox'])) ? intval($_data['def_quota_for_mbox']) * 1048576 : 0; - $attr['max_quota_for_mbox'] = (isset($_data['max_quota_for_mbox'])) ? intval($_data['max_quota_for_mbox']) * 1048576 : 0; - $attr['max_quota_for_domain'] = (isset($_data['max_quota_for_domain'])) ? intval($_data['max_quota_for_domain']) * 1048576 : 0; + $attr['max_num_aliases_for_domain'] = (!empty($_data['max_num_aliases_for_domain'])) ? intval($_data['max_num_aliases_for_domain']) : 400; + $attr['max_num_mboxes_for_domain'] = (!empty($_data['max_num_mboxes_for_domain'])) ? intval($_data['max_num_mboxes_for_domain']) : 10; + $attr['def_quota_for_mbox'] = (!empty($_data['def_quota_for_mbox'])) ? intval($_data['def_quota_for_mbox']) * 1048576 : 3072 * 1048576; + $attr['max_quota_for_mbox'] = (!empty($_data['max_quota_for_mbox'])) ? intval($_data['max_quota_for_mbox']) * 1048576 : 10240 * 1048576; + $attr['max_quota_for_domain'] = (!empty($_data['max_quota_for_domain'])) ? intval($_data['max_quota_for_domain']) * 1048576 : 10240 * 1048576; $attr['rl_frame'] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s"; $attr['rl_value'] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : ""; $attr['active'] = isset($_data['active']) ? intval($_data['active']) : 1; @@ -1435,7 +1435,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attr['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : "dkim"; $attr['key_size'] = isset($_data['key_size']) ? intval($_data['key_size']) : 2048; - // save template $stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`) VALUES (:type, :template, :attributes)"); @@ -2880,67 +2879,68 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' + 'msg' => 'extended_sender_acl_denied' ); - return false; } - $extra_acls = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['extended_sender_acl'])); - foreach ($extra_acls as $i => &$extra_acl) { - if (empty($extra_acl)) { - continue; - } - if (substr($extra_acl, 0, 1) === "@") { - $extra_acl = ltrim($extra_acl, '@'); - } - if (!filter_var($extra_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name($extra_acl)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => array('extra_acl_invalid', htmlspecialchars($extra_acl)) - ); - unset($extra_acls[$i]); - continue; - } - $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains')); - if (filter_var($extra_acl, FILTER_VALIDATE_EMAIL)) { - $extra_acl_domain = idn_to_ascii(substr(strstr($extra_acl, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46); - if (in_array($extra_acl_domain, $domains)) { + else { + $extra_acls = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['extended_sender_acl'])); + foreach ($extra_acls as $i => &$extra_acl) { + if (empty($extra_acl)) { + continue; + } + if (substr($extra_acl, 0, 1) === "@") { + $extra_acl = ltrim($extra_acl, '@'); + } + if (!filter_var($extra_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name($extra_acl)) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => array('extra_acl_invalid_domain', $extra_acl_domain) + 'msg' => array('extra_acl_invalid', htmlspecialchars($extra_acl)) ); unset($extra_acls[$i]); continue; } - } - else { - if (in_array($extra_acl, $domains)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => array('extra_acl_invalid_domain', $extra_acl_domain) - ); - unset($extra_acls[$i]); - continue; + $domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains')); + if (filter_var($extra_acl, FILTER_VALIDATE_EMAIL)) { + $extra_acl_domain = idn_to_ascii(substr(strstr($extra_acl, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46); + if (in_array($extra_acl_domain, $domains)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('extra_acl_invalid_domain', $extra_acl_domain) + ); + unset($extra_acls[$i]); + continue; + } + } + else { + if (in_array($extra_acl, $domains)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('extra_acl_invalid_domain', $extra_acl_domain) + ); + unset($extra_acls[$i]); + continue; + } + $extra_acl = '@' . $extra_acl; } - $extra_acl = '@' . $extra_acl; } - } - $extra_acls = array_filter($extra_acls); - $extra_acls = array_values($extra_acls); - $extra_acls = array_unique($extra_acls); - $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `logged_in_as` = :username"); - $stmt->execute(array( - ':username' => $username - )); - foreach ($extra_acls as $sender_acl_external) { - $stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`, `external`) - VALUES (:sender_acl, :username, 1)"); + $extra_acls = array_filter($extra_acls); + $extra_acls = array_values($extra_acls); + $extra_acls = array_unique($extra_acls); + $stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `logged_in_as` = :username"); $stmt->execute(array( - ':sender_acl' => $sender_acl_external, ':username' => $username )); + foreach ($extra_acls as $sender_acl_external) { + $stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`, `external`) + VALUES (:sender_acl, :username, 1)"); + $stmt->execute(array( + ':sender_acl' => $sender_acl_external, + ':username' => $username + )); + } } } if (isset($_data['sender_acl'])) { @@ -4756,15 +4756,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ":id" => $id, ":type" => "domain", ":template" => "Default" - )); - } + )); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'template_removed' - ); - return true; + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('template_removed', htmlspecialchars($id)) + ); + return true; + } break; case 'alias': if (!is_array($_data['id'])) { @@ -5171,15 +5171,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $tags = $_data['tags']; if (!is_array($tags)) $tags = array(); - - if ($_SESSION['mailcow_cc_role'] != "admin") { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'access_denied' - ); - return false; - } $wasModified = false; foreach ($domains as $domain) { @@ -5191,7 +5182,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } - + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + return false; + } + foreach($tags as $tag){ // delete tag $wasModified = true; @@ -5265,7 +5264,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } break; } - if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource'))) { + if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource')) && getenv('SKIP_SOGO') != "y") { update_sogo_static_view(); } } diff --git a/data/web/inc/functions.pushover.inc.php b/data/web/inc/functions.pushover.inc.php index 74e8bb1c..5393c0d5 100644 --- a/data/web/inc/functions.pushover.inc.php +++ b/data/web/inc/functions.pushover.inc.php @@ -51,6 +51,7 @@ function pushover($_action, $_data = null) { $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; $evaluate_x_prio = (isset($_data['evaluate_x_prio'])) ? intval($_data['evaluate_x_prio']) : $is_now['evaluate_x_prio']; $only_x_prio = (isset($_data['only_x_prio'])) ? intval($_data['only_x_prio']) : $is_now['only_x_prio']; + $sound = (isset($_data['sound'])) ? $_data['sound'] : $is_now['sound']; } else { $_SESSION['return'][] = array( @@ -101,7 +102,8 @@ function pushover($_action, $_data = null) { $po_attributes = json_encode( array( 'evaluate_x_prio' => strval(intval($evaluate_x_prio)), - 'only_x_prio' => strval(intval($only_x_prio)) + 'only_x_prio' => strval(intval($only_x_prio)), + 'sound' => strval($sound) ) ); $stmt = $pdo->prepare("REPLACE INTO `pushover` (`username`, `key`, `attributes`, `senders_regex`, `senders`, `token`, `title`, `text`, `active`) diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index d43c9060..e286ab55 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -1,1464 +1,1478 @@ -query("SHOW TABLES LIKE 'versions'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'db_schema'"); - if ($stmt->fetch(PDO::FETCH_ASSOC)['version'] == $db_version) { - return true; - } - if (!preg_match('/y|yes/i', getenv('MASTER'))) { - $_SESSION['return'][] = array( - 'type' => 'warning', - 'log' => array(__FUNCTION__), - 'msg' => 'Database not initialized: not running db_init on slave.' - ); - return true; - } - } - - $views = array( - "grouped_mail_aliases" => "CREATE VIEW grouped_mail_aliases (username, aliases) AS - SELECT goto, IFNULL(GROUP_CONCAT(address ORDER BY address SEPARATOR ' '), '') AS address FROM alias - WHERE address!=goto - AND active = '1' - AND sogo_visible = '1' - AND address NOT LIKE '@%' - GROUP BY goto;", - // START - // Unused at the moment - we cannot allow to show a foreign mailbox as sender address in SOGo, as SOGo does not like this - // We need to create delegation in SOGo AND set a sender_acl in mailcow to allow to send as user X - "grouped_sender_acl" => "CREATE VIEW grouped_sender_acl (username, send_as_acl) AS - SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl - WHERE send_as NOT LIKE '@%' - GROUP BY logged_in_as;", - // END - "grouped_sender_acl_external" => "CREATE VIEW grouped_sender_acl_external (username, send_as_acl) AS - SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl - WHERE send_as NOT LIKE '@%' AND external = '1' - GROUP BY logged_in_as;", - "grouped_domain_alias_address" => "CREATE VIEW grouped_domain_alias_address (username, ad_alias) AS - SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '), '') AS ad_alias FROM mailbox - LEFT OUTER JOIN alias_domain ON target_domain=domain - GROUP BY username;", - "sieve_before" => "CREATE VIEW sieve_before (id, username, script_name, script_data) AS - SELECT md5(script_data), username, script_name, script_data FROM sieve_filters - WHERE filter_type = 'prefilter';", - "sieve_after" => "CREATE VIEW sieve_after (id, username, script_name, script_data) AS - SELECT md5(script_data), username, script_name, script_data FROM sieve_filters - WHERE filter_type = 'postfilter';" - ); - - $tables = array( - "versions" => array( - "cols" => array( - "application" => "VARCHAR(255) NOT NULL", - "version" => "VARCHAR(100) NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - ), - "keys" => array( - "primary" => array( - "" => array("application") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "admin" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "password" => "VARCHAR(255) NOT NULL", - "superadmin" => "TINYINT(1) NOT NULL DEFAULT '0'", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE NOW(0)", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("username") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "fido2" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "friendlyName" => "VARCHAR(255)", - "rpId" => "VARCHAR(255) NOT NULL", - "credentialPublicKey" => "TEXT NOT NULL", - "certificateChain" => "TEXT", - // Can be null for format "none" - "certificate" => "TEXT", - "certificateIssuer" => "VARCHAR(255)", - "certificateSubject" => "VARCHAR(255)", - "signatureCounter" => "INT", - "AAGUID" => "BLOB", - "credentialId" => "BLOB NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE NOW(0)", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "_sogo_static_view" => array( - "cols" => array( - "c_uid" => "VARCHAR(255) NOT NULL", - "domain" => "VARCHAR(255) NOT NULL", - "c_name" => "VARCHAR(255) NOT NULL", - "c_password" => "VARCHAR(255) NOT NULL DEFAULT ''", - "c_cn" => "VARCHAR(255)", - "mail" => "VARCHAR(255) NOT NULL", - // TODO -> use TEXT and check if SOGo login breaks on empty aliases - "aliases" => "TEXT NOT NULL", - "ad_aliases" => "VARCHAR(6144) NOT NULL DEFAULT ''", - "ext_acl" => "VARCHAR(6144) NOT NULL DEFAULT ''", - "kind" => "VARCHAR(100) NOT NULL DEFAULT ''", - "multiple_bookings" => "INT NOT NULL DEFAULT -1" - ), - "keys" => array( - "primary" => array( - "" => array("c_uid") - ), - "key" => array( - "domain" => array("domain") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "relayhosts" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "hostname" => "VARCHAR(255) NOT NULL", - "username" => "VARCHAR(255) NOT NULL", - "password" => "VARCHAR(255) NOT NULL", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "hostname" => array("hostname") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "transports" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "destination" => "VARCHAR(255) NOT NULL", - "nexthop" => "VARCHAR(255) NOT NULL", - "username" => "VARCHAR(255) NOT NULL DEFAULT ''", - "password" => "VARCHAR(255) NOT NULL DEFAULT ''", - "is_mx_based" => "TINYINT(1) NOT NULL DEFAULT '0'", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "destination" => array("destination"), - "nexthop" => array("nexthop"), - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "alias" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "address" => "VARCHAR(255) NOT NULL", - "goto" => "TEXT NOT NULL", - "domain" => "VARCHAR(255) NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "private_comment" => "TEXT", - "public_comment" => "TEXT", - "sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "unique" => array( - "address" => array("address") - ), - "key" => array( - "domain" => array("domain") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "api" => array( - "cols" => array( - "api_key" => "VARCHAR(255) NOT NULL", - "allow_from" => "VARCHAR(512) NOT NULL", - "skip_ip_check" => "TINYINT(1) NOT NULL DEFAULT '0'", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE NOW(0)", - "access" => "ENUM('ro', 'rw') NOT NULL DEFAULT 'rw'", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("api_key") - ), - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sender_acl" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "logged_in_as" => "VARCHAR(255) NOT NULL", - "send_as" => "VARCHAR(255) NOT NULL", - "external" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "templates" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "template" => "VARCHAR(255) NOT NULL", - "type" => "VARCHAR(255) NOT NULL", - "attributes" => "JSON", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "domain" => array( - // Todo: Move some attributes to json - "cols" => array( - "domain" => "VARCHAR(255) NOT NULL", - "description" => "VARCHAR(255)", - "aliases" => "INT(10) NOT NULL DEFAULT '0'", - "mailboxes" => "INT(10) NOT NULL DEFAULT '0'", - "defquota" => "BIGINT(20) NOT NULL DEFAULT '3072'", - "maxquota" => "BIGINT(20) NOT NULL DEFAULT '102400'", - "quota" => "BIGINT(20) NOT NULL DEFAULT '102400'", - "relayhost" => "VARCHAR(255) NOT NULL DEFAULT '0'", - "backupmx" => "TINYINT(1) NOT NULL DEFAULT '0'", - "gal" => "TINYINT(1) NOT NULL DEFAULT '1'", - "relay_all_recipients" => "TINYINT(1) NOT NULL DEFAULT '0'", - "relay_unknown_only" => "TINYINT(1) NOT NULL DEFAULT '0'", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("domain") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "tags_domain" => array( - "cols" => array( - "tag_name" => "VARCHAR(255) NOT NULL", - "domain" => "VARCHAR(255) NOT NULL" - ), - "keys" => array( - "fkey" => array( - "fk_tags_domain" => array( - "col" => "domain", - "ref" => "domain.domain", - "delete" => "CASCADE", - "update" => "NO ACTION" - ) - ), - "unique" => array( - "tag_name" => array("tag_name", "domain") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "tls_policy_override" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "dest" => "VARCHAR(255) NOT NULL", - "policy" => "ENUM('none', 'may', 'encrypt', 'dane', 'dane-only', 'fingerprint', 'verify', 'secure') NOT NULL", - "parameters" => "VARCHAR(255) DEFAULT ''", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "unique" => array( - "dest" => array("dest") - ), - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "quarantine" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "qid" => "VARCHAR(30) NOT NULL", - "subject" => "VARCHAR(500)", - "score" => "FLOAT(8,2)", - "ip" => "VARCHAR(50)", - "action" => "CHAR(20) NOT NULL DEFAULT 'unknown'", - "symbols" => "JSON", - "fuzzy_hashes" => "JSON", - "sender" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'", - "rcpt" => "VARCHAR(255)", - "msg" => "LONGTEXT", - "domain" => "VARCHAR(255)", - "notified" => "TINYINT(1) NOT NULL DEFAULT '0'", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "user" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'", - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "mailbox" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "password" => "VARCHAR(255) NOT NULL", - "name" => "VARCHAR(255)", - "description" => "VARCHAR(255)", - // mailbox_path_prefix is followed by domain/local_part/ - "mailbox_path_prefix" => "VARCHAR(150) DEFAULT '/var/vmail/'", - "quota" => "BIGINT(20) NOT NULL DEFAULT '102400'", - "local_part" => "VARCHAR(255) NOT NULL", - "domain" => "VARCHAR(255) NOT NULL", - "attributes" => "JSON", - "kind" => "VARCHAR(100) NOT NULL DEFAULT ''", - "multiple_bookings" => "INT NOT NULL DEFAULT -1", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("username") - ), - "key" => array( - "domain" => array("domain"), - "kind" => array("kind") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "tags_mailbox" => array( - "cols" => array( - "tag_name" => "VARCHAR(255) NOT NULL", - "username" => "VARCHAR(255) NOT NULL" - ), - "keys" => array( - "fkey" => array( - "fk_tags_mailbox" => array( - "col" => "username", - "ref" => "mailbox.username", - "delete" => "CASCADE", - "update" => "NO ACTION" - ) - ), - "unique" => array( - "tag_name" => array("tag_name", "username") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sieve_filters" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "username" => "VARCHAR(255) NOT NULL", - "script_desc" => "VARCHAR(255) NOT NULL", - "script_name" => "ENUM('active','inactive')", - "script_data" => "TEXT NOT NULL", - "filter_type" => "ENUM('postfilter','prefilter')", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "username" => array("username"), - "script_desc" => array("script_desc") - ), - "fkey" => array( - "fk_username_sieve_global_before" => array( - "col" => "username", - "ref" => "mailbox.username", - "delete" => "CASCADE", - "update" => "NO ACTION" - ) - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "app_passwd" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "name" => "VARCHAR(255) NOT NULL", - "mailbox" => "VARCHAR(255) NOT NULL", - "domain" => "VARCHAR(255) NOT NULL", - "password" => "VARCHAR(255) NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "imap_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "smtp_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "dav_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "eas_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "pop3_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "sieve_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "mailbox" => array("mailbox"), - "password" => array("password"), - "domain" => array("domain"), - ), - "fkey" => array( - "fk_username_app_passwd" => array( - "col" => "mailbox", - "ref" => "mailbox.username", - "delete" => "CASCADE", - "update" => "NO ACTION" - ) - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "user_acl" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "spam_alias" => "TINYINT(1) NOT NULL DEFAULT '1'", - "tls_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", - "spam_score" => "TINYINT(1) NOT NULL DEFAULT '1'", - "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", - "delimiter_action" => "TINYINT(1) NOT NULL DEFAULT '1'", - "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '0'", - "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'", - "sogo_profile_reset" => "TINYINT(1) NOT NULL DEFAULT '0'", - "pushover" => "TINYINT(1) NOT NULL DEFAULT '1'", - // quarantine is for quarantine actions, todo: rename - "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'", - "quarantine_attachments" => "TINYINT(1) NOT NULL DEFAULT '1'", - "quarantine_notification" => "TINYINT(1) NOT NULL DEFAULT '1'", - "quarantine_category" => "TINYINT(1) NOT NULL DEFAULT '1'", - "app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'", - ), - "keys" => array( - "primary" => array( - "" => array("username") - ), - "fkey" => array( - "fk_username" => array( - "col" => "username", - "ref" => "mailbox.username", - "delete" => "CASCADE", - "update" => "NO ACTION" - ) - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "alias_domain" => array( - "cols" => array( - "alias_domain" => "VARCHAR(255) NOT NULL", - "target_domain" => "VARCHAR(255) NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("alias_domain") - ), - "key" => array( - "active" => array("active"), - "target_domain" => array("target_domain") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "spamalias" => array( - "cols" => array( - "address" => "VARCHAR(255) NOT NULL", - "goto" => "TEXT NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "validity" => "INT(11)" - ), - "keys" => array( - "primary" => array( - "" => array("address") - ), - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "filterconf" => array( - "cols" => array( - "object" => "VARCHAR(255) NOT NULL DEFAULT ''", - "option" => "VARCHAR(50) NOT NULL DEFAULT ''", - "value" => "VARCHAR(100) NOT NULL DEFAULT ''", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "prefid" => "INT(11) NOT NULL AUTO_INCREMENT" - ), - "keys" => array( - "primary" => array( - "" => array("prefid") - ), - "key" => array( - "object" => array("object") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "settingsmap" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "desc" => "VARCHAR(255) NOT NULL", - "content" => "LONGTEXT NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "logs" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "task" => "CHAR(32) NOT NULL DEFAULT '000000'", - "type" => "VARCHAR(32) DEFAULT ''", - "msg" => "TEXT", - "call" => "TEXT", - "user" => "VARCHAR(64) NOT NULL", - "role" => "VARCHAR(32) NOT NULL", - "remote" => "VARCHAR(39) NOT NULL", - "time" => "INT(11) NOT NULL" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sasl_log" => array( - "cols" => array( - "service" => "VARCHAR(32) NOT NULL DEFAULT ''", - "app_password" => "INT", - "username" => "VARCHAR(255) NOT NULL", - "real_rip" => "VARCHAR(64) NOT NULL", - "datetime" => "DATETIME(0) NOT NULL DEFAULT NOW(0)" - ), - "keys" => array( - "primary" => array( - "" => array("service", "real_rip", "username") - ), - "key" => array( - "username" => array("username"), - "service" => array("service"), - "datetime" => array("datetime"), - "real_rip" => array("real_rip") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "quota2" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "bytes" => "BIGINT(20) NOT NULL DEFAULT '0'", - "messages" => "BIGINT(20) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("username") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "quota2replica" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "bytes" => "BIGINT(20) NOT NULL DEFAULT '0'", - "messages" => "BIGINT(20) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("username") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "domain_admins" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "username" => "VARCHAR(255) NOT NULL", - "domain" => "VARCHAR(255) NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "username" => array("username") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "da_acl" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'", - "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'", - "login_as" => "TINYINT(1) NOT NULL DEFAULT '1'", - "sogo_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'", - "bcc_maps" => "TINYINT(1) NOT NULL DEFAULT '1'", - "pushover" => "TINYINT(1) NOT NULL DEFAULT '0'", - "filters" => "TINYINT(1) NOT NULL DEFAULT '1'", - "ratelimit" => "TINYINT(1) NOT NULL DEFAULT '1'", - "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", - "extend_sender_acl" => "TINYINT(1) NOT NULL DEFAULT '0'", - "unlimited_quota" => "TINYINT(1) NOT NULL DEFAULT '0'", - "protocol_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "smtp_ip_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "alias_domains" => "TINYINT(1) NOT NULL DEFAULT '0'", - "mailbox_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'", - "domain_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'", - "domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("username") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "imapsync" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "user2" => "VARCHAR(255) NOT NULL", - "host1" => "VARCHAR(255) NOT NULL", - "authmech1" => "ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN'", - "regextrans2" => "VARCHAR(255) DEFAULT ''", - "authmd51" => "TINYINT(1) NOT NULL DEFAULT 0", - "domain2" => "VARCHAR(255) NOT NULL DEFAULT ''", - "subfolder2" => "VARCHAR(255) NOT NULL DEFAULT ''", - "user1" => "VARCHAR(255) NOT NULL", - "password1" => "VARCHAR(255) NOT NULL", - "exclude" => "VARCHAR(500) NOT NULL DEFAULT ''", - "maxage" => "SMALLINT NOT NULL DEFAULT '0'", - "mins_interval" => "SMALLINT UNSIGNED NOT NULL DEFAULT '0'", - "maxbytespersecond" => "VARCHAR(50) NOT NULL DEFAULT '0'", - "port1" => "SMALLINT UNSIGNED NOT NULL", - "enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'", - "delete2duplicates" => "TINYINT(1) NOT NULL DEFAULT '1'", - "delete1" => "TINYINT(1) NOT NULL DEFAULT '0'", - "delete2" => "TINYINT(1) NOT NULL DEFAULT '0'", - "automap" => "TINYINT(1) NOT NULL DEFAULT '0'", - "skipcrossduplicates" => "TINYINT(1) NOT NULL DEFAULT '0'", - "custom_params" => "VARCHAR(512) NOT NULL DEFAULT ''", - "timeout1" => "SMALLINT NOT NULL DEFAULT '600'", - "timeout2" => "SMALLINT NOT NULL DEFAULT '600'", - "subscribeall" => "TINYINT(1) NOT NULL DEFAULT '1'", - "is_running" => "TINYINT(1) NOT NULL DEFAULT '0'", - "returned_text" => "LONGTEXT", - "last_run" => "TIMESTAMP NULL DEFAULT NULL", - "success" => "TINYINT(1) UNSIGNED DEFAULT NULL", - "exit_status" => "VARCHAR(50) DEFAULT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "bcc_maps" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "local_dest" => "VARCHAR(255) NOT NULL", - "bcc_dest" => "VARCHAR(255) NOT NULL", - "domain" => "VARCHAR(255) NOT NULL", - "type" => "ENUM('sender','rcpt')", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "local_dest" => array("local_dest"), - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "recipient_maps" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "old_dest" => "VARCHAR(255) NOT NULL", - "new_dest" => "VARCHAR(255) NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "local_dest" => array("old_dest"), - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "tfa" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "key_id" => "VARCHAR(255) NOT NULL", - "username" => "VARCHAR(255) NOT NULL", - "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')", - "secret" => "VARCHAR(255) DEFAULT NULL", - "keyHandle" => "VARCHAR(1023) DEFAULT NULL", - "publicKey" => "VARCHAR(4096) DEFAULT NULL", - "counter" => "INT NOT NULL DEFAULT '0'", - "certificate" => "TEXT", - "active" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "forwarding_hosts" => array( - "cols" => array( - "host" => "VARCHAR(255) NOT NULL", - "source" => "VARCHAR(255) NOT NULL", - "filter_spam" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("host") - ), - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_acl" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "c_folder_id" => "INT NOT NULL", - "c_object" => "VARCHAR(255) NOT NULL", - "c_uid" => "VARCHAR(255) NOT NULL", - "c_role" => "VARCHAR(80) NOT NULL" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "sogo_acl_c_folder_id_idx" => array("c_folder_id"), - "sogo_acl_c_uid_idx" => array("c_uid") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_alarms_folder" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "c_path" => "VARCHAR(255) NOT NULL", - "c_name" => "VARCHAR(255) NOT NULL", - "c_uid" => "VARCHAR(255) NOT NULL", - "c_recurrence_id" => "INT(11) DEFAULT NULL", - "c_alarm_number" => "INT(11) NOT NULL", - "c_alarm_date" => "INT(11) NOT NULL" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_cache_folder" => array( - "cols" => array( - "c_uid" => "VARCHAR(255) NOT NULL", - "c_path" => "VARCHAR(255) NOT NULL", - "c_parent_path" => "VARCHAR(255) DEFAULT NULL", - "c_type" => "TINYINT(3) unsigned NOT NULL", - "c_creationdate" => "INT(11) NOT NULL", - "c_lastmodified" => "INT(11) NOT NULL", - "c_version" => "INT(11) NOT NULL DEFAULT '0'", - "c_deleted" => "TINYINT(4) NOT NULL DEFAULT '0'", - "c_content" => "LONGTEXT" - ), - "keys" => array( - "primary" => array( - "" => array("c_uid", "c_path") - ), - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_folder_info" => array( - "cols" => array( - "c_folder_id" => "BIGINT(20) unsigned NOT NULL AUTO_INCREMENT", - "c_path" => "VARCHAR(255) NOT NULL", - "c_path1" => "VARCHAR(255) NOT NULL", - "c_path2" => "VARCHAR(255) DEFAULT NULL", - "c_path3" => "VARCHAR(255) DEFAULT NULL", - "c_path4" => "VARCHAR(255) DEFAULT NULL", - "c_foldername" => "VARCHAR(255) NOT NULL", - "c_location" => "VARCHAR(2048) DEFAULT NULL", - "c_quick_location" => "VARCHAR(2048) DEFAULT NULL", - "c_acl_location" => "VARCHAR(2048) DEFAULT NULL", - "c_folder_type" => "VARCHAR(255) NOT NULL" - ), - "keys" => array( - "primary" => array( - "" => array("c_path") - ), - "unique" => array( - "c_folder_id" => array("c_folder_id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_quick_appointment" => array( - "cols" => array( - "c_folder_id" => "INT NOT NULL", - "c_name" => "VARCHAR(255) NOT NULL", - "c_uid" => "VARCHAR(1000) NOT NULL", - "c_startdate" => "INT", - "c_enddate" => "INT", - "c_cycleenddate" => "INT", - "c_title" => "VARCHAR(1000) NOT NULL", - "c_participants" => "TEXT", - "c_isallday" => "INT", - "c_iscycle" => "INT", - "c_cycleinfo" => "TEXT", - "c_classification" => "INT NOT NULL", - "c_isopaque" => "INT NOT NULL", - "c_status" => "INT NOT NULL", - "c_priority" => "INT", - "c_location" => "VARCHAR(255)", - "c_orgmail" => "VARCHAR(255)", - "c_partmails" => "TEXT", - "c_partstates" => "TEXT", - "c_category" => "VARCHAR(255)", - "c_sequence" => "INT", - "c_component" => "VARCHAR(10) NOT NULL", - "c_nextalarm" => "INT", - "c_description" => "TEXT" - ), - "keys" => array( - "primary" => array( - "" => array("c_folder_id", "c_name") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_quick_contact" => array( - "cols" => array( - "c_folder_id" => "INT NOT NULL", - "c_name" => "VARCHAR(255) NOT NULL", - "c_givenname" => "VARCHAR(255)", - "c_cn" => "VARCHAR(255)", - "c_sn" => "VARCHAR(255)", - "c_screenname" => "VARCHAR(255)", - "c_l" => "VARCHAR(255)", - "c_mail" => "TEXT", - "c_o" => "VARCHAR(500)", - "c_ou" => "VARCHAR(255)", - "c_telephonenumber" => "VARCHAR(255)", - "c_categories" => "VARCHAR(255)", - "c_component" => "VARCHAR(10) NOT NULL", - "c_hascertificate" => "INT4 DEFAULT 0" - ), - "keys" => array( - "primary" => array( - "" => array("c_folder_id", "c_name") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_sessions_folder" => array( - "cols" => array( - "c_id" => "VARCHAR(255) NOT NULL", - "c_value" => "VARCHAR(4096) NOT NULL", - "c_creationdate" => "INT(11) NOT NULL", - "c_lastseen" => "INT(11) NOT NULL" - ), - "keys" => array( - "primary" => array( - "" => array("c_id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_store" => array( - "cols" => array( - "c_folder_id" => "INT NOT NULL", - "c_name" => "VARCHAR(255) NOT NULL", - "c_content" => "MEDIUMTEXT NOT NULL", - "c_creationdate" => "INT NOT NULL", - "c_lastmodified" => "INT NOT NULL", - "c_version" => "INT NOT NULL", - "c_deleted" => "INT" - ), - "keys" => array( - "primary" => array( - "" => array("c_folder_id", "c_name") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "pushover" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "key" => "VARCHAR(255) NOT NULL", - "token" => "VARCHAR(255) NOT NULL", - "attributes" => "JSON", - "title" => "TEXT", - "text" => "TEXT", - "senders" => "TEXT", - "senders_regex" => "TEXT", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("username") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_user_profile" => array( - "cols" => array( - "c_uid" => "VARCHAR(255) NOT NULL", - "c_defaults" => "LONGTEXT", - "c_settings" => "LONGTEXT" - ), - "keys" => array( - "primary" => array( - "" => array("c_uid") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "oauth_clients" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "client_id" => "VARCHAR(80) NOT NULL", - "client_secret" => "VARCHAR(80)", - "redirect_uri" => "VARCHAR(2000)", - "grant_types" => "VARCHAR(80)", - "scope" => "VARCHAR(4000)", - "user_id" => "VARCHAR(80)" - ), - "keys" => array( - "primary" => array( - "" => array("client_id") - ), - "unique" => array( - "id" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "oauth_access_tokens" => array( - "cols" => array( - "access_token" => "VARCHAR(40) NOT NULL", - "client_id" => "VARCHAR(80) NOT NULL", - "user_id" => "VARCHAR(80)", - "expires" => "TIMESTAMP NOT NULL", - "scope" => "VARCHAR(4000)" - ), - "keys" => array( - "primary" => array( - "" => array("access_token") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "oauth_authorization_codes" => array( - "cols" => array( - "authorization_code" => "VARCHAR(40) NOT NULL", - "client_id" => "VARCHAR(80) NOT NULL", - "user_id" => "VARCHAR(80)", - "redirect_uri" => "VARCHAR(2000)", - "expires" => "TIMESTAMP NOT NULL", - "scope" => "VARCHAR(4000)", - "id_token" => "VARCHAR(1000)" - ), - "keys" => array( - "primary" => array( - "" => array("authorization_code") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "oauth_refresh_tokens" => array( - "cols" => array( - "refresh_token" => "VARCHAR(40) NOT NULL", - "client_id" => "VARCHAR(80) NOT NULL", - "user_id" => "VARCHAR(80)", - "expires" => "TIMESTAMP NOT NULL", - "scope" => "VARCHAR(4000)" - ), - "keys" => array( - "primary" => array( - "" => array("refresh_token") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ) - ); - - foreach ($tables as $table => $properties) { - // Migrate to quarantine - if ($table == 'quarantine') { - $stmt = $pdo->query("SHOW TABLES LIKE 'quarantaine'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $stmt = $pdo->query("SHOW TABLES LIKE 'quarantine'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("RENAME TABLE `quarantaine` TO `quarantine`"); - } - } - } - - // Migrate tls_enforce_* options - if ($table == 'mailbox') { - $stmt = $pdo->query("SHOW TABLES LIKE 'mailbox'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE '%tls_enforce%'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $stmt = $pdo->query("SELECT `username`, `tls_enforce_in`, `tls_enforce_out` FROM `mailbox`"); - $tls_options_rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($row = array_shift($tls_options_rows)) { - $tls_options[$row['username']] = array('tls_enforce_in' => $row['tls_enforce_in'], 'tls_enforce_out' => $row['tls_enforce_out']); - } - } - } - } - - $stmt = $pdo->query("SHOW TABLES LIKE '" . $table . "'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $stmt = $pdo->prepare("SELECT CONCAT('ALTER TABLE ', `table_schema`, '.', `table_name`, ' DROP FOREIGN KEY ', `constraint_name`, ';') AS `FKEY_DROP` FROM `information_schema`.`table_constraints` - WHERE `constraint_type` = 'FOREIGN KEY' AND `table_name` = :table;"); - $stmt->execute(array(':table' => $table)); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($row = array_shift($rows)) { - $pdo->query($row['FKEY_DROP']); - } - foreach($properties['cols'] as $column => $type) { - $stmt = $pdo->query("SHOW COLUMNS FROM `" . $table . "` LIKE '" . $column . "'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - if (strpos($type, 'AUTO_INCREMENT') !== false) { - $type = $type . ' PRIMARY KEY '; - // Adding an AUTO_INCREMENT key, need to drop primary keys first, if exists - $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $pdo->query("ALTER TABLE `" . $table . "` DROP PRIMARY KEY"); - } - } - $pdo->query("ALTER TABLE `" . $table . "` ADD `" . $column . "` " . $type); - } - else { - $pdo->query("ALTER TABLE `" . $table . "` MODIFY COLUMN `" . $column . "` " . $type); - } - } - foreach($properties['keys'] as $key_type => $key_content) { - if (strtolower($key_type) == 'primary') { - foreach ($key_content as $key_values) { - $fields = "`" . implode("`, `", $key_values) . "`"; - $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - $is_drop = ($num_results != 0) ? "DROP PRIMARY KEY, " : ""; - $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD PRIMARY KEY (" . $fields . ")"); - } - } - if (strtolower($key_type) == 'key') { - foreach ($key_content as $key_name => $key_values) { - $fields = "`" . implode("`, `", $key_values) . "`"; - $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = '" . $key_name . "'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - $is_drop = ($num_results != 0) ? "DROP INDEX `" . $key_name . "`, " : ""; - $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD KEY `" . $key_name . "` (" . $fields . ")"); - } - } - if (strtolower($key_type) == 'unique') { - foreach ($key_content as $key_name => $key_values) { - $fields = "`" . implode("`, `", $key_values) . "`"; - $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = '" . $key_name . "'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - $is_drop = ($num_results != 0) ? "DROP INDEX `" . $key_name . "`, " : ""; - $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD UNIQUE KEY `" . $key_name . "` (" . $fields . ")"); - } - } - if (strtolower($key_type) == 'fkey') { - foreach ($key_content as $key_name => $key_values) { - $fields = "`" . implode("`, `", $key_values) . "`"; - $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = '" . $key_name . "'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $pdo->query("ALTER TABLE `" . $table . "` DROP INDEX `" . $key_name . "`"); - } - @list($table_ref, $field_ref) = explode('.', $key_values['ref']); - $pdo->query("ALTER TABLE `" . $table . "` ADD FOREIGN KEY `" . $key_name . "` (" . $key_values['col'] . ") REFERENCES `" . $table_ref . "` (`" . $field_ref . "`) - ON DELETE " . $key_values['delete'] . " ON UPDATE " . $key_values['update']); - } - } - } - // Drop all vanished columns - $stmt = $pdo->query("SHOW COLUMNS FROM `" . $table . "`"); - $cols_in_table = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($row = array_shift($cols_in_table)) { - if (!array_key_exists($row['Field'], $properties['cols'])) { - $pdo->query("ALTER TABLE `" . $table . "` DROP COLUMN `" . $row['Field'] . "`;"); - } - } - - // Step 1: Get all non-primary keys, that currently exist and those that should exist - $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE `Key_name` != 'PRIMARY'"); - $keys_in_table = $stmt->fetchAll(PDO::FETCH_ASSOC); - $keys_to_exist = array(); - if (isset($properties['keys']['unique']) && is_array($properties['keys']['unique'])) { - foreach ($properties['keys']['unique'] as $key_name => $key_values) { - $keys_to_exist[] = $key_name; - } - } - if (isset($properties['keys']['key']) && is_array($properties['keys']['key'])) { - foreach ($properties['keys']['key'] as $key_name => $key_values) { - $keys_to_exist[] = $key_name; - } - } - // Index for foreign key must exist - if (isset($properties['keys']['fkey']) && is_array($properties['keys']['fkey'])) { - foreach ($properties['keys']['fkey'] as $key_name => $key_values) { - $keys_to_exist[] = $key_name; - } - } - // Step 2: Drop all vanished indexes - while ($row = array_shift($keys_in_table)) { - if (!in_array($row['Key_name'], $keys_to_exist)) { - $pdo->query("ALTER TABLE `" . $table . "` DROP INDEX `" . $row['Key_name'] . "`"); - } - } - // Step 3: Drop all vanished primary keys - if (!isset($properties['keys']['primary'])) { - $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $pdo->query("ALTER TABLE `" . $table . "` DROP PRIMARY KEY"); - } - } - } - else { - // Create table if it is missing - $sql = "CREATE TABLE IF NOT EXISTS `" . $table . "` ("; - foreach($properties['cols'] as $column => $type) { - $sql .= "`" . $column . "` " . $type . ","; - } - foreach($properties['keys'] as $key_type => $key_content) { - if (strtolower($key_type) == 'primary') { - foreach ($key_content as $key_values) { - $fields = "`" . implode("`, `", $key_values) . "`"; - $sql .= "PRIMARY KEY (" . $fields . ")" . ","; - } - } - elseif (strtolower($key_type) == 'key') { - foreach ($key_content as $key_name => $key_values) { - $fields = "`" . implode("`, `", $key_values) . "`"; - $sql .= "KEY `" . $key_name . "` (" . $fields . ")" . ","; - } - } - elseif (strtolower($key_type) == 'unique') { - foreach ($key_content as $key_name => $key_values) { - $fields = "`" . implode("`, `", $key_values) . "`"; - $sql .= "UNIQUE KEY `" . $key_name . "` (" . $fields . ")" . ","; - } - } - elseif (strtolower($key_type) == 'fkey') { - foreach ($key_content as $key_name => $key_values) { - @list($table_ref, $field_ref) = explode('.', $key_values['ref']); - $sql .= "FOREIGN KEY `" . $key_name . "` (" . $key_values['col'] . ") REFERENCES `" . $table_ref . "` (`" . $field_ref . "`) - ON DELETE " . $key_values['delete'] . " ON UPDATE " . $key_values['update'] . ","; - } - } - } - $sql = rtrim($sql, ","); - $sql .= ") " . $properties['attr']; - $pdo->query($sql); - } - // Reset table attributes - $pdo->query("ALTER TABLE `" . $table . "` " . $properties['attr'] . ";"); - - } - - // Recreate SQL views - foreach ($views as $view => $create) { - $pdo->query("DROP VIEW IF EXISTS `" . $view . "`;"); - $pdo->query($create); - } - - // Mitigate imapsync argument injection issue - $pdo->query("UPDATE `imapsync` SET `custom_params` = '' - WHERE `custom_params` LIKE '%pipemess%' - OR custom_params LIKE '%skipmess%' - OR custom_params LIKE '%delete2foldersonly%' - OR custom_params LIKE '%delete2foldersbutnot%' - OR custom_params LIKE '%regexflag%' - OR custom_params LIKE '%pipemess%' - OR custom_params LIKE '%regextrans2%' - OR custom_params LIKE '%maxlinelengthcmd%';"); - - // Migrate webauthn tfa - $stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')"); - - // Inject admin if not exists - $stmt = $pdo->query("SELECT NULL FROM `admin`"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("INSERT INTO `admin` (`username`, `password`, `superadmin`, `created`, `modified`, `active`) - VALUES ('admin', '{SSHA256}K8eVJ6YsZbQCfuJvSUbaQRLr0HPLz5rC9IAp0PAFl0tmNDBkMDc0NDAyOTAxN2Rk', 1, NOW(), NOW(), 1)"); - $pdo->query("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) - SELECT `username`, 'ALL', NOW(), 1 FROM `admin` - WHERE superadmin='1' AND `username` NOT IN (SELECT `username` FROM `domain_admins`);"); - $pdo->query("DELETE FROM `admin` WHERE `username` NOT IN (SELECT `username` FROM `domain_admins`);"); - } - // Insert new DB schema version - $pdo->query("REPLACE INTO `versions` (`application`, `version`) VALUES ('db_schema', '" . $db_version . "');"); - - // Fix dangling domain admins - $pdo->query("DELETE FROM `admin` WHERE `superadmin` = 0 AND `username` NOT IN (SELECT `username`FROM `domain_admins`);"); - $pdo->query("DELETE FROM `da_acl` WHERE `username` NOT IN (SELECT `username`FROM `domain_admins`);"); - - // Migrate attributes - // pushover - $pdo->query("UPDATE `pushover` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;"); - $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.evaluate_x_prio', \"0\") WHERE JSON_VALUE(`attributes`, '$.evaluate_x_prio') IS NULL;"); - $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.only_x_prio', \"0\") WHERE JSON_VALUE(`attributes`, '$.only_x_prio') IS NULL;"); - // mailbox - $pdo->query("UPDATE `mailbox` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.passwd_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.passwd_update') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.relayhost', \"0\") WHERE JSON_VALUE(`attributes`, '$.relayhost') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_pw_update') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sieve_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sieve_access') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sogo_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.imap_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.imap_access') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.pop3_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.pop3_access') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.smtp_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.smtp_access') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.mailbox_format', \"maildir:\") WHERE JSON_VALUE(`attributes`, '$.mailbox_format') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_notification', \"never\") WHERE JSON_VALUE(`attributes`, '$.quarantine_notification') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_category', \"reject\") WHERE JSON_VALUE(`attributes`, '$.quarantine_category') IS NULL;"); - foreach($tls_options as $tls_user => $tls_options) { - $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', :tls_enforce_in), - `attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', :tls_enforce_out) - WHERE `username` = :username"); - $stmt->execute(array(':tls_enforce_in' => $tls_options['tls_enforce_in'], ':tls_enforce_out' => $tls_options['tls_enforce_out'], ':username' => $tls_user)); - } - // Set tls_enforce_* if still missing (due to deleted attrs for example) - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', \"1\") WHERE JSON_VALUE(`attributes`, '$.tls_enforce_out') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', \"1\") WHERE JSON_VALUE(`attributes`, '$.tls_enforce_in') IS NULL;"); - // Fix ACL - $pdo->query("INSERT INTO `user_acl` (`username`) SELECT `username` FROM `mailbox` WHERE `kind` = '' AND NOT EXISTS (SELECT `username` FROM `user_acl`);"); - $pdo->query("INSERT INTO `da_acl` (`username`) SELECT DISTINCT `username` FROM `domain_admins` WHERE `username` != 'admin' AND NOT EXISTS (SELECT `username` FROM `da_acl`);"); - // Fix domain_admins - $pdo->query("DELETE FROM `domain_admins` WHERE `domain` = 'ALL';"); - - // add default templates - $default_domain_template = array( - "template" => "Default", - "type" => "domain", - "attributes" => array( - "tags" => array(), - "max_num_aliases_for_domain" => 400, - "max_num_mboxes_for_domain" => 10, - "def_quota_for_mbox" => 3072 * 1048576, - "max_quota_for_mbox" => 10240 * 1048576, - "max_quota_for_domain" => 10240 * 1048576, - "rl_frame" => "s", - "rl_value" => "", - "active" => 1, - "gal" => 1, - "backupmx" => 0, - "relay_all_recipients" => 0, - "relay_unknown_only" => 0, - "dkim_selector" => "dkim", - "key_size" => 2048, - "max_quota_for_domain" => 10240 * 1048576, - ) - ); - $default_mailbox_template = array( - "template" => "Default", - "type" => "mailbox", - "attributes" => array( - "tags" => array(), - "quota" => 0, - "quarantine_notification" => strval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['quarantine_notification']), - "quarantine_category" => strval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['quarantine_category']), - "rl_frame" => "s", - "rl_value" => "", - "force_pw_update" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_pw_update']), - "sogo_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sogo_access']), - "active" => 1, - "tls_enforce_in" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_in']), - "tls_enforce_out" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_out']), - "imap_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['imap_access']), - "pop3_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['pop3_access']), - "smtp_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['smtp_access']), - "sieve_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sieve_access']), - "acl_spam_alias" => 1, - "acl_tls_policy" => 1, - "acl_spam_score" => 1, - "acl_spam_policy" => 1, - "acl_delimiter_action" => 1, - "acl_syncjobs" => 0, - "acl_eas_reset" => 1, - "acl_sogo_profile_reset" => 0, - "acl_pushover" => 1, - "acl_quarantine" => 1, - "acl_quarantine_attachments" => 1, - "acl_quarantine_notification" => 1, - "acl_quarantine_category" => 1, - "acl_app_passwds" => 1, - ) - ); - $stmt = $pdo->prepare("SELECT id FROM `templates` WHERE `type` = :type AND `template` = :template"); - $stmt->execute(array( - ":type" => "domain", - ":template" => $default_domain_template["template"] - )); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (empty($row)){ - $stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`) - VALUES (:type, :template, :attributes)"); - $stmt->execute(array( - ":type" => "domain", - ":template" => $default_domain_template["template"], - ":attributes" => json_encode($default_domain_template["attributes"]) - )); - } - $stmt = $pdo->prepare("SELECT id FROM `templates` WHERE `type` = :type AND `template` = :template"); - $stmt->execute(array( - ":type" => "mailbox", - ":template" => $default_mailbox_template["template"] - )); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (empty($row)){ - $stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`) - VALUES (:type, :template, :attributes)"); - $stmt->execute(array( - ":type" => "mailbox", - ":template" => $default_mailbox_template["template"], - ":attributes" => json_encode($default_mailbox_template["attributes"]) - )); - } - - if (php_sapi_name() == "cli") { - echo "DB initialization completed" . PHP_EOL; - } else { - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__), - 'msg' => 'db_init_complete' - ); - } - } - catch (PDOException $e) { - if (php_sapi_name() == "cli") { - echo "DB initialization failed: " . print_r($e, true) . PHP_EOL; - } else { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__), - 'msg' => array('mysql_error', $e) - ); - } - } -} -if (php_sapi_name() == "cli") { - include '/web/inc/vars.inc.php'; - include '/web/inc/functions.docker.inc.php'; - // $now = new DateTime(); - // $mins = $now->getOffset() / 60; - // $sgn = ($mins < 0 ? -1 : 1); - // $mins = abs($mins); - // $hrs = floor($mins / 60); - // $mins -= $hrs * 60; - // $offset = sprintf('%+d:%02d', $hrs*$sgn, $mins); - $dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name; - $opt = [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_EMULATE_PREPARES => false, - //PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '" . $offset . "', group_concat_max_len = 3423543543;", - ]; - $pdo = new PDO($dsn, $database_user, $database_pass, $opt); - $stmt = $pdo->query("SELECT COUNT('OK') AS OK_C FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view' OR TABLE_NAME = '_sogo_static_view';"); - $res = $stmt->fetch(PDO::FETCH_ASSOC); - if (intval($res['OK_C']) === 2) { - // Be more precise when replacing into _sogo_static_view, col orders may change - try { - $stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`) - SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view"); - $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');"); - echo "Fixed _sogo_static_view" . PHP_EOL; - } - catch ( Exception $e ) { - // Dunno - } - } - try { - $m = new Memcached(); - $m->addServer('memcached', 11211); - $m->flush(); - echo "Cleaned up memcached". PHP_EOL; - } - catch ( Exception $e ) { - // Dunno - } - init_db_schema(); -} +query("SHOW TABLES LIKE 'versions'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'db_schema'"); + if ($stmt->fetch(PDO::FETCH_ASSOC)['version'] == $db_version) { + return true; + } + if (!preg_match('/y|yes/i', getenv('MASTER'))) { + $_SESSION['return'][] = array( + 'type' => 'warning', + 'log' => array(__FUNCTION__), + 'msg' => 'Database not initialized: not running db_init on slave.' + ); + return true; + } + } + + $views = array( + "grouped_mail_aliases" => "CREATE VIEW grouped_mail_aliases (username, aliases) AS + SELECT goto, IFNULL(GROUP_CONCAT(address ORDER BY address SEPARATOR ' '), '') AS address FROM alias + WHERE address!=goto + AND active = '1' + AND sogo_visible = '1' + AND address NOT LIKE '@%' + GROUP BY goto;", + // START + // Unused at the moment - we cannot allow to show a foreign mailbox as sender address in SOGo, as SOGo does not like this + // We need to create delegation in SOGo AND set a sender_acl in mailcow to allow to send as user X + "grouped_sender_acl" => "CREATE VIEW grouped_sender_acl (username, send_as_acl) AS + SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl + WHERE send_as NOT LIKE '@%' + GROUP BY logged_in_as;", + // END + "grouped_sender_acl_external" => "CREATE VIEW grouped_sender_acl_external (username, send_as_acl) AS + SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl + WHERE send_as NOT LIKE '@%' AND external = '1' + GROUP BY logged_in_as;", + "grouped_domain_alias_address" => "CREATE VIEW grouped_domain_alias_address (username, ad_alias) AS + SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '), '') AS ad_alias FROM mailbox + LEFT OUTER JOIN alias_domain ON target_domain=domain + GROUP BY username;", + "sieve_before" => "CREATE VIEW sieve_before (id, username, script_name, script_data) AS + SELECT md5(script_data), username, script_name, script_data FROM sieve_filters + WHERE filter_type = 'prefilter';", + "sieve_after" => "CREATE VIEW sieve_after (id, username, script_name, script_data) AS + SELECT md5(script_data), username, script_name, script_data FROM sieve_filters + WHERE filter_type = 'postfilter';" + ); + + $tables = array( + "versions" => array( + "cols" => array( + "application" => "VARCHAR(255) NOT NULL", + "version" => "VARCHAR(100) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + ), + "keys" => array( + "primary" => array( + "" => array("application") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "admin" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "password" => "VARCHAR(255) NOT NULL", + "superadmin" => "TINYINT(1) NOT NULL DEFAULT '0'", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE NOW(0)", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "fido2" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "friendlyName" => "VARCHAR(255)", + "rpId" => "VARCHAR(255) NOT NULL", + "credentialPublicKey" => "TEXT NOT NULL", + "certificateChain" => "TEXT", + // Can be null for format "none" + "certificate" => "TEXT", + "certificateIssuer" => "VARCHAR(255)", + "certificateSubject" => "VARCHAR(255)", + "signatureCounter" => "INT", + "AAGUID" => "BLOB", + "credentialId" => "BLOB NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE NOW(0)", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "_sogo_static_view" => array( + "cols" => array( + "c_uid" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "c_name" => "VARCHAR(255) NOT NULL", + "c_password" => "VARCHAR(255) NOT NULL DEFAULT ''", + "c_cn" => "VARCHAR(255)", + "mail" => "VARCHAR(255) NOT NULL", + // TODO -> use TEXT and check if SOGo login breaks on empty aliases + "aliases" => "TEXT NOT NULL", + "ad_aliases" => "VARCHAR(6144) NOT NULL DEFAULT ''", + "ext_acl" => "VARCHAR(6144) NOT NULL DEFAULT ''", + "kind" => "VARCHAR(100) NOT NULL DEFAULT ''", + "multiple_bookings" => "INT NOT NULL DEFAULT -1" + ), + "keys" => array( + "primary" => array( + "" => array("c_uid") + ), + "key" => array( + "domain" => array("domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "relayhosts" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "hostname" => "VARCHAR(255) NOT NULL", + "username" => "VARCHAR(255) NOT NULL", + "password" => "VARCHAR(255) NOT NULL", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "hostname" => array("hostname") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "transports" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "destination" => "VARCHAR(255) NOT NULL", + "nexthop" => "VARCHAR(255) NOT NULL", + "username" => "VARCHAR(255) NOT NULL DEFAULT ''", + "password" => "VARCHAR(255) NOT NULL DEFAULT ''", + "is_mx_based" => "TINYINT(1) NOT NULL DEFAULT '0'", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "destination" => array("destination"), + "nexthop" => array("nexthop"), + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "alias" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "address" => "VARCHAR(255) NOT NULL", + "goto" => "TEXT NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "private_comment" => "TEXT", + "public_comment" => "TEXT", + "sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "unique" => array( + "address" => array("address") + ), + "key" => array( + "domain" => array("domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "api" => array( + "cols" => array( + "api_key" => "VARCHAR(255) NOT NULL", + "allow_from" => "VARCHAR(512) NOT NULL", + "skip_ip_check" => "TINYINT(1) NOT NULL DEFAULT '0'", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE NOW(0)", + "access" => "ENUM('ro', 'rw') NOT NULL DEFAULT 'rw'", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("api_key") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sender_acl" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "logged_in_as" => "VARCHAR(255) NOT NULL", + "send_as" => "VARCHAR(255) NOT NULL", + "external" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "templates" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "template" => "VARCHAR(255) NOT NULL", + "type" => "VARCHAR(255) NOT NULL", + "attributes" => "JSON", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "domain" => array( + // Todo: Move some attributes to json + "cols" => array( + "domain" => "VARCHAR(255) NOT NULL", + "description" => "VARCHAR(255)", + "aliases" => "INT(10) NOT NULL DEFAULT '0'", + "mailboxes" => "INT(10) NOT NULL DEFAULT '0'", + "defquota" => "BIGINT(20) NOT NULL DEFAULT '3072'", + "maxquota" => "BIGINT(20) NOT NULL DEFAULT '102400'", + "quota" => "BIGINT(20) NOT NULL DEFAULT '102400'", + "relayhost" => "VARCHAR(255) NOT NULL DEFAULT '0'", + "backupmx" => "TINYINT(1) NOT NULL DEFAULT '0'", + "gal" => "TINYINT(1) NOT NULL DEFAULT '1'", + "relay_all_recipients" => "TINYINT(1) NOT NULL DEFAULT '0'", + "relay_unknown_only" => "TINYINT(1) NOT NULL DEFAULT '0'", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "tags_domain" => array( + "cols" => array( + "tag_name" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL" + ), + "keys" => array( + "fkey" => array( + "fk_tags_domain" => array( + "col" => "domain", + "ref" => "domain.domain", + "delete" => "CASCADE", + "update" => "NO ACTION" + ) + ), + "unique" => array( + "tag_name" => array("tag_name", "domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "tls_policy_override" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "dest" => "VARCHAR(255) NOT NULL", + "policy" => "ENUM('none', 'may', 'encrypt', 'dane', 'dane-only', 'fingerprint', 'verify', 'secure') NOT NULL", + "parameters" => "VARCHAR(255) DEFAULT ''", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "unique" => array( + "dest" => array("dest") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "quarantine" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "qid" => "VARCHAR(30) NOT NULL", + "subject" => "VARCHAR(500)", + "score" => "FLOAT(8,2)", + "ip" => "VARCHAR(50)", + "action" => "CHAR(20) NOT NULL DEFAULT 'unknown'", + "symbols" => "JSON", + "fuzzy_hashes" => "JSON", + "sender" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'", + "rcpt" => "VARCHAR(255)", + "msg" => "LONGTEXT", + "domain" => "VARCHAR(255)", + "notified" => "TINYINT(1) NOT NULL DEFAULT '0'", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "user" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'", + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "mailbox" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "password" => "VARCHAR(255) NOT NULL", + "name" => "VARCHAR(255)", + "description" => "VARCHAR(255)", + // mailbox_path_prefix is followed by domain/local_part/ + "mailbox_path_prefix" => "VARCHAR(150) DEFAULT '/var/vmail/'", + "quota" => "BIGINT(20) NOT NULL DEFAULT '102400'", + "local_part" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "attributes" => "JSON", + "kind" => "VARCHAR(100) NOT NULL DEFAULT ''", + "multiple_bookings" => "INT NOT NULL DEFAULT -1", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ), + "key" => array( + "domain" => array("domain"), + "kind" => array("kind") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "tags_mailbox" => array( + "cols" => array( + "tag_name" => "VARCHAR(255) NOT NULL", + "username" => "VARCHAR(255) NOT NULL" + ), + "keys" => array( + "fkey" => array( + "fk_tags_mailbox" => array( + "col" => "username", + "ref" => "mailbox.username", + "delete" => "CASCADE", + "update" => "NO ACTION" + ) + ), + "unique" => array( + "tag_name" => array("tag_name", "username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sieve_filters" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "username" => "VARCHAR(255) NOT NULL", + "script_desc" => "VARCHAR(255) NOT NULL", + "script_name" => "ENUM('active','inactive')", + "script_data" => "TEXT NOT NULL", + "filter_type" => "ENUM('postfilter','prefilter')", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "username" => array("username"), + "script_desc" => array("script_desc") + ), + "fkey" => array( + "fk_username_sieve_global_before" => array( + "col" => "username", + "ref" => "mailbox.username", + "delete" => "CASCADE", + "update" => "NO ACTION" + ) + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "app_passwd" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "name" => "VARCHAR(255) NOT NULL", + "mailbox" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "password" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "imap_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "smtp_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "dav_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "eas_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "pop3_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "sieve_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "mailbox" => array("mailbox"), + "password" => array("password"), + "domain" => array("domain"), + ), + "fkey" => array( + "fk_username_app_passwd" => array( + "col" => "mailbox", + "ref" => "mailbox.username", + "delete" => "CASCADE", + "update" => "NO ACTION" + ) + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "user_acl" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "spam_alias" => "TINYINT(1) NOT NULL DEFAULT '1'", + "tls_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", + "spam_score" => "TINYINT(1) NOT NULL DEFAULT '1'", + "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", + "delimiter_action" => "TINYINT(1) NOT NULL DEFAULT '1'", + "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '0'", + "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'", + "sogo_profile_reset" => "TINYINT(1) NOT NULL DEFAULT '0'", + "pushover" => "TINYINT(1) NOT NULL DEFAULT '1'", + // quarantine is for quarantine actions, todo: rename + "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'", + "quarantine_attachments" => "TINYINT(1) NOT NULL DEFAULT '1'", + "quarantine_notification" => "TINYINT(1) NOT NULL DEFAULT '1'", + "quarantine_category" => "TINYINT(1) NOT NULL DEFAULT '1'", + "app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'", + ), + "keys" => array( + "primary" => array( + "" => array("username") + ), + "fkey" => array( + "fk_username" => array( + "col" => "username", + "ref" => "mailbox.username", + "delete" => "CASCADE", + "update" => "NO ACTION" + ) + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "alias_domain" => array( + "cols" => array( + "alias_domain" => "VARCHAR(255) NOT NULL", + "target_domain" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("alias_domain") + ), + "key" => array( + "active" => array("active"), + "target_domain" => array("target_domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "spamalias" => array( + "cols" => array( + "address" => "VARCHAR(255) NOT NULL", + "goto" => "TEXT NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "validity" => "INT(11)" + ), + "keys" => array( + "primary" => array( + "" => array("address") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "filterconf" => array( + "cols" => array( + "object" => "VARCHAR(255) NOT NULL DEFAULT ''", + "option" => "VARCHAR(50) NOT NULL DEFAULT ''", + "value" => "VARCHAR(100) NOT NULL DEFAULT ''", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "prefid" => "INT(11) NOT NULL AUTO_INCREMENT" + ), + "keys" => array( + "primary" => array( + "" => array("prefid") + ), + "key" => array( + "object" => array("object") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "settingsmap" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "desc" => "VARCHAR(255) NOT NULL", + "content" => "LONGTEXT NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "logs" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "task" => "CHAR(32) NOT NULL DEFAULT '000000'", + "type" => "VARCHAR(32) DEFAULT ''", + "msg" => "TEXT", + "call" => "TEXT", + "user" => "VARCHAR(64) NOT NULL", + "role" => "VARCHAR(32) NOT NULL", + "remote" => "VARCHAR(39) NOT NULL", + "time" => "INT(11) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sasl_log" => array( + "cols" => array( + "service" => "VARCHAR(32) NOT NULL DEFAULT ''", + "app_password" => "INT", + "username" => "VARCHAR(255) NOT NULL", + "real_rip" => "VARCHAR(64) NOT NULL", + "datetime" => "DATETIME(0) NOT NULL DEFAULT NOW(0)" + ), + "keys" => array( + "primary" => array( + "" => array("service", "real_rip", "username") + ), + "key" => array( + "username" => array("username"), + "service" => array("service"), + "datetime" => array("datetime"), + "real_rip" => array("real_rip") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "quota2" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "bytes" => "BIGINT(20) NOT NULL DEFAULT '0'", + "messages" => "BIGINT(20) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "quota2replica" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "bytes" => "BIGINT(20) NOT NULL DEFAULT '0'", + "messages" => "BIGINT(20) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "domain_admins" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "username" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "username" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "da_acl" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'", + "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'", + "login_as" => "TINYINT(1) NOT NULL DEFAULT '1'", + "sogo_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'", + "bcc_maps" => "TINYINT(1) NOT NULL DEFAULT '1'", + "pushover" => "TINYINT(1) NOT NULL DEFAULT '0'", + "filters" => "TINYINT(1) NOT NULL DEFAULT '1'", + "ratelimit" => "TINYINT(1) NOT NULL DEFAULT '1'", + "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", + "extend_sender_acl" => "TINYINT(1) NOT NULL DEFAULT '0'", + "unlimited_quota" => "TINYINT(1) NOT NULL DEFAULT '0'", + "protocol_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "smtp_ip_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "alias_domains" => "TINYINT(1) NOT NULL DEFAULT '0'", + "mailbox_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'", + "domain_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'", + "domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "da_sso" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "token" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + ), + "keys" => array( + "primary" => array( + "" => array("token", "created") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "imapsync" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "user2" => "VARCHAR(255) NOT NULL", + "host1" => "VARCHAR(255) NOT NULL", + "authmech1" => "ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN'", + "regextrans2" => "VARCHAR(255) DEFAULT ''", + "authmd51" => "TINYINT(1) NOT NULL DEFAULT 0", + "domain2" => "VARCHAR(255) NOT NULL DEFAULT ''", + "subfolder2" => "VARCHAR(255) NOT NULL DEFAULT ''", + "user1" => "VARCHAR(255) NOT NULL", + "password1" => "VARCHAR(255) NOT NULL", + "exclude" => "VARCHAR(500) NOT NULL DEFAULT ''", + "maxage" => "SMALLINT NOT NULL DEFAULT '0'", + "mins_interval" => "SMALLINT UNSIGNED NOT NULL DEFAULT '0'", + "maxbytespersecond" => "VARCHAR(50) NOT NULL DEFAULT '0'", + "port1" => "SMALLINT UNSIGNED NOT NULL", + "enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'", + "delete2duplicates" => "TINYINT(1) NOT NULL DEFAULT '1'", + "delete1" => "TINYINT(1) NOT NULL DEFAULT '0'", + "delete2" => "TINYINT(1) NOT NULL DEFAULT '0'", + "automap" => "TINYINT(1) NOT NULL DEFAULT '0'", + "skipcrossduplicates" => "TINYINT(1) NOT NULL DEFAULT '0'", + "custom_params" => "VARCHAR(512) NOT NULL DEFAULT ''", + "timeout1" => "SMALLINT NOT NULL DEFAULT '600'", + "timeout2" => "SMALLINT NOT NULL DEFAULT '600'", + "subscribeall" => "TINYINT(1) NOT NULL DEFAULT '1'", + "is_running" => "TINYINT(1) NOT NULL DEFAULT '0'", + "returned_text" => "LONGTEXT", + "last_run" => "TIMESTAMP NULL DEFAULT NULL", + "success" => "TINYINT(1) UNSIGNED DEFAULT NULL", + "exit_status" => "VARCHAR(50) DEFAULT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "bcc_maps" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "local_dest" => "VARCHAR(255) NOT NULL", + "bcc_dest" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "type" => "ENUM('sender','rcpt')", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "local_dest" => array("local_dest"), + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "recipient_maps" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "old_dest" => "VARCHAR(255) NOT NULL", + "new_dest" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "local_dest" => array("old_dest"), + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "tfa" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "key_id" => "VARCHAR(255) NOT NULL", + "username" => "VARCHAR(255) NOT NULL", + "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')", + "secret" => "VARCHAR(255) DEFAULT NULL", + "keyHandle" => "VARCHAR(1023) DEFAULT NULL", + "publicKey" => "VARCHAR(4096) DEFAULT NULL", + "counter" => "INT NOT NULL DEFAULT '0'", + "certificate" => "TEXT", + "active" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "forwarding_hosts" => array( + "cols" => array( + "host" => "VARCHAR(255) NOT NULL", + "source" => "VARCHAR(255) NOT NULL", + "filter_spam" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("host") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_acl" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "c_folder_id" => "INT NOT NULL", + "c_object" => "VARCHAR(255) NOT NULL", + "c_uid" => "VARCHAR(255) NOT NULL", + "c_role" => "VARCHAR(80) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "sogo_acl_c_folder_id_idx" => array("c_folder_id"), + "sogo_acl_c_uid_idx" => array("c_uid") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_alarms_folder" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "c_path" => "VARCHAR(255) NOT NULL", + "c_name" => "VARCHAR(255) NOT NULL", + "c_uid" => "VARCHAR(255) NOT NULL", + "c_recurrence_id" => "INT(11) DEFAULT NULL", + "c_alarm_number" => "INT(11) NOT NULL", + "c_alarm_date" => "INT(11) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_cache_folder" => array( + "cols" => array( + "c_uid" => "VARCHAR(255) NOT NULL", + "c_path" => "VARCHAR(255) NOT NULL", + "c_parent_path" => "VARCHAR(255) DEFAULT NULL", + "c_type" => "TINYINT(3) unsigned NOT NULL", + "c_creationdate" => "INT(11) NOT NULL", + "c_lastmodified" => "INT(11) NOT NULL", + "c_version" => "INT(11) NOT NULL DEFAULT '0'", + "c_deleted" => "TINYINT(4) NOT NULL DEFAULT '0'", + "c_content" => "LONGTEXT" + ), + "keys" => array( + "primary" => array( + "" => array("c_uid", "c_path") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_folder_info" => array( + "cols" => array( + "c_folder_id" => "BIGINT(20) unsigned NOT NULL AUTO_INCREMENT", + "c_path" => "VARCHAR(255) NOT NULL", + "c_path1" => "VARCHAR(255) NOT NULL", + "c_path2" => "VARCHAR(255) DEFAULT NULL", + "c_path3" => "VARCHAR(255) DEFAULT NULL", + "c_path4" => "VARCHAR(255) DEFAULT NULL", + "c_foldername" => "VARCHAR(255) NOT NULL", + "c_location" => "VARCHAR(2048) DEFAULT NULL", + "c_quick_location" => "VARCHAR(2048) DEFAULT NULL", + "c_acl_location" => "VARCHAR(2048) DEFAULT NULL", + "c_folder_type" => "VARCHAR(255) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("c_path") + ), + "unique" => array( + "c_folder_id" => array("c_folder_id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_quick_appointment" => array( + "cols" => array( + "c_folder_id" => "INT NOT NULL", + "c_name" => "VARCHAR(255) NOT NULL", + "c_uid" => "VARCHAR(1000) NOT NULL", + "c_startdate" => "INT", + "c_enddate" => "INT", + "c_cycleenddate" => "INT", + "c_title" => "VARCHAR(1000) NOT NULL", + "c_participants" => "TEXT", + "c_isallday" => "INT", + "c_iscycle" => "INT", + "c_cycleinfo" => "TEXT", + "c_classification" => "INT NOT NULL", + "c_isopaque" => "INT NOT NULL", + "c_status" => "INT NOT NULL", + "c_priority" => "INT", + "c_location" => "VARCHAR(255)", + "c_orgmail" => "VARCHAR(255)", + "c_partmails" => "TEXT", + "c_partstates" => "TEXT", + "c_category" => "VARCHAR(255)", + "c_sequence" => "INT", + "c_component" => "VARCHAR(10) NOT NULL", + "c_nextalarm" => "INT", + "c_description" => "TEXT" + ), + "keys" => array( + "primary" => array( + "" => array("c_folder_id", "c_name") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_quick_contact" => array( + "cols" => array( + "c_folder_id" => "INT NOT NULL", + "c_name" => "VARCHAR(255) NOT NULL", + "c_givenname" => "VARCHAR(255)", + "c_cn" => "VARCHAR(255)", + "c_sn" => "VARCHAR(255)", + "c_screenname" => "VARCHAR(255)", + "c_l" => "VARCHAR(255)", + "c_mail" => "TEXT", + "c_o" => "VARCHAR(500)", + "c_ou" => "VARCHAR(255)", + "c_telephonenumber" => "VARCHAR(255)", + "c_categories" => "VARCHAR(255)", + "c_component" => "VARCHAR(10) NOT NULL", + "c_hascertificate" => "INT4 DEFAULT 0" + ), + "keys" => array( + "primary" => array( + "" => array("c_folder_id", "c_name") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_sessions_folder" => array( + "cols" => array( + "c_id" => "VARCHAR(255) NOT NULL", + "c_value" => "VARCHAR(4096) NOT NULL", + "c_creationdate" => "INT(11) NOT NULL", + "c_lastseen" => "INT(11) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("c_id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_store" => array( + "cols" => array( + "c_folder_id" => "INT NOT NULL", + "c_name" => "VARCHAR(255) NOT NULL", + "c_content" => "MEDIUMTEXT NOT NULL", + "c_creationdate" => "INT NOT NULL", + "c_lastmodified" => "INT NOT NULL", + "c_version" => "INT NOT NULL", + "c_deleted" => "INT" + ), + "keys" => array( + "primary" => array( + "" => array("c_folder_id", "c_name") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "pushover" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "key" => "VARCHAR(255) NOT NULL", + "token" => "VARCHAR(255) NOT NULL", + "attributes" => "JSON", + "title" => "TEXT", + "text" => "TEXT", + "senders" => "TEXT", + "senders_regex" => "TEXT", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_user_profile" => array( + "cols" => array( + "c_uid" => "VARCHAR(255) NOT NULL", + "c_defaults" => "LONGTEXT", + "c_settings" => "LONGTEXT" + ), + "keys" => array( + "primary" => array( + "" => array("c_uid") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "oauth_clients" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "client_id" => "VARCHAR(80) NOT NULL", + "client_secret" => "VARCHAR(80)", + "redirect_uri" => "VARCHAR(2000)", + "grant_types" => "VARCHAR(80)", + "scope" => "VARCHAR(4000)", + "user_id" => "VARCHAR(80)" + ), + "keys" => array( + "primary" => array( + "" => array("client_id") + ), + "unique" => array( + "id" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "oauth_access_tokens" => array( + "cols" => array( + "access_token" => "VARCHAR(40) NOT NULL", + "client_id" => "VARCHAR(80) NOT NULL", + "user_id" => "VARCHAR(80)", + "expires" => "TIMESTAMP NOT NULL", + "scope" => "VARCHAR(4000)" + ), + "keys" => array( + "primary" => array( + "" => array("access_token") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "oauth_authorization_codes" => array( + "cols" => array( + "authorization_code" => "VARCHAR(40) NOT NULL", + "client_id" => "VARCHAR(80) NOT NULL", + "user_id" => "VARCHAR(80)", + "redirect_uri" => "VARCHAR(2000)", + "expires" => "TIMESTAMP NOT NULL", + "scope" => "VARCHAR(4000)", + "id_token" => "VARCHAR(1000)" + ), + "keys" => array( + "primary" => array( + "" => array("authorization_code") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "oauth_refresh_tokens" => array( + "cols" => array( + "refresh_token" => "VARCHAR(40) NOT NULL", + "client_id" => "VARCHAR(80) NOT NULL", + "user_id" => "VARCHAR(80)", + "expires" => "TIMESTAMP NOT NULL", + "scope" => "VARCHAR(4000)" + ), + "keys" => array( + "primary" => array( + "" => array("refresh_token") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ) + ); + + foreach ($tables as $table => $properties) { + // Migrate to quarantine + if ($table == 'quarantine') { + $stmt = $pdo->query("SHOW TABLES LIKE 'quarantaine'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $stmt = $pdo->query("SHOW TABLES LIKE 'quarantine'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results == 0) { + $pdo->query("RENAME TABLE `quarantaine` TO `quarantine`"); + } + } + } + + // Migrate tls_enforce_* options + if ($table == 'mailbox') { + $stmt = $pdo->query("SHOW TABLES LIKE 'mailbox'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE '%tls_enforce%'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $stmt = $pdo->query("SELECT `username`, `tls_enforce_in`, `tls_enforce_out` FROM `mailbox`"); + $tls_options_rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($tls_options_rows)) { + $tls_options[$row['username']] = array('tls_enforce_in' => $row['tls_enforce_in'], 'tls_enforce_out' => $row['tls_enforce_out']); + } + } + } + } + + $stmt = $pdo->query("SHOW TABLES LIKE '" . $table . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $stmt = $pdo->prepare("SELECT CONCAT('ALTER TABLE `', `table_schema`, '`.', `table_name`, ' DROP FOREIGN KEY ', `constraint_name`, ';') AS `FKEY_DROP` FROM `information_schema`.`table_constraints` + WHERE `constraint_type` = 'FOREIGN KEY' AND `table_name` = :table;"); + $stmt->execute(array(':table' => $table)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($rows)) { + $pdo->query($row['FKEY_DROP']); + } + foreach($properties['cols'] as $column => $type) { + $stmt = $pdo->query("SHOW COLUMNS FROM `" . $table . "` LIKE '" . $column . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results == 0) { + if (strpos($type, 'AUTO_INCREMENT') !== false) { + $type = $type . ' PRIMARY KEY '; + // Adding an AUTO_INCREMENT key, need to drop primary keys first, if exists + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $pdo->query("ALTER TABLE `" . $table . "` DROP PRIMARY KEY"); + } + } + $pdo->query("ALTER TABLE `" . $table . "` ADD `" . $column . "` " . $type); + } + else { + $pdo->query("ALTER TABLE `" . $table . "` MODIFY COLUMN `" . $column . "` " . $type); + } + } + foreach($properties['keys'] as $key_type => $key_content) { + if (strtolower($key_type) == 'primary') { + foreach ($key_content as $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + $is_drop = ($num_results != 0) ? "DROP PRIMARY KEY, " : ""; + $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD PRIMARY KEY (" . $fields . ")"); + } + } + if (strtolower($key_type) == 'key') { + foreach ($key_content as $key_name => $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = '" . $key_name . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + $is_drop = ($num_results != 0) ? "DROP INDEX `" . $key_name . "`, " : ""; + $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD KEY `" . $key_name . "` (" . $fields . ")"); + } + } + if (strtolower($key_type) == 'unique') { + foreach ($key_content as $key_name => $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = '" . $key_name . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + $is_drop = ($num_results != 0) ? "DROP INDEX `" . $key_name . "`, " : ""; + $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD UNIQUE KEY `" . $key_name . "` (" . $fields . ")"); + } + } + if (strtolower($key_type) == 'fkey') { + foreach ($key_content as $key_name => $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = '" . $key_name . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $pdo->query("ALTER TABLE `" . $table . "` DROP INDEX `" . $key_name . "`"); + } + @list($table_ref, $field_ref) = explode('.', $key_values['ref']); + $pdo->query("ALTER TABLE `" . $table . "` ADD FOREIGN KEY `" . $key_name . "` (" . $key_values['col'] . ") REFERENCES `" . $table_ref . "` (`" . $field_ref . "`) + ON DELETE " . $key_values['delete'] . " ON UPDATE " . $key_values['update']); + } + } + } + // Drop all vanished columns + $stmt = $pdo->query("SHOW COLUMNS FROM `" . $table . "`"); + $cols_in_table = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($cols_in_table)) { + if (!array_key_exists($row['Field'], $properties['cols'])) { + $pdo->query("ALTER TABLE `" . $table . "` DROP COLUMN `" . $row['Field'] . "`;"); + } + } + + // Step 1: Get all non-primary keys, that currently exist and those that should exist + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE `Key_name` != 'PRIMARY'"); + $keys_in_table = $stmt->fetchAll(PDO::FETCH_ASSOC); + $keys_to_exist = array(); + if (isset($properties['keys']['unique']) && is_array($properties['keys']['unique'])) { + foreach ($properties['keys']['unique'] as $key_name => $key_values) { + $keys_to_exist[] = $key_name; + } + } + if (isset($properties['keys']['key']) && is_array($properties['keys']['key'])) { + foreach ($properties['keys']['key'] as $key_name => $key_values) { + $keys_to_exist[] = $key_name; + } + } + // Index for foreign key must exist + if (isset($properties['keys']['fkey']) && is_array($properties['keys']['fkey'])) { + foreach ($properties['keys']['fkey'] as $key_name => $key_values) { + $keys_to_exist[] = $key_name; + } + } + // Step 2: Drop all vanished indexes + while ($row = array_shift($keys_in_table)) { + if (!in_array($row['Key_name'], $keys_to_exist)) { + $pdo->query("ALTER TABLE `" . $table . "` DROP INDEX `" . $row['Key_name'] . "`"); + } + } + // Step 3: Drop all vanished primary keys + if (!isset($properties['keys']['primary'])) { + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $pdo->query("ALTER TABLE `" . $table . "` DROP PRIMARY KEY"); + } + } + } + else { + // Create table if it is missing + $sql = "CREATE TABLE IF NOT EXISTS `" . $table . "` ("; + foreach($properties['cols'] as $column => $type) { + $sql .= "`" . $column . "` " . $type . ","; + } + foreach($properties['keys'] as $key_type => $key_content) { + if (strtolower($key_type) == 'primary') { + foreach ($key_content as $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $sql .= "PRIMARY KEY (" . $fields . ")" . ","; + } + } + elseif (strtolower($key_type) == 'key') { + foreach ($key_content as $key_name => $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $sql .= "KEY `" . $key_name . "` (" . $fields . ")" . ","; + } + } + elseif (strtolower($key_type) == 'unique') { + foreach ($key_content as $key_name => $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $sql .= "UNIQUE KEY `" . $key_name . "` (" . $fields . ")" . ","; + } + } + elseif (strtolower($key_type) == 'fkey') { + foreach ($key_content as $key_name => $key_values) { + @list($table_ref, $field_ref) = explode('.', $key_values['ref']); + $sql .= "FOREIGN KEY `" . $key_name . "` (" . $key_values['col'] . ") REFERENCES `" . $table_ref . "` (`" . $field_ref . "`) + ON DELETE " . $key_values['delete'] . " ON UPDATE " . $key_values['update'] . ","; + } + } + } + $sql = rtrim($sql, ","); + $sql .= ") " . $properties['attr']; + $pdo->query($sql); + } + // Reset table attributes + $pdo->query("ALTER TABLE `" . $table . "` " . $properties['attr'] . ";"); + + } + + // Recreate SQL views + foreach ($views as $view => $create) { + $pdo->query("DROP VIEW IF EXISTS `" . $view . "`;"); + $pdo->query($create); + } + + // Mitigate imapsync argument injection issue + $pdo->query("UPDATE `imapsync` SET `custom_params` = '' + WHERE `custom_params` LIKE '%pipemess%' + OR custom_params LIKE '%skipmess%' + OR custom_params LIKE '%delete2foldersonly%' + OR custom_params LIKE '%delete2foldersbutnot%' + OR custom_params LIKE '%regexflag%' + OR custom_params LIKE '%pipemess%' + OR custom_params LIKE '%regextrans2%' + OR custom_params LIKE '%maxlinelengthcmd%';"); + + // Migrate webauthn tfa + $stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')"); + + // Inject admin if not exists + $stmt = $pdo->query("SELECT NULL FROM `admin`"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results == 0) { + $pdo->query("INSERT INTO `admin` (`username`, `password`, `superadmin`, `created`, `modified`, `active`) + VALUES ('admin', '{SSHA256}K8eVJ6YsZbQCfuJvSUbaQRLr0HPLz5rC9IAp0PAFl0tmNDBkMDc0NDAyOTAxN2Rk', 1, NOW(), NOW(), 1)"); + $pdo->query("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) + SELECT `username`, 'ALL', NOW(), 1 FROM `admin` + WHERE superadmin='1' AND `username` NOT IN (SELECT `username` FROM `domain_admins`);"); + $pdo->query("DELETE FROM `admin` WHERE `username` NOT IN (SELECT `username` FROM `domain_admins`);"); + } + // Insert new DB schema version + $pdo->query("REPLACE INTO `versions` (`application`, `version`) VALUES ('db_schema', '" . $db_version . "');"); + + // Fix dangling domain admins + $pdo->query("DELETE FROM `admin` WHERE `superadmin` = 0 AND `username` NOT IN (SELECT `username`FROM `domain_admins`);"); + $pdo->query("DELETE FROM `da_acl` WHERE `username` NOT IN (SELECT `username`FROM `domain_admins`);"); + + // Migrate attributes + // pushover + $pdo->query("UPDATE `pushover` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;"); + $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.evaluate_x_prio', \"0\") WHERE JSON_VALUE(`attributes`, '$.evaluate_x_prio') IS NULL;"); + $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.only_x_prio', \"0\") WHERE JSON_VALUE(`attributes`, '$.only_x_prio') IS NULL;"); + $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.sound', \"pushover\") WHERE JSON_VALUE(`attributes`, '$.sound') IS NULL;"); + // mailbox + $pdo->query("UPDATE `mailbox` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.passwd_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.passwd_update') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.relayhost', \"0\") WHERE JSON_VALUE(`attributes`, '$.relayhost') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_pw_update') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sieve_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sieve_access') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sogo_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.imap_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.imap_access') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.pop3_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.pop3_access') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.smtp_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.smtp_access') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.mailbox_format', \"maildir:\") WHERE JSON_VALUE(`attributes`, '$.mailbox_format') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_notification', \"never\") WHERE JSON_VALUE(`attributes`, '$.quarantine_notification') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_category', \"reject\") WHERE JSON_VALUE(`attributes`, '$.quarantine_category') IS NULL;"); + foreach($tls_options as $tls_user => $tls_options) { + $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', :tls_enforce_in), + `attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', :tls_enforce_out) + WHERE `username` = :username"); + $stmt->execute(array(':tls_enforce_in' => $tls_options['tls_enforce_in'], ':tls_enforce_out' => $tls_options['tls_enforce_out'], ':username' => $tls_user)); + } + // Set tls_enforce_* if still missing (due to deleted attrs for example) + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', \"1\") WHERE JSON_VALUE(`attributes`, '$.tls_enforce_out') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', \"1\") WHERE JSON_VALUE(`attributes`, '$.tls_enforce_in') IS NULL;"); + // Fix ACL + $pdo->query("INSERT INTO `user_acl` (`username`) SELECT `username` FROM `mailbox` WHERE `kind` = '' AND NOT EXISTS (SELECT `username` FROM `user_acl`);"); + $pdo->query("INSERT INTO `da_acl` (`username`) SELECT DISTINCT `username` FROM `domain_admins` WHERE `username` != 'admin' AND NOT EXISTS (SELECT `username` FROM `da_acl`);"); + // Fix domain_admins + $pdo->query("DELETE FROM `domain_admins` WHERE `domain` = 'ALL';"); + + // add default templates + $default_domain_template = array( + "template" => "Default", + "type" => "domain", + "attributes" => array( + "tags" => array(), + "max_num_aliases_for_domain" => 400, + "max_num_mboxes_for_domain" => 10, + "def_quota_for_mbox" => 3072 * 1048576, + "max_quota_for_mbox" => 10240 * 1048576, + "max_quota_for_domain" => 10240 * 1048576, + "rl_frame" => "s", + "rl_value" => "", + "active" => 1, + "gal" => 1, + "backupmx" => 0, + "relay_all_recipients" => 0, + "relay_unknown_only" => 0, + "dkim_selector" => "dkim", + "key_size" => 2048, + "max_quota_for_domain" => 10240 * 1048576, + ) + ); + $default_mailbox_template = array( + "template" => "Default", + "type" => "mailbox", + "attributes" => array( + "tags" => array(), + "quota" => 0, + "quarantine_notification" => strval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['quarantine_notification']), + "quarantine_category" => strval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['quarantine_category']), + "rl_frame" => "s", + "rl_value" => "", + "force_pw_update" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['force_pw_update']), + "sogo_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sogo_access']), + "active" => 1, + "tls_enforce_in" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_in']), + "tls_enforce_out" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['tls_enforce_out']), + "imap_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['imap_access']), + "pop3_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['pop3_access']), + "smtp_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['smtp_access']), + "sieve_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sieve_access']), + "acl_spam_alias" => 1, + "acl_tls_policy" => 1, + "acl_spam_score" => 1, + "acl_spam_policy" => 1, + "acl_delimiter_action" => 1, + "acl_syncjobs" => 0, + "acl_eas_reset" => 1, + "acl_sogo_profile_reset" => 0, + "acl_pushover" => 1, + "acl_quarantine" => 1, + "acl_quarantine_attachments" => 1, + "acl_quarantine_notification" => 1, + "acl_quarantine_category" => 1, + "acl_app_passwds" => 1, + ) + ); + $stmt = $pdo->prepare("SELECT id FROM `templates` WHERE `type` = :type AND `template` = :template"); + $stmt->execute(array( + ":type" => "domain", + ":template" => $default_domain_template["template"] + )); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (empty($row)){ + $stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`) + VALUES (:type, :template, :attributes)"); + $stmt->execute(array( + ":type" => "domain", + ":template" => $default_domain_template["template"], + ":attributes" => json_encode($default_domain_template["attributes"]) + )); + } + $stmt = $pdo->prepare("SELECT id FROM `templates` WHERE `type` = :type AND `template` = :template"); + $stmt->execute(array( + ":type" => "mailbox", + ":template" => $default_mailbox_template["template"] + )); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (empty($row)){ + $stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`) + VALUES (:type, :template, :attributes)"); + $stmt->execute(array( + ":type" => "mailbox", + ":template" => $default_mailbox_template["template"], + ":attributes" => json_encode($default_mailbox_template["attributes"]) + )); + } + + if (php_sapi_name() == "cli") { + echo "DB initialization completed" . PHP_EOL; + } else { + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__), + 'msg' => 'db_init_complete' + ); + } + } + catch (PDOException $e) { + if (php_sapi_name() == "cli") { + echo "DB initialization failed: " . print_r($e, true) . PHP_EOL; + } else { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__), + 'msg' => array('mysql_error', $e) + ); + } + } +} +if (php_sapi_name() == "cli") { + include '/web/inc/vars.inc.php'; + include '/web/inc/functions.docker.inc.php'; + // $now = new DateTime(); + // $mins = $now->getOffset() / 60; + // $sgn = ($mins < 0 ? -1 : 1); + // $mins = abs($mins); + // $hrs = floor($mins / 60); + // $mins -= $hrs * 60; + // $offset = sprintf('%+d:%02d', $hrs*$sgn, $mins); + $dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name; + $opt = [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + //PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '" . $offset . "', group_concat_max_len = 3423543543;", + ]; + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); + $stmt = $pdo->query("SELECT COUNT('OK') AS OK_C FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view' OR TABLE_NAME = '_sogo_static_view';"); + $res = $stmt->fetch(PDO::FETCH_ASSOC); + if (intval($res['OK_C']) === 2) { + // Be more precise when replacing into _sogo_static_view, col orders may change + try { + $stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`) + SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view"); + $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');"); + echo "Fixed _sogo_static_view" . PHP_EOL; + } + catch ( Exception $e ) { + // Dunno + } + } + try { + $m = new Memcached(); + $m->addServer('memcached', 11211); + $m->flush(); + echo "Cleaned up memcached". PHP_EOL; + } + catch ( Exception $e ) { + // Dunno + } + init_db_schema(); +} diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index 754d98d5..b3b1cc13 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -234,7 +234,7 @@ if (!isset($_SESSION['mailcow_locale']) && !isset($_COOKIE['mailcow_locale'])) { // Try suggest match // e.g. suggest en-gb when only en-us is provided - if (!isset($_COOKIE['mailcow_locale'])) { + if (!isset($_SESSION['mailcow_locale'])) { foreach ($lang2pref as $lang => $q) { if (array_key_exists(substr($lang, 0, 2), $AVAILABLE_BASE_LANGUAGES)) { $_SESSION['mailcow_locale'] = $AVAILABLE_BASE_LANGUAGES[substr($lang, 0, 2)]; diff --git a/data/web/inc/sessions.inc.php b/data/web/inc/sessions.inc.php index 5c7ec710..1a33e760 100644 --- a/data/web/inc/sessions.inc.php +++ b/data/web/inc/sessions.inc.php @@ -1,140 +1,140 @@ - $SESSION_LIFETIME)) { - session_unset(); - session_destroy(); -} -$_SESSION['LAST_ACTIVITY'] = time(); - -// API -if (!empty($_SERVER['HTTP_X_API_KEY'])) { - $stmt = $pdo->prepare("SELECT * FROM `api` WHERE `api_key` = :api_key AND `active` = '1';"); - $stmt->execute(array( - ':api_key' => preg_replace('/[^a-zA-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY']) - )); - $api_return = $stmt->fetch(PDO::FETCH_ASSOC); - if (!empty($api_return['api_key'])) { - $skip_ip_check = ($api_return['skip_ip_check'] == 1); - $remote = get_remote_ip(false); - $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from'])); - if ($skip_ip_check === true || ip_acl($remote, $allow_from)) { - $_SESSION['mailcow_cc_username'] = 'API'; - $_SESSION['mailcow_cc_role'] = 'admin'; - $_SESSION['mailcow_cc_api'] = true; - if ($api_return['access'] == 'rw') { - $_SESSION['mailcow_cc_api_access'] = 'rw'; - } - else { - $_SESSION['mailcow_cc_api_access'] = 'ro'; - } - } - else { - $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); - error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); - http_response_code(401); - echo json_encode(array( - 'type' => 'error', - 'msg' => 'api access denied for ip ' . $_SERVER['REMOTE_ADDR'] - )); - unset($_POST); - exit(); - } - } - else { - $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); - error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); - http_response_code(401); - echo json_encode(array( - 'type' => 'error', - 'msg' => 'authentication failed' - )); - unset($_POST); - exit(); - } -} - -// Handle logouts -if (isset($_POST["logout"])) { - if (isset($_SESSION["dual-login"])) { - $_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"]; - $_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"]; - unset($_SESSION["dual-login"]); - header("Location: /mailbox"); - exit(); - } - else { - session_regenerate_id(true); - session_unset(); - session_destroy(); - session_write_close(); - header("Location: /"); - } -} - -// Check session -function session_check() { - if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) { - return true; - } - if (!isset($_SESSION['SESS_REMOTE_UA']) || ($_SESSION['SESS_REMOTE_UA'] != $_SERVER['HTTP_USER_AGENT'])) { - $_SESSION['return'][] = array( - 'type' => 'warning', - 'msg' => 'session_ua' - ); - return false; - } - if (!empty($_POST)) { - if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) { - $_SESSION['return'][] = array( - 'type' => 'warning', - 'msg' => 'session_token' - ); - return false; - } - unset($_POST['csrf_token']); - $_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32)); - $_SESSION['CSRF']['TIME'] = time(); - } - return true; -} - -if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) { - $_POST = array(); - $_FILES = array(); -} + $SESSION_LIFETIME)) { + session_unset(); + session_destroy(); +} +$_SESSION['LAST_ACTIVITY'] = time(); + +// API +if (!empty($_SERVER['HTTP_X_API_KEY'])) { + $stmt = $pdo->prepare("SELECT * FROM `api` WHERE `api_key` = :api_key AND `active` = '1';"); + $stmt->execute(array( + ':api_key' => preg_replace('/[^a-zA-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY']) + )); + $api_return = $stmt->fetch(PDO::FETCH_ASSOC); + if (!empty($api_return['api_key'])) { + $skip_ip_check = ($api_return['skip_ip_check'] == 1); + $remote = get_remote_ip(false); + $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from'])); + if ($skip_ip_check === true || ip_acl($remote, $allow_from)) { + $_SESSION['mailcow_cc_username'] = 'API'; + $_SESSION['mailcow_cc_role'] = 'admin'; + $_SESSION['mailcow_cc_api'] = true; + if ($api_return['access'] == 'rw') { + $_SESSION['mailcow_cc_api_access'] = 'rw'; + } + else { + $_SESSION['mailcow_cc_api_access'] = 'ro'; + } + } + else { + $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); + error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); + http_response_code(401); + echo json_encode(array( + 'type' => 'error', + 'msg' => 'api access denied for ip ' . $_SERVER['REMOTE_ADDR'] + )); + unset($_POST); + exit(); + } + } + else { + $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); + error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); + http_response_code(401); + echo json_encode(array( + 'type' => 'error', + 'msg' => 'authentication failed' + )); + unset($_POST); + exit(); + } +} + +// Handle logouts +if (isset($_POST["logout"])) { + if (isset($_SESSION["dual-login"])) { + $_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"]; + $_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"]; + unset($_SESSION["dual-login"]); + header("Location: /mailbox"); + exit(); + } + else { + session_regenerate_id(true); + session_unset(); + session_destroy(); + session_write_close(); + header("Location: /"); + } +} + +// Check session +function session_check() { + if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) { + return true; + } + if (!isset($_SESSION['SESS_REMOTE_UA']) || ($_SESSION['SESS_REMOTE_UA'] != $_SERVER['HTTP_USER_AGENT'])) { + $_SESSION['return'][] = array( + 'type' => 'warning', + 'msg' => 'session_ua' + ); + return false; + } + if (!empty($_POST)) { + if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) { + $_SESSION['return'][] = array( + 'type' => 'warning', + 'msg' => 'session_token' + ); + return false; + } + unset($_POST['csrf_token']); + $_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32)); + $_SESSION['CSRF']['TIME'] = time(); + } + return true; +} + +if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) { + $_POST = array(); + $_FILES = array(); +} diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index aec043e9..fde1507f 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -1,4 +1,15 @@ <'col-sm-12 col-md-6'f>>" + - "<'row'<'col-sm-12'tr>>" + + "<'row dt-row'<'col-sm-12'tr>>" + "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", renderer: 'bootstrap' } ); @@ -15645,7 +15696,7 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu var classes = settings.oClasses; var lang = settings.oLanguage.oPaginate; var aria = settings.oLanguage.oAria.paginate || {}; - var btnDisplay, btnClass, counter=0; + var btnDisplay, btnClass; var attach = function( container, buttons ) { var i, ien, node, button; @@ -15714,7 +15765,7 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu 'href': '#', 'aria-controls': settings.sTableId, 'aria-label': aria[ button ], - 'data-dt-idx': counter, + 'data-dt-idx': button, 'tabindex': settings.iTabIndex, 'class': 'page-link' } ) @@ -15725,13 +15776,12 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu settings.oApi._fnBindAction( node, {action: button}, clickHandler ); - - counter++; } } } }; + var hostEl = $(host); // IE9 throws an 'unknown error' if document.activeElement is used // inside an iframe or frame. var activeEl; @@ -15741,17 +15791,26 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu // elements, focus is lost on the select button which is bad for // accessibility. So we want to restore focus once the draw has // completed - activeEl = $(host).find(document.activeElement).data('dt-idx'); + activeEl = hostEl.find(document.activeElement).data('dt-idx'); } catch (e) {} + var paginationEl = hostEl.children('ul.pagination'); + + if (paginationEl.length) { + paginationEl.empty(); + } + else { + paginationEl = hostEl.html('
    ').children('ul').addClass('pagination'); + } + attach( - $(host).empty().html('
      ').children('ul'), + paginationEl, buttons ); if ( activeEl !== undefined ) { - $(host).find( '[data-dt-idx='+activeEl+']' ).trigger('focus'); + hostEl.find('[data-dt-idx='+activeEl+']').trigger('focus'); } }; @@ -15760,14 +15819,54 @@ return DataTable; })); -/*! Responsive 2.3.0 +/*! Responsive 2.4.0 * 2014-2022 SpryMedia Ltd - datatables.net/license */ +(function( factory ){ + if ( typeof define === 'function' && define.amd ) { + // AMD + define( ['jquery', 'datatables.net'], function ( $ ) { + return factory( $, window, document ); + } ); + } + else if ( typeof exports === 'object' ) { + // CommonJS + module.exports = function (root, $) { + if ( ! root ) { + // CommonJS environments without a window global must pass a + // root. This will give an error otherwise + root = window; + } + + if ( ! $ ) { + $ = typeof window !== 'undefined' ? // jQuery's factory checks for a global window + require('jquery') : + require('jquery')( root ); + } + + if ( ! $.fn.dataTable ) { + require('datatables.net')(root, $); + } + + + return factory( $, root, root.document ); + }; + } + else { + // Browser + factory( jQuery, window, document ); + } +}(function( $, window, document, undefined ) { +'use strict'; +var DataTable = $.fn.dataTable; + + + /** * @summary Responsive * @description Responsive tables plug-in for DataTables - * @version 2.3.0 + * @version 2.4.0 * @author SpryMedia Ltd (www.sprymedia.co.uk) * @contact www.sprymedia.co.uk/contact * @copyright SpryMedia Ltd. @@ -15781,35 +15880,6 @@ return DataTable; * * For details please refer to: http://www.datatables.net */ -(function( factory ){ - if ( typeof define === 'function' && define.amd ) { - // AMD - define( ['jquery', 'datatables.net'], function ( $ ) { - return factory( $, window, document ); - } ); - } - else if ( typeof exports === 'object' ) { - // CommonJS - module.exports = function (root, $) { - if ( ! root ) { - root = window; - } - - if ( ! $ || ! $.fn.dataTable ) { - $ = require('datatables.net')(root, $).$; - } - - return factory( $, root, root.document ); - }; - } - else { - // Browser - factory( jQuery, window, document ); - } -}(function( $, window, document, undefined ) { -'use strict'; -var DataTable = $.fn.dataTable; - /** * Responsive is a plug-in for the DataTables library that makes use of @@ -15863,9 +15933,10 @@ var Responsive = function ( settings, opts ) { } this.s = { - dt: new DataTable.Api( settings ), + childNodeStore: {}, columns: [], - current: [] + current: [], + dt: new DataTable.Api( settings ) }; // Check if responsive has already been initialised on this table @@ -16070,6 +16141,63 @@ $.extend( Responsive.prototype, { * Private methods */ + /** + * Get and store nodes from a cell - use for node moving renderers + * + * @param {*} dt DT instance + * @param {*} row Row index + * @param {*} col Column index + */ + _childNodes: function( dt, row, col ) { + var name = row+'-'+col; + + if ( this.s.childNodeStore[ name ] ) { + return this.s.childNodeStore[ name ]; + } + + // https://jsperf.com/childnodes-array-slice-vs-loop + var nodes = []; + var children = dt.cell( row, col ).node().childNodes; + for ( var i=0, ien=children.length ; i