mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-11-11 02:41:33 +00:00
Compare commits
63
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a2ffca169 | ||
|
|
1a5095c0f4 | ||
|
|
2418da031f | ||
|
|
9825c8d290 | ||
|
|
dc880d861c | ||
|
|
a03fd8aaad | ||
|
|
e3a90718c2 | ||
|
|
54a85ba439 | ||
|
|
4ced82d554 | ||
|
|
f83e02b740 | ||
|
|
04c9413ed9 | ||
|
|
5e98d8e279 | ||
|
|
4caecf77e0 | ||
|
|
37d304d075 | ||
|
|
25086a9b3f | ||
|
|
7a3f1cbd41 | ||
|
|
616717bc0d | ||
|
|
6fd271ee64 | ||
|
|
7459cc046a | ||
|
|
58dad73eee | ||
|
|
b21acd4787 | ||
|
|
4ab954dcd1 | ||
|
|
eefc17274e | ||
|
|
4878840bdd | ||
|
|
869bcb3c42 | ||
|
|
0bcde13500 | ||
|
|
867b9dddab | ||
|
|
a4326e6642 | ||
|
|
c5e89a9e03 | ||
|
|
45d31b3279 | ||
|
|
24780ea5fb | ||
|
|
38b031e10c | ||
|
|
e2fb833ceb | ||
|
|
e27cb83326 | ||
|
|
f810cecd3a | ||
|
|
cfad223824 | ||
|
|
6d20463acb | ||
|
|
1a42c19050 | ||
|
|
b9f8eb4c31 | ||
|
|
ce4150f921 | ||
|
|
7cc8cac705 | ||
|
|
3fd1d017cf | ||
|
|
8597954f91 | ||
|
|
2b4ffe8808 | ||
|
|
1911847c3b | ||
|
|
497872080b | ||
|
|
a0dc3447f8 | ||
|
|
e0640949a2 | ||
|
|
bf744413bd | ||
|
|
262de81da1 | ||
|
|
5f13bb2046 | ||
|
|
0a0686ef19 | ||
|
|
24b91f6089 | ||
|
|
5705a48724 | ||
|
|
2f76230daf | ||
|
|
498434ee3d | ||
|
|
f662af4c50 | ||
|
|
8d4895a54d | ||
|
|
81d6b1f547 | ||
|
|
8db3cae826 | ||
|
|
d93f32072e | ||
|
|
7b658fa65c | ||
|
|
9f934af3f2 |
No files matched your search
@@ -1,76 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# _modules/scripts/backup.sh
|
|
||||||
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
|
|
||||||
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
|
|
||||||
|
|
||||||
# Backup with restic before the update when it is set up (target, confirmed password, initialized repository):
|
|
||||||
# interactive: asked for; --backup: without asking; --force: none (with --backup: without asking, a failure or a backup
|
|
||||||
# that cannot run stops the update; only "not set up" continues)
|
|
||||||
backup_failed() {
|
|
||||||
echo -e "\e[31m${1}\e[0m"
|
|
||||||
if [ "$FORCE" ]; then
|
|
||||||
echo "The update was started with --force --backup, it stops here. Nothing was changed."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
read -r -p "Continue the update without a backup? [y/N] " backup_response
|
|
||||||
if [[ ! "${backup_response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
|
||||||
echo "OK, exiting."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Whether to back up (asked with the other questions); the backup itself runs before the first change of the checkout
|
|
||||||
backup_decide() {
|
|
||||||
local output rc
|
|
||||||
# exported: update.sh restarts itself after a branch switch, the decision and a backup already made stay
|
|
||||||
if [ "$MAILCOW_UPDATE_BACKUP_DECIDED" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
export MAILCOW_UPDATE_BACKUP_DECIDED=y
|
|
||||||
export RUN_BACKUP=
|
|
||||||
if [ "$FORCE" ] && [ ! "$BACKUP" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [[ -z "$($COMPOSE_COMMAND ps -q restic-mailcow 2> /dev/null)" ]]; then
|
|
||||||
output="the backup container does not run"
|
|
||||||
rc=1
|
|
||||||
else
|
|
||||||
# exit code 0: set up, 2: not set up, others: error (e.g. credentials this container cannot read)
|
|
||||||
output=$($COMPOSE_COMMAND exec -T restic-mailcow python3 /usr/local/bin/restic_runner.py --configured 2>&1 < /dev/null)
|
|
||||||
rc=$?
|
|
||||||
fi
|
|
||||||
if [[ ${rc} -eq 2 ]]; then
|
|
||||||
[ "$BACKUP" ] && echo -e "\e[33mNo backup before the update: ${output}.\e[0m"
|
|
||||||
return 0
|
|
||||||
elif [[ ${rc} -ne 0 ]]; then
|
|
||||||
if [ "$BACKUP" ]; then
|
|
||||||
backup_failed "No backup before the update possible: ${output}"
|
|
||||||
elif [[ -n "$($COMPOSE_COMMAND ps -q restic-mailcow 2> /dev/null)" ]]; then
|
|
||||||
echo -e "\e[33mNo backup before the update: ${output}\e[0m"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if [ ! "$BACKUP" ]; then
|
|
||||||
read -r -p "Create a backup before the update? [Y/n] " backup_response
|
|
||||||
if [[ "${backup_response}" =~ ^([nN][oO]|[nN])$ ]]; then
|
|
||||||
echo "OK, no backup."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
RUN_BACKUP=y
|
|
||||||
}
|
|
||||||
|
|
||||||
# The backup chosen by backup_decide, once
|
|
||||||
backup_run() {
|
|
||||||
if [ ! "$RUN_BACKUP" ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
RUN_BACKUP=
|
|
||||||
if [[ "${RESTIC_BACKUP_MODE}" =~ ^(data|config)$ ]]; then
|
|
||||||
echo -e "\e[33mRESTIC_BACKUP_MODE=${RESTIC_BACKUP_MODE}: the backup holds no complete mailcow directory, a restore does not bring back this mailcow version.\e[0m"
|
|
||||||
fi
|
|
||||||
echo -e "\e[32mCreating a backup before the update...\e[0m"
|
|
||||||
if ! $COMPOSE_COMMAND exec -T restic-mailcow python3 /usr/local/bin/restic_runner.py --backup < /dev/null; then
|
|
||||||
backup_failed "The backup before the update failed, see the output above and the backup jobs in the mailcow UI."
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,11 @@
|
|||||||
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
|
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
|
||||||
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
|
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
|
||||||
|
|
||||||
|
# Global IPv6 address that is no unique local address (fc00::/7, e.g. Docker bridges)
|
||||||
|
_has_public_ipv6() {
|
||||||
|
ip -6 -o addr show scope global 2>/dev/null | awk '{print $4}' | grep -qviE '^f[cd]'
|
||||||
|
}
|
||||||
|
|
||||||
# 1) Check if the host supports IPv6
|
# 1) Check if the host supports IPv6
|
||||||
get_ipv6_support() {
|
get_ipv6_support() {
|
||||||
# ---- helper: probe external IPv6 connectivity without DNS ----
|
# ---- helper: probe external IPv6 connectivity without DNS ----
|
||||||
@@ -37,6 +42,9 @@ get_ipv6_support() {
|
|||||||
if _probe_ipv6_connectivity; then
|
if _probe_ipv6_connectivity; then
|
||||||
DETECTED_IPV6=true
|
DETECTED_IPV6=true
|
||||||
echo -e "IPv6 detected on host – ${LIGHT_GREEN}leaving IPv6 support enabled${YELLOW}.${NC}"
|
echo -e "IPv6 detected on host – ${LIGHT_GREEN}leaving IPv6 support enabled${YELLOW}.${NC}"
|
||||||
|
elif _has_public_ipv6; then
|
||||||
|
DETECTED_IPV6=true
|
||||||
|
echo -e "${YELLOW}External IPv6 connectivity test failed, but the host has a public IPv6 address – ${LIGHT_GREEN}leaving IPv6 support enabled${YELLOW}.${NC}"
|
||||||
else
|
else
|
||||||
DETECTED_IPV6=false
|
DETECTED_IPV6=false
|
||||||
echo -e "${YELLOW}Default IPv6 route present but external IPv6 connectivity failed – ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
|
echo -e "${YELLOW}Default IPv6 route present but external IPv6 connectivity failed – ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
|
||||||
@@ -192,9 +200,31 @@ EOF
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_set_enable_ipv6() {
|
||||||
|
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]]; then
|
||||||
|
if grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
|
||||||
|
sed -i "s/^ENABLE_IPV6=.*/ENABLE_IPV6=$1/" "$MAILCOW_CONF"
|
||||||
|
else
|
||||||
|
echo "ENABLE_IPV6=$1" >> "$MAILCOW_CONF"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
export IPV6_BOOL=$1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_warn_ipv6_disabled() {
|
||||||
|
_has_public_ipv6 || return 0
|
||||||
|
ip -6 route show default 2>/dev/null | grep -qE '^default' || return 0
|
||||||
|
grep -Eq '"userland-proxy"[[:space:]]*:[[:space:]]*false' /etc/docker/daemon.json 2>/dev/null && return 0
|
||||||
|
echo -e "${LIGHT_RED}WARNING: ENABLE_IPV6=false, but this host has a public IPv6 address.${NC}"
|
||||||
|
echo -e "${YELLOW}Docker forwards IPv6 connections to mailcow with an internal source address, which Postfix trusts. This can make your server an open relay over IPv6.${NC}"
|
||||||
|
echo -e "${YELLOW}Set ENABLE_IPV6=true in mailcow.conf, or set \"userland-proxy\": false in /etc/docker/daemon.json and restart Docker.${NC}"
|
||||||
|
echo -e "${YELLOW}See https://docs.mailcow.email/post_installation/firststeps-disable_ipv6/${NC}"
|
||||||
|
sleep 5
|
||||||
|
}
|
||||||
|
|
||||||
# 3) Main wrapper for generate_config.sh and update.sh
|
# 3) Main wrapper for generate_config.sh and update.sh
|
||||||
configure_ipv6() {
|
configure_ipv6() {
|
||||||
# detect manual override if mailcow.conf is present
|
|
||||||
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]] && grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
|
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]] && grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
|
||||||
MANUAL_SETTING=$(grep '^ENABLE_IPV6=' "$MAILCOW_CONF" | cut -d= -f2)
|
MANUAL_SETTING=$(grep '^ENABLE_IPV6=' "$MAILCOW_CONF" | cut -d= -f2)
|
||||||
elif [[ -z "$MAILCOW_CONF" ]] && [[ -n "${ENABLE_IPV6:-}" ]]; then
|
elif [[ -z "$MAILCOW_CONF" ]] && [[ -n "${ENABLE_IPV6:-}" ]]; then
|
||||||
@@ -203,19 +233,23 @@ configure_ipv6() {
|
|||||||
MANUAL_SETTING=""
|
MANUAL_SETTING=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${MANUAL_SETTING,,}" == "false" ]]; then
|
||||||
|
_set_enable_ipv6 false
|
||||||
|
echo "ENABLE_IPV6=false is set, skipping Docker IPv6 configuration."
|
||||||
|
_warn_ipv6_disabled
|
||||||
|
echo "IPv6 configuration complete: ENABLE_IPV6=false"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
get_ipv6_support
|
get_ipv6_support
|
||||||
|
|
||||||
# if user manually set it, check for mismatch
|
if [[ "$DETECTED_IPV6" != "true" && "${MANUAL_SETTING,,}" == "true" ]] && _has_public_ipv6; then
|
||||||
|
echo -e "${YELLOW}Keeping ENABLE_IPV6=true, the host has a public IPv6 address.${NC}"
|
||||||
|
DETECTED_IPV6=true
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$DETECTED_IPV6" != "true" ]]; then
|
if [[ "$DETECTED_IPV6" != "true" ]]; then
|
||||||
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]]; then
|
_set_enable_ipv6 false
|
||||||
if grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
|
|
||||||
sed -i 's/^ENABLE_IPV6=.*/ENABLE_IPV6=false/' "$MAILCOW_CONF"
|
|
||||||
else
|
|
||||||
echo "ENABLE_IPV6=false" >> "$MAILCOW_CONF"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
export IPV6_BOOL=false
|
|
||||||
fi
|
|
||||||
echo "Skipping Docker IPv6 configuration because host does not support IPv6."
|
echo "Skipping Docker IPv6 configuration because host does not support IPv6."
|
||||||
echo "Make sure to check if your docker daemon.json does not include \"enable_ipv6\": true if you do not want IPv6."
|
echo "Make sure to check if your docker daemon.json does not include \"enable_ipv6\": true if you do not want IPv6."
|
||||||
echo "IPv6 configuration complete: ENABLE_IPV6=false"
|
echo "IPv6 configuration complete: ENABLE_IPV6=false"
|
||||||
@@ -224,16 +258,7 @@ configure_ipv6() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
docker_daemon_edit
|
docker_daemon_edit
|
||||||
|
_set_enable_ipv6 true
|
||||||
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]]; then
|
|
||||||
if grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
|
|
||||||
sed -i 's/^ENABLE_IPV6=.*/ENABLE_IPV6=true/' "$MAILCOW_CONF"
|
|
||||||
else
|
|
||||||
echo "ENABLE_IPV6=true" >> "$MAILCOW_CONF"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
export IPV6_BOOL=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "IPv6 configuration complete: ENABLE_IPV6=true"
|
echo "IPv6 configuration complete: ENABLE_IPV6=true"
|
||||||
}
|
}
|
||||||
@@ -61,8 +61,6 @@ adapt_new_options() {
|
|||||||
"ACME_DNS_PROVIDER"
|
"ACME_DNS_PROVIDER"
|
||||||
"ACME_ACCOUNT_EMAIL"
|
"ACME_ACCOUNT_EMAIL"
|
||||||
"NGINX_MAX_BODY_SIZE"
|
"NGINX_MAX_BODY_SIZE"
|
||||||
"RESTIC_LOCAL_PATH"
|
|
||||||
"RESTIC_BACKUP_MODE"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
sed -i --follow-symlinks '$a\' mailcow.conf
|
sed -i --follow-symlinks '$a\' mailcow.conf
|
||||||
@@ -317,19 +315,6 @@ adapt_new_options() {
|
|||||||
echo '# Keep it above the message size limit of Postfix (100 MB by default). The mailcow UI and API stay limited to 64m.' >> mailcow.conf
|
echo '# Keep it above the message size limit of Postfix (100 MB by default). The mailcow UI and API stay limited to 64m.' >> mailcow.conf
|
||||||
echo 'NGINX_MAX_BODY_SIZE=128m' >> mailcow.conf
|
echo 'NGINX_MAX_BODY_SIZE=128m' >> mailcow.conf
|
||||||
;;
|
;;
|
||||||
RESTIC_LOCAL_PATH)
|
|
||||||
echo '# Folder on the host for backups to the target "local" (backup settings in the mailcow UI), as an absolute path,' >> mailcow.conf
|
|
||||||
echo '# e.g. a NAS mounted from another disk. The repository is created in a subfolder of it.' >> mailcow.conf
|
|
||||||
echo '# Empty: the Docker volume restic-repo-vol-1 of mailcow, on the same disk as the mailcow data.' >> mailcow.conf
|
|
||||||
echo 'RESTIC_LOCAL_PATH=' >> mailcow.conf
|
|
||||||
;;
|
|
||||||
RESTIC_BACKUP_MODE)
|
|
||||||
echo '# What the backups of the mailcow UI hold:' >> mailcow.conf
|
|
||||||
echo '# - all (default),' >> mailcow.conf
|
|
||||||
echo '# - data (vmail, vmail-index, database, redis, mail crypt keys and mailcow.conf)' >> mailcow.conf
|
|
||||||
echo '# - config (database, redis, mail crypt keys, mailcow.conf and data/conf).' >> mailcow.conf
|
|
||||||
echo 'RESTIC_BACKUP_MODE=all' >> mailcow.conf
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
echo "${option}=" >> mailcow.conf
|
echo "${option}=" >> mailcow.conf
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -159,9 +159,6 @@ async def post_containers(container_id : str, post_action : str, request: Reques
|
|||||||
api_call_method = getattr(dockerapi, 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"))
|
api_call_method = getattr(dockerapi, 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"))
|
||||||
|
|
||||||
dockerapi.logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id))
|
dockerapi.logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id))
|
||||||
if getattr(api_call_method, 'long_running', False):
|
|
||||||
# runs for minutes (e.g. sogo-tool of a backup restore): in a worker thread, the other api calls go on
|
|
||||||
return await asyncio.to_thread(api_call_method, request_json, container_id=container_id)
|
|
||||||
return api_call_method(request_json, container_id=container_id)
|
return api_call_method(request_json, container_id=container_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
dockerapi.logger.error("error - container_post: %s" % str(e))
|
dockerapi.logger.error("error - container_post: %s" % str(e))
|
||||||
|
|||||||
@@ -6,15 +6,9 @@ import time
|
|||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
import platform
|
import platform
|
||||||
import docker
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from fastapi import FastAPI, Response, Request
|
from fastapi import FastAPI, Response, Request
|
||||||
|
|
||||||
def long_running(method):
|
|
||||||
"""Marks an api call that runs for minutes: main.py runs it in a worker thread, the other api calls go on"""
|
|
||||||
method.long_running = True
|
|
||||||
return method
|
|
||||||
|
|
||||||
class DockerApi:
|
class DockerApi:
|
||||||
def __init__(self, redis_client, sync_docker_client, async_docker_client, logger):
|
def __init__(self, redis_client, sync_docker_client, async_docker_client, logger):
|
||||||
self.redis_client = redis_client
|
self.redis_client = redis_client
|
||||||
@@ -451,35 +445,6 @@ class DockerApi:
|
|||||||
return Response(content=json.dumps({ 'type': 'danger', 'msg': 'users required' }, indent=4), media_type="application/json")
|
return Response(content=json.dumps({ 'type': 'danger', 'msg': 'users required' }, indent=4), media_type="application/json")
|
||||||
init_return = container.exec_run(["/bin/bash", "-c", 'for user in "$@"; do sogo-tool init-user "$user" || exit 1; done', "init_user"] + users, user='sogo')
|
init_return = container.exec_run(["/bin/bash", "-c", 'for user in "$@"; do sogo-tool init-user "$user" || exit 1; done', "init_user"] + users, user='sogo')
|
||||||
return self.exec_run_handler('generic', init_return)
|
return self.exec_run_handler('generic', init_return)
|
||||||
# api call: container_post - post_action: exec - cmd: sogo - task: restore
|
|
||||||
# Resets calendars, address books and preferences of a user to the sogo-tool backup file <dir>/<username> in a
|
|
||||||
# restore batch of the backup (/restore/<job>/b<batch>/sogo_backup, only the sogo user can read it); values are
|
|
||||||
# checked and passed as arguments, never through the shell. Runs in a worker thread (main.py) with its own client.
|
|
||||||
@long_running
|
|
||||||
def container_post__exec__sogo__restore(self, request_json, **kwargs):
|
|
||||||
if 'container_id' in kwargs:
|
|
||||||
filters = {"id": kwargs['container_id']}
|
|
||||||
elif 'container_name' in kwargs:
|
|
||||||
filters = {"name": kwargs['container_name']}
|
|
||||||
|
|
||||||
directory = str(request_json.get('dir', ''))
|
|
||||||
username = str(request_json.get('username', ''))
|
|
||||||
if (not re.fullmatch(r'/restore/[0-9]+/b[0-9]+/sogo_backup', directory)
|
|
||||||
or not re.fullmatch(r'[A-Za-z0-9_][A-Za-z0-9._+-]*@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+', username)):
|
|
||||||
return Response(content=json.dumps({ 'type': 'danger', 'msg': 'dir and username required' }, indent=4), media_type="application/json")
|
|
||||||
# the folder: no symlink, uid 999, mode 0700; the file: plain, uid 999, mode 0600. sogo-tool restores folders and
|
|
||||||
# preferences in two runs, both stopped before the runner gives up waiting (30 minutes)
|
|
||||||
script = ('[ -d "$1" ] && [ ! -L "$1" ] && [ "$(realpath -e -- "$1")" = "$1" ] && [ "$(stat -c "%u %a" -- "$1")" = "999 700" ] '
|
|
||||||
'&& [ -f "$1/$2" ] && [ ! -L "$1/$2" ] && [ "$(stat -c "%u %a" -- "$1/$2")" = "999 600" ] '
|
|
||||||
'|| { echo "invalid directory or file"; exit 1; }; '
|
|
||||||
'timeout -k 30 850 sogo-tool restore -f ALL "$1" "$2" && exec timeout -k 30 850 sogo-tool restore -p "$1" "$2"')
|
|
||||||
client = docker.DockerClient(base_url='unix:///var/run/docker.sock', version='auto', timeout=1800)
|
|
||||||
try:
|
|
||||||
for container in client.containers.list(filters=filters):
|
|
||||||
restore_return = container.exec_run(["/bin/bash", "-c", script, "restore", directory, username], user='sogo')
|
|
||||||
return self.exec_run_handler('generic', restore_return)
|
|
||||||
finally:
|
|
||||||
client.close()
|
|
||||||
# api call: container_post - post_action: exec - cmd: doveadm - task: subscribe_shared
|
# api call: container_post - post_action: exec - cmd: doveadm - task: subscribe_shared
|
||||||
# Subscribes users to every folder of a mailbox shared with them (Shared/<owner>/...), or unsubscribes them
|
# Subscribes users to every folder of a mailbox shared with them (Shared/<owner>/...), or unsubscribes them
|
||||||
def container_post__exec__doveadm__subscribe_shared(self, request_json, **kwargs):
|
def container_post__exec__doveadm__subscribe_shared(self, request_json, **kwargs):
|
||||||
@@ -621,8 +586,8 @@ class DockerApi:
|
|||||||
if not user or id in ('user=', 'group='):
|
if not user or id in ('user=', 'group='):
|
||||||
return Response(content=json.dumps({ 'type': 'danger', 'msg': 'user and id required' }, indent=4), media_type="application/json")
|
return Response(content=json.dumps({ 'type': 'danger', 'msg': 'user and id required' }, indent=4), media_type="application/json")
|
||||||
action = 'doveadm acl set -u "$ACL_USER" -- "$folder" "$ACL_ID" $ACL_RIGHTS' if rights else 'doveadm acl delete -u "$ACL_USER" -- "$folder" "$ACL_ID"'
|
action = 'doveadm acl set -u "$ACL_USER" -- "$folder" "$ACL_ID" $ACL_RIGHTS' if rights else 'doveadm acl delete -u "$ACL_USER" -- "$folder" "$ACL_ID"'
|
||||||
script = ('set -o pipefail; doveadm mailbox list -u "$ACL_USER" | while IFS= read -r folder; do '
|
script = ('set -o pipefail; doveadm -f tab mailbox status -u "$ACL_USER" guid "*" | tail -n +2 | cut -f1 | while IFS= read -r folder; do '
|
||||||
'case "$folder" in Shared/*) continue;; esac; ' + action + ' || exit 1; done && doveadm acl recalc -u "$ACL_USER"')
|
'case "$folder" in Shared|Shared/*) continue;; esac; ' + action + ' || exit 1; done && doveadm acl recalc -u "$ACL_USER"')
|
||||||
acl_return = container.exec_run(["/bin/bash", "-c", script],
|
acl_return = container.exec_run(["/bin/bash", "-c", script],
|
||||||
environment={ 'ACL_USER': user, 'ACL_ID': id, 'ACL_RIGHTS': ' '.join(rights) })
|
environment={ 'ACL_USER': user, 'ACL_ID': id, 'ACL_RIGHTS': ' '.join(rights) })
|
||||||
return self.exec_run_handler('generic', acl_return)
|
return self.exec_run_handler('generic', acl_return)
|
||||||
|
|||||||
@@ -150,8 +150,6 @@ COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
|||||||
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
||||||
COPY imapsync /usr/local/bin/imapsync
|
COPY imapsync /usr/local/bin/imapsync
|
||||||
COPY imapsync_runner.py /usr/local/bin/imapsync_runner.py
|
COPY imapsync_runner.py /usr/local/bin/imapsync_runner.py
|
||||||
COPY retention_runner.py /usr/local/bin/retention_runner.py
|
|
||||||
COPY restore_runner.py /usr/local/bin/restore_runner.py
|
|
||||||
COPY report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve
|
COPY report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve
|
||||||
COPY report-ham.sieve /usr/lib/dovecot/sieve/report-ham.sieve
|
COPY report-ham.sieve /usr/lib/dovecot/sieve/report-ham.sieve
|
||||||
COPY rspamd-pipe-ham /usr/lib/dovecot/sieve/rspamd-pipe-ham
|
COPY rspamd-pipe-ham /usr/lib/dovecot/sieve/rspamd-pipe-ham
|
||||||
|
|||||||
@@ -281,8 +281,6 @@ chown root:tty /dev/console
|
|||||||
chmod +x /usr/lib/dovecot/sieve/rspamd-pipe-ham \
|
chmod +x /usr/lib/dovecot/sieve/rspamd-pipe-ham \
|
||||||
/usr/lib/dovecot/sieve/rspamd-pipe-spam \
|
/usr/lib/dovecot/sieve/rspamd-pipe-spam \
|
||||||
/usr/local/bin/imapsync_runner.py \
|
/usr/local/bin/imapsync_runner.py \
|
||||||
/usr/local/bin/retention_runner.py \
|
|
||||||
/usr/local/bin/restore_runner.py \
|
|
||||||
/usr/local/bin/imapsync \
|
/usr/local/bin/imapsync \
|
||||||
/usr/local/bin/trim_logs.sh \
|
/usr/local/bin/trim_logs.sh \
|
||||||
/usr/local/bin/sa-rules.sh \
|
/usr/local/bin/sa-rules.sh \
|
||||||
|
|||||||
@@ -1,712 +0,0 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
# Mail part of restores (ofelia, dovecot container, as vmail): the restic container restores a batch of mailboxes
|
|
||||||
# into /restore/<job id>/b<n> and sets the job to step 'mails'. Per mailbox this imports the mails folder by folder
|
|
||||||
# (into "Restored <snapshot time>" or only the missing ones into the original folders), adds IMAP ACLs and missing
|
|
||||||
# Sieve scripts, merges the results into the job and sets step 'mails_done'; the restic runner then removes the staging
|
|
||||||
# and goes on with the next batch. Nothing that exists is changed or deleted.
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import fcntl
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import smtplib
|
|
||||||
import stat
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
from email.message import EmailMessage
|
|
||||||
from email.utils import formatdate, make_msgid
|
|
||||||
import MySQLdb
|
|
||||||
import MySQLdb.cursors
|
|
||||||
|
|
||||||
lock_file = '/tmp/restore_runner.lock'
|
|
||||||
restore_dir = '/restore'
|
|
||||||
max_log = 1000000
|
|
||||||
# import, moves and the removal of the temporary folder ignore the quota (a restore may need more than is free),
|
|
||||||
# the quota is recalculated afterwards
|
|
||||||
no_quota = ['-o', 'plugin/quota=']
|
|
||||||
# uid ranges per import (one argument stays far below the 128 KiB limit), folders per fetch and status
|
|
||||||
uid_ranges = 2000
|
|
||||||
folder_batch = 200
|
|
||||||
# the restic runner waits for the batch while this heartbeat is fresh
|
|
||||||
heartbeat_interval = 30
|
|
||||||
# seconds to wait for the next batch of a restore
|
|
||||||
batch_wait = 120
|
|
||||||
rights_by_letter = {'l': 'lookup', 'r': 'read', 'w': 'write', 's': 'write-seen', 't': 'write-deleted', 'i': 'insert',
|
|
||||||
'p': 'post', 'e': 'expunge', 'k': 'create', 'x': 'delete', 'a': 'admin'}
|
|
||||||
components_here = {'mails', 'imap_acl', 'filters', 'sieve_activate'}
|
|
||||||
|
|
||||||
conn = None
|
|
||||||
|
|
||||||
|
|
||||||
def connect():
|
|
||||||
return MySQLdb.connect(unix_socket='/var/run/mysqld/mysqld.sock', user=os.environ['DBUSER'],
|
|
||||||
passwd=os.environ['DBPASS'], db=os.environ['DBNAME'], charset='utf8mb4', autocommit=True)
|
|
||||||
|
|
||||||
# Reconnect when the server dropped the connection (imports can outlast wait_timeout)
|
|
||||||
def query(sql, args=None):
|
|
||||||
global conn
|
|
||||||
try:
|
|
||||||
conn.ping()
|
|
||||||
except (AttributeError, MySQLdb.Error):
|
|
||||||
conn = connect()
|
|
||||||
cur = conn.cursor(MySQLdb.cursors.DictCursor)
|
|
||||||
cur.execute(sql, args)
|
|
||||||
return cur
|
|
||||||
|
|
||||||
# state_restore_seen while a batch runs, in its own connection
|
|
||||||
class Heartbeat(threading.Thread):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(daemon=True)
|
|
||||||
self.done = threading.Event()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
db = None
|
|
||||||
while not self.done.is_set():
|
|
||||||
try:
|
|
||||||
if db is None:
|
|
||||||
db = connect()
|
|
||||||
db.cursor().execute("""INSERT INTO backup_setting (name, value) VALUES ('state_restore_seen', %s)
|
|
||||||
ON DUPLICATE KEY UPDATE value = VALUES(value)""", (str(int(time.time())),))
|
|
||||||
except MySQLdb.Error:
|
|
||||||
db = None
|
|
||||||
self.done.wait(heartbeat_interval)
|
|
||||||
|
|
||||||
def log(job_id, text):
|
|
||||||
print('restore job %d: %s' % (job_id, text), flush=True)
|
|
||||||
line = '%s %s\n' % (time.strftime('%Y-%m-%d %H:%M:%S'), text)
|
|
||||||
query("UPDATE backup_job SET log = RIGHT(CONCAT(IFNULL(log, ''), %s), %s) WHERE id = %s", (line, max_log, job_id))
|
|
||||||
|
|
||||||
def save(job_id, result):
|
|
||||||
query("UPDATE backup_job SET result = %s WHERE id = %s", (json.dumps(result), job_id))
|
|
||||||
|
|
||||||
def doveadm(*args, data=None, check=True, partial=False):
|
|
||||||
result = subprocess.run(['doveadm', *args], input=data, stdin=None if data is not None else subprocess.DEVNULL,
|
|
||||||
capture_output=True, timeout=86400)
|
|
||||||
# mailbox status fails for folders that cannot be opened (implicit parents); the others are listed
|
|
||||||
if check and result.returncode != 0 and not (partial and result.stdout.strip()):
|
|
||||||
# mail_log writes an info line per message, the errors are the other lines
|
|
||||||
lines = [l for l in result.stderr.decode('utf-8', 'replace').splitlines() if ': Info: ' not in l]
|
|
||||||
raise RuntimeError('\n'.join(lines)[-500:] or 'doveadm exit code %d' % result.returncode)
|
|
||||||
return result.stdout.decode('utf-8', 'replace')
|
|
||||||
|
|
||||||
def doveadm_json(*args, partial=False):
|
|
||||||
out = doveadm('-f', 'json', *args, partial=partial)
|
|
||||||
return json.loads(out) if out.strip() else []
|
|
||||||
|
|
||||||
# free disk space that must stay after the import: max(2 GiB, 5 % of the filesystem), a full disk stops mail
|
|
||||||
# (disk_reserve, mib and count_errors as in restic_runner.py of the backup image; tests/python/backup compares them)
|
|
||||||
def disk_reserve(total):
|
|
||||||
return max(2 << 30, total // 20)
|
|
||||||
|
|
||||||
def import_space_ok(need, free, total):
|
|
||||||
return need * 11 // 10 + disk_reserve(total) <= free
|
|
||||||
|
|
||||||
def mib(size):
|
|
||||||
return '%.1f MiB' % (size / 1048576)
|
|
||||||
|
|
||||||
def component(errors=None):
|
|
||||||
return {'added': 0, 'skipped': 0, 'errors': errors or []}
|
|
||||||
|
|
||||||
# Folder names on disk: modified UTF-7, listescape writes . / \ as \xx
|
|
||||||
def mutf7_decode(text):
|
|
||||||
def utf16(m):
|
|
||||||
if not m.group(1):
|
|
||||||
return '&'
|
|
||||||
data = m.group(1).replace(',', '/')
|
|
||||||
return base64.b64decode(data + '=' * (-len(data) % 4)).decode('utf-16-be')
|
|
||||||
return re.sub(r'&([A-Za-z0-9+,]*)-', utf16, text)
|
|
||||||
|
|
||||||
def unescape(part):
|
|
||||||
return mutf7_decode(re.sub(r'\\([0-9a-fA-F]{2})', lambda m: chr(int(m.group(1), 16)), part))
|
|
||||||
|
|
||||||
# Maildir++: the maildir is INBOX, ".A.B" is A/B
|
|
||||||
def folder_name(entry):
|
|
||||||
return '/'.join(unescape(part) for part in entry[1:].split('.')) if entry else 'INBOX'
|
|
||||||
|
|
||||||
def is_dir(path):
|
|
||||||
try:
|
|
||||||
return stat.S_ISDIR(os.lstat(path).st_mode)
|
|
||||||
except OSError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def read_file(path):
|
|
||||||
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
|
|
||||||
with os.fdopen(fd, 'rb') as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
# Prefixes of the shared and public namespaces, their folders belong to others
|
|
||||||
def shared_prefixes():
|
|
||||||
prefixes, namespace, depth = {'Shared/'}, None, 0
|
|
||||||
# as vmail doveconf cannot read every included file (mail_crypt keys) and fails, but prints the namespaces
|
|
||||||
for line in subprocess.run(['doveconf', '-n'], capture_output=True, text=True).stdout.splitlines():
|
|
||||||
stripped = line.strip()
|
|
||||||
if stripped.endswith('{'):
|
|
||||||
depth += 1
|
|
||||||
if depth == 1 and stripped.startswith('namespace'):
|
|
||||||
namespace = {}
|
|
||||||
elif stripped == '}':
|
|
||||||
if depth == 1 and namespace is not None:
|
|
||||||
if namespace.get('type') in ('shared', 'public') and namespace.get('prefix'):
|
|
||||||
prefixes.add(namespace['prefix'].split('%')[0])
|
|
||||||
namespace = None
|
|
||||||
depth = max(depth - 1, 0)
|
|
||||||
elif depth == 1 and namespace is not None and '=' in stripped:
|
|
||||||
key, value = (part.strip() for part in stripped.split('=', 1))
|
|
||||||
namespace[key] = value
|
|
||||||
return tuple(prefix for prefix in prefixes if prefix)
|
|
||||||
|
|
||||||
# name => guid of the user's own folders (implicit parents have none); the names are looked up literally
|
|
||||||
def own_folders(user, prefixes):
|
|
||||||
names = [n for n in doveadm('mailbox', 'list', '-u', user).splitlines() if n and not n.startswith(prefixes)]
|
|
||||||
wanted, guids = set(names), {}
|
|
||||||
for i in range(0, len(names), folder_batch):
|
|
||||||
for row in doveadm_json('mailbox', 'status', '-u', user, 'guid', '--', *names[i:i + folder_batch], partial=True):
|
|
||||||
if row.get('mailbox') in wanted and re.fullmatch(r'[0-9a-f]{32}', row.get('guid') or ''):
|
|
||||||
guids[row['mailbox']] = row['guid']
|
|
||||||
return guids
|
|
||||||
|
|
||||||
def masked(user):
|
|
||||||
return '*' in user or '?' in user
|
|
||||||
|
|
||||||
def free_name(base, existing):
|
|
||||||
name, count = base, 1
|
|
||||||
while name in existing or any(folder.startswith(name + '/') for folder in existing):
|
|
||||||
count += 1
|
|
||||||
name = '%s (%d)' % (base, count)
|
|
||||||
return name
|
|
||||||
|
|
||||||
# Path of the maildir now (/var/vmail/<domain>/<local>/Maildir), None for unknown or inactive mailboxes
|
|
||||||
def mail_location(user):
|
|
||||||
result = subprocess.run(['doveadm', 'user', '-f', 'mail', '--', user], capture_output=True, text=True)
|
|
||||||
m = re.match(r'maildir:([^:]+)', result.stdout.strip()) if result.returncode == 0 else None
|
|
||||||
return m.group(1).rstrip('/') if m else None
|
|
||||||
|
|
||||||
# The restored maildir of a mailbox (may hold only ACL files), None if the snapshot has nothing of it
|
|
||||||
def staged_maildir(staging, home, location):
|
|
||||||
for path in (staging + location, home + '/' + os.environ.get('MAILDIR_SUB', 'Maildir'), home):
|
|
||||||
if is_dir(path):
|
|
||||||
return path
|
|
||||||
return None
|
|
||||||
|
|
||||||
def uid_chunks(uids):
|
|
||||||
ranges, start, prev = [], None, None
|
|
||||||
for uid in sorted(uids):
|
|
||||||
if start is None:
|
|
||||||
start = prev = uid
|
|
||||||
elif uid == prev + 1:
|
|
||||||
prev = uid
|
|
||||||
else:
|
|
||||||
ranges.append('%d:%d' % (start, prev) if prev != start else str(start))
|
|
||||||
start = prev = uid
|
|
||||||
if start is not None:
|
|
||||||
ranges.append('%d:%d' % (start, prev) if prev != start else str(start))
|
|
||||||
return [','.join(ranges[i:i + uid_ranges]) for i in range(0, len(ranges), uid_ranges)]
|
|
||||||
|
|
||||||
# Same message: same Message-ID, without one the same Date and size
|
|
||||||
def message_key(row):
|
|
||||||
message_id = re.sub(r'\s+', '', row.get('hdr.message-id') or '').strip('<>')
|
|
||||||
if message_id:
|
|
||||||
return 'id:' + message_id
|
|
||||||
return 'date:%s:%s' % (row.get('date.sent'), row.get('size.physical'))
|
|
||||||
|
|
||||||
def present_keys(user, guids):
|
|
||||||
keys = set()
|
|
||||||
guids = list(guids)
|
|
||||||
for i in range(0, len(guids), folder_batch):
|
|
||||||
search = []
|
|
||||||
for guid in guids[i:i + folder_batch]:
|
|
||||||
search += (['OR'] if search else []) + ['mailbox-guid', guid]
|
|
||||||
keys.update(message_key(row) for row in doveadm_json('fetch', '-u', user, 'hdr.message-id date.sent size.physical',
|
|
||||||
*search))
|
|
||||||
return keys
|
|
||||||
|
|
||||||
def location(path):
|
|
||||||
if ':' in path:
|
|
||||||
raise RuntimeError('unsupported path')
|
|
||||||
return 'maildir:%s:INDEX=MEMORY' % path
|
|
||||||
|
|
||||||
# The folders of a restored maildir as standalone maildirs (their messages are the INBOX there): the subfolders are
|
|
||||||
# moved out of the maildir, so every folder is imported without the name rules of the source (listescape)
|
|
||||||
def split_folders(maildir, home):
|
|
||||||
folders, failed = [('INBOX', maildir)], []
|
|
||||||
out = home + '/_restore_folders'
|
|
||||||
for entry in sorted(os.listdir(maildir)):
|
|
||||||
path = os.path.join(maildir, entry)
|
|
||||||
if not entry.startswith('.') or entry in ('.', '..') or not is_dir(path) or not is_dir(path + '/cur'):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
name = folder_name(entry)
|
|
||||||
except (ValueError, UnicodeDecodeError):
|
|
||||||
failed.append(entry)
|
|
||||||
continue
|
|
||||||
if not os.path.isdir(out):
|
|
||||||
os.mkdir(out, 0o700)
|
|
||||||
target = '%s/f%d' % (out, len(folders))
|
|
||||||
os.rename(path, target)
|
|
||||||
folders.append((name, target))
|
|
||||||
return folders, failed
|
|
||||||
|
|
||||||
def source_messages(user, root):
|
|
||||||
return doveadm_json('-o', 'mail_location=' + location(root), 'fetch', '-u', user,
|
|
||||||
'uid hdr.message-id date.sent size.physical', 'mailbox', 'INBOX')
|
|
||||||
|
|
||||||
def remove_tree(user, name):
|
|
||||||
doveadm(*no_quota, 'mailbox', 'delete', '-u', user, '-r', '-s', '--', name, check=False)
|
|
||||||
|
|
||||||
def folder_guid(user, name):
|
|
||||||
rows = doveadm_json('mailbox', 'status', '-u', user, 'guid', '--', name)
|
|
||||||
for row in rows:
|
|
||||||
if row.get('mailbox') == name:
|
|
||||||
return row['guid']
|
|
||||||
raise RuntimeError('folder not found after the import')
|
|
||||||
|
|
||||||
def quota_room(user):
|
|
||||||
# free bytes of the storage quota, None without a limit
|
|
||||||
for row in doveadm_json('quota', 'get', '-u', user):
|
|
||||||
if row.get('type') == 'STORAGE' and str(row.get('limit', '-')).isdigit():
|
|
||||||
return (int(row['limit']) - int(row.get('value') or 0)) * 1024
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Subscriptions of the backup (subscriptions file: "V\t2", then names with \t between the levels)
|
|
||||||
def backup_subscriptions(maildir):
|
|
||||||
try:
|
|
||||||
lines = read_file(maildir + '/subscriptions').decode('utf-8', 'replace').splitlines()
|
|
||||||
except OSError:
|
|
||||||
return []
|
|
||||||
if lines and lines[0].startswith('V\t'):
|
|
||||||
return ['/'.join(unescape(part) for part in line.split('\t')) for line in lines[1:] if line.strip()]
|
|
||||||
# version 1: storage names with the Maildir++ separator
|
|
||||||
return [folder_name('.' + line.strip()) for line in lines if line.strip()]
|
|
||||||
|
|
||||||
def restore_subscriptions(user, wanted, prefixes):
|
|
||||||
if not wanted:
|
|
||||||
return 0
|
|
||||||
existing = set(own_folders(user, prefixes)) | {'INBOX'}
|
|
||||||
subscribed = set(doveadm('mailbox', 'list', '-u', user, '-s').splitlines())
|
|
||||||
missing = [name for name in dict.fromkeys(wanted) if name in existing and name not in subscribed]
|
|
||||||
if missing:
|
|
||||||
doveadm('mailbox', 'subscribe', '-u', user, '--', *missing)
|
|
||||||
return len(missing)
|
|
||||||
|
|
||||||
# Mails folder by folder: original = only messages missing anywhere in the mailbox, into their original folders;
|
|
||||||
# folder = all messages below "Restored <snapshot time>"
|
|
||||||
def restore_mails(job_id, user, home, maildir, mode, snapshot_time, prefixes, check_quota):
|
|
||||||
result = component()
|
|
||||||
result.update(folders=0, folders_failed=0)
|
|
||||||
if not maildir or not is_dir(maildir + '/cur'):
|
|
||||||
# a key the UI translates
|
|
||||||
result['note'] = 'no_mails'
|
|
||||||
return result
|
|
||||||
tmp = 'Restoring %d' % job_id
|
|
||||||
# a leftover of an interrupted run goes first, it is never counted as present
|
|
||||||
remove_tree(user, tmp)
|
|
||||||
subscriptions = backup_subscriptions(maildir)
|
|
||||||
folders, broken = split_folders(maildir, home)
|
|
||||||
for entry in broken:
|
|
||||||
result['errors'].append('folder %s: name not readable' % entry)
|
|
||||||
result['folders_failed'] += 1
|
|
||||||
live = own_folders(user, prefixes)
|
|
||||||
present = present_keys(user, live.values()) if mode == 'original' else set()
|
|
||||||
plan, need = [], 0
|
|
||||||
for name, root in folders:
|
|
||||||
try:
|
|
||||||
rows = source_messages(user, root)
|
|
||||||
except RuntimeError as e:
|
|
||||||
result['errors'].append('%s: %s' % (name, e))
|
|
||||||
result['folders_failed'] += 1
|
|
||||||
continue
|
|
||||||
wanted = [row for row in rows if mode != 'original' or message_key(row) not in present]
|
|
||||||
result['skipped'] += len(rows) - len(wanted)
|
|
||||||
if wanted:
|
|
||||||
size = sum(int(row.get('size.physical') or 0) for row in wanted)
|
|
||||||
plan.append((name, root, [int(row['uid']) for row in wanted], size))
|
|
||||||
need += size
|
|
||||||
if not plan:
|
|
||||||
if mode == 'original':
|
|
||||||
result['subscribed'] = restore_subscriptions(user, subscriptions, prefixes)
|
|
||||||
return result
|
|
||||||
vfs = os.statvfs('/var/vmail')
|
|
||||||
free, total = vfs.f_bavail * vfs.f_frsize, vfs.f_blocks * vfs.f_frsize
|
|
||||||
if not import_space_ok(need, free, total):
|
|
||||||
raise RuntimeError('not enough free disk space for the mails: %s needed plus %s reserve, %s free' % (
|
|
||||||
mib(need * 11 // 10), mib(disk_reserve(total)), mib(free)))
|
|
||||||
if check_quota:
|
|
||||||
room = quota_room(user)
|
|
||||||
if room is not None and need > room:
|
|
||||||
raise RuntimeError('the mails do not fit into the quota of the mailbox: %s needed, %s free' % (
|
|
||||||
mib(need), mib(max(room, 0))))
|
|
||||||
names = set(live) | {'INBOX'}
|
|
||||||
dest = None
|
|
||||||
if mode != 'original':
|
|
||||||
dest = free_name('Restored ' + snapshot_time, names)
|
|
||||||
doveadm('mailbox', 'create', '-u', user, '-s', '--', dest)
|
|
||||||
names.add(dest)
|
|
||||||
result['folder'] = dest
|
|
||||||
created = []
|
|
||||||
try:
|
|
||||||
for number, (name, root, uids, size) in enumerate(plan):
|
|
||||||
target = dest + '/' + name if dest else name
|
|
||||||
stage = '%s/f%d' % (tmp, number)
|
|
||||||
try:
|
|
||||||
for chunk in uid_chunks(uids):
|
|
||||||
doveadm(*no_quota, 'import', '-u', user, '-s', location(root), stage, 'mailbox', 'INBOX', 'uid', chunk)
|
|
||||||
guid = folder_guid(user, stage + '/INBOX')
|
|
||||||
if target not in names:
|
|
||||||
doveadm('mailbox', 'create', '-u', user, '-s', '--', target)
|
|
||||||
names.add(target)
|
|
||||||
created.append(target)
|
|
||||||
doveadm(*no_quota, 'move', '-u', user, '--', target, 'mailbox-guid', guid, 'all')
|
|
||||||
result['added'] += len(uids)
|
|
||||||
result['folders'] += 1
|
|
||||||
except RuntimeError as e:
|
|
||||||
result['errors'].append('%s: %s' % (name, e))
|
|
||||||
result['folders_failed'] += 1
|
|
||||||
finally:
|
|
||||||
remove_tree(user, tmp)
|
|
||||||
result['folders_created'] = len(created)
|
|
||||||
if mode == 'original':
|
|
||||||
result['subscribed'] = restore_subscriptions(user, subscriptions, prefixes)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def acl_entries(path):
|
|
||||||
entries = []
|
|
||||||
try:
|
|
||||||
lines = read_file(path).decode('utf-8', 'replace').splitlines()
|
|
||||||
except OSError:
|
|
||||||
return entries
|
|
||||||
for line in lines:
|
|
||||||
parts = line.split()
|
|
||||||
# negative rights would take rights away
|
|
||||||
if len(parts) < 2 or parts[0].startswith('-'):
|
|
||||||
continue
|
|
||||||
rights = [rights_by_letter[c] for c in parts[1] if c in rights_by_letter] + [p for p in parts[2:] if p.startswith(':')]
|
|
||||||
if rights:
|
|
||||||
entries.append((parts[0], rights))
|
|
||||||
return entries
|
|
||||||
|
|
||||||
# (folder, entries) of the dovecot-acl files of a restored maildir
|
|
||||||
def maildir_acls(maildir):
|
|
||||||
acls = []
|
|
||||||
if not maildir or not is_dir(maildir):
|
|
||||||
return acls
|
|
||||||
for entry in [''] + sorted(e for e in os.listdir(maildir) if e.startswith('.') and e not in ('.', '..')):
|
|
||||||
path = os.path.join(maildir, entry, 'dovecot-acl') if entry else os.path.join(maildir, 'dovecot-acl')
|
|
||||||
if entry and not is_dir(os.path.join(maildir, entry)):
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
if stat.S_ISREG(os.lstat(path).st_mode):
|
|
||||||
acls.append((folder_name(entry), acl_entries(path)))
|
|
||||||
except (OSError, ValueError, UnicodeDecodeError):
|
|
||||||
continue
|
|
||||||
return acls
|
|
||||||
|
|
||||||
# owner => (folder, entries) of every mailbox in the ACL staging (domain admins: only their domains)
|
|
||||||
def staged_acls(acl_dir, allowed):
|
|
||||||
acls = {}
|
|
||||||
base = (acl_dir or '') + '/var/vmail'
|
|
||||||
sub = os.environ.get('MAILDIR_SUB', '')
|
|
||||||
if not acl_dir or not is_dir(base):
|
|
||||||
return acls
|
|
||||||
for domain in os.listdir(base):
|
|
||||||
if '.' not in domain or not is_dir(os.path.join(base, domain)) or (allowed is not None and domain.lower() not in allowed):
|
|
||||||
continue
|
|
||||||
for local in os.listdir(os.path.join(base, domain)):
|
|
||||||
home = os.path.join(base, domain, local)
|
|
||||||
maildir = os.path.join(home, sub) if sub and is_dir(os.path.join(home, sub)) else home
|
|
||||||
found = maildir_acls(maildir)
|
|
||||||
if found:
|
|
||||||
acls['%s@%s' % (local, domain)] = found
|
|
||||||
return acls
|
|
||||||
|
|
||||||
class AclRestore:
|
|
||||||
def __init__(self, prefixes, allowed):
|
|
||||||
self.prefixes = prefixes
|
|
||||||
self.allowed = allowed
|
|
||||||
self.users, self.folders = {}, {}
|
|
||||||
# anyone/authenticated only when the configuration allows them
|
|
||||||
self.anyone = os.environ.get('ACL_ANYONE', 'disallow') == 'allow'
|
|
||||||
|
|
||||||
def in_scope(self, address):
|
|
||||||
return self.allowed is None or address.rpartition('@')[2].lower() in self.allowed
|
|
||||||
|
|
||||||
def user_exists(self, user):
|
|
||||||
if user not in self.users:
|
|
||||||
self.users[user] = query("SELECT 1 FROM mailbox WHERE username = %s", (user,)).fetchone() is not None
|
|
||||||
return self.users[user]
|
|
||||||
|
|
||||||
def identifier_ok(self, identifier):
|
|
||||||
if identifier.startswith('user='):
|
|
||||||
return self.user_exists(identifier[5:].lower())
|
|
||||||
# group=mcg_<id> belongs to the mailbox groups, the group grants cron keeps them
|
|
||||||
if identifier.startswith('group=mcg_'):
|
|
||||||
return False
|
|
||||||
if identifier in ('anyone', 'authenticated'):
|
|
||||||
return self.anyone
|
|
||||||
return True
|
|
||||||
|
|
||||||
def shown(self, identifier):
|
|
||||||
# domain admins do not see addresses of other domains
|
|
||||||
if identifier.startswith('user=') and not self.in_scope(identifier[5:]):
|
|
||||||
return 'user=(other domain)'
|
|
||||||
return identifier
|
|
||||||
|
|
||||||
def add(self, owner, folder, identifier, rights, result):
|
|
||||||
if not self.in_scope(owner) or masked(owner):
|
|
||||||
result['skipped'] += 1
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
if owner not in self.folders:
|
|
||||||
self.folders[owner] = set(own_folders(owner, self.prefixes)) | {'INBOX'}
|
|
||||||
if folder not in self.folders[owner] or not self.identifier_ok(identifier):
|
|
||||||
result['skipped'] += 1
|
|
||||||
return
|
|
||||||
have = set()
|
|
||||||
for row in doveadm_json('acl', 'get', '-u', owner, '--', folder):
|
|
||||||
if row.get('id') == identifier:
|
|
||||||
have.update((row.get('rights') or '').split())
|
|
||||||
missing = [right for right in rights if right not in have]
|
|
||||||
if not missing:
|
|
||||||
result['skipped'] += 1
|
|
||||||
return
|
|
||||||
doveadm('acl', 'add', '-u', owner, '--', folder, identifier, *missing)
|
|
||||||
result['added'] += 1
|
|
||||||
except RuntimeError as e:
|
|
||||||
result['errors'].append('%s %s %s: %s' % (owner, folder, self.shown(identifier), e))
|
|
||||||
|
|
||||||
# Rights the mailbox granted on its folders and rights other mailboxes granted to it; added, never removed
|
|
||||||
def restore_acl(acl, user, own, granted):
|
|
||||||
result = component()
|
|
||||||
for folder, entries in own:
|
|
||||||
for identifier, rights in entries:
|
|
||||||
acl.add(user, folder, identifier, rights, result)
|
|
||||||
if acl.user_exists(user):
|
|
||||||
for owner, folders in granted.items():
|
|
||||||
if owner == user or not acl.in_scope(owner) or not acl.user_exists(owner):
|
|
||||||
continue
|
|
||||||
for folder, entries in folders:
|
|
||||||
for identifier, rights in entries:
|
|
||||||
if identifier.lower() == 'user=' + user:
|
|
||||||
acl.add(owner, folder, identifier, rights, result)
|
|
||||||
return result
|
|
||||||
|
|
||||||
# Own Sieve scripts (incl. the SOGo vacation script): missing names are added; with activate the active script of the
|
|
||||||
# backup is activated if none is active now
|
|
||||||
def restore_sieve(user, home, active_link, activate):
|
|
||||||
result = component()
|
|
||||||
folder = home + '/sieve'
|
|
||||||
scripts = []
|
|
||||||
if is_dir(folder):
|
|
||||||
scripts = sorted(f[:-6] for f in os.listdir(folder) if f.endswith('.sieve') and stat.S_ISREG(os.lstat(os.path.join(folder, f)).st_mode))
|
|
||||||
current, active = set(), None
|
|
||||||
for line in doveadm('sieve', 'list', '-u', user).splitlines():
|
|
||||||
name, _, flag = line.rpartition(' ')
|
|
||||||
if name:
|
|
||||||
current.add(name)
|
|
||||||
if flag == 'ACTIVE':
|
|
||||||
active = name
|
|
||||||
for name in scripts:
|
|
||||||
if name in current:
|
|
||||||
result['skipped'] += 1
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
doveadm('sieve', 'put', '-u', user, '--', name, data=read_file(os.path.join(folder, name + '.sieve')))
|
|
||||||
current.add(name)
|
|
||||||
result['added'] += 1
|
|
||||||
except (OSError, RuntimeError) as e:
|
|
||||||
result['errors'].append('%s: %s' % (name, e))
|
|
||||||
if not activate:
|
|
||||||
return result
|
|
||||||
target = os.readlink(active_link) if os.path.islink(active_link) else ''
|
|
||||||
backup_active = os.path.basename(target)[:-6] if target.endswith('.sieve') else None
|
|
||||||
if backup_active and not active and backup_active in current:
|
|
||||||
try:
|
|
||||||
doveadm('sieve', 'activate', '-u', user, '--', backup_active)
|
|
||||||
result['activated'] = backup_active
|
|
||||||
except RuntimeError as e:
|
|
||||||
result['errors'].append('%s: %s' % (backup_active, e))
|
|
||||||
return result
|
|
||||||
|
|
||||||
# filters: backup.php reported its part already, the counts are added up
|
|
||||||
def merge(results, key, part):
|
|
||||||
old = results.get(key)
|
|
||||||
if not isinstance(old, dict):
|
|
||||||
results[key] = part
|
|
||||||
return
|
|
||||||
for count in ('added', 'skipped'):
|
|
||||||
old[count] = (old.get(count) or 0) + part.get(count, 0)
|
|
||||||
old['errors'] = (old.get('errors') or []) + part.get('errors', [])
|
|
||||||
for name, value in part.items():
|
|
||||||
if name not in ('added', 'skipped', 'errors'):
|
|
||||||
old[name] = value
|
|
||||||
|
|
||||||
def count_errors(result):
|
|
||||||
count = sum(len(result.get(key, {}).get('errors') or []) for key in ('domain', 'aliases') if isinstance(result.get(key), dict))
|
|
||||||
for entry in (result.get('mailboxes') or {}).values():
|
|
||||||
count += int(bool(entry.get('error')))
|
|
||||||
count += sum(len(value.get('errors') or []) for value in entry.values() if isinstance(value, dict))
|
|
||||||
return count
|
|
||||||
|
|
||||||
def restore_mailbox(job_id, user, components, mode, staging, snapshot_time, prefixes, acl, granted, check_quota, result):
|
|
||||||
local, _, domain = user.rpartition('@')
|
|
||||||
home = '%s/var/vmail/%s/%s' % (staging, domain, local)
|
|
||||||
if masked(user):
|
|
||||||
for key in components & {'mails', 'imap_acl', 'filters'}:
|
|
||||||
merge(result, key, component(['username with * or ?, doveadm reads it as a mask']))
|
|
||||||
return
|
|
||||||
location = mail_location(user)
|
|
||||||
if not location:
|
|
||||||
for key in components & {'mails', 'imap_acl', 'filters'}:
|
|
||||||
merge(result, key, component(['the mailbox does not exist now or is inactive']))
|
|
||||||
return
|
|
||||||
maildir = staged_maildir(staging, home, location)
|
|
||||||
# the ACL files are read before the folders of the maildir are moved for the import
|
|
||||||
own = maildir_acls(maildir) if 'imap_acl' in components else []
|
|
||||||
if 'mails' in components:
|
|
||||||
try:
|
|
||||||
result['mails'] = restore_mails(job_id, user, home, maildir, mode, snapshot_time, prefixes, check_quota)
|
|
||||||
except Exception as e:
|
|
||||||
result['mails'] = component([str(e)[:1000]])
|
|
||||||
try:
|
|
||||||
doveadm('quota', 'recalc', '-u', user)
|
|
||||||
except RuntimeError as e:
|
|
||||||
result['mails']['errors'].append('quota recalc: %s' % e)
|
|
||||||
# new folders
|
|
||||||
acl.folders.pop(user, None)
|
|
||||||
if 'imap_acl' in components:
|
|
||||||
try:
|
|
||||||
result['imap_acl'] = restore_acl(acl, user, own, granted)
|
|
||||||
except Exception as e:
|
|
||||||
result['imap_acl'] = component([str(e)[:1000]])
|
|
||||||
if 'filters' in components:
|
|
||||||
try:
|
|
||||||
part = restore_sieve(user, home, '%s/var/vmail/sieve/%s.sieve' % (staging, user), 'sieve_activate' in components)
|
|
||||||
except Exception as e:
|
|
||||||
part = component([str(e)[:1000]])
|
|
||||||
merge(result, 'filters', part)
|
|
||||||
|
|
||||||
def fail(job_id, error):
|
|
||||||
query("""UPDATE backup_job SET state = IF(state = 'cancelled', state, 'failed'), error = %s, finished = NOW()
|
|
||||||
WHERE id = %s""", (error, job_id))
|
|
||||||
log(job_id, 'restore job failed: ' + error)
|
|
||||||
notify(job_id, error)
|
|
||||||
|
|
||||||
def notify(job_id, error):
|
|
||||||
hostname = os.environ.get('MAILCOW_HOSTNAME', '')
|
|
||||||
row = query("SELECT value FROM backup_setting WHERE name = 'notify_email'").fetchone()
|
|
||||||
rcpts = [r.strip() for r in (row['value'] if row else '').split(',') if re.fullmatch(r'[^@\s<>,]+@[^@\s<>,]+', r.strip())]
|
|
||||||
if not rcpts:
|
|
||||||
return
|
|
||||||
msg = EmailMessage()
|
|
||||||
msg['From'] = 'backup@' + hostname
|
|
||||||
msg['To'] = ', '.join(rcpts)
|
|
||||||
msg['Subject'] = 'mailcow backup on %s: restore job %d failed' % (hostname, job_id)
|
|
||||||
msg['Date'] = formatdate(localtime=True)
|
|
||||||
msg['Message-ID'] = make_msgid(domain=hostname or None)
|
|
||||||
msg.set_content('The restore job %d failed:\n\n%s\n\nThe job log is in the backup section of the mailcow UI.\n'
|
|
||||||
% (job_id, error))
|
|
||||||
try:
|
|
||||||
with smtplib.SMTP('postfix', 588, hostname or None, timeout=60) as smtp:
|
|
||||||
smtp.send_message(msg)
|
|
||||||
except (OSError, smtplib.SMTPException) as e:
|
|
||||||
print('restore job %d: the notification could not be sent: %s' % (job_id, e), file=sys.stderr)
|
|
||||||
|
|
||||||
def staging_dir(job_id, path):
|
|
||||||
# the batch folder of this job, never a symlink
|
|
||||||
if not isinstance(path, str) or not re.fullmatch(r'/restore/%d/[A-Za-z0-9_-]+' % job_id, path) or \
|
|
||||||
os.path.realpath(path) != path or not is_dir(path):
|
|
||||||
return None
|
|
||||||
return path
|
|
||||||
|
|
||||||
def run_batch(job):
|
|
||||||
job_id = job['id']
|
|
||||||
params = json.loads(job['params'] or '{}')
|
|
||||||
result = json.loads(job['result'] or '{}')
|
|
||||||
batch = result.get('batch') or {}
|
|
||||||
staging = staging_dir(job_id, batch.get('dir'))
|
|
||||||
acl_dir = staging_dir(job_id, batch.get('acl_dir')) if batch.get('acl_dir') else None
|
|
||||||
mailboxes = [m for m in batch.get('mailboxes') or [] if isinstance(m, str)]
|
|
||||||
components = {c for c in params.get('components') or [] if c in components_here}
|
|
||||||
mode = 'original' if params.get('mail_target') == 'original' else 'folder'
|
|
||||||
allowed = {d.lower() for d in params['allowed_domains']} if isinstance(params.get('allowed_domains'), list) else None
|
|
||||||
check_quota = params.get('requested_role', 'admin') != 'admin'
|
|
||||||
snapshot_time = (result.get('snapshot') or {}).get('time') or time.strftime('%Y-%m-%d %H:%M')
|
|
||||||
results = result.setdefault('mailboxes', {})
|
|
||||||
prefixes = shared_prefixes()
|
|
||||||
acl = AclRestore(prefixes, allowed)
|
|
||||||
granted = staged_acls(acl_dir, allowed) if 'imap_acl' in components else {}
|
|
||||||
log(job_id, 'restoring %s for %d mailboxes%s' % (', '.join(sorted(components)), len(mailboxes),
|
|
||||||
' (mails: %s)' % mode if 'mails' in components else ''))
|
|
||||||
for user in mailboxes:
|
|
||||||
entry = results.setdefault(user, {})
|
|
||||||
if entry.get('error'):
|
|
||||||
continue
|
|
||||||
started = time.monotonic()
|
|
||||||
if not staging:
|
|
||||||
for key in components & {'mails', 'imap_acl', 'filters'}:
|
|
||||||
merge(entry, key, component(['the restored files are missing']))
|
|
||||||
else:
|
|
||||||
restore_mailbox(job_id, user, components, mode, staging, snapshot_time, prefixes, acl, granted, check_quota, entry)
|
|
||||||
summary = ', '.join('%s +%d' % (key, entry[key].get('added', 0)) for key in sorted(components) if isinstance(entry.get(key), dict))
|
|
||||||
errors = sum(len(entry[key].get('errors') or []) for key in components if isinstance(entry.get(key), dict))
|
|
||||||
log(job_id, '%s: %s%s (%d s)' % (user, summary, ', %d errors' % errors if errors else '', time.monotonic() - started))
|
|
||||||
result['errors'] = count_errors(result)
|
|
||||||
save(job_id, result)
|
|
||||||
result['errors'] = count_errors(result)
|
|
||||||
save(job_id, result)
|
|
||||||
# the restic runner removes the staging and goes on
|
|
||||||
query("UPDATE backup_job SET step = 'mails_done' WHERE id = %s AND state = 'running' AND step = 'mails_running'", (job_id,))
|
|
||||||
|
|
||||||
def main():
|
|
||||||
global conn
|
|
||||||
if os.geteuid() == 0:
|
|
||||||
# as vmail like the ofelia job: a run as root would leave a lock file the runs as vmail cannot open
|
|
||||||
os.execv('/usr/local/bin/gosu', ['gosu', 'vmail', sys.executable, os.path.abspath(__file__)] + sys.argv[1:])
|
|
||||||
os.umask(0o077)
|
|
||||||
# released by the kernel when the process ends, a crashed run leaves no stale lock
|
|
||||||
lock_fd = os.open(lock_file, os.O_RDWR | os.O_CREAT, 0o600)
|
|
||||||
try:
|
|
||||||
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
||||||
except OSError:
|
|
||||||
sys.exit("restore: can't lock " + lock_file)
|
|
||||||
conn = connect()
|
|
||||||
heartbeat = Heartbeat()
|
|
||||||
heartbeat.start()
|
|
||||||
try:
|
|
||||||
# under the lock: a batch in mails_running was left by a crashed or stopped run, its temporary folders go
|
|
||||||
for row in query("""SELECT id, result FROM backup_job WHERE type = 'restore' AND state = 'running'
|
|
||||||
AND step = 'mails_running'""").fetchall():
|
|
||||||
try:
|
|
||||||
for user in (json.loads(row['result'] or '{}').get('batch') or {}).get('mailboxes') or []:
|
|
||||||
remove_tree(user, 'Restoring %d' % row['id'])
|
|
||||||
doveadm('quota', 'recalc', '-u', user, check=False)
|
|
||||||
except (ValueError, AttributeError, RuntimeError):
|
|
||||||
pass
|
|
||||||
fail(row['id'], 'interrupted: the restore of the mails was stopped')
|
|
||||||
last, waited = None, 0
|
|
||||||
while True:
|
|
||||||
job = query("""SELECT id, params, result FROM backup_job WHERE type = 'restore' AND state = 'running'
|
|
||||||
AND step = 'mails' ORDER BY id LIMIT 1""").fetchone()
|
|
||||||
if not job:
|
|
||||||
# the next batch of a restore follows within seconds: wait for it instead of the next ofelia run
|
|
||||||
row = query("SELECT state FROM backup_job WHERE id = %s", (last,)).fetchone() if last else None
|
|
||||||
if row and row['state'] == 'running' and waited < batch_wait:
|
|
||||||
time.sleep(2)
|
|
||||||
waited += 2
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
last, waited = job['id'], 0
|
|
||||||
if query("UPDATE backup_job SET step = 'mails_running' WHERE id = %s AND state = 'running' AND step = 'mails'",
|
|
||||||
(job['id'],)).rowcount != 1:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
run_batch(job)
|
|
||||||
except Exception as e:
|
|
||||||
fail(job['id'], str(e)[:1000])
|
|
||||||
finally:
|
|
||||||
heartbeat.done.set()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
# Nightly run of the retention rules (ofelia, dovecot container): gets the plan page by page from the mailcow UI
|
|
||||||
# (nginx:8081/retention.php), gives every folder of a mailbox to the first entry of its plan that matches it, counts
|
|
||||||
# the mail saved in the folders before the period of the rule with doveadm, deletes it unless the rule only
|
|
||||||
# simulates, and reports the results back.
|
|
||||||
|
|
||||||
import fcntl
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
plan_url = 'http://nginx:8081/retention.php'
|
|
||||||
lock_file = '/tmp/retention_runner.lock'
|
|
||||||
# folder keys of the rules and the special-use flag of their folders in the Dovecot configuration
|
|
||||||
special_use = {'trash': '\\Trash', 'junk': '\\Junk', 'sent': '\\Sent', 'drafts': '\\Drafts', 'archive': '\\Archive'}
|
|
||||||
own_folder = 'folder:'
|
|
||||||
# mailboxes per page of the plan and results per report, a large instance has many mailboxes
|
|
||||||
plan_page = 200
|
|
||||||
report_batch = 200
|
|
||||||
# a request is tried this often, waiting longer each time
|
|
||||||
tries = 3
|
|
||||||
|
|
||||||
token = hmac.new(os.environ.get('DBPASS', '').encode(), b'retention', hashlib.sha256).hexdigest()
|
|
||||||
|
|
||||||
def request(url, body=None):
|
|
||||||
data = None if body is None else json.dumps(body).encode()
|
|
||||||
req = urllib.request.Request(url, data=data, headers={'Content-Type': 'application/json', 'X-Retention-Token': token},
|
|
||||||
method='GET' if body is None else 'POST')
|
|
||||||
with urllib.request.urlopen(req, timeout=300) as response:
|
|
||||||
return json.load(response)
|
|
||||||
|
|
||||||
def retried(url, body=None):
|
|
||||||
for attempt in range(1, tries + 1):
|
|
||||||
try:
|
|
||||||
return request(url, body)
|
|
||||||
except Exception as e:
|
|
||||||
if attempt == tries:
|
|
||||||
raise
|
|
||||||
print('retention: request failed, trying again: %s' % e, file=sys.stderr)
|
|
||||||
time.sleep(30 * attempt)
|
|
||||||
|
|
||||||
def plan():
|
|
||||||
# one page after the other, so memory stays small; rule changes apply to the pages fetched after them
|
|
||||||
after = ''
|
|
||||||
while after is not None:
|
|
||||||
page = retried(plan_url + '?' + urllib.parse.urlencode({'after': after, 'limit': plan_page}))
|
|
||||||
yield from page['mailboxes']
|
|
||||||
after = page['next']
|
|
||||||
|
|
||||||
def post(body):
|
|
||||||
# False if the report could not be sent
|
|
||||||
try:
|
|
||||||
retried(plan_url, body)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print('retention: report failed: %s' % e, file=sys.stderr)
|
|
||||||
return False
|
|
||||||
|
|
||||||
class Reports:
|
|
||||||
# results go back in parts; once a part cannot be sent, the rest waits for the end of the run
|
|
||||||
def __init__(self):
|
|
||||||
self.pending, self.failed = [], False
|
|
||||||
|
|
||||||
def add(self, result):
|
|
||||||
self.pending.append(result)
|
|
||||||
if len(self.pending) >= report_batch and not self.failed:
|
|
||||||
self.failed = not self.send()
|
|
||||||
|
|
||||||
def send(self, summary=None, run=None):
|
|
||||||
# full parts, at the end all results with the summary and the run in the last part
|
|
||||||
final = summary is not None
|
|
||||||
while len(self.pending) >= report_batch or final:
|
|
||||||
body = {'results': self.pending[:report_batch]}
|
|
||||||
if final and len(self.pending) <= report_batch:
|
|
||||||
body.update(summary=summary, run=run)
|
|
||||||
if not post(body):
|
|
||||||
return False
|
|
||||||
del self.pending[:report_batch]
|
|
||||||
if 'summary' in body:
|
|
||||||
break
|
|
||||||
return True
|
|
||||||
|
|
||||||
def doveadm(*args, partial=False):
|
|
||||||
result = subprocess.run(['doveadm', *args], capture_output=True, text=True, timeout=3600)
|
|
||||||
# mailbox status also fails for single folders that cannot be opened; the others are still listed
|
|
||||||
if result.returncode != 0 and not (partial and result.stdout):
|
|
||||||
raise RuntimeError((result.stderr or result.stdout).strip()[:500] or 'doveadm exit %d' % result.returncode)
|
|
||||||
return result.stdout
|
|
||||||
|
|
||||||
def configuration():
|
|
||||||
# names of the special folders per key, and the prefixes of the shared and public namespaces (not the user's)
|
|
||||||
names = {key: set() for key in special_use}
|
|
||||||
prefixes = ['Shared/']
|
|
||||||
namespace, mailbox = None, None
|
|
||||||
# as vmail doveconf cannot read every included file (mail_crypt keys) and fails, but prints the namespaces
|
|
||||||
for line in subprocess.run(['doveconf', '-n'], capture_output=True, text=True).stdout.splitlines():
|
|
||||||
stripped = line.strip()
|
|
||||||
if line.startswith('namespace ') and line.endswith('{'):
|
|
||||||
namespace = {}
|
|
||||||
elif line == '}':
|
|
||||||
if namespace and namespace.get('type') in ('shared', 'public') and namespace.get('prefix'):
|
|
||||||
prefixes.append(namespace['prefix'].split('%')[0])
|
|
||||||
namespace = None
|
|
||||||
elif namespace is None:
|
|
||||||
continue
|
|
||||||
elif re.match(r'mailbox .+ \{$', stripped):
|
|
||||||
mailbox = stripped[len('mailbox '):-2].strip().strip('"')
|
|
||||||
elif stripped == '}':
|
|
||||||
mailbox = None
|
|
||||||
elif '=' in stripped:
|
|
||||||
key, value = (part.strip() for part in stripped.split('=', 1))
|
|
||||||
if mailbox is not None and key == 'special_use':
|
|
||||||
for folder, flag in special_use.items():
|
|
||||||
if flag in value.split():
|
|
||||||
names[folder].add(mailbox)
|
|
||||||
elif mailbox is None and key in ('type', 'prefix'):
|
|
||||||
namespace[key] = value
|
|
||||||
if not names['trash']:
|
|
||||||
raise RuntimeError('no special folders in the output of doveconf -n')
|
|
||||||
return names, [prefix for prefix in prefixes if prefix]
|
|
||||||
|
|
||||||
def own_folders(user, prefixes):
|
|
||||||
# folder name => guid of the user's own folders; listed first, so folders others share are not opened
|
|
||||||
names = set(name for name in doveadm('-f', 'tab', 'mailbox', 'list', '-u', user).splitlines()[1:]
|
|
||||||
if name and not any((name + '/').startswith(prefix) for prefix in prefixes))
|
|
||||||
folders = {}
|
|
||||||
if not names:
|
|
||||||
return folders
|
|
||||||
for line in doveadm('-f', 'tab', 'mailbox', 'status', '-u', user, 'guid', *sorted(names), partial=True).splitlines()[1:]:
|
|
||||||
name, _, guid = line.rpartition('\t')
|
|
||||||
if name in names and re.fullmatch(r'[0-9a-f]{32}', guid):
|
|
||||||
folders[name] = guid
|
|
||||||
return folders
|
|
||||||
|
|
||||||
def canonical(name):
|
|
||||||
# Dovecot writes INBOX in capitals, also as the parent of folders
|
|
||||||
return 'INBOX' + name[5:] if name[:5].upper() == 'INBOX' and name[5:6] in ('', '/') else name
|
|
||||||
|
|
||||||
def matches(action, name, names):
|
|
||||||
# match: the folder itself, an own folder name with *, the subfolders of a special folder or the inbox, all folders
|
|
||||||
key, match = action['folder'], action['match']
|
|
||||||
if match == 'all':
|
|
||||||
return True
|
|
||||||
if key.startswith(own_folder):
|
|
||||||
# * is the only wildcard of own folder names
|
|
||||||
pattern = '.*'.join(re.escape(part) for part in canonical(key[len(own_folder):]).split('*'))
|
|
||||||
return re.fullmatch(pattern, name) is not None
|
|
||||||
folders = ['INBOX'] if key == 'inbox' else names.get(key, ())
|
|
||||||
if match == 'subfolders':
|
|
||||||
return any(name.startswith(folder + '/') for folder in folders)
|
|
||||||
return name in folders
|
|
||||||
|
|
||||||
def decide(actions, folders, names):
|
|
||||||
# rule id => (its action, the folders it decides about): every folder goes to the first action that matches it
|
|
||||||
decided = {}
|
|
||||||
for name in sorted(folders):
|
|
||||||
for action in actions:
|
|
||||||
if matches(action, canonical(name), names):
|
|
||||||
decided.setdefault(action['rule_id'], (action, []))[1].append(name)
|
|
||||||
break
|
|
||||||
return decided
|
|
||||||
|
|
||||||
def found(user, query):
|
|
||||||
return len(doveadm('search', '-u', user, *query).splitlines())
|
|
||||||
|
|
||||||
def clean(user, action, selected, folders, result):
|
|
||||||
# name and guid together: doveadm then only opens these folders
|
|
||||||
query = []
|
|
||||||
for name in selected:
|
|
||||||
query += (['OR'] if query else []) + ['(', 'mailbox', name, 'mailbox-guid', folders[name], ')']
|
|
||||||
query = ['('] + query + [')', 'savedbefore', '%dd' % int(action['days'])]
|
|
||||||
if action['keep_flagged']:
|
|
||||||
query += ['NOT', 'FLAGGED']
|
|
||||||
result['matched'] = found(user, query)
|
|
||||||
if not result['matched'] or action['mode'] != 'delete':
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
doveadm('expunge', '-u', user, *query)
|
|
||||||
result['deleted'] = result['matched']
|
|
||||||
except Exception as e:
|
|
||||||
# part of the mail may be gone: what is left is counted
|
|
||||||
result['error'] = str(e)[:1000]
|
|
||||||
try:
|
|
||||||
result['deleted'] = max(0, result['matched'] - found(user, query))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# released by the kernel when the process ends, a crashed run leaves no stale lock
|
|
||||||
lock_fd = os.open(lock_file, os.O_RDWR | os.O_CREAT, 0o600)
|
|
||||||
try:
|
|
||||||
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
||||||
except OSError:
|
|
||||||
sys.exit("retention: can't lock " + lock_file)
|
|
||||||
reports = Reports()
|
|
||||||
run = {'started': int(time.time()), 'mailboxes': 0, 'matched': 0, 'deleted': 0, 'errors': 0, 'error': ''}
|
|
||||||
summary = {}
|
|
||||||
try:
|
|
||||||
names, prefixes = configuration()
|
|
||||||
for mailbox in plan():
|
|
||||||
user = mailbox['username']
|
|
||||||
run['mailboxes'] += 1
|
|
||||||
try:
|
|
||||||
folders, error = own_folders(user, prefixes), ''
|
|
||||||
decided = decide(mailbox['actions'], folders, names)
|
|
||||||
except Exception as e:
|
|
||||||
# the folders are unknown: an error for every rule of the mailbox
|
|
||||||
folders, error, decided = {}, str(e)[:1000], {}
|
|
||||||
for action in mailbox['actions']:
|
|
||||||
decided.setdefault(action['rule_id'], (action, []))
|
|
||||||
for rule_id, (action, selected) in decided.items():
|
|
||||||
rule = summary.setdefault(rule_id, {'rule_id': rule_id, 'mailboxes': 0, 'matched': 0, 'deleted': 0, 'errors': 0,
|
|
||||||
'simulated': int(action['mode'] != 'delete')})
|
|
||||||
result = {'username': user, 'rule_id': rule_id, 'folder': '\n'.join(selected), 'days': action['days'], 'matched': 0,
|
|
||||||
'deleted': 0, 'simulated': rule['simulated'], 'error': error}
|
|
||||||
if not error:
|
|
||||||
try:
|
|
||||||
clean(user, action, selected, folders, result)
|
|
||||||
except Exception as e:
|
|
||||||
result['error'] = str(e)[:1000]
|
|
||||||
for counter in (rule, run):
|
|
||||||
counter['matched'] += result['matched']
|
|
||||||
counter['deleted'] += result['deleted']
|
|
||||||
counter['errors'] += int(result['error'] != '')
|
|
||||||
rule['mailboxes'] += 1
|
|
||||||
# only mailboxes with found mail or an error are logged, every rule gets a summary
|
|
||||||
if result['matched'] or result['error']:
|
|
||||||
reports.add(result)
|
|
||||||
except Exception as e:
|
|
||||||
run['error'] = str(e)[:1000]
|
|
||||||
reported = reports.send(list(summary.values()), run)
|
|
||||||
print('retention: %d mailboxes, %d mails deleted, %d found by simulating rules, %d errors' % (run['mailboxes'],
|
|
||||||
run['deleted'], sum(rule['matched'] for rule in summary.values() if rule['simulated']), run['errors']))
|
|
||||||
if run['error']:
|
|
||||||
print('retention: %s' % run['error'], file=sys.stderr)
|
|
||||||
if not reported:
|
|
||||||
print('retention: the results could not be reported', file=sys.stderr)
|
|
||||||
if run['error'] or not reported:
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -4,7 +4,7 @@ catch_non_zero() {
|
|||||||
${CMD} > /dev/null
|
${CMD} > /dev/null
|
||||||
EC=$?
|
EC=$?
|
||||||
if [ ${EC} -ne 0 ]; then
|
if [ ${EC} -ne 0 ]; then
|
||||||
echo "Command ${CMD} failed to execute, exit code was ${EC}"
|
echo "Command ${CMD//${REDISPASS}/***} failed to execute, exit code was ${EC}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
source /source_env.sh
|
source /source_env.sh
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
|||||||
|
|
||||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||||
|
|
||||||
RUN apk add --no-cache nginx \
|
RUN apk upgrade --no-cache && \
|
||||||
|
apk add --no-cache nginx \
|
||||||
python3 \
|
python3 \
|
||||||
py3-pip && \
|
py3-pip && \
|
||||||
pip install --upgrade pip && \
|
pip install --upgrade pip && \
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ WORKDIR /src
|
|||||||
ENV CGO_ENABLED=0 \
|
ENV CGO_ENABLED=0 \
|
||||||
GO111MODULE=on \
|
GO111MODULE=on \
|
||||||
NOOPT=1 \
|
NOOPT=1 \
|
||||||
VERSION=1.14.0
|
VERSION=1.14.5
|
||||||
|
|
||||||
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
|
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
|
||||||
cd /src/postfix-tlspol && \
|
cd /src/postfix-tlspol && \
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ RUN groupadd -g 102 postfix \
|
|||||||
postfix \
|
postfix \
|
||||||
postfix-mysql \
|
postfix-mysql \
|
||||||
postfix-pcre \
|
postfix-pcre \
|
||||||
python3 \
|
|
||||||
redis-tools \
|
redis-tools \
|
||||||
sasl2-bin \
|
sasl2-bin \
|
||||||
sudo \
|
sudo \
|
||||||
@@ -47,7 +46,6 @@ COPY postfix.sh /opt/postfix.sh
|
|||||||
COPY rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham
|
COPY rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham
|
||||||
COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam
|
COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam
|
||||||
COPY whitelist_forwardinghosts.sh /usr/local/bin/whitelist_forwardinghosts.sh
|
COPY whitelist_forwardinghosts.sh /usr/local/bin/whitelist_forwardinghosts.sh
|
||||||
COPY mailcow-policy.py /usr/local/bin/mailcow-policy.py
|
|
||||||
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
|
|
||||||
@@ -55,7 +53,6 @@ RUN chmod +x /opt/postfix.sh \
|
|||||||
/usr/local/bin/rspamd-pipe-ham \
|
/usr/local/bin/rspamd-pipe-ham \
|
||||||
/usr/local/bin/rspamd-pipe-spam \
|
/usr/local/bin/rspamd-pipe-spam \
|
||||||
/usr/local/bin/whitelist_forwardinghosts.sh \
|
/usr/local/bin/whitelist_forwardinghosts.sh \
|
||||||
/usr/local/bin/mailcow-policy.py \
|
|
||||||
/usr/local/sbin/stop-supervisor.sh
|
/usr/local/sbin/stop-supervisor.sh
|
||||||
RUN rm -rf /tmp/* /var/tmp/*
|
RUN rm -rf /tmp/* /var/tmp/*
|
||||||
|
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
#!/usr/bin/python3
|
|
||||||
# Policy service of the mail policies (mailcow-policy in master.cf, spawned by Postfix): reads the policy requests
|
|
||||||
# of smtpd and asks the mailcow UI for the action (nginx:8081/mailpolicy.php). Without an answer the recipient is
|
|
||||||
# deferred; smtpd only asks while policies or internal aliases exist (mysql_mail_policy_maps.cf).
|
|
||||||
|
|
||||||
import http.client
|
|
||||||
import re
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
from urllib.parse import urlencode
|
|
||||||
|
|
||||||
# one line: DUNNO or a rejection with code and text
|
|
||||||
valid_action = re.compile(rb'DUNNO|[45]\d\d [45]\.\d{1,3}\.\d{1,3} [\x20-\x7e]{1,200}')
|
|
||||||
unavailable = b'DEFER_IF_PERMIT Mail policy check temporarily unavailable'
|
|
||||||
# address of nginx, resolved once per process (a lookup costs more than the check)
|
|
||||||
nginx = None
|
|
||||||
|
|
||||||
def check(request):
|
|
||||||
global nginx
|
|
||||||
body = urlencode(sorted(request.items())).encode()
|
|
||||||
try:
|
|
||||||
if nginx is None:
|
|
||||||
nginx = socket.gethostbyname('nginx')
|
|
||||||
# http.client sets TCP_NODELAY, so the body is not held back until the headers are acknowledged
|
|
||||||
# below the timeout of Postfix (main.cf), so an answer always arrives
|
|
||||||
conn = http.client.HTTPConnection(nginx, 8081, timeout=8)
|
|
||||||
try:
|
|
||||||
conn.request('POST', '/mailpolicy.php', body, {'Host': 'nginx', 'Content-Type': 'application/x-www-form-urlencoded'})
|
|
||||||
response = conn.getresponse()
|
|
||||||
action = response.read()
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
except (OSError, http.client.HTTPException):
|
|
||||||
# nginx may have a new address after a restart
|
|
||||||
nginx = None
|
|
||||||
return unavailable
|
|
||||||
return action if response.status == 200 and valid_action.fullmatch(action) else unavailable
|
|
||||||
|
|
||||||
def main():
|
|
||||||
request = {}
|
|
||||||
# attributes as bytes: addresses may contain any 8-bit characters
|
|
||||||
for line in sys.stdin.buffer:
|
|
||||||
line = line.rstrip(b'\r\n')
|
|
||||||
if line:
|
|
||||||
name, separator, value = line.partition(b'=')
|
|
||||||
if separator:
|
|
||||||
request[name] = value
|
|
||||||
continue
|
|
||||||
sys.stdout.buffer.write(b'action=' + check(request) + b'\n\n')
|
|
||||||
sys.stdout.buffer.flush()
|
|
||||||
request = {}
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -15,21 +15,6 @@ until dig +short mailcow.email > /dev/null; do
|
|||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
||||||
# Wait for updated schema (DB_VERSION from docker-compose.yml), the maps read tables the init_db of php-fpm creates on updates
|
|
||||||
DBV_NOW=$(mariadb --ssl=false --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN 2>/dev/null)
|
|
||||||
DBV_WAIT=0
|
|
||||||
while [[ "${DBV_NOW}" != "${DB_VERSION}" ]] && [[ ${DBV_WAIT} -lt 60 ]]; do
|
|
||||||
echo "Waiting for schema update..."
|
|
||||||
sleep 5
|
|
||||||
DBV_WAIT=$((DBV_WAIT + 1))
|
|
||||||
DBV_NOW=$(mariadb --ssl=false --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN 2>/dev/null)
|
|
||||||
done
|
|
||||||
if [[ "${DBV_NOW}" != "${DB_VERSION}" ]]; then
|
|
||||||
echo -e "\e[31mDB schema is ${DBV_NOW}, expected ${DB_VERSION} - starting anyway, check the php-fpm log (init_db)\e[0m"
|
|
||||||
else
|
|
||||||
echo "DB schema is ${DBV_NOW}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat <<EOF > /etc/aliases
|
cat <<EOF > /etc/aliases
|
||||||
# Autogenerated by mailcow
|
# Autogenerated by mailcow
|
||||||
null: /dev/null
|
null: /dev/null
|
||||||
@@ -239,19 +224,6 @@ query = SELECT CONCAT_WS(':', username, password) AS auth_data FROM transports
|
|||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Mail policies: while policies or internal aliases exist, smtpd asks the policy service (mailcow_mail_policy in main.cf)
|
|
||||||
cat <<EOF > /opt/postfix/conf/sql/mysql_mail_policy_maps.cf
|
|
||||||
# Autogenerated by mailcow
|
|
||||||
user = ${DBUSER}
|
|
||||||
password = ${DBPASS}
|
|
||||||
hosts = unix:/var/run/mysqld/mysqld.sock
|
|
||||||
dbname = ${DBNAME}
|
|
||||||
query = SELECT 'mailcow_mail_policy' FROM DUAL WHERE '%s' != ''
|
|
||||||
AND (EXISTS (SELECT 1 FROM mail_policy_member JOIN mail_policy ON mail_policy.id = mail_policy_member.policy_id
|
|
||||||
WHERE mail_policy.active = '1')
|
|
||||||
OR EXISTS (SELECT 1 FROM alias WHERE internal = '1' AND active IN ('1', '2')));
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf
|
cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_maps.cf
|
||||||
# Autogenerated by mailcow
|
# Autogenerated by mailcow
|
||||||
user = ${DBUSER}
|
user = ${DBUSER}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
FROM alpine:3.24
|
|
||||||
|
|
||||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
|
||||||
|
|
||||||
ENV LANG=C.UTF-8
|
|
||||||
ENV LC_ALL=C.UTF-8
|
|
||||||
# MariaDB Connector/C >= 3.4 refuses DB servers without TLS, the runner connects through the socket
|
|
||||||
ENV MARIADB_TLS_DISABLE_PEER_VERIFICATION=1
|
|
||||||
|
|
||||||
RUN apk add --no-cache --update \
|
|
||||||
ca-certificates \
|
|
||||||
git \
|
|
||||||
mariadb-client \
|
|
||||||
openssh-client \
|
|
||||||
py3-mysqlclient \
|
|
||||||
py3-pynacl \
|
|
||||||
python3 \
|
|
||||||
restic \
|
|
||||||
tini \
|
|
||||||
tzdata \
|
|
||||||
# dump, export, manifest and the mailcow directory are below /mailcow (paths of the snapshot), only root may read them
|
|
||||||
&& mkdir -m 700 /mailcow
|
|
||||||
|
|
||||||
COPY restic_runner.py /usr/local/bin/restic_runner.py
|
|
||||||
COPY sogo_restore.py /usr/local/bin/sogo_restore.py
|
|
||||||
|
|
||||||
ENTRYPOINT ["/sbin/tini", "--"]
|
|
||||||
CMD ["/usr/bin/python3", "-u", "/usr/local/bin/restic_runner.py"]
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,118 +0,0 @@
|
|||||||
"""SOGo part of a mailbox restore (component sogo, overwrites), called by the restic runner for one mailbox.
|
|
||||||
|
|
||||||
Runs `sogo-tool restore -f ALL <dir> <user>` and `sogo-tool restore -p <dir> <user>` in the sogo container through
|
|
||||||
dockerapi on the user's file of the nightly sogo-tool backup: missing calendars, address books and entries come back,
|
|
||||||
the names and shares of existing folders and all SOGo preferences are replaced by the backup's, entries changed since
|
|
||||||
stay. Then the user's entries in SOGo's cache (memcached) are removed, so the change shows at once.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import socket
|
|
||||||
import ssl
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
dockerapi_url = 'https://dockerapi:443'
|
|
||||||
memcached_addr = ('memcached-mailcow', 11211)
|
|
||||||
# dockerapi stops each of the two sogo-tool runs after 850 s
|
|
||||||
restore_timeout = 1800
|
|
||||||
# accepted by the dockerapi handler as well
|
|
||||||
user_re = re.compile(r'[A-Za-z0-9_][A-Za-z0-9._+-]*@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+')
|
|
||||||
|
|
||||||
|
|
||||||
def _short(text, limit=300):
|
|
||||||
# sogo-tool may print database URLs: no credentials, one line, the end has the error
|
|
||||||
text = re.sub(r'([A-Za-z][A-Za-z0-9+.-]*://)[^\s/@]*@', r'\1***@', str(text))
|
|
||||||
text = ' '.join(text.split())
|
|
||||||
return text if len(text) <= limit else '...' + text[-limit:]
|
|
||||||
|
|
||||||
|
|
||||||
def _api(path, body=None, timeout=60):
|
|
||||||
# dockerapi uses the mail certificate, its name does not match
|
|
||||||
ctx = ssl.create_default_context()
|
|
||||||
ctx.check_hostname = False
|
|
||||||
ctx.verify_mode = ssl.CERT_NONE
|
|
||||||
data = None if body is None else json.dumps(body).encode()
|
|
||||||
req = urllib.request.Request(dockerapi_url + path, data=data, headers={'Content-Type': 'application/json'},
|
|
||||||
method='GET' if body is None else 'POST')
|
|
||||||
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as response:
|
|
||||||
return json.load(response)
|
|
||||||
|
|
||||||
|
|
||||||
def _sogo_container():
|
|
||||||
project = os.environ.get('COMPOSE_PROJECT_NAME', '').lower()
|
|
||||||
by_name = None
|
|
||||||
for cid, info in _api('/containers/json').items():
|
|
||||||
if not cid.isalnum() or not isinstance(info, dict):
|
|
||||||
continue
|
|
||||||
labels = (info.get('Config') or {}).get('Labels') or {}
|
|
||||||
if labels.get('com.docker.compose.service') == 'sogo-mailcow':
|
|
||||||
if not project or labels.get('com.docker.compose.project', '').lower() == project:
|
|
||||||
return cid
|
|
||||||
elif by_name is None and 'sogo-mailcow' in str(info.get('Name', '')):
|
|
||||||
by_name = cid
|
|
||||||
if by_name:
|
|
||||||
return by_name
|
|
||||||
raise RuntimeError('sogo container not found')
|
|
||||||
|
|
||||||
|
|
||||||
def _memcached_delete(keys):
|
|
||||||
keys = [k for k in keys if 0 < len(k.encode()) <= 250 and not re.search(r'[\s\x00-\x1f\x7f]', k)]
|
|
||||||
if not keys:
|
|
||||||
return
|
|
||||||
with socket.create_connection(memcached_addr, timeout=10) as s:
|
|
||||||
for key in keys:
|
|
||||||
s.sendall(b'delete ' + key.encode() + b'\r\n')
|
|
||||||
reply = b''
|
|
||||||
while not reply.endswith(b'\r\n'):
|
|
||||||
chunk = s.recv(64)
|
|
||||||
if not chunk:
|
|
||||||
raise RuntimeError('memcached closed the connection')
|
|
||||||
reply += chunk
|
|
||||||
if reply.strip() not in (b'DELETED', b'NOT_FOUND'):
|
|
||||||
raise RuntimeError('memcached: %s' % _short(reply.decode('ascii', 'replace')))
|
|
||||||
|
|
||||||
|
|
||||||
def _cache_keys(db, username):
|
|
||||||
# preferences and the ACLs of the user's folders (<uid>/Calendar/personal+acl); never read the c_*location
|
|
||||||
# columns of sogo_folder_info (database credentials)
|
|
||||||
keys = ['%s+defaults' % username, '%s+settings' % username]
|
|
||||||
cur = db.cursor()
|
|
||||||
try:
|
|
||||||
cur.execute('SELECT c_path FROM sogo_folder_info WHERE c_path2 = %s', (username,))
|
|
||||||
keys += ['%s+acl' % row[0][len('/Users/'):] for row in cur.fetchall() if row[0].startswith('/Users/')]
|
|
||||||
finally:
|
|
||||||
cur.close()
|
|
||||||
return keys
|
|
||||||
|
|
||||||
|
|
||||||
def restore(db, backup_dir, username, log):
|
|
||||||
"""Restores the SOGo data of one user from <backup_dir>/<username> (staged readable for the sogo user only).
|
|
||||||
db: open MySQLdb connection; log: callable for short progress lines. Returns {"added": 0|1, "errors": [...]}."""
|
|
||||||
result = {'added': 0, 'errors': []}
|
|
||||||
if not user_re.fullmatch(username):
|
|
||||||
result['errors'].append('username not supported by the SOGo restore')
|
|
||||||
return result
|
|
||||||
path = os.path.join(backup_dir, username)
|
|
||||||
if not os.path.isfile(path) or os.path.islink(path):
|
|
||||||
# sogo-tool backup ALL skips users without SOGo folders
|
|
||||||
# a key the UI translates
|
|
||||||
result['note'] = 'no_sogo_data'
|
|
||||||
log('sogo: no SOGo data in the snapshot')
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
res = _api('/containers/%s/exec' % _sogo_container(), {'cmd': 'sogo', 'task': 'restore', 'dir': backup_dir,
|
|
||||||
'username': username}, timeout=restore_timeout)
|
|
||||||
if not isinstance(res, dict) or res.get('type') != 'success':
|
|
||||||
raise RuntimeError('sogo-tool restore failed: %s' % _short(res.get('msg', '') if isinstance(res, dict) else res))
|
|
||||||
result['added'] = 1
|
|
||||||
log('sogo: calendars, address books and preferences reset to the backup')
|
|
||||||
except Exception as e:
|
|
||||||
result['errors'].append(_short(e))
|
|
||||||
try:
|
|
||||||
# SOGo caches ACLs and preferences for 15 minutes
|
|
||||||
_memcached_delete(_cache_keys(db, username))
|
|
||||||
except Exception as e:
|
|
||||||
result['errors'].append('SOGo cache not cleared, changes may show up after 15 minutes: %s' % _short(e))
|
|
||||||
return result
|
|
||||||
@@ -277,9 +277,34 @@ if [[ ! -z ${SPAMHAUS_DQS_KEY} ]]; then
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"DBL" {
|
"DBL" {
|
||||||
# override the defaults for DBL defined in modules.d/rbl.conf
|
# full copy of the DBL rule from modules.d/rbl.conf, pointed at DQS:
|
||||||
|
# the override include replaces the stock rule instead of merging into it
|
||||||
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
|
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
|
||||||
disable_monitoring = true;
|
disable_monitoring = true;
|
||||||
|
checks = ["emails", "dkim", "helo", "rdns", "replyto", "urls", "content_urls"];
|
||||||
|
no_ip = true;
|
||||||
|
ignore_defaults = true;
|
||||||
|
exclude_users = false;
|
||||||
|
emails_domainonly = true;
|
||||||
|
selector = {
|
||||||
|
from_smtp = "from('smtp'):domain";
|
||||||
|
from_mime = "from('mime'):domain";
|
||||||
|
mid = "header(Message-Id).regexp('@([^\.]+\.[^>]+)').last";
|
||||||
|
}
|
||||||
|
returncodes = {
|
||||||
|
DBL_SPAM = "127.0.1.2";
|
||||||
|
DBL_PHISH = "127.0.1.4";
|
||||||
|
DBL_MALWARE = "127.0.1.5";
|
||||||
|
DBL_BOTNET = "127.0.1.6";
|
||||||
|
DBL_ABUSE = "127.0.1.102";
|
||||||
|
DBL_ABUSE_REDIR = "127.0.1.103";
|
||||||
|
DBL_ABUSE_PHISH = "127.0.1.104";
|
||||||
|
DBL_ABUSE_MALWARE = "127.0.1.105";
|
||||||
|
DBL_ABUSE_BOTNET = "127.0.1.106";
|
||||||
|
DBL_PROHIBIT = "127.0.1.255";
|
||||||
|
DBL_BLOCKED_OPENRESOLVER = "127.255.255.254";
|
||||||
|
DBL_BLOCKED = "127.255.255.255";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"ZRD" {
|
"ZRD" {
|
||||||
ignore_defaults = true;
|
ignore_defaults = true;
|
||||||
|
|||||||
@@ -116,8 +116,8 @@ if ($result === false){
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
if ($result) {
|
if ($result) {
|
||||||
error_log('MAILCOWAUTH: User auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $client_ip);
|
error_log('MAILCOWAUTH: User auth for user ' . $post['username'] . " with service " . $service . " from IP " . $client_ip);
|
||||||
set_sasl_log($post['username'], $client_ip, $post['service']);
|
set_sasl_log($post['username'], $client_ip, $service);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -199,8 +199,6 @@ plugin {
|
|||||||
sieve_plugins = sieve_imapsieve sieve_extprograms
|
sieve_plugins = sieve_imapsieve sieve_extprograms
|
||||||
sieve_vacation_send_from_recipient = yes
|
sieve_vacation_send_from_recipient = yes
|
||||||
sieve_redirect_envelope_from = recipient
|
sieve_redirect_envelope_from = recipient
|
||||||
# Notifications are sent with the address of the mailbox (not :from or postmaster), so mail policies apply
|
|
||||||
sieve_notify_mailto_envelope_from = recipient
|
|
||||||
# From elsewhere to Spam folder
|
# From elsewhere to Spam folder
|
||||||
imapsieve_mailbox1_name = Junk
|
imapsieve_mailbox1_name = Junk
|
||||||
imapsieve_mailbox1_causes = COPY
|
imapsieve_mailbox1_causes = COPY
|
||||||
|
|||||||
@@ -150,15 +150,6 @@ http {
|
|||||||
access_log /var/log/nginx/access.log;
|
access_log /var/log/nginx/access.log;
|
||||||
root /dynmaps;
|
root /dynmaps;
|
||||||
|
|
||||||
# backup endpoint of the restic runner: large restore requests, long merges of mailboxes and domains
|
|
||||||
location = /backup.php {
|
|
||||||
client_max_body_size 32m;
|
|
||||||
fastcgi_read_timeout 600s;
|
|
||||||
fastcgi_pass {{ PHPFPMHOST }}:9001;
|
|
||||||
include fastcgi_params;
|
|
||||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ \.php$ {
|
location ~ \.php$ {
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||||
|
|||||||
@@ -94,11 +94,11 @@ location / {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /qhandler {
|
location /qhandler {
|
||||||
rewrite ^/qhandler/(.*)/(.*) /qhandler.php?action=$1&hash=$2;
|
rewrite ^/qhandler/([^/]+)/(.+?)/?$ /qhandler.php?action=$1&hash=$2;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /edit {
|
location /edit {
|
||||||
rewrite ^/edit/(.*)/(.*) /edit.php?$1=$2;
|
rewrite ^/edit/([^/]+)/(.+?)/?$ /edit.php?$1=$2;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ ^/api/v1/(.*)$ {
|
location ~ ^/api/v1/(.*)$ {
|
||||||
@@ -187,6 +187,12 @@ location /sogo-auth-verify {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location ^~ /Microsoft-Server-ActiveSync {
|
location ^~ /Microsoft-Server-ActiveSync {
|
||||||
|
# Outlook for iOS/Android (Microsoft's sync service) sends EAS commands without credentials
|
||||||
|
# and only authenticates after a 401 Basic challenge. SOGo answers those with 403, so challenge here.
|
||||||
|
if ($http_authorization = "") {
|
||||||
|
add_header WWW-Authenticate 'Basic realm="SOGo"' always;
|
||||||
|
return 401;
|
||||||
|
}
|
||||||
auth_request /sogo-auth-verify;
|
auth_request /sogo-auth-verify;
|
||||||
auth_request_set $user $upstream_http_x_user;
|
auth_request_set $user $upstream_http_x_user;
|
||||||
auth_request_set $auth $upstream_http_x_auth;
|
auth_request_set $auth $upstream_http_x_auth;
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ postscreen_greet_wait = 3s
|
|||||||
postscreen_non_smtp_command_enable = no
|
postscreen_non_smtp_command_enable = no
|
||||||
postscreen_pipelining_enable = no
|
postscreen_pipelining_enable = no
|
||||||
proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf,
|
proxy_read_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_transport_maps.cf,
|
||||||
proxy:mysql:/opt/postfix/conf/sql/mysql_mail_policy_maps.cf,
|
|
||||||
proxy:mysql:/opt/postfix/conf/sql/mysql_mbr_access_maps.cf,
|
proxy:mysql:/opt/postfix/conf/sql/mysql_mbr_access_maps.cf,
|
||||||
proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf,
|
proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf,
|
||||||
$sender_dependent_default_transport_maps,
|
$sender_dependent_default_transport_maps,
|
||||||
@@ -91,7 +90,6 @@ smtpd_hard_error_limit = ${stress?1}${stress:5}
|
|||||||
smtpd_helo_required = yes
|
smtpd_helo_required = yes
|
||||||
smtpd_proxy_timeout = 600s
|
smtpd_proxy_timeout = 600s
|
||||||
smtpd_recipient_restrictions = check_recipient_mx_access proxy:mysql:/opt/postfix/conf/sql/mysql_mbr_access_maps.cf,
|
smtpd_recipient_restrictions = check_recipient_mx_access proxy:mysql:/opt/postfix/conf/sql/mysql_mbr_access_maps.cf,
|
||||||
check_recipient_access proxy:mysql:/opt/postfix/conf/sql/mysql_mail_policy_maps.cf,
|
|
||||||
permit_sasl_authenticated,
|
permit_sasl_authenticated,
|
||||||
permit_mynetworks,
|
permit_mynetworks,
|
||||||
check_recipient_access proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf,
|
check_recipient_access proxy:mysql:/opt/postfix/conf/sql/mysql_tls_enforce_in_policy.cf,
|
||||||
@@ -172,11 +170,5 @@ parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks
|
|||||||
# This Option is added to correctly set the X-Original-To Header when mails are send to lmtp (dovecot)
|
# This Option is added to correctly set the X-Original-To Header when mails are send to lmtp (dovecot)
|
||||||
lmtp_destination_recipient_limit=1
|
lmtp_destination_recipient_limit=1
|
||||||
|
|
||||||
# Mail policies: while policies or internal aliases exist, mysql_mail_policy_maps.cf returns this class and smtpd
|
|
||||||
# asks the policy service for every recipient (mailcow-policy in master.cf); no answer defers the recipient
|
|
||||||
smtpd_restriction_classes = mailcow_mail_policy
|
|
||||||
mailcow_mail_policy = check_policy_service { unix:private/mailcow-policy, timeout=10s, try_limit=1, default_action=DEFER_IF_PERMIT }
|
|
||||||
mailcow-policy_time_limit = 3600s
|
|
||||||
|
|
||||||
# DO NOT EDIT ANYTHING BELOW #
|
# DO NOT EDIT ANYTHING BELOW #
|
||||||
# Overrides #
|
# Overrides #
|
||||||
@@ -117,9 +117,6 @@ smtp_sender_cleanup unix n - y - 0 cleanup
|
|||||||
127.0.0.1:10027 inet n n n - 0 spawn user=nobody argv=/usr/local/bin/whitelist_forwardinghosts.sh
|
127.0.0.1:10027 inet n n n - 0 spawn user=nobody argv=/usr/local/bin/whitelist_forwardinghosts.sh
|
||||||
# end whitelist_fwd
|
# end whitelist_fwd
|
||||||
|
|
||||||
# mail policies: policy service of smtpd, asks the mailcow UI (mailcow_mail_policy in main.cf)
|
|
||||||
mailcow-policy unix - n n - 0 spawn user=nobody argv=/usr/local/bin/mailcow-policy.py
|
|
||||||
|
|
||||||
# start watchdog-specific
|
# start watchdog-specific
|
||||||
# logs to local7 (hidden)
|
# logs to local7 (hidden)
|
||||||
589 inet n - n - - smtpd
|
589 inet n - n - - smtpd
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Backup with restic: endpoint of the runner in the restic container (Mailcow\Backup)
|
|
||||||
// GET ?domains=1: {"domains": [...]}, all domains
|
|
||||||
// GET ?export=<domain>[&after=<username>]: a page of the export of a domain (the first with domain_data and aliases,
|
|
||||||
// every page with a part of the mailboxes and "next_after", the last username to ask for the next page with, null
|
|
||||||
// after the last), incl. password hashes, DKIM keys and secrets; the runner merges the pages into
|
|
||||||
// /mailcow/export/<domain>.json of the snapshot.
|
|
||||||
// POST {"action": "restore_domain", "job": <id>, "data": <domain_data>}: recreates a deleted domain
|
|
||||||
// POST {"action": "restore_aliases", "job": <id>, "aliases": [...]}: a part (at most 500) of the aliases of a domain restore
|
|
||||||
// POST {"action": "restore", "job": <id>, "username": <mailbox>, "data": <its section of the export>}: one mailbox
|
|
||||||
// (created first when it was deleted); all answer {"result": {<component>: {"added", "skipped", "errors"}}}
|
|
||||||
// All need the header X-Backup-Token: HMAC-SHA256 of "backup" with the database password as key
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
require_once "vars.inc.php";
|
|
||||||
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
|
|
||||||
// Do not show errors, we log to using error_log
|
|
||||||
ini_set('error_reporting', 0);
|
|
||||||
if (empty($database_pass) || !hash_equals(hash_hmac('sha256', 'backup', $database_pass), (string)($_SERVER['HTTP_X_BACKUP_TOKEN'] ?? ''))) {
|
|
||||||
http_response_code(403);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
// Init database
|
|
||||||
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
|
||||||
$opt = [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
|
||||||
];
|
|
||||||
try {
|
|
||||||
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
error_log("BACKUP: " . $e . PHP_EOL);
|
|
||||||
http_response_code(501);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// DKIM keys, rate limits and the domain map are kept in Redis
|
|
||||||
$redis = new Redis();
|
|
||||||
if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
|
|
||||||
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$redis->connect('redis-mailcow', 6379);
|
|
||||||
}
|
|
||||||
$redis->auth(getenv("REDISPASS"));
|
|
||||||
$config = array(
|
|
||||||
'timezone' => getenv('TZ') ?: 'UTC',
|
|
||||||
'alias_disable_external_domains' => ($ALIAS_DISABLE_EXTERNAL_DOMAINS ?? false) === true,
|
|
||||||
);
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$body = json_decode((string)file_get_contents('php://input'), true);
|
|
||||||
if (!is_array($body)) {
|
|
||||||
http_response_code(400);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
// The restore has the side effects of the UI: SOGo view and restart, Dovecot auth cache, group shares (legacy code)
|
|
||||||
require_once __DIR__ . '/../web/inc/functions.inc.php';
|
|
||||||
require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
|
|
||||||
require_once __DIR__ . '/../web/inc/functions.docker.inc.php';
|
|
||||||
require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php';
|
|
||||||
require_once __DIR__ . '/../web/inc/functions.ratelimit.inc.php';
|
|
||||||
require_once __DIR__ . '/../web/inc/functions.acl.inc.php';
|
|
||||||
// The legacy mailbox functions run as admin, without a session
|
|
||||||
$_SESSION = array(
|
|
||||||
'mailcow_cc_username' => 'admin',
|
|
||||||
'mailcow_cc_role' => 'admin',
|
|
||||||
'access_all_exception' => '0',
|
|
||||||
'acl' => array_fill_keys(array('tls_policy', 'quarantine_notification', 'quarantine_category', 'ratelimit', 'sogo_access',
|
|
||||||
'protocol_access', 'mailbox_relayhost', 'unlimited_quota'), '1'),
|
|
||||||
);
|
|
||||||
$backup = \Mailcow\Backup\BackupModule::create($pdo, \Mailcow\Core\Context::system(), $config, $redis);
|
|
||||||
echo json_encode($backup->api->post($body), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$backup = \Mailcow\Backup\BackupModule::create($pdo, \Mailcow\Core\Context::system(), $config, $redis);
|
|
||||||
if (isset($_GET['export'])) {
|
|
||||||
echo json_encode($backup->api->export((string)$_GET['export'], (isset($_GET['after']) && is_string($_GET['after'])) ? $_GET['after'] : null),
|
|
||||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
|
||||||
}
|
|
||||||
elseif (isset($_GET['domains'])) {
|
|
||||||
echo json_encode($backup->api->domains());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
http_response_code(400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (\Mailcow\Core\UserError $e) {
|
|
||||||
http_response_code(($e->key === 'domain_not_found') ? 404 : 400);
|
|
||||||
echo json_encode(array('error' => $e->key, 'args' => $e->args));
|
|
||||||
}
|
|
||||||
catch (Throwable $e) {
|
|
||||||
error_log("BACKUP: " . $e->getMessage() . PHP_EOL);
|
|
||||||
http_response_code(500);
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Mail policy check for one recipient, asked by the Postfix policy service (mailcow-policy in master.cf)
|
|
||||||
// Request: POST with the attributes of the Postfix policy request; answer: the action (DUNNO or the rejection)
|
|
||||||
header('Content-Type: text/plain');
|
|
||||||
require_once "vars.inc.php";
|
|
||||||
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
|
|
||||||
// Do not show errors, we log to using error_log
|
|
||||||
ini_set('error_reporting', 0);
|
|
||||||
// Init database
|
|
||||||
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
|
||||||
$opt = [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
|
||||||
];
|
|
||||||
try {
|
|
||||||
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
error_log("MAILPOLICY: " . $e . PHP_EOL);
|
|
||||||
http_response_code(501);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($_POST['request']) || $_POST['request'] !== 'smtpd_access_policy') {
|
|
||||||
http_response_code(400);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
$value = fn(string $key): string => is_string($_POST[$key] ?? null) ? $_POST[$key] : '';
|
|
||||||
|
|
||||||
try {
|
|
||||||
$policy = \Mailcow\MailPolicy\MailPolicyModule::create($pdo, \Mailcow\Core\Context::system());
|
|
||||||
echo $policy->api->check($value('sender'), $value('recipient'), $value('sasl_username'), (int)$value('server_port'), $value('helo_name'));
|
|
||||||
}
|
|
||||||
catch (Throwable $e) {
|
|
||||||
// the policy service answers with a temporary error
|
|
||||||
error_log("MAILPOLICY: " . $e->getMessage() . PHP_EOL);
|
|
||||||
http_response_code(500);
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Retention rules for the nightly run in the dovecot container (retention_runner.py)
|
|
||||||
// GET ?after=<username>&limit=<n>: a page of the plan, per mailbox the folders to clean up (JSON)
|
|
||||||
// POST (JSON {"results": [...], "summary": [...], "run": {...}}): the results
|
|
||||||
// Both need the header X-Retention-Token: HMAC-SHA256 of "retention" with the database password as key
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
require_once "vars.inc.php";
|
|
||||||
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
|
|
||||||
// Do not show errors, we log to using error_log
|
|
||||||
ini_set('error_reporting', 0);
|
|
||||||
if (empty($database_pass) || !hash_equals(hash_hmac('sha256', 'retention', $database_pass), (string)($_SERVER['HTTP_X_RETENTION_TOKEN'] ?? ''))) {
|
|
||||||
http_response_code(403);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
// Init database
|
|
||||||
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
|
||||||
$opt = [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
||||||
PDO::ATTR_EMULATE_PREPARES => false,
|
|
||||||
];
|
|
||||||
try {
|
|
||||||
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
|
||||||
}
|
|
||||||
catch (PDOException $e) {
|
|
||||||
error_log("RETENTION: " . $e . PHP_EOL);
|
|
||||||
http_response_code(501);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$retention = \Mailcow\Retention\RetentionModule::create($pdo, \Mailcow\Core\Context::system());
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$body = json_decode((string)file_get_contents('php://input'), true);
|
|
||||||
if (!is_array($body) || !is_array($body['results'] ?? null)) {
|
|
||||||
http_response_code(400);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
echo json_encode(['stored' => $retention->api->report($body['results'], is_array($body['summary'] ?? null) ? $body['summary'] : [],
|
|
||||||
is_array($body['run'] ?? null) ? $body['run'] : null)]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
echo json_encode($retention->api->plan((string)($_GET['after'] ?? ''), (int)($_GET['limit'] ?? 0)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (\Mailcow\Core\UserError $e) {
|
|
||||||
http_response_code(400);
|
|
||||||
}
|
|
||||||
catch (Throwable $e) {
|
|
||||||
error_log("RETENTION: " . $e->getMessage() . PHP_EOL);
|
|
||||||
http_response_code(500);
|
|
||||||
}
|
|
||||||
@@ -469,5 +469,35 @@ while ($row = array_shift($rows)) {
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Start internal aliases
|
||||||
|
|
||||||
|
$stmt = $pdo->query("SELECT `id`, `address`, `domain` FROM `alias` WHERE `active` = '1' AND `internal` = '1'");
|
||||||
|
$aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
while ($alias = array_shift($aliases)) {
|
||||||
|
// build allowed_domains regex and add target domain and alias domains
|
||||||
|
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `active` = '1' AND `target_domain` = :target_domain");
|
||||||
|
$stmt->execute(array(':target_domain' => $alias['domain']));
|
||||||
|
$allowed_domains = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
$allowed_domains = array_map(function($item) {
|
||||||
|
return str_replace('.', '\.', $item['alias_domain']);
|
||||||
|
}, $allowed_domains);
|
||||||
|
$allowed_domains[] = str_replace('.', '\.', $alias['domain']);
|
||||||
|
$allowed_domains = implode('|', $allowed_domains);
|
||||||
|
?>
|
||||||
|
internal_alias_<?=$alias['id'];?> {
|
||||||
|
priority = 10;
|
||||||
|
rcpt = "<?=$alias['address'];?>";
|
||||||
|
from = "/^((?!.*@(<?=$allowed_domains;?>)).)*$/";
|
||||||
|
apply "default" {
|
||||||
|
MAILCOW_INTERNAL_ALIAS = 9999.0;
|
||||||
|
}
|
||||||
|
symbols [
|
||||||
|
"MAILCOW_INTERNAL_ALIAS"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
MX_IMPLICIT {
|
|
||||||
expression = "MX_GOOD & MX_MISSING";
|
|
||||||
score = -0.01;
|
|
||||||
}
|
|
||||||
VIRUS_FOUND {
|
VIRUS_FOUND {
|
||||||
expression = "CLAM_VIRUS & !MAILCOW_WHITE";
|
expression = "CLAM_VIRUS & !MAILCOW_WHITE";
|
||||||
score = 2000.0;
|
score = 2000.0;
|
||||||
|
|||||||
@@ -23,24 +23,6 @@ symbols {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
group "MX" {
|
|
||||||
"MX_INVALID" {
|
|
||||||
score = 0.5;
|
|
||||||
description = "No connectable MX";
|
|
||||||
one_shot = true;
|
|
||||||
}
|
|
||||||
"MX_MISSING" {
|
|
||||||
score = 2.0;
|
|
||||||
description = "No MX record";
|
|
||||||
one_shot = true;
|
|
||||||
}
|
|
||||||
"MX_GOOD" {
|
|
||||||
score = -0.01;
|
|
||||||
description = "MX was ok";
|
|
||||||
one_shot = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
group "reputation" {
|
group "reputation" {
|
||||||
symbols = {
|
symbols = {
|
||||||
"IP_REPUTATION_HAM" {
|
"IP_REPUTATION_HAM" {
|
||||||
|
|||||||
@@ -5,12 +5,6 @@ RCPT_MAILCOW_DOMAIN {
|
|||||||
symbols_set = ["RCPT_MAILCOW_DOMAIN"];
|
symbols_set = ["RCPT_MAILCOW_DOMAIN"];
|
||||||
}
|
}
|
||||||
|
|
||||||
WHITELISTED_FWD_HOST {
|
|
||||||
type = "ip";
|
|
||||||
map = "redis://WHITELISTED_FWD_HOST";
|
|
||||||
symbols_set = ["WHITELISTED_FWD_HOST"];
|
|
||||||
}
|
|
||||||
|
|
||||||
BULK_HEADER {
|
BULK_HEADER {
|
||||||
type = "content";
|
type = "content";
|
||||||
map = "${LOCAL_CONFDIR}/custom/bulk_header.map";
|
map = "${LOCAL_CONFDIR}/custom/bulk_header.map";
|
||||||
@@ -151,20 +145,6 @@ BAD_LANG {
|
|||||||
score = 5.0;
|
score = 5.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
BAZAAR_ABUSE_CH {
|
|
||||||
type = "selector";
|
|
||||||
selector = "attachments(hex,md5)";
|
|
||||||
map = "https://bazaar.abuse.ch/export/txt/md5/recent/";
|
|
||||||
score = 10.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
URLHAUS_ABUSE_CH {
|
|
||||||
type = "selector";
|
|
||||||
selector = "urls";
|
|
||||||
map = "https://urlhaus.abuse.ch/downloads/text_online/";
|
|
||||||
score = 10.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
SMTP_LIMITED_ACCESS {
|
SMTP_LIMITED_ACCESS {
|
||||||
type = "user";
|
type = "user";
|
||||||
map = "redis://SMTP_LIMITED_ACCESS";
|
map = "redis://SMTP_LIMITED_ACCESS";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
timeout = 8.0;
|
timeout = 8.0;
|
||||||
symbol_bad_mx = "MX_INVALID";
|
symbol_bad_mx = "MX_INVALID";
|
||||||
symbol_no_mx = "MX_MISSING";
|
|
||||||
symbol_good_mx = "MX_GOOD";
|
symbol_good_mx = "MX_GOOD";
|
||||||
expire = 86400;
|
expire = 86400;
|
||||||
key_prefix = "rmx";
|
key_prefix = "rmx";
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ disable_monitoring = true;
|
|||||||
# instead of silently accepting the message without further processing.
|
# instead of silently accepting the message without further processing.
|
||||||
soft_reject_on_timeout = true;
|
soft_reject_on_timeout = true;
|
||||||
local_addrs = /etc/rspamd/custom/mailcow_networks.map;
|
local_addrs = /etc/rspamd/custom/mailcow_networks.map;
|
||||||
|
# Must not be lower than message_size_limit in postfix main.cf
|
||||||
|
max_message = 134217728;
|
||||||
@@ -362,6 +362,61 @@ rspamd_config:register_symbol({
|
|||||||
priority = 19
|
priority = 19
|
||||||
})
|
})
|
||||||
|
|
||||||
|
rspamd_config:register_symbol({
|
||||||
|
name = 'WHITELISTED_FWD_HOST',
|
||||||
|
type = 'prefilter',
|
||||||
|
score = 0.0,
|
||||||
|
callback = function(task)
|
||||||
|
local rspamd_logger = require "rspamd_logger"
|
||||||
|
local redis_params = rspamd_parse_redis_server('fwd_host')
|
||||||
|
local ip = task:get_from_ip()
|
||||||
|
|
||||||
|
if ip == nil or not ip:is_valid() then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local from_ip_string = tostring(ip)
|
||||||
|
local ip_check_table = {'WHITELISTED_FWD_HOST', from_ip_string}
|
||||||
|
|
||||||
|
local maxbits = 128
|
||||||
|
local minbits = 7
|
||||||
|
if ip:get_version() == 4 then
|
||||||
|
maxbits = 32
|
||||||
|
minbits = 8
|
||||||
|
end
|
||||||
|
for i=maxbits,minbits,-1 do
|
||||||
|
table.insert(ip_check_table, ip:apply_mask(i):to_string() .. "/" .. i)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function fwd_host_cb(err, data)
|
||||||
|
if err or type(data) ~= 'table' then
|
||||||
|
rspamd_logger.infox(rspamd_config, "fwd_host query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
for _,v in pairs(data) do
|
||||||
|
if type(v) == 'string' then
|
||||||
|
task:insert_result('WHITELISTED_FWD_HOST', 1.0, from_ip_string)
|
||||||
|
task:set_flag('pass_all')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local redis_ret = rspamd_redis_make_request(task,
|
||||||
|
redis_params, -- connect params
|
||||||
|
'WHITELISTED_FWD_HOST', -- hash key
|
||||||
|
false, -- is write
|
||||||
|
fwd_host_cb, --callback
|
||||||
|
'HMGET', -- command
|
||||||
|
ip_check_table -- arguments
|
||||||
|
)
|
||||||
|
if not redis_ret then
|
||||||
|
rspamd_logger.infox(rspamd_config, "cannot check fwd_host redis map")
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
priority = 18
|
||||||
|
})
|
||||||
|
|
||||||
rspamd_config:register_symbol({
|
rspamd_config:register_symbol({
|
||||||
name = 'TLS_HEADER',
|
name = 'TLS_HEADER',
|
||||||
type = 'postfilter',
|
type = 'postfilter',
|
||||||
@@ -390,9 +445,8 @@ rspamd_config:register_symbol({
|
|||||||
local rspamd_logger = require "rspamd_logger"
|
local rspamd_logger = require "rspamd_logger"
|
||||||
local redis_params = rspamd_parse_redis_server('taghandler')
|
local redis_params = rspamd_parse_redis_server('taghandler')
|
||||||
local rspamd_http = require "rspamd_http"
|
local rspamd_http = require "rspamd_http"
|
||||||
local rcpts = task:get_recipients('smtp')
|
local rcpts = task:get_recipients({'smtp', 'orig'})
|
||||||
local lua_util = require "lua_util"
|
local lua_util = require "lua_util"
|
||||||
local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
|
|
||||||
|
|
||||||
local function remove_moo_tag()
|
local function remove_moo_tag()
|
||||||
local moo_tag_header = task:get_header('X-Moo-Tag', false)
|
local moo_tag_header = task:get_header('X-Moo-Tag', false)
|
||||||
@@ -417,8 +471,9 @@ rspamd_config:register_symbol({
|
|||||||
|
|
||||||
-- Check if recipient has a tag (contains '+')
|
-- Check if recipient has a tag (contains '+')
|
||||||
local tag = nil
|
local tag = nil
|
||||||
if tagged_rcpt ~= nil then
|
local base_user, tag_part = rcpt_user:match('^([^%+]+)%+(.+)$')
|
||||||
tag = tagged_rcpt
|
if base_user and tag_part then
|
||||||
|
tag = tag_part
|
||||||
rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag)
|
rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -498,8 +553,7 @@ rspamd_config:register_symbol({
|
|||||||
else
|
else
|
||||||
rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
|
rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
|
||||||
local sbj = task:get_header('Subject') or ''
|
local sbj = task:get_header('Subject') or ''
|
||||||
local tag_value = tag[1] and tag[1].options and tag[1].options[1] or ''
|
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
|
||||||
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag_value .. '] ' .. sbj)) .. '?='
|
|
||||||
task:set_milter_reply({
|
task:set_milter_reply({
|
||||||
remove_headers = {
|
remove_headers = {
|
||||||
['Subject'] = 1,
|
['Subject'] = 1,
|
||||||
@@ -907,6 +961,9 @@ rspamd_config:register_symbol({
|
|||||||
|
|
||||||
if not seen_cte and rewrite.need_rewrite_ct then
|
if not seen_cte and rewrite.need_rewrite_ct then
|
||||||
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', new_cte)
|
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', new_cte)
|
||||||
|
task:set_milter_reply({
|
||||||
|
add_headers = {['Content-Transfer-Encoding'] = new_cte}
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
-- End of headers
|
-- End of headers
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ foreach ($rcpts as $rcpt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skip if domain is excluded
|
// Skip if domain is excluded
|
||||||
if (in_array($parsed_rcpt['domain'], $exclude_domains)) {
|
if (is_array($exclude_domains) && in_array($parsed_rcpt['domain'], $exclude_domains)) {
|
||||||
error_log(sprintf("QUARANTINE: Skipped domain %s", $parsed_rcpt['domain']) . PHP_EOL);
|
error_log(sprintf("QUARANTINE: Skipped domain %s", $parsed_rcpt['domain']) . PHP_EOL);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
|||||||
|
|
||||||
$js_minifier->add('/web/js/site/imapsync.js');
|
$js_minifier->add('/web/js/site/imapsync.js');
|
||||||
$js_minifier->add('/web/js/site/groups.js');
|
$js_minifier->add('/web/js/site/groups.js');
|
||||||
$js_minifier->add('/web/js/site/mail-policy.js');
|
|
||||||
$js_minifier->add('/web/js/site/retention.js');
|
|
||||||
$js_minifier->add('/web/js/site/mailbox.js');
|
$js_minifier->add('/web/js/site/mailbox.js');
|
||||||
$js_minifier->add('/web/js/presets/sieveMailbox.js');
|
$js_minifier->add('/web/js/presets/sieveMailbox.js');
|
||||||
$js_minifier->add('/web/js/site/pwgen.js');
|
$js_minifier->add('/web/js/site/pwgen.js');
|
||||||
@@ -48,8 +46,6 @@ $template_data = [
|
|||||||
'lang_edit' => json_encode($lang['edit']),
|
'lang_edit' => json_encode($lang['edit']),
|
||||||
'lang_datatables' => json_encode($lang['datatables']),
|
'lang_datatables' => json_encode($lang['datatables']),
|
||||||
'imapsync_redirect_uri' => \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->oauth->redirectUri(),
|
'imapsync_redirect_uri' => \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->oauth->redirectUri(),
|
||||||
'mail_policy_groups' => \Mailcow\MailPolicy\MailPolicyModule::fromGlobals()->policies->groupChoices($domains),
|
|
||||||
'retention_groups' => \Mailcow\Retention\RetentionModule::fromGlobals()->rules->groupChoices($domains),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
|
||||||
|
|
||||||
protect_route(['admin']);
|
|
||||||
|
|
||||||
$role = "admin";
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/restore.inc.php';
|
|
||||||
@@ -13,7 +13,6 @@ $js_minifier->add('/web/js/site/admin.js');
|
|||||||
$js_minifier->add('/web/js/presets/rspamd.js');
|
$js_minifier->add('/web/js/presets/rspamd.js');
|
||||||
$js_minifier->add('/web/js/site/pwgen.js');
|
$js_minifier->add('/web/js/site/pwgen.js');
|
||||||
$js_minifier->add('/web/js/site/identity.js');
|
$js_minifier->add('/web/js/site/identity.js');
|
||||||
$js_minifier->add('/web/js/site/backup.js');
|
|
||||||
|
|
||||||
// all domains
|
// all domains
|
||||||
$domains = mailbox('get', 'domains');
|
$domains = mailbox('get', 'domains');
|
||||||
@@ -122,9 +121,6 @@ $template_data = [
|
|||||||
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
|
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
|
||||||
'iam_settings' => $iam_settings,
|
'iam_settings' => $iam_settings,
|
||||||
'mbox_templates' => $mbox_templates,
|
'mbox_templates' => $mbox_templates,
|
||||||
'backup_timezone' => getenv('TZ') ?: 'UTC',
|
|
||||||
'backup_hostname' => getenv('MAILCOW_HOSTNAME') ?: '',
|
|
||||||
'backup_demo_mode' => $DEMO_MODE,
|
|
||||||
'lang_admin' => json_encode($lang['admin']),
|
'lang_admin' => json_encode($lang['admin']),
|
||||||
'lang_datatables' => json_encode($lang['datatables'])
|
'lang_datatables' => json_encode($lang['datatables'])
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -14,3 +14,19 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* mailcow: inline code in descriptions in the text colour instead of bold purple */
|
||||||
|
.swagger-ui .markdown code,
|
||||||
|
.swagger-ui .renderedMarkdown code {
|
||||||
|
color: inherit;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 0.9em;
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
padding: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark-mode .swagger-ui .markdown code,
|
||||||
|
html.dark-mode .swagger-ui .renderedMarkdown code {
|
||||||
|
color: inherit;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
@@ -1,79 +1,6 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en-US">
|
<html lang="en-US">
|
||||||
<head>
|
|
||||||
<title>Swagger UI: OAuth2 Redirect</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
<body>
|
||||||
<script>
|
<script src="oauth2-redirect.js"></script>
|
||||||
'use strict';
|
|
||||||
function run () {
|
|
||||||
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
|
||||||
var sentState = oauth2.state;
|
|
||||||
var redirectUrl = oauth2.redirectUrl;
|
|
||||||
var isValid, qp, arr;
|
|
||||||
|
|
||||||
if (/code|token|error/.test(window.location.hash)) {
|
|
||||||
qp = window.location.hash.substring(1).replace('?', '&');
|
|
||||||
} else {
|
|
||||||
qp = location.search.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
arr = qp.split("&");
|
|
||||||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
|
|
||||||
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
|
||||||
function (key, value) {
|
|
||||||
return key === "" ? value : decodeURIComponent(value);
|
|
||||||
}
|
|
||||||
) : {};
|
|
||||||
|
|
||||||
isValid = qp.state === sentState;
|
|
||||||
|
|
||||||
if ((
|
|
||||||
oauth2.auth.schema.get("flow") === "accessCode" ||
|
|
||||||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
|
|
||||||
oauth2.auth.schema.get("flow") === "authorization_code"
|
|
||||||
) && !oauth2.auth.code) {
|
|
||||||
if (!isValid) {
|
|
||||||
oauth2.errCb({
|
|
||||||
authId: oauth2.auth.name,
|
|
||||||
source: "auth",
|
|
||||||
level: "warning",
|
|
||||||
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (qp.code) {
|
|
||||||
delete oauth2.state;
|
|
||||||
oauth2.auth.code = qp.code;
|
|
||||||
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
|
||||||
} else {
|
|
||||||
let oauthErrorMsg;
|
|
||||||
if (qp.error) {
|
|
||||||
oauthErrorMsg = "["+qp.error+"]: " +
|
|
||||||
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
|
||||||
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
|
||||||
}
|
|
||||||
|
|
||||||
oauth2.errCb({
|
|
||||||
authId: oauth2.auth.name,
|
|
||||||
source: "auth",
|
|
||||||
level: "error",
|
|
||||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
|
||||||
}
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (document.readyState !== 'loading') {
|
|
||||||
run();
|
|
||||||
} else {
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
run();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"use strict";function run(){var e,r,t,a=window.opener.swaggerUIRedirectOauth2,o=a.state,n=a.redirectUrl;if((t=(r=/code|token|error/.test(window.location.hash)?window.location.hash.substring(1).replace("?","&"):location.search.substring(1)).split("&")).forEach((function(e,r,t){t[r]='"'+e.replace("=",'":"')+'"'})),e=(r=r?JSON.parse("{"+t.join()+"}",(function(e,r){return""===e?r:decodeURIComponent(r)})):{}).state===o,"accessCode"!==a.auth.schema.get("flow")&&"authorizationCode"!==a.auth.schema.get("flow")&&"authorization_code"!==a.auth.schema.get("flow")||a.auth.code)a.callback({auth:a.auth,token:r,isValid:e,redirectUrl:n});else if(e||a.errCb({authId:a.auth.name,source:"auth",level:"warning",message:"Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"}),r.code)delete a.state,a.auth.code=r.code,a.callback({auth:a.auth,redirectUrl:n});else{let e;r.error&&(e="["+r.error+"]: "+(r.error_description?r.error_description+". ":"no accessCode received from the server. ")+(r.error_uri?"More info: "+r.error_uri:"")),a.errCb({authId:a.auth.name,source:"auth",level:"error",message:e||"[Authorization failed]: no accessCode received from the server"})}window.close()}"loading"!==document.readyState?run():document.addEventListener("DOMContentLoaded",(function(){run()}));
|
||||||
+13923
-9196
File diff suppressed because it is too large.
Load diff
@@ -1,3 +1,2 @@
|
|||||||
/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */
|
/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */
|
||||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SwaggerUIBundle=t():e.SwaggerUIBundle=t()}(this,(()=>(()=>{var e={17967:(e,t)=>{"use strict";t.N=void 0;var n=/^([^\w]*)(javascript|data|vbscript)/im,r=/&#(\w+)(^\w|;)?/g,o=/&(newline|tab);/gi,s=/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim,i=/^.+(:|:)/gim,a=[".","/"];t.N=function(e){var t,l=(t=e||"",t.replace(r,(function(e,t){return String.fromCharCode(t)}))).replace(o,"").replace(s,"").trim();if(!l)return"about:blank";if(function(e){return a.indexOf(e[0])>-1}(l))return l;var c=l.match(i);if(!c)return l;var u=c[0];return n.test(u)?"about:blank":l}},53795:(e,t,n)=>{"use strict";n.d(t,{Z:()=>P});var r=n(23101),o=n.n(r),s=n(61125),i=n.n(s),a=n(11882),l=n.n(a),c=n(97606),u=n.n(c),p=n(67294),h=n(43393);function f(e){return f="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},f(e)}function d(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function m(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function g(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function y(e){return y=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},y(e)}function v(e,t){return v=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},v(e,t)}function b(e,t){return!t||"object"!=typeof t&&"function"!=typeof t?function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e):t}var w={};function E(e,t,n){return function(e){return null==e}(e)?n:function(e){return null!==e&&"object"===f(e)&&"function"==typeof e.get&&"function"==typeof e.has}(e)?e.has(t)?e.get(t):n:hasOwnProperty.call(e,t)?e[t]:n}function x(e,t,n){for(var r=0;r!==t.length;)if((e=E(e,t[r++],w))===w)return n;return e}function S(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=function(e,t){return function(n){if("string"==typeof n)return(0,h.is)(t[n],e[n]);if(Array.isArray(n))return(0,h.is)(x(t,n),x(e,n));throw new TypeError("Invalid key: expected Array or string: "+n)}}(t,n),o=e||Object.keys(function(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?g(n,!0).forEach((function(t){m(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):g(n).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}({},n,{},t));return o.every(r)}const _=function(e){function t(){return function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,t),b(this,y(t).apply(this,arguments))}var n,r,o;return function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&v(e,t)}(t,e),n=t,r=[{key:"shouldComponentUpdate",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return!S(this.updateOnProps,this.props,e,"updateOnProps")||!S(this.updateOnStates,this.state,t,"updateOnStates")}}],r&&d(n.prototype,r),o&&d(n,o),t}(p.Component);var j=n(23930),O=n.n(j),k=n(45697),A=n.n(k);const C=e=>{const t=e.replace(/~1/g,"/").replace(/~0/g,"~");try{return decodeURIComponent(t)}catch{return t}};class P extends _{constructor(){super(...arguments),i()(this,"getModelName",(e=>-1!==l()(e).call(e,"#/definitions/")?C(e.replace(/^.*#\/definitions\//,"")):-1!==l()(e).call(e,"#/components/schemas/")?C(e.replace(/^.*#\/components\/schemas\//,"")):void 0)),i()(this,"getRefSchema",(e=>{let{specSelectors:t}=this.props;return t.findDefinition(e)}))}render(){let{getComponent:e,getConfigs:t,specSelectors:r,schema:s,required:i,name:a,isRef:l,specPath:c,displayName:u,includeReadOnly:h,includeWriteOnly:f}=this.props;const d=e("ObjectModel"),m=e("ArrayModel"),g=e("PrimitiveModel");let y="object",v=s&&s.get("$$ref");if(!a&&v&&(a=this.getModelName(v)),!s&&v&&(s=this.getRefSchema(a)),!s)return p.createElement("span",{className:"model model-title"},p.createElement("span",{className:"model-title__text"},u||a),p.createElement("img",{src:n(2517),height:"20px",width:"20px"}));const b=r.isOAS3()&&s.get("deprecated");switch(l=void 0!==l?l:!!v,y=s&&s.get("type")||y,y){case"object":return p.createElement(d,o()({className:"object"},this.props,{specPath:c,getConfigs:t,scLine truncated
|
!function webpackUniversalModuleDefinition(s,o){"object"==typeof exports&&"object"==typeof module?module.exports=o():"function"==typeof define&&define.amd?define([],o):"object"==typeof exports?exports.SwaggerUIBundle=o():s.SwaggerUIBundle=o()}(this,(()=>(()=>{var s={67526(s,o){"use strict";o.byteLength=function byteLength(s){var o=getLens(s),i=o[0],a=o[1];return 3*(i+a)/4-a},o.toByteArray=function toByteArray(s){var o,i,_=getLens(s),w=_[0],x=_[1],C=new u(function _byteLength(s,o,i){return 3*(o+i)/4-i}(0,w,x)),j=0,L=x>0?w-4:w;for(i=0;i<L;i+=4)o=a[s.charCodeAt(i)]<<18|a[s.charCodeAt(i+1)]<<12|a[s.charCodeAt(i+2)]<<6|a[s.charCodeAt(i+3)],C[j++]=o>>16&255,C[j++]=o>>8&255,C[j++]=255&o;2===x&&(o=a[s.charCodeAt(i)]<<2|a[s.charCodeAt(i+1)]>>4,C[j++]=255&o);1===x&&(o=a[s.charCodeAt(i)]<<10|a[s.charCodeAt(i+1)]<<4|a[s.charCodeAt(i+2)]>>2,C[j++]=o>>8&255,C[j++]=255&o);return C},o.fromByteArray=function fromByteArray(s){for(var o,a=s.length,u=a%3,_=[],w=16383,x=0,C=a-u;x<C;x+=w)_.push(encodeChunk(s,x,x+w>C?C:x+w));1===u?(o=s[a-1],_.push(i[o>>2]+i[o<<4&63]+"==")):2===u&&(o=(s[a-2]<<8)+s[a-1],_.push(i[o>>10]+i[o>>4&63]+i[o<<2&63]+"="));return _.join("")};for(var i=[],a=[],u="undefined"!=typeof Uint8Array?Uint8Array:Array,_="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",w=0;w<64;++w)i[w]=_[w],a[_.charCodeAt(w)]=w;function getLens(s){var o=s.length;if(o%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var i=s.indexOf("=");return-1===i&&(i=o),[i,i===o?0:4-i%4]}function encodeChunk(s,o,a){for(var u,_,w=[],x=o;x<a;x+=3)u=(s[x]<<16&16711680)+(s[x+1]<<8&65280)+(255&s[x+2]),w.push(i[(_=u)>>18&63]+i[_>>12&63]+i[_>>6&63]+i[63&_]);return w.join("")}a["-".charCodeAt(0)]=62,a["_".charCodeAt(0)]=63},48287(s,o,i){"use strict";const a=i(67526),u=i(251),_="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):null;o.Buffer=Buffer,o.SlowBuffer=function SlowBuffer(s){+s!=s&&(s=0);return Buffer.alloc(+s)},o.INSPECT_MAX_BYTES=50;const w=2147483647;function createBuffer(s){if(s>w)throw new RangeError('The value "'+s+'" is invalid for option "size"');const o=new Uint8Array(s);return Object.setPrototypeOf(o,Buffer.prototype),o}function Buffer(s,o,i){if("number"==typeof s){if("string"==typeof o)throw new TypeError('The "string" argument must be of type string. Received type number');return allocUnsafe(s)}return from(s,o,i)}function from(s,o,i){if("string"==typeof s)return function fromString(s,o){"string"==typeof o&&""!==o||(o="utf8");if(!Buffer.isEncoding(o))throw new TypeError("Unknown encoding: "+o);const i=0|byteLength(s,o);let a=createBuffer(i);const u=a.write(s,o);u!==i&&(a=a.slice(0,u));return a}(s,o);if(ArrayBuffer.isView(s))return function fromArrayView(s){if(isInstance(s,Uint8Array)){const o=new Uint8Array(s);return fromArrayBuffer(o.buffer,o.byteOffset,o.byteLength)}return fromArrayLike(s)}(s);if(null==s)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof s);if(isInstance(s,ArrayBuffer)||s&&isInstance(s.buffer,ArrayBuffer))return fromArrayBuffer(s,o,i);if("undefined"!=typeof SharedArrayBuffer&&(isInstance(s,SharedArrayBuffer)||s&&isInstance(s.buffer,SharedArrayBuffer)))return fromArrayBuffer(s,o,i);if("number"==typeof s)throw new TypeError('The "value" argument must not be of type number. Received type number');const a=s.valueOf&&s.valueOf();if(null!=a&&a!==s)return Buffer.from(a,o,i);const u=function fromObject(s){if(Buffer.isBuffer(s)){const o=0|checked(s.length),i=createBuffer(o);return 0===i.length||s.copy(i,0,0,o),i}if(void 0!==s.length)return"number"!=typeof s.length||numberIsNaN(s.length)?createBuffer(0):fromArrayLike(s);if("Buffer"===s.type&&Array.isArray(s.data))return fromArrayLike(s.data)}(s);if(u)return u;if("undefined"!=typeof Symbol&&null!=Symbol.toPrimitive&&"function"==typeof s[Symbol.toPrimitive])return Buffer.from(s[Symbol.toPrimitive]("string"),o,i);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof s)}function assertSize(s){if("number"!=typeof s)throw new TypeError('"size" argument must be of type number');if(s<0)throw new RangeError('The value "'+s+'" is invalid for option "size"')}function allocUnsafe(s){return assertSize(s),createBuffer(s<0?0:0|checked(s))}function fromArrayLike(s){const o=s.length<0?0:0|checked(s.length),i=createBuffer(o);for(let a=0;a<o;a+=1)i[a]=255&s[a];return i}function fromArrayBuffer(s,o,i){if(o<0||s.byteLength<o)throw new RangeError('"offset" is outside of buffer bounds');if(s.byteLength<o+(i||0))throw new RangeError('"length" is outside of buffer bounds');let a;return a=void 0===o&&void 0===i?new Uint8Array(s):void 0===i?new Uint8Array(s,o):new Uint8Array(s,o,i),Object.setPrototypeOf(a,Buffer.prototype),a}function checked(s){if(s>=w)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+w.toString(16)+"
Line truncated
|
||||||
//# sourceMappingURL=swagger-ui-bundle.js.map
|
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/*!
|
||||||
|
Copyright (c) 2018 Jed Watson.
|
||||||
|
Licensed under the MIT License (MIT), see
|
||||||
|
http://jedwatson.github.io/classnames
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* @description Recursive object extending
|
||||||
|
* @author Viacheslav Lotsmanov <lotsmanov89@gmail.com>
|
||||||
|
* @license MIT
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2013-2018 Viacheslav Lotsmanov
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* The buffer module from node.js, for the browser.
|
||||||
|
*
|
||||||
|
* @author Feross Aboukhadijeh <https://feross.org>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* https://github.com/Starcounter-Jack/JSON-Patch
|
||||||
|
* (c) 2017-2021 Joachim Wester
|
||||||
|
* MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* https://github.com/Starcounter-Jack/JSON-Patch
|
||||||
|
* (c) 2017-2022 Joachim Wester
|
||||||
|
* MIT licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* repeat-string <https://github.com/jonschlinkert/repeat-string>
|
||||||
|
*
|
||||||
|
* Copyright (c) 2014-2015, Jon Schlinkert.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*! @license DOMPurify 3.4.17 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.17/LICENSE */
|
||||||
|
|
||||||
|
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */
|
||||||
|
|
||||||
|
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2014-present, Lee Byron and other contributors.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* react-dom.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* react.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* scheduler.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* use-sync-external-store-with-selector.production.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"swagger-ui-bundle.js","mappings":"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 truncated
|
{"version":3,"file":"swagger-ui-bundle.js","mappings":"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 truncated
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
/*! For license information please see swagger-ui-es-bundle-core.js.LICENSE.txt */
|
/*! For license information please see swagger-ui-es-bundle-core.js.LICENSE.txt */
|
||||||
import*as e from"react-immutable-pure-component";import*as t from"remarkable/linkify";import*as a from"dompurify";import*as n from"zenscroll";import*as r from"lodash/reduce";import*as s from"@babel/runtime-corejs3/core-js-stable/string/raw";import*as o from"@babel/runtime-corejs3/core-js-stable/number/epsilon";import*as l from"@babel/runtime-corejs3/core-js-stable/instance/repeat";import*as i from"@babel/runtime-corejs3/core-js-stable/instance/fill";import*as c from"lodash/zipObject";import*as u from"@babel/runtime-corejs3/core-js-stable/promise";import*as d from"@babel/runtime-corejs3/core-js-stable/date/now";import*as p from"lodash/isString";import*as m from"lodash/debounce";import*as f from"lodash/set";import*as h from"lodash/fp/assocPath";import*as g from"swagger-client/es/resolver/strategies/generic";import*as y from"swagger-client/es/resolver/strategies/openapi-2";import*as v from"swagger-client/es/resolver/strategies/openapi-3-0";import*as E from"swagger-client/es/resolver/strategies/openapi-3-1-apidom";import*as S from"swagger-client/es/resolver";import*as w from"swagger-client/es/execute";import*as b from"swagger-client/es/http";import*as x from"swagger-client/es/subtree-resolver";import*as _ from"react-dom";import*as C from"react-redux";import*as j from"lodash/omit";import*as O from"react-syntax-highlighter/dist/esm/light";import*as N from"react-syntax-highlighter/dist/esm/languages/hljs/javascript";import*as k from"react-syntax-highlighter/dist/esm/languages/hljs/json";import*as A from"react-syntax-highlighter/dist/esm/languages/hljs/xml";import*as I from"react-syntax-highlighter/dist/esm/languages/hljs/bash";import*as R from"react-syntax-highlighter/dist/esm/languages/hljs/yaml";import*as P from"react-syntax-highlighter/dist/esm/languages/hljs/http";import*as T from"react-syntax-highlighter/dist/esm/languages/hljs/powershell";import*as M from"react-syntax-highlighter/dist/esm/styles/hljs/agate";import*as D from"react-syntax-highlighter/dist/esm/styles/hljs/arta";import*as L from"react-syntax-highlighter/dist/esm/styles/hljs/monokai";import*as q from"react-syntax-highlighter/dist/esm/styles/hljs/nord";import*as B from"react-syntax-highlighter/dist/esm/styles/hljs/obsidian";import*as U from"react-syntax-highlighter/dist/esm/styles/hljs/tomorrow-night";import*as J from"@babel/runtime-corejs3/core-js-stable/instance/starts-with";import*as $ from"@braintree/sanitize-url";import*as K from"lodash/camelCase";import*as V from"lodash/upperFirst";import*as F from"lodash/find";import*as z from"lodash/eq";import*as W from"css.escape";import*as H from"@babel/runtime-corejs3/core-js-stable/instance/find-index";import*as G from"@babel/runtime-corejs3/core-js-stable/array/from";import*as Z from"@babel/runtime-corejs3/core-js-stable/array/is-array";import*as Y from"@babel/runtime-corejs3/core-js-stable/instance/bind";import*as X from"@babel/runtime-corejs3/core-js-stable/instance/concat";import*as Q from"@babel/runtime-corejs3/core-js-stable/instance/entries";import*as ee from"@babel/runtime-corejs3/core-js-stable/instance/every";import*as te from"@babel/runtime-corejs3/core-js-stable/instance/filter";import*as ae from"@babel/runtime-corejs3/core-js-stable/instance/find";import*as ne from"@babel/runtime-corejs3/core-js-stable/instance/for-each";import*as re from"@babel/runtime-corejs3/core-js-stable/instance/includes";import*as se from"@babel/runtime-corejs3/core-js-stable/instance/index-of";import*as oe from"@babel/runtime-corejs3/core-js-stable/instance/keys";import*as le from"@babel/runtime-corejs3/core-js-stable/instance/map";import*as ie from"@babel/runtime-corejs3/core-js-stable/instance/reduce";import*as ce from"@babel/runtime-corejs3/core-js-stable/instance/slice";import*as ue from"@babel/runtime-corejs3/core-js-stable/instance/some";import*as de from"@babel/runtime-corejs3/core-js-stable/instance/sort";import*as pe from"@babel/runtime-corejs3/core-js-stable/instance/trim";import*as me from"@babel/runtime-corejs3/core-js-stable/json/stringify";import*as fe from"@babel/runtime-corejs3/core-js-stable/map";import*as he from"@babel/runtime-corejs3/core-js-stable/number/is-integer";import*as ge from"@babel/runtime-corejs3/core-js-stable/object/assign";import*as ye from"@babel/runtime-corejs3/core-js-stable/object/entries";import*as ve from"@babel/runtime-corejs3/core-js-stable/object/from-entries";import*as Ee from"@babel/runtime-corejs3/core-js-stable/object/keys";import*as Se from"@babel/runtime-corejs3/core-js-stable/object/values";import*as we from"@babel/runtime-corejs3/core-js-stable/set";import*as be from"@babel/runtime-corejs3/core-js-stable/set-timeout";import*as xe from"@babel/runtime-corejs3/core-js-stable/url";import*as _e from"@babel/runtime-corejs3/core-js-stable/weak-map";import*as Ce from"@babel/runtime-corejs3/core-js-stable/weak-set";import*as je from"@babel/runtime-corejs3/helpers/classPrivateFieldGet";import*as Oe from"@babel/runtime-corejs3/helpers/defineProperty";import*as Ne from"@babelLine truncated
|
import*as e from"base64-js";import*as t from"ieee754";import*as r from"to-buffer";import{Component as n,PureComponent as a,createContext as s,createElement as o,default as i,forwardRef as l,useCallback as c,useContext as u,useEffect as p,useLayoutEffect as m,useMemo as d,useRef as h,useState as f}from"react";import{applyMiddleware as g,bindActionCreators as y,compose as S,createStore as v}from"redux";import*as b from"immutable";import{serializeError as w}from"serialize-error";import{default as _}from"lodash/merge";import"lodash/camelCase";import"lodash/upperFirst";import{default as C}from"lodash/memoize";import{default as x}from"lodash/find";import{default as O}from"lodash/some";import{default as N}from"lodash/eq";import{default as A}from"lodash/isFunction";import{default as k}from"css.escape";import{default as I}from"url-parse";import{createSelector as j}from"reselect";import{default as T}from"prop-types";import{default as R}from"lodash/omit";import{JSON_SCHEMA as M,default as P}from"js-yaml";import{default as B}from"zenscroll";import{default as L}from"react-immutable-proptypes";import{default as q}from"lodash/reduce";import{default as U}from"lodash/get";import{default as D}from"@babel/runtime-corejs3/helpers/extends";import{default as J}from"classnames";import{CopyToClipboard as $}from"react-copy-to-clipboard";import{default as V}from"react-immutable-pure-component";import{useVirtualizer as F,useWindowVirtualizer as z}from"@tanstack/react-virtual";import{default as K}from"react-debounce-input";import{default as W}from"lodash/isPlainObject";import{default as H}from"randexp";import{default as G}from"lodash/isEmpty";import{default as X}from"lodash/constant";import{default as Y}from"lodash/isString";import{default as Q}from"lodash/debounce";import{default as Z}from"lodash/set";import{default as ee}from"lodash/fp/assocPath";import{default as te}from"swagger-client/es/resolver/strategies/generic";import{default as re}from"swagger-client/es/resolver/strategies/openapi-2";import{default as ne}from"swagger-client/es/resolver/strategies/openapi-3-0";import{default as ae}from"swagger-client/es/resolver/strategies/openapi-3-1-apidom";import{default as se}from"swagger-client/es/resolver/strategies/openapi-3-2-apidom";import{makeResolve as oe}from"swagger-client/es/resolver";import{buildRequest as ie,execute as le}from"swagger-client/es/execute";import{default as ce,makeHttp as ue,serializeRes as pe}from"swagger-client/es/http";import{makeResolveSubtree as me}from"swagger-client/es/subtree-resolver";import{opId as de}from"swagger-client/es/helpers";import{default as he}from"react-dom";import{Provider as fe,connect as ge}from"react-redux";import{default as ye}from"lodash/identity";import{default as Se}from"react-syntax-highlighter/dist/esm/light";import{default as Ee}from"react-syntax-highlighter/dist/esm/languages/hljs/javascript";import{default as ve}from"react-syntax-highlighter/dist/esm/languages/hljs/json";import{default as be}from"react-syntax-highlighter/dist/esm/languages/hljs/xml";import{default as we}from"react-syntax-highlighter/dist/esm/languages/hljs/bash";import{default as _e}from"react-syntax-highlighter/dist/esm/languages/hljs/yaml";import{default as Ce}from"react-syntax-highlighter/dist/esm/languages/hljs/http";import{default as xe}from"react-syntax-highlighter/dist/esm/languages/hljs/powershell";import{default as Oe}from"react-syntax-highlighter/dist/esm/styles/hljs/agate";import{default as Ne}from"react-syntax-highlighter/dist/esm/styles/hljs/arta";import{default as Ae}from"react-syntax-highlighter/dist/esm/styles/hljs/monokai";import{default as ke}from"react-syntax-highlighter/dist/esm/styles/hljs/nord";import{default as Ie}from"react-syntax-highlighter/dist/esm/styles/hljs/obsidian";import{default as je}from"react-syntax-highlighter/dist/esm/styles/hljs/tomorrow-night";import{default as Te}from"react-syntax-highlighter/dist/esm/styles/hljs/idea";import{default as Re}from"js-file-download";import{default as Me}from"lodash/zipObject";import{default as Pe}from"lodash/toString";import{default as Be}from"xml-but-prettier";import{default as Le}from"lodash/toLower";import{Remarkable as qe}from"remarkable";import{linkify as Ue}from"remarkable/linkify";import{default as De}from"dompurify";import{default as Je}from"lodash/escapeRegExp";import{default as $e}from"lodash/has";import{default as Ve}from"lodash/fp/set";var Fe={287:function(e,t,r){const n=r(987),a=r(362),s="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):null;t.Buffer=Buffer,t.SlowBuffer=function SlowBuffer(e){+e!=e&&(e=0);return Buffer.alloc(+e)},t.INSPECT_MAX_BYTES=50;const o=2147483647;function createBuffer(e){if(e>o)throw new RangeError('The value "'+e+'" is invalid for option "size"');const t=new Uint8Array(e);return Object.setPrototypeOf(t,Buffer.prototype),t}function Buffer(e,t,r){if("number"==typeof e){if("string"==typeof t)throw new TypeError('The "string" argument must be of type string. ReLine truncated
|
||||||
//# sourceMappingURL=swagger-ui-es-bundle-core.js.map
|
//# sourceMappingURL=swagger-ui-es-bundle-core.js.map
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*!
|
||||||
|
* @description Recursive object extending
|
||||||
|
* @author Viacheslav Lotsmanov <lotsmanov89@gmail.com>
|
||||||
|
* @license MIT
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2013-2018 Viacheslav Lotsmanov
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* The buffer module from node.js, for the browser.
|
||||||
|
*
|
||||||
|
* @author Feross Aboukhadijeh <https://feross.org>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"swagger-ui-es-bundle-core.js","mappings":"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 truncated
|
{"version":3,"file":"swagger-ui-es-bundle-core.js","mappings":"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 truncated
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
/*! For license information please see swagger-ui-es-bundle.js.LICENSE.txt */
|
/*! For license information please see swagger-ui-es-bundle.js.LICENSE.txt */
|
||||||
(()=>{var e={17967:(e,t)=>{"use strict";t.N=void 0;var n=/^([^\w]*)(javascript|data|vbscript)/im,r=/&#(\w+)(^\w|;)?/g,o=/&(newline|tab);/gi,s=/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim,i=/^.+(:|:)/gim,a=[".","/"];t.N=function(e){var t,l=(t=e||"",t.replace(r,(function(e,t){return String.fromCharCode(t)}))).replace(o,"").replace(s,"").trim();if(!l)return"about:blank";if(function(e){return a.indexOf(e[0])>-1}(l))return l;var c=l.match(i);if(!c)return l;var u=c[0];return n.test(u)?"about:blank":l}},53795:(e,t,n)=>{"use strict";n.d(t,{Z:()=>P});var r=n(23101),o=n.n(r),s=n(61125),i=n.n(s),a=n(11882),l=n.n(a),c=n(97606),u=n.n(c),p=n(67294),h=n(43393);function f(e){return f="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},f(e)}function d(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,r.key,r)}}function m(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function g(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function y(e){return y=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)},y(e)}function v(e,t){return v=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},v(e,t)}function b(e,t){return!t||"object"!=typeof t&&"function"!=typeof t?function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e):t}var w={};function E(e,t,n){return function(e){return null==e}(e)?n:function(e){return null!==e&&"object"===f(e)&&"function"==typeof e.get&&"function"==typeof e.has}(e)?e.has(t)?e.get(t):n:hasOwnProperty.call(e,t)?e[t]:n}function x(e,t,n){for(var r=0;r!==t.length;)if((e=E(e,t[r++],w))===w)return n;return e}function S(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},r=function(e,t){return function(n){if("string"==typeof n)return(0,h.is)(t[n],e[n]);if(Array.isArray(n))return(0,h.is)(x(t,n),x(e,n));throw new TypeError("Invalid key: expected Array or string: "+n)}}(t,n),o=e||Object.keys(function(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?g(n,!0).forEach((function(t){m(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):g(n).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}({},n,{},t));return o.every(r)}const _=function(e){function t(){return function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,t),b(this,y(t).apply(this,arguments))}var n,r,o;return function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&v(e,t)}(t,e),n=t,r=[{key:"shouldComponentUpdate",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return!S(this.updateOnProps,this.props,e,"updateOnProps")||!S(this.updateOnStates,this.state,t,"updateOnStates")}}],r&&d(n.prototype,r),o&&d(n,o),t}(p.Component);var j=n(23930),O=n.n(j),k=n(45697),A=n.n(k);const C=e=>{const t=e.replace(/~1/g,"/").replace(/~0/g,"~");try{return decodeURIComponent(t)}catch{return t}};class P extends _{constructor(){super(...arguments),i()(this,"getModelName",(e=>-1!==l()(e).call(e,"#/definitions/")?C(e.replace(/^.*#\/definitions\//,"")):-1!==l()(e).call(e,"#/components/schemas/")?C(e.replace(/^.*#\/components\/schemas\//,"")):void 0)),i()(this,"getRefSchema",(e=>{let{specSelectors:t}=this.props;return t.findDefinition(e)}))}render(){let{getComponent:e,getConfigs:t,specSelectors:r,schema:s,required:i,name:a,isRef:l,specPath:c,displayName:u,includeReadOnly:h,includeWriteOnly:f}=this.props;const d=e("ObjectModel"),m=e("ArrayModel"),g=e("PrimitiveModel");let y="object",v=s&&s.get("$$ref");if(!a&&v&&(a=this.getModelName(v)),!s&&v&&(s=this.getRefSchema(a)),!s)return p.createElement("span",{className:"model model-title"},p.createElement("span",{className:"model-title__text"},u||a),p.createElement("img",{src:n(2517),height:"20px",width:"20px"}));const b=r.isOAS3()&&s.get("deprecated");switch(l=void 0!==l?l:!!v,y=s&&s.get("type")||y,y){case"object":return p.createElement(d,o()({className:"object"},this.props,{specPath:c,getConfigs:t,schema:s,name:a,deprecated:b,isRef:l,includeReadOnly:h,includeWriteOnly:f}));case"array":return p.createElement(m,o()({className:"array"},this.props,{getConfigs:t,schema:s,name:a,deprecated:b,required:i,includeReadOnly:h,inLine truncated
|
(()=>{var s={67526(s,o){"use strict";o.byteLength=function byteLength(s){var o=getLens(s),i=o[0],a=o[1];return 3*(i+a)/4-a},o.toByteArray=function toByteArray(s){var o,i,_=getLens(s),w=_[0],x=_[1],C=new u(function _byteLength(s,o,i){return 3*(o+i)/4-i}(0,w,x)),j=0,L=x>0?w-4:w;for(i=0;i<L;i+=4)o=a[s.charCodeAt(i)]<<18|a[s.charCodeAt(i+1)]<<12|a[s.charCodeAt(i+2)]<<6|a[s.charCodeAt(i+3)],C[j++]=o>>16&255,C[j++]=o>>8&255,C[j++]=255&o;2===x&&(o=a[s.charCodeAt(i)]<<2|a[s.charCodeAt(i+1)]>>4,C[j++]=255&o);1===x&&(o=a[s.charCodeAt(i)]<<10|a[s.charCodeAt(i+1)]<<4|a[s.charCodeAt(i+2)]>>2,C[j++]=o>>8&255,C[j++]=255&o);return C},o.fromByteArray=function fromByteArray(s){for(var o,a=s.length,u=a%3,_=[],w=16383,x=0,C=a-u;x<C;x+=w)_.push(encodeChunk(s,x,x+w>C?C:x+w));1===u?(o=s[a-1],_.push(i[o>>2]+i[o<<4&63]+"==")):2===u&&(o=(s[a-2]<<8)+s[a-1],_.push(i[o>>10]+i[o>>4&63]+i[o<<2&63]+"="));return _.join("")};for(var i=[],a=[],u="undefined"!=typeof Uint8Array?Uint8Array:Array,_="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",w=0;w<64;++w)i[w]=_[w],a[_.charCodeAt(w)]=w;function getLens(s){var o=s.length;if(o%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var i=s.indexOf("=");return-1===i&&(i=o),[i,i===o?0:4-i%4]}function encodeChunk(s,o,a){for(var u,_,w=[],x=o;x<a;x+=3)u=(s[x]<<16&16711680)+(s[x+1]<<8&65280)+(255&s[x+2]),w.push(i[(_=u)>>18&63]+i[_>>12&63]+i[_>>6&63]+i[63&_]);return w.join("")}a["-".charCodeAt(0)]=62,a["_".charCodeAt(0)]=63},48287(s,o,i){"use strict";const a=i(67526),u=i(251),_="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):null;o.Buffer=Buffer,o.SlowBuffer=function SlowBuffer(s){+s!=s&&(s=0);return Buffer.alloc(+s)},o.INSPECT_MAX_BYTES=50;const w=2147483647;function createBuffer(s){if(s>w)throw new RangeError('The value "'+s+'" is invalid for option "size"');const o=new Uint8Array(s);return Object.setPrototypeOf(o,Buffer.prototype),o}function Buffer(s,o,i){if("number"==typeof s){if("string"==typeof o)throw new TypeError('The "string" argument must be of type string. Received type number');return allocUnsafe(s)}return from(s,o,i)}function from(s,o,i){if("string"==typeof s)return function fromString(s,o){"string"==typeof o&&""!==o||(o="utf8");if(!Buffer.isEncoding(o))throw new TypeError("Unknown encoding: "+o);const i=0|byteLength(s,o);let a=createBuffer(i);const u=a.write(s,o);u!==i&&(a=a.slice(0,u));return a}(s,o);if(ArrayBuffer.isView(s))return function fromArrayView(s){if(isInstance(s,Uint8Array)){const o=new Uint8Array(s);return fromArrayBuffer(o.buffer,o.byteOffset,o.byteLength)}return fromArrayLike(s)}(s);if(null==s)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof s);if(isInstance(s,ArrayBuffer)||s&&isInstance(s.buffer,ArrayBuffer))return fromArrayBuffer(s,o,i);if("undefined"!=typeof SharedArrayBuffer&&(isInstance(s,SharedArrayBuffer)||s&&isInstance(s.buffer,SharedArrayBuffer)))return fromArrayBuffer(s,o,i);if("number"==typeof s)throw new TypeError('The "value" argument must not be of type number. Received type number');const a=s.valueOf&&s.valueOf();if(null!=a&&a!==s)return Buffer.from(a,o,i);const u=function fromObject(s){if(Buffer.isBuffer(s)){const o=0|checked(s.length),i=createBuffer(o);return 0===i.length||s.copy(i,0,0,o),i}if(void 0!==s.length)return"number"!=typeof s.length||numberIsNaN(s.length)?createBuffer(0):fromArrayLike(s);if("Buffer"===s.type&&Array.isArray(s.data))return fromArrayLike(s.data)}(s);if(u)return u;if("undefined"!=typeof Symbol&&null!=Symbol.toPrimitive&&"function"==typeof s[Symbol.toPrimitive])return Buffer.from(s[Symbol.toPrimitive]("string"),o,i);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof s)}function assertSize(s){if("number"!=typeof s)throw new TypeError('"size" argument must be of type number');if(s<0)throw new RangeError('The value "'+s+'" is invalid for option "size"')}function allocUnsafe(s){return assertSize(s),createBuffer(s<0?0:0|checked(s))}function fromArrayLike(s){const o=s.length<0?0:0|checked(s.length),i=createBuffer(o);for(let a=0;a<o;a+=1)i[a]=255&s[a];return i}function fromArrayBuffer(s,o,i){if(o<0||s.byteLength<o)throw new RangeError('"offset" is outside of buffer bounds');if(s.byteLength<o+(i||0))throw new RangeError('"length" is outside of buffer bounds');let a;return a=void 0===o&&void 0===i?new Uint8Array(s):void 0===i?new Uint8Array(s,o):new Uint8Array(s,o,i),Object.setPrototypeOf(a,Buffer.prototype),a}function checked(s){if(s>=w)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+w.toString(16)+" bytes");return 0|s}function byteLength(s,o){if(Buffer.isBuffer(s))return s.length;if(ArrayBuffer.isView(s)||isInstance(s,ArrayBuffer))return s.byteLength;if("string"!=typeof s)throw new TypeError('The "string" argument must be one of type string, Buffer,Line truncated
|
||||||
//# sourceMappingURL=swagger-ui-es-bundle.js.map
|
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/*!
|
||||||
|
Copyright (c) 2018 Jed Watson.
|
||||||
|
Licensed under the MIT License (MIT), see
|
||||||
|
http://jedwatson.github.io/classnames
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* @description Recursive object extending
|
||||||
|
* @author Viacheslav Lotsmanov <lotsmanov89@gmail.com>
|
||||||
|
* @license MIT
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2013-2018 Viacheslav Lotsmanov
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* The buffer module from node.js, for the browser.
|
||||||
|
*
|
||||||
|
* @author Feross Aboukhadijeh <https://feross.org>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* https://github.com/Starcounter-Jack/JSON-Patch
|
||||||
|
* (c) 2017-2021 Joachim Wester
|
||||||
|
* MIT license
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* https://github.com/Starcounter-Jack/JSON-Patch
|
||||||
|
* (c) 2017-2022 Joachim Wester
|
||||||
|
* MIT licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* repeat-string <https://github.com/jonschlinkert/repeat-string>
|
||||||
|
*
|
||||||
|
* Copyright (c) 2014-2015, Jon Schlinkert.
|
||||||
|
* Licensed under the MIT License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*! @license DOMPurify 3.4.17 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.17/LICENSE */
|
||||||
|
|
||||||
|
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */
|
||||||
|
|
||||||
|
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2014-present, Lee Byron and other contributors.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* react-dom.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* react.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* scheduler.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* use-sync-external-store-with-selector.production.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"swagger-ui-es-bundle.js","mappings":"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 truncated
|
{"version":3,"file":"swagger-ui-es-bundle.js","mappings":";UACIA,EADAC,gCCCJC,OAAOC,eAAeC,EAAS,aAAc,CAAEC,OAAO,IACtDD,EAAQE,UAAYF,EAAQG,wBAA0BH,EAAQI,eAAiBJ,EAAQK,oBAAsBL,EAAQM,oBAAsBN,EAAQO,kBAAoBP,EAAQQ,0BAAuB,EACtMR,EAAQQ,qBAAuB,wCAC/BR,EAAQO,kBAAoB,mBAC5BP,EAAQM,oBAAsB,oBAC9BN,EAAQK,oBAAsB,qDAC9BL,EAAQI,eAAiB,oBACzBJ,EAAQG,wBAA0B,CAAC,IAAK,KACxCH,EAAQE,UAAY,4CCPpBF,EAAQ,OAAc,EACtB,IAAIS,EAAc,EAAQ,OA4C1BT,EAAQ,EAjCR,SAASU,YAAYC,GACjB,IAAKA,EACD,OAAOF,EAAYP,UAEvB,IAAIU,EAVsBC,EAWtBC,EAAaH,EACjB,GAKIC,GAJAE,GAbsBD,EAaYC,EAZhBD,EAAIE,QAAQN,EAAYJ,oBAAqB,IAC5CU,QAAQN,EAAYF,mBAAmB,SAAUS,EAAOC,GAC3E,OAAOC,OAAOC,aAAaF,EAC/B,KAUSF,QAAQN,EAAYH,oBAAqB,IACzCS,QAAQN,EAAYJ,oBAAqB,IACzCe,QAEUJ,MAAMP,EAAYJ,sBACzBS,EAAWE,MAAMP,EAAYF,oBAC7BO,EAAWE,MAAMP,EAAYH,2BAChCM,GAAiBA,EAAcS,OAAS,GACjD,IAAIC,EAAeR,EACnB,IAAKQ,EACD,OAAOb,EAAYP,UAEvB,GA9BJ,SAASqB,6BAA6BZ,GAClC,OAAOF,EAAYN,wBAAwBqB,QAAQb,EAAI,KAAO,CAClE,CA4BQY,CAA6BD,GAC7B,OAAOA,EAEX,IAAIG,EAAwBH,EAAaN,MAAMP,EAAYL,gBAC3D,IAAKqB,EACD,OAAOH,EAEX,IAAII,EAAYD,EAAsB,GACtC,OAAIhB,EAAYD,qBAAqBmB,KAAKD,GAC/BjB,EAAYP,UAEhBoB,CACX,8BC5CAtB,EAAQ4B,WAuCR,SAASA,WAAYC,GACnB,IAAIC,EAAOC,QAAQF,GACfG,EAAWF,EAAK,GAChBG,EAAkBH,EAAK,GAC3B,OAAuC,GAA9BE,EAAWC,GAAuB,EAAKA,CAClD,EA3CAjC,EAAQkC,YAiDR,SAASA,YAAaL,GACpB,IAAIM,EAcAC,EAbAN,EAAOC,QAAQF,GACfG,EAAWF,EAAK,GAChBG,EAAkBH,EAAK,GAEvBO,EAAM,IAAIC,EAVhB,SAASC,YAAaV,EAAKG,EAAUC,GACnC,OAAuC,GAA9BD,EAAWC,GAAuB,EAAKA,CAClD,CAQoBM,CAAYV,EAAKG,EAAUC,IAEzCO,EAAU,EAGVC,EAAMR,EAAkB,EACxBD,EAAW,EACXA,EAGJ,IAAKI,EAAI,EAAGA,EAAIK,EAAKL,GAAK,EACxBD,EACGO,EAAUb,EAAIc,WAAWP,KAAO,GAChCM,EAAUb,EAAIc,WAAWP,EAAI,KAAO,GACpCM,EAAUb,EAAIc,WAAWP,EAAI,KAAO,EACrCM,EAAUb,EAAIc,WAAWP,EAAI,IAC/BC,EAAIG,KAAcL,GAAO,GAAM,IAC/BE,EAAIG,KAAcL,GAAO,EAAK,IAC9BE,EAAIG,KAAmB,IAANL,EAGK,IAApBF,IACFE,EACGO,EAAUb,EAAIc,WAAWP,KAAO,EAChCM,EAAUb,EAAIc,WAAWP,EAAI,KAAO,EACvCC,EAAIG,KAAmB,IAANL,GAGK,IAApBF,IACFE,EACGO,EAAUb,EAAIc,WAAWP,KAAO,GAChCM,EAAUb,EAAIc,WAAWP,EAAI,KAAO,EACpCM,EAAUb,EAAIc,WAAWP,EAAI,KAAO,EACvCC,EAAIG,KAAcL,GAAO,EAAK,IAC9BE,EAAIG,KAAmB,IAANL,GAGnB,OAAOE,CACT,EA5FArC,EAAQ4C,cAkHR,SAASA,cAAeC,GAQtB,IAPA,IAAIV,EACAM,EAAMI,EAAMxB,OACZyB,EAAaL,EAAM,EACnBM,EAAQ,GACRC,EAAiB,MAGZZ,EAAI,EAAGa,EAAOR,EAAMK,EAAYV,EAAIa,EAAMb,GAAKY,EACtDD,EAAMG,KAAKC,YAAYN,EAAOT,EAAIA,EAAIY,EAAkBC,EAAOA,EAAQb,EAAIY,IAI1D,IAAfF,GACFX,EAAMU,EAAMJ,EAAM,GAClBM,EAAMG,KACJE,EAAOjB,GAAO,GACdiB,EAAQjB,GAAO,EAAK,IACpB,OAEsB,IAAfW,IACTX,GAAOU,EAAMJ,EAAM,IAAM,GAAKI,EAAMJ,EAAM,GAC1CM,EAAMG,KACJE,EAAOjB,GAAO,IACdiB,EAAQjB,GAAO,EAAK,IACpBiB,EAAQjB,GAAO,EAAK,IACpB,MAIJ,OAAOY,EAAMM,KAAK,GACpB,EA1IA,IALA,IAAID,EAAS,GACTV,EAAY,GACZJ,EAA4B,oBAAfgB,WAA6BA,WAAaC,MAEvDC,EAAO,mEACFpB,EAAI,EAAsBA,EAAboB,KAAwBpB,EAC5CgB,EAAOhB,GAAKoB,EAAKpB,GACjBM,EAAUc,EAAKb,WAAWP,IAAMA,EAQlC,SAASL,QAASF,GAChB,IAAIY,EAAMZ,EAAIR,OAEd,GAAIoB,EAAM,EAAI,EACZ,MAAM,IAAIgB,MAAM,kDAKlB,IAAIzB,EAAWH,EAAIL,QAAQ,KAO3B,OANkB,IAAdQ,IAAiBA,EAAWS,GAMzB,CAACT,EAJcA,IAAaS,EAC/B,EACA,EAAKT,EAAW,EAGtB,CAmEA,SAASmB,YAAaN,EAAOa,EAAOC,GAGlC,IAFA,IAAIxB,EARoByB,EASpBC,EAAS,GACJzB,EAAIsB,EAAOtB,EAAIuB,EAAKvB,GAAK,EAChCD,GACIU,EAAMT,IAAM,GAAM,WAClBS,EAAMT,EAAI,IAAM,EAAK,QACP,IAAfS,EAAMT,EAAI,IACbyB,EAAOX,KAdFE,GADiBQ,EAeMzB,IAdT,GAAK,IACxBiB,EAAOQ,GAAO,GAAK,IACnBR,EAAOQ,GAAO,EAAI,IAClBR,EAAa,GAANQ,IAaT,OAAOC,EAAOR,KAAK,GACrB,CAlGAX,EAAU,IAAIC,WAAW,IAAM,GAC/BD,EAAU,IAAIC,WAAW,IAAM,iCCT/B,MAAMmB,EAAS,EAAQ,OACjBC,EAAU,EAAQ,KAClBC,EACe,mBAAXC,QAAkD,mBAAlBA,OAAY,IAChDA,OAAY,IAAE,8BACd,KAENjE,EAAQkE,OAASA,OACjBlE,EAAQmE,WAyTR,SAASA,WAAY9C,IACdA,GAAUA,IACbA,EAAS,GAEX,OAAO6C,OAAOE,OAAO/C,EACvB,EA7TArB,EAAQqE,kBAAoB,GAE5B,MAAMC,EAAe,WAwDrB,SAASC,aAAclD,GACrB,GAAIA,EAASiD,EACX,MAAM,IAAIE,WAAW,cAAgBnD,EAAS,kCAGhD,MAAMoD,EAAM,IAAInB,WAAWjC,GAE3B,OADAvB,OAAO4E,eAAeD,EAAKP,OAAOS,WAC3BF,CACT,CAYA,SAASP,OAAQU,EAAKC,EAAkBxD,GAEtC,GAAmB,iBAARuD,EAAkB,CAC3B,GAAgC,iBAArBC,EACT,MAAM,IAAIC,UACR,sEAGJ,OAAOC,YAAYH,EACrB,CACA,OAAOI,KAAKJ,EAAKC,EAAkBxD,EACrC,CAIA,SAAS2D,KAAM/E,EAAO4E,EAAkBxD,GACtC,GAAqB,iBAAVpB,EACT,OAqHJ,SAASgF,WAAYC,EAAQC,GACH,iBAAbA,GAAsC,KAAbA,IAClCA,EAAW,QAGb,IAAKjB,OAAOkB,WAAWD,GACrB,MAAM,IAAIL,UAAU,qBAAuBK,GAG7C,MAAM9D,EAAwC,EAA/BO,WAAWsD,EAAQC,GAClC,IAAIV,EAAMF,aAAalD,GAEvB,MAAMgE,EAASZ,EAAIa,MAAMJ,EAAQC,GAE7BE,IAAWhE,IAIboD,EAAMA,EAAIc,MAAM,EAAGF,IAGrB,OAAOZ,CACT,CA3IWQ,CAAWhF,EAAO4E,GAG3B,GAAIW,YAAYC,OAAOxF,GACrB,OAkJJ,SAASyF,cAAeC,GACtB,GAAIC,WAAWD,EAAWrC,YAAa,CACrC,MAAMuC,EAAO,IAAIvC,WAAWqC,GAC5B,OAAOG,gBAAgBD,EAAKE,OAAQF,EAAKG,WAAYH,EAAKjE,WAC5D,CACA,OAAOqE,cAAcN,EACvB,CAxJWD,CAAczF,GAGvB,GAAa,MAATA,EACF,MAAM,IAAI6E,UACR,yHACiD7E,GAIrD,GAAI2F,WAAW3F,EAAOuF,cACjBvF,GAAS2F,WAAW3F,EAAM8F,OAAQP,aACrC,OAAOM,gBAAgB7F,EAAO4E,EAAkBxD,GAGlD,GAAiC,oBAAtB6E,oBACNN,WAAW3F,EAAOiG,oBAClBjG,GAAS2F,WAAW3F,EAAM8F,OAAQG,oBACrC,OAAOJ,gBAAgB7F,EAAO4E,EAAkBxD,GAGlD,GAAqB,iBAAVpB,EACT,MAAM,IAAI6E,UACR,yEAIJ,MAAMqB,EAAUlG,EAAMkG,SAAWlG,EAAMkG,UACvC,GAAe,MAAXA,GAAmBA,IAAYlG,EACjC,OAAOiE,OAAOc,KAAKmB,EAAStB,EAAkBxD,GAGhD,MAAM+E,EAkJR,SAASC,WAAYC,GACnB,GAAIpC,OAAOqC,SAASD,GAAM,CACxB,MAAM7D,EAA4B,EAAtB+D,QAAQF,EAAIjF,QAClBoD,EAAMF,aAAa9B,GAEzB,OAAmB,IAAfgC,EAAIpD,QAIRiF,EAAIT,KAAKpBLine truncated
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
/*! For license information please see swagger-ui-standalone-preset.js.LICENSE.txt */
|
/*! For license information please see swagger-ui-standalone-preset.js.LICENSE.txt */
|
||||||
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.SwaggerUIStandalonePreset=e():t.SwaggerUIStandalonePreset=e()}(this,(()=>(()=>{var t={7967:(t,e)=>{"use strict";var r=/^([^\w]*)(javascript|data|vbscript)/im,n=/&#(\w+)(^\w|;)?/g,i=/&(newline|tab);/gi,o=/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim,u=/^.+(:|:)/gim,s=[".","/"]},9742:(t,e)=>{"use strict";e.byteLength=function(t){var e=s(t),r=e[0],n=e[1];return 3*(r+n)/4-n},e.toByteArray=function(t){var e,r,o=s(t),u=o[0],a=o[1],c=new i(function(t,e,r){return 3*(e+r)/4-r}(0,u,a)),f=0,l=a>0?u-4:u;for(r=0;r<l;r+=4)e=n[t.charCodeAt(r)]<<18|n[t.charCodeAt(r+1)]<<12|n[t.charCodeAt(r+2)]<<6|n[t.charCodeAt(r+3)],c[f++]=e>>16&255,c[f++]=e>>8&255,c[f++]=255&e;2===a&&(e=n[t.charCodeAt(r)]<<2|n[t.charCodeAt(r+1)]>>4,c[f++]=255&e);1===a&&(e=n[t.charCodeAt(r)]<<10|n[t.charCodeAt(r+1)]<<4|n[t.charCodeAt(r+2)]>>2,c[f++]=e>>8&255,c[f++]=255&e);return c},e.fromByteArray=function(t){for(var e,n=t.length,i=n%3,o=[],u=16383,s=0,c=n-i;s<c;s+=u)o.push(a(t,s,s+u>c?c:s+u));1===i?(e=t[n-1],o.push(r[e>>2]+r[e<<4&63]+"==")):2===i&&(e=(t[n-2]<<8)+t[n-1],o.push(r[e>>10]+r[e>>4&63]+r[e<<2&63]+"="));return o.join("")};for(var r=[],n=[],i="undefined"!=typeof Uint8Array?Uint8Array:Array,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",u=0;u<64;++u)r[u]=o[u],n[o.charCodeAt(u)]=u;function s(t){var e=t.length;if(e%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var r=t.indexOf("=");return-1===r&&(r=e),[r,r===e?0:4-r%4]}function a(t,e,n){for(var i,o,u=[],s=e;s<n;s+=3)i=(t[s]<<16&16711680)+(t[s+1]<<8&65280)+(255&t[s+2]),u.push(r[(o=i)>>18&63]+r[o>>12&63]+r[o>>6&63]+r[63&o]);return u.join("")}n["-".charCodeAt(0)]=62,n["_".charCodeAt(0)]=63},8764:(t,e,r)=>{"use strict";const n=r(9742),i=r(645),o="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):null;e.Buffer=a,e.SlowBuffer=function(t){+t!=t&&(t=0);return a.alloc(+t)},e.INSPECT_MAX_BYTES=50;const u=2147483647;function s(t){if(t>u)throw new RangeError('The value "'+t+'" is invalid for option "size"');const e=new Uint8Array(t);return Object.setPrototypeOf(e,a.prototype),e}function a(t,e,r){if("number"==typeof t){if("string"==typeof e)throw new TypeError('The "string" argument must be of type string. Received type number');return l(t)}return c(t,e,r)}function c(t,e,r){if("string"==typeof t)return function(t,e){"string"==typeof e&&""!==e||(e="utf8");if(!a.isEncoding(e))throw new TypeError("Unknown encoding: "+e);const r=0|M(t,e);let n=s(r);const i=n.write(t,e);i!==r&&(n=n.slice(0,i));return n}(t,e);if(ArrayBuffer.isView(t))return function(t){if($(t,Uint8Array)){const e=new Uint8Array(t);return p(e.buffer,e.byteOffset,e.byteLength)}return h(t)}(t);if(null==t)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof t);if($(t,ArrayBuffer)||t&&$(t.buffer,ArrayBuffer))return p(t,e,r);if("undefined"!=typeof SharedArrayBuffer&&($(t,SharedArrayBuffer)||t&&$(t.buffer,SharedArrayBuffer)))return p(t,e,r);if("number"==typeof t)throw new TypeError('The "value" argument must not be of type number. Received type number');const n=t.valueOf&&t.valueOf();if(null!=n&&n!==t)return a.from(n,e,r);const i=function(t){if(a.isBuffer(t)){const e=0|y(t.length),r=s(e);return 0===r.length||t.copy(r,0,0,e),r}if(void 0!==t.length)return"number"!=typeof t.length||X(t.length)?s(0):h(t);if("Buffer"===t.type&&Array.isArray(t.data))return h(t.data)}(t);if(i)return i;if("undefined"!=typeof Symbol&&null!=Symbol.toPrimitive&&"function"==typeof t[Symbol.toPrimitive])return a.from(t[Symbol.toPrimitive]("string"),e,r);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof t)}function f(t){if("number"!=typeof t)throw new TypeError('"size" argument must be of type number');if(t<0)throw new RangeError('The value "'+t+'" is invalid for option "size"')}function l(t){return f(t),s(t<0?0:0|y(t))}function h(t){const e=t.length<0?0:0|y(t.length),r=s(e);for(let n=0;n<e;n+=1)r[n]=255&t[n];return r}function p(t,e,r){if(e<0||t.byteLength<e)throw new RangeError('"offset" is outside of buffer bounds');if(t.byteLength<e+(r||0))throw new RangeError('"length" is outside of buffer bounds');let n;return n=void 0===e&&void 0===r?new Uint8Array(t):void 0===r?new Uint8Array(t,e):new Uint8Array(t,e,r),Object.setPrototypeOf(n,a.prototype),n}function y(t){if(t>=u)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+u.toString(16)+" bytes");return 0|t}function M(t,e){if(a.isBuffer(t))return t.length;if(ArrayBuffer.isView(t)||$(t,ArrayBuffer))return t.byteLength;if("string"!=typeof t)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof t);const r=t.leLine truncated
|
!function webpackUniversalModuleDefinition(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.SwaggerUIStandalonePreset=e():t.SwaggerUIStandalonePreset=e()}(this,(()=>(()=>{var t={7526(t,e){"use strict";e.byteLength=function byteLength(t){var e=getLens(t),r=e[0],n=e[1];return 3*(r+n)/4-n},e.toByteArray=function toByteArray(t){var e,r,i=getLens(t),a=i[0],s=i[1],u=new o(function _byteLength(t,e,r){return 3*(e+r)/4-r}(0,a,s)),c=0,l=s>0?a-4:a;for(r=0;r<l;r+=4)e=n[t.charCodeAt(r)]<<18|n[t.charCodeAt(r+1)]<<12|n[t.charCodeAt(r+2)]<<6|n[t.charCodeAt(r+3)],u[c++]=e>>16&255,u[c++]=e>>8&255,u[c++]=255&e;2===s&&(e=n[t.charCodeAt(r)]<<2|n[t.charCodeAt(r+1)]>>4,u[c++]=255&e);1===s&&(e=n[t.charCodeAt(r)]<<10|n[t.charCodeAt(r+1)]<<4|n[t.charCodeAt(r+2)]>>2,u[c++]=e>>8&255,u[c++]=255&e);return u},e.fromByteArray=function fromByteArray(t){for(var e,n=t.length,o=n%3,i=[],a=16383,s=0,u=n-o;s<u;s+=a)i.push(encodeChunk(t,s,s+a>u?u:s+a));1===o?(e=t[n-1],i.push(r[e>>2]+r[e<<4&63]+"==")):2===o&&(e=(t[n-2]<<8)+t[n-1],i.push(r[e>>10]+r[e>>4&63]+r[e<<2&63]+"="));return i.join("")};for(var r=[],n=[],o="undefined"!=typeof Uint8Array?Uint8Array:Array,i="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",a=0;a<64;++a)r[a]=i[a],n[i.charCodeAt(a)]=a;function getLens(t){var e=t.length;if(e%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var r=t.indexOf("=");return-1===r&&(r=e),[r,r===e?0:4-r%4]}function encodeChunk(t,e,n){for(var o,i,a=[],s=e;s<n;s+=3)o=(t[s]<<16&16711680)+(t[s+1]<<8&65280)+(255&t[s+2]),a.push(r[(i=o)>>18&63]+r[i>>12&63]+r[i>>6&63]+r[63&i]);return a.join("")}n["-".charCodeAt(0)]=62,n["_".charCodeAt(0)]=63},8287(t,e,r){"use strict";const n=r(7526),o=r(251),i="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):null;e.Buffer=Buffer,e.SlowBuffer=function SlowBuffer(t){+t!=t&&(t=0);return Buffer.alloc(+t)},e.INSPECT_MAX_BYTES=50;const a=2147483647;function createBuffer(t){if(t>a)throw new RangeError('The value "'+t+'" is invalid for option "size"');const e=new Uint8Array(t);return Object.setPrototypeOf(e,Buffer.prototype),e}function Buffer(t,e,r){if("number"==typeof t){if("string"==typeof e)throw new TypeError('The "string" argument must be of type string. Received type number');return allocUnsafe(t)}return from(t,e,r)}function from(t,e,r){if("string"==typeof t)return function fromString(t,e){"string"==typeof e&&""!==e||(e="utf8");if(!Buffer.isEncoding(e))throw new TypeError("Unknown encoding: "+e);const r=0|byteLength(t,e);let n=createBuffer(r);const o=n.write(t,e);o!==r&&(n=n.slice(0,o));return n}(t,e);if(ArrayBuffer.isView(t))return function fromArrayView(t){if(isInstance(t,Uint8Array)){const e=new Uint8Array(t);return fromArrayBuffer(e.buffer,e.byteOffset,e.byteLength)}return fromArrayLike(t)}(t);if(null==t)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof t);if(isInstance(t,ArrayBuffer)||t&&isInstance(t.buffer,ArrayBuffer))return fromArrayBuffer(t,e,r);if("undefined"!=typeof SharedArrayBuffer&&(isInstance(t,SharedArrayBuffer)||t&&isInstance(t.buffer,SharedArrayBuffer)))return fromArrayBuffer(t,e,r);if("number"==typeof t)throw new TypeError('The "value" argument must not be of type number. Received type number');const n=t.valueOf&&t.valueOf();if(null!=n&&n!==t)return Buffer.from(n,e,r);const o=function fromObject(t){if(Buffer.isBuffer(t)){const e=0|checked(t.length),r=createBuffer(e);return 0===r.length||t.copy(r,0,0,e),r}if(void 0!==t.length)return"number"!=typeof t.length||numberIsNaN(t.length)?createBuffer(0):fromArrayLike(t);if("Buffer"===t.type&&Array.isArray(t.data))return fromArrayLike(t.data)}(t);if(o)return o;if("undefined"!=typeof Symbol&&null!=Symbol.toPrimitive&&"function"==typeof t[Symbol.toPrimitive])return Buffer.from(t[Symbol.toPrimitive]("string"),e,r);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof t)}function assertSize(t){if("number"!=typeof t)throw new TypeError('"size" argument must be of type number');if(t<0)throw new RangeError('The value "'+t+'" is invalid for option "size"')}function allocUnsafe(t){return assertSize(t),createBuffer(t<0?0:0|checked(t))}function fromArrayLike(t){const e=t.length<0?0:0|checked(t.length),r=createBuffer(e);for(let n=0;n<e;n+=1)r[n]=255&t[n];return r}function fromArrayBuffer(t,e,r){if(e<0||t.byteLength<e)throw new RangeError('"offset" is outside of buffer bounds');if(t.byteLength<e+(r||0))throw new RangeError('"length" is outside of buffer bounds');let n;return n=void 0===e&&void 0===r?new Uint8Array(t):void 0===r?new Uint8Array(t,e):new Uint8Array(t,e,r),Object.setPrototypeOf(n,Buffer.prototype),n}function checked(t){if(t>=a)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+Line truncated
|
||||||
//# sourceMappingURL=swagger-ui-standalone-preset.js.map
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*!
|
||||||
|
* The buffer module from node.js, for the browser.
|
||||||
|
*
|
||||||
|
* @author Feross Aboukhadijeh <https://feross.org>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* Copyright (c) 2014-present, Lee Byron and other contributors.
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license React
|
||||||
|
* react.production.min.js
|
||||||
|
*
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*/
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"swagger-ui-standalone-preset.js","mappings":";CAAA,SAA2CA,EAAMC,GAC1B,iBAAZC,SAA0C,iBAAXC,OACxCA,OAAOD,QAAUD,IACQ,mBAAXG,QAAyBA,OAAOC,IAC9CD,OAAO,GAAIH,GACe,iBAAZC,QACdA,QAAmC,0BAAID,IAEvCD,EAAgC,0BAAIC,GACrC,CATD,CASGK,MAAM,2CCNT,IAAIC,EAAuB,wCACvBC,EAAoB,mBACpBC,EAAsB,oBACtBC,EAAsB,qDACtBC,EAAiB,oBACjBC,EAA0B,CAAC,IAAK,gCCNpCV,EAAQW,WAuCR,SAAqBC,GACnB,IAAIC,EAAOC,EAAQF,GACfG,EAAWF,EAAK,GAChBG,EAAkBH,EAAK,GAC3B,OAAuC,GAA9BE,EAAWC,GAAuB,EAAKA,CAClD,EA3CAhB,EAAQiB,YAiDR,SAAsBL,GACpB,IAAIM,EAcAC,EAbAN,EAAOC,EAAQF,GACfG,EAAWF,EAAK,GAChBG,EAAkBH,EAAK,GAEvBO,EAAM,IAAIC,EAVhB,SAAsBT,EAAKG,EAAUC,GACnC,OAAuC,GAA9BD,EAAWC,GAAuB,EAAKA,CAClD,CAQoBM,CAAYV,EAAKG,EAAUC,IAEzCO,EAAU,EAGVC,EAAMR,EAAkB,EACxBD,EAAW,EACXA,EAGJ,IAAKI,EAAI,EAAGA,EAAIK,EAAKL,GAAK,EACxBD,EACGO,EAAUb,EAAIc,WAAWP,KAAO,GAChCM,EAAUb,EAAIc,WAAWP,EAAI,KAAO,GACpCM,EAAUb,EAAIc,WAAWP,EAAI,KAAO,EACrCM,EAAUb,EAAIc,WAAWP,EAAI,IAC/BC,EAAIG,KAAcL,GAAO,GAAM,IAC/BE,EAAIG,KAAcL,GAAO,EAAK,IAC9BE,EAAIG,KAAmB,IAANL,EAGK,IAApBF,IACFE,EACGO,EAAUb,EAAIc,WAAWP,KAAO,EAChCM,EAAUb,EAAIc,WAAWP,EAAI,KAAO,EACvCC,EAAIG,KAAmB,IAANL,GAGK,IAApBF,IACFE,EACGO,EAAUb,EAAIc,WAAWP,KAAO,GAChCM,EAAUb,EAAIc,WAAWP,EAAI,KAAO,EACpCM,EAAUb,EAAIc,WAAWP,EAAI,KAAO,EACvCC,EAAIG,KAAcL,GAAO,EAAK,IAC9BE,EAAIG,KAAmB,IAANL,GAGnB,OAAOE,CACT,EA5FApB,EAAQ2B,cAkHR,SAAwBC,GAQtB,IAPA,IAAIV,EACAM,EAAMI,EAAMC,OACZC,EAAaN,EAAM,EACnBO,EAAQ,GACRC,EAAiB,MAGZb,EAAI,EAAGc,EAAOT,EAAMM,EAAYX,EAAIc,EAAMd,GAAKa,EACtDD,EAAMG,KAAKC,EAAYP,EAAOT,EAAIA,EAAIa,EAAkBC,EAAOA,EAAQd,EAAIa,IAI1D,IAAfF,GACFZ,EAAMU,EAAMJ,EAAM,GAClBO,EAAMG,KACJE,EAAOlB,GAAO,GACdkB,EAAQlB,GAAO,EAAK,IACpB,OAEsB,IAAfY,IACTZ,GAAOU,EAAMJ,EAAM,IAAM,GAAKI,EAAMJ,EAAM,GAC1CO,EAAMG,KACJE,EAAOlB,GAAO,IACdkB,EAAQlB,GAAO,EAAK,IACpBkB,EAAQlB,GAAO,EAAK,IACpB,MAIJ,OAAOa,EAAMM,KAAK,GACpB,EA1IA,IALA,IAAID,EAAS,GACTX,EAAY,GACZJ,EAA4B,oBAAfiB,WAA6BA,WAAaC,MAEvDC,EAAO,mEACFrB,EAAI,EAAsBA,EAAbqB,KAAwBrB,EAC5CiB,EAAOjB,GAAKqB,EAAKrB,GACjBM,EAAUe,EAAKd,WAAWP,IAAMA,EAQlC,SAASL,EAASF,GAChB,IAAIY,EAAMZ,EAAIiB,OAEd,GAAIL,EAAM,EAAI,EACZ,MAAM,IAAIiB,MAAM,kDAKlB,IAAI1B,EAAWH,EAAI8B,QAAQ,KAO3B,OANkB,IAAd3B,IAAiBA,EAAWS,GAMzB,CAACT,EAJcA,IAAaS,EAC/B,EACA,EAAKT,EAAW,EAGtB,CAmEA,SAASoB,EAAaP,EAAOe,EAAOC,GAGlC,IAFA,IAAI1B,EARoB2B,EASpBC,EAAS,GACJ3B,EAAIwB,EAAOxB,EAAIyB,EAAKzB,GAAK,EAChCD,GACIU,EAAMT,IAAM,GAAM,WAClBS,EAAMT,EAAI,IAAM,EAAK,QACP,IAAfS,EAAMT,EAAI,IACb2B,EAAOZ,KAdFE,GADiBS,EAeM3B,IAdT,GAAK,IACxBkB,EAAOS,GAAO,GAAK,IACnBT,EAAOS,GAAO,EAAI,IAClBT,EAAa,GAANS,IAaT,OAAOC,EAAOT,KAAK,GACrB,CAlGAZ,EAAU,IAAIC,WAAW,IAAM,GAC/BD,EAAU,IAAIC,WAAW,IAAM,gCCT/B,MAAMqB,EAAS,EAAQ,MACjBC,EAAU,EAAQ,KAClBC,EACe,mBAAXC,QAAkD,mBAAlBA,OAAY,IAChDA,OAAY,IAAE,8BACd,KAENlD,EAAQmD,OAASA,EACjBnD,EAAQoD,WAyTR,SAAqBvB,IACdA,GAAUA,IACbA,EAAS,GAEX,OAAOsB,EAAOE,OAAOxB,EACvB,EA7TA7B,EAAQsD,kBAAoB,GAE5B,MAAMC,EAAe,WAwDrB,SAASC,EAAc3B,GACrB,GAAIA,EAAS0B,EACX,MAAM,IAAIE,WAAW,cAAgB5B,EAAS,kCAGhD,MAAM6B,EAAM,IAAIpB,WAAWT,GAE3B,OADA8B,OAAOC,eAAeF,EAAKP,EAAOU,WAC3BH,CACT,CAYA,SAASP,EAAQW,EAAKC,EAAkBlC,GAEtC,GAAmB,iBAARiC,EAAkB,CAC3B,GAAgC,iBAArBC,EACT,MAAM,IAAIC,UACR,sEAGJ,OAAOC,EAAYH,EACrB,CACA,OAAOI,EAAKJ,EAAKC,EAAkBlC,EACrC,CAIA,SAASqC,EAAMC,EAAOJ,EAAkBlC,GACtC,GAAqB,iBAAVsC,EACT,OAqHJ,SAAqBC,EAAQC,GACH,iBAAbA,GAAsC,KAAbA,IAClCA,EAAW,QAGb,IAAKlB,EAAOmB,WAAWD,GACrB,MAAM,IAAIL,UAAU,qBAAuBK,GAG7C,MAAMxC,EAAwC,EAA/BlB,EAAWyD,EAAQC,GAClC,IAAIX,EAAMF,EAAa3B,GAEvB,MAAM0C,EAASb,EAAIc,MAAMJ,EAAQC,GAE7BE,IAAW1C,IAIb6B,EAAMA,EAAIe,MAAM,EAAGF,IAGrB,OAAOb,CACT,CA3IWgB,CAAWP,EAAOJ,GAG3B,GAAIY,YAAYC,OAAOT,GACrB,OAkJJ,SAAwBU,GACtB,GAAIC,EAAWD,EAAWvC,YAAa,CACrC,MAAMyC,EAAO,IAAIzC,WAAWuC,GAC5B,OAAOG,EAAgBD,EAAKE,OAAQF,EAAKG,WAAYH,EAAKpE,WAC5D,CACA,OAAOwE,EAAcN,EACvB,CAxJWO,CAAcjB,GAGvB,GAAa,MAATA,EACF,MAAM,IAAIH,UACR,yHACiDG,GAIrD,GAAIW,EAAWX,EAAOQ,cACjBR,GAASW,EAAWX,EAAMc,OAAQN,aACrC,OAAOK,EAAgBb,EAAOJ,EAAkBlC,GAGlD,GAAiC,oBAAtBwD,oBACNP,EAAWX,EAAOkB,oBAClBlB,GAASW,EAAWX,EAAMc,OAAQI,oBACrC,OAAOL,EAAgBb,EAAOJ,EAAkBlC,GAGlD,GAAqB,iBAAVsC,EACT,MAAM,IAAIH,UACR,yEAIJ,MAAMsB,EAAUnB,EAAMmB,SAAWnB,EAAMmB,UACvC,GAAe,MAAXA,GAAmBA,IAAYnB,EACjC,OAAOhB,EAAOe,KAAKoB,EAASvB,EAAkBlC,GAGhD,MAAM0D,EAkJR,SAAqBC,GACnB,GAAIrC,EAAOsC,SAASD,GAAM,CACxB,MAAMhE,EAA4B,EAAtBkE,EAAQF,EAAI3D,QAClB6B,EAAMF,EAAahC,GAEzB,OAAmB,IAAfkC,EAAI7B,QAIR2D,EAAIT,KAAKrB,EAAK,EAAG,EAAGlC,GAHXkC,CAKX,CAEA,QAAmBiC,IAAfH,EAAI3D,OACN,MAA0B,iBAAf2D,EAAI3D,QAAuB+D,EAAYJ,EAAI3D,QAC7C2B,EAAa,GAEf2B,EAAcK,GAGvB,GAAiB,WAAbA,EAAIK,MAAqBtD,MAAMuD,QAAQN,EAAIO,MAC7C,OAAOZ,EAAcK,EAAIO,KAE7B,CAzKYC,CAAW7B,GACrB,GAAIoB,EAAG,OAAOA,EAEd,GAAsB,oBAAXrC,QAAgD,MAAtBA,OAAO+C,aACH,mBAA9B9B,EAAMjB,OAAO+C,aACtB,OAAO9C,EAAOe,KAAKC,EAAMjB,OAAO+C,aAAa,UAAWlC,EAAkBlC,GAG5E,MAAM,IAAImC,UACR,yHACiDG,EAErD,CAmBA,SAAS+B,EAAYC,GACnB,GAAoB,iBAATA,EACT,MAAM,IAAInC,UAAU,0CACf,GAAImC,EAAO,EAChB,MAAM,IAAI1C,WAAW,cAAgB0C,EAAO,iCAEhD,CA0BA,SAASlC,EAAakC,GAEpB,OADAD,EAAWC,GACJ3C,EAAa2C,EAAO,EAAI,EAAoB,EAAhBT,EAAQS,GAC7C,CAuCA,SAAShB,EAAeiB,GACtB,MAAMvE,EAASuE,EAAMvE,OAAS,EAAI,EAA4B,EAAxB6D,EAAQU,EAAMvE,QAC9C6B,EAAMF,EAAa3B,GACzB,IAAK,IAAIV,EAAI,EAAGA,EAAIU,EAAQV,GAAK,EAC/BuC,EAAIvC,GAAgB,IAAXiF,EAAMLine truncated
|
{"version":3,"file":"swagger-ui-standalone-preset.js","mappings":"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 truncated
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
.swagger-ui{color:#3b4151;font-family:sans-serif/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */}.swagger-ui html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;line-height:1.15}.swagger-ui body{margin:0}.swagger-ui article,.swagger-ui aside,.swagger-ui footer,.swagger-ui header,.swagger-ui nav,.swagger-ui section{display:block}.swagger-ui h1{font-size:2em;margin:.67em 0}.swagger-ui figcaption,.swagger-ui figure,.swagger-ui main{display:block}.swagger-ui figure{margin:1em 40px}.swagger-ui hr{box-sizing:content-box;height:0;overflow:visible}.swagger-ui pre{font-family:monospace,monospace;font-size:1em}.swagger-ui a{-webkit-text-decoration-skip:objects;background-color:transparent}.swagger-ui abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.swagger-ui b,.swagger-ui strong{font-weight:inherit;font-weight:bolder}.swagger-ui code,.swagger-ui kbd,.swagger-ui samp{font-family:monospace,monospace;font-size:1em}.swagger-ui dfn{font-style:italic}.swagger-ui mark{background-color:#ff0;color:#000}.swagger-ui small{font-size:80%}.swagger-ui sub,.swagger-ui sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}.swagger-ui sub{bottom:-.25em}.swagger-ui sup{top:-.5em}.swagger-ui audio,.swagger-ui video{display:inline-block}.swagger-ui audio:not([controls]){display:none;height:0}.swagger-ui img{border-style:none}.swagger-ui svg:not(:root){overflow:hidden}.swagger-ui button,.swagger-ui input,.swagger-ui optgroup,.swagger-ui select,.swagger-ui textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}.swagger-ui button,.swagger-ui input{overflow:visible}.swagger-ui button,.swagger-ui select{text-transform:none}.swagger-ui [type=reset],.swagger-ui [type=submit],.swagger-ui button,.swagger-ui html [type=button]{-webkit-appearance:button}.swagger-ui [type=button]::-moz-focus-inner,.swagger-ui [type=reset]::-moz-focus-inner,.swagger-ui [type=submit]::-moz-focus-inner,.swagger-ui button::-moz-focus-inner{border-style:none;padding:0}.swagger-ui [type=button]:-moz-focusring,.swagger-ui [type=reset]:-moz-focusring,.swagger-ui [type=submit]:-moz-focusring,.swagger-ui button:-moz-focusring{outline:1px dotted ButtonText}.swagger-ui fieldset{padding:.35em .75em .625em}.swagger-ui legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}.swagger-ui progress{display:inline-block;vertical-align:baseline}.swagger-ui textarea{overflow:auto}.swagger-ui [type=checkbox],.swagger-ui [type=radio]{box-sizing:border-box;padding:0}.swagger-ui [type=number]::-webkit-inner-spin-button,.swagger-ui [type=number]::-webkit-outer-spin-button{height:auto}.swagger-ui [type=search]{-webkit-appearance:textfield;outline-offset:-2px}.swagger-ui [type=search]::-webkit-search-cancel-button,.swagger-ui [type=search]::-webkit-search-decoration{-webkit-appearance:none}.swagger-ui ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}.swagger-ui details,.swagger-ui menu{display:block}.swagger-ui summary{display:list-item}.swagger-ui canvas{display:inline-block}.swagger-ui [hidden],.swagger-ui template{display:none}.swagger-ui .debug *{outline:1px solid gold}.swagger-ui .debug-white *{outline:1px solid #fff}.swagger-ui .debug-black *{outline:1px solid #000}.swagger-ui .debug-grid{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTRDOTY4N0U2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTRDOTY4N0Q2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3NjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3NzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsBS+GMAAAAjSURBVHjaYvz//z8DLsD4gcGXiYEAGBIKGBne//fFpwAgwAB98AaF2pjlUQAAAABJRU5ErkJggg==) repeat 0 0}.swagger-ui .debug-grid-16{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+ILine truncated
|
.swagger-ui{color:#3b4151;container-name:swagger-ui;container-type:inline-size;/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */font-family:sans-serif}.swagger-ui html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}.swagger-ui body{margin:0}.swagger-ui article,.swagger-ui aside,.swagger-ui footer,.swagger-ui header,.swagger-ui nav,.swagger-ui section{display:block}.swagger-ui h1{font-size:2em;margin:.67em 0}.swagger-ui figcaption,.swagger-ui figure,.swagger-ui main{display:block}.swagger-ui figure{margin:1em 40px}.swagger-ui hr{box-sizing:content-box;height:0;overflow:visible}.swagger-ui pre{font-family:monospace,monospace;font-size:1em}.swagger-ui a{background-color:transparent;-webkit-text-decoration-skip:objects}.swagger-ui abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.swagger-ui b,.swagger-ui strong{font-weight:inherit;font-weight:bolder}.swagger-ui code,.swagger-ui kbd,.swagger-ui samp{font-family:monospace,monospace;font-size:1em}.swagger-ui dfn{font-style:italic}.swagger-ui mark{background-color:#ff0;color:#000}.swagger-ui small{font-size:80%}.swagger-ui sub,.swagger-ui sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}.swagger-ui sub{bottom:-.25em}.swagger-ui sup{top:-.5em}.swagger-ui audio,.swagger-ui video{display:inline-block}.swagger-ui audio:not([controls]){display:none;height:0}.swagger-ui img{border-style:none}.swagger-ui svg:not(:root){overflow:hidden}.swagger-ui button,.swagger-ui input,.swagger-ui optgroup,.swagger-ui select,.swagger-ui textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}.swagger-ui button,.swagger-ui input{overflow:visible}.swagger-ui button,.swagger-ui select{text-transform:none}.swagger-ui [type=reset],.swagger-ui [type=submit],.swagger-ui button,.swagger-ui html [type=button]{-webkit-appearance:button}.swagger-ui [type=button]::-moz-focus-inner,.swagger-ui [type=reset]::-moz-focus-inner,.swagger-ui [type=submit]::-moz-focus-inner,.swagger-ui button::-moz-focus-inner{border-style:none;padding:0}.swagger-ui [type=button]:-moz-focusring,.swagger-ui [type=reset]:-moz-focusring,.swagger-ui [type=submit]:-moz-focusring,.swagger-ui button:-moz-focusring{outline:1px dotted ButtonText}.swagger-ui fieldset{padding:.35em .75em .625em}.swagger-ui legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}.swagger-ui progress{display:inline-block;vertical-align:baseline}.swagger-ui textarea{overflow:auto}.swagger-ui [type=checkbox],.swagger-ui [type=radio]{box-sizing:border-box;padding:0}.swagger-ui [type=number]::-webkit-inner-spin-button,.swagger-ui [type=number]::-webkit-outer-spin-button{height:auto}.swagger-ui [type=search]{-webkit-appearance:textfield;outline-offset:-2px}.swagger-ui [type=search]::-webkit-search-cancel-button,.swagger-ui [type=search]::-webkit-search-decoration{-webkit-appearance:none}.swagger-ui ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}.swagger-ui details,.swagger-ui menu{display:block}.swagger-ui summary{display:list-item}.swagger-ui canvas{display:inline-block}.swagger-ui [hidden],.swagger-ui template{display:none}.swagger-ui .debug *{outline:1px solid gold}.swagger-ui .debug-white *{outline:1px solid #fff}.swagger-ui .debug-black *{outline:1px solid #000}.swagger-ui .debug-grid{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTRDOTY4N0U2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTRDOTY4N0Q2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3NjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3NzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsBS+GMAAAAjSURBVHjaYvz//z8DLsD4gcGXiYEAGBIKGBne//fFpwAgwAB98AaF2pjlUQAAAABJRU5ErkJggg==) repeat 0 0}.swagger-ui .debug-grid-16{background:transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpLine truncated
|
||||||
|
|
||||||
/*# sourceMappingURL=swagger-ui.css.map*/
|
/*# sourceMappingURL=swagger-ui.css.map*/
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"swagger-ui.css","mappings":"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,CDrDb,oDC8BW,CD7BX,oDC8BW,CD7BX,oDC8BW,CD7BX,oDC8BW,CD7BX,oDC8BW,CD7BX,oDC8BW,CD7BX,oDC8BW,CD7BX,oDC8BW,CD7BX,oDC8BW,CD7BX,qDC8BW,CD7BX,uDC8BY,CD7BZ,wDC8Ba,CD5Bb,6CCyCW,CDxCX,wCCyCM,CDxCN,8CCyCY,CDxCZ,2CCyCS,CDxCT,yCCyCO,CDxCP,wCCyCS,CDxCT,iDCyCe,CDxCf,2CCyCS,CDxCT,iDCyCe,CDxCf,8CCyCY,CDxCZ,6CCyCW,CDxCX,yCCyCO,CDxCP,+CCyCa,CDxCb,+CCyCa,CDxCb,0CCyCQ,CDxCR,gDCyCc,CDxCd,yCCyCO,CDxCP,8CCyCY,CDxCZ,yCCyCO,CDxCP,+CCyCa,CDxCb,kDCyCgB,CDxChB,gDCyCc,CDxCd,iDCyCe,CDxCf,kDCyCgB,CDxChB,+CCyCa,CDvCb,oDCVc,CDWd,6CE3DE,gCDmBmB,CClBnB,sCDmBgB,CClBhB,qCDmBgB,CClBhB,oCDmBgB,CClBhB,mCDmBgB,CClBhB,sCDmBqB,CClBrB,yCDmBmB,CClBnB,wBACI,yBACA,0BAEJ,qBACI,4BACA,6BAEJ,uBAEI,4BADA,wBACA,CAEJ,sBAEI,6BADA,yBACA,CAGN,mCACE,mCDNmB,CCOnB,yCDNgB,CCOhB,wCDNgB,CCOhB,uCDNgB,CCOhB,sCDNgB,CCOhB,yCDNqB,CCOrB,4CDNmB,CCOnB,2BACI,yBACA,0BAEJ,wBACI,4BACA,6BAEJ,0BAEI,4BADA,wBACA,CAEJ,yBAEI,6BADA,yBACA,EAIN,uDACE,kCDhCmB,CCiCnB,wCDhCgB,CCiChB,uCDhCgB,CCiChB,sCDhCgB,CCiChB,qCDhCgB,CCiChB,wCDhCqB,CCiCrB,2CDhCmB,CCiCnB,0BACI,yBACA,0BAEJ,uBACI,4BACA,6BAEJ,yBAEI,4BADA,wBACA,CAEJ,wBAEI,6BADA,yBACA,EAIN,mCACE,kCD1DmB,CC2DnB,wCD1DgB,CC2DhB,uCD1DgB,CC2DhB,sCD1DgB,CC2DhB,qCD1DgB,CC2DhB,wCD1DqB,CC2DrB,2CD1DmB,CC2DnB,0BACI,yBACA,0BAEJ,uBACI,4BACA,6BAEJ,yBAEI,4BADA,wBACA,CAEJ,wBAEI,6BADA,yBACA,ECrGN,2CACA,2CACA,yCACA,uCAEA,mCACE,8CACA,8CACA,4CACA,2CAGF,uDACE,6CACA,6CACA,2CACA,0CAGF,mCACE,6CACA,6CACA,2CACA,0CCvBF,+BH6BoB,CG5BpB,qCH6BiB,CG5BjB,oCH6BiB,CG5BjB,mCH6BiB,CG5BjB,kCH6BiB,CG5BjB,kCH6BiB,CG1BjB,oCHqBoB,CGpBpB,sCHoBoB,CGnBpB,uCHmBoB,CGlBpB,qCHkBoB,CGhBpB,mCACE,kCHekB,CGdlB,wCHee,CGdf,uCHee,CGdf,sCHee,CGdf,qCHee,CGdf,qCHee,CGdf,uCHSkB,CGRlB,yCHQkB,CGPlB,0CHOkB,CGNlB,wCHMkB,EGHpB,uDACE,iCHEkB,CGDlB,uCHEe,CGDf,sCHEe,CGDf,qCHEe,CGDf,oCHEe,CGDf,oCHEe,CGDf,sCHJkB,CGKlB,wCHLkB,CGMlB,yCHNkB,CGOlB,uCHPkB,EGUpB,mCACE,iCHXkB,CGYlB,uCHXe,CGYf,sCHXe,CGYf,qCHXe,CGYf,oCHXe,CGYf,oCHXe,CGYf,sCHjBkB,CGkBlB,wCHlBkB,CGmBlB,yCHnBkB,CGoBlB,uCHpBkB,EIxCpB,2DJ8Ce,CI7Cf,2DJ8Ce,CI7Cf,+DJ8Ce,CI7Cf,6DJ8Ce,CI7Cf,6DJ8Ce,CI5Cf,mCACE,8DJuCa,CItCb,8DJuCa,CItCb,kEJuCa,CItCb,gEJuCa,CItCb,gEJuCa,EIpCf,uDACE,6DJ+Ba,CI9Bb,6DJ+Ba,CI9Bb,iEJ+Ba,CI9Bb,+DJ+Ba,CI9Bb,+DJ+Ba,EI5Bf,mCACE,6DJuBa,CItBb,6DJuBa,CItBb,iEJuBa,CItBb,+DJuBa,CItBb,+DJuBa,EKxDf,iBACE,gBACA,kBACA,gBCkBF,yBACA,6BACA,+BACA,2BAEA,4BACA,gCACA,kCACA,8BAEA,4BACA,gCACA,kCACA,8BAEA,8BACA,kCACA,oCACA,gCAEA,8BACA,kCACA,oCACA,gCAGA,4BAGE,SACA,OAFA,QADA,KAGA,CAGF,mCACE,4BACA,8BACA,gCACA,kCACA,+BACA,iCACA,mCACA,qCACA,+BACA,iCACA,mCACA,qCACA,iCACA,qCACA,uCACA,mCACA,iCACA,qCACA,uCACA,mCACA,+BAGE,SACA,OAFA,QADA,KAGA,EAIJ,uDACE,2BACA,6BACA,+BACA,iLine truncated
|
{"version":3,"file":"swagger-ui.css","mappings":"AAAA,YCII,aCcU,CFfd,yBACE,2BACA,4EGEF,CFJE,sBEIF,CDWc,iCCCZ,0BACA,8BACA,kBAUF,QACE,iHAOF,aAME,gBAQF,aACE,eACA,4DAWF,aAGE,oBAOF,eACE,gBAQF,sBACE,SACA,iBACA,iBAQF,+BACE,cACA,eAWF,4BACE,qCACA,yBAQF,kBACE,CACA,yBACA,CADA,wCACA,CADA,gCACA,kCAOF,mBAEE,CAOF,kBAPE,CASA,kDAQF,+BAGE,cACA,iBAOF,iBACE,kBAOF,qBACE,WACA,mBAOF,aACE,iCAQF,aAEE,cACA,kBACA,wBACA,iBAGF,aACE,iBAGF,SACE,qCAUF,oBAEE,mCAOF,YACE,SACA,iBAOF,iBACE,4BAOF,eACE,mGAWF,sBAKE,eACA,iBACA,SACA,sCAQF,gBAEE,uCAQF,mBAEE,sGASF,yBAIE,yKAOF,iBAIE,UACA,6JAOF,6BAIE,sBAOF,0BACE,oBAUF,qBACE,cACA,cACA,eACA,UACA,mBACA,sBAQF,oBACE,wBACA,sBAOF,aACE,sDAQF,qBAEE,UACA,2GAOF,WAEE,2BAQF,4BACE,oBACA,8GAOF,uBAEE,0CAQF,yBACE,aACA,sCAWF,aAEE,qBAOF,iBACE,oBAUF,oBACE,CAQA,0CAUF,YACE,sBCnbF,kDACA,kDACA,+CCDA,yEACE,4BAGF,yEACE,iCAGF,kEACE,kCAGF,kEACE,2tBClBF,qBAkCE,2BCrBF,QACE,kBACA,iCAGF,sDACA,sDAEA,kDACA,sDAEA,oDACA,mDAEA,oDACA,mDAEA,qDACA,mDAEA,sDAKI,QACA,CAEA,WACA,CAHA,MACA,CALJ,iBACI,CACA,OACA,CAFA,KACA,CAGA,UACA,CACA,WACA,oCAGJ,6BACI,QACE,kBACA,oCAEF,yDACA,yDACA,qDACA,yDACA,uDACA,sDACA,uDACA,sDACA,wDACA,sDACA,yDAII,QACA,CAEA,WACA,CAHA,MACA,CALJ,iBACI,CACA,OACA,CAFA,KACA,CAGA,UACA,CACA,WACA,yDAIR,4BACI,QACE,kBACA,mCAEF,wDACA,wDACA,oDACA,wDACA,sDACA,qDACA,sDACA,qDACA,uDACA,qDACA,wDAII,QACA,CAEA,WACA,CAHA,MACA,CALJ,iBACI,CACA,OACA,CAFA,KACA,CAGA,UACA,CACA,WACA,qCAIR,4BACI,QACE,kBACA,mCAEF,wDACA,wDACA,oDACA,wDACA,sDACA,qDACA,sDACA,qDACA,uDACA,qDACA,wDAII,QACA,CAEA,WACA,CAHA,MACA,CALJ,iBACI,CACA,OACA,CAFA,KACA,CAGA,UACA,CACA,WACA,kBC3HR,kCCQE,qDACA,qEAEF,sBACE,wDACA,0FAGF,qBACE,uDACA,sEAGF,qBACE,uDACA,0DCVA,uBACA,CAFF,2BAEE,qBAIA,uBACA,CAFF,2BAEE,uBAIA,wBACA,CAFF,2BAEE,wBAIA,0BACA,CAFF,2BAEE,sBAIA,qBACA,CAFF,2BAEE,oCAGF,0BAEI,uBACA,CAFF,2BAEE,wBAIA,uBACA,CAFF,2BAEE,0BAIA,wBACA,CAFF,2BAEE,2BAIA,0BACA,CAFF,2BAEE,yBAIA,qBACA,CAFF,2BAEE,yDAIJ,yBAEI,uBACA,CAFF,2BAEE,uBAIA,uBACA,CAFF,2BAEE,yBAIA,wBACA,CAFF,2BAEE,0BAIA,0BACA,CAFF,2BAEE,wBAIA,qBACA,CAFF,2BAEE,qCAIJ,yBAEI,uBACA,CAFF,2BAEE,uBAIA,uBACA,CAFF,2BAEE,yBAIA,wBACA,CAFF,2BAEE,0BAIA,0BACA,CAFF,2BAEE,wBAIA,qBACA,CAFF,2BAEE,uBChHJ,mDACA,qDACA,6CAEA,wBACE,sDACA,wDACA,kEAGF,uBACE,qDACA,uDACA,8CAGF,uBACE,qDACA,uDACA,2BCPA,oDACA,4DACA,gEACA,kEACA,8DACA,oEAGF,mBACE,uDACA,+DACA,mEACA,qEACA,iEACA,yFAGF,kBACE,sDACA,8DACA,kEACA,oEACA,gEACA,qEAGF,kBACE,sDACA,8DACA,kEACA,oEACA,gEACA,wDCnCF,iBC2CQ,4BD1CR,iBC2Ca,2BD1Cb,iBC2CY,0BD1CZ,iBC2CW,sBD1CX,iBC2CO,wBD1CP,iBC2CS,8BD1CT,iBC2Ce,2BD1Cf,iBC2CY,4BD1CZ,iBC2Ca,4BD1Cb,oBC2Ca,uBD1Cb,iBC2CQ,0BDzCR,+BCuDW,0BDtDX,+BCuDW,0BDtDX,+BCuDW,0BDtDX,+BCuDW,0BDtDX,+BCuDW,0BDtDX,+BCuDW,0BDtDX,+BCuDW,0BDtDX,+BCuDW,0BDtDX,+BCuDW,0BDtDX,gCCuDW,2BDtDX,iCCuDY,4BDtDZ,iCCuDa,0BDrDb,2BC8BW,0BD7BX,2BC8BW,0BD7BX,2BC8BW,0BD7BX,2BC8BW,0BD7BX,2BC8BW,0BD7BX,2BC8BW,0BD7BX,2BC8BW,0BD7BX,2BC8BW,0BD7BX,2BC8BW,0BD7BX,4BC8BW,2BD7BX,6BC8BY,4BD7BZ,6BC8Ba,0BD5Bb,oBCyCW,qBDxCX,oBCyCM,2BDxCN,oBCyCY,wBDxCZ,oBCyCS,sBDxCT,oBCyCO,wBDxCP,iBCyCS,8BDxCT,oBCyCe,wBDxCf,oBCyCS,8BDxCT,oBCyCe,2BDxCf,oBCyCY,0BDxCZ,oBCyCW,sBDxCX,oBCyCO,4BDxCP,oBCyCa,4BDxCb,oBCyCa,uBDxCb,oBCyCQ,6BDxCR,oBCyCc,sBDxCd,oBCyCO,2BDxCP,oBCyCY,sBDxCZ,oBCyCO,4BDxCP,oBCyCa,+BDxCb,oBCyCgB,6BDxChB,oBCyCc,8BDxCd,oBCyCe,+BDxCf,oBCyCgB,4BDxChB,oBCyCa,6BDvCb,wBCVc,yBDWd,sCE3DE,eDmBmB,kBClBnB,qBDmBgB,kBClBhB,oBDmBgB,kBClBhB,mBDmBgB,kBClBhB,kBDmBgB,qBClBhB,kBDmBqB,sBClBrB,oBDmBmB,yBClBnB,wBACI,0BACA,sBAEJ,2BACI,6BACA,wBAGA,2BACA,CAFJ,wBAEI,uBAGA,4BACA,CAFJ,yBAEI,oCAGN,oBACE,eDNmB,qBCOnB,qBDNgB,qBCOhB,oBDNgB,qBCOhB,mBDNgB,qBCOhB,kBDNgB,wBCOhB,kBDNqB,yBCOrB,oBDNmB,4BCOnB,wBACI,0BACA,yBAEJ,2BACI,6BACA,2BAGA,2BACA,CAFJ,wBAEI,0BAGA,4BACA,CAFJ,yBAEI,yDAIN,mBACE,eDhCmB,oBCiCnB,qBDhCgB,oBCiChB,oBDhCgB,oBCiChB,mBDhCgB,oBCiChB,kBDhCgB,uBCiChB,kBDhCqB,wBCiCrB,oBDhCmB,2BCiCnB,wBACI,0BACA,wBAEJ,2BACI,6BACA,0BAGA,2BACA,CAFJ,wBAEI,yBAGA,4BACA,CAFJ,yBAEI,qCAIN,mBACE,eD1DmB,oBC2DnB,qBD1DgB,oBC2DhB,oBD1DgB,oBC2DhB,mBD1DgB,oBC2DhB,kBD1DgB,uBC2DhB,kBD1DqB,wBC2DrB,oBD1DmB,2BC2DnB,wBACI,0BACA,wBAEJ,2BACI,6BACA,0BAGA,2BACA,CAFJ,wBAEI,yBAGA,4BACA,CAFJ,yBAEI,yBCrGN,2CACA,0CACA,wCACA,qDAEA,0BACE,8CACA,6CACA,2CACA,0EAGF,yBACE,6CACA,4CACA,0CACA,sDAGF,yBACE,6CACA,4CACA,0CACA,oCCvBF,cH6BoB,kBG5BpB,oBH6BiB,kBG5BjB,mBH6BiB,kBG5BjB,kBH6BiB,kBG5BjB,iBH6BiB,kBG5BjB,iBH6BiB,mBG1BjB,kBHqBoB,mBGpBpB,oBHoBoB,mBGnBpB,qBHmBoB,mBGlBpB,mBHkBoB,oCGhBpB,oBACE,cHekB,qBGdlB,oBHee,qBGdf,mBHee,qBGdf,kBHee,qBGdf,iBHee,qBGdf,iBHee,sBGdf,kBHSkB,sBGRlB,oBHQkB,sBGPlB,qBHOkB,sBGNlB,mBHMkB,yDGHpB,mBACE,cHEkB,oBGDlB,oBHEe,oBGDf,mBHEe,oBGDf,kBHEe,oBGDf,iBHEe,oBGDf,iBHEe,qBGDf,kBHJkB,qBGKlB,oBHLkB,qBGMlB,qBHNkB,qBGOlB,mBHPkB,qCGUpB,mBACE,cHXkB,oBGYlB,oBHXe,oBGYf,mBHXe,oBGYf,kBHXe,oBGYf,iBHXe,oBGYf,iBHXe,qBGYf,kBHjBkB,qBGkBlB,oBHlBkB,qBGmBlB,qBHnBkB,qBGoBlB,mBHpBkB,wBIxCpB,qCJ8Ce,uBI7Cf,qCJ8Ce,uBI7Cf,yCJ8Ce,uBI7Cf,uCJ8Ce,uBI7Cf,uCJ8Ce,oCI5Cf,yBACE,qCJuCa,0BItCb,qCJuCa,0BItCb,yCJuCa,0BItCb,uCJuCa,0BItCb,uCJuCa,yDIpCf,wBACE,qCJ+Ba,yBI9Bb,qCJ+Ba,yBI9Bb,yCJ+Ba,yBI9Bb,uCJ+Ba,yBI9Bb,uCJ+Ba,qCI5Bf,wBACE,qCJuBa,yBItBb,qCJuBa,yBItBb,yCJuBa,yBItBb,uCJuBa,yBItBb,uCJuBa,mBKxDf,eACE,kBACA,gBACA,oBCkBF,2BACA,8BACALine truncated
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SwaggerUICore=t():e.SwaggerUICore=t()}(this,(()=>(()=>{var e={6024:(e,t,n)=>{"use strict";n.d(t,{Z:()=>E});var s=n(4250),r=n.n(s),a=n(1093),o=n.n(a),l=n(8493),c=n.n(l),i=n(3942),u=n.n(i),p=n(6689),m=n.n(p);const d=require("react-immutable-pure-component");var h=n.n(d),g=n(8082),f=n.n(g),y=n(580),S=n.n(y);const v=e=>{const t=e.replace(/~1/g,"/").replace(/~0/g,"~");try{return decodeURIComponent(t)}catch{return t}};class E extends(h()){constructor(){super(...arguments),o()(this,"getModelName",(e=>-1!==c()(e).call(e,"#/definitions/")?v(e.replace(/^.*#\/definitions\//,"")):-1!==c()(e).call(e,"#/components/schemas/")?v(e.replace(/^.*#\/components\/schemas\//,"")):void 0)),o()(this,"getRefSchema",(e=>{let{specSelectors:t}=this.props;return t.findDefinition(e)}))}render(){let{getComponent:e,getConfigs:t,specSelectors:s,schema:a,required:o,name:l,isRef:c,specPath:i,displayName:u,includeReadOnly:p,includeWriteOnly:d}=this.props;const h=e("ObjectModel"),g=e("ArrayModel"),f=e("PrimitiveModel");let y="object",S=a&&a.get("$$ref");if(!l&&S&&(l=this.getModelName(S)),!a&&S&&(a=this.getRefSchema(l)),!a)return m().createElement("span",{className:"model model-title"},m().createElement("span",{className:"model-title__text"},u||l),m().createElement("img",{src:n(2517),height:"20px",width:"20px"}));const v=s.isOAS3()&&a.get("deprecated");switch(c=void 0!==c?c:!!S,y=a&&a.get("type")||y,y){case"object":return m().createElement(h,r()({className:"object"},this.props,{specPath:i,getConfigs:t,schema:a,name:l,deprecated:v,isRef:c,includeReadOnly:p,includeWriteOnly:d}));case"array":return m().createElement(g,r()({className:"array"},this.props,{getConfigs:t,schema:a,name:l,deprecated:v,required:o,includeReadOnly:p,includeWriteOnly:d}));default:return m().createElement(f,r()({},this.props,{getComponent:e,getConfigs:t,schema:a,name:l,deprecated:v,required:o}))}}}o()(E,"propTypes",{schema:u()(f()).isRequired,getComponent:S().func.isRequired,getConfigs:S().func.isRequired,specSelectors:S().object.isRequired,name:S().string,displayName:S().string,isRef:S().bool,required:S().bool,expandDepth:S().number,depth:S().number,specPath:f().list.isRequired,includeReadOnly:S().bool,includeWriteOnly:S().bool})},5623:(e,t,n)=>{"use strict";n.d(t,{Z:()=>d});var s=n(1093),r=n.n(s),a=n(7252),o=n.n(a),l=n(6689),c=n.n(l),i=n(3883),u=n.n(i),p=(n(580),n(1669)),m=n(7504);class d extends c().Component{constructor(e,t){super(e,t),r()(this,"getDefinitionUrl",(()=>{let{specSelectors:e}=this.props;return new(u())(e.url(),m.Z.location).toString()}));let{getConfigs:n}=e,{validatorUrl:s}=n();this.state={url:this.getDefinitionUrl(),validatorUrl:void 0===s?"https://validator.swagger.io/validator":s}}UNSAFE_componentWillReceiveProps(e){let{getConfigs:t}=e,{validatorUrl:n}=t();this.setState({url:this.getDefinitionUrl(),validatorUrl:void 0===n?"https://validator.swagger.io/validator":n})}render(){let{getConfigs:e}=this.props,{spec:t}=e(),n=(0,p.Nm)(this.state.validatorUrl);return"object"==typeof t&&o()(t).length?null:this.state.url&&(0,p.hW)(this.state.validatorUrl)&&(0,p.hW)(this.state.url)?c().createElement("span",{className:"float-right"},c().createElement("a",{target:"_blank",rel:"noopener noreferrer",href:`${n}/debug?url=${encodeURIComponent(this.state.url)}`},c().createElement(h,{src:`${n}?url=${encodeURIComponent(this.state.url)}`,alt:"Online validator badge"}))):null}}class h extends c().Component{constructor(e){super(e),this.state={loaded:!1,error:!1}}componentDidMount(){const e=new Image;e.onload=()=>{this.setState({loaded:!0})},e.onerror=()=>{this.setState({error:!0})},e.src=this.props.src}UNSAFE_componentWillReceiveProps(e){if(e.src!==this.props.src){const t=new Image;t.onload=()=>{this.setState({loaded:!0})},t.onerror=()=>{this.setState({error:!0})},t.src=e.src}}render(){return this.state.error?c().createElement("img",{alt:"Error"}):this.state.loaded?c().createElement("img",{src:this.props.src,alt:this.props.alt}):null}}},2552:(e,t,n)=>{"use strict";n.d(t,{Z:()=>m,s:()=>d});var s=n(6689),r=n.n(s),a=(n(580),n(963));const o=require("remarkable/linkify"),l=require("dompurify");var c=n.n(l),i=n(9003),u=n.n(i);function p(e){let{source:t,className:n="",getConfigs:s}=e;if("string"!=typeof t)return null;const l=new a.Remarkable({html:!0,typographer:!0,breaks:!0,linkTarget:"_blank"}).use(o.linkify);l.core.ruler.disable(["replacements","smartquotes"]);const{useUnsafeMarkdown:c}=s(),i=l.render(t),p=d(i,{useUnsafeMarkdown:c});return t&&i&&p?r().createElement("div",{className:u()(n,"markdown"),dangerouslySetInnerHTML:{__html:p}}):null}c().addHook&&c().addHook("beforeSanitizeElements",(function(e){return e.href&&e.setAttribute("rel","noopener noreferrer"),e})),p.defaultProps={getConfigs:()=>({useUnsafeMarkdown:!1})};const m=p;function d(e){let{useUnsafeMarkdown:t=!1}=arguments.length>1&&void 0!==arguments[1]?arguLine truncated
|
!function webpackUniversalModuleDefinition(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SwaggerUICore=t():e.SwaggerUICore=t()}(this,(()=>(()=>{"use strict";var e={158(e){e.exports=require("buffer")}},t={};function __webpack_require__(a){var r=t[a];if(void 0!==r)return r.exports;var n=t[a]={exports:{}};return e[a](n,n.exports,__webpack_require__),n.exports}__webpack_require__.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return __webpack_require__.d(t,{a:t}),t},__webpack_require__.d=(e,t)=>{for(var a in t)__webpack_require__.o(t,a)&&!__webpack_require__.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},__webpack_require__.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),__webpack_require__.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var a={};__webpack_require__.d(a,{default:()=>Cl});var r={};__webpack_require__.r(r),__webpack_require__.d(r,{CLEAR:()=>$,CLEAR_BY:()=>L,NEW_AUTH_ERR:()=>V,NEW_SPEC_ERR:()=>R,NEW_SPEC_ERR_BATCH:()=>J,NEW_THROWN_ERR:()=>P,NEW_THROWN_ERR_BATCH:()=>T,clear:()=>clear,clearBy:()=>clearBy,newAuthErr:()=>newAuthErr,newSpecErr:()=>newSpecErr,newSpecErrBatch:()=>newSpecErrBatch,newThrownErr:()=>newThrownErr,newThrownErrBatch:()=>newThrownErrBatch});var n={};__webpack_require__.r(n),__webpack_require__.d(n,{AUTHORIZE:()=>ue,AUTHORIZE_OAUTH2:()=>he,CONFIGURE_AUTH:()=>ge,LOGOUT:()=>de,RESTORE_AUTHORIZATION:()=>ye,SHOW_AUTH_POPUP:()=>me,authPopup:()=>authPopup,authorize:()=>authorize,authorizeAccessCodeWithBasicAuthentication:()=>authorizeAccessCodeWithBasicAuthentication,authorizeAccessCodeWithFormParams:()=>authorizeAccessCodeWithFormParams,authorizeApplication:()=>authorizeApplication,authorizeOauth2:()=>authorizeOauth2,authorizeOauth2WithPersistOption:()=>authorizeOauth2WithPersistOption,authorizePassword:()=>authorizePassword,authorizeRequest:()=>authorizeRequest,authorizeWithPersistOption:()=>authorizeWithPersistOption,configureAuth:()=>configureAuth,logout:()=>logout,logoutWithPersistOption:()=>logoutWithPersistOption,persistAuthorizationIfNeeded:()=>persistAuthorizationIfNeeded,preAuthorizeImplicit:()=>preAuthorizeImplicit,restoreAuthorization:()=>restoreAuthorization,showDefinitions:()=>showDefinitions});var s={};__webpack_require__.r(s),__webpack_require__.d(s,{authorized:()=>ve,definitionsForRequirements:()=>definitionsForRequirements,definitionsToAuthorize:()=>_e,getConfigs:()=>we,getDefinitionsByNames:()=>getDefinitionsByNames,isAuthorized:()=>isAuthorized,selectAuthPath:()=>selectAuthPath,shownDefinitions:()=>Ee});var o={};__webpack_require__.r(o),__webpack_require__.d(o,{TOGGLE_CONFIGS:()=>qe,UPDATE_CONFIGS:()=>Ie,downloadConfig:()=>downloadConfig,getConfigByUrl:()=>getConfigByUrl,loaded:()=>actions_loaded,toggle:()=>toggle,update:()=>update});var l={};__webpack_require__.r(l),__webpack_require__.d(l,{get:()=>get});var c={};__webpack_require__.r(c),__webpack_require__.d(c,{transform:()=>transform});var i={};__webpack_require__.r(i),__webpack_require__.d(i,{transform:()=>parameter_oneof_transform});var p={};__webpack_require__.r(p),__webpack_require__.d(p,{allErrors:()=>Ye,lastError:()=>Qe});var m={};__webpack_require__.r(m),__webpack_require__.d(m,{SHOW:()=>nt,UPDATE_FILTER:()=>at,UPDATE_LAYOUT:()=>tt,UPDATE_MODE:()=>rt,changeMode:()=>changeMode,show:()=>actions_show,updateFilter:()=>updateFilter,updateLayout:()=>updateLayout});var u={};__webpack_require__.r(u),__webpack_require__.d(u,{current:()=>current,currentFilter:()=>currentFilter,isShown:()=>isShown,showSummary:()=>ot,whatMode:()=>whatMode});var d={};__webpack_require__.r(d),__webpack_require__.d(d,{taggedOperations:()=>taggedOperations});var h={};__webpack_require__.r(h),__webpack_require__.d(h,{getActiveLanguage:()=>it,getDefaultExpanded:()=>pt,getGenerators:()=>ct,getSnippetGenerators:()=>getSnippetGenerators});var g={};__webpack_require__.r(g),__webpack_require__.d(g,{JsonSchemaArrayItemFile:()=>JsonSchemaArrayItemFile,JsonSchemaArrayItemText:()=>JsonSchemaArrayItemText,JsonSchemaForm:()=>JsonSchemaForm,JsonSchema_array:()=>JsonSchema_array,JsonSchema_boolean:()=>JsonSchema_boolean,JsonSchema_object:()=>JsonSchema_object,JsonSchema_string:()=>JsonSchema_string});var y={};__webpack_require__.r(y),__webpack_require__.d(y,{allowTryItOutFor:()=>allowTryItOutFor,basePath:()=>ga,canExecuteScheme:()=>canExecuteScheme,consumes:()=>pa,consumesOptionsFor:()=>consumesOptionsFor,contentTypeValues:()=>contentTypeValues,currentProducesFor:()=>currentProducesFor,definitions:()=>ha,externalDocs:()=>na,findDefinition:()=>findDefinition,getOAS3RequiredRequestBodyContentType:()=>getOAS3RequiredRequestBodyContentType,getParameter:()=>getParameter,hasHost:()=>Ca,host:()=>ya,info:()=>ra,isMediaTypeSchemaPropertiesEqual:()=>isMediaTypeSchemaPropertiesEqual,isOAS3:()=>aa,lastError:()=>Ht,mutatedLine truncated
|
||||||
//# sourceMappingURL=swagger-ui.js.map
|
//# sourceMappingURL=swagger-ui.js.map
|
||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"swagger-ui.js","mappings":"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 truncated
|
{"version":3,"file":"swagger-ui.js","mappings":"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 truncated
|
||||||
@@ -8,7 +8,7 @@ if(file_exists('inc/vars.local.inc.php')) {
|
|||||||
$autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
|
$autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
|
||||||
|
|
||||||
header('Content-type: application/json');
|
header('Content-type: application/json');
|
||||||
if (strtolower($_GET['Protocol']) == 'activesync' && getenv('SKIP_SOGO') != "y") {
|
if (strtolower($_GET['Protocol']) == 'activesync' && $autodiscover_config['autodiscoverType'] == 'activesync' && getenv('SKIP_SOGO') != "y") {
|
||||||
echo '{"Protocol":"ActiveSync","Url":"' . $autodiscover_config['activesync']['url'] . '"}';
|
echo '{"Protocol":"ActiveSync","Url":"' . $autodiscover_config['activesync']['url'] . '"}';
|
||||||
}
|
}
|
||||||
elseif (strtolower($_GET['Protocol']) == 'autodiscoverv1') {
|
elseif (strtolower($_GET['Protocol']) == 'autodiscoverv1') {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ error_reporting(0);
|
|||||||
|
|
||||||
$data = trim(file_get_contents("php://input"));
|
$data = trim(file_get_contents("php://input"));
|
||||||
|
|
||||||
if (strpos($data, 'autodiscover/outlook/responseschema') !== false) {
|
if (strpos($data, 'autodiscover/outlook/responseschema') !== false && $autodiscover_config['autodiscoverType'] == 'activesync') {
|
||||||
$autodiscover_config['autodiscoverType'] = 'imap';
|
$autodiscover_config['autodiscoverType'] = 'imap';
|
||||||
if ($autodiscover_config['useEASforOutlook'] == 'yes' &&
|
if ($autodiscover_config['useEASforOutlook'] == 'yes' &&
|
||||||
// Office for macOS does not support EAS
|
// Office for macOS does not support EAS
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ table.dataTable>tbody>tr.child ul.dtr-details>li {
|
|||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.129);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.129);
|
||||||
padding: 0.5em 0;
|
padding: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
/* like the first entry has no top padding, the last one needs no bottom padding */
|
||||||
|
table.dataTable>tbody>tr.child ul.dtr-details>li:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
|
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
|
||||||
background-color: var(--mc-dtr-hover-bg);
|
background-color: var(--mc-dtr-hover-bg);
|
||||||
@@ -28,7 +32,8 @@ table.dataTable td.dt-control:before {
|
|||||||
color: var(--mc-dtr-color);
|
color: var(--mc-dtr-color);
|
||||||
height: 1em;
|
height: 1em;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
line-height: 1.25em;
|
line-height: 1em;
|
||||||
|
margin-top: calc(-0.5em - 1.5px);
|
||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -39,11 +44,29 @@ table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before,
|
|||||||
table.dataTable td.dt-control:before {
|
table.dataTable td.dt-control:before {
|
||||||
background-color: var(--mc-dtr-active-bg) !important;
|
background-color: var(--mc-dtr-active-bg) !important;
|
||||||
}
|
}
|
||||||
|
/* The default initial order targets column 0, which is usually the non-orderable expand column */
|
||||||
|
table.dataTable thead > tr > th.sorting_disabled.sorting_asc,
|
||||||
|
table.dataTable thead > tr > th.sorting_disabled.sorting_desc {
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
table.dataTable thead > tr > th.sorting_disabled.sorting_asc:before,
|
||||||
|
table.dataTable thead > tr > th.sorting_disabled.sorting_asc:after,
|
||||||
|
table.dataTable thead > tr > th.sorting_disabled.sorting_desc:before,
|
||||||
|
table.dataTable thead > tr > th.sorting_disabled.sorting_desc:after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
table.dataTable.table-striped>tbody>tr>td {
|
table.dataTable.table-striped>tbody>tr>td {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
table.dataTable.table-striped>tbody>tr>td>input[type="checkbox"] {
|
table.dataTable.table-striped>tbody>tr>td>input[type="checkbox"] {
|
||||||
margin-top: 7px;
|
/* centers the 1em checkbox in the 1.5em line, level with the expand control */
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
/* the vertical margin is meant for the tag editor in edit forms, in tables it only adds height */
|
||||||
|
table.dataTable .tag-badge {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
td.dtr-col-lg {
|
td.dtr-col-lg {
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
|
|||||||
@@ -139,6 +139,13 @@ body {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
.badge .bi {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
/* keeps icons from making a badge taller than one with text only */
|
||||||
|
.badge .bi::before {
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
.btn-group-xs > .btn, .btn-xs {
|
.btn-group-xs > .btn, .btn-xs {
|
||||||
padding: .25rem .4rem;
|
padding: .25rem .4rem;
|
||||||
font-size: .875rem;
|
font-size: .875rem;
|
||||||
@@ -343,6 +350,7 @@ code {
|
|||||||
}
|
}
|
||||||
.tag-badge .bi {
|
.tag-badge .bi {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
line-height: 0;
|
||||||
}
|
}
|
||||||
.tag-badge.btn-badge:hover {
|
.tag-badge.btn-badge:hover {
|
||||||
filter: brightness(0.9);
|
filter: brightness(0.9);
|
||||||
|
|||||||
@@ -191,7 +191,7 @@
|
|||||||
height: 2rem;
|
height: 2rem;
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
margin-top: -15px;
|
margin-top: calc(-1rem - 1.5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
li .dtr-data {
|
li .dtr-data {
|
||||||
|
|||||||
+83
-133
@@ -1,133 +1,83 @@
|
|||||||
.pagination a {
|
.pagination a {
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
.panel.panel-default {
|
.panel.panel-default {
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
.table-responsive {
|
.table-responsive {
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
.table-responsive {
|
.table-responsive {
|
||||||
overflow-x: scroll !important;
|
overflow-x: scroll !important;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
overflow-y:scroll;
|
overflow-y:scroll;
|
||||||
}
|
}
|
||||||
/* Fix modal moving content left */
|
/* Fix modal moving content left */
|
||||||
body.modal-open {
|
body.modal-open {
|
||||||
overflow-y:scroll;
|
overflow-y:scroll;
|
||||||
padding-right: inherit !important;
|
padding-right: inherit !important;
|
||||||
}
|
}
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
.container {
|
.container {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.mass-actions-admin {
|
.mass-actions-admin {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
.inputMissingAttr {
|
.inputMissingAttr {
|
||||||
border-color: #FF4136;
|
border-color: #FF4136;
|
||||||
}
|
}
|
||||||
.rotate {
|
.rotate {
|
||||||
-moz-transition: all 0.3s linear;
|
-moz-transition: all 0.3s linear;
|
||||||
-webkit-transition: all 0.3s linear;
|
-webkit-transition: all 0.3s linear;
|
||||||
transition: all 0.3s linear;
|
transition: all 0.3s linear;
|
||||||
}
|
}
|
||||||
.rotate.animation {
|
.rotate.animation {
|
||||||
-ms-transform:rotateX(180deg);
|
-ms-transform:rotateX(180deg);
|
||||||
-moz-transform:rotateX(180deg);
|
-moz-transform:rotateX(180deg);
|
||||||
-webkit-transform:rotateX(180deg);
|
-webkit-transform:rotateX(180deg);
|
||||||
transform:rotateX(180deg);
|
transform:rotateX(180deg);
|
||||||
}
|
}
|
||||||
.anchor {
|
.anchor {
|
||||||
display: block;
|
display: block;
|
||||||
height: 65px;
|
height: 65px;
|
||||||
margin-top: -65px;
|
margin-top: -65px;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
.thumbnail img {
|
.thumbnail img {
|
||||||
min-height:100px;
|
min-height:100px;
|
||||||
height:100px;
|
height:100px;
|
||||||
}
|
}
|
||||||
.nav-tabs>li>a {
|
.nav-tabs>li>a {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.table-condensed .input-sm {
|
.table-condensed .input-sm {
|
||||||
width: 100%!important;
|
width: 100%!important;
|
||||||
}
|
}
|
||||||
.table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td {
|
.table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
}
|
}
|
||||||
table tbody tr td input[type="checkbox"] {
|
table tbody tr td input[type="checkbox"] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
#quarantine_template {
|
#quarantine_template {
|
||||||
margin:20px;
|
margin:20px;
|
||||||
}
|
}
|
||||||
.regex-input {
|
.regex-input {
|
||||||
font-family: Consolas,monaco,monospace;
|
font-family: Consolas,monaco,monospace;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
.label-keys {
|
.label-keys {
|
||||||
font-size:100%;
|
font-size:100%;
|
||||||
margin: 0px !important;
|
margin: 0px !important;
|
||||||
white-space: normal !important;
|
white-space: normal !important;
|
||||||
}
|
}
|
||||||
.key-action {
|
.key-action {
|
||||||
font-weight:bold;
|
font-weight:bold;
|
||||||
color:white !important;
|
color:white !important;
|
||||||
}
|
}
|
||||||
.dkim-label {
|
.dkim-label {
|
||||||
margin: 0 0 8px !important;
|
margin: 0 0 8px !important;
|
||||||
}
|
}
|
||||||
/* backup schedule: the weekday toggles and the remove buttons of the entries react to the mouse, in every theme */
|
|
||||||
.backup-schedule-days .btn:hover {
|
|
||||||
background-image: linear-gradient(rgba(128, 128, 128, .3), rgba(128, 128, 128, .3));
|
|
||||||
}
|
|
||||||
.backup-schedule-remove {
|
|
||||||
opacity: .6;
|
|
||||||
}
|
|
||||||
.backup-schedule-remove:hover, .backup-schedule-remove:focus-visible {
|
|
||||||
opacity: 1;
|
|
||||||
color: var(--bs-danger) !important;
|
|
||||||
}
|
|
||||||
/* result and log of a backup job: the border of the theme in light and dark mode (Bootstrap's .border stays light) */
|
|
||||||
.backup-pre {
|
|
||||||
border: 1px solid var(--mc-border);
|
|
||||||
}
|
|
||||||
/* result of a restore job: an entry to open per mailbox, numbers in columns */
|
|
||||||
.backup-result {
|
|
||||||
border: 1px solid var(--mc-border);
|
|
||||||
}
|
|
||||||
.backup-result > details + details {
|
|
||||||
border-top: 1px solid var(--mc-border);
|
|
||||||
}
|
|
||||||
.backup-result summary {
|
|
||||||
list-style: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.backup-result summary::-webkit-details-marker {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.backup-result summary:hover {
|
|
||||||
background-image: linear-gradient(rgba(128, 128, 128, .12), rgba(128, 128, 128, .12));
|
|
||||||
}
|
|
||||||
.backup-result summary .bi-chevron-right {
|
|
||||||
transition: transform .15s;
|
|
||||||
}
|
|
||||||
.backup-result details[open] > summary .bi-chevron-right {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
.backup-result-body {
|
|
||||||
padding-left: 2.6rem !important;
|
|
||||||
}
|
|
||||||
.backup-result-body .table {
|
|
||||||
--bs-table-bg: transparent;
|
|
||||||
}
|
|
||||||
.backup-result-body .num {
|
|
||||||
width: 8.5rem;
|
|
||||||
text-align: right;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
@@ -495,16 +495,4 @@ input::placeholder {
|
|||||||
}
|
}
|
||||||
.btn-light, .btn-light:hover {
|
.btn-light, .btn-light:hover {
|
||||||
background-image: none;
|
background-image: none;
|
||||||
}
|
}
|
||||||
/* native time picker icon and popup in dark mode */
|
|
||||||
input[type="time"] {
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
/* switches while off: a light knob and a visible border (Bootstrap's dark knob is for light pages, the dark mode
|
|
||||||
does not set data-bs-theme) */
|
|
||||||
.form-switch .form-check-input:not(:checked):not(:focus) {
|
|
||||||
--bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.6%29'/%3e%3c/svg%3e");
|
|
||||||
}
|
|
||||||
.form-switch .form-check-input:not(:checked) {
|
|
||||||
border-color: var(--mc-btn-outline-border);
|
|
||||||
}
|
|
||||||
@@ -11,8 +11,6 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
|||||||
|
|
||||||
$js_minifier->add('/web/js/site/imapsync.js');
|
$js_minifier->add('/web/js/site/imapsync.js');
|
||||||
$js_minifier->add('/web/js/site/groups.js');
|
$js_minifier->add('/web/js/site/groups.js');
|
||||||
$js_minifier->add('/web/js/site/mail-policy.js');
|
|
||||||
$js_minifier->add('/web/js/site/retention.js');
|
|
||||||
$js_minifier->add('/web/js/site/mailbox.js');
|
$js_minifier->add('/web/js/site/mailbox.js');
|
||||||
$js_minifier->add('/web/js/presets/sieveMailbox.js');
|
$js_minifier->add('/web/js/presets/sieveMailbox.js');
|
||||||
$js_minifier->add('/web/js/site/pwgen.js');
|
$js_minifier->add('/web/js/site/pwgen.js');
|
||||||
@@ -47,8 +45,6 @@ $template_data = [
|
|||||||
'lang_edit' => json_encode($lang['edit']),
|
'lang_edit' => json_encode($lang['edit']),
|
||||||
'lang_datatables' => json_encode($lang['datatables']),
|
'lang_datatables' => json_encode($lang['datatables']),
|
||||||
'imapsync_redirect_uri' => \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->oauth->redirectUri(),
|
'imapsync_redirect_uri' => \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->oauth->redirectUri(),
|
||||||
'mail_policy_groups' => \Mailcow\MailPolicy\MailPolicyModule::fromGlobals()->policies->groupChoices($domains),
|
|
||||||
'retention_groups' => \Mailcow\Retention\RetentionModule::fromGlobals()->rules->groupChoices($domains),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
|
||||||
|
|
||||||
protect_route(['domainadmin']);
|
|
||||||
|
|
||||||
$role = "domainadmin";
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/restore.inc.php';
|
|
||||||
@@ -115,28 +115,6 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
|||||||
'mbox_templates' => ($_SESSION['mailcow_cc_role'] == 'admin') ? mailbox('get', 'mailbox_templates') : [],
|
'mbox_templates' => ($_SESSION['mailcow_cc_role'] == 'admin') ? mailbox('get', 'mailbox_templates') : [],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
elseif (isset($_GET['mail-policy']) &&
|
|
||||||
is_numeric($_GET['mail-policy'])) {
|
|
||||||
$mail_policy = \Mailcow\MailPolicy\MailPolicyModule::fromGlobals();
|
|
||||||
$result = $mail_policy->api->getPolicy($_GET['mail-policy']);
|
|
||||||
$template = 'edit/mail-policy.twig';
|
|
||||||
// Choices: mailboxes and groups of the domain of the policy
|
|
||||||
$template_data = [
|
|
||||||
'mailboxes' => !empty($result) ? mailbox('get', 'mailboxes', $result['domain']) : [],
|
|
||||||
'mail_policy_groups' => !empty($result) ? $mail_policy->policies->groupChoices([$result['domain']]) : [],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
elseif (isset($_GET['retention-rule']) &&
|
|
||||||
is_numeric($_GET['retention-rule'])) {
|
|
||||||
$retention = \Mailcow\Retention\RetentionModule::fromGlobals();
|
|
||||||
$result = $retention->api->getRule($_GET['retention-rule']);
|
|
||||||
$template = 'edit/retention-rule.twig';
|
|
||||||
// Choices: mailboxes and groups of the domain of the rule
|
|
||||||
$template_data = [
|
|
||||||
'mailboxes' => !empty($result) ? mailbox('get', 'mailboxes', $result['domain']) : [],
|
|
||||||
'retention_groups' => !empty($result) ? $retention->rules->groupChoices([$result['domain']]) : [],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
elseif (isset($_GET['api-key']) &&
|
elseif (isset($_GET['api-key']) &&
|
||||||
is_numeric($_GET['api-key'])) {
|
is_numeric($_GET['api-key'])) {
|
||||||
$result = \Mailcow\ApiKey\ApiKeyModule::fromGlobals()->api->getKey($_GET['api-key']);
|
$result = \Mailcow\ApiKey\ApiKeyModule::fromGlobals()->api->getKey($_GET['api-key']);
|
||||||
|
|||||||
@@ -74,9 +74,6 @@ $globalVariables = [
|
|||||||
'lang_danger' => json_encode($lang['danger']),
|
'lang_danger' => json_encode($lang['danger']),
|
||||||
'lang_syncjobs' => json_encode($lang['syncjobs']),
|
'lang_syncjobs' => json_encode($lang['syncjobs']),
|
||||||
'lang_groups' => json_encode($lang['groups']),
|
'lang_groups' => json_encode($lang['groups']),
|
||||||
'lang_mail_policy' => json_encode($lang['mail_policy'] ?? []),
|
|
||||||
'lang_retention' => json_encode($lang['retention'] ?? []),
|
|
||||||
'lang_backup' => json_encode($lang['backup'] ?? []),
|
|
||||||
'docker_timeout' => $DOCKER_TIMEOUT,
|
'docker_timeout' => $DOCKER_TIMEOUT,
|
||||||
'session_lifetime' => (int)$SESSION_LIFETIME,
|
'session_lifetime' => (int)$SESSION_LIFETIME,
|
||||||
'csrf_token' => $_SESSION['CSRF']['TOKEN'],
|
'csrf_token' => $_SESSION['CSRF']['TOKEN'],
|
||||||
|
|||||||
@@ -601,6 +601,13 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
|
|||||||
|
|
||||||
// validate mailcow_password
|
// validate mailcow_password
|
||||||
$mailcow_password = $user_res['attributes']['mailcow_password'][0];
|
$mailcow_password = $user_res['attributes']['mailcow_password'][0];
|
||||||
|
// Keycloak returns binary LDAP attributes like userPassword Base64 encoded
|
||||||
|
if (substr($mailcow_password, 0, 1) != '{') {
|
||||||
|
$decoded_password = base64_decode($mailcow_password, true);
|
||||||
|
if ($decoded_password !== false && substr($decoded_password, 0, 1) == '{') {
|
||||||
|
$mailcow_password = $decoded_password;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!verify_hash($mailcow_password, $pass)) {
|
if (!verify_hash($mailcow_password, $pass)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ function fwdhost($_action, $_data = null) {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
$hosts = array_unique(array_map(function ($host) {
|
||||||
|
$net = explode('/', $host, 2);
|
||||||
|
if (filter_var($net[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
$net[0] = inet_ntop(inet_pton($net[0]));
|
||||||
|
}
|
||||||
|
return implode('/', $net);
|
||||||
|
}, $hosts));
|
||||||
foreach ($hosts as $host) {
|
foreach ($hosts as $host) {
|
||||||
try {
|
try {
|
||||||
$redis->hSet('WHITELISTED_FWD_HOST', $host, $source);
|
$redis->hSet('WHITELISTED_FWD_HOST', $host, $source);
|
||||||
|
|||||||
@@ -1394,6 +1394,18 @@ function set_tfa($_data) {
|
|||||||
case "webauthn":
|
case "webauthn":
|
||||||
$key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
|
$key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
|
||||||
|
|
||||||
|
// Refuse incomplete WebAuthn registrations. Without this guard a request
|
||||||
|
// that does not contain the parsed credential data (e.g. a plain form
|
||||||
|
// POST) would silently insert an empty keyHandle while reporting success.
|
||||||
|
if (empty($_data['registration']) || empty($_data['registration']->credentialId)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_data_log),
|
||||||
|
'msg' => 'webauthn_registration_failed'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`)
|
$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`)
|
||||||
VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')");
|
VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
@@ -1416,11 +1428,11 @@ function set_tfa($_data) {
|
|||||||
// Block TFA removal if force_tfa policy is active
|
// Block TFA removal if force_tfa policy is active
|
||||||
$is_forced_tfa = false;
|
$is_forced_tfa = false;
|
||||||
if ($_SESSION['mailcow_cc_role'] === 'user') {
|
if ($_SESSION['mailcow_cc_role'] === 'user') {
|
||||||
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
|
$stmt_check = $pdo->prepare("SELECT JSON_VALUE(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
|
||||||
$stmt_check->execute(array($username));
|
$stmt_check->execute(array($username));
|
||||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||||
} else {
|
} else {
|
||||||
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
|
$stmt_check = $pdo->prepare("SELECT JSON_VALUE(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
|
||||||
$stmt_check->execute(array($username));
|
$stmt_check->execute(array($username));
|
||||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||||
}
|
}
|
||||||
@@ -1687,11 +1699,11 @@ function unset_tfa_key($_data) {
|
|||||||
// Block key removal if force_tfa policy is active
|
// Block key removal if force_tfa policy is active
|
||||||
$is_forced_tfa = false;
|
$is_forced_tfa = false;
|
||||||
if ($_SESSION['mailcow_cc_role'] === 'user') {
|
if ($_SESSION['mailcow_cc_role'] === 'user') {
|
||||||
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
|
$stmt_check = $pdo->prepare("SELECT JSON_VALUE(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
|
||||||
$stmt_check->execute(array($username));
|
$stmt_check->execute(array($username));
|
||||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||||
} else {
|
} else {
|
||||||
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
|
$stmt_check = $pdo->prepare("SELECT JSON_VALUE(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
|
||||||
$stmt_check->execute(array($username));
|
$stmt_check->execute(array($username));
|
||||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||||
}
|
}
|
||||||
@@ -2420,7 +2432,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
|||||||
case "use_ssl":
|
case "use_ssl":
|
||||||
case "use_tls":
|
case "use_tls":
|
||||||
case "login_provisioning":
|
case "login_provisioning":
|
||||||
case "ignore_ssl_errors":
|
case "ignore_ssl_error":
|
||||||
$settings[$row["key"]] = boolval($row["value"]);
|
$settings[$row["key"]] = boolval($row["value"]);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -2444,7 +2456,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
|||||||
if ($settings["authsource"] == "ldap"){
|
if ($settings["authsource"] == "ldap"){
|
||||||
$settings['use_ssl'] = !isset($settings['use_ssl']) ? false : $settings['use_ssl'];
|
$settings['use_ssl'] = !isset($settings['use_ssl']) ? false : $settings['use_ssl'];
|
||||||
$settings['use_tls'] = !isset($settings['use_tls']) ? false : $settings['use_tls'];
|
$settings['use_tls'] = !isset($settings['use_tls']) ? false : $settings['use_tls'];
|
||||||
$settings['ignore_ssl_errors'] = !isset($settings['ignore_ssl_errors']) ? false : $settings['ignore_ssl_errors'];
|
$settings['ignore_ssl_error'] = !isset($settings['ignore_ssl_error']) ? false : $settings['ignore_ssl_error'];
|
||||||
}
|
}
|
||||||
return $settings;
|
return $settings;
|
||||||
break;
|
break;
|
||||||
@@ -3106,7 +3118,17 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
|||||||
case "get-keycloak-admin-token":
|
case "get-keycloak-admin-token":
|
||||||
// get access_token for service account of mailcow client
|
// get access_token for service account of mailcow client
|
||||||
if ($iam_settings['authsource'] !== 'keycloak') return false;
|
if ($iam_settings['authsource'] !== 'keycloak') return false;
|
||||||
if (isset($iam_settings['access_token'])) {
|
// only introspect a cached access_token that is not (about to be) expired,
|
||||||
|
// introspecting an expired token makes Keycloak log INTROSPECT_TOKEN_ERROR
|
||||||
|
$token_not_expired = false;
|
||||||
|
if (!empty($iam_settings['access_token'])) {
|
||||||
|
$token_parts = explode('.', $iam_settings['access_token']);
|
||||||
|
if (count($token_parts) === 3) {
|
||||||
|
$token_payload = json_decode(base64_decode(strtr($token_parts[1], '-_', '+/')), true);
|
||||||
|
$token_not_expired = is_array($token_payload) && isset($token_payload['exp']) && intval($token_payload['exp']) > time() + 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($token_not_expired) {
|
||||||
// check if access_token is valid
|
// check if access_token is valid
|
||||||
$url = "{$iam_settings['server_url']}/realms/{$iam_settings['realm']}/protocol/openid-connect/token/introspect";
|
$url = "{$iam_settings['server_url']}/realms/{$iam_settings['realm']}/protocol/openid-connect/token/introspect";
|
||||||
$req = http_build_query(array(
|
$req = http_build_query(array(
|
||||||
@@ -3157,6 +3179,8 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
|||||||
':key' => 'access_token',
|
':key' => 'access_token',
|
||||||
':value' => $res['access_token']
|
':value' => $res['access_token']
|
||||||
));
|
));
|
||||||
|
// keep the cached token in sync, so subsequent calls in the same run reuse it
|
||||||
|
$iam_settings['access_token'] = $res['access_token'];
|
||||||
return $res['access_token'];
|
return $res['access_token'];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2432,7 +2432,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
if ($address != $is_now['address']) {
|
if ($address != $is_now['address']) {
|
||||||
// Send-as grants of groups follow the address
|
// Send-as grants of groups follow the address
|
||||||
\Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->api->addressRenamed($is_now['address'], $address);
|
\Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->api->addressRenamed($is_now['address'], $address);
|
||||||
\Mailcow\MailPolicy\MailPolicyModule::fromGlobals()->api->addressRenamed($is_now['address'], $address);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@@ -2525,6 +2524,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
':domain' => $domain,
|
':domain' => $domain,
|
||||||
':tag_name' => $tag,
|
':tag_name' => $tag,
|
||||||
));
|
));
|
||||||
|
$stmt = $pdo->prepare("UPDATE `domain` SET `modified` = NOW() WHERE `domain` = :domain");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':domain' => $domain,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@@ -2693,6 +2696,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
':domain' => $domain,
|
':domain' => $domain,
|
||||||
':tag_name' => $tag,
|
':tag_name' => $tag,
|
||||||
));
|
));
|
||||||
|
$stmt = $pdo->prepare("UPDATE `domain` SET `modified` = NOW() WHERE `domain` = :domain");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':domain' => $domain,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@@ -3171,6 +3178,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
':username' => $username,
|
':username' => $username,
|
||||||
':tag_name' => $tag,
|
':tag_name' => $tag,
|
||||||
));
|
));
|
||||||
|
$stmt = $pdo->prepare("UPDATE `mailbox` SET `modified` = NOW() WHERE `username` = :username");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':username' => $username,
|
||||||
|
));
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3296,9 +3307,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
'mailbox_group_imap_grant' => ['owner'],
|
'mailbox_group_imap_grant' => ['owner'],
|
||||||
'mailbox_group_sogo_grant' => ['owner'],
|
'mailbox_group_sogo_grant' => ['owner'],
|
||||||
'mailbox_group_sogo_acl' => ['c_uid'],
|
'mailbox_group_sogo_acl' => ['c_uid'],
|
||||||
'mailbox_group_subscription' => ['username', 'owner'],
|
'mailbox_group_subscription' => ['username', 'owner']
|
||||||
'mail_policy_member' => ['username'],
|
|
||||||
'retention_rule_member' => ['username']
|
|
||||||
];
|
];
|
||||||
foreach ($tables as $table => $columns) {
|
foreach ($tables as $table => $columns) {
|
||||||
foreach ($columns as $column) {
|
foreach ($columns as $column) {
|
||||||
@@ -3314,8 +3323,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->sources->onAccountRenamed($old_username, $new_username);
|
\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->sources->onAccountRenamed($old_username, $new_username);
|
||||||
// Group memberships and send-as grants follow, group aliases are rebuilt
|
// Group memberships and send-as grants follow, group aliases are rebuilt
|
||||||
\Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->hooks->onMailboxRenamed($old_username, $new_username);
|
\Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->hooks->onMailboxRenamed($old_username, $new_username);
|
||||||
// Address entries of mail policies follow
|
|
||||||
\Mailcow\MailPolicy\MailPolicyModule::fromGlobals()->api->addressRenamed($old_username, $new_username);
|
|
||||||
|
|
||||||
// Update c_uid, c_name and mail in _sogo_static_view table
|
// Update c_uid, c_name and mail in _sogo_static_view table
|
||||||
$pdo->prepare("UPDATE _sogo_static_view SET c_uid = :new_username, c_name = :new_username2, mail = :new_username3 WHERE c_uid = :old_username")
|
$pdo->prepare("UPDATE _sogo_static_view SET c_uid = :new_username, c_name = :new_username2, mail = :new_username3 WHERE c_uid = :old_username")
|
||||||
@@ -3993,6 +4000,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)'));
|
$stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)'));
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
while ($row = array_shift($rows)) {
|
while ($row = array_shift($rows)) {
|
||||||
|
if (!hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['address'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Keep old array for backward compatibility
|
// Keep old array for backward compatibility
|
||||||
$data['fixed_sender_aliases'][] = $row['address'];
|
$data['fixed_sender_aliases'][] = $row['address'];
|
||||||
// Split into allowed/blocked for proper display
|
// Split into allowed/blocked for proper display
|
||||||
@@ -4928,6 +4938,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
else if ($SaslLogs['service'] == 'SSO') {
|
else if ($SaslLogs['service'] == 'SSO') {
|
||||||
$last_sso_login = strtotime($SaslLogs['datetime']);
|
$last_sso_login = strtotime($SaslLogs['datetime']);
|
||||||
}
|
}
|
||||||
|
else if ($SaslLogs['service'] == 'SOGO') {
|
||||||
|
$last_sogo_login = strtotime($SaslLogs['datetime']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!isset($last_imap_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
|
if (!isset($last_imap_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
|
||||||
$last_imap_login = 0;
|
$last_imap_login = 0;
|
||||||
@@ -4941,10 +4954,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
if (!isset($last_sso_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
|
if (!isset($last_sso_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
|
||||||
$last_sso_login = 0;
|
$last_sso_login = 0;
|
||||||
}
|
}
|
||||||
|
if (!isset($last_sogo_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
|
||||||
|
$last_sogo_login = 0;
|
||||||
|
}
|
||||||
$mailboxdata['last_imap_login'] = $last_imap_login;
|
$mailboxdata['last_imap_login'] = $last_imap_login;
|
||||||
$mailboxdata['last_smtp_login'] = $last_smtp_login;
|
$mailboxdata['last_smtp_login'] = $last_smtp_login;
|
||||||
$mailboxdata['last_pop3_login'] = $last_pop3_login;
|
$mailboxdata['last_pop3_login'] = $last_pop3_login;
|
||||||
$mailboxdata['last_sso_login'] = $last_sso_login;
|
$mailboxdata['last_sso_login'] = $last_sso_login;
|
||||||
|
$mailboxdata['last_sogo_login'] = $last_sogo_login;
|
||||||
|
|
||||||
if (!isset($_extra) || $_extra != 'reduced') {
|
if (!isset($_extra) || $_extra != 'reduced') {
|
||||||
$rl = ratelimit('get', 'mailbox', $_data);
|
$rl = ratelimit('get', 'mailbox', $_data);
|
||||||
@@ -4982,7 +4999,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$stmt->execute(array(':username' => $_data));
|
$stmt->execute(array(':username' => $_data));
|
||||||
$sender_acl_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$sender_acl_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
foreach ($sender_acl_rows as $sender_acl_row) {
|
foreach ($sender_acl_rows as $sender_acl_row) {
|
||||||
$mailboxdata['sender_acl'][] = $sender_acl_row['send_as'];
|
$send_as = $sender_acl_row['send_as'];
|
||||||
|
if ($_SESSION['mailcow_cc_role'] == 'domainadmin' && $send_as != '*') {
|
||||||
|
$in_scope = (substr($send_as, 0, 1) == '@')
|
||||||
|
? hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], substr($send_as, 1))
|
||||||
|
: hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $send_as);
|
||||||
|
if (!$in_scope) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$mailboxdata['sender_acl'][] = $send_as;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("SELECT `tag_name`
|
$stmt = $pdo->prepare("SELECT `tag_name`
|
||||||
@@ -5353,7 +5379,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
':domain' => $domain,
|
':domain' => $domain,
|
||||||
));
|
));
|
||||||
\Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->api->domainDeleted($domain);
|
\Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->api->domainDeleted($domain);
|
||||||
\Mailcow\Retention\RetentionModule::fromGlobals()->api->domainDeleted($domain);
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM `imapsync_source_domain` WHERE `domain` = :domain");
|
$stmt = $pdo->prepare("DELETE FROM `imapsync_source_domain` WHERE `domain` = :domain");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':domain' => $domain,
|
':domain' => $domain,
|
||||||
@@ -5922,6 +5947,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
':domain' => $domain,
|
':domain' => $domain,
|
||||||
':tag_name' => $tag,
|
':tag_name' => $tag,
|
||||||
));
|
));
|
||||||
|
if ($stmt->rowCount() > 0) {
|
||||||
|
$stmt = $pdo->prepare("UPDATE `domain` SET `modified` = NOW() WHERE `domain` = :domain");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':domain' => $domain,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5974,6 +6005,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
':username' => $username,
|
':username' => $username,
|
||||||
':tag_name' => $tag,
|
':tag_name' => $tag,
|
||||||
));
|
));
|
||||||
|
if ($stmt->rowCount() > 0) {
|
||||||
|
$stmt = $pdo->prepare("UPDATE `mailbox` SET `modified` = NOW() WHERE `username` = :username");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':username' => $username,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -491,257 +491,6 @@ function init_db_schema()
|
|||||||
),
|
),
|
||||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||||
),
|
),
|
||||||
// Mail policies: whom the assigned mailboxes and groups may send to and receive from (Mailcow\MailPolicy)
|
|
||||||
"mail_policy" => array(
|
|
||||||
"cols" => array(
|
|
||||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
|
||||||
"domain" => "VARCHAR(255) NOT NULL",
|
|
||||||
"name" => "VARCHAR(100) NOT NULL",
|
|
||||||
"description" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
|
||||||
// all, internal or list; the entries are JSON lists of addresses, domains and group:<id>
|
|
||||||
"send_mode" => "VARCHAR(16) NOT NULL DEFAULT 'all'",
|
|
||||||
"send_allow" => "MEDIUMTEXT NOT NULL",
|
|
||||||
"send_block" => "MEDIUMTEXT NOT NULL",
|
|
||||||
"receive_mode" => "VARCHAR(16) NOT NULL DEFAULT 'all'",
|
|
||||||
"receive_allow" => "MEDIUMTEXT NOT NULL",
|
|
||||||
"receive_block" => "MEDIUMTEXT NOT NULL",
|
|
||||||
"reject_text" => "VARCHAR(255) NOT NULL 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(
|
|
||||||
"domain_name" => array("domain", "name")
|
|
||||||
),
|
|
||||||
"fkey" => array(
|
|
||||||
"fk_mail_policy_domain" => array(
|
|
||||||
"col" => "domain",
|
|
||||||
"ref" => "domain.domain",
|
|
||||||
"delete" => "CASCADE",
|
|
||||||
"update" => "NO ACTION"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
|
||||||
),
|
|
||||||
// a mailbox or a group (its members and its address) a policy is assigned to
|
|
||||||
"mail_policy_member" => array(
|
|
||||||
"cols" => array(
|
|
||||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
|
||||||
"policy_id" => "INT NOT NULL",
|
|
||||||
"username" => "VARCHAR(255) NULL DEFAULT NULL",
|
|
||||||
"group_id" => "INT NULL DEFAULT NULL"
|
|
||||||
),
|
|
||||||
"keys" => array(
|
|
||||||
"primary" => array(
|
|
||||||
"" => array("id")
|
|
||||||
),
|
|
||||||
"unique" => array(
|
|
||||||
"policy_mailbox" => array("policy_id", "username"),
|
|
||||||
"policy_group" => array("policy_id", "group_id")
|
|
||||||
),
|
|
||||||
"key" => array(
|
|
||||||
"username" => array("username"),
|
|
||||||
"group_id" => array("group_id")
|
|
||||||
),
|
|
||||||
"fkey" => array(
|
|
||||||
"fk_mail_policy_member_policy" => array(
|
|
||||||
"col" => "policy_id",
|
|
||||||
"ref" => "mail_policy.id",
|
|
||||||
"delete" => "CASCADE",
|
|
||||||
"update" => "NO ACTION"
|
|
||||||
),
|
|
||||||
"fk_mail_policy_member_mailbox" => array(
|
|
||||||
"col" => "username",
|
|
||||||
"ref" => "mailbox.username",
|
|
||||||
"delete" => "CASCADE",
|
|
||||||
"update" => "NO ACTION"
|
|
||||||
),
|
|
||||||
"fk_mail_policy_member_group" => array(
|
|
||||||
"col" => "group_id",
|
|
||||||
"ref" => "mailbox_group.id",
|
|
||||||
"delete" => "CASCADE",
|
|
||||||
"update" => "NO ACTION"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
|
||||||
),
|
|
||||||
"retention_rule" => array(
|
|
||||||
"cols" => array(
|
|
||||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
|
||||||
"domain" => "VARCHAR(255) NOT NULL",
|
|
||||||
"name" => "VARCHAR(100) NOT NULL",
|
|
||||||
"description" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
|
||||||
// JSON list: trash, junk, sent, drafts, archive, inbox, all, folder:<name>
|
|
||||||
"folders" => "TEXT NOT NULL",
|
|
||||||
"days" => "INT NOT NULL DEFAULT '30'",
|
|
||||||
"keep_flagged" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
|
||||||
// simulate (count only) or delete
|
|
||||||
"mode" => "VARCHAR(16) NOT NULL DEFAULT 'simulate'",
|
|
||||||
"all_mailboxes" => "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("id")
|
|
||||||
),
|
|
||||||
"unique" => array(
|
|
||||||
"domain_name" => array("domain", "name")
|
|
||||||
),
|
|
||||||
"fkey" => array(
|
|
||||||
"fk_retention_rule_domain" => array(
|
|
||||||
"col" => "domain",
|
|
||||||
"ref" => "domain.domain",
|
|
||||||
"delete" => "CASCADE",
|
|
||||||
"update" => "NO ACTION"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
|
||||||
),
|
|
||||||
// a mailbox or a group (its members of the rule's domain) a retention rule is assigned to
|
|
||||||
"retention_rule_member" => array(
|
|
||||||
"cols" => array(
|
|
||||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
|
||||||
"rule_id" => "INT NOT NULL",
|
|
||||||
"username" => "VARCHAR(255) NULL DEFAULT NULL",
|
|
||||||
"group_id" => "INT NULL DEFAULT NULL"
|
|
||||||
),
|
|
||||||
"keys" => array(
|
|
||||||
"primary" => array(
|
|
||||||
"" => array("id")
|
|
||||||
),
|
|
||||||
"unique" => array(
|
|
||||||
"rule_mailbox" => array("rule_id", "username"),
|
|
||||||
"rule_group" => array("rule_id", "group_id")
|
|
||||||
),
|
|
||||||
"key" => array(
|
|
||||||
"username" => array("username"),
|
|
||||||
"group_id" => array("group_id")
|
|
||||||
),
|
|
||||||
"fkey" => array(
|
|
||||||
"fk_retention_rule_member_rule" => array(
|
|
||||||
"col" => "rule_id",
|
|
||||||
"ref" => "retention_rule.id",
|
|
||||||
"delete" => "CASCADE",
|
|
||||||
"update" => "NO ACTION"
|
|
||||||
),
|
|
||||||
"fk_retention_rule_member_mailbox" => array(
|
|
||||||
"col" => "username",
|
|
||||||
"ref" => "mailbox.username",
|
|
||||||
"delete" => "CASCADE",
|
|
||||||
"update" => "NO ACTION"
|
|
||||||
),
|
|
||||||
"fk_retention_rule_member_group" => array(
|
|
||||||
"col" => "group_id",
|
|
||||||
"ref" => "mailbox_group.id",
|
|
||||||
"delete" => "CASCADE",
|
|
||||||
"update" => "NO ACTION"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
|
||||||
),
|
|
||||||
// results of the nightly run: per mailbox and rule with found mail or an error (folder: the folder names), per
|
|
||||||
// rule a summary (username '') and the whole run (username '', rule_id NULL); kept 90 days, of simulating
|
|
||||||
// rules only the last run
|
|
||||||
"retention_log" => array(
|
|
||||||
"cols" => array(
|
|
||||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
|
||||||
"domain" => "VARCHAR(255) NOT NULL",
|
|
||||||
"username" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
|
||||||
"rule_id" => "INT NULL DEFAULT NULL",
|
|
||||||
"rule_name" => "VARCHAR(100) NOT NULL DEFAULT ''",
|
|
||||||
"folder" => "TEXT NULL DEFAULT NULL",
|
|
||||||
"days" => "INT NOT NULL DEFAULT '0'",
|
|
||||||
"mailboxes" => "INT NOT NULL DEFAULT '0'",
|
|
||||||
"matched" => "INT NOT NULL DEFAULT '0'",
|
|
||||||
"deleted" => "INT NOT NULL DEFAULT '0'",
|
|
||||||
"errors" => "INT NOT NULL DEFAULT '0'",
|
|
||||||
"simulated" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
|
||||||
"error" => "TEXT NULL DEFAULT NULL",
|
|
||||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)"
|
|
||||||
),
|
|
||||||
"keys" => array(
|
|
||||||
"primary" => array(
|
|
||||||
"" => array("id")
|
|
||||||
),
|
|
||||||
"key" => array(
|
|
||||||
"domain_created" => array("domain", "created"),
|
|
||||||
"rule_created" => array("rule_id", "username", "created"),
|
|
||||||
"created" => array("created")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
|
||||||
),
|
|
||||||
"backup_setting" => array(
|
|
||||||
"cols" => array(
|
|
||||||
"name" => "VARCHAR(64) NOT NULL",
|
|
||||||
"value" => "TEXT NOT NULL",
|
|
||||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
|
|
||||||
),
|
|
||||||
"keys" => array(
|
|
||||||
"primary" => array(
|
|
||||||
"" => array("name")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
|
||||||
),
|
|
||||||
"backup_job" => array(
|
|
||||||
"cols" => array(
|
|
||||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
|
||||||
"type" => "ENUM('backup', 'check', 'prune', 'init', 'restore') NOT NULL",
|
|
||||||
"state" => "ENUM('queued', 'running', 'done', 'failed', 'cancelled') NOT NULL DEFAULT 'queued'",
|
|
||||||
"step" => "VARCHAR(32) NULL DEFAULT NULL",
|
|
||||||
"params" => "MEDIUMTEXT NULL DEFAULT NULL",
|
|
||||||
"result" => "MEDIUMTEXT NULL DEFAULT NULL",
|
|
||||||
"log" => "MEDIUMTEXT NULL DEFAULT NULL",
|
|
||||||
"error" => "TEXT NULL DEFAULT NULL",
|
|
||||||
"domain" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
|
||||||
"requested_by" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
|
||||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
|
||||||
"started" => "DATETIME NULL DEFAULT NULL",
|
|
||||||
"finished" => "DATETIME NULL DEFAULT NULL"
|
|
||||||
),
|
|
||||||
"keys" => array(
|
|
||||||
"primary" => array(
|
|
||||||
"" => array("id")
|
|
||||||
),
|
|
||||||
"key" => array(
|
|
||||||
"state_type" => array("state", "type"),
|
|
||||||
"domain_created" => array("domain", "created")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
|
||||||
),
|
|
||||||
"backup_snapshot" => array(
|
|
||||||
"cols" => array(
|
|
||||||
"id" => "VARCHAR(64) NOT NULL",
|
|
||||||
"short_id" => "VARCHAR(16) NOT NULL",
|
|
||||||
"time" => "DATETIME NOT NULL",
|
|
||||||
"hostname" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
|
||||||
"tags" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
|
||||||
"summary" => "TEXT NULL DEFAULT NULL",
|
|
||||||
"domains" => "MEDIUMTEXT NULL DEFAULT NULL",
|
|
||||||
"content" => "MEDIUMBLOB NULL DEFAULT NULL"
|
|
||||||
),
|
|
||||||
"keys" => array(
|
|
||||||
"primary" => array(
|
|
||||||
"" => array("id")
|
|
||||||
),
|
|
||||||
"key" => array(
|
|
||||||
"time" => array("time")
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
|
||||||
),
|
|
||||||
"mailbox_group_imap_grant" => array(
|
"mailbox_group_imap_grant" => array(
|
||||||
"cols" => array(
|
"cols" => array(
|
||||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||||
@@ -954,8 +703,7 @@ function init_db_schema()
|
|||||||
"group_id" => array("group_id")
|
"group_id" => array("group_id")
|
||||||
),
|
),
|
||||||
"key" => array(
|
"key" => array(
|
||||||
"domain" => array("domain"),
|
"domain" => array("domain")
|
||||||
"internal" => array("internal", "active")
|
|
||||||
),
|
),
|
||||||
"fkey" => array(
|
"fkey" => array(
|
||||||
"fk_alias_mailbox_group" => array(
|
"fk_alias_mailbox_group" => array(
|
||||||
@@ -1305,9 +1053,7 @@ function init_db_schema()
|
|||||||
"domain_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
"domain_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||||
"domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
"domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||||
"alias_external_goto" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
"alias_external_goto" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||||
"mailbox_groups" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
"mailbox_groups" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||||
"mail_policies" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
|
||||||
"retention" => "TINYINT(1) NOT NULL DEFAULT '0'"
|
|
||||||
),
|
),
|
||||||
"keys" => array(
|
"keys" => array(
|
||||||
"primary" => array(
|
"primary" => array(
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup;
|
|
||||||
|
|
||||||
use Mailcow\Backup\Controller\BackupApiController;
|
|
||||||
use Mailcow\Backup\Legacy\LegacyDomain;
|
|
||||||
use Mailcow\Backup\Legacy\NoTemplates;
|
|
||||||
use Mailcow\Backup\Model\Settings;
|
|
||||||
use Mailcow\Backup\Repository\ExportRepository;
|
|
||||||
use Mailcow\Backup\Repository\JobRepository;
|
|
||||||
use Mailcow\Backup\Repository\RestoreRepository;
|
|
||||||
use Mailcow\Backup\Repository\SettingsRepository;
|
|
||||||
use Mailcow\Backup\Repository\SnapshotRepository;
|
|
||||||
use Mailcow\Backup\Service\DomainRestoreService;
|
|
||||||
use Mailcow\Backup\Service\ExportService;
|
|
||||||
use Mailcow\Backup\Service\GroupMembers;
|
|
||||||
use Mailcow\Backup\Service\JobService;
|
|
||||||
use Mailcow\Backup\Service\RestoreService;
|
|
||||||
use Mailcow\Backup\Service\SettingsService;
|
|
||||||
use Mailcow\Core\ApiResponse;
|
|
||||||
use Mailcow\Core\Context;
|
|
||||||
use Mailcow\Dovecot\DovecotModule;
|
|
||||||
use Mailcow\MailboxGroup\Repository\GroupRepository;
|
|
||||||
use Mailcow\MailboxGroup\Repository\LookupRepository;
|
|
||||||
use Mailcow\MailboxGroup\Service\ChangeEffects;
|
|
||||||
use Mailcow\MailboxGroup\Service\SharingService;
|
|
||||||
use Mailcow\MailboxGroup\Service\TemplateService;
|
|
||||||
use Mailcow\Sogo\SogoModule;
|
|
||||||
use PDO;
|
|
||||||
use Redis;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composition root of the backup with restic: settings, jobs of the runners, snapshots, the export of a domain and the
|
|
||||||
* restore of mailboxes and domains. The runner in the restic container reaches the export and the restore actions
|
|
||||||
* through data/conf/rspamd/dynmaps/backup.php.
|
|
||||||
*
|
|
||||||
* process_get_return(BackupModule::fromGlobals()->api->getSettings());
|
|
||||||
* echo json_encode(BackupModule::create($pdo, Context::system(), $config, $redis)->api->export($domain, $page)); // backup.php
|
|
||||||
*
|
|
||||||
* $config: timezone (TZ of mailcow, for the schedule), alias_disable_external_domains ($ALIAS_DISABLE_EXTERNAL_DOMAINS),
|
|
||||||
* demo_mode ($DEMO_MODE: the API changes nothing).
|
|
||||||
* $redis: for DKIM keys, rate limits and the domain map (export and restore).
|
|
||||||
*/
|
|
||||||
class BackupModule
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public readonly SettingsService $settings,
|
|
||||||
public readonly JobService $jobs,
|
|
||||||
public readonly ExportService $export,
|
|
||||||
public readonly RestoreService $restore,
|
|
||||||
public readonly DomainRestoreService $domainRestore,
|
|
||||||
public readonly BackupApiController $api
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function create(PDO $pdo, Context $context, array $config = [], ?Redis $redis = null): self
|
|
||||||
{
|
|
||||||
$jobRepo = new JobRepository($pdo);
|
|
||||||
$restoreRepo = new RestoreRepository($pdo);
|
|
||||||
$legacy = new LegacyDomain($redis);
|
|
||||||
$settings = new SettingsService(new SettingsRepository($pdo), $context);
|
|
||||||
$jobs = new JobService($jobRepo, new SnapshotRepository($pdo), $settings, $context, (string)($config['timezone'] ?? 'UTC'));
|
|
||||||
$export = new ExportService(new ExportRepository($pdo), $legacy);
|
|
||||||
$sogo = SogoModule::create()->sogo;
|
|
||||||
$dovecot = DovecotModule::create()->dovecot;
|
|
||||||
// the group module without group templates: a restored mailbox keeps the settings of the backup
|
|
||||||
$groupRepo = new GroupRepository($pdo);
|
|
||||||
$lookup = new LookupRepository($pdo);
|
|
||||||
$templates = new TemplateService($lookup, new NoTemplates());
|
|
||||||
$effects = new ChangeEffects($templates, $sogo, $groupRepo, $dovecot);
|
|
||||||
$groups = new GroupMembers($groupRepo, $lookup, $templates, $effects, new SharingService($groupRepo, $dovecot, $sogo, $effects));
|
|
||||||
$restore = new RestoreService($restoreRepo, $jobRepo, $groups, $sogo, $dovecot, $legacy, (bool)($config['alias_disable_external_domains'] ?? false));
|
|
||||||
$domainRestore = new DomainRestoreService($restoreRepo, $jobRepo, $groups, $legacy);
|
|
||||||
return new self($settings, $jobs, $export, $restore, $domainRestore,
|
|
||||||
new BackupApiController($settings, $jobs, $export, $restore, $domainRestore, new ApiResponse(Settings::SECRETS),
|
|
||||||
(bool)($config['demo_mode'] ?? false)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Legacy entry point for procedural callers: reads $pdo, $redis, $_SESSION, TZ, $ALIAS_DISABLE_EXTERNAL_DOMAINS and $DEMO_MODE */
|
|
||||||
public static function fromGlobals(?Context $context = null): self
|
|
||||||
{
|
|
||||||
return self::create($GLOBALS['pdo'], $context ?? Context::fromSession($_SESSION ?? []), [
|
|
||||||
'timezone' => getenv('TZ') ?: 'UTC',
|
|
||||||
'alias_disable_external_domains' => ($GLOBALS['ALIAS_DISABLE_EXTERNAL_DOMAINS'] ?? false) === true,
|
|
||||||
'demo_mode' => ($GLOBALS['DEMO_MODE'] ?? false) === true,
|
|
||||||
],($GLOBALS['redis'] ?? null) instanceof Redis ? $GLOBALS['redis'] : null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Controller;
|
|
||||||
|
|
||||||
use Mailcow\Backup\Model\Job;
|
|
||||||
use Mailcow\Backup\Service\DomainRestoreService;
|
|
||||||
use Mailcow\Backup\Service\ExportService;
|
|
||||||
use Mailcow\Backup\Service\JobService;
|
|
||||||
use Mailcow\Backup\Service\RestoreService;
|
|
||||||
use Mailcow\Backup\Service\SettingsService;
|
|
||||||
use Mailcow\Core\AccessDenied;
|
|
||||||
use Mailcow\Core\ApiResponse;
|
|
||||||
use Mailcow\Core\UserError;
|
|
||||||
use PDOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP API adapter for json_api.php (routes backup-settings, backup-status, backup-snapshots, backup-snapshot-content,
|
|
||||||
* backup-jobs, backup-job, backup-restore) and the runner's endpoint data/conf/rspamd/dynmaps/backup.php (domains,
|
|
||||||
* export pages, restore actions). Messages are logged as ['backup', action, object, data].
|
|
||||||
*/
|
|
||||||
class BackupApiController
|
|
||||||
{
|
|
||||||
public function __construct(private SettingsService $settings, private JobService $jobs, private ExportService $export,
|
|
||||||
private RestoreService $restore, private DomainRestoreService $domainRestore, private ApiResponse $response,
|
|
||||||
private bool $demoMode = false)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET backup-settings: secrets as <key>_set; {} for non-admins */
|
|
||||||
public function getSettings(): array
|
|
||||||
{
|
|
||||||
return $this->settings->get()?->toArray() ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** POST edit/backup-settings */
|
|
||||||
public function editSettings(array $attr): bool
|
|
||||||
{
|
|
||||||
return $this->handle('edit', 'settings', $attr, function (array $log) use ($attr): bool {
|
|
||||||
$this->settings->save($attr);
|
|
||||||
return $this->response->success('backup_settings_saved', $log);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET backup-status */
|
|
||||||
public function getStatus(): array
|
|
||||||
{
|
|
||||||
return $this->jobs->status();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET backup-snapshots */
|
|
||||||
public function getSnapshots(): array
|
|
||||||
{
|
|
||||||
return $this->jobs->snapshots();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET backup-snapshot-content/<snapshot>[/<domain>]: domains of a snapshot, or the mailboxes of one, with their state now */
|
|
||||||
public function getSnapshotContent(mixed $snapshot, mixed $domain = null): array
|
|
||||||
{
|
|
||||||
return ($domain === null || $domain === '') ? $this->jobs->content($snapshot) : $this->jobs->domainContent($snapshot, $domain);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET backup-snapshot-search/<snapshot>/<term>: domains and mailboxes of a snapshot matching the term */
|
|
||||||
public function getSnapshotSearch(mixed $snapshot, mixed $term): array
|
|
||||||
{
|
|
||||||
return $this->jobs->search($snapshot, $term);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET backup-jobs[/<count>[/<type>]]: newest first, without logs; count 0 = default */
|
|
||||||
public function getJobs(int $limit = 0, mixed $type = null): array
|
|
||||||
{
|
|
||||||
return array_map(fn(Job $job): array => $this->jobs->view($job), $this->jobs->list($limit, $type));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET backup-job/<id>: with its log */
|
|
||||||
public function getJob(mixed $id): array
|
|
||||||
{
|
|
||||||
$id = $this->toId($id);
|
|
||||||
$job = ($id === null) ? null : $this->jobs->get($id);
|
|
||||||
return ($job === null) ? [] : $this->jobs->view($job, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** POST add/backup-job: {type: backup|check|prune|init} */
|
|
||||||
public function addJob(array $attr): bool
|
|
||||||
{
|
|
||||||
return $this->handle('add', 'job', $attr, function (array $log) use ($attr): bool {
|
|
||||||
$job = $this->jobs->create($attr);
|
|
||||||
return $this->response->success(['backup_job_added', $job->type, $job->id], $log);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** POST add/backup-restore: {snapshot, scope, mailboxes | domain, components, mail_target} */
|
|
||||||
public function addRestore(array $attr): bool
|
|
||||||
{
|
|
||||||
return $this->handle('add', 'restore', $attr, function (array $log) use ($attr): bool {
|
|
||||||
$job = $this->jobs->restore($attr);
|
|
||||||
return $this->response->success(['backup_restore_added', $job->id, count($job->mailboxes())], $log);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** POST delete/backup-job: cancels queued and running jobs, one message per id */
|
|
||||||
public function deleteJobs(array $items): bool
|
|
||||||
{
|
|
||||||
return $this->handle('delete', 'job', ['id' => $items], function (array $log) use ($items): bool {
|
|
||||||
foreach ($items as $id) {
|
|
||||||
try {
|
|
||||||
$job = $this->jobs->cancel($this->requireId($id));
|
|
||||||
$this->response->success(['backup_job_cancelled', $job->id], $log);
|
|
||||||
} catch (UserError $e) {
|
|
||||||
$this->response->error($e, $log);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** backup.php ?domains=1 */
|
|
||||||
public function domains(): array
|
|
||||||
{
|
|
||||||
return ['domains' => $this->export->domains()];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** backup.php ?export=<domain>[&after=<username>] @throws UserError */
|
|
||||||
public function export(string $domain, ?string $after = null): array
|
|
||||||
{
|
|
||||||
return $this->export->export($domain, $after);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* backup.php POST of the runner, {"result": ...} of
|
|
||||||
* {"action": "restore_domain", "job": id, "data": <domain_data>}: creates a deleted domain
|
|
||||||
* {"action": "restore_aliases", "job": id, "aliases": [...]}: a part of the domain's aliases (scope domain)
|
|
||||||
* {"action": "restore", "job": id, "username": u, "data": <mailbox section>}: one mailbox
|
|
||||||
*
|
|
||||||
* @throws UserError
|
|
||||||
*/
|
|
||||||
public function post(array $body): array
|
|
||||||
{
|
|
||||||
return ['result' => match ($body['action'] ?? null) {
|
|
||||||
'restore' => $this->restore->merge($body['job'] ?? null, $body['username'] ?? null, $body['data'] ?? null),
|
|
||||||
'restore_aliases' => $this->restore->mergeAliases($body['job'] ?? null, $body['aliases'] ?? null),
|
|
||||||
'restore_domain' => $this->domainRestore->restore($body['job'] ?? null, $body['data'] ?? null),
|
|
||||||
default => throw new UserError('backup_action_invalid'),
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs one write call: $work gets the log entry and returns the result for process_*_return().
|
|
||||||
* A UserError becomes a danger message, a database error stops the call with one `mysql_error` message.
|
|
||||||
* In demo mode no write call runs.
|
|
||||||
*/
|
|
||||||
private function handle(string $action, string $object, mixed $data, callable $work): bool
|
|
||||||
{
|
|
||||||
$log = ['backup', $action, $object, $data];
|
|
||||||
try {
|
|
||||||
if ($this->demoMode) {
|
|
||||||
throw new UserError('demo_mode_enabled');
|
|
||||||
}
|
|
||||||
$result = $work($log);
|
|
||||||
} catch (UserError $e) {
|
|
||||||
$result = $this->response->error($e, $log);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
$result = $this->response->danger(['mysql_error', $e->getMessage()], $log);
|
|
||||||
}
|
|
||||||
$this->response->flush();
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Integer id from request input ("12", 12); null for anything else */
|
|
||||||
private function toId(mixed $id): ?int
|
|
||||||
{
|
|
||||||
return (is_numeric($id) && (float)$id == (int)(float)$id) ? (int)(float)$id : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @throws AccessDenied for ids that cannot exist */
|
|
||||||
private function requireId(mixed $id): int
|
|
||||||
{
|
|
||||||
return $this->toId($id) ?? throw new AccessDenied();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Legacy;
|
|
||||||
|
|
||||||
use Redis;
|
|
||||||
use RuntimeException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What the legacy code keeps outside the database for domains and mailboxes: DKIM keys, rate limits and the domain
|
|
||||||
* map in Redis (functions.dkim/ratelimit/mailbox.inc.php), and the SOGo restart through dockerapi. Redis and docker()
|
|
||||||
* sit in protected methods so tests can replace them.
|
|
||||||
*/
|
|
||||||
class LegacyDomain
|
|
||||||
{
|
|
||||||
public function __construct(private ?Redis $redis = null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return ?array{selector: string, pubkey: string, privkey: string} the DKIM key of a domain */
|
|
||||||
public function dkim(string $domain): ?array
|
|
||||||
{
|
|
||||||
$pubkey = $this->redis()->hGet('DKIM_PUB_KEYS', $domain);
|
|
||||||
if (!is_string($pubkey) || $pubkey === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$selector = (string)$this->redis()->hGet('DKIM_SELECTORS', $domain);
|
|
||||||
return ['selector' => $selector, 'pubkey' => $pubkey, 'privkey' => (string)$this->redis()->hGet('DKIM_PRIV_KEYS', $selector . '.' . $domain)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Stores a DKIM key like dkim('import') does */
|
|
||||||
public function addDkim(string $domain, string $selector, string $pubkey, string $privkey): void
|
|
||||||
{
|
|
||||||
$this->redis()->hSet('DKIM_PUB_KEYS', $domain, $pubkey);
|
|
||||||
$this->redis()->hSet('DKIM_SELECTORS', $domain, $selector);
|
|
||||||
$this->redis()->hSet('DKIM_PRIV_KEYS', $selector . '.' . $domain, $privkey);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string[] $objects domains or mailboxes
|
|
||||||
* @return array<string, string> object => rate limit ("10 / 1h") of those with one
|
|
||||||
*/
|
|
||||||
public function rateLimits(array $objects): array
|
|
||||||
{
|
|
||||||
if (empty($objects)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$values = $this->redis()->hMGet('RL_VALUE', array_values($objects));
|
|
||||||
return array_filter(is_array($values) ? $values : [], fn(mixed $value): bool => is_string($value) && $value !== '');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setRateLimit(string $object, string $value): void
|
|
||||||
{
|
|
||||||
$this->redis()->hSet('RL_VALUE', $object, $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The domain is local for rspamd (legacy domain and alias domain add) */
|
|
||||||
public function addDomainMap(string $domain): void
|
|
||||||
{
|
|
||||||
$this->redis()->hSet('DOMAIN_MAP', $domain, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SOGo reads its domains at start: restart it after a domain was added (like the "restart SOGo" option of the form)
|
|
||||||
*
|
|
||||||
* @return ?string null on success, else the error of dockerapi
|
|
||||||
*/
|
|
||||||
public function restartSogo(): ?string
|
|
||||||
{
|
|
||||||
$result = json_decode((string)$this->dockerRestart('sogo-mailcow'), true);
|
|
||||||
return (($result['type'] ?? '') === 'success') ? null : (string)($result['msg'] ?? 'dockerapi did not answer');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function redis(): Redis
|
|
||||||
{
|
|
||||||
return $this->redis ?? throw new RuntimeException('Redis is not connected');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function dockerRestart(string $service): mixed
|
|
||||||
{
|
|
||||||
return docker('post', $service, 'restart');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Legacy;
|
|
||||||
|
|
||||||
use Mailcow\MailboxGroup\Legacy\LegacyMailbox;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The group module's bridge to the mailbox code for restores: a membership restored from a backup never applies the
|
|
||||||
* group's mailbox template (the mailbox gets its settings from the backup).
|
|
||||||
*/
|
|
||||||
class NoTemplates extends LegacyMailbox
|
|
||||||
{
|
|
||||||
public function applyTemplate(string $username, string $name, string $template): ?string
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A job of the backup runners (table `backup_job`): backup, check, prune and init run in the restic container; a
|
|
||||||
* restore runs there (step `files`, then the merge through backup.php) and, for mails, IMAP ACLs and Sieve
|
|
||||||
* scripts, in the dovecot container (step `mails`). Times are unix timestamps.
|
|
||||||
*/
|
|
||||||
class Job
|
|
||||||
{
|
|
||||||
public const TYPES = ['backup', 'check', 'prune', 'init', 'restore'];
|
|
||||||
/** Types an admin starts with add/backup-job */
|
|
||||||
public const MANUAL_TYPES = ['backup', 'check', 'prune', 'init'];
|
|
||||||
public const ACTIVE_STATES = ['queued', 'running'];
|
|
||||||
|
|
||||||
public ?int $id = null;
|
|
||||||
public string $type = 'backup';
|
|
||||||
public string $state = 'queued';
|
|
||||||
public ?string $step = null;
|
|
||||||
/**
|
|
||||||
* restore: snapshot, snapshot_time, scope, domain, mailboxes, components, mail_target, requested_role, create_domain (0|1),
|
|
||||||
* create_mailboxes (missing when requested), acl and allowed_domains (domain admins)
|
|
||||||
*/
|
|
||||||
public array $params = [];
|
|
||||||
public ?array $result = null;
|
|
||||||
/** Only loaded for a single job */
|
|
||||||
public ?string $log = null;
|
|
||||||
public ?string $error = null;
|
|
||||||
/** restore: the domain of its mailboxes, so domain admins see their restores */
|
|
||||||
public string $domain = '';
|
|
||||||
public string $requestedBy = '';
|
|
||||||
public ?int $created = null;
|
|
||||||
public ?int $started = null;
|
|
||||||
public ?int $finished = null;
|
|
||||||
|
|
||||||
public function __construct(string $type = 'backup')
|
|
||||||
{
|
|
||||||
$this->type = $type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Row of `backup_job` with the times as unix timestamps */
|
|
||||||
public static function fromRow(array $row): self
|
|
||||||
{
|
|
||||||
$job = new self((string)$row['type']);
|
|
||||||
$job->id = (int)$row['id'];
|
|
||||||
$job->state = (string)$row['state'];
|
|
||||||
$job->step = isset($row['step']) ? (string)$row['step'] : null;
|
|
||||||
$params = is_string($row['params'] ?? null) ? json_decode($row['params'], true) : null;
|
|
||||||
$job->params = is_array($params) ? $params : [];
|
|
||||||
$result = is_string($row['result'] ?? null) ? json_decode($row['result'], true) : null;
|
|
||||||
$job->result = is_array($result) ? $result : null;
|
|
||||||
$job->log = isset($row['log']) ? (string)$row['log'] : null;
|
|
||||||
$job->error = isset($row['error']) ? (string)$row['error'] : null;
|
|
||||||
$job->domain = (string)($row['domain'] ?? '');
|
|
||||||
$job->requestedBy = (string)($row['requested_by'] ?? '');
|
|
||||||
foreach (['created', 'started', 'finished'] as $time) {
|
|
||||||
$job->$time = isset($row[$time]) ? (int)$row[$time] : null;
|
|
||||||
}
|
|
||||||
return $job;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Column values for a new row */
|
|
||||||
public function toRow(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'type' => $this->type,
|
|
||||||
'state' => $this->state,
|
|
||||||
'step' => $this->step,
|
|
||||||
'params' => json_encode($this->params ?: new \stdClass(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
||||||
'domain' => $this->domain,
|
|
||||||
'requested_by' => $this->requestedBy,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return string[] mailboxes of a restore */
|
|
||||||
public function mailboxes(): array
|
|
||||||
{
|
|
||||||
return array_values(array_filter((array)($this->params['mailboxes'] ?? []), 'is_string'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return string[] components of a restore */
|
|
||||||
public function components(): array
|
|
||||||
{
|
|
||||||
return array_values(array_filter((array)($this->params['components'] ?? []), 'is_string'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return string[] mailboxes of a restore that did not exist when it was requested */
|
|
||||||
public function createMailboxes(): array
|
|
||||||
{
|
|
||||||
return array_values(array_filter((array)($this->params['create_mailboxes'] ?? []), 'is_string'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The restore creates its domain (requested by an admin while the domain did not exist) */
|
|
||||||
public function createsDomain(): bool
|
|
||||||
{
|
|
||||||
return (int)($this->params['create_domain'] ?? 0) === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isActive(): bool
|
|
||||||
{
|
|
||||||
return in_array($this->state, self::ACTIVE_STATES, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Entry of the API; the log only with $withLog (get/backup-job/<id>) */
|
|
||||||
public function toArray(bool $withLog = false): array
|
|
||||||
{
|
|
||||||
$job = [
|
|
||||||
'id' => $this->id,
|
|
||||||
'type' => $this->type,
|
|
||||||
'state' => $this->state,
|
|
||||||
'step' => $this->step,
|
|
||||||
'params' => $this->params ?: new \stdClass(),
|
|
||||||
'result' => $this->result,
|
|
||||||
'error' => $this->error,
|
|
||||||
'domain' => $this->domain,
|
|
||||||
'requested_by' => $this->requestedBy,
|
|
||||||
'created' => $this->created,
|
|
||||||
'started' => $this->started,
|
|
||||||
'finished' => $this->finished,
|
|
||||||
];
|
|
||||||
if ($withLog) {
|
|
||||||
$job['log'] = (string)$this->log;
|
|
||||||
}
|
|
||||||
return $job;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Model;
|
|
||||||
|
|
||||||
use Mailcow\Core\Context;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The merge of one mailbox of a restore job: what the backup holds, who asked for the restore and what the merge
|
|
||||||
* did per component (added, skipped, errors), plus the caches to refresh afterwards.
|
|
||||||
*/
|
|
||||||
class RestoreRun
|
|
||||||
{
|
|
||||||
/** @var array<string, array{added: int, skipped: int, errors: string[]}> */
|
|
||||||
public array $results = [];
|
|
||||||
/** Mailboxes whose SOGo identities (aliases, send-as addresses) changed */
|
|
||||||
public array $sogoUsers = [];
|
|
||||||
/** Mailboxes whose userdb or passdb values changed (Dovecot auth cache) */
|
|
||||||
public array $authUsers = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array $mailbox the mailbox row as it is now
|
|
||||||
* @param array $params params of the restore job (requested_role, acl of a domain admin, scope, ...)
|
|
||||||
* @param array $data the mailbox's section of the domain export in the snapshot
|
|
||||||
* @param string[] $domains domains whose aliases and send-as addresses the restore may change, [] = all (admins)
|
|
||||||
* @param string $requestedBy who requested the restore
|
|
||||||
*/
|
|
||||||
/** The restore created the mailbox (its own alias gets the forwarding of the backup) */
|
|
||||||
public bool $created = false;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
public readonly string $username,
|
|
||||||
public readonly string $domain,
|
|
||||||
public readonly array $mailbox,
|
|
||||||
public readonly array $params,
|
|
||||||
public readonly array $data,
|
|
||||||
public readonly array $domains = [],
|
|
||||||
public readonly string $requestedBy = ''
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The caller the group module checks: an admin, or the domain admin who requested the restore with his ACL of then
|
|
||||||
* and his domains now
|
|
||||||
*/
|
|
||||||
public function requester(): Context
|
|
||||||
{
|
|
||||||
if ($this->byAdmin()) {
|
|
||||||
return new Context('admin', 'backup', [], true);
|
|
||||||
}
|
|
||||||
return new Context('domainadmin', $this->requestedBy, array_map(fn(mixed $flag): string => (string)(int)$flag, (array)($this->params['acl'] ?? [])));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function start(string $component): void
|
|
||||||
{
|
|
||||||
$this->results[$component] = ['added' => 0, 'skipped' => 0, 'errors' => []];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function added(string $component, int $count = 1): void
|
|
||||||
{
|
|
||||||
$this->results[$component]['added'] += $count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function skipped(string $component, int $count = 1): void
|
|
||||||
{
|
|
||||||
$this->results[$component]['skipped'] += $count;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function error(string $component, string $error): void
|
|
||||||
{
|
|
||||||
$this->results[$component]['errors'][] = $error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The part of a component was rolled back: nothing of it counts */
|
|
||||||
public function failed(string $component, string $error): void
|
|
||||||
{
|
|
||||||
$this->start($component);
|
|
||||||
$this->error($component, $error);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The restore was requested by an admin (domain admins are limited by their ACL and the job's domain) */
|
|
||||||
public function byAdmin(): bool
|
|
||||||
{
|
|
||||||
return ($this->params['requested_role'] ?? '') === 'admin';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Admins, or a domain admin with this ACL when he requested the restore */
|
|
||||||
public function may(string $acl): bool
|
|
||||||
{
|
|
||||||
return $this->byAdmin() || (int)($this->params['acl'][$acl] ?? 0) === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mayChangeDomain(string $domain): bool
|
|
||||||
{
|
|
||||||
return $this->domains === [] || in_array(strtolower($domain), $this->domains, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Rows of a list in the backup (sieve_filters, tfa, ...) */
|
|
||||||
public function rows(string $key): array
|
|
||||||
{
|
|
||||||
return array_values(array_filter((array)($this->data[$key] ?? []), 'is_array'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sogo(string ...$usernames): void
|
|
||||||
{
|
|
||||||
array_push($this->sogoUsers, ...$usernames);
|
|
||||||
$this->sogoUsers = array_values(array_unique($this->sogoUsers));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function auth(string ...$usernames): void
|
|
||||||
{
|
|
||||||
array_push($this->authUsers, ...$usernames);
|
|
||||||
$this->authUsers = array_values(array_unique($this->authUsers));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup settings (table `backup_setting`, name/value pairs) over their defaults. Secrets are stored sealed for the
|
|
||||||
* runner (`sealed:v1:<base64>`) and never leave toArray(): the API gets `<key>_set` instead. The state_* values, the
|
|
||||||
* SSH public key and the SFTP host key are written by the runner in the restic container and cannot be set through
|
|
||||||
* the API (PHP writes state_target_changed when the target changes).
|
|
||||||
*/
|
|
||||||
class Settings
|
|
||||||
{
|
|
||||||
public const DEFAULTS = [
|
|
||||||
'enabled' => '0',
|
|
||||||
'target' => 's3',
|
|
||||||
's3_endpoint' => 'https://s3.amazonaws.com',
|
|
||||||
's3_bucket' => '',
|
|
||||||
's3_path' => '',
|
|
||||||
's3_region' => '',
|
|
||||||
's3_access_key' => '',
|
|
||||||
's3_secret_key' => '',
|
|
||||||
's3_path_style' => '0',
|
|
||||||
'sftp_host' => '',
|
|
||||||
'sftp_port' => '22',
|
|
||||||
'sftp_user' => '',
|
|
||||||
'sftp_path' => '',
|
|
||||||
'sftp_public_key' => '',
|
|
||||||
'sftp_host_key' => '',
|
|
||||||
'rest_url' => '',
|
|
||||||
'rest_user' => '',
|
|
||||||
'rest_password' => '',
|
|
||||||
'local_path' => 'mailcow',
|
|
||||||
'password' => '',
|
|
||||||
'password_confirmed' => '0',
|
|
||||||
// JSON list of the scheduled backups: {"days": [weekdays, 0 = Sunday], "time": "HH:MM"}
|
|
||||||
'schedule' => '[{"days":[0,1,2,3,4,5,6],"time":"01:00"}]',
|
|
||||||
'keep_hourly' => '0',
|
|
||||||
'keep_daily' => '7',
|
|
||||||
'keep_weekly' => '4',
|
|
||||||
'keep_monthly' => '12',
|
|
||||||
'keep_yearly' => '0',
|
|
||||||
'prune_weekday' => '0',
|
|
||||||
'max_unused' => '10%',
|
|
||||||
'check_subset' => '30',
|
|
||||||
'include_index' => '1',
|
|
||||||
'include_fts' => '1',
|
|
||||||
'limit_upload' => '0',
|
|
||||||
'notify_email' => '',
|
|
||||||
'state_initialized' => '0',
|
|
||||||
'state_runner_seen' => '',
|
|
||||||
'state_last_backup' => '',
|
|
||||||
'state_last_success' => '',
|
|
||||||
'state_repo_size' => '',
|
|
||||||
'state_repo_stats' => '',
|
|
||||||
'state_schedule_since' => '',
|
|
||||||
'state_schedule' => '',
|
|
||||||
// public key (base64, X25519) the secrets are sealed with; the private key stays in the restic container
|
|
||||||
'state_secrets_pubkey' => '',
|
|
||||||
'state_secrets_error' => '',
|
|
||||||
// JSON {old_target, old_addresses, changed, changed_by, time}: the runner mails the old addresses and clears it
|
|
||||||
'state_target_changed' => '',
|
|
||||||
// RESTIC_BACKUP_MODE of mailcow.conf as the runner sees it
|
|
||||||
'state_backup_mode' => '',
|
|
||||||
];
|
|
||||||
public const SECRETS = ['password', 's3_secret_key', 'rest_password'];
|
|
||||||
/** Written by the runner only */
|
|
||||||
public const RUNNER = ['sftp_public_key', 'sftp_host_key', 'state_initialized', 'state_runner_seen', 'state_last_backup',
|
|
||||||
'state_last_success', 'state_repo_size', 'state_repo_stats', 'state_schedule_since', 'state_schedule', 'state_secrets_pubkey',
|
|
||||||
'state_secrets_error', 'state_target_changed', 'state_backup_mode'];
|
|
||||||
/** Prefix of a sealed secret (sodium_crypto_box_seal with state_secrets_pubkey) */
|
|
||||||
public const SEALED = 'sealed:v1:';
|
|
||||||
public const TARGETS = ['s3', 'sftp', 'rest', 'local'];
|
|
||||||
/** The connection of each target: a change (or another target or repository password) resets state_initialized */
|
|
||||||
public const CONNECTION = [
|
|
||||||
's3' => ['s3_endpoint', 's3_bucket', 's3_path', 's3_region', 's3_access_key', 's3_secret_key', 's3_path_style'],
|
|
||||||
'sftp' => ['sftp_host', 'sftp_port', 'sftp_user', 'sftp_path'],
|
|
||||||
'rest' => ['rest_url', 'rest_user', 'rest_password'],
|
|
||||||
'local' => ['local_path'],
|
|
||||||
];
|
|
||||||
/** Values a target cannot work without (besides the repository password) */
|
|
||||||
public const REQUIRED = [
|
|
||||||
's3' => ['s3_endpoint', 's3_bucket', 's3_access_key', 's3_secret_key'],
|
|
||||||
'sftp' => ['sftp_host', 'sftp_port', 'sftp_user', 'sftp_path'],
|
|
||||||
'rest' => ['rest_url'],
|
|
||||||
'local' => ['local_path'],
|
|
||||||
];
|
|
||||||
/** Returned as numbers by the API ('' = not known yet: null; prune_weekday '' = never stays '') */
|
|
||||||
private const NUMBERS = ['enabled', 's3_path_style', 'sftp_port', 'password_confirmed', 'keep_hourly', 'keep_daily', 'keep_weekly', 'keep_monthly',
|
|
||||||
'keep_yearly', 'prune_weekday', 'check_subset', 'include_index', 'include_fts', 'limit_upload', 'state_initialized',
|
|
||||||
'state_runner_seen', 'state_last_backup', 'state_last_success', 'state_repo_size', 'state_schedule_since'];
|
|
||||||
private const JSON = ['schedule', 'state_repo_stats', 'state_target_changed'];
|
|
||||||
|
|
||||||
/** @param array<string, string> $values every key of DEFAULTS */
|
|
||||||
public function __construct(public array $values = self::DEFAULTS)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Stored values (name => value) over the defaults; unknown names are dropped */
|
|
||||||
public static function fromStored(array $stored): self
|
|
||||||
{
|
|
||||||
$values = self::DEFAULTS;
|
|
||||||
foreach ($stored as $name => $value) {
|
|
||||||
if (array_key_exists($name, $values)) {
|
|
||||||
$values[$name] = (string)$value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new self($values);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function get(string $name): string
|
|
||||||
{
|
|
||||||
return $this->values[$name] ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The target has its required values and the repository password is set */
|
|
||||||
public function configured(): bool
|
|
||||||
{
|
|
||||||
$required = self::REQUIRED[$this->get('target')] ?? null;
|
|
||||||
if ($required === null || $this->get('password') === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
foreach ($required as $name) {
|
|
||||||
if ($this->get($name) === '') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The schedule as [weekday (0 = Sunday), hour, minute] slots, sorted and unique
|
|
||||||
*
|
|
||||||
* @return array<int, array{int, int, int}>
|
|
||||||
*/
|
|
||||||
public function scheduleSlots(): array
|
|
||||||
{
|
|
||||||
$entries = json_decode($this->get('schedule'), true);
|
|
||||||
$slots = [];
|
|
||||||
foreach (is_array($entries) ? $entries : [] as $entry) {
|
|
||||||
if (!is_array($entry) || !preg_match('/^([01]?\d|2[0-3]):([0-5]\d)$/', (string)($entry['time'] ?? ''), $time) || !is_array($entry['days'] ?? null)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
foreach ($entry['days'] as $day) {
|
|
||||||
if (is_int($day) && $day >= 0 && $day <= 6) {
|
|
||||||
$slots[] = $day * 1440 + (int)$time[1] * 60 + (int)$time[2];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$slots = array_unique($slots);
|
|
||||||
sort($slots);
|
|
||||||
return array_map(fn(int $slot): array => [intdiv($slot, 1440), intdiv($slot % 1440, 60), $slot % 60], $slots);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The longest time between two scheduled backups of a week in seconds, null without a schedule */
|
|
||||||
public function scheduleGap(): ?int
|
|
||||||
{
|
|
||||||
$slots = array_map(fn(array $slot): int => $slot[0] * 86400 + $slot[1] * 3600 + $slot[2] * 60, $this->scheduleSlots());
|
|
||||||
if (empty($slots)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$gaps = [$slots[0] + 7 * 86400 - end($slots)];
|
|
||||||
for ($i = 1; $i < count($slots); $i++) {
|
|
||||||
$gaps[] = $slots[$i] - $slots[$i - 1];
|
|
||||||
}
|
|
||||||
return max($gaps);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Entry of get/backup-settings: every key, secrets as <key>_set, plus `configured` */
|
|
||||||
public function toArray(): array
|
|
||||||
{
|
|
||||||
$settings = [];
|
|
||||||
foreach ($this->values as $name => $value) {
|
|
||||||
if (in_array($name, self::SECRETS, true)) {
|
|
||||||
$settings[$name . '_set'] = (int)($value !== '');
|
|
||||||
} elseif (in_array($name, self::JSON, true)) {
|
|
||||||
$decoded = json_decode($value, true);
|
|
||||||
$settings[$name] = is_array($decoded) ? $decoded : null;
|
|
||||||
} elseif (in_array($name, self::NUMBERS, true)) {
|
|
||||||
$settings[$name] = ($value === '') ? (($name === 'prune_weekday') ? '' : null) : (int)$value;
|
|
||||||
} else {
|
|
||||||
$settings[$name] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$settings['configured'] = (int)$this->configured();
|
|
||||||
return $settings;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A snapshot of the repository (table `backup_snapshot`, the runner's cache of `restic snapshots --json`).
|
|
||||||
*/
|
|
||||||
class Snapshot
|
|
||||||
{
|
|
||||||
public string $id = '';
|
|
||||||
public string $shortId = '';
|
|
||||||
/** unix timestamp */
|
|
||||||
public int $time = 0;
|
|
||||||
public string $hostname = '';
|
|
||||||
/** @var string[] */
|
|
||||||
public array $tags = [];
|
|
||||||
/** restic's summary of the backup (data_added, files_new, ...) */
|
|
||||||
public ?array $summary = null;
|
|
||||||
/** @var ?string[] domains of its manifest, null for snapshots not made by mailcow */
|
|
||||||
public ?array $domains = null;
|
|
||||||
/** @var array<string, ?int> domain => its creation time (unix) when the snapshot was made, from the snapshot row */
|
|
||||||
public array $created = [];
|
|
||||||
/**
|
|
||||||
* @var ?array<string, array{created: ?int, mailboxes: array<string, string>}> per domain its creation time (unix, null
|
|
||||||
* when not recorded) and username => name of its mailboxes; null = not known (snapshots this installation did
|
|
||||||
* not make itself, e.g. of another server in the same repository)
|
|
||||||
*/
|
|
||||||
public ?array $content = null;
|
|
||||||
|
|
||||||
/** Row of `backup_snapshot` with the time as unix timestamp */
|
|
||||||
public static function fromRow(array $row): self
|
|
||||||
{
|
|
||||||
$snapshot = new self();
|
|
||||||
$snapshot->id = (string)$row['id'];
|
|
||||||
$snapshot->shortId = (string)$row['short_id'];
|
|
||||||
$snapshot->time = (int)$row['time'];
|
|
||||||
$snapshot->hostname = (string)($row['hostname'] ?? '');
|
|
||||||
$tags = (string)($row['tags'] ?? '');
|
|
||||||
$decoded = json_decode($tags, true);
|
|
||||||
$tags = is_array($decoded) ? $decoded : explode(',', $tags);
|
|
||||||
$snapshot->tags = array_values(array_filter(array_map(fn(mixed $tag): string => is_scalar($tag) ? trim((string)$tag) : '', $tags), 'strlen'));
|
|
||||||
$summary = is_string($row['summary'] ?? null) ? json_decode($row['summary'], true) : null;
|
|
||||||
$snapshot->summary = is_array($summary) ? $summary : null;
|
|
||||||
// {domain: creation time}: lists of snapshots need no content
|
|
||||||
$domains = is_string($row['domains'] ?? null) ? json_decode($row['domains'], true) : null;
|
|
||||||
if (is_array($domains)) {
|
|
||||||
$snapshot->domains = [];
|
|
||||||
foreach ($domains as $domain => $created) {
|
|
||||||
$snapshot->domains[] = (string)$domain;
|
|
||||||
$snapshot->created[strtolower((string)$domain)] = is_numeric($created) ? (int)$created : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (is_string($row['content'] ?? null) && $row['content'] !== '') {
|
|
||||||
$snapshot->content = self::decodeContent($row['content']);
|
|
||||||
}
|
|
||||||
return $snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `content` (gzip JSON) written by the runner: {"domains": {"<domain>": {"created": <unix>, "mailboxes": {"<username>": "<name>"}}}}.
|
|
||||||
* Keys lower case; null when broken.
|
|
||||||
*/
|
|
||||||
public static function decodeContent(string $gzip): ?array
|
|
||||||
{
|
|
||||||
$json = @gzdecode($gzip);
|
|
||||||
$data = is_string($json) ? json_decode($json, true) : null;
|
|
||||||
if (!is_array($data) || !is_array($data['domains'] ?? null)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$content = [];
|
|
||||||
foreach ($data['domains'] as $domain => $entry) {
|
|
||||||
$mailboxes = is_array($entry) ? ($entry['mailboxes'] ?? []) : [];
|
|
||||||
$created = is_numeric($entry['created'] ?? null) ? (int)$entry['created'] : null;
|
|
||||||
$names = [];
|
|
||||||
foreach (is_array($mailboxes) ? $mailboxes : [] as $username => $name) {
|
|
||||||
$names[strtolower((string)$username)] = is_scalar($name) ? (string)$name : '';
|
|
||||||
}
|
|
||||||
$content[strtolower((string)$domain)] = ['created' => $created, 'mailboxes' => $names];
|
|
||||||
}
|
|
||||||
return $content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Domains of the snapshot: those of its content and of its manifest (an export may have failed) */
|
|
||||||
public function domainList(): array
|
|
||||||
{
|
|
||||||
return array_values(array_unique(array_merge(array_map('strval', array_keys($this->content ?? [])), array_map('strtolower', $this->domains ?? []))));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return ?array<string, string> username => name of the domain's mailboxes, null when the content does not list the domain */
|
|
||||||
public function mailboxesOf(string $domain): ?array
|
|
||||||
{
|
|
||||||
return $this->content[strtolower($domain)]['mailboxes'] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Creation time of the domain when the snapshot was made, null when not recorded */
|
|
||||||
public function createdOf(string $domain): ?int
|
|
||||||
{
|
|
||||||
return $this->content[strtolower($domain)]['created'] ?? $this->created[strtolower($domain)] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function hasDomain(string $domain): bool
|
|
||||||
{
|
|
||||||
return in_array(strtolower($domain), $this->domainList(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RESTIC_BACKUP_MODE of the backup (tag mode:<mode>, none for all): all, data (without the mailcow directory) or
|
|
||||||
* config (without mails)
|
|
||||||
*/
|
|
||||||
public function mode(): string
|
|
||||||
{
|
|
||||||
foreach ($this->tags as $tag) {
|
|
||||||
if (str_starts_with($tag, 'mode:')) {
|
|
||||||
return substr($tag, 5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'all';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Snapshots not made by mailcow know neither domains nor mailboxes */
|
|
||||||
public function knowsDomains(): bool
|
|
||||||
{
|
|
||||||
return $this->content !== null || $this->domains !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Entry of the API for admins */
|
|
||||||
public function toArray(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'id' => $this->id,
|
|
||||||
'short_id' => $this->shortId,
|
|
||||||
'time' => $this->time,
|
|
||||||
'hostname' => $this->hostname,
|
|
||||||
'tags' => $this->tags,
|
|
||||||
'summary' => $this->summary,
|
|
||||||
'domains' => $this->domains,
|
|
||||||
'mode' => $this->mode(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Repository;
|
|
||||||
|
|
||||||
use Mailcow\Core\Repository;
|
|
||||||
use PDO;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads what the export of a domain holds: the domain with its settings, its aliases and a page of its mailboxes with
|
|
||||||
* every row that belongs to a mailbox. Rows are read with all their columns (SELECT *).
|
|
||||||
*/
|
|
||||||
class ExportRepository extends Repository
|
|
||||||
{
|
|
||||||
/** @return string[] all domains */
|
|
||||||
public function domains(): array
|
|
||||||
{
|
|
||||||
return $this->query("SELECT `domain` FROM `domain` ORDER BY `domain`")->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function dbVersion(): string
|
|
||||||
{
|
|
||||||
return (string)$this->query("SELECT `version` FROM `versions` WHERE `application` = 'db_schema'")->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array[] rows of a table matching column => value (table and column names from code) */
|
|
||||||
public function rows(string $table, string $column, string $value): array
|
|
||||||
{
|
|
||||||
self::identifier($table);
|
|
||||||
self::identifier($column);
|
|
||||||
return $this->query("SELECT * FROM `$table` WHERE `$column` = :value", ['value' => $value])->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return string[] usernames of the domain's mailboxes */
|
|
||||||
public function usernames(string $domain): array
|
|
||||||
{
|
|
||||||
return $this->query("SELECT `username` FROM `mailbox` WHERE `domain` = :domain", ['domain' => $domain])->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array[] the domain's mailbox rows after $after in username order (keyset paging), at most $limit */
|
|
||||||
public function mailboxesAfter(string $domain, string $after, int $limit): array
|
|
||||||
{
|
|
||||||
return $this->query("SELECT * FROM `mailbox` WHERE `domain` = :domain AND `username` > :after ORDER BY `username` LIMIT " . max(1, $limit),
|
|
||||||
['domain' => $domain, 'after' => $after])->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array[] alias rows of the domain and its alias domains */
|
|
||||||
public function aliases(string $domain): array
|
|
||||||
{
|
|
||||||
return $this->query("SELECT * FROM `alias` WHERE `domain` = :domain
|
|
||||||
OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :target) ORDER BY `id`",
|
|
||||||
['domain' => $domain, 'target' => $domain])->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array[] alias rows of any domain whose goto may name a mailbox of the domain (the caller checks) */
|
|
||||||
public function aliasesPointingTo(string $domain): array
|
|
||||||
{
|
|
||||||
return $this->query("SELECT * FROM `alias` WHERE `goto` LIKE :pattern ORDER BY `id`",
|
|
||||||
['pattern' => '%@' . addcslashes($domain, '%_\\') . '%'])->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $table table with rows of mailboxes
|
|
||||||
* @param string $column its column holding the username
|
|
||||||
* @param string[] $usernames
|
|
||||||
* @return array[] rows of these mailboxes
|
|
||||||
*/
|
|
||||||
public function rowsOfMailboxes(string $table, string $column, array $usernames): array
|
|
||||||
{
|
|
||||||
self::identifier($table);
|
|
||||||
self::identifier($column);
|
|
||||||
if (empty($usernames)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
[$in, $params] = self::in($usernames);
|
|
||||||
return $this->query("SELECT * FROM `$table` WHERE `$column` IN ($in)", $params)->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array[] username, id, name, domain, source of the group memberships of these mailboxes */
|
|
||||||
public function groupMemberships(array $usernames): array
|
|
||||||
{
|
|
||||||
if (empty($usernames)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
[$in, $params] = self::in($usernames);
|
|
||||||
return $this->query("SELECT `gm`.`username`, `g`.`id`, `g`.`name`, `g`.`domain`, `gm`.`source` FROM `mailbox_group_member` `gm`
|
|
||||||
JOIN `mailbox_group` `g` ON `g`.`id` = `gm`.`group_id` WHERE `gm`.`username` IN ($in) ORDER BY `g`.`id`", $params)->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $table mail_policy or retention_rule
|
|
||||||
* @param string $member its member table
|
|
||||||
* @param string $key the member table's column with the id
|
|
||||||
* @return array[] username, id, name, domain of the direct assignments of these mailboxes
|
|
||||||
*/
|
|
||||||
public function assignments(string $table, string $member, string $key, array $usernames): array
|
|
||||||
{
|
|
||||||
self::identifier($table);
|
|
||||||
self::identifier($member);
|
|
||||||
self::identifier($key);
|
|
||||||
if (empty($usernames)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
[$in, $params] = self::in($usernames);
|
|
||||||
return $this->query("SELECT `a`.`username`, `p`.`id`, `p`.`name`, `p`.`domain` FROM `$member` `a`
|
|
||||||
JOIN `$table` `p` ON `p`.`id` = `a`.`$key` WHERE `a`.`username` IN ($in) ORDER BY `p`.`id`", $params)->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array[] sync jobs of these mailboxes (without their log) with host1, port1 and name of the source */
|
|
||||||
public function syncJobs(array $usernames): array
|
|
||||||
{
|
|
||||||
if (empty($usernames)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
[$in, $params] = self::in($usernames);
|
|
||||||
$rows = $this->query("SELECT `i`.*, `s`.`host1` AS `source_host1`, `s`.`port1` AS `source_port1`, `s`.`name` AS `source_name`
|
|
||||||
FROM `imapsync` `i` LEFT JOIN `imapsync_source` `s` ON `s`.`id` = `i`.`source_id` WHERE `i`.`user2` IN ($in) ORDER BY `i`.`id`", $params)->fetchAll();
|
|
||||||
foreach ($rows as &$row) {
|
|
||||||
unset($row['returned_text']);
|
|
||||||
}
|
|
||||||
return $rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array[] BCC maps of the whole domain (local_dest "@domain", older rows "domain") */
|
|
||||||
public function domainBccMaps(string $domain): array
|
|
||||||
{
|
|
||||||
return $this->query("SELECT * FROM `bcc_maps` WHERE `local_dest` IN (:domain, :at)", ['domain' => $domain, 'at' => '@' . $domain])->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $table mail_policy or retention_rule
|
|
||||||
* @param string $member its member table
|
|
||||||
* @param string $key the member table's column with the id
|
|
||||||
* @return array[] rows of the domain, each with `group_members`: the names of the groups it is assigned to
|
|
||||||
*/
|
|
||||||
public function domainRules(string $table, string $member, string $key, string $domain): array
|
|
||||||
{
|
|
||||||
self::identifier($table);
|
|
||||||
self::identifier($member);
|
|
||||||
self::identifier($key);
|
|
||||||
$rows = $this->query("SELECT * FROM `$table` WHERE `domain` = :domain ORDER BY `id`", ['domain' => $domain])->fetchAll();
|
|
||||||
foreach ($rows as &$row) {
|
|
||||||
$row['group_members'] = $this->query("SELECT `g`.`name` FROM `$member` `a` JOIN `mailbox_group` `g` ON `g`.`id` = `a`.`group_id`
|
|
||||||
WHERE `a`.`$key` = :id ORDER BY `g`.`name`", ['id' => $row['id']])->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
}
|
|
||||||
return $rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array{0: string, 1: array} placeholders and parameters of an IN list */
|
|
||||||
private static function in(array $values): array
|
|
||||||
{
|
|
||||||
$names = [];
|
|
||||||
$params = [];
|
|
||||||
foreach (array_values($values) as $i => $value) {
|
|
||||||
$names[] = ':in' . $i;
|
|
||||||
$params['in' . $i] = $value;
|
|
||||||
}
|
|
||||||
return [implode(', ', $names), $params];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Table and column names come from code only; this keeps it that way */
|
|
||||||
private static function identifier(string $name): void
|
|
||||||
{
|
|
||||||
if (!preg_match('/^[a-z_0-9]+$/i', $name)) {
|
|
||||||
throw new \InvalidArgumentException('invalid identifier');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Repository;
|
|
||||||
|
|
||||||
use Mailcow\Backup\Model\Job;
|
|
||||||
use Mailcow\Core\Context;
|
|
||||||
use Mailcow\Core\Repository;
|
|
||||||
use Mailcow\Core\UserError;
|
|
||||||
use PDO;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table `backup_job` (the runners claim, log and finish the jobs) and the lookups a new restore job needs.
|
|
||||||
*/
|
|
||||||
class JobRepository extends Repository
|
|
||||||
{
|
|
||||||
private const COLUMNS = "`id`, `type`, `state`, `step`, `params`, `result`, `error`, `domain`, `requested_by`,
|
|
||||||
UNIX_TIMESTAMP(`created`) AS `created`, UNIX_TIMESTAMP(`started`) AS `started`, UNIX_TIMESTAMP(`finished`) AS `finished`";
|
|
||||||
/** Serializes the check for active jobs and the insert of a new one */
|
|
||||||
private const LOCK = 'mailcow_backup_job';
|
|
||||||
|
|
||||||
public function find(int $id, bool $withLog = false): ?Job
|
|
||||||
{
|
|
||||||
$row = $this->query("SELECT " . self::COLUMNS . ($withLog ? ", `log`" : "") . " FROM `backup_job` WHERE `id` = :id", ['id' => $id])->fetch();
|
|
||||||
return is_array($row) ? Job::fromRow($row) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param ?string $type only jobs of this type
|
|
||||||
* @return Job[] newest first: all jobs for admins, the restores of their domains (since the domain was created) for
|
|
||||||
* domain admins
|
|
||||||
*/
|
|
||||||
public function visible(Context $context, int $limit, ?string $type = null): array
|
|
||||||
{
|
|
||||||
if ($context->hasAdminRights()) {
|
|
||||||
[$where, $params] = ['1=1', []];
|
|
||||||
} elseif ($context->isDomainAdmin()) {
|
|
||||||
// restores of their domains, requested since the domain was created (not those of a former domain of that name)
|
|
||||||
[$where, $params] = ["`type` = 'restore' AND `domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :da)
|
|
||||||
AND `created` >= (SELECT `d`.`created` FROM `domain` `d` WHERE `d`.`domain` = `backup_job`.`domain`)", ['da' => $context->username]];
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if ($type !== null) {
|
|
||||||
$where .= " AND `type` = :type";
|
|
||||||
$params['type'] = $type;
|
|
||||||
}
|
|
||||||
return array_map(fn(array $row): Job => Job::fromRow($row),
|
|
||||||
$this->query("SELECT " . self::COLUMNS . " FROM `backup_job` WHERE $where ORDER BY `id` DESC LIMIT " . max(1, $limit), $params)->fetchAll());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return Job[] queued and running jobs */
|
|
||||||
public function active(): array
|
|
||||||
{
|
|
||||||
return array_map(fn(array $row): Job => Job::fromRow($row),
|
|
||||||
$this->query("SELECT " . self::COLUMNS . " FROM `backup_job` WHERE `state` IN ('queued', 'running') ORDER BY `id`")->fetchAll());
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<string, Job> type => its newest job */
|
|
||||||
public function latestOfEachType(): array
|
|
||||||
{
|
|
||||||
$jobs = [];
|
|
||||||
foreach ($this->query("SELECT " . self::COLUMNS . " FROM `backup_job` WHERE `id` IN (SELECT MAX(`id`) FROM `backup_job` GROUP BY `type`)")->fetchAll() as $row) {
|
|
||||||
$jobs[(string)$row['type']] = Job::fromRow($row);
|
|
||||||
}
|
|
||||||
return $jobs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A backup job was created by the schedule since this unix time */
|
|
||||||
public function scheduledSince(int $since): bool
|
|
||||||
{
|
|
||||||
return (bool)$this->query("SELECT 1 FROM `backup_job` WHERE `type` = 'backup' AND `requested_by` = 'schedule' AND `created` >= FROM_UNIXTIME(:since) LIMIT 1",
|
|
||||||
['since' => $since])->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the job after $check(active jobs) passed; $check throws a UserError on a conflict. A named lock keeps two
|
|
||||||
* requests from queueing the same work at once.
|
|
||||||
*
|
|
||||||
* @param callable(Job[]): void $check
|
|
||||||
* @throws UserError
|
|
||||||
*/
|
|
||||||
public function insertExclusive(Job $job, callable $check): int
|
|
||||||
{
|
|
||||||
if ((int)$this->query("SELECT GET_LOCK('" . self::LOCK . "', 10)")->fetchColumn() !== 1) {
|
|
||||||
throw new UserError('backup_job_active', [$job->type]);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$check($this->active());
|
|
||||||
$job->id = $this->insertRow('backup_job', $job->toRow());
|
|
||||||
return $job->id;
|
|
||||||
} finally {
|
|
||||||
$this->query("SELECT RELEASE_LOCK('" . self::LOCK . "')");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels a queued or running job; the restic runner stops at its next check. A restore in the dovecot step
|
|
||||||
* (mails, mails_running) cannot be cancelled. False when the job finished or reached that step meanwhile.
|
|
||||||
*/
|
|
||||||
public function cancel(int $id): bool
|
|
||||||
{
|
|
||||||
return $this->query("UPDATE `backup_job` SET `state` = 'cancelled', `finished` = NOW() WHERE `id` = :id
|
|
||||||
AND `state` IN ('queued', 'running') AND NOT (`type` = 'restore' AND COALESCE(`step`, '') IN ('mails', 'mails_running'))",
|
|
||||||
['id' => $id])->rowCount() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isDomain(string $domain): bool
|
|
||||||
{
|
|
||||||
return (bool)$this->query("SELECT 1 FROM `domain` WHERE `domain` = :domain", ['domain' => $domain])->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Domain of a mailbox, null if there is none */
|
|
||||||
public function mailboxDomain(string $username): ?string
|
|
||||||
{
|
|
||||||
$domain = $this->query("SELECT `domain` FROM `mailbox` WHERE `username` = :username", ['username' => $username])->fetchColumn();
|
|
||||||
return is_string($domain) ? strtolower($domain) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Unix time the domain was created, null if it does not exist */
|
|
||||||
public function domainCreated(string $domain): ?int
|
|
||||||
{
|
|
||||||
$created = $this->query("SELECT UNIX_TIMESTAMP(`created`) FROM `domain` WHERE `domain` = :domain", ['domain' => $domain])->fetchColumn();
|
|
||||||
return ($created === false || $created === null) ? null : (int)$created;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string[] $domains
|
|
||||||
* @return array{0: array<string, true>, 1: array<string, array<string, true>>} the domains that exist, and per domain
|
|
||||||
* the usernames of its mailboxes; one query each per 500 domains
|
|
||||||
*/
|
|
||||||
public function domainsAndUsernames(array $domains): array
|
|
||||||
{
|
|
||||||
$existing = [];
|
|
||||||
$usernames = [];
|
|
||||||
foreach (array_chunk(array_values(array_unique($domains)), 500) as $chunk) {
|
|
||||||
$params = [];
|
|
||||||
foreach ($chunk as $i => $domain) {
|
|
||||||
$params['d' . $i] = $domain;
|
|
||||||
}
|
|
||||||
$in = implode(', ', array_map(fn(string $name): string => ':' . $name, array_keys($params)));
|
|
||||||
foreach ($this->query("SELECT `domain` FROM `domain` WHERE `domain` IN ($in)", $params)->fetchAll(PDO::FETCH_COLUMN) as $domain) {
|
|
||||||
$existing[strtolower((string)$domain)] = true;
|
|
||||||
}
|
|
||||||
foreach ($this->query("SELECT `domain`, `username` FROM `mailbox` WHERE `domain` IN ($in)", $params)->fetchAll() as $row) {
|
|
||||||
$usernames[strtolower((string)$row['domain'])][strtolower((string)$row['username'])] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [$existing, $usernames];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<string, string> username => name of the mailboxes of a domain */
|
|
||||||
public function mailboxNames(string $domain): array
|
|
||||||
{
|
|
||||||
$names = [];
|
|
||||||
foreach ($this->query("SELECT `username`, `name` FROM `mailbox` WHERE `domain` = :domain ORDER BY `username`", ['domain' => $domain])->fetchAll() as $row) {
|
|
||||||
$names[strtolower((string)$row['username'])] = (string)$row['name'];
|
|
||||||
}
|
|
||||||
return $names;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return string[] all mailboxes of a domain */
|
|
||||||
public function mailboxesOf(string $domain): array
|
|
||||||
{
|
|
||||||
return array_map('strtolower', $this->query("SELECT `username` FROM `mailbox` WHERE `domain` = :domain ORDER BY `username`",
|
|
||||||
['domain' => $domain])->fetchAll(PDO::FETCH_COLUMN));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return string[] alias domains of a domain */
|
|
||||||
public function aliasDomainsOf(string $domain): array
|
|
||||||
{
|
|
||||||
return array_map('strtolower', $this->query("SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :domain",
|
|
||||||
['domain' => $domain])->fetchAll(PDO::FETCH_COLUMN));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Repository;
|
|
||||||
|
|
||||||
use Mailcow\Core\Repository;
|
|
||||||
use PDO;
|
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads and writes of the restore merge. Rows of a backup may come from an older schema, so inserts and updates only
|
|
||||||
* use the columns the table has now. Table and column names come from code, never from the backup.
|
|
||||||
*/
|
|
||||||
class RestoreRepository extends Repository
|
|
||||||
{
|
|
||||||
/** @var array<string, string[]> table => columns */
|
|
||||||
private array $columns = [];
|
|
||||||
|
|
||||||
/** Runs $work in one transaction */
|
|
||||||
public function inTransaction(callable $work): mixed
|
|
||||||
{
|
|
||||||
return $this->transaction($work);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Runs $work in a savepoint of the running transaction: an error undoes only its writes and is thrown on */
|
|
||||||
public function savepoint(string $name, callable $work): mixed
|
|
||||||
{
|
|
||||||
self::identifier($name);
|
|
||||||
$this->pdo->exec("SAVEPOINT `$name`");
|
|
||||||
try {
|
|
||||||
$result = $work();
|
|
||||||
$this->pdo->exec("RELEASE SAVEPOINT `$name`");
|
|
||||||
return $result;
|
|
||||||
} catch (Throwable $e) {
|
|
||||||
$this->pdo->exec("ROLLBACK TO SAVEPOINT `$name`");
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs $work while holding a named lock, so a retried request of the runner waits for the first one instead of
|
|
||||||
* merging the same mailbox twice; null when the lock was not free within $timeout seconds
|
|
||||||
*/
|
|
||||||
public function locked(string $key, callable $work, int $timeout = 120): mixed
|
|
||||||
{
|
|
||||||
$name = 'mailcow_backup_' . md5($key);
|
|
||||||
if ((int)$this->query("SELECT GET_LOCK(:name, " . max(0, $timeout) . ")", ['name' => $name])->fetchColumn() !== 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return $work();
|
|
||||||
} finally {
|
|
||||||
$this->query("SELECT RELEASE_LOCK(:name)", ['name' => $name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Quota rows of a new mailbox (like the legacy mailbox add) */
|
|
||||||
public function resetQuota(string $username): void
|
|
||||||
{
|
|
||||||
foreach (['quota2', 'quota2replica'] as $table) {
|
|
||||||
$this->query("INSERT INTO `$table` (`username`, `bytes`, `messages`) VALUES (:username, 0, 0)
|
|
||||||
ON DUPLICATE KEY UPDATE `bytes` = 0, `messages` = 0", ['username' => $username]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Regular mailboxes of a domain (the domain's mailbox limit counts these) */
|
|
||||||
public function mailboxCount(string $domain): int
|
|
||||||
{
|
|
||||||
return (int)$this->query("SELECT COUNT(*) FROM `mailbox` WHERE `domain` = :domain AND `kind` = ''", ['domain' => $domain])->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** External send-as rights for a domain that is local now (legacy domain and alias domain add) */
|
|
||||||
public function dropExternalSenderAcl(string $domain): void
|
|
||||||
{
|
|
||||||
$this->query("DELETE FROM `sender_acl` WHERE `external` = 1 AND `send_as` LIKE :domain", ['domain' => '%@' . addcslashes($domain, '%_\\')]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return string[] the columns of a table in the current schema */
|
|
||||||
public function columns(string $table): array
|
|
||||||
{
|
|
||||||
self::identifier($table);
|
|
||||||
return $this->columns[$table] ??= $this->query("SELECT `COLUMN_NAME` FROM `information_schema`.`COLUMNS`
|
|
||||||
WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = :table ORDER BY `ORDINAL_POSITION`", ['table' => $table])->fetchAll(PDO::FETCH_COLUMN);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array[] rows of a table matching every condition (column => value) */
|
|
||||||
public function rows(string $table, array $where): array
|
|
||||||
{
|
|
||||||
[$condition, $params] = $this->where($table, $where);
|
|
||||||
return $this->query("SELECT * FROM `$table` WHERE $condition", $params)->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Inserts a row with the values of the columns the table has now; returns the new id (0 without one) */
|
|
||||||
public function insert(string $table, array $values): int
|
|
||||||
{
|
|
||||||
return $this->insertRow($table, $this->existing($table, $values));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Updates the rows matching $where with the values of the columns the table has now */
|
|
||||||
public function update(string $table, array $where, array $values): int
|
|
||||||
{
|
|
||||||
$values = $this->existing($table, $values);
|
|
||||||
if (empty($values)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
[$condition, $params] = $this->where($table, $where);
|
|
||||||
$set = [];
|
|
||||||
foreach (array_keys($values) as $i => $column) {
|
|
||||||
$set[] = "`$column` = :v$i";
|
|
||||||
$params['v' . $i] = $values[$column];
|
|
||||||
}
|
|
||||||
return $this->query("UPDATE `$table` SET " . implode(', ', $set) . " WHERE $condition", $params)->rowCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A primary or alias domain */
|
|
||||||
public function domainExists(string $domain): bool
|
|
||||||
{
|
|
||||||
return (bool)$this->query("SELECT 1 FROM `domain` WHERE `domain` = :domain UNION SELECT 1 FROM `alias_domain` WHERE `alias_domain` = :alias",
|
|
||||||
['domain' => $domain, 'alias' => $domain])->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The primary domain of a domain or alias domain, null if mailcow has neither */
|
|
||||||
public function primaryDomain(string $domain): ?string
|
|
||||||
{
|
|
||||||
$primary = $this->query("SELECT `domain` FROM `domain` WHERE `domain` = :domain
|
|
||||||
UNION SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :alias", ['domain' => $domain, 'alias' => $domain])->fetchColumn();
|
|
||||||
return is_string($primary) ? strtolower($primary) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An active primary or alias domain (like the legacy is_local_mailcow_domain()) */
|
|
||||||
public function isLocalDomain(string $domain): bool
|
|
||||||
{
|
|
||||||
return (bool)$this->query("SELECT 1 FROM `domain` WHERE `domain` = :domain AND `active` = 1
|
|
||||||
UNION SELECT 1 FROM `alias_domain` WHERE `alias_domain` = :alias AND `active` = 1", ['domain' => $domain, 'alias' => $domain])->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return string[] alias domains of a domain */
|
|
||||||
public function aliasDomainsOf(string $domain): array
|
|
||||||
{
|
|
||||||
return array_map('strtolower', $this->query("SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :domain",
|
|
||||||
['domain' => $domain])->fetchAll(PDO::FETCH_COLUMN));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What an address is now: 'mailbox' (also local@alias-domain of a mailbox), 'resource' (a mailbox of kind
|
|
||||||
* location, thing or group), 'alias', 'spamalias' or null
|
|
||||||
*/
|
|
||||||
public function addressKind(string $address): ?string
|
|
||||||
{
|
|
||||||
$kind = $this->query("SELECT `kind` FROM `mailbox` WHERE `username` = :address", ['address' => $address])->fetchColumn();
|
|
||||||
if (is_string($kind)) {
|
|
||||||
return preg_match('/location|thing|group/', $kind) ? 'resource' : 'mailbox';
|
|
||||||
}
|
|
||||||
[$local, $domain] = array_pad(explode('@', $address, 2), 2, '');
|
|
||||||
if ((bool)$this->query("SELECT 1 FROM `mailbox` `m` JOIN `alias_domain` `ad` ON `ad`.`alias_domain` = :domain
|
|
||||||
WHERE `m`.`username` = CONCAT(:local, '@', `ad`.`target_domain`)", ['domain' => $domain, 'local' => $local])->fetchColumn()) {
|
|
||||||
return 'mailbox';
|
|
||||||
}
|
|
||||||
if ((bool)$this->query("SELECT 1 FROM `alias` WHERE `address` = :address", ['address' => $address])->fetchColumn()) {
|
|
||||||
return 'alias';
|
|
||||||
}
|
|
||||||
if ((bool)$this->query("SELECT 1 FROM `spamalias` WHERE `address` = :address", ['address' => $address])->fetchColumn()) {
|
|
||||||
return 'spamalias';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Aliases the domain may still get (domain limit minus its aliases and those of its alias domains, like the legacy code) */
|
|
||||||
public function aliasesLeft(string $domain): int
|
|
||||||
{
|
|
||||||
$limit = $this->query("SELECT `aliases` FROM `domain` WHERE `domain` = :domain", ['domain' => $domain])->fetchColumn();
|
|
||||||
if ($limit === false) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
$used = (int)$this->query("SELECT COUNT(*) FROM `alias` WHERE (`domain` = :domain
|
|
||||||
OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :target))
|
|
||||||
AND `address` NOT IN (SELECT `username` FROM `mailbox`)", ['domain' => $domain, 'target' => $domain])->fetchColumn();
|
|
||||||
return (int)$limit - $used;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Quota (bytes) the other mailboxes of the domain have */
|
|
||||||
public function quotaInUse(string $domain, string $exceptUsername): int
|
|
||||||
{
|
|
||||||
return (int)$this->query("SELECT COALESCE(SUM(`quota`), 0) FROM `mailbox` WHERE `domain` = :domain AND `kind` = '' AND `username` != :username",
|
|
||||||
['domain' => $domain, 'username' => $exceptUsername])->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array[] user1, host1 and port1 of the mailbox's sync jobs */
|
|
||||||
public function syncJobsOf(string $username): array
|
|
||||||
{
|
|
||||||
return $this->query("SELECT `i`.`user1`, `s`.`host1`, `s`.`port1` FROM `imapsync` `i`
|
|
||||||
LEFT JOIN `imapsync_source` `s` ON `s`.`id` = `i`.`source_id` WHERE `i`.`user2` = :username", ['username' => $username])->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return int[] ids of the sync sources with this host and port the mailbox may use, oldest first */
|
|
||||||
public function syncSourcesFor(string $host, int $port, string $username, string $domain): array
|
|
||||||
{
|
|
||||||
return array_map('intval', $this->query("SELECT `s`.`id` FROM `imapsync_source` `s` WHERE `s`.`host1` = :host AND `s`.`port1` = :port
|
|
||||||
AND (`s`.`scope` = 'all'
|
|
||||||
OR (`s`.`scope` = 'domain' AND EXISTS (SELECT 1 FROM `imapsync_source_domain` `d` WHERE `d`.`source_id` = `s`.`id` AND `d`.`domain` = :domain))
|
|
||||||
OR (`s`.`scope` = 'user' AND EXISTS (SELECT 1 FROM `imapsync_source_user` `u` WHERE `u`.`source_id` = `s`.`id` AND `u`.`username` = :username)))
|
|
||||||
ORDER BY `s`.`id`", ['host' => $host, 'port' => $port, 'domain' => $domain, 'username' => $username])->fetchAll(PDO::FETCH_COLUMN));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array{0: string, 1: array} condition and parameters of column => value pairs */
|
|
||||||
private function where(string $table, array $where): array
|
|
||||||
{
|
|
||||||
self::identifier($table);
|
|
||||||
$parts = [];
|
|
||||||
$params = [];
|
|
||||||
foreach (array_keys($where) as $i => $column) {
|
|
||||||
self::identifier((string)$column);
|
|
||||||
if ($where[$column] === null) {
|
|
||||||
$parts[] = "`$column` IS NULL";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$parts[] = "`$column` = :w$i";
|
|
||||||
$params['w' . $i] = $where[$column];
|
|
||||||
}
|
|
||||||
return [empty($parts) ? '1=1' : implode(' AND ', $parts), $params];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The values of the columns the table has now */
|
|
||||||
private function existing(string $table, array $values): array
|
|
||||||
{
|
|
||||||
return array_intersect_key($values, array_flip($this->columns($table)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Table, column and savepoint names come from code only; this keeps it that way */
|
|
||||||
private static function identifier(string $name): void
|
|
||||||
{
|
|
||||||
if (!preg_match('/^[a-z_0-9]+$/i', $name)) {
|
|
||||||
throw new \InvalidArgumentException('invalid identifier');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Repository;
|
|
||||||
|
|
||||||
use Mailcow\Core\Repository;
|
|
||||||
use PDO;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table `backup_setting` (name/value pairs; the runner writes its state_* values into it as well).
|
|
||||||
*/
|
|
||||||
class SettingsRepository extends Repository
|
|
||||||
{
|
|
||||||
/** @return array<string, string> name => value of the stored settings */
|
|
||||||
public function load(): array
|
|
||||||
{
|
|
||||||
return $this->query("SELECT `name`, `value` FROM `backup_setting`")->fetchAll(PDO::FETCH_KEY_PAIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Stores the given values (name => value) */
|
|
||||||
public function save(array $values): void
|
|
||||||
{
|
|
||||||
$this->transaction(function () use ($values): void {
|
|
||||||
foreach ($values as $name => $value) {
|
|
||||||
$this->query("INSERT INTO `backup_setting` (`name`, `value`) VALUES (:name, :value)
|
|
||||||
ON DUPLICATE KEY UPDATE `value` = VALUES(`value`)", ['name' => (string)$name, 'value' => (string)$value]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Repository;
|
|
||||||
|
|
||||||
use Mailcow\Backup\Model\Snapshot;
|
|
||||||
use Mailcow\Core\Repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table `backup_snapshot`: the runner's cache of the repository's snapshots (read only here).
|
|
||||||
*/
|
|
||||||
class SnapshotRepository extends Repository
|
|
||||||
{
|
|
||||||
private const COLUMNS = "`id`, `short_id`, UNIX_TIMESTAMP(`time`) AS `time`, `hostname`, `tags`, `summary`, `domains`";
|
|
||||||
|
|
||||||
/** By id or short id, with its content (the mailboxes in it) */
|
|
||||||
public function find(string $id): ?Snapshot
|
|
||||||
{
|
|
||||||
$row = $this->query("SELECT " . self::COLUMNS . ", `content` FROM `backup_snapshot` WHERE `id` = :id OR `short_id` = :short_id ORDER BY `id` = :exact DESC LIMIT 1",
|
|
||||||
['id' => $id, 'short_id' => $id, 'exact' => $id])->fetch();
|
|
||||||
return is_array($row) ? Snapshot::fromRow($row) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return Snapshot[] newest first; the content (the mailboxes in it) only with $withContent */
|
|
||||||
public function all(bool $withContent = false): array
|
|
||||||
{
|
|
||||||
return array_map(fn(array $row): Snapshot => Snapshot::fromRow($row),
|
|
||||||
$this->query("SELECT " . self::COLUMNS . ($withContent ? ", `content`" : "") . " FROM `backup_snapshot` ORDER BY `time` DESC, `id`")->fetchAll());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Service;
|
|
||||||
|
|
||||||
use Mailcow\Backup\Legacy\LegacyDomain;
|
|
||||||
use Mailcow\Backup\Model\Job;
|
|
||||||
use Mailcow\Backup\Repository\JobRepository;
|
|
||||||
use Mailcow\Backup\Repository\RestoreRepository;
|
|
||||||
use Mailcow\Core\AccessDenied;
|
|
||||||
use Mailcow\Core\Context;
|
|
||||||
use Mailcow\Core\Input;
|
|
||||||
use Mailcow\Core\UserError;
|
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Action `restore_domain`: recreates a domain deleted since the snapshot from its export (`domain_data`), before its
|
|
||||||
* mailboxes are restored. Only for running restores an admin requested while the domain did not exist
|
|
||||||
* (params.create_domain).
|
|
||||||
*
|
|
||||||
* Creates the domain like the legacy domain add (row, tags, domain map, external send-as rights for it dropped, SOGo
|
|
||||||
* restarted so it knows the domain), unless it exists by now. Then adds what is missing: alias domains, DKIM keys, rate
|
|
||||||
* limit, spam settings, BCC maps, footer, MTA-STS, domain admin assignments of accounts that still exist, groups (no
|
|
||||||
* members, send-as rights or shares; members follow with the mailboxes), mail policies and retention rules with their
|
|
||||||
* group assignments (matched by name).
|
|
||||||
*/
|
|
||||||
class DomainRestoreService
|
|
||||||
{
|
|
||||||
private const NEW_ROW = ['id', 'created', 'modified'];
|
|
||||||
private const POLICY_LISTS = ['send_allow', 'send_block', 'receive_allow', 'receive_block'];
|
|
||||||
private const RATE_LIMIT = '/^\d+ \/ 1[smhd]$/';
|
|
||||||
|
|
||||||
public function __construct(private RestoreRepository $repo, private JobRepository $jobs, private GroupMembers $groups,
|
|
||||||
private LegacyDomain $legacy)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{domain: array{added: int, skipped: int, errors: string[], created: int}}
|
|
||||||
* @throws UserError
|
|
||||||
*/
|
|
||||||
public function restore(mixed $jobId, mixed $data): array
|
|
||||||
{
|
|
||||||
$id = Input::intInRange($jobId, 1, PHP_INT_MAX);
|
|
||||||
$job = ($id === null) ? null : $this->jobs->find($id);
|
|
||||||
if ($job === null || $job->type !== 'restore' || $job->state !== 'running') {
|
|
||||||
throw new UserError('backup_job_not_running');
|
|
||||||
}
|
|
||||||
if (!$job->createsDomain() || ($job->params['requested_role'] ?? '') !== 'admin') {
|
|
||||||
throw new AccessDenied();
|
|
||||||
}
|
|
||||||
$domain = strtolower((string)$job->domain);
|
|
||||||
if (!is_array($data) || !is_array($data['domain'] ?? null) || strtolower((string)($data['domain']['domain'] ?? '')) !== $domain) {
|
|
||||||
throw new UserError('backup_export_invalid');
|
|
||||||
}
|
|
||||||
$result = $this->repo->locked($job->id . ':domain', fn(): array => $this->restoreDomain($job, $domain, $data));
|
|
||||||
return is_array($result) ? ['domain' => $result] : throw new UserError('backup_job_busy');
|
|
||||||
}
|
|
||||||
|
|
||||||
private function restoreDomain(Job $job, string $domain, array $data): array
|
|
||||||
{
|
|
||||||
$result = ['added' => 0, 'skipped' => 0, 'errors' => [], 'created' => 0];
|
|
||||||
if (!empty($this->repo->rows('alias_domain', ['alias_domain' => $domain]))) {
|
|
||||||
$result['errors'][] = "$domain is an alias domain now";
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
if (!empty($this->repo->rows('domain', ['domain' => $domain]))) {
|
|
||||||
// created meanwhile: only what is missing is added
|
|
||||||
$result['skipped']++;
|
|
||||||
} else {
|
|
||||||
$this->createDomain($domain, $data, $result);
|
|
||||||
}
|
|
||||||
$this->repo->inTransaction(function () use ($domain, $data, &$result): void {
|
|
||||||
$this->aliasDomains($domain, $data, $result);
|
|
||||||
$this->domainSettings($domain, $data, $result);
|
|
||||||
});
|
|
||||||
$this->redisSettings($domain, $data, $result);
|
|
||||||
$groupIds = $this->createGroups($domain, $data, $result);
|
|
||||||
$this->repo->inTransaction(function () use ($domain, $data, $groupIds, &$result): void {
|
|
||||||
$this->rules($domain, $data, 'mail_policies', 'mail_policy', 'mail_policy_member', 'policy_id', $groupIds, $result);
|
|
||||||
$this->rules($domain, $data, 'retention_rules', 'retention_rule', 'retention_rule_member', 'rule_id', $groupIds, $result);
|
|
||||||
});
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The domain row, its tags and the effects of the legacy domain add */
|
|
||||||
private function createDomain(string $domain, array $data, array &$result): void
|
|
||||||
{
|
|
||||||
// the domain keeps its creation time: domain admins may use the snapshots made after it
|
|
||||||
$row = array_diff_key(RestoreService::decode($data['domain']), array_flip(['modified']));
|
|
||||||
$relayhost = (string)($row['relayhost'] ?? '0');
|
|
||||||
if ($relayhost !== '0' && $relayhost !== '' && empty($this->repo->rows('relayhosts', ['id' => (int)$relayhost]))) {
|
|
||||||
$row['relayhost'] = '0';
|
|
||||||
$result['errors'][] = 'relayhost: does not exist';
|
|
||||||
}
|
|
||||||
$this->repo->inTransaction(function () use ($domain, $row, $data): void {
|
|
||||||
$this->repo->insert('domain', ['domain' => $domain] + $row);
|
|
||||||
$this->repo->dropExternalSenderAcl($domain);
|
|
||||||
foreach (array_unique(array_filter(array_map(fn(mixed $tag): string => is_scalar($tag) ? trim((string)$tag) : '', (array)($data['tags'] ?? [])), 'strlen')) as $tag) {
|
|
||||||
$this->repo->insert('tags_domain', ['domain' => $domain, 'tag_name' => $tag]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$result['added']++;
|
|
||||||
$result['created'] = 1;
|
|
||||||
$this->effect($result, 'domain map', fn() => $this->legacy->addDomainMap($domain));
|
|
||||||
$error = $this->legacy->restartSogo();
|
|
||||||
if ($error !== null) {
|
|
||||||
$result['errors'][] = 'SOGo restart failed: ' . $error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function aliasDomains(string $domain, array $data, array &$result): void
|
|
||||||
{
|
|
||||||
foreach (self::rows($data, 'alias_domains') as $row) {
|
|
||||||
$alias = strtolower(trim((string)($row['alias_domain'] ?? '')));
|
|
||||||
if ($alias === '' || $this->repo->domainExists($alias)) {
|
|
||||||
$result['skipped']++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$this->repo->insert('alias_domain', ['alias_domain' => $alias, 'target_domain' => $domain] + array_diff_key($row, array_flip(self::NEW_ROW)));
|
|
||||||
$this->repo->dropExternalSenderAcl($alias);
|
|
||||||
$this->effect($result, "domain map of $alias", fn() => $this->legacy->addDomainMap($alias));
|
|
||||||
$result['added']++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Spam settings, BCC maps, footer, MTA-STS and domain admins of the domain that are missing */
|
|
||||||
private function domainSettings(string $domain, array $data, array &$result): void
|
|
||||||
{
|
|
||||||
foreach (self::rows($data, 'filterconf') as $row) {
|
|
||||||
$option = (string)($row['option'] ?? '');
|
|
||||||
$value = (string)($row['value'] ?? '');
|
|
||||||
$current = $this->repo->rows('filterconf', ['object' => $domain]);
|
|
||||||
$present = in_array($option, ['whitelist_from', 'blacklist_from'], true)
|
|
||||||
? array_filter($current, fn(array $c): bool => in_array($c['option'], ['whitelist_from', 'blacklist_from'], true) && strtolower((string)$c['value']) === strtolower($value))
|
|
||||||
: array_filter($current, fn(array $c): bool => $c['option'] === $option && (in_array($option, ['lowspamlevel', 'highspamlevel'], true) || (string)$c['value'] === $value));
|
|
||||||
if ($option === '' || !empty($present)) {
|
|
||||||
$result['skipped']++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$this->repo->insert('filterconf', ['object' => $domain, 'option' => $option, 'value' => $value]);
|
|
||||||
$result['added']++;
|
|
||||||
}
|
|
||||||
foreach (self::rows($data, 'bcc_maps') as $row) {
|
|
||||||
$localDest = strtolower((string)($row['local_dest'] ?? ''));
|
|
||||||
if (!in_array($localDest, [$domain, '@' . $domain], true) || !filter_var((string)($row['bcc_dest'] ?? ''), FILTER_VALIDATE_EMAIL)
|
|
||||||
|| !in_array($row['type'] ?? '', ['sender', 'rcpt'], true)) {
|
|
||||||
$result['errors'][] = "BCC map $localDest: invalid";
|
|
||||||
} elseif (!empty($this->repo->rows('bcc_maps', ['local_dest' => $localDest, 'type' => $row['type']]))) {
|
|
||||||
$result['skipped']++;
|
|
||||||
} else {
|
|
||||||
$this->repo->insert('bcc_maps', ['domain' => $domain] + array_diff_key($row, array_flip(self::NEW_ROW)));
|
|
||||||
$result['added']++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (['footer' => 'domain_wide_footer', 'mta_sts' => 'mta_sts'] as $key => $table) {
|
|
||||||
if (!is_array($data[$key] ?? null)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!empty($this->repo->rows($table, ['domain' => $domain]))) {
|
|
||||||
$result['skipped']++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$this->repo->insert($table, ['domain' => $domain] + array_diff_key(RestoreService::decode($data[$key]), array_flip(['created', 'modified'])));
|
|
||||||
$result['added']++;
|
|
||||||
}
|
|
||||||
foreach (self::rows($data, 'domain_admins') as $row) {
|
|
||||||
$username = (string)($row['username'] ?? '');
|
|
||||||
if (empty($this->repo->rows('admin', ['username' => $username, 'superadmin' => 0]))) {
|
|
||||||
$result['errors'][] = "domain admin $username: does not exist";
|
|
||||||
} elseif (!empty($this->repo->rows('domain_admins', ['username' => $username, 'domain' => $domain]))) {
|
|
||||||
$result['skipped']++;
|
|
||||||
} else {
|
|
||||||
$this->repo->insert('domain_admins', ['username' => $username, 'domain' => $domain, 'active' => (int)($row['active'] ?? 1)]);
|
|
||||||
$result['added']++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** DKIM keys of the domain and its alias domains, and the rate limit of the domain (Redis), when missing */
|
|
||||||
private function redisSettings(string $domain, array $data, array &$result): void
|
|
||||||
{
|
|
||||||
$domains = array_merge([$domain], $this->repo->aliasDomainsOf($domain));
|
|
||||||
foreach (self::rows($data, 'dkim') as $key) {
|
|
||||||
$name = strtolower((string)($key['domain'] ?? ''));
|
|
||||||
$selector = (string)($key['selector'] ?? '');
|
|
||||||
if (!in_array($name, $domains, true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!preg_match('/^[a-z0-9._-]{1,63}$/i', $selector) || (string)($key['pubkey'] ?? '') === '' || (string)($key['privkey'] ?? '') === '') {
|
|
||||||
$result['errors'][] = "DKIM key of $name: invalid";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$this->effect($result, "DKIM key of $name", function () use ($name, $selector, $key, &$result): void {
|
|
||||||
if ($this->legacy->dkim($name) !== null) {
|
|
||||||
$result['skipped']++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$this->legacy->addDkim($name, $selector, (string)$key['pubkey'], (string)$key['privkey']);
|
|
||||||
$result['added']++;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
$rateLimit = $data['ratelimit'] ?? null;
|
|
||||||
if (is_string($rateLimit) && preg_match(self::RATE_LIMIT, $rateLimit)) {
|
|
||||||
$this->effect($result, 'rate limit', function () use ($domain, $rateLimit, &$result): void {
|
|
||||||
if (isset($this->legacy->rateLimits([$domain])[$domain])) {
|
|
||||||
$result['skipped']++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$this->legacy->setRateLimit($domain, $rateLimit);
|
|
||||||
$result['added']++;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Groups of the domain missing by name, through the group module (address, flags, priority, IdP link)
|
|
||||||
*
|
|
||||||
* @return array<int, int> id in the backup => id now, of all groups found by name
|
|
||||||
*/
|
|
||||||
private function createGroups(string $domain, array $data, array &$result): array
|
|
||||||
{
|
|
||||||
$ids = [];
|
|
||||||
$admin = new Context('admin', 'backup', [], true);
|
|
||||||
foreach (self::rows($data, 'groups') as $row) {
|
|
||||||
$name = (string)($row['name'] ?? '');
|
|
||||||
$current = $this->repo->rows('mailbox_group', ['domain' => $domain, 'name' => $name])[0] ?? null;
|
|
||||||
if ($current !== null) {
|
|
||||||
$ids[(int)($row['id'] ?? 0)] = (int)$current['id'];
|
|
||||||
$result['skipped']++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$input = ['domain' => $domain, 'name' => $name, 'description' => (string)($row['description'] ?? '')];
|
|
||||||
foreach (['sender_allowed', 'sogo_visible', 'internal', 'active', 'priority'] as $key) {
|
|
||||||
if (isset($row[$key])) {
|
|
||||||
$input[$key] = (int)$row[$key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (['address', 'idp_ref'] as $key) {
|
|
||||||
if (($row[$key] ?? null) !== null && $row[$key] !== '') {
|
|
||||||
$input[$key] = (string)$row[$key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$ids[(int)($row['id'] ?? 0)] = $this->groups->create($admin, $input);
|
|
||||||
$result['added']++;
|
|
||||||
} catch (UserError $e) {
|
|
||||||
$result['errors'][] = "group $name: " . $e->key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mail policies or retention rules missing by name, with their group assignments; group:<id> entries of policies
|
|
||||||
* follow the groups' new ids
|
|
||||||
*/
|
|
||||||
private function rules(string $domain, array $data, string $key, string $table, string $member, string $column, array $groupIds, array &$result): void
|
|
||||||
{
|
|
||||||
foreach (self::rows($data, $key) as $row) {
|
|
||||||
$name = (string)($row['name'] ?? '');
|
|
||||||
if (!empty($this->repo->rows($table, ['domain' => $domain, 'name' => $name]))) {
|
|
||||||
$result['skipped']++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$values = array_diff_key($row, array_flip([...self::NEW_ROW, 'group_members']));
|
|
||||||
foreach (self::POLICY_LISTS as $list) {
|
|
||||||
if (isset($values[$list])) {
|
|
||||||
$values[$list] = json_encode($this->remapGroups((string)$values[$list], $groupIds, "$name ($list)", $result));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$id = $this->repo->insert($table, ['domain' => $domain, 'name' => $name] + $values);
|
|
||||||
foreach ((array)($row['group_members'] ?? []) as $groupName) {
|
|
||||||
$group = $this->repo->rows('mailbox_group', ['domain' => $domain, 'name' => (string)$groupName])[0] ?? null;
|
|
||||||
if ($group === null) {
|
|
||||||
$result['errors'][] = "$name: group $groupName does not exist";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$this->repo->insert($member, [$column => $id, 'group_id' => (int)$group['id']]);
|
|
||||||
}
|
|
||||||
$result['added']++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return string[] entries of a JSON list with group:<id> of the backup turned into the group's id now */
|
|
||||||
private function remapGroups(string $json, array $groupIds, string $what, array &$result): array
|
|
||||||
{
|
|
||||||
$entries = [];
|
|
||||||
foreach ((array)json_decode($json, true) as $entry) {
|
|
||||||
if (!is_string($entry)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (preg_match('/^group:(\d+)$/', $entry, $match)) {
|
|
||||||
if (!isset($groupIds[(int)$match[1]])) {
|
|
||||||
$result['errors'][] = "$what: $entry dropped (group does not exist)";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$entry = 'group:' . $groupIds[(int)$match[1]];
|
|
||||||
}
|
|
||||||
$entries[] = $entry;
|
|
||||||
}
|
|
||||||
return $entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A Redis or dockerapi step: a failure is reported and the rest goes on */
|
|
||||||
private function effect(array &$result, string $what, callable $work): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$work();
|
|
||||||
} catch (Throwable) {
|
|
||||||
$result['errors'][] = "$what: not stored (Redis)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function rows(array $data, string $key): array
|
|
||||||
{
|
|
||||||
return array_values(array_filter((array)($data[$key] ?? []), 'is_array'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Service;
|
|
||||||
|
|
||||||
use Mailcow\Backup\Legacy\LegacyDomain;
|
|
||||||
use Mailcow\Backup\Repository\ExportRepository;
|
|
||||||
use Mailcow\Core\UserError;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export of a domain for the backup (/mailcow/export/<domain>.json in a snapshot), in pages of mailboxes the runner
|
|
||||||
* merges into one file. The first page holds version, db_version, domain, domain_data (what recreating the domain
|
|
||||||
* needs), the aliases and `mailbox_aliases` (username => the aliases of any domain whose goto names the mailbox,
|
|
||||||
* without its own alias and group addresses; the runner moves them into the mailbox sections when it reads the file);
|
|
||||||
* every page a part of `mailboxes` and `next_after`, the last username of the page to ask for the next one with
|
|
||||||
* (keyset paging; null after the last page).
|
|
||||||
*
|
|
||||||
* Per mailbox every row a restore needs, with all columns. Password hashes, DKIM keys and the secrets of app
|
|
||||||
* passwords, 2FA and sync jobs are part of it on purpose; only the runner gets it (backup.php with the token). Binary
|
|
||||||
* values (fido2) are written as {"base64": "..."}. Beside the keys of the spec, a mailbox has `own_alias` (its alias
|
|
||||||
* row: forwarding with or without a copy), `aliases` (empty, see mailbox_aliases) and `ratelimit` (Redis).
|
|
||||||
*/
|
|
||||||
class ExportService
|
|
||||||
{
|
|
||||||
public const VERSION = 1;
|
|
||||||
public const PAGE_SIZE = 500;
|
|
||||||
/** Lists of a mailbox: key => [table, column with the username] */
|
|
||||||
private const MAILBOX_ROWS = [
|
|
||||||
'sender_acl' => ['sender_acl', 'logged_in_as'],
|
|
||||||
'sender_acl_granted' => ['sender_acl', 'send_as'],
|
|
||||||
'sieve_filters' => ['sieve_filters', 'username'],
|
|
||||||
'filterconf' => ['filterconf', 'object'],
|
|
||||||
'spamalias' => ['spamalias', 'goto'],
|
|
||||||
'bcc_maps' => ['bcc_maps', 'local_dest'],
|
|
||||||
'app_passwd' => ['app_passwd', 'mailbox'],
|
|
||||||
'tfa' => ['tfa', 'username'],
|
|
||||||
// FIDO2 keys of mailbox users (passwordless login on the user page), like those of admins
|
|
||||||
'fido2' => ['fido2', 'username'],
|
|
||||||
'identity_link' => ['identity_link', 'username'],
|
|
||||||
];
|
|
||||||
/** Single rows of a mailbox: key => [table, column with the username] */
|
|
||||||
private const MAILBOX_ROW = [
|
|
||||||
'user_acl' => ['user_acl', 'username'],
|
|
||||||
'pushover' => ['pushover', 'username'],
|
|
||||||
];
|
|
||||||
|
|
||||||
public function __construct(private ExportRepository $repo, private LegacyDomain $legacy)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return string[] all domains */
|
|
||||||
public function domains(): array
|
|
||||||
{
|
|
||||||
return array_values(array_map('strval', $this->repo->domains()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param ?string $after the last username of the previous page (keyset paging), null for the first page
|
|
||||||
* @throws UserError domain_not_found
|
|
||||||
*/
|
|
||||||
public function export(string $domain, ?string $after = null, int $pageSize = self::PAGE_SIZE): array
|
|
||||||
{
|
|
||||||
$domain = strtolower(trim($domain));
|
|
||||||
$row = ($domain === '') ? null : ($this->repo->rows('domain', 'domain', $domain)[0] ?? null);
|
|
||||||
if ($row === null) {
|
|
||||||
throw new UserError('domain_not_found', [htmlspecialchars($domain)]);
|
|
||||||
}
|
|
||||||
$rows = $this->repo->mailboxesAfter($domain, strtolower($after ?? ''), $pageSize + 1);
|
|
||||||
$more = count($rows) > $pageSize;
|
|
||||||
$rows = array_slice($rows, 0, $pageSize);
|
|
||||||
$export = ['version' => self::VERSION, 'domain' => $domain];
|
|
||||||
if ($after === null) {
|
|
||||||
$export += [
|
|
||||||
'db_version' => $this->repo->dbVersion(),
|
|
||||||
'domain_data' => $this->domainData($domain, $row),
|
|
||||||
'aliases' => array_map(fn(array $alias): array => self::encode($alias), $this->repo->aliases($domain)),
|
|
||||||
'mailbox_aliases' => $this->mailboxAliases($domain) ?: new \stdClass(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
$export['mailboxes'] = $this->mailboxes($domain, $rows) ?: new \stdClass();
|
|
||||||
$export['next_after'] = ($more && !empty($rows)) ? strtolower((string)end($rows)['username']) : null;
|
|
||||||
return $export;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** What recreating the domain needs: its row and settings, alias domains, DKIM keys, groups, policies, rules */
|
|
||||||
private function domainData(string $domain, array $row): array
|
|
||||||
{
|
|
||||||
$aliasDomains = $this->repo->rows('alias_domain', 'target_domain', $domain);
|
|
||||||
$dkim = [];
|
|
||||||
foreach (array_merge([$domain], array_map(fn(array $alias): string => strtolower((string)$alias['alias_domain']), $aliasDomains)) as $name) {
|
|
||||||
$key = $this->legacy->dkim($name);
|
|
||||||
if ($key !== null) {
|
|
||||||
$dkim[] = ['domain' => $name] + $key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
'domain' => self::encode($row),
|
|
||||||
'tags' => array_map(fn(array $tag): string => (string)$tag['tag_name'], $this->repo->rows('tags_domain', 'domain', $domain)),
|
|
||||||
'alias_domains' => $aliasDomains,
|
|
||||||
'dkim' => $dkim,
|
|
||||||
'ratelimit' => $this->legacy->rateLimits([$domain])[$domain] ?? null,
|
|
||||||
'filterconf' => $this->repo->rows('filterconf', 'object', $domain),
|
|
||||||
'bcc_maps' => $this->repo->domainBccMaps($domain),
|
|
||||||
'footer' => $this->repo->rows('domain_wide_footer', 'domain', $domain)[0] ?? null,
|
|
||||||
'mta_sts' => $this->repo->rows('mta_sts', 'domain', $domain)[0] ?? null,
|
|
||||||
'domain_admins' => $this->repo->rows('domain_admins', 'domain', $domain),
|
|
||||||
'groups' => $this->repo->rows('mailbox_group', 'domain', $domain),
|
|
||||||
'mail_policies' => $this->repo->domainRules('mail_policy', 'mail_policy_member', 'policy_id', $domain),
|
|
||||||
'retention_rules' => $this->repo->domainRules('retention_rule', 'retention_rule_member', 'rule_id', $domain),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<string, array> username => its section */
|
|
||||||
private function mailboxes(string $domain, array $rows): array
|
|
||||||
{
|
|
||||||
$mailboxes = [];
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
$mailboxes[strtolower((string)$row['username'])] = [
|
|
||||||
'mailbox' => self::encode($row) + ['tags' => []],
|
|
||||||
'user_acl' => null,
|
|
||||||
'pushover' => null,
|
|
||||||
'ratelimit' => null,
|
|
||||||
'own_alias' => null,
|
|
||||||
'aliases' => [],
|
|
||||||
'groups' => [],
|
|
||||||
'mail_policies' => [],
|
|
||||||
'retention_rules' => [],
|
|
||||||
'imapsync' => [],
|
|
||||||
] + array_fill_keys(array_keys(self::MAILBOX_ROWS), []);
|
|
||||||
}
|
|
||||||
$usernames = array_map('strval', array_keys($mailboxes));
|
|
||||||
if (empty($usernames)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
// the mailbox of the page a row belongs to, null for anything else
|
|
||||||
$owner = fn(array $row, string $column): ?string => isset($mailboxes[strtolower((string)$row[$column])]) ? strtolower((string)$row[$column]) : null;
|
|
||||||
foreach ($this->repo->rowsOfMailboxes('tags_mailbox', 'username', $usernames) as $row) {
|
|
||||||
if (($username = $owner($row, 'username')) !== null) {
|
|
||||||
$mailboxes[$username]['mailbox']['tags'][] = (string)$row['tag_name'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (self::MAILBOX_ROW as $key => [$table, $column]) {
|
|
||||||
foreach ($this->repo->rowsOfMailboxes($table, $column, $usernames) as $row) {
|
|
||||||
if (($username = $owner($row, $column)) !== null) {
|
|
||||||
$mailboxes[$username][$key] = self::encode($row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (self::MAILBOX_ROWS as $key => [$table, $column]) {
|
|
||||||
foreach ($this->repo->rowsOfMailboxes($table, $column, $usernames) as $row) {
|
|
||||||
if (($username = $owner($row, $column)) !== null) {
|
|
||||||
$mailboxes[$username][$key][] = self::encode($row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach ($this->legacy->rateLimits($usernames) as $username => $value) {
|
|
||||||
if (isset($mailboxes[strtolower((string)$username)])) {
|
|
||||||
$mailboxes[strtolower((string)$username)]['ratelimit'] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach ($this->repo->groupMemberships($usernames) as $row) {
|
|
||||||
if (($username = $owner($row, 'username')) !== null) {
|
|
||||||
$mailboxes[$username]['groups'][] = ['id' => (int)$row['id'], 'name' => (string)$row['name'], 'domain' => (string)$row['domain'],
|
|
||||||
'source' => (string)$row['source']];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach (['mail_policies' => ['mail_policy', 'mail_policy_member', 'policy_id'], 'retention_rules' => ['retention_rule', 'retention_rule_member', 'rule_id']]
|
|
||||||
as $key => [$table, $member, $column]) {
|
|
||||||
foreach ($this->repo->assignments($table, $member, $column, $usernames) as $row) {
|
|
||||||
if (($username = $owner($row, 'username')) !== null) {
|
|
||||||
$mailboxes[$username][$key][] = ['id' => (int)$row['id'], 'name' => (string)$row['name'], 'domain' => (string)$row['domain']];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach ($this->repo->syncJobs($usernames) as $row) {
|
|
||||||
if (($username = $owner($row, 'user2')) !== null) {
|
|
||||||
$mailboxes[$username]['imapsync'][] = self::encode($row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach ($this->repo->rowsOfMailboxes('alias', 'address', $usernames) as $row) {
|
|
||||||
if (($username = $owner($row, 'address')) !== null) {
|
|
||||||
$mailboxes[$username]['own_alias'] = self::encode($row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $mailboxes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, array[]> username => aliases of any domain whose goto names the mailbox (once per domain: the
|
|
||||||
* alias table is scanned, not once per page)
|
|
||||||
*/
|
|
||||||
private function mailboxAliases(string $domain): array
|
|
||||||
{
|
|
||||||
$usernames = array_fill_keys(array_map(fn(mixed $username): string => strtolower((string)$username), $this->repo->usernames($domain)), true);
|
|
||||||
$aliases = [];
|
|
||||||
foreach ($this->repo->aliasesPointingTo($domain) as $row) {
|
|
||||||
if ($row['group_id'] !== null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$address = strtolower((string)$row['address']);
|
|
||||||
foreach (array_unique(array_map('strtolower', self::gotos((string)$row['goto']))) as $goto) {
|
|
||||||
if ($goto !== $address && isset($usernames[$goto])) {
|
|
||||||
$aliases[$goto][] = self::encode($row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $aliases;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return string[] addresses of a goto list */
|
|
||||||
public static function gotos(string $goto): array
|
|
||||||
{
|
|
||||||
return array_values(array_filter(array_map('trim', explode(',', $goto)), 'strlen'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Binary values (not UTF-8) as {"base64": "..."}, so the row survives JSON */
|
|
||||||
public static function encode(array $row): array
|
|
||||||
{
|
|
||||||
foreach ($row as $column => $value) {
|
|
||||||
if (is_string($value) && !mb_check_encoding($value, 'UTF-8')) {
|
|
||||||
$row[$column] = ['base64' => base64_encode($value)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Service;
|
|
||||||
|
|
||||||
use Mailcow\Core\Context;
|
|
||||||
use Mailcow\Core\UserError;
|
|
||||||
use Mailcow\MailboxGroup\Repository\GroupRepository;
|
|
||||||
use Mailcow\MailboxGroup\Repository\LookupRepository;
|
|
||||||
use Mailcow\MailboxGroup\Service\ChangeEffects;
|
|
||||||
use Mailcow\MailboxGroup\Service\GroupService;
|
|
||||||
use Mailcow\MailboxGroup\Service\SharingService;
|
|
||||||
use Mailcow\MailboxGroup\Service\TemplateService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group writes of a restore through the group module (addresses, send-as rights, shares and caches follow), as the
|
|
||||||
* requester of the restore: an admin, or the domain admin with his rights now (GroupService::canEdit). The module is
|
|
||||||
* wired without group templates (Legacy\NoTemplates).
|
|
||||||
*/
|
|
||||||
class GroupMembers
|
|
||||||
{
|
|
||||||
public function __construct(private GroupRepository $repo, private LookupRepository $lookup, private TemplateService $templates,
|
|
||||||
private ChangeEffects $effects, private SharingService $sharing)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @throws UserError e.g. access_denied when the group is read-only for the requester */
|
|
||||||
public function add(Context $context, int $groupId, string $username): void
|
|
||||||
{
|
|
||||||
$this->service($context)->update($groupId, ['add_members' => [$username]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a group (restore of a deleted domain); members follow with the mailboxes.
|
|
||||||
*
|
|
||||||
* @throws UserError
|
|
||||||
*/
|
|
||||||
public function create(Context $context, array $input): int
|
|
||||||
{
|
|
||||||
return (int)$this->service($context)->create($input)->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function service(Context $context): GroupService
|
|
||||||
{
|
|
||||||
return new GroupService($this->repo, $this->lookup, $this->templates, $this->effects, $this->sharing, $context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,666 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Service;
|
|
||||||
|
|
||||||
use DateTimeImmutable;
|
|
||||||
use DateTimeZone;
|
|
||||||
use Mailcow\Backup\Model\Job;
|
|
||||||
use Mailcow\Backup\Model\Settings;
|
|
||||||
use Mailcow\Backup\Model\Snapshot;
|
|
||||||
use Mailcow\Backup\Repository\JobRepository;
|
|
||||||
use Mailcow\Backup\Repository\SnapshotRepository;
|
|
||||||
use Mailcow\Core\AccessDenied;
|
|
||||||
use Mailcow\Core\Context;
|
|
||||||
use Mailcow\Core\Input;
|
|
||||||
use Mailcow\Core\UserError;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Jobs of the backup runners, the snapshots with their content and the status page.
|
|
||||||
*
|
|
||||||
* Backup, check, prune and init jobs: admins (not read-only API keys) queue them, one of each type at a time.
|
|
||||||
* Restores: admins, and domain admins for mailboxes or a domain they administer now. Mailboxes and domains deleted
|
|
||||||
* since the snapshot are created from it (deleted domains by admins only). A component a domain admin could not
|
|
||||||
* manage himself (e.g. sync jobs without the `syncjobs` ACL) is refused; his ACL and domains are stored with the job,
|
|
||||||
* so the merge and the runners apply the same limits.
|
|
||||||
* Reading: admins see every job and snapshot; domain admins the restores of their domains and, of each snapshot made
|
|
||||||
* after their domain was created, their domains in it.
|
|
||||||
*/
|
|
||||||
class JobService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Restore components in their order. mails, imap_acl, sieve_activate and the Sieve scripts of filters are restored by
|
|
||||||
* the dovecot runner, sogo by the restic runner, the others by RestoreService.
|
|
||||||
*/
|
|
||||||
public const COMPONENTS = ['mails', 'imap_acl', 'sogo', 'aliases', 'groups', 'sender_acl', 'mail_policies', 'retention_rules',
|
|
||||||
'filters', 'sieve_activate', 'spam', 'bcc', 'syncjobs', 'app_passwords', 'tfa', 'settings', 'password'];
|
|
||||||
/** Components that replace current data with the backup's (all others only add what is missing) */
|
|
||||||
public const OVERWRITE = ['sogo', 'settings', 'password'];
|
|
||||||
/** Components that need the mails: not in snapshots without them (RESTIC_BACKUP_MODE=config) */
|
|
||||||
public const MAIL_COMPONENTS = ['mails', 'imap_acl', 'sogo', 'sieve_activate'];
|
|
||||||
public const MAIL_TARGETS = ['folder', 'original'];
|
|
||||||
/** Domain admin ACL a component needs */
|
|
||||||
private const COMPONENT_ACL = [
|
|
||||||
'groups' => 'mailbox_groups',
|
|
||||||
'mail_policies' => 'mail_policies',
|
|
||||||
'retention_rules' => 'retention',
|
|
||||||
'filters' => 'filters',
|
|
||||||
'sieve_activate' => 'filters',
|
|
||||||
'spam' => 'spam_policy',
|
|
||||||
'bcc' => 'bcc_maps',
|
|
||||||
'syncjobs' => 'syncjobs',
|
|
||||||
'app_passwords' => 'app_passwds',
|
|
||||||
];
|
|
||||||
/** Components that only work together with another one */
|
|
||||||
private const REQUIRES = ['sieve_activate' => 'filters'];
|
|
||||||
/** Domain admin ACLs the merge respects, stored with the job */
|
|
||||||
private const RESTORE_ACL = ['filters', 'mailbox_groups', 'mail_policies', 'retention', 'spam_policy', 'bcc_maps', 'syncjobs',
|
|
||||||
'app_passwds', 'protocol_access', 'mailbox_relayhost', 'unlimited_quota', 'sogo_access', 'pushover', 'extend_sender_acl',
|
|
||||||
'alias_external_goto', 'ratelimit', 'tls_policy', 'quarantine', 'quarantine_notification', 'quarantine_category'];
|
|
||||||
private const MAX_MAILBOXES = 1000;
|
|
||||||
private const LIST_LIMIT = 100;
|
|
||||||
private const MAX_LIST = 1000;
|
|
||||||
/** Mailboxes a search returns at most */
|
|
||||||
private const SEARCH_LIMIT = 500;
|
|
||||||
/** The runner reports every 10 seconds */
|
|
||||||
private const RUNNER_TIMEOUT = 120;
|
|
||||||
/** @var array<string, array{bool, ?int}> domain => [access of the domain admin, creation time now], per request */
|
|
||||||
private array $domainState = [];
|
|
||||||
|
|
||||||
public function __construct(private JobRepository $jobs, private SnapshotRepository $snapshots, private SettingsService $settings,
|
|
||||||
private Context $context, private string $timezone = 'UTC')
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param mixed $type only jobs of this type ('' or null = all; an unknown type finds nothing)
|
|
||||||
* @return Job[] newest first, at most $limit (0 = LIST_LIMIT)
|
|
||||||
*/
|
|
||||||
public function list(int $limit = 0, mixed $type = null): array
|
|
||||||
{
|
|
||||||
$type = Input::text($type);
|
|
||||||
if ($type !== '' && !in_array($type, Job::TYPES, true)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return $this->jobs->visible($this->context, ($limit > 0) ? min($limit, self::MAX_LIST) : self::LIST_LIMIT, ($type === '') ? null : $type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Entry of the API. Domain admins get it restricted: e-mail addresses outside the job's domain and its alias domains
|
|
||||||
* and repository details (URLs, restic repository strings, paths of the repository) are shown as "…".
|
|
||||||
*/
|
|
||||||
public function view(Job $job, bool $withLog = false): array
|
|
||||||
{
|
|
||||||
$entry = $job->toArray($withLog);
|
|
||||||
if ($this->context->hasAdminRights()) {
|
|
||||||
return $entry;
|
|
||||||
}
|
|
||||||
$domains = array_merge([strtolower($job->domain)], $this->jobs->aliasDomainsOf(strtolower($job->domain)));
|
|
||||||
foreach (['result', 'error', 'log'] as $key) {
|
|
||||||
if (isset($entry[$key])) {
|
|
||||||
$entry[$key] = self::restricted($entry[$key], $domains);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// the account of an admin who requested the restore stays hidden, the UI shows "Administrator"
|
|
||||||
if (($job->params['requested_role'] ?? '') !== 'domainadmin') {
|
|
||||||
$entry['requested_by'] = '';
|
|
||||||
if (isset($entry['log'])) {
|
|
||||||
$entry['log'] = str_replace('(requested by ' . $job->requestedBy . ')', '(requested by an administrator)', $entry['log']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The job with its log, null when it does not exist or the caller may not see it */
|
|
||||||
public function get(int $id): ?Job
|
|
||||||
{
|
|
||||||
$job = $this->jobs->find($id, true);
|
|
||||||
return ($job !== null && $this->visible($job)) ? $job : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queues a backup, check, prune or init job (admins).
|
|
||||||
*
|
|
||||||
* @throws UserError
|
|
||||||
*/
|
|
||||||
public function create(array $input): Job
|
|
||||||
{
|
|
||||||
$this->context->requireAdmin();
|
|
||||||
$type = Input::text($input['type'] ?? null);
|
|
||||||
if (!in_array($type, Job::MANUAL_TYPES, true)) {
|
|
||||||
throw new UserError('backup_job_type_invalid', [htmlspecialchars((string)$type)]);
|
|
||||||
}
|
|
||||||
$settings = $this->settings->load();
|
|
||||||
if (!$settings->configured()) {
|
|
||||||
throw new UserError('backup_not_configured');
|
|
||||||
}
|
|
||||||
if ($type === 'backup' && $settings->get('password_confirmed') !== '1') {
|
|
||||||
throw new UserError('backup_password_not_confirmed');
|
|
||||||
}
|
|
||||||
$job = new Job($type);
|
|
||||||
$job->requestedBy = (string)$this->context->username;
|
|
||||||
$this->jobs->insertExclusive($job, function (array $active) use ($type): void {
|
|
||||||
foreach ($active as $other) {
|
|
||||||
if ($other->type === $type) {
|
|
||||||
throw new UserError('backup_job_active', [$type]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return $job;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queues a restore: {snapshot, scope: mailbox|domain, mailboxes | domain, components, mail_target: folder|original}.
|
|
||||||
* All mailboxes belong to one domain. Mailboxes deleted since the snapshot are created from it (create_mailboxes);
|
|
||||||
* an admin may restore a deleted domain (create_domain). Scope domain restores the snapshot's mailboxes of the
|
|
||||||
* domain (the current ones for snapshots without content).
|
|
||||||
*
|
|
||||||
* @throws UserError
|
|
||||||
*/
|
|
||||||
public function restore(array $input): Job
|
|
||||||
{
|
|
||||||
$this->requireRestorer();
|
|
||||||
if (!$this->settings->load()->configured()) {
|
|
||||||
throw new UserError('backup_not_configured');
|
|
||||||
}
|
|
||||||
$snapshot = $this->snapshot($input['snapshot'] ?? null);
|
|
||||||
$scope = Input::text($input['scope'] ?? 'mailbox');
|
|
||||||
[$domain, $mailboxes] = match ($scope) {
|
|
||||||
'domain' => $this->domainScope($snapshot, $input['domain'] ?? null),
|
|
||||||
'mailbox' => $this->mailboxScope($snapshot, $input['mailboxes'] ?? null, $input['domain'] ?? null),
|
|
||||||
default => throw new UserError('backup_scope_invalid'),
|
|
||||||
};
|
|
||||||
$components = $this->components($input['components'] ?? null);
|
|
||||||
$missing = array_values(array_intersect($components, self::MAIL_COMPONENTS));
|
|
||||||
if ($snapshot->mode() === 'config' && !empty($missing)) {
|
|
||||||
throw new UserError('backup_components_no_mails', [htmlspecialchars(implode(', ', $missing))]);
|
|
||||||
}
|
|
||||||
$mailTarget = Input::text($input['mail_target'] ?? null) ?: 'folder';
|
|
||||||
if (!in_array($mailTarget, self::MAIL_TARGETS, true)) {
|
|
||||||
throw new UserError('backup_mail_target_invalid');
|
|
||||||
}
|
|
||||||
$job = new Job('restore');
|
|
||||||
$job->step = 'files';
|
|
||||||
$job->domain = $domain;
|
|
||||||
$job->requestedBy = (string)$this->context->username;
|
|
||||||
$job->params = [
|
|
||||||
'snapshot' => $snapshot->id,
|
|
||||||
'snapshot_time' => $snapshot->time,
|
|
||||||
'scope' => $scope,
|
|
||||||
'domain' => $domain,
|
|
||||||
'mailboxes' => $mailboxes,
|
|
||||||
'components' => $components,
|
|
||||||
'mail_target' => $mailTarget,
|
|
||||||
'requested_role' => $this->context->hasAdminRights() ? 'admin' : 'domainadmin',
|
|
||||||
'create_domain' => (int)!$this->jobs->isDomain($domain),
|
|
||||||
'create_mailboxes' => array_values(array_filter($mailboxes, fn(string $username): bool => $this->jobs->mailboxDomain($username) === null)),
|
|
||||||
];
|
|
||||||
if (!$this->context->hasAdminRights()) {
|
|
||||||
$job->params['acl'] = array_combine(self::RESTORE_ACL, array_map(fn(string $acl): int => (int)$this->context->hasAcl($acl), self::RESTORE_ACL));
|
|
||||||
$job->params['allowed_domains'] = array_values(array_unique(array_merge([$domain], $this->jobs->aliasDomainsOf($domain))));
|
|
||||||
}
|
|
||||||
$domainAdmin = !$this->context->hasAdminRights();
|
|
||||||
$this->jobs->insertExclusive($job, function (array $active) use ($mailboxes, $domain, $domainAdmin): void {
|
|
||||||
foreach ($active as $other) {
|
|
||||||
if ($other->type !== 'restore') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// a domain admin has one restore per domain at a time
|
|
||||||
if ($domainAdmin && strtolower($other->domain) === $domain) {
|
|
||||||
throw new UserError('backup_restore_domain_active', [htmlspecialchars($domain)]);
|
|
||||||
}
|
|
||||||
$both = array_intersect($mailboxes, $other->mailboxes());
|
|
||||||
if (!empty($both)) {
|
|
||||||
throw new UserError('backup_restore_active', [htmlspecialchars((string)reset($both))]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return $job;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels a queued or running job (the restic runner stops at its next check): admins any, domain admins the
|
|
||||||
* restores of their domains that domain admins requested. A restore in the dovecot step (mails, mails_running) runs
|
|
||||||
* to its end.
|
|
||||||
*
|
|
||||||
* @throws UserError
|
|
||||||
*/
|
|
||||||
public function cancel(int $id): Job
|
|
||||||
{
|
|
||||||
if ($this->context->isReadOnlyApi()) {
|
|
||||||
throw new AccessDenied();
|
|
||||||
}
|
|
||||||
$job = $this->jobs->find($id);
|
|
||||||
// domain admins cancel only restores domain admins requested
|
|
||||||
if ($job === null || !$this->visible($job) || (!$this->context->hasAdminRights() && ($job->params['requested_role'] ?? '') !== 'domainadmin')) {
|
|
||||||
throw new AccessDenied();
|
|
||||||
}
|
|
||||||
if (!$job->isActive() || ($job->type === 'restore' && in_array($job->step, ['mails', 'mails_running'], true)) || !$this->jobs->cancel($id)) {
|
|
||||||
throw new UserError('backup_job_not_cancellable', [$id]);
|
|
||||||
}
|
|
||||||
$job->state = 'cancelled';
|
|
||||||
return $job;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Snapshots, newest first. Domain admins get those holding one of their domains (see mayUse()), with id, short_id,
|
|
||||||
* time and those domains.
|
|
||||||
*/
|
|
||||||
public function snapshots(): array
|
|
||||||
{
|
|
||||||
if ($this->context->hasAdminRights()) {
|
|
||||||
return array_map(fn(Snapshot $snapshot): array => $snapshot->toArray(), $this->snapshots->all());
|
|
||||||
}
|
|
||||||
if (!$this->context->isDomainAdmin()) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$snapshots = [];
|
|
||||||
// the domains with their creation time come from the snapshot row, the content is not needed here
|
|
||||||
foreach ($this->snapshots->all() as $snapshot) {
|
|
||||||
$domains = array_values(array_filter($snapshot->domainList(), fn(string $domain): bool => $this->mayUse($snapshot, $domain)));
|
|
||||||
if (!empty($domains)) {
|
|
||||||
$snapshots[] = ['id' => $snapshot->id, 'short_id' => $snapshot->shortId, 'time' => $snapshot->time, 'domains' => $domains,
|
|
||||||
'mode' => $snapshot->mode()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $snapshots;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Domains of a snapshot with their state now: GET backup-snapshot-content/<snapshot>. `available` 0: the snapshot has
|
|
||||||
* no content (not made by this installation), the list comes from its manifest and only existing mailboxes can be restored. A domain
|
|
||||||
* of the manifest missing in the content (its export failed) lists its current mailboxes as well.
|
|
||||||
*
|
|
||||||
* @return array{snapshot: string, time: int, available: int, domains: array[]}|array{} [] when not found or not visible
|
|
||||||
*/
|
|
||||||
public function content(mixed $snapshotId): array
|
|
||||||
{
|
|
||||||
$snapshot = $this->visibleSnapshot($snapshotId);
|
|
||||||
if ($snapshot === null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$visible = array_values(array_filter($snapshot->domainList(), fn(string $domain): bool => $this->mayUse($snapshot, $domain)));
|
|
||||||
[$existing, $usernames] = $this->jobs->domainsAndUsernames($visible);
|
|
||||||
$domains = [];
|
|
||||||
foreach ($visible as $domain) {
|
|
||||||
$current = $usernames[$domain] ?? [];
|
|
||||||
$mailboxes = $snapshot->mailboxesOf($domain) ?? $current;
|
|
||||||
$domains[] = ['domain' => $domain, 'exists' => (int)isset($existing[$domain]), 'mailboxes' => count($mailboxes),
|
|
||||||
'missing' => count(array_diff_key($mailboxes, $current))];
|
|
||||||
}
|
|
||||||
return ['snapshot' => $snapshot->id, 'time' => $snapshot->time, 'available' => (int)($snapshot->content !== null), 'domains' => $domains];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search in a snapshot: GET backup-snapshot-search/<snapshot>/<term>. Matches domain names and mailboxes by local
|
|
||||||
* part, address or name ("x@": local part, "@d": domain, "x@d": address), with the rules of content(); at most
|
|
||||||
* SEARCH_LIMIT mailboxes. A matching domain comes with all its mailboxes.
|
|
||||||
*
|
|
||||||
* @return array{domains: array[], truncated: int}|array{} [] when the snapshot is not found or not visible
|
|
||||||
*/
|
|
||||||
public function search(mixed $snapshotId, mixed $term): array
|
|
||||||
{
|
|
||||||
$snapshot = $this->visibleSnapshot($snapshotId);
|
|
||||||
$term = mb_strtolower((string)Input::text($term));
|
|
||||||
if ($snapshot === null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
if (trim($term, '@') === '') {
|
|
||||||
return ['domains' => [], 'truncated' => 0];
|
|
||||||
}
|
|
||||||
$domains = [];
|
|
||||||
$hits = 0;
|
|
||||||
$truncated = 0;
|
|
||||||
[$local, $at, $domainTerm] = str_contains($term, '@') ? [strstr($term, '@', true), true, substr((string)strstr($term, '@'), 1)] : [$term, false, ''];
|
|
||||||
foreach ($snapshot->domainList() as $domain) {
|
|
||||||
if (!$this->mayUse($snapshot, $domain)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$domainMatch = $at ? ($domainTerm !== '' && str_contains($domain, $domainTerm)) : str_contains($domain, $term);
|
|
||||||
if ($at && $domainTerm !== '' && !$domainMatch) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$current = $this->jobs->mailboxNames($domain);
|
|
||||||
$mailboxes = [];
|
|
||||||
foreach (($snapshot->mailboxesOf($domain) ?? $current) as $username => $name) {
|
|
||||||
$username = (string)$username;
|
|
||||||
$part = (string)strstr($username, '@', true);
|
|
||||||
$match = match (true) {
|
|
||||||
// "x@d": the address, "x@": the local part, "@d": every mailbox of the matching domains
|
|
||||||
$at && $local !== '' && $domainTerm !== '' => str_contains($username, $term),
|
|
||||||
$at && $local !== '' => str_contains($part, $local),
|
|
||||||
$at => true,
|
|
||||||
default => $domainMatch || str_contains($username, $term) || str_contains(mb_strtolower($name), $term),
|
|
||||||
};
|
|
||||||
if (!$match) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ($hits >= self::SEARCH_LIMIT) {
|
|
||||||
$truncated = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$hits++;
|
|
||||||
$mailboxes[] = ['username' => $username, 'name' => $name, 'exists' => (int)isset($current[$username])];
|
|
||||||
}
|
|
||||||
// a domain without matching mailboxes only for a search by domain
|
|
||||||
if (!empty($mailboxes) || ($domainMatch && !($at && $local !== '') && !$truncated)) {
|
|
||||||
$domains[] = ['domain' => $domain, 'exists' => (int)$this->jobs->isDomain($domain), 'mailboxes' => $mailboxes];
|
|
||||||
}
|
|
||||||
if ($truncated) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ['domains' => $domains, 'truncated' => $truncated];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mailboxes of a domain in a snapshot with their state now: GET backup-snapshot-content/<snapshot>/<domain>
|
|
||||||
*
|
|
||||||
* @return array{domain: string, exists: int, mailboxes: array[]}|array{} [] when not found or not visible
|
|
||||||
*/
|
|
||||||
public function domainContent(mixed $snapshotId, mixed $domain): array
|
|
||||||
{
|
|
||||||
$snapshot = $this->visibleSnapshot($snapshotId);
|
|
||||||
$domain = strtolower((string)Input::text($domain));
|
|
||||||
if ($snapshot === null || !$snapshot->hasDomain($domain) || !$this->mayUse($snapshot, $domain)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$current = $this->jobs->mailboxNames($domain);
|
|
||||||
$mailboxes = [];
|
|
||||||
foreach (($snapshot->mailboxesOf($domain) ?? $current) as $username => $name) {
|
|
||||||
$mailboxes[] = ['username' => (string)$username, 'name' => $name, 'exists' => (int)isset($current[$username])];
|
|
||||||
}
|
|
||||||
return ['domain' => $domain, 'exists' => (int)$this->jobs->isDomain($domain), 'mailboxes' => $mailboxes];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* State of the backup for admins: settings state, runner heartbeat, repository, newest job of each type, queued and
|
|
||||||
* running jobs, the next scheduled run. [] for everybody else.
|
|
||||||
*/
|
|
||||||
public function status(?int $now = null): array
|
|
||||||
{
|
|
||||||
if (!$this->context->hasAdminRights()) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$now ??= time();
|
|
||||||
$settings = $this->settings->load();
|
|
||||||
$seen = ($settings->get('state_runner_seen') === '') ? null : (int)$settings->get('state_runner_seen');
|
|
||||||
$snapshots = $this->snapshots->all();
|
|
||||||
$latest = $this->jobs->latestOfEachType();
|
|
||||||
$stats = json_decode($settings->get('state_repo_stats'), true);
|
|
||||||
return [
|
|
||||||
'enabled' => (int)$settings->get('enabled'),
|
|
||||||
'target' => $settings->get('target'),
|
|
||||||
'configured' => (int)$settings->configured(),
|
|
||||||
'password_confirmed' => (int)$settings->get('password_confirmed'),
|
|
||||||
'initialized' => (int)$settings->get('state_initialized'),
|
|
||||||
// the runner cannot decrypt the stored credentials (e.g. a database restored on another server): enter them again
|
|
||||||
'secrets_error' => ($settings->get('state_secrets_error') === '') ? null : $settings->get('state_secrets_error'),
|
|
||||||
// RESTIC_BACKUP_MODE (null until the runner reported it)
|
|
||||||
'mode' => ($settings->get('state_backup_mode') === '') ? null : $settings->get('state_backup_mode'),
|
|
||||||
'runner_seen' => $seen,
|
|
||||||
'runner_age' => ($seen === null) ? null : max(0, $now - $seen),
|
|
||||||
'runner_ok' => (int)($seen !== null && $now - $seen <= self::RUNNER_TIMEOUT),
|
|
||||||
'last_success' => ($settings->get('state_last_success') === '') ? null : (int)$settings->get('state_last_success'),
|
|
||||||
'repo_size' => ($settings->get('state_repo_size') === '') ? null : (int)$settings->get('state_repo_size'),
|
|
||||||
'repo_stats' => is_array($stats) ? $stats : null,
|
|
||||||
'snapshots' => count($snapshots),
|
|
||||||
'latest_snapshot' => empty($snapshots) ? null : $snapshots[0]->time,
|
|
||||||
'next_run' => $this->nextRun($settings, $now),
|
|
||||||
// the longest time between two scheduled backups (seconds): a later last success is overdue
|
|
||||||
'schedule_gap' => $settings->scheduleGap(),
|
|
||||||
'jobs' => array_map(fn(string $type): ?array => isset($latest[$type]) ? $latest[$type]->toArray() : null,
|
|
||||||
array_combine(Job::TYPES, Job::TYPES)),
|
|
||||||
'active' => array_map(fn(Job $job): array => $job->toArray(), $this->jobs->active()),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unix time of the next scheduled backup, null while the schedule is off. Like the runner: the schedule is armed
|
|
||||||
* (state_schedule_since/state_schedule) when it is switched on or changed, a run is due at the first schedule time
|
|
||||||
* after that; a due run without a scheduled backup since its time is the next one (now).
|
|
||||||
*/
|
|
||||||
private function nextRun(Settings $settings, int $now): ?int
|
|
||||||
{
|
|
||||||
if ($settings->get('enabled') !== '1' || $settings->get('password_confirmed') !== '1' || !$settings->configured()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$slots = $settings->scheduleSlots();
|
|
||||||
try {
|
|
||||||
$zone = new DateTimeZone($this->timezone);
|
|
||||||
} catch (\Exception) {
|
|
||||||
$zone = new DateTimeZone('UTC');
|
|
||||||
}
|
|
||||||
$today = (new DateTimeImmutable('@' . $now))->setTimezone($zone);
|
|
||||||
// the last schedule time up to now, and the one after it
|
|
||||||
$due = self::slot($slots, $today, -1);
|
|
||||||
$next = self::slot($slots, $today, 1);
|
|
||||||
$since = $settings->get('state_schedule_since');
|
|
||||||
if ($due === null || $since === '' || $settings->get('state_schedule') !== $settings->get('schedule') || (int)$since > $due) {
|
|
||||||
return $next;
|
|
||||||
}
|
|
||||||
return $this->jobs->scheduledSince($due) ? $next : $due;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The newest schedule time up to $now ($direction -1) or the first one after it (1) as Unix time, null without
|
|
||||||
* slots ([weekday, hour, minute], sorted)
|
|
||||||
*/
|
|
||||||
private static function slot(array $slots, DateTimeImmutable $now, int $direction): ?int
|
|
||||||
{
|
|
||||||
for ($i = 0; $i <= 7; $i++) {
|
|
||||||
$day = $now->modify(sprintf('%+d day', $direction * $i));
|
|
||||||
foreach (($direction < 0) ? array_reverse($slots) : $slots as [$weekday, $hour, $minute]) {
|
|
||||||
if ($weekday !== (int)$day->format('w')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$slot = $day->setTime($hour, $minute)->getTimestamp();
|
|
||||||
if (($direction < 0) ? $slot <= $now->getTimestamp() : $slot > $now->getTimestamp()) {
|
|
||||||
return $slot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @throws AccessDenied unless the caller may request restores */
|
|
||||||
private function requireRestorer(): void
|
|
||||||
{
|
|
||||||
if ($this->context->isReadOnlyApi() || !($this->context->hasAdminRights() || $this->context->isDomainAdmin())) {
|
|
||||||
throw new AccessDenied();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Admins every job; domain admins the restores of their domains requested after the domain was created */
|
|
||||||
private function visible(Job $job): bool
|
|
||||||
{
|
|
||||||
if ($this->context->hasAdminRights()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!$this->context->isDomainAdmin() || $job->type !== 'restore' || $job->domain === '' || !$this->context->canAccessDomain($job->domain)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$created = $this->jobs->domainCreated(strtolower($job->domain));
|
|
||||||
return $created !== null && (int)$job->created >= $created;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A value of a job as a domain admin sees it: e-mail addresses outside $domains and repository details become "…"
|
|
||||||
*
|
|
||||||
* @param string[] $domains
|
|
||||||
*/
|
|
||||||
private static function restricted(mixed $value, array $domains): mixed
|
|
||||||
{
|
|
||||||
if (is_array($value)) {
|
|
||||||
$restricted = [];
|
|
||||||
foreach ($value as $key => $item) {
|
|
||||||
$restricted[is_string($key) ? (string)self::restricted($key, $domains) : $key] = self::restricted($item, $domains);
|
|
||||||
}
|
|
||||||
return $restricted;
|
|
||||||
}
|
|
||||||
if (!is_string($value)) {
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
$value = preg_replace(['~\b(?:s3|sftp|rest|b2|azure|gs|swift|rclone):\S+~i', '~\bhttps?://\S+~i', '~/repository\S*~'], '…', $value);
|
|
||||||
return preg_replace_callback('/[^\s@<>"\'(),;:\[\]]+@([a-z0-9.-]+\.[a-z]{2,})/i',
|
|
||||||
fn(array $match): string => in_array(strtolower($match[1]), $domains, true) ? $match[0] : '…', (string)$value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @throws UserError backup_snapshot_not_found (also for snapshots a domain admin may not use) */
|
|
||||||
private function snapshot(mixed $value): Snapshot
|
|
||||||
{
|
|
||||||
$id = (string)Input::text($value);
|
|
||||||
$snapshot = ($id === '') ? null : $this->snapshots->find($id);
|
|
||||||
if ($snapshot === null || (!$this->context->hasAdminRights() && empty(array_filter($snapshot->domainList(),
|
|
||||||
fn(string $domain): bool => $this->mayUse($snapshot, $domain))))) {
|
|
||||||
throw new UserError('backup_snapshot_not_found', [htmlspecialchars($id)]);
|
|
||||||
}
|
|
||||||
return $snapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function visibleSnapshot(mixed $value): ?Snapshot
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
return ($this->context->hasAdminRights() || $this->context->isDomainAdmin()) ? $this->snapshot($value) : null;
|
|
||||||
} catch (UserError) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Admins every domain of the snapshot; domain admins the domains they administer now, when the snapshot holds this
|
|
||||||
* domain and not a former one of the same name: its recorded creation time is the domain's (snapshots without it:
|
|
||||||
* made after the domain was created)
|
|
||||||
*/
|
|
||||||
private function mayUse(Snapshot $snapshot, string $domain): bool
|
|
||||||
{
|
|
||||||
if ($this->context->hasAdminRights()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (!$this->context->isDomainAdmin()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$this->domainState[$domain] ??= [$this->context->canAccessDomain($domain), $this->jobs->domainCreated($domain)];
|
|
||||||
[$access, $created] = $this->domainState[$domain];
|
|
||||||
if (!$access) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
$recorded = $snapshot->createdOf($domain);
|
|
||||||
return $created !== null && (($recorded !== null) ? $recorded === $created : $snapshot->time > $created);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{0: string, 1: string[]} the domain and its mailboxes in the snapshot (now, for snapshots without content)
|
|
||||||
* @throws UserError
|
|
||||||
*/
|
|
||||||
private function domainScope(Snapshot $snapshot, mixed $value): array
|
|
||||||
{
|
|
||||||
$domain = strtolower((string)Input::text($value));
|
|
||||||
if ($domain !== '' && !$this->context->hasAdminRights() && !$this->context->canAccessDomain($domain)) {
|
|
||||||
throw new AccessDenied();
|
|
||||||
}
|
|
||||||
$exists = $domain !== '' && $this->jobs->isDomain($domain);
|
|
||||||
// a deleted domain is created from the snapshot's export: only when the content lists it
|
|
||||||
if (!$exists && !($this->context->hasAdminRights() && $snapshot->mailboxesOf($domain) !== null)) {
|
|
||||||
throw new UserError('domain_not_found', [htmlspecialchars((string)Input::text($value))]);
|
|
||||||
}
|
|
||||||
$this->requireDomain($snapshot, $domain);
|
|
||||||
$listed = $snapshot->mailboxesOf($domain);
|
|
||||||
$mailboxes = ($listed === null) ? $this->jobs->mailboxesOf($domain) : array_map('strval', array_keys($listed));
|
|
||||||
// a deleted domain without mailboxes comes back with its aliases and settings
|
|
||||||
if (empty($mailboxes) && $exists) {
|
|
||||||
throw new UserError('backup_mailboxes_empty');
|
|
||||||
}
|
|
||||||
return [$domain, $mailboxes];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array{0: string, 1: string[]} the domain of the mailboxes and the mailboxes
|
|
||||||
* @throws UserError
|
|
||||||
*/
|
|
||||||
private function mailboxScope(Snapshot $snapshot, mixed $value, mixed $domainValue): array
|
|
||||||
{
|
|
||||||
$mailboxes = array_values(array_unique(array_map('strtolower', self::values($value))));
|
|
||||||
if (empty($mailboxes)) {
|
|
||||||
throw new UserError('backup_mailboxes_empty');
|
|
||||||
}
|
|
||||||
if (count($mailboxes) > self::MAX_MAILBOXES) {
|
|
||||||
throw new UserError('backup_entries_exceeded', [self::MAX_MAILBOXES]);
|
|
||||||
}
|
|
||||||
$domain = strtolower((string)Input::text($domainValue));
|
|
||||||
foreach ($mailboxes as $username) {
|
|
||||||
if (str_contains($username, '@') && !$this->context->hasAdminRights() && !$this->context->canAccessDomain(self::domainOf($username))) {
|
|
||||||
throw new AccessDenied();
|
|
||||||
}
|
|
||||||
// a deleted mailbox can be restored when the snapshot knows it
|
|
||||||
$mailboxDomain = $this->jobs->mailboxDomain($username) ?? (isset($snapshot->mailboxesOf(self::domainOf($username))[$username]) ? self::domainOf($username) : null);
|
|
||||||
if ($mailboxDomain === null) {
|
|
||||||
throw new UserError('backup_mailbox_not_found', [htmlspecialchars($username)]);
|
|
||||||
}
|
|
||||||
if ($domain !== '' && $mailboxDomain !== $domain) {
|
|
||||||
throw new UserError('backup_mailboxes_one_domain');
|
|
||||||
}
|
|
||||||
$domain = $mailboxDomain;
|
|
||||||
}
|
|
||||||
if (!$this->jobs->isDomain($domain) && !$this->context->hasAdminRights()) {
|
|
||||||
throw new AccessDenied();
|
|
||||||
}
|
|
||||||
$this->requireDomain($snapshot, $domain);
|
|
||||||
return [$domain, $mailboxes];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @throws UserError the snapshot does not hold the domain, or the caller may not restore into it */
|
|
||||||
private function requireDomain(Snapshot $snapshot, string $domain): void
|
|
||||||
{
|
|
||||||
if (!$this->context->hasAdminRights() && !$this->context->canAccessDomain($domain)) {
|
|
||||||
throw new AccessDenied();
|
|
||||||
}
|
|
||||||
if ($snapshot->knowsDomains() && !$snapshot->hasDomain($domain)) {
|
|
||||||
throw new UserError('backup_domain_not_in_snapshot', [htmlspecialchars($domain)]);
|
|
||||||
}
|
|
||||||
if (!$this->mayUse($snapshot, $domain)) {
|
|
||||||
throw new UserError('backup_snapshot_not_found', [htmlspecialchars($snapshot->shortId)]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string[] the requested components in their order
|
|
||||||
* @throws UserError
|
|
||||||
*/
|
|
||||||
private function components(mixed $value): array
|
|
||||||
{
|
|
||||||
$requested = array_values(array_unique(self::values($value)));
|
|
||||||
if (empty($requested)) {
|
|
||||||
throw new UserError('backup_components_invalid', ['']);
|
|
||||||
}
|
|
||||||
foreach ($requested as $component) {
|
|
||||||
if (!in_array($component, self::COMPONENTS, true)) {
|
|
||||||
throw new UserError('backup_components_invalid', [htmlspecialchars($component)]);
|
|
||||||
}
|
|
||||||
if (!$this->context->hasAdminRights() && isset(self::COMPONENT_ACL[$component]) && !$this->context->hasAcl(self::COMPONENT_ACL[$component])) {
|
|
||||||
throw new UserError('backup_component_denied', [$component]);
|
|
||||||
}
|
|
||||||
if (isset(self::REQUIRES[$component]) && !in_array(self::REQUIRES[$component], $requested, true)) {
|
|
||||||
throw new UserError('backup_component_requires', [$component, self::REQUIRES[$component]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return array_values(array_intersect(self::COMPONENTS, $requested));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function domainOf(string $address): string
|
|
||||||
{
|
|
||||||
return strtolower(substr((string)strrchr($address, '@'), 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Values of a list field: an array (multi-select, JSON) or a string separated by line breaks, commas or spaces.
|
|
||||||
*
|
|
||||||
* @return string[]
|
|
||||||
*/
|
|
||||||
private static function values(mixed $value): array
|
|
||||||
{
|
|
||||||
$values = is_array($value) ? $value : preg_split('/[\s,;]+/', (string)Input::text($value));
|
|
||||||
return array_values(array_filter(array_map(fn(mixed $v): string => is_scalar($v) ? trim((string)$v) : '', $values), 'strlen'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,327 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\Backup\Service;
|
|
||||||
|
|
||||||
use Mailcow\Backup\Model\Settings;
|
|
||||||
use Mailcow\Backup\Repository\SettingsRepository;
|
|
||||||
use Mailcow\Core\Context;
|
|
||||||
use Mailcow\Core\Input;
|
|
||||||
use Mailcow\Core\UserError;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup settings: admins read them (also read-only API keys) and save them. Values are checked strictly (at least as
|
|
||||||
* strictly as the runner), since the runner passes them to restic and ssh. Secrets are write-only and stored sealed
|
|
||||||
* with the public key the runner published (only the runner can decrypt them); an empty secret keeps the stored one,
|
|
||||||
* and without a published key no secret can be saved. A changed target, connection or repository password resets
|
|
||||||
* state_initialized (the runner tests the repository again); a changed SFTP host or port drops the known host key. A
|
|
||||||
* new repository password needs a new confirmation that it is stored outside mailcow. A change of a configured target,
|
|
||||||
* its connection or the notify addresses leaves state_target_changed for the runner, which mails the notify addresses
|
|
||||||
* of before the change.
|
|
||||||
*/
|
|
||||||
class SettingsService
|
|
||||||
{
|
|
||||||
private const FLAGS = ['enabled', 's3_path_style', 'password_confirmed', 'include_index', 'include_fts'];
|
|
||||||
/** Integer settings: name => [min, max] */
|
|
||||||
private const RANGES = [
|
|
||||||
'sftp_port' => [1, 65535],
|
|
||||||
'keep_hourly' => [0, 1000],
|
|
||||||
'keep_daily' => [0, 1000],
|
|
||||||
'keep_weekly' => [0, 1000],
|
|
||||||
'keep_monthly' => [0, 1000],
|
|
||||||
'keep_yearly' => [0, 1000],
|
|
||||||
'prune_weekday' => [0, 6],
|
|
||||||
'check_subset' => [0, 365],
|
|
||||||
'limit_upload' => [0, 100000000],
|
|
||||||
];
|
|
||||||
private const MIN_PASSWORD = 8;
|
|
||||||
private const MAX_LENGTH = 255;
|
|
||||||
private const MAX_NOTIFY = 20;
|
|
||||||
/** Entries of the schedule */
|
|
||||||
private const MAX_SCHEDULE = 24;
|
|
||||||
private const HOSTNAME = '/^(?=.{1,253}$)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i';
|
|
||||||
|
|
||||||
public function __construct(private SettingsRepository $repo, private Context $context)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The settings for admins, null for everybody else */
|
|
||||||
public function get(): ?Settings
|
|
||||||
{
|
|
||||||
return $this->context->hasAdminRights() ? $this->load() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The settings without permission check (status, job rules) */
|
|
||||||
public function load(): Settings
|
|
||||||
{
|
|
||||||
return Settings::fromStored($this->repo->load());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the given settings; missing ones keep their value, unknown and runner-owned ones are ignored.
|
|
||||||
*
|
|
||||||
* @throws UserError
|
|
||||||
*/
|
|
||||||
public function save(array $input): Settings
|
|
||||||
{
|
|
||||||
$this->context->requireAdmin();
|
|
||||||
$current = $this->load();
|
|
||||||
$values = $current->values;
|
|
||||||
foreach ($input as $name => $value) {
|
|
||||||
if (!is_string($name) || !array_key_exists($name, Settings::DEFAULTS) || in_array($name, Settings::RUNNER, true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$value = $this->normalize($name, $value);
|
|
||||||
if (in_array($name, Settings::SECRETS, true)) {
|
|
||||||
if ($value === '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$value = self::seal($value, $current->get('state_secrets_pubkey'));
|
|
||||||
}
|
|
||||||
$values[$name] = $value;
|
|
||||||
}
|
|
||||||
if ($values['password'] !== $current->get('password')
|
|
||||||
&& !(array_key_exists('password_confirmed', $input) && $values['password_confirmed'] === '1')) {
|
|
||||||
$values['password_confirmed'] = '0';
|
|
||||||
}
|
|
||||||
foreach (array_merge(['target', 'password'], Settings::CONNECTION[$values['target']]) as $name) {
|
|
||||||
if ($values[$name] !== $current->get($name)) {
|
|
||||||
$values['state_initialized'] = '0';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($values['sftp_host'] !== $current->get('sftp_host') || $values['sftp_port'] !== $current->get('sftp_port')) {
|
|
||||||
$values['sftp_host_key'] = '';
|
|
||||||
}
|
|
||||||
$settings = new Settings($values);
|
|
||||||
if ($values['enabled'] === '1' && !$settings->configured()) {
|
|
||||||
throw new UserError('backup_not_configured');
|
|
||||||
}
|
|
||||||
// switched on without a time
|
|
||||||
if ($values['enabled'] === '1' && empty($settings->scheduleSlots())) {
|
|
||||||
throw self::invalid('schedule');
|
|
||||||
}
|
|
||||||
$values['state_target_changed'] = $this->targetChange($current, $values);
|
|
||||||
$settings = new Settings($values);
|
|
||||||
$changed = array_diff_assoc($values, $current->values);
|
|
||||||
if (!empty($changed)) {
|
|
||||||
$this->repo->save($changed);
|
|
||||||
}
|
|
||||||
return $settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* state_target_changed after a save: when the target, its connection or the notify addresses change, what the target
|
|
||||||
* was, whom to tell (the notify addresses before the change) and what changed ("target", "notify_email"). An earlier
|
|
||||||
* change the runner has not mailed yet keeps its state and addresses and only gets the new kinds of change, so new
|
|
||||||
* addresses followed by a new target still go to the original addresses. Nothing is recorded for a target that was
|
|
||||||
* not configured or without notify addresses.
|
|
||||||
*/
|
|
||||||
private function targetChange(Settings $current, array $values): string
|
|
||||||
{
|
|
||||||
$pending = json_decode($current->get('state_target_changed'), true);
|
|
||||||
if (!$current->configured() && !is_array($pending)) {
|
|
||||||
return $current->get('state_target_changed');
|
|
||||||
}
|
|
||||||
$old = $current->get('target');
|
|
||||||
$watched = array_unique(array_merge(['target'], Settings::CONNECTION[$old] ?? [], Settings::CONNECTION[$values['target']] ?? []));
|
|
||||||
$changed = [];
|
|
||||||
if (!empty(array_filter($watched, fn(string $name): bool => $values[$name] !== $current->get($name)))) {
|
|
||||||
$changed[] = 'target';
|
|
||||||
}
|
|
||||||
if ($values['notify_email'] !== $current->get('notify_email')) {
|
|
||||||
$changed[] = 'notify_email';
|
|
||||||
}
|
|
||||||
if (is_array($pending)) {
|
|
||||||
$pending['changed'] = array_values(array_unique(array_merge((array)($pending['changed'] ?? ['target']), $changed)));
|
|
||||||
return (string)json_encode($pending, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
||||||
}
|
|
||||||
$addresses = array_values(array_filter(explode(',', $current->get('notify_email')), 'strlen'));
|
|
||||||
if (empty($changed) || empty($addresses)) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
$summary = ['target' => $old];
|
|
||||||
foreach (array_diff(Settings::CONNECTION[$old] ?? [], Settings::SECRETS) as $name) {
|
|
||||||
$summary[$name] = $current->get($name);
|
|
||||||
}
|
|
||||||
return (string)json_encode(['old_target' => $summary, 'old_addresses' => $addresses, 'changed' => $changed,
|
|
||||||
'changed_by' => (string)$this->context->username, 'time' => time()], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A secret sealed for the runner: sealed:v1:<base64 of sodium_crypto_box_seal>
|
|
||||||
*
|
|
||||||
* @throws UserError backup_secrets_key_missing while the runner has not published its key
|
|
||||||
*/
|
|
||||||
private static function seal(string $secret, string $publicKey): string
|
|
||||||
{
|
|
||||||
$key = base64_decode($publicKey, true);
|
|
||||||
if ($key === false || strlen($key) !== SODIUM_CRYPTO_BOX_PUBLICKEYBYTES) {
|
|
||||||
throw new UserError('backup_secrets_key_missing');
|
|
||||||
}
|
|
||||||
return Settings::SEALED . base64_encode(sodium_crypto_box_seal($secret, $key));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The value to store, '' for an empty optional value.
|
|
||||||
*
|
|
||||||
* @throws UserError backup_setting_invalid, backup_password_too_short
|
|
||||||
*/
|
|
||||||
private function normalize(string $name, mixed $value): string
|
|
||||||
{
|
|
||||||
if (in_array($name, self::FLAGS, true)) {
|
|
||||||
return self::flag($value) ?? throw self::invalid($name);
|
|
||||||
}
|
|
||||||
if ($name === 'notify_email' && is_string($value)) {
|
|
||||||
// one address per line is fine as well
|
|
||||||
$value = preg_replace('/[\r\n\t]+/', ',', $value);
|
|
||||||
}
|
|
||||||
if ($name === 'schedule') {
|
|
||||||
return self::schedule($value) ?? throw self::invalid($name);
|
|
||||||
}
|
|
||||||
// secrets as typed: spaces at the ends are part of a password
|
|
||||||
$text = (in_array($name, Settings::SECRETS, true) && is_string($value)) ? $value : Input::text($value);
|
|
||||||
if ($text === null || mb_strlen($text) > (($name === 'notify_email') ? self::MAX_LENGTH * self::MAX_NOTIFY : self::MAX_LENGTH) || preg_match('/[\x00-\x1f\x7f]/', $text) || !mb_check_encoding($text, 'UTF-8')) {
|
|
||||||
throw self::invalid($name);
|
|
||||||
}
|
|
||||||
// no scheduled prune (append-only or object-lock targets); a prune job still works
|
|
||||||
if ($name === 'prune_weekday' && $text === '') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
if (isset(self::RANGES[$name])) {
|
|
||||||
$int = Input::intInRange($text, ...self::RANGES[$name]);
|
|
||||||
return ($int === null) ? throw self::invalid($name) : (string)$int;
|
|
||||||
}
|
|
||||||
$valid = match ($name) {
|
|
||||||
'target' => in_array($text, Settings::TARGETS, true),
|
|
||||||
// like the runner: scheme, host and port only
|
|
||||||
's3_endpoint' => self::url($text) && in_array(parse_url($text, PHP_URL_PATH) ?? '', ['', '/'], true),
|
|
||||||
'rest_url' => $text === '' || self::url($text),
|
|
||||||
's3_bucket' => $text === '' || preg_match('/^[a-z0-9][a-z0-9._-]{1,62}$/i', $text),
|
|
||||||
's3_path' => $text === '' || self::path(trim($text, '/'), false),
|
|
||||||
's3_region' => $text === '' || preg_match('/^[a-z0-9-]{1,64}$/i', $text),
|
|
||||||
's3_access_key' => preg_match('/^[\x21-\x7e]*$/', $text),
|
|
||||||
's3_secret_key', 'rest_password' => true,
|
|
||||||
'sftp_host' => $text === '' || preg_match(self::HOSTNAME, $text) || filter_var($text, FILTER_VALIDATE_IP),
|
|
||||||
// the runner's character set (ssh -l)
|
|
||||||
'sftp_user' => $text === '' || preg_match('/^[a-z0-9_][a-z0-9._-]{0,63}$/i', $text),
|
|
||||||
'sftp_path' => $text === '' || self::path($text, true),
|
|
||||||
'rest_user' => $text === '' || preg_match('/^[a-z0-9][a-z0-9._-]{0,63}$/i', $text),
|
|
||||||
// the runner's character set, below the repository folder
|
|
||||||
'local_path' => preg_match('~^[a-z0-9._-]+(/[a-z0-9._-]+)*$~i', $text) && self::path($text, false),
|
|
||||||
'password' => true,
|
|
||||||
'max_unused' => preg_match('/^(\d{1,3})\s*%?$/', $text, $percent) && (int)$percent[1] <= 100,
|
|
||||||
'notify_email' => true,
|
|
||||||
default => false,
|
|
||||||
};
|
|
||||||
if (!$valid) {
|
|
||||||
throw self::invalid($name);
|
|
||||||
}
|
|
||||||
return match ($name) {
|
|
||||||
's3_path' => trim($text, '/'),
|
|
||||||
's3_endpoint' => rtrim($text, '/'),
|
|
||||||
'sftp_path' => rtrim($text, '/'),
|
|
||||||
'password' => self::password($text),
|
|
||||||
'max_unused' => (int)$text . '%',
|
|
||||||
'notify_email' => self::emails($text),
|
|
||||||
default => $text,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The schedule as stored: a list (or its JSON) of {days: weekdays 0 (Sunday) to 6, time: "H:MM"}, up to MAX_SCHEDULE
|
|
||||||
* entries, each with at least one weekday; days sorted, times as HH:MM, repeated entries dropped. Null when invalid.
|
|
||||||
*/
|
|
||||||
private static function schedule(mixed $value): ?string
|
|
||||||
{
|
|
||||||
if (is_string($value)) {
|
|
||||||
$value = json_decode($value, true);
|
|
||||||
}
|
|
||||||
if (!is_array($value) || !array_is_list($value) || count($value) > self::MAX_SCHEDULE) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$entries = [];
|
|
||||||
foreach ($value as $entry) {
|
|
||||||
$days = $entry['days'] ?? null;
|
|
||||||
$time = is_array($entry) && is_string($entry['time'] ?? null) ? $entry['time'] : '';
|
|
||||||
if (!is_array($days) || empty($days) || !preg_match('/^([01]?\d|2[0-3]):([0-5]\d)$/', $time, $match)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
foreach ($days as $day) {
|
|
||||||
if (Input::intInRange($day, 0, 6) === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$days = array_values(array_unique(array_map('intval', $days)));
|
|
||||||
sort($days);
|
|
||||||
$entries[] = ['days' => $days, 'time' => sprintf('%02d:%02d', $match[1], $match[2])];
|
|
||||||
}
|
|
||||||
return (string)json_encode(array_values(array_unique($entries, SORT_REGULAR)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** http(s) URL without credentials, query or fragment (rest_user/rest_password hold the credentials) */
|
|
||||||
private static function url(string $url): bool
|
|
||||||
{
|
|
||||||
$parts = parse_url($url);
|
|
||||||
return filter_var($url, FILTER_VALIDATE_URL) !== false && is_array($parts) && in_array(strtolower($parts['scheme'] ?? ''), ['http', 'https'], true)
|
|
||||||
&& !isset($parts['user']) && !isset($parts['pass']) && !isset($parts['query']) && !isset($parts['fragment'])
|
|
||||||
&& (preg_match(self::HOSTNAME, (string)($parts['host'] ?? '')) || filter_var(trim((string)($parts['host'] ?? ''), '[]'), FILTER_VALIDATE_IP));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A path of plain names separated by /, without . and .. parts; a leading / only where $absolute is allowed */
|
|
||||||
private static function path(string $path, bool $absolute): bool
|
|
||||||
{
|
|
||||||
if ($path === '' || (str_starts_with($path, '/') && !$absolute)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
foreach (explode('/', trim($path, '/')) as $part) {
|
|
||||||
if ($part === '.' || $part === '..' || !preg_match('/^[a-z0-9_~+@][a-z0-9._~+@-]*$/i', $part)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @throws UserError backup_password_too_short ('' keeps the stored password) */
|
|
||||||
private static function password(string $password): string
|
|
||||||
{
|
|
||||||
if ($password !== '' && mb_strlen($password) < self::MIN_PASSWORD) {
|
|
||||||
throw new UserError('backup_password_too_short', [self::MIN_PASSWORD]);
|
|
||||||
}
|
|
||||||
return $password;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @throws UserError for an invalid address or too many */
|
|
||||||
private static function emails(string $text): string
|
|
||||||
{
|
|
||||||
$emails = array_values(array_unique(array_filter(preg_split('/[\s,;]+/', $text), 'strlen')));
|
|
||||||
if (count($emails) > self::MAX_NOTIFY) {
|
|
||||||
throw self::invalid('notify_email');
|
|
||||||
}
|
|
||||||
foreach ($emails as $email) {
|
|
||||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
||||||
throw self::invalid('notify_email');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return implode(',', $emails);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Checkbox or API flag as '0'/'1'; a hidden "0" + checkbox pair arrives as ["0", "1"] when checked; null = invalid */
|
|
||||||
private static function flag(mixed $value): ?string
|
|
||||||
{
|
|
||||||
if (is_array($value)) {
|
|
||||||
$value = end($value);
|
|
||||||
}
|
|
||||||
if (!is_scalar($value) && $value !== null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$flag = filter_var($value ?? false, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
||||||
return ($flag === null) ? null : (string)(int)$flag;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function invalid(string $name): UserError
|
|
||||||
{
|
|
||||||
return new UserError('backup_setting_invalid', [$name]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\MailPolicy\Controller;
|
|
||||||
|
|
||||||
use Mailcow\Core\AccessDenied;
|
|
||||||
use Mailcow\Core\ApiResponse;
|
|
||||||
use Mailcow\Core\UserError;
|
|
||||||
use Mailcow\MailPolicy\Service\PolicyEngine;
|
|
||||||
use Mailcow\MailPolicy\Service\PolicyService;
|
|
||||||
use PDOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP API adapter for json_api.php and edit.php (routes `mail-policy`), and the answer to the Postfix policy
|
|
||||||
* service (data/conf/rspamd/dynmaps/mailpolicy.php). Messages are logged as ['mail_policy', action, 'policy', data].
|
|
||||||
*/
|
|
||||||
class MailPolicyApiController
|
|
||||||
{
|
|
||||||
public function __construct(private PolicyService $policies, private PolicyEngine $engine, private ApiResponse $response)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/** POST add/mail-policy */
|
|
||||||
public function addPolicy(array $attr): bool
|
|
||||||
{
|
|
||||||
return $this->handle('add', $attr, function (array $log) use ($attr): bool {
|
|
||||||
$policy = $this->policies->create($attr);
|
|
||||||
return $this->response->success(['mail_policy_added', htmlspecialchars($policy->name)], $log);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** POST edit/mail-policy: one message per id */
|
|
||||||
public function editPolicies(?array $items, array $attr): bool
|
|
||||||
{
|
|
||||||
$data = array_merge(['id' => $items], $attr);
|
|
||||||
return $this->handle('edit', $data, function (array $log) use ($data, $attr): bool {
|
|
||||||
$this->policies->requireEditor();
|
|
||||||
return $this->batch((array)($data['id'] ?? []), $log,
|
|
||||||
fn(int $id): array => ['mail_policy_modified', htmlspecialchars($this->policies->update($id, $attr)->name)]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** POST delete/mail-policy: one message per id */
|
|
||||||
public function deletePolicies(array $items): bool
|
|
||||||
{
|
|
||||||
return $this->handle('delete', ['id' => $items], function (array $log) use ($items): bool {
|
|
||||||
$this->policies->requireEditor();
|
|
||||||
return $this->batch($items, $log, fn(int $id): array => ['mail_policy_removed', htmlspecialchars($this->policies->delete($id)->name)]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET mail-policy/all[/<domain,domain>] */
|
|
||||||
public function getPolicies(?string $domains = null): array
|
|
||||||
{
|
|
||||||
$list = array_values(array_filter(array_map('trim', explode(',', (string)$domains)), 'strlen'));
|
|
||||||
return array_values(array_filter(array_map(fn(int $id): array => $this->getPolicy($id), $this->policies->visibleIds($list))));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET mail-policy/<id>, also edit.php */
|
|
||||||
public function getPolicy(mixed $id): array
|
|
||||||
{
|
|
||||||
$id = $this->toId($id);
|
|
||||||
$policy = ($id === null) ? null : $this->policies->get($id);
|
|
||||||
return ($policy === null) ? [] : $this->policies->toArray($policy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** GET mail-policy/test/<sender>/<recipient>[/login]: would the mail be accepted? Sender "-" = null sender; [] when not allowed to ask */
|
|
||||||
public function test(string $sender, string $recipient, bool $loggedIn): array
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
return $this->policies->test(($sender === '-') ? '' : $sender, $recipient, $loggedIn);
|
|
||||||
} catch (UserError) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** functions.mailbox.inc.php: a mailbox or alias was renamed */
|
|
||||||
public function addressRenamed(string $old, string $new): void
|
|
||||||
{
|
|
||||||
$this->policies->addressRenamed($old, $new);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Answer for the Postfix policy service: DUNNO or the rejection */
|
|
||||||
public function check(string $sender, string $recipient, string $login, int $port, string $helo = ''): string
|
|
||||||
{
|
|
||||||
return $this->engine->check($sender, $recipient, $login, $port, $helo)->action();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs one write call: $work gets the log entry and returns the result for process_*_return().
|
|
||||||
* A UserError becomes a danger message, a database error stops the call with one `mysql_error` message.
|
|
||||||
*/
|
|
||||||
private function handle(string $action, mixed $data, callable $work): bool
|
|
||||||
{
|
|
||||||
$log = ['mail_policy', $action, 'policy', $data];
|
|
||||||
try {
|
|
||||||
$result = $work($log);
|
|
||||||
} catch (UserError $e) {
|
|
||||||
$result = $this->response->error($e, $log);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
$result = $this->response->danger(['mysql_error', $e->getMessage()], $log);
|
|
||||||
}
|
|
||||||
$this->response->flush();
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** One message per id: the success message returned by $work(id), or the UserError */
|
|
||||||
private function batch(array $ids, array $log, callable $work): bool
|
|
||||||
{
|
|
||||||
foreach ($ids as $id) {
|
|
||||||
try {
|
|
||||||
$this->response->success($work($this->requireId($id)), $log);
|
|
||||||
} catch (UserError $e) {
|
|
||||||
$this->response->error($e, $log);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Integer id from request input ("12", 12); null for anything else */
|
|
||||||
private function toId(mixed $id): ?int
|
|
||||||
{
|
|
||||||
return (is_numeric($id) && (float)$id == (int)(float)$id) ? (int)(float)$id : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @throws AccessDenied for ids that cannot exist */
|
|
||||||
private function requireId(mixed $id): int
|
|
||||||
{
|
|
||||||
return $this->toId($id) ?? throw new AccessDenied();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\MailPolicy;
|
|
||||||
|
|
||||||
use Mailcow\Core\ApiResponse;
|
|
||||||
use Mailcow\Core\Context;
|
|
||||||
use Mailcow\MailPolicy\Controller\MailPolicyApiController;
|
|
||||||
use Mailcow\MailPolicy\Repository\LookupRepository;
|
|
||||||
use Mailcow\MailPolicy\Repository\PolicyRepository;
|
|
||||||
use Mailcow\MailPolicy\Service\PolicyEngine;
|
|
||||||
use Mailcow\MailPolicy\Service\PolicyService;
|
|
||||||
use PDO;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Composition root of the mail policies: whom mailboxes and groups may send to and receive from, checked by
|
|
||||||
* Postfix at RCPT TO through the policy service (mailcow-policy in master.cf, data/conf/rspamd/dynmaps/mailpolicy.php).
|
|
||||||
*
|
|
||||||
* process_get_return(MailPolicyModule::fromGlobals()->api->getPolicies());
|
|
||||||
* echo MailPolicyModule::create($pdo, Context::system())->api->check($sender, $recipient, $login, $port); // policy service
|
|
||||||
*/
|
|
||||||
class MailPolicyModule
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public readonly PolicyService $policies,
|
|
||||||
public readonly PolicyEngine $engine,
|
|
||||||
public readonly MailPolicyApiController $api
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function create(PDO $pdo, Context $context): self
|
|
||||||
{
|
|
||||||
$engine = new PolicyEngine(new LookupRepository($pdo));
|
|
||||||
$policies = new PolicyService(new PolicyRepository($pdo), $engine, $context);
|
|
||||||
return new self($policies, $engine, new MailPolicyApiController($policies, $engine, new ApiResponse()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Legacy entry point for procedural callers: reads $pdo and $_SESSION */
|
|
||||||
public static function fromGlobals(?Context $context = null): self
|
|
||||||
{
|
|
||||||
return self::create($GLOBALS['pdo'], $context ?? Context::fromSession($_SESSION ?? []));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\MailPolicy\Model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Result of the policy check for one recipient: allowed, refused by the send or receive part of a policy, or refused
|
|
||||||
* because the recipient is written in a form the check cannot follow (routing through another address).
|
|
||||||
*/
|
|
||||||
class Decision
|
|
||||||
{
|
|
||||||
private const TEXT = [
|
|
||||||
'send' => 'Sending to this recipient is not allowed by the mail policy of the sender',
|
|
||||||
'receive' => 'This recipient does not accept mail from this sender',
|
|
||||||
'address' => 'This form of the recipient address is not allowed by the mail policy',
|
|
||||||
];
|
|
||||||
|
|
||||||
private function __construct(public readonly bool $allowed, public readonly string $side = '', public readonly ?Policy $policy = null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function allow(): self
|
|
||||||
{
|
|
||||||
return new self(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** $side send or receive; without $policy the default text is used */
|
|
||||||
public static function deny(string $side, ?Policy $policy = null): self
|
|
||||||
{
|
|
||||||
return new self(false, $side, $policy);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The recipient routes through another address (user%domain, domain!user, a second @) */
|
|
||||||
public static function routedAddress(): self
|
|
||||||
{
|
|
||||||
return new self(false, 'address');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Text of the rejection: the text of the policy or the default */
|
|
||||||
public function text(): string
|
|
||||||
{
|
|
||||||
return ($this->policy !== null && $this->policy->rejectText !== '') ? $this->policy->rejectText : self::defaultText($this->side);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function defaultText(string $side): string
|
|
||||||
{
|
|
||||||
return self::TEXT[$side] ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Answer for the Postfix policy protocol */
|
|
||||||
public function action(): string
|
|
||||||
{
|
|
||||||
return $this->allowed ? 'DUNNO' : '550 5.7.1 ' . $this->text();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\MailPolicy\Model;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A mail policy (table `mail_policy`): whom its mailboxes and groups may send to and receive from.
|
|
||||||
*
|
|
||||||
* Each direction has a mode: `all` (only the blocked entries are refused), `internal` (the own domain and its
|
|
||||||
* alias domains, plus the allowed entries) or `list` (only the allowed entries). Blocked entries are always
|
|
||||||
* refused. Entries are addresses, domains or `group:<id>`. A sender counts as internal only when it is logged in
|
|
||||||
* to this mailcow (SASL) with a mailbox of the domain, never because of its address.
|
|
||||||
*/
|
|
||||||
class Policy
|
|
||||||
{
|
|
||||||
public const MODES = ['all', 'internal', 'list'];
|
|
||||||
|
|
||||||
public ?int $id = null;
|
|
||||||
public string $domain = '';
|
|
||||||
public string $name = '';
|
|
||||||
public string $description = '';
|
|
||||||
public string $sendMode = 'all';
|
|
||||||
/** @var string[] */
|
|
||||||
public array $sendAllow = [];
|
|
||||||
/** @var string[] */
|
|
||||||
public array $sendBlock = [];
|
|
||||||
public string $receiveMode = 'all';
|
|
||||||
/** @var string[] */
|
|
||||||
public array $receiveAllow = [];
|
|
||||||
/** @var string[] */
|
|
||||||
public array $receiveBlock = [];
|
|
||||||
/** Text of the rejection, '' = default text */
|
|
||||||
public string $rejectText = '';
|
|
||||||
public int $active = 1;
|
|
||||||
/** @var string[] usernames the policy is assigned to */
|
|
||||||
public array $mailboxes = [];
|
|
||||||
/** @var int[] groups the policy is assigned to: their members and their address */
|
|
||||||
public array $groups = [];
|
|
||||||
public ?string $created = null;
|
|
||||||
public ?string $modified = null;
|
|
||||||
|
|
||||||
/** Row of `mail_policy`; assignments are set by the repository */
|
|
||||||
public static function fromRow(array $row): self
|
|
||||||
{
|
|
||||||
$policy = new self();
|
|
||||||
$policy->id = (int)$row['id'];
|
|
||||||
$policy->domain = (string)$row['domain'];
|
|
||||||
$policy->name = (string)$row['name'];
|
|
||||||
$policy->description = (string)($row['description'] ?? '');
|
|
||||||
$policy->sendMode = (string)$row['send_mode'];
|
|
||||||
$policy->sendAllow = self::list($row['send_allow'] ?? null);
|
|
||||||
$policy->sendBlock = self::list($row['send_block'] ?? null);
|
|
||||||
$policy->receiveMode = (string)$row['receive_mode'];
|
|
||||||
$policy->receiveAllow = self::list($row['receive_allow'] ?? null);
|
|
||||||
$policy->receiveBlock = self::list($row['receive_block'] ?? null);
|
|
||||||
$policy->rejectText = (string)($row['reject_text'] ?? '');
|
|
||||||
$policy->active = (int)$row['active'];
|
|
||||||
$policy->created = $row['created'] ?? null;
|
|
||||||
$policy->modified = $row['modified'] ?? null;
|
|
||||||
return $policy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Column values for `mail_policy` */
|
|
||||||
public function toRow(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'domain' => $this->domain,
|
|
||||||
'name' => $this->name,
|
|
||||||
'description' => $this->description,
|
|
||||||
'send_mode' => $this->sendMode,
|
|
||||||
'send_allow' => json_encode($this->sendAllow),
|
|
||||||
'send_block' => json_encode($this->sendBlock),
|
|
||||||
'receive_mode' => $this->receiveMode,
|
|
||||||
'receive_allow' => json_encode($this->receiveAllow),
|
|
||||||
'receive_block' => json_encode($this->receiveBlock),
|
|
||||||
'reject_text' => $this->rejectText,
|
|
||||||
'active' => $this->active,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Entry of the API and the edit page */
|
|
||||||
public function toArray(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'id' => $this->id,
|
|
||||||
'domain' => $this->domain,
|
|
||||||
'name' => $this->name,
|
|
||||||
'description' => $this->description,
|
|
||||||
'send_mode' => $this->sendMode,
|
|
||||||
'send_allow' => $this->sendAllow,
|
|
||||||
'send_block' => $this->sendBlock,
|
|
||||||
'receive_mode' => $this->receiveMode,
|
|
||||||
'receive_allow' => $this->receiveAllow,
|
|
||||||
'receive_block' => $this->receiveBlock,
|
|
||||||
'reject_text' => $this->rejectText,
|
|
||||||
'active' => $this->active,
|
|
||||||
'mailboxes' => $this->mailboxes,
|
|
||||||
'groups' => $this->groups,
|
|
||||||
'created' => $this->created,
|
|
||||||
'modified' => $this->modified,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return string[] entries of a JSON list column */
|
|
||||||
private static function list(mixed $json): array
|
|
||||||
{
|
|
||||||
$list = is_string($json) ? json_decode($json, true) : null;
|
|
||||||
return is_array($list) ? array_values(array_filter($list, 'is_string')) : [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\MailPolicy\Repository;
|
|
||||||
|
|
||||||
use Mailcow\Core\Repository;
|
|
||||||
use Mailcow\MailPolicy\Model\Policy;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read-only lookups of the policy check: mailboxes, aliases, alias domains, groups and the active policies of a
|
|
||||||
* mailbox or group. Addresses are passed in lower case.
|
|
||||||
*/
|
|
||||||
class LookupRepository extends Repository
|
|
||||||
{
|
|
||||||
/** @return array{username: string, domain: string}|null a regular mailbox that is not disabled */
|
|
||||||
public function mailbox(string $username): ?array
|
|
||||||
{
|
|
||||||
$row = $this->query("SELECT `username`, `domain` FROM `mailbox` WHERE `username` = :username AND `kind` = '' AND `active` != '0'",
|
|
||||||
['username' => $username])->fetch();
|
|
||||||
return is_array($row) ? ['username' => strtolower((string)$row['username']), 'domain' => strtolower((string)$row['domain'])] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Target domain of an active alias domain */
|
|
||||||
public function aliasDomainTarget(string $domain): ?string
|
|
||||||
{
|
|
||||||
$target = $this->query("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'",
|
|
||||||
['domain' => $domain])->fetchColumn();
|
|
||||||
return is_string($target) ? strtolower($target) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A domain or alias domain Postfix delivers itself (as mysql_virtual_domains_maps.cf: active, no backup MX) */
|
|
||||||
public function isLocalDomain(string $domain): bool
|
|
||||||
{
|
|
||||||
return (bool)$this->query("SELECT 1 FROM `alias_domain` WHERE `alias_domain` = :d1 AND `active` = '1'
|
|
||||||
UNION SELECT 1 FROM `domain` WHERE `domain` = :d2 AND `active` = '1' AND `backupmx` = '0'",
|
|
||||||
['d1' => $domain, 'd2' => $domain])->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stored spelling of a mailbox, alias or temporary alias address. MySQL compares without case and accents, so
|
|
||||||
* Postfix delivers mail for chéf@ to chef@.
|
|
||||||
*/
|
|
||||||
public function storedAddress(string $address): ?string
|
|
||||||
{
|
|
||||||
$stored = $this->query("SELECT `username` FROM `mailbox` WHERE `username` = :a1 UNION SELECT `address` FROM `alias` WHERE `address` = :a2
|
|
||||||
UNION SELECT `address` FROM `spamalias` WHERE `address` = :a3 LIMIT 1", ['a1' => $address, 'a2' => $address, 'a3' => $address])->fetchColumn();
|
|
||||||
return is_string($stored) ? strtolower($stored) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Stored spelling of a domain or alias domain, see storedAddress() */
|
|
||||||
public function storedDomain(string $domain): ?string
|
|
||||||
{
|
|
||||||
$stored = $this->query("SELECT `domain` FROM `domain` WHERE `domain` = :d1 UNION SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain` = :d2 LIMIT 1",
|
|
||||||
['d1' => $domain, 'd2' => $domain])->fetchColumn();
|
|
||||||
return is_string($stored) ? strtolower($stored) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Active group with this distribution address */
|
|
||||||
public function groupByAddress(string $address): ?int
|
|
||||||
{
|
|
||||||
$id = $this->query("SELECT `id` FROM `mailbox_group` WHERE `address` = :address AND `active` = '1'", ['address' => $address])->fetchColumn();
|
|
||||||
return ($id === false) ? null : (int)$id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function groupAddress(int $id): ?string
|
|
||||||
{
|
|
||||||
$address = $this->query("SELECT `address` FROM `mailbox_group` WHERE `id` = :id AND `active` = '1'", ['id' => $id])->fetchColumn();
|
|
||||||
return is_string($address) && $address !== '' ? strtolower($address) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Member of an active group */
|
|
||||||
public function isGroupMember(int $groupId, string $username): bool
|
|
||||||
{
|
|
||||||
return (bool)$this->query("SELECT 1 FROM `mailbox_group_member` `gm` JOIN `mailbox_group` `g` ON `g`.`id` = `gm`.`group_id`
|
|
||||||
WHERE `gm`.`group_id` = :id AND `gm`.`username` = :username AND `g`.`active` = '1'",
|
|
||||||
['id' => $groupId, 'username' => $username])->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* What Postfix' virtual alias maps find for this key, in their order: an alias (also the own alias of a mailbox
|
|
||||||
* and the alias of a group address) or a temporary alias.
|
|
||||||
*
|
|
||||||
* @return array{kind: string, domain?: string, internal?: bool, owner?: string}|null
|
|
||||||
*/
|
|
||||||
public function aliasTarget(string $address): ?array
|
|
||||||
{
|
|
||||||
$alias = $this->query("SELECT `domain`, `internal` FROM `alias` WHERE `address` = :address AND `active` IN ('1', '2')",
|
|
||||||
['address' => $address])->fetch();
|
|
||||||
if (is_array($alias)) {
|
|
||||||
return ['kind' => 'alias', 'domain' => strtolower((string)$alias['domain']), 'internal' => $alias['internal'] == '1'];
|
|
||||||
}
|
|
||||||
$owner = $this->query("SELECT `goto` FROM `spamalias` WHERE `address` = :address AND (`validity` >= UNIX_TIMESTAMP() OR `permanent` != 0)",
|
|
||||||
['address' => $address])->fetchColumn();
|
|
||||||
return is_string($owner) ? ['kind' => 'temporary', 'owner' => strtolower($owner)] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An active catch-all alias of the domain has the internal flag (applies to the whole domain) */
|
|
||||||
public function internalCatchAll(string $domain): bool
|
|
||||||
{
|
|
||||||
return (bool)$this->query("SELECT 1 FROM `alias` WHERE `address` = :address AND `internal` = '1' AND `active` IN ('1', '2')",
|
|
||||||
['address' => '@' . $domain])->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** An active policy with members restricts sending */
|
|
||||||
public function hasSendRules(): bool
|
|
||||||
{
|
|
||||||
return (bool)$this->query("SELECT 1 FROM `mail_policy` `p` JOIN `mail_policy_member` `pm` ON `pm`.`policy_id` = `p`.`id`
|
|
||||||
WHERE `p`.`active` = '1' AND (`p`.`send_mode` != 'all' OR `p`.`send_block` != '[]') LIMIT 1")->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Policy[] active policies of the mailbox: assigned to it or to an active group it is a member of; only
|
|
||||||
* policies of its own domain, so a group with members of other domains cannot restrict them
|
|
||||||
*/
|
|
||||||
public function policiesForMailbox(string $username): array
|
|
||||||
{
|
|
||||||
return $this->policies("SELECT DISTINCT `p`.* FROM `mail_policy` `p`
|
|
||||||
JOIN `mail_policy_member` `pm` ON `pm`.`policy_id` = `p`.`id`
|
|
||||||
JOIN `mailbox` `m` ON `m`.`username` = :u1 AND `m`.`domain` = `p`.`domain`
|
|
||||||
WHERE `p`.`active` = '1' AND (`pm`.`username` = :u2 OR `pm`.`group_id` IN (
|
|
||||||
SELECT `gm`.`group_id` FROM `mailbox_group_member` `gm` JOIN `mailbox_group` `g` ON `g`.`id` = `gm`.`group_id`
|
|
||||||
WHERE `gm`.`username` = :u3 AND `g`.`active` = '1'))
|
|
||||||
ORDER BY `p`.`id`", ['u1' => $username, 'u2' => $username, 'u3' => $username]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return Policy[] active policies assigned to the group */
|
|
||||||
public function policiesForGroup(int $groupId): array
|
|
||||||
{
|
|
||||||
return $this->policies("SELECT DISTINCT `p`.* FROM `mail_policy` `p`
|
|
||||||
JOIN `mail_policy_member` `pm` ON `pm`.`policy_id` = `p`.`id`
|
|
||||||
WHERE `p`.`active` = '1' AND `pm`.`group_id` = :id
|
|
||||||
ORDER BY `p`.`id`", ['id' => $groupId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return Policy[] */
|
|
||||||
private function policies(string $sql, array $params): array
|
|
||||||
{
|
|
||||||
return array_map(fn(array $row): Policy => Policy::fromRow($row), $this->query($sql, $params)->fetchAll());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Mailcow\MailPolicy\Repository;
|
|
||||||
|
|
||||||
use Mailcow\Core\Context;
|
|
||||||
use Mailcow\Core\Repository;
|
|
||||||
use Mailcow\MailPolicy\Model\Policy;
|
|
||||||
use PDO;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tables `mail_policy` and `mail_policy_member` (the mailboxes and groups a policy is assigned to), plus the
|
|
||||||
* lookups the editing needs. Assignments go with their mailbox, group or domain (foreign keys, ON DELETE CASCADE).
|
|
||||||
*/
|
|
||||||
class PolicyRepository extends Repository
|
|
||||||
{
|
|
||||||
public function find(int $id): ?Policy
|
|
||||||
{
|
|
||||||
$row = $this->query("SELECT * FROM `mail_policy` WHERE `id` = :id", ['id' => $id])->fetch();
|
|
||||||
return is_array($row) ? $this->withMembers(Policy::fromRow($row)) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string[] $domains limit to these domains, [] = all visible
|
|
||||||
* @return int[] ids of the policies visible to the caller, ordered by domain and name
|
|
||||||
*/
|
|
||||||
public function visibleIds(Context $context, array $domains = []): array
|
|
||||||
{
|
|
||||||
if ($context->hasAdminRights()) {
|
|
||||||
[$visible, $params] = ['1=1', []];
|
|
||||||
} elseif ($context->isDomainAdmin()) {
|
|
||||||
[$visible, $params] = ["`domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :vis_da)",
|
|
||||||
['vis_da' => $context->username]];
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$sql = "SELECT `id` FROM `mail_policy` WHERE $visible";
|
|
||||||
if (!empty($domains)) {
|
|
||||||
$names = [];
|
|
||||||
foreach (array_values($domains) as $i => $domain) {
|
|
||||||
$names[] = ':dom' . $i;
|
|
||||||
$params['dom' . $i] = $domain;
|
|
||||||
}
|
|
||||||
$sql .= " AND `domain` IN (" . implode(', ', $names) . ")";
|
|
||||||
}
|
|
||||||
return array_map('intval', $this->query($sql . " ORDER BY `domain`, `name`", $params)->fetchAll(PDO::FETCH_COLUMN));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function nameExists(string $domain, string $name, ?int $exceptId = null): bool
|
|
||||||
{
|
|
||||||
return (bool)$this->query("SELECT 1 FROM `mail_policy` WHERE `domain` = :domain AND `name` = :name AND `id` != :id",
|
|
||||||
['domain' => $domain, 'name' => $name, 'id' => $exceptId ?? 0])->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Inserts or updates the policy with its assignments; returns its id */
|
|
||||||
public function save(Policy $policy): int
|
|
||||||
{
|
|
||||||
return $this->transaction(function () use ($policy): int {
|
|
||||||
if ($policy->id === null) {
|
|
||||||
$policy->id = $this->insertRow('mail_policy', $policy->toRow());
|
|
||||||
} else {
|
|
||||||
$this->updateRow('mail_policy', $policy->id, $policy->toRow());
|
|
||||||
$this->query("DELETE FROM `mail_policy_member` WHERE `policy_id` = :id", ['id' => $policy->id]);
|
|
||||||
}
|
|
||||||
foreach ($policy->mailboxes as $username) {
|
|
||||||
$this->insertRow('mail_policy_member', ['policy_id' => $policy->id, 'username' => $username]);
|
|
||||||
}
|
|
||||||
foreach ($policy->groups as $groupId) {
|
|
||||||
$this->insertRow('mail_policy_member', ['policy_id' => $policy->id, 'group_id' => $groupId]);
|
|
||||||
}
|
|
||||||
return $policy->id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete(int $id): void
|
|
||||||
{
|
|
||||||
$this->query("DELETE FROM `mail_policy` WHERE `id` = :id", ['id' => $id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Address entries of all lists follow a renamed mailbox or alias */
|
|
||||||
public function renameAddress(string $old, string $new): void
|
|
||||||
{
|
|
||||||
$this->transaction(function () use ($old, $new): void {
|
|
||||||
$columns = ['send_allow', 'send_block', 'receive_allow', 'receive_block'];
|
|
||||||
foreach ($this->query("SELECT `id`, `" . implode('`, `', $columns) . "` FROM `mail_policy`")->fetchAll() as $row) {
|
|
||||||
$changed = [];
|
|
||||||
foreach ($columns as $column) {
|
|
||||||
$entries = json_decode((string)$row[$column], true);
|
|
||||||
if (is_array($entries) && in_array($old, $entries, true)) {
|
|
||||||
$changed[$column] = json_encode(array_values(array_unique(array_map(fn(string $entry): string => ($entry === $old) ? $new : $entry, $entries))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($changed !== []) {
|
|
||||||
$this->updateRow('mail_policy', (int)$row['id'], $changed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A primary domain (policies never belong to alias domains) */
|
|
||||||
public function isDomain(string $domain): bool
|
|
||||||
{
|
|
||||||
return (bool)$this->query("SELECT 1 FROM `domain` WHERE `domain` = :domain", ['domain' => $domain])->fetchColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Domain of a regular mailbox, null if there is none */
|
|
||||||
public function mailboxDomain(string $username): ?string
|
|
||||||
{
|
|
||||||
$domain = $this->query("SELECT `domain` FROM `mailbox` WHERE `username` = :username AND `kind` = ''", ['username' => $username])->fetchColumn();
|
|
||||||
return is_string($domain) ? $domain : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function groupDomain(int $id): ?string
|
|
||||||
{
|
|
||||||
$domain = $this->query("SELECT `domain` FROM `mailbox_group` WHERE `id` = :id", ['id' => $id])->fetchColumn();
|
|
||||||
return is_string($domain) ? $domain : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array[] id, name, domain, address of the groups in these domains (choices of the forms) */
|
|
||||||
public function groupsIn(array $domains): array
|
|
||||||
{
|
|
||||||
if (empty($domains)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
$names = [];
|
|
||||||
$params = [];
|
|
||||||
foreach (array_values($domains) as $i => $domain) {
|
|
||||||
$names[] = ':dom' . $i;
|
|
||||||
$params['dom' . $i] = $domain;
|
|
||||||
}
|
|
||||||
return $this->query("SELECT `id`, `name`, `domain`, `address` FROM `mailbox_group` WHERE `domain` IN (" . implode(', ', $names) . ")
|
|
||||||
ORDER BY `domain`, `name`", $params)->fetchAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function withMembers(Policy $policy): Policy
|
|
||||||
{
|
|
||||||
foreach ($this->query("SELECT `username`, `group_id` FROM `mail_policy_member` WHERE `policy_id` = :id ORDER BY `username`, `group_id`",
|
|
||||||
['id' => $policy->id])->fetchAll() as $row) {
|
|
||||||
if ($row['username'] !== null) {
|
|
||||||
$policy->mailboxes[] = (string)$row['username'];
|
|
||||||
} elseif ($row['group_id'] !== null) {
|
|
||||||
$policy->groups[] = (int)$row['group_id'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $policy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loaded 100 of 206 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user