mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-11-11 02:41:33 +00:00
Compare commits
139
Commits
2026-10
...
feat/api-docs
| 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 | ||
|
|
329bca1d46 | ||
|
|
6a83b9e0f0 | ||
|
|
3ffecc505e | ||
|
|
e8f12cd061 | ||
|
|
3a4e479424 | ||
|
|
f1e51c0a40 | ||
|
|
ddcba178af | ||
|
|
1752dd6ddf | ||
|
|
adb1c74ce3 | ||
|
|
716f67844d | ||
|
|
a2ab4301e7 | ||
|
|
52f28025a5 | ||
|
|
3c03f8e567 | ||
|
|
72613e7e2b | ||
|
|
9293e4ce43 | ||
|
|
a8d1121f0e | ||
|
|
a4b60ae26f | ||
|
|
e96fb24a67 | ||
|
|
5c762d9949 | ||
|
|
06cbdc4409 | ||
|
|
7c73199bde | ||
|
|
4b7ebab8f3 | ||
|
|
5eb3148bd5 | ||
|
|
41467dc47f | ||
|
|
a1cbd51d70 | ||
|
|
1cb0656924 | ||
|
|
24b91f6089 | ||
|
|
510674ee67 | ||
|
|
a21604cd7b | ||
|
|
4c7e2ec036 | ||
|
|
085d10118d | ||
|
|
1b6fec6a89 | ||
|
|
9eb3a79f3b | ||
|
|
d5e3de3ae8 | ||
|
|
e986803980 | ||
|
|
44429fa0d0 | ||
|
|
bab03ed88b | ||
|
|
0cd71c7a00 | ||
|
|
92ad6c5e6d | ||
|
|
aa7388810c | ||
|
|
4dd7aead73 | ||
|
|
bd8c784118 | ||
|
|
9cc8ac2908 | ||
|
|
002400315d | ||
|
|
8934a84d18 | ||
|
|
5ce88b5795 | ||
|
|
b2b21b1a85 | ||
|
|
4b1b9362f3 | ||
|
|
fb32d8d345 | ||
|
|
af5d0e3d29 | ||
|
|
4884ed0cbf | ||
|
|
8aaf68ffb5 | ||
|
|
503e64fe85 | ||
|
|
ba034568e0 | ||
|
|
b7d24881b1 | ||
|
|
1c7c328a2a | ||
|
|
7457fd59aa | ||
|
|
60177720ed | ||
|
|
15042e5921 | ||
|
|
3959e311a0 | ||
|
|
ad86403b87 | ||
|
|
2c5718c9c9 | ||
|
|
0445f85d7d | ||
|
|
714ca5a3d8 | ||
|
|
5705a48724 | ||
|
|
7d27460fee | ||
|
|
8bc8fbdd2c | ||
|
|
f63042bbaf | ||
|
|
4828f0cf64 | ||
|
|
40488b2120 | ||
|
|
02a9f59530 | ||
|
|
0dadf49934 | ||
|
|
2f76230daf | ||
|
|
c145f858e2 | ||
|
|
82b1cf7198 | ||
|
|
498434ee3d | ||
|
|
f662af4c50 | ||
|
|
8d4895a54d | ||
|
|
81d6b1f547 | ||
|
|
474f1c688b | ||
|
|
7c62a57910 | ||
|
|
e756b7857e | ||
|
|
b2a152fe45 | ||
|
|
8db3cae826 | ||
|
|
d93f32072e | ||
|
|
7b658fa65c | ||
|
|
9f934af3f2 |
No files matched your search
+3
-1
@@ -78,4 +78,6 @@ refresh_images.sh
|
||||
update_diffs/
|
||||
create_cold_standby.sh
|
||||
!data/conf/nginx/mailcow_auth.conf
|
||||
data/conf/postfix/postfix-tlspol
|
||||
data/conf/postfix/postfix-tlspol
|
||||
tests/php/vendor/
|
||||
tests/php/.phpunit.cache/
|
||||
@@ -192,6 +192,7 @@ detect_major_update() {
|
||||
"2025-02"
|
||||
"2025-03"
|
||||
"2025-09"
|
||||
"2026-11"
|
||||
)
|
||||
|
||||
current_version=""
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
|
||||
# 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
|
||||
get_ipv6_support() {
|
||||
# ---- helper: probe external IPv6 connectivity without DNS ----
|
||||
@@ -37,6 +42,9 @@ get_ipv6_support() {
|
||||
if _probe_ipv6_connectivity; then
|
||||
DETECTED_IPV6=true
|
||||
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
|
||||
DETECTED_IPV6=false
|
||||
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
|
||||
}
|
||||
|
||||
_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
|
||||
configure_ipv6() {
|
||||
# detect manual override if mailcow.conf is present
|
||||
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]] && grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
|
||||
MANUAL_SETTING=$(grep '^ENABLE_IPV6=' "$MAILCOW_CONF" | cut -d= -f2)
|
||||
elif [[ -z "$MAILCOW_CONF" ]] && [[ -n "${ENABLE_IPV6:-}" ]]; then
|
||||
@@ -203,19 +233,23 @@ configure_ipv6() {
|
||||
MANUAL_SETTING=""
|
||||
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
|
||||
|
||||
# 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 [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]]; then
|
||||
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
|
||||
_set_enable_ipv6 false
|
||||
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 "IPv6 configuration complete: ENABLE_IPV6=false"
|
||||
@@ -224,16 +258,7 @@ configure_ipv6() {
|
||||
fi
|
||||
|
||||
docker_daemon_edit
|
||||
|
||||
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
|
||||
_set_enable_ipv6 true
|
||||
|
||||
echo "IPv6 configuration complete: ENABLE_IPV6=true"
|
||||
}
|
||||
@@ -60,6 +60,7 @@ adapt_new_options() {
|
||||
"ACME_DNS_CHALLENGE"
|
||||
"ACME_DNS_PROVIDER"
|
||||
"ACME_ACCOUNT_EMAIL"
|
||||
"NGINX_MAX_BODY_SIZE"
|
||||
)
|
||||
|
||||
sed -i --follow-symlinks '$a\' mailcow.conf
|
||||
@@ -111,16 +112,16 @@ adapt_new_options() {
|
||||
echo "SQL_PORT=127.0.0.1:13306" >> mailcow.conf
|
||||
;;
|
||||
API_KEY)
|
||||
echo '# Create or override API key for web UI' >> mailcow.conf
|
||||
echo '# Read-write service API key, shown read-only in the web UI' >> mailcow.conf
|
||||
echo "#API_KEY=" >> mailcow.conf
|
||||
;;
|
||||
API_KEY_READ_ONLY)
|
||||
echo '# Create or override read-only API key for web UI' >> mailcow.conf
|
||||
echo '# Read-only service API key, shown read-only in the web UI' >> mailcow.conf
|
||||
echo "#API_KEY_READ_ONLY=" >> mailcow.conf
|
||||
;;
|
||||
API_ALLOW_FROM)
|
||||
echo '# Must be set for API_KEY to be active' >> mailcow.conf
|
||||
echo '# IPs only, no networks (networks can be set via UI)' >> mailcow.conf
|
||||
echo '# Must be set for API_KEY and API_KEY_READ_ONLY to be active' >> mailcow.conf
|
||||
echo '# Comma separated list of IPs/networks' >> mailcow.conf
|
||||
echo "#API_ALLOW_FROM=" >> mailcow.conf
|
||||
;;
|
||||
SNAT_TO_SOURCE)
|
||||
@@ -309,6 +310,11 @@ adapt_new_options() {
|
||||
echo '# Account email for ACME DNS-01 challenge registration' >> mailcow.conf
|
||||
echo 'ACME_ACCOUNT_EMAIL=me@example.com' >> mailcow.conf
|
||||
;;
|
||||
NGINX_MAX_BODY_SIZE)
|
||||
echo '# Maximum request size for SOGo webmail, CalDAV/CardDAV and ActiveSync, e.g. 128m or 1g - 0 means unlimited' >> 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 "${option}=" >> mailcow.conf
|
||||
;;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.24
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.24
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -318,7 +318,7 @@ class DockerApi:
|
||||
|
||||
if 'username' in request_json and 'script_name' in request_json:
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
|
||||
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' -- '" + request_json['script_name'].replace("'", "'\\''") + "'"]
|
||||
sieve_return = container.exec_run(cmd)
|
||||
return self.exec_run_handler('utf8_text_only', sieve_return)
|
||||
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
|
||||
@@ -412,6 +412,63 @@ class DockerApi:
|
||||
|
||||
sogo_return = container.exec_run(["/bin/bash", "-c", f"sogo-tool rename-user '{old_username}' '{new_username}'"], user='sogo')
|
||||
return self.exec_run_handler('generic', sogo_return)
|
||||
# api call: container_post - post_action: exec - cmd: sogo - task: subscribe
|
||||
# Subscribes users to a folder of the owner, or unsubscribes them; values are passed as environment and
|
||||
# arguments, never through the shell
|
||||
def container_post__exec__sogo__subscribe(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
owner = str(request_json.get('owner', ''))
|
||||
folder = str(request_json.get('folder', ''))
|
||||
users = [str(user) for user in request_json.get('users', []) if str(user)]
|
||||
if not owner or not users or not re.match(r'^(Calendar|Contacts)/[A-Za-z0-9._-]+$', folder):
|
||||
return Response(content=json.dumps({ 'type': 'danger', 'msg': 'owner, folder and users required' }, indent=4), media_type="application/json")
|
||||
script = 'for user in "$@"; do sogo-tool manage-acl "$SOGO_ACTION" "$SOGO_OWNER" "$SOGO_FOLDER" "$user" || exit 1; done'
|
||||
subscribe_return = container.exec_run(["/bin/bash", "-c", script, "subscribe"] + users, user='sogo',
|
||||
environment={ 'SOGO_ACTION': 'unsubscribe' if request_json.get('unsubscribe') else 'subscribe', 'SOGO_OWNER': owner, 'SOGO_FOLDER': folder })
|
||||
return self.exec_run_handler('generic', subscribe_return)
|
||||
# api call: container_post - post_action: exec - cmd: sogo - task: init_user
|
||||
# Creates the personal folders of users that never logged in to SOGo (e.g. shared mailboxes)
|
||||
def container_post__exec__sogo__init_user(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
users = [str(user) for user in request_json.get('users', []) if str(user)]
|
||||
if not users:
|
||||
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')
|
||||
return self.exec_run_handler('generic', init_return)
|
||||
# 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
|
||||
def container_post__exec__doveadm__subscribe_shared(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
owner = str(request_json.get('owner', ''))
|
||||
users = [str(user) for user in request_json.get('users', []) if str(user)]
|
||||
if not owner or not users or any(c in owner for c in '/*%'):
|
||||
return Response(content=json.dumps({ 'type': 'danger', 'msg': 'owner and users required' }, indent=4), media_type="application/json")
|
||||
if request_json.get('unsubscribe'):
|
||||
# Without access the user no longer sees the folders, their names come from the owner
|
||||
script = ('set -o pipefail; for user in "$@"; do doveadm mailbox list -u "$SUB_OWNER" | while IFS= read -r folder; do '
|
||||
'case "$folder" in Shared|Shared/*) continue;; esac; doveadm mailbox unsubscribe -u "$user" "Shared/$SUB_OWNER/$folder" || exit 1; '
|
||||
'done || exit 1; done')
|
||||
else:
|
||||
# No visible folder means no access yet (ACL, auth cache): fail, so the subscription is retried
|
||||
script = ('for user in "$@"; do folders=$(doveadm mailbox list -u "$user" "Shared/$SUB_OWNER/*") && [ -n "$folders" ] || exit 1; '
|
||||
'while IFS= read -r folder; do doveadm mailbox subscribe -u "$user" "$folder" || exit 1; done <<< "$folders"; done')
|
||||
subscribe_return = container.exec_run(["/bin/bash", "-c", script, "subscribe_shared"] + users, environment={ 'SUB_OWNER': owner })
|
||||
return self.exec_run_handler('generic', subscribe_return)
|
||||
# api call: container_post - post_action: exec - cmd: doveadm - task: get_acl
|
||||
def container_post__exec__doveadm__get_acl(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
@@ -434,12 +491,13 @@ class DockerApi:
|
||||
if mailbox in mailbox_seen:
|
||||
continue
|
||||
|
||||
acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u '{id}' '{mailbox}'"])
|
||||
acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u '{id}' -- '{mailbox}'"])
|
||||
acls = acls.output.decode('utf-8').strip().splitlines()
|
||||
if len(acls) >= 2:
|
||||
for acl in acls[1:]:
|
||||
user_id, rights = acl.split(maxsplit=1)
|
||||
user_id = user_id.split('=')[1]
|
||||
# user=<name> as plain name, other identifiers (group=...) keep their type
|
||||
user_id = user_id.split('=', 1)[1] if user_id.startswith('user=') else user_id
|
||||
mailbox_seen.append(mailbox)
|
||||
formatted_acls.append({ 'user': id, 'id': user_id, 'mailbox': mailbox, 'rights': rights.split() })
|
||||
elif "Shared" in shared_folder and "/" in shared_folder:
|
||||
@@ -452,7 +510,7 @@ class DockerApi:
|
||||
if mailbox in mailbox_seen:
|
||||
continue
|
||||
|
||||
acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u '{user}' '{mailbox}'"])
|
||||
acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u '{user}' -- '{mailbox}'"])
|
||||
acls = acls.output.decode('utf-8').strip().splitlines()
|
||||
if len(acls) >= 2:
|
||||
for acl in acls[1:]:
|
||||
@@ -476,7 +534,7 @@ class DockerApi:
|
||||
id = request_json['id'].replace("'", "'\\''")
|
||||
|
||||
if user and mailbox and id:
|
||||
acl_delete_return = container.exec_run(["/bin/bash", "-c", f"doveadm acl delete -u '{user}' '{mailbox}' 'user={id}'"])
|
||||
acl_delete_return = container.exec_run(["/bin/bash", "-c", f"doveadm acl delete -u '{user}' -- '{mailbox}' '{self.acl_identifier(id)}'"])
|
||||
return self.exec_run_handler('generic', acl_delete_return)
|
||||
# api call: container_post - post_action: exec - cmd: doveadm - task: set_acl
|
||||
def container_post__exec__doveadm__set_acl(self, request_json, **kwargs):
|
||||
@@ -510,8 +568,50 @@ class DockerApi:
|
||||
rights += right + " "
|
||||
|
||||
if user and mailbox and id and rights:
|
||||
acl_set_return = container.exec_run(["/bin/bash", "-c", f"doveadm acl set -u '{user}' '{mailbox}' 'user={id}' {rights}"])
|
||||
acl_set_return = container.exec_run(["/bin/bash", "-c", f"doveadm acl set -u '{user}' -- '{mailbox}' '{self.acl_identifier(id)}' {rights}"])
|
||||
return self.exec_run_handler('generic', acl_set_return)
|
||||
# api call: container_post - post_action: exec - cmd: doveadm - task: set_acl_all
|
||||
# Sets the rights of an identifier on every folder of a mailbox, no rights = delete; values are passed as
|
||||
# environment, never through the shell
|
||||
def container_post__exec__doveadm__set_acl_all(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
user = request_json.get('user', '')
|
||||
id = self.acl_identifier(request_json.get('id', ''))
|
||||
rights = [right.lower() for right in request_json.get('rights', []) if right.lower() in self.acl_rights]
|
||||
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")
|
||||
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 -f tab mailbox status -u "$ACL_USER" guid "*" | tail -n +2 | cut -f1 | while IFS= read -r folder; do '
|
||||
'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],
|
||||
environment={ 'ACL_USER': user, 'ACL_ID': id, 'ACL_RIGHTS': ' '.join(rights) })
|
||||
return self.exec_run_handler('generic', acl_return)
|
||||
# api call: container_post - post_action: exec - cmd: doveadm - task: auth_cache_flush
|
||||
# Drops cached userdb results (e.g. acl_groups) of the given users, all users if none are given
|
||||
def container_post__exec__doveadm__auth_cache_flush(self, request_json, **kwargs):
|
||||
if 'container_id' in kwargs:
|
||||
filters = {"id": kwargs['container_id']}
|
||||
elif 'container_name' in kwargs:
|
||||
filters = {"name": kwargs['container_name']}
|
||||
|
||||
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||
users = [str(user) for user in request_json.get('users', []) if str(user)]
|
||||
flush_return = container.exec_run(["doveadm", "auth", "cache", "flush", "--"] + users)
|
||||
return self.exec_run_handler('generic', flush_return)
|
||||
|
||||
# Rights doveadm acl set accepts from the API
|
||||
acl_rights = ["admin", "create", "delete", "expunge", "insert", "lookup", "post", "read", "write", "write-deleted", "write-seen"]
|
||||
|
||||
# ACL identifier for doveadm: "user=<name>" for a plain name, "user=..." / "group=..." as given
|
||||
def acl_identifier(self, id):
|
||||
if id.startswith('user=') or id.startswith('group='):
|
||||
return id
|
||||
return 'user=' + id
|
||||
|
||||
|
||||
# Collect host stats
|
||||
|
||||
@@ -1,12 +1,48 @@
|
||||
FROM alpine:3.21
|
||||
# Alpine >= 3.22 only ships Dovecot 2.4, Dovecot 2.3 is built from the Alpine 3.21 packaging
|
||||
FROM alpine:3.24 AS builder
|
||||
|
||||
# Last commit of the aports 3.21-stable branch
|
||||
ARG APORTS_COMMIT=55126a11a4b9dcd14de3068e3348e9951d144ca6
|
||||
|
||||
RUN apk add --no-cache alpine-sdk git \
|
||||
&& git init -q /aports \
|
||||
&& cd /aports \
|
||||
&& git remote add origin https://github.com/alpinelinux/aports.git \
|
||||
&& git sparse-checkout set main/dovecot community/dovecot-fts-flatcurve \
|
||||
&& git fetch -q --depth 1 --filter=blob:none origin ${APORTS_COMMIT} \
|
||||
&& git checkout -q FETCH_HEAD \
|
||||
&& abuild-keygen -a -n \
|
||||
&& cp /root/.abuild/*.rsa.pub /etc/apk/keys/ \
|
||||
&& apk update \
|
||||
&& cd /aports/main/dovecot \
|
||||
# newer gettext does not overwrite the shipped config.rpath without -f
|
||||
&& sed -i 's/autoreconf -vi/autoreconf -fvi/' APKBUILD \
|
||||
&& abuild -F -r \
|
||||
# flatcurve must be built against the Dovecot 2.3 headers
|
||||
&& apk add --no-cache /root/packages/main/*/dovecot-2*.apk /root/packages/main/*/dovecot-dev-*.apk \
|
||||
&& cd /aports/community/dovecot-fts-flatcurve \
|
||||
&& abuild -F -r \
|
||||
&& mkdir /packages \
|
||||
&& cp /root/.abuild/*.rsa.pub /packages/ \
|
||||
&& for pkg in dovecot dovecot-dev dovecot-lmtpd dovecot-lua dovecot-ldap dovecot-mysql dovecot-sql \
|
||||
dovecot-submissiond dovecot-pigeonhole-plugin dovecot-pop3d dovecot-fts-flatcurve; do \
|
||||
cp /root/packages/*/*/${pkg}-[0-9]*.apk /packages/; \
|
||||
done
|
||||
|
||||
|
||||
FROM alpine:3.24
|
||||
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||
ARG GOSU_VERSION=1.19
|
||||
ARG GOSU_VERSION=1.20
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
# MariaDB Connector/C >= 3.4 refuses DB servers without TLS, this covers Perl DBI, client.cnf covers Dovecot
|
||||
ENV MARIADB_TLS_DISABLE_PEER_VERIFICATION=1
|
||||
|
||||
COPY --from=builder /packages /tmp/packages
|
||||
|
||||
# Add groups and users before installing Dovecot to not break compatibility
|
||||
RUN addgroup -g 5000 vmail \
|
||||
@@ -18,6 +54,7 @@ RUN addgroup -g 5000 vmail \
|
||||
&& adduser -D -u 5000 -G vmail -h /var/vmail vmail \
|
||||
&& adduser -D -G dovecot -u 401 -h /dev/null -s /sbin/nologin dovecot \
|
||||
&& adduser -D -G dovenull -u 402 -h /dev/null -s /sbin/nologin dovenull \
|
||||
&& cp /tmp/packages/*.rsa.pub /etc/apk/keys/ \
|
||||
&& apk add --no-cache --update \
|
||||
bash \
|
||||
bind-tools \
|
||||
@@ -68,7 +105,6 @@ RUN addgroup -g 5000 vmail \
|
||||
perl-package-stash-xs \
|
||||
perl-par-packer \
|
||||
perl-parse-recdescent \
|
||||
perl-lockfile-simple \
|
||||
libproc2 \
|
||||
perl-readonly \
|
||||
perl-regexp-common \
|
||||
@@ -99,28 +135,21 @@ RUN addgroup -g 5000 vmail \
|
||||
supervisor \
|
||||
tzdata \
|
||||
wget \
|
||||
dovecot \
|
||||
dovecot-dev \
|
||||
dovecot-lmtpd \
|
||||
dovecot-lua \
|
||||
dovecot-ldap \
|
||||
dovecot-mysql \
|
||||
dovecot-sql \
|
||||
dovecot-submissiond \
|
||||
dovecot-pigeonhole-plugin \
|
||||
dovecot-pop3d \
|
||||
dovecot-fts-flatcurve \
|
||||
/tmp/packages/*.apk \
|
||||
&& rm -rf /tmp/packages \
|
||||
&& arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
|
||||
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$arch" \
|
||||
&& chmod +x /usr/local/bin/gosu \
|
||||
&& gosu nobody true
|
||||
|
||||
# Dovecot's MySQL driver reads [client] from /etc/my.cnf, Connector/C ignores the !includedir there
|
||||
COPY client.cnf /etc/my.cnf
|
||||
COPY trim_logs.sh /usr/local/bin/trim_logs.sh
|
||||
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
|
||||
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 imapsync /usr/local/bin/imapsync
|
||||
COPY imapsync_runner.pl /usr/local/bin/imapsync_runner.pl
|
||||
COPY imapsync_runner.py /usr/local/bin/imapsync_runner.py
|
||||
COPY report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve
|
||||
COPY report-ham.sieve /usr/lib/dovecot/sieve/report-ham.sieve
|
||||
COPY rspamd-pipe-ham /usr/lib/dovecot/sieve/rspamd-pipe-ham
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[client]
|
||||
ssl = false
|
||||
ssl-verify-server-cert = false
|
||||
@@ -127,7 +127,8 @@ cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
|
||||
# Autogenerated by mailcow
|
||||
driver = mysql
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
||||
user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%n/${MAILDIR_SUB}:VOLATILEDIR=/var/volatile/%u:INDEX=/var/vmail_index/%u') AS mail, '%s' AS protocol, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND (active = '1' OR active = '2')
|
||||
# acl_groups: active mailbox groups of the user, IMAP grants are ACLs on group=mcg_<id>
|
||||
user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%n/${MAILDIR_SUB}:VOLATILEDIR=/var/volatile/%u:INDEX=/var/vmail_index/%u') AS mail, '%s' AS protocol, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule, (SELECT GROUP_CONCAT(CONCAT('mcg_', gm.group_id)) FROM mailbox_group_member gm JOIN mailbox_group g ON g.id = gm.group_id AND g.active = 1 WHERE gm.username = mailbox.username) AS acl_groups FROM mailbox WHERE username = '%u' AND (active = '1' OR active = '2')
|
||||
iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
|
||||
EOF
|
||||
|
||||
@@ -279,7 +280,7 @@ chmod g+rw /dev/console
|
||||
chown root:tty /dev/console
|
||||
chmod +x /usr/lib/dovecot/sieve/rspamd-pipe-ham \
|
||||
/usr/lib/dovecot/sieve/rspamd-pipe-spam \
|
||||
/usr/local/bin/imapsync_runner.pl \
|
||||
/usr/local/bin/imapsync_runner.py \
|
||||
/usr/local/bin/imapsync \
|
||||
/usr/local/bin/trim_logs.sh \
|
||||
/usr/local/bin/sa-rules.sh \
|
||||
@@ -304,6 +305,22 @@ IMAPSYNC_TABLE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DB
|
||||
# Envsubst maildir_gc
|
||||
echo "$(envsubst < /usr/local/bin/maildir_gc.sh)" > /usr/local/bin/maildir_gc.sh
|
||||
|
||||
# Wait for updated schema (DB_VERSION from docker-compose.yml), the userdb query reads tables the init_db of php-fpm creates on updates
|
||||
# No error while the versions table is missing (fresh install, set -e)
|
||||
DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN 2>/dev/null || true)
|
||||
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 --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN 2>/dev/null || true)
|
||||
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
|
||||
|
||||
# GUID generation
|
||||
while [[ ${VERSIONS_OK} != 'OK' ]]; do
|
||||
if [[ ! -z $(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = \"${DBNAME}\" AND TABLE_NAME = 'versions'") ]]; then
|
||||
|
||||
+6759
-3364
File diff suppressed because it is too large.
Load diff
@@ -1,196 +0,0 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
use DBI;
|
||||
use LockFile::Simple qw(lock trylock unlock);
|
||||
use Proc::ProcessTable;
|
||||
use Data::Dumper qw(Dumper);
|
||||
use IPC::Run 'run';
|
||||
use File::Temp;
|
||||
use Try::Tiny;
|
||||
use sigtrap 'handler' => \&sig_handler, qw(INT TERM KILL QUIT);
|
||||
|
||||
sub trim { my $s = shift; $s =~ s/^\s+|\s+$//g; return $s };
|
||||
my $t = Proc::ProcessTable->new;
|
||||
my $imapsync_running = grep { $_->{cmndline} =~ /imapsync\s/i } @{$t->table};
|
||||
if ($imapsync_running ge 1)
|
||||
{
|
||||
print "imapsync is active, exiting...";
|
||||
exit;
|
||||
}
|
||||
|
||||
sub qqw($) {
|
||||
my @params = ();
|
||||
my @values = split(/(?=--)/, $_[0]);
|
||||
foreach my $val (@values) {
|
||||
my @tmpparam = split(/ /, $val, 2);
|
||||
foreach my $tmpval (@tmpparam) {
|
||||
if ($tmpval ne '') {
|
||||
push @params, $tmpval;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach my $val (@params) {
|
||||
$val=trim($val);
|
||||
}
|
||||
return @params;
|
||||
}
|
||||
|
||||
$run_dir="/tmp";
|
||||
$dsn = 'DBI:mysql:database=' . $ENV{'DBNAME'} . ';mysql_socket=/var/run/mysqld/mysqld.sock';
|
||||
$lock_file = $run_dir . "/imapsync_busy";
|
||||
$lockmgr = LockFile::Simple->make(-autoclean => 1, -max => 1);
|
||||
$lockmgr->lock($lock_file) || die "can't lock ${lock_file}";
|
||||
$dbh = DBI->connect($dsn, $ENV{'DBUSER'}, $ENV{'DBPASS'}, {
|
||||
mysql_auto_reconnect => 1,
|
||||
mysql_enable_utf8mb4 => 1
|
||||
});
|
||||
$dbh->do("UPDATE imapsync SET is_running = 0");
|
||||
|
||||
sub sig_handler {
|
||||
# Send die to force exception in "run"
|
||||
die "sig_handler received signal, preparing to exit...\n";
|
||||
};
|
||||
|
||||
open my $file, '<', "/etc/sogo/sieve.creds";
|
||||
my $creds = <$file>;
|
||||
close $file;
|
||||
my ($master_user, $master_pass) = split /:/, $creds;
|
||||
my $sth = $dbh->prepare("SELECT id,
|
||||
user1,
|
||||
user2,
|
||||
host1,
|
||||
authmech1,
|
||||
password1,
|
||||
exclude,
|
||||
port1,
|
||||
enc1,
|
||||
delete2duplicates,
|
||||
maxage,
|
||||
subfolder2,
|
||||
delete1,
|
||||
delete2,
|
||||
automap,
|
||||
skipcrossduplicates,
|
||||
maxbytespersecond,
|
||||
custom_params,
|
||||
subscribeall,
|
||||
timeout1,
|
||||
timeout2,
|
||||
dry
|
||||
FROM imapsync
|
||||
WHERE active = 1
|
||||
AND is_running = 0
|
||||
AND (
|
||||
UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60
|
||||
OR
|
||||
last_run IS NULL)
|
||||
ORDER BY last_run");
|
||||
|
||||
$sth->execute();
|
||||
my $row;
|
||||
|
||||
while ($row = $sth->fetchrow_arrayref()) {
|
||||
|
||||
$id = @$row[0];
|
||||
$user1 = @$row[1];
|
||||
$user2 = @$row[2];
|
||||
$host1 = @$row[3];
|
||||
$authmech1 = @$row[4];
|
||||
$password1 = @$row[5];
|
||||
$exclude = @$row[6];
|
||||
$port1 = @$row[7];
|
||||
$enc1 = @$row[8];
|
||||
$delete2duplicates = @$row[9];
|
||||
$maxage = @$row[10];
|
||||
$subfolder2 = @$row[11];
|
||||
$delete1 = @$row[12];
|
||||
$delete2 = @$row[13];
|
||||
$automap = @$row[14];
|
||||
$skipcrossduplicates = @$row[15];
|
||||
$maxbytespersecond = @$row[16];
|
||||
$custom_params = @$row[17];
|
||||
$subscribeall = @$row[18];
|
||||
$timeout1 = @$row[19];
|
||||
$timeout2 = @$row[20];
|
||||
$dry = @$row[21];
|
||||
|
||||
if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
|
||||
|
||||
my $template = $run_dir . '/imapsync.XXXXXXX';
|
||||
my $passfile1 = File::Temp->new(TEMPLATE => $template);
|
||||
my $passfile2 = File::Temp->new(TEMPLATE => $template);
|
||||
|
||||
binmode( $passfile1, ":utf8" );
|
||||
|
||||
print $passfile1 "$password1\n";
|
||||
print $passfile2 trim($master_pass) . "\n";
|
||||
|
||||
my @custom_params_a = qqw($custom_params);
|
||||
my $custom_params_ref = \@custom_params_a;
|
||||
|
||||
my $generated_cmds = [ "/usr/local/bin/imapsync",
|
||||
"--tmpdir", "/tmp",
|
||||
"--nofoldersizes",
|
||||
"--addheader",
|
||||
($timeout1 le "0" ? () : ('--timeout1', $timeout1)),
|
||||
($timeout2 le "0" ? () : ('--timeout2', $timeout2)),
|
||||
($exclude eq "" ? () : ("--exclude", $exclude)),
|
||||
($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
|
||||
($maxage eq "0" ? () : ('--maxage', $maxage)),
|
||||
($maxbytespersecond eq "0" ? () : ('--maxbytespersecond', $maxbytespersecond)),
|
||||
($delete2duplicates ne "1" ? () : ('--delete2duplicates')),
|
||||
($subscribeall ne "1" ? () : ('--subscribeall')),
|
||||
($delete1 ne "1" ? () : ('--delete')),
|
||||
($delete2 ne "1" ? () : ('--delete2')),
|
||||
($automap ne "1" ? () : ('--automap')),
|
||||
($skipcrossduplicates ne "1" ? () : ('--skipcrossduplicates')),
|
||||
(!defined($enc1) ? () : ($enc1)),
|
||||
"--host1", $host1,
|
||||
"--user1", $user1,
|
||||
"--passfile1", $passfile1->filename,
|
||||
"--port1", $port1,
|
||||
"--host2", "localhost",
|
||||
"--user2", $user2 . '*' . trim($master_user),
|
||||
"--passfile2", $passfile2->filename,
|
||||
($dry eq "1" ? ('--dry') : ()),
|
||||
'--no-modulesversion',
|
||||
'--noreleasecheck'];
|
||||
|
||||
try {
|
||||
$is_running = $dbh->prepare("UPDATE imapsync SET is_running = 1, success = NULL, exit_status = NULL WHERE id = ?");
|
||||
$is_running->bind_param( 1, ${id} );
|
||||
$is_running->execute();
|
||||
|
||||
run [@$generated_cmds, @$custom_params_ref], '&>', \my $stdout;
|
||||
|
||||
# check exit code and status
|
||||
($exit_code, $exit_status) = ($stdout =~ m/Exiting\swith\sreturn\svalue\s(\d+)\s\(([^:)]+)/);
|
||||
|
||||
$success = 0;
|
||||
if (defined $exit_code && $exit_code == 0) {
|
||||
$success = 1;
|
||||
}
|
||||
|
||||
$update = $dbh->prepare("UPDATE imapsync SET returned_text = ?, success = ?, exit_status = ? WHERE id = ?");
|
||||
$update->bind_param( 1, ${stdout} );
|
||||
$update->bind_param( 2, ${success} );
|
||||
$update->bind_param( 3, ${exit_status} );
|
||||
$update->bind_param( 4, ${id} );
|
||||
$update->execute();
|
||||
} catch {
|
||||
$update = $dbh->prepare("UPDATE imapsync SET returned_text = 'Could not start or finish imapsync', success = 0 WHERE id = ?");
|
||||
$update->bind_param( 1, ${id} );
|
||||
$update->execute();
|
||||
} finally {
|
||||
$update = $dbh->prepare("UPDATE imapsync SET last_run = NOW(), is_running = 0 WHERE id = ?");
|
||||
$update->bind_param( 1, ${id} );
|
||||
$update->execute();
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
$sth->finish();
|
||||
$dbh->disconnect();
|
||||
|
||||
$lockmgr->unlock($lock_file);
|
||||
Executable
+381
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
import fcntl
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import MySQLdb
|
||||
import MySQLdb.cursors
|
||||
|
||||
run_dir = '/tmp'
|
||||
# Max size of imapsync output stored in returned_text (the tail holds the summary)
|
||||
max_output = 16 * 1024 * 1024
|
||||
lock_file = run_dir + '/imapsync_busy.lock'
|
||||
# An OAuth run needs a token valid this long at start (half the lifetime for short-lived tokens)
|
||||
token_min_valid = 900
|
||||
# Custom params never passed, also when stored before (same list as CustomParams::BLOCKED in the web UI)
|
||||
blocked_params = {'abort', 'delete2foldersbutnot', 'delete2foldersonly', 'logdir', 'logfile', 'maxlinelengthcmd',
|
||||
'oauthrefreshcmd1', 'oauthrefreshcmd2', 'passfile1', 'passfile2', 'pidfile', 'pipemess', 'regexflag', 'regexmess',
|
||||
'regextrans2', 'showpasswords', 'skipmess', 'tests', 'testsdebug', 'testslive', 'testslive6', 'testsunit', 'tmpdir'}
|
||||
# imapsync only gets these variables, not the secrets of the container environment
|
||||
job_env_keys = ('PATH', 'LANG', 'LC_ALL', 'TZ')
|
||||
# A source must not point to mailcow itself (internal Docker network), host1 is checked before each run
|
||||
internal_nets = [ipaddress.ip_network(os.environ.get('IPV4_NETWORK', '172.22.1') + '.0/24', strict=False),
|
||||
ipaddress.ip_network(os.environ.get('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64'), strict=False)]
|
||||
# Refreshed tokens are written to the token files of running jobs this often, imapsync re-reads them on reconnect
|
||||
token_sync_interval = 60
|
||||
|
||||
# Next due job: higher prio first, never-run jobs first, then oldest run
|
||||
next_sql = """SELECT i.id, i.user1, i.user2, i.password1, i.exclude, i.delete2duplicates, i.maxage,
|
||||
i.subfolder2, i.delete1, i.delete2, i.automap, i.skipcrossduplicates, i.maxbytespersecond, i.custom_params,
|
||||
i.subscribeall, i.timeout1, i.timeout2, i.dry, i.source_id,
|
||||
s.host1, s.port1, s.enc1, s.verify_cert, s.auth_type, s.oauth_flow
|
||||
FROM imapsync i JOIN imapsync_source s ON i.source_id = s.id
|
||||
WHERE i.active = 1
|
||||
AND s.active = 1
|
||||
AND i.is_running = 0
|
||||
AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(i.last_run) > i.mins_interval * 60 OR i.last_run IS NULL)
|
||||
{skip}
|
||||
ORDER BY i.prio DESC, i.last_run IS NOT NULL, i.last_run ASC, i.id ASC
|
||||
LIMIT 1"""
|
||||
# Atomic claim, a job is only started when this flips is_running 0 -> 1
|
||||
claim_sql = """UPDATE imapsync SET is_running = 1, success = NULL, exit_status = NULL
|
||||
WHERE id = %s AND is_running = 0 AND active = 1"""
|
||||
# Access token: client_credentials share the source token, authorization_code use the token of
|
||||
# source + local mailbox (user2) + remote user (user1). issued/error describe the last refresh.
|
||||
source_token_sql = """SELECT oauth_access_token AS access_token, oauth_token_expires AS token_expires,
|
||||
UNIX_TIMESTAMP(oauth_last_refresh_attempt) AS issued, oauth_last_refresh_error AS error
|
||||
FROM imapsync_source WHERE id = %s"""
|
||||
user_token_sql = """SELECT access_token, token_expires, UNIX_TIMESTAMP(oauth_last_refresh_attempt) AS issued,
|
||||
last_refresh_error AS error FROM imapsync_source_oauth_token
|
||||
WHERE source_id = %s AND owner = %s AND username = %s"""
|
||||
|
||||
conn = None
|
||||
# Popen => job of running workers
|
||||
running = {}
|
||||
# Jobs released without a run (token not ready), skipped for the rest of this runner round
|
||||
deferred = set()
|
||||
stopping = False
|
||||
|
||||
def num(value):
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
def unlink(files):
|
||||
for path in files:
|
||||
try:
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def imapsync_running():
|
||||
for pid in filter(str.isdigit, os.listdir('/proc')):
|
||||
try:
|
||||
with open('/proc/' + pid + '/cmdline', 'rb') as f:
|
||||
cmdline = f.read().replace(b'\0', b' ')
|
||||
except OSError:
|
||||
continue
|
||||
if re.search(rb'imapsync\s', cmdline, re.I):
|
||||
return True
|
||||
return False
|
||||
|
||||
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 (syncs can outlast wait_timeout)
|
||||
def query(sql, args=None):
|
||||
global conn
|
||||
try:
|
||||
conn.ping()
|
||||
except MySQLdb.Error:
|
||||
conn = connect()
|
||||
cur = conn.cursor(MySQLdb.cursors.DictCursor)
|
||||
cur.execute(sql, args)
|
||||
return cur
|
||||
|
||||
# Store a result and release the job
|
||||
def finish_job(job_id, text, success, exit_status):
|
||||
try:
|
||||
query("UPDATE imapsync SET returned_text = %s, success = %s, exit_status = %s, last_run = NOW(), is_running = 0 WHERE id = %s",
|
||||
(text, success, exit_status, job_id))
|
||||
except MySQLdb.Error:
|
||||
query("UPDATE imapsync SET success = 0, last_run = NOW(), is_running = 0 WHERE id = %s", (job_id,))
|
||||
|
||||
# Release a job without a run; last_run stays, so the next runner round retries it
|
||||
def defer_job(job_id, text, exit_status):
|
||||
query("UPDATE imapsync SET returned_text = %s, success = 0, exit_status = %s, is_running = 0 WHERE id = %s",
|
||||
(text, exit_status, job_id))
|
||||
deferred.add(job_id)
|
||||
|
||||
# True when host resolves to loopback, link-local or the internal network; unknown hosts are left to imapsync
|
||||
def internal_host(host):
|
||||
try:
|
||||
addrs = {a[4][0] for a in socket.getaddrinfo(host, None)}
|
||||
except (socket.gaierror, UnicodeError):
|
||||
return False
|
||||
for addr in addrs:
|
||||
ip = ipaddress.ip_address(addr.split('%')[0])
|
||||
if ip.version == 6 and ip.ipv4_mapped:
|
||||
ip = ip.ipv4_mapped
|
||||
if ip.is_loopback or ip.is_unspecified or ip.is_link_local or any(ip in net for net in internal_nets):
|
||||
return True
|
||||
return False
|
||||
|
||||
# SNI takes host names only
|
||||
def ip_literal(host):
|
||||
try:
|
||||
ipaddress.ip_address(host)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def oauth_token(j):
|
||||
if j['oauth_flow'] == 'authorization_code':
|
||||
return query(user_token_sql, (j['source_id'], j['user2'], j['user1'])).fetchone() or {}
|
||||
return query(source_token_sql, (j['source_id'],)).fetchone() or {}
|
||||
|
||||
# Valid for token_min_valid, or half the lifetime of a short-lived token (known when its last refresh succeeded)
|
||||
def token_ready(row):
|
||||
token, expires, issued = row.get('access_token'), row.get('token_expires'), row.get('issued')
|
||||
if not token or expires is None:
|
||||
return False
|
||||
need = token_min_valid
|
||||
if issued and not row.get('error') and expires > int(issued):
|
||||
need = min(need, (expires - int(issued)) // 2)
|
||||
return expires - time.time() >= need
|
||||
|
||||
# Write a refreshed token over the token file of a running job (atomic, same path)
|
||||
def sync_token_file(oauth):
|
||||
token = oauth_token(oauth['job']).get('access_token')
|
||||
if not token or token == oauth['token']:
|
||||
return
|
||||
fd, tmp = tempfile.mkstemp(prefix='imapsync.', dir=run_dir)
|
||||
try:
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as f:
|
||||
f.write(token + '\n')
|
||||
os.replace(tmp, oauth['file'])
|
||||
oauth['token'] = token
|
||||
except OSError:
|
||||
unlink([tmp])
|
||||
|
||||
# Max concurrent imapsync processes and global per-process bandwidth cap in bytes/s (0 = off),
|
||||
# re-read before every claim round so changes apply to long runs
|
||||
def read_settings():
|
||||
settings = {r['name']: r['value'] for r in query("SELECT name, value FROM imapsync_settings")}
|
||||
return max(num(settings.get('max_parallel')), 1), max(num(settings.get('max_bytes_per_second')), 0)
|
||||
|
||||
# Build temp files and argv for a claimed job. Returns None when the job was finished without running imapsync.
|
||||
def prepare_job(j, global_bw, files):
|
||||
def tmpfile(content):
|
||||
fd, path = tempfile.mkstemp(prefix='imapsync.', dir=run_dir)
|
||||
files.append(path)
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
return path
|
||||
|
||||
if internal_host(j['host1']):
|
||||
finish_job(j['id'], 'Host1 %s is an address of mailcow itself, the sync job does not connect to it\n' % j['host1'], 0,
|
||||
'EXIT_CONNECTION_FAILURE_HOST1')
|
||||
return None
|
||||
|
||||
auth_type = j['auth_type'] or 'PLAIN'
|
||||
oauth = None
|
||||
if auth_type == 'XOAUTH2':
|
||||
row = oauth_token(j)
|
||||
if not token_ready(row):
|
||||
defer_job(j['id'], "OAuth access token for the configured source is missing or expires too soon; refresh cron will retry.",
|
||||
'OAUTH_TOKEN_NOT_READY')
|
||||
return None
|
||||
oauth = {'job': j, 'token': row['access_token'], 'file': tmpfile(row['access_token'] + '\n')}
|
||||
auth_args = ['--authmech1', 'XOAUTH2', '--oauthaccesstoken1', oauth['file']]
|
||||
else:
|
||||
auth_args = ['--passfile1', tmpfile(j['password1'] + '\n')]
|
||||
# imapsync auto-picks PLAIN, only pass --authmech1 for LOGIN/CRAM-MD5
|
||||
if auth_type != 'PLAIN':
|
||||
auth_args += ['--authmech1', auth_type]
|
||||
|
||||
# Custom params: JSON array of {o,v}, validated by the backend on write. One argv token per pair,
|
||||
# "--opt" or "--opt=value" (value glued to its option, no shell)
|
||||
custom_params = []
|
||||
try:
|
||||
pairs = json.loads(j['custom_params'] or '[]')
|
||||
except ValueError:
|
||||
pairs = []
|
||||
for p in pairs if isinstance(pairs, list) else []:
|
||||
if not isinstance(p, dict) or not isinstance(p.get('o'), str) or not re.fullmatch(r'[a-z0-9]+', p['o']) \
|
||||
or p['o'] in blocked_params:
|
||||
continue
|
||||
# The certificate check protects the OAuth token of the source, a job must not turn it off
|
||||
if oauth and p['o'] == 'sslargs1':
|
||||
continue
|
||||
value = '' if p.get('v') is None else str(p['v'])
|
||||
custom_params.append('--' + p['o'] if value == '' else '--' + p['o'] + '=' + value)
|
||||
|
||||
# Effective per-process bandwidth cap (bytes/s): per-job value, capped by the global limit
|
||||
eff_bw = num(j['maxbytespersecond'])
|
||||
if global_bw > 0 and (eff_bw <= 0 or eff_bw > global_bw):
|
||||
eff_bw = global_bw
|
||||
|
||||
# --nolog: the output is stored in returned_text, imapsync's own log dir would be the (read-only) cwd
|
||||
cmd = ['/usr/local/bin/imapsync', '--tmpdir', '/tmp', '--nolog', '--nofoldersizes', '--addheader']
|
||||
if num(j['timeout1']) > 0:
|
||||
cmd += ['--timeout1', str(j['timeout1'])]
|
||||
if num(j['timeout2']) > 0:
|
||||
cmd += ['--timeout2', str(j['timeout2'])]
|
||||
if j['exclude']:
|
||||
cmd += ['--exclude', j['exclude']]
|
||||
if j['subfolder2']:
|
||||
cmd += ['--subfolder2', j['subfolder2']]
|
||||
if num(j['maxage']) != 0:
|
||||
cmd += ['--maxage', str(j['maxage'])]
|
||||
if eff_bw > 0:
|
||||
cmd += ['--maxbytespersecond', str(eff_bw)]
|
||||
for col, opt in (('delete2duplicates', '--delete2duplicates'), ('subscribeall', '--subscribeall'),
|
||||
('delete1', '--delete1'), ('delete2', '--delete2'), ('automap', '--automap'),
|
||||
('skipcrossduplicates', '--skipcrossduplicates')):
|
||||
if j[col] == 1:
|
||||
cmd.append(opt)
|
||||
enc1 = {'TLS': '--tls1', 'SSL': '--ssl1'}.get(j['enc1'])
|
||||
if enc1:
|
||||
# Mail::IMAPClient starts TLS without a host name: check the certificate against host1, not its address
|
||||
cmd += [enc1, '--sslargs1', 'SSL_verifycn_scheme=imap', '--sslargs1', 'SSL_verifycn_name=' + j['host1']]
|
||||
if not ip_literal(j['host1']):
|
||||
cmd += ['--sslargs1', 'SSL_hostname=' + j['host1']]
|
||||
# imapsync does not check certificates by default; custom sslargs1 of password jobs can still change it
|
||||
if j['verify_cert'] == 1:
|
||||
cmd += ['--sslargs1', 'SSL_verify_mode=1']
|
||||
cmd += ['--host1', j['host1'], '--user1', j['user1']] + auth_args + ['--port1', str(j['port1'])]
|
||||
cmd += ['--host2', 'localhost', '--user2', j['user2'] + '*' + master_user, '--passfile2', tmpfile(master_pass + '\n')]
|
||||
if j['dry'] == 1:
|
||||
cmd.append('--dry')
|
||||
cmd += ['--no-modulesversion', '--noreleasecheck'] + custom_params
|
||||
|
||||
return {'id': j['id'], 'files': files, 'outfile': tmpfile(''), 'cmd': cmd, 'oauth': oauth}
|
||||
|
||||
# Worker: imapsync with output to a file, no inherited fds (DB socket, lock)
|
||||
def start_job(job):
|
||||
env = {k: os.environ[k] for k in job_env_keys if k in os.environ}
|
||||
env['HOME'] = run_dir
|
||||
with open(job['outfile'], 'wb') as out:
|
||||
return subprocess.Popen(job['cmd'], stdin=subprocess.DEVNULL, stdout=out, stderr=subprocess.STDOUT, env=env)
|
||||
|
||||
# Store the result of a finished worker and remove its temp files
|
||||
def reap_job(job):
|
||||
stdout = b''
|
||||
try:
|
||||
with open(job['outfile'], 'rb') as f:
|
||||
size = os.fstat(f.fileno()).st_size
|
||||
if size > max_output:
|
||||
f.seek(size - max_output)
|
||||
stdout = b'[output truncated, last %d bytes]\n' % max_output
|
||||
stdout += f.read()
|
||||
except OSError:
|
||||
pass
|
||||
unlink(job['files'])
|
||||
text = stdout.decode('utf-8', 'replace')
|
||||
# Last match: the echoed command line at the top may contain user controlled values
|
||||
m = re.findall(r'Exiting\swith\sreturn\svalue\s(\d+)\s\(([^:)]+)', text)
|
||||
exit_code, exit_status = m[-1] if m else (None, None)
|
||||
if exit_code is None:
|
||||
text = 'Could not start or finish imapsync\n' + text
|
||||
finish_job(job['id'], text, 1 if exit_code is not None and int(exit_code) == 0 else 0, exit_status)
|
||||
|
||||
# Stop claiming new jobs, pass the signal on to the running imapsync processes
|
||||
def stop(signum, frame):
|
||||
global stopping
|
||||
stopping = True
|
||||
for proc in list(running):
|
||||
proc.send_signal(signal.SIGTERM)
|
||||
|
||||
# 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("can't lock " + lock_file)
|
||||
|
||||
if imapsync_running():
|
||||
print('imapsync is active, exiting...')
|
||||
sys.exit(0)
|
||||
|
||||
for sig in (signal.SIGINT, signal.SIGTERM, signal.SIGQUIT):
|
||||
signal.signal(sig, stop)
|
||||
|
||||
conn = connect()
|
||||
# Stale flags from crashed runs, safe only here: under the lock and with no imapsync running
|
||||
query("UPDATE imapsync SET is_running = 0")
|
||||
|
||||
with open('/etc/sogo/sieve.creds') as f:
|
||||
master_user, master_pass = [s.strip() for s in f.readline().split(':', 1)]
|
||||
|
||||
# Worker pool: keep up to max_parallel workers busy, claim the next due job whenever a slot is free
|
||||
# (after every worker exit and at least every 10 seconds while workers run)
|
||||
last_token_sync = time.monotonic()
|
||||
try:
|
||||
while True:
|
||||
for proc, job in list(running.items()):
|
||||
if proc.poll() is not None:
|
||||
del running[proc]
|
||||
reap_job(job)
|
||||
|
||||
if time.monotonic() - last_token_sync >= token_sync_interval:
|
||||
last_token_sync = time.monotonic()
|
||||
for job in running.values():
|
||||
if job['oauth']:
|
||||
try:
|
||||
sync_token_file(job['oauth'])
|
||||
except MySQLdb.Error as e:
|
||||
print('imapsync job %d: %s' % (job['id'], e), file=sys.stderr)
|
||||
|
||||
max_parallel, global_bw = read_settings()
|
||||
# Deferred and finished jobs drop out of next_sql, only failed claims are capped
|
||||
failed_claims = 0
|
||||
while not stopping and len(running) < max_parallel and failed_claims < 100:
|
||||
skip = ('AND i.id NOT IN (%s)' % ', '.join(['%s'] * len(deferred))) if deferred else ''
|
||||
j = query(next_sql.format(skip=skip), tuple(deferred) or None).fetchone()
|
||||
if not j:
|
||||
break
|
||||
if query(claim_sql, (j['id'],)).rowcount != 1:
|
||||
failed_claims += 1
|
||||
continue
|
||||
|
||||
files = []
|
||||
try:
|
||||
job = prepare_job(j, global_bw, files)
|
||||
except Exception as e:
|
||||
print('imapsync job %d: %s' % (j['id'], e), file=sys.stderr)
|
||||
finish_job(j['id'], 'Could not start or finish imapsync', 0, None)
|
||||
job = None
|
||||
if not job:
|
||||
unlink(files)
|
||||
continue
|
||||
try:
|
||||
running[start_job(job)] = job
|
||||
except OSError as e:
|
||||
# Release the claim, the job is picked up again later
|
||||
print('imapsync job %d: %s' % (j['id'], e), file=sys.stderr)
|
||||
unlink(files)
|
||||
query("UPDATE imapsync SET is_running = 0 WHERE id = %s", (j['id'],))
|
||||
break
|
||||
|
||||
if not running:
|
||||
break
|
||||
# A worker exit ends the wait early
|
||||
for _ in range(50):
|
||||
if any(proc.poll() is not None for proc in running):
|
||||
break
|
||||
time.sleep(0.2)
|
||||
finally:
|
||||
# Never leave password/token files behind
|
||||
for job in running.values():
|
||||
unlink(job['files'])
|
||||
@@ -4,7 +4,7 @@ catch_non_zero() {
|
||||
${CMD} > /dev/null
|
||||
EC=$?
|
||||
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
|
||||
}
|
||||
source /source_env.sh
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.24
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ if [ $nftables_found -gt $iptables_found ]; then
|
||||
fi
|
||||
|
||||
if [ $nftables_found -eq 0 ] && [ $nftables_found -eq $iptables_found ]; then
|
||||
nftables_lines=$(nft list ruleset | wc -l)
|
||||
iptables_lines=$(iptables-save | wc -l)
|
||||
nftables_lines=$(nft list ruleset 2>/dev/null | wc -l)
|
||||
iptables_lines=$(iptables-save 2>/dev/null | wc -l)
|
||||
if [ $nftables_lines -gt $iptables_lines ]; then
|
||||
backend=nftables
|
||||
else
|
||||
|
||||
@@ -1,548 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
DEBUG = False
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import atexit
|
||||
import signal
|
||||
import ipaddress
|
||||
from collections import Counter
|
||||
from random import randint
|
||||
from threading import Thread
|
||||
from threading import Lock
|
||||
import redis
|
||||
import json
|
||||
import dns.resolver
|
||||
import dns.exception
|
||||
import uuid
|
||||
from modules.Logger import Logger
|
||||
from modules.IPTables import IPTables
|
||||
from modules.NFTables import NFTables
|
||||
|
||||
def logdebug(msg):
|
||||
if DEBUG:
|
||||
logger.logInfo("DEBUG: %s" % msg)
|
||||
|
||||
# Globals
|
||||
WHITELIST = []
|
||||
BLACKLIST = []
|
||||
bans = {}
|
||||
quit_now = False
|
||||
exit_code = 0
|
||||
lock = Lock()
|
||||
chain_name = "MAILCOW"
|
||||
r = None
|
||||
pubsub = None
|
||||
clear_before_quit = False
|
||||
|
||||
def refreshF2boptions():
|
||||
global f2boptions
|
||||
global quit_now
|
||||
global exit_code
|
||||
f2boptions = {}
|
||||
|
||||
if not r.get('F2B_OPTIONS'):
|
||||
f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
|
||||
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
|
||||
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
|
||||
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
|
||||
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
|
||||
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
|
||||
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
|
||||
else:
|
||||
try:
|
||||
f2boptions = json.loads(r.get('F2B_OPTIONS'))
|
||||
except ValueError as e:
|
||||
logger.logCrit(
|
||||
'Error loading F2B options: F2B_OPTIONS is not json. Exception: %s' % e)
|
||||
quit_now = True
|
||||
exit_code = 2
|
||||
|
||||
verifyF2boptions(f2boptions)
|
||||
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
||||
|
||||
def verifyF2boptions(f2boptions):
|
||||
verifyF2boption(f2boptions, 'ban_time', 1800)
|
||||
verifyF2boption(f2boptions, 'max_ban_time', 10000)
|
||||
verifyF2boption(f2boptions, 'ban_time_increment', True)
|
||||
verifyF2boption(f2boptions, 'max_attempts', 10)
|
||||
verifyF2boption(f2boptions, 'retry_window', 600)
|
||||
verifyF2boption(f2boptions, 'netban_ipv4', 32)
|
||||
verifyF2boption(f2boptions, 'netban_ipv6', 128)
|
||||
verifyF2boption(f2boptions, 'banlist_id', str(uuid.uuid4()))
|
||||
verifyF2boption(f2boptions, 'manage_external', 0)
|
||||
|
||||
def verifyF2boption(f2boptions, f2boption, f2bdefault):
|
||||
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
|
||||
|
||||
def refreshF2bregex():
|
||||
global f2bregex
|
||||
global quit_now
|
||||
global exit_code
|
||||
if not r.get('F2B_REGEX'):
|
||||
f2bregex = {}
|
||||
f2bregex[1] = r'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
|
||||
f2bregex[2] = r'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
|
||||
f2bregex[3] = r'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
|
||||
f2bregex[4] = r'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
|
||||
f2bregex[5] = r'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
|
||||
f2bregex[6] = r'\w+\([^,]+,([0-9a-f\.:]+),<[^>]+>\): Password mismatch \(SHA1 of given password: [a-f0-9]+\)'
|
||||
f2bregex[7] = r'\w+\([^,]+,([0-9a-f\.:]+),<[^>]+>\): unknown user \(SHA1 of given password: [a-f0-9]+\)'
|
||||
f2bregex[8] = r'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
|
||||
f2bregex[9] = r'([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
|
||||
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
|
||||
else:
|
||||
try:
|
||||
f2bregex = {}
|
||||
f2bregex = json.loads(r.get('F2B_REGEX'))
|
||||
except ValueError:
|
||||
logger.logCrit('Error loading F2B options: F2B_REGEX is not json')
|
||||
quit_now = True
|
||||
exit_code = 2
|
||||
|
||||
def get_ip(address):
|
||||
ip = ipaddress.ip_address(address)
|
||||
if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
|
||||
ip = ip.ipv4_mapped
|
||||
if ip.is_private or ip.is_loopback:
|
||||
return False
|
||||
|
||||
return ip
|
||||
|
||||
def ban(address):
|
||||
global f2boptions
|
||||
global lock
|
||||
logdebug("ban() called with address=%s" % address)
|
||||
refreshF2boptions()
|
||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||
RETRY_WINDOW = int(f2boptions['retry_window'])
|
||||
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
|
||||
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
|
||||
|
||||
ip = get_ip(address)
|
||||
if not ip:
|
||||
logdebug("No valid IP -- skipping ban()")
|
||||
return
|
||||
address = str(ip)
|
||||
self_network = ipaddress.ip_network(address)
|
||||
|
||||
with lock:
|
||||
temp_whitelist = set(WHITELIST)
|
||||
logdebug("Checking if %s overlaps with any WHITELIST entries" % self_network)
|
||||
if temp_whitelist:
|
||||
for wl_key in temp_whitelist:
|
||||
wl_net = ipaddress.ip_network(wl_key, False)
|
||||
logdebug("Checking overlap between %s and %s" % (self_network, wl_net))
|
||||
if wl_net.overlaps(self_network):
|
||||
logger.logInfo(
|
||||
'Address %s is allowlisted by rule %s' % (self_network, wl_net))
|
||||
return
|
||||
|
||||
net = ipaddress.ip_network(
|
||||
(address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
|
||||
net = str(net)
|
||||
logdebug("Ban net: %s" % net)
|
||||
|
||||
if not net in bans:
|
||||
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
|
||||
logdebug("Initing new ban counter for %s" % net)
|
||||
|
||||
current_attempt = time.time()
|
||||
logdebug("Current attempt ts=%s, previous: %s, retry_window: %s" %
|
||||
(current_attempt, bans[net]['last_attempt'], RETRY_WINDOW))
|
||||
if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW:
|
||||
bans[net]['attempts'] = 0
|
||||
logdebug("Ban counter for %s reset as window expired" % net)
|
||||
|
||||
bans[net]['attempts'] += 1
|
||||
bans[net]['last_attempt'] = current_attempt
|
||||
logdebug("%s attempts now %d" % (net, bans[net]['attempts']))
|
||||
|
||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||
cur_time = int(round(time.time()))
|
||||
NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter'])
|
||||
logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
|
||||
if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1:
|
||||
with lock:
|
||||
logdebug("Calling tables.banIPv4(%s)" % net)
|
||||
tables.banIPv4(net)
|
||||
elif int(f2boptions['manage_external']) != 1:
|
||||
with lock:
|
||||
logdebug("Calling tables.banIPv6(%s)" % net)
|
||||
tables.banIPv6(net)
|
||||
|
||||
logdebug("Updating F2B_ACTIVE_BANS[%s]=%d" %
|
||||
(net, cur_time + NET_BAN_TIME))
|
||||
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
|
||||
else:
|
||||
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (
|
||||
MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
||||
|
||||
def unban(net):
|
||||
global lock
|
||||
logdebug("Calling unban() with net=%s" % net)
|
||||
if not net in bans:
|
||||
logger.logInfo(
|
||||
'%s is not banned, skipping unban and deleting from queue (if any)' % net)
|
||||
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||
return
|
||||
logger.logInfo('Unbanning %s' % net)
|
||||
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
|
||||
with lock:
|
||||
logdebug("Calling tables.unbanIPv4(%s)" % net)
|
||||
tables.unbanIPv4(net)
|
||||
else:
|
||||
with lock:
|
||||
logdebug("Calling tables.unbanIPv6(%s)" % net)
|
||||
tables.unbanIPv6(net)
|
||||
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
|
||||
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||
if net in bans:
|
||||
logdebug("Unban for %s, setting attempts=0, ban_counter+=1" % net)
|
||||
bans[net]['attempts'] = 0
|
||||
bans[net]['ban_counter'] += 1
|
||||
|
||||
def permBan(net, unban=False):
|
||||
global f2boptions
|
||||
global lock
|
||||
|
||||
is_unbanned = False
|
||||
is_banned = False
|
||||
if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
|
||||
with lock:
|
||||
if unban:
|
||||
is_unbanned = tables.unbanIPv4(net)
|
||||
elif int(f2boptions['manage_external']) != 1:
|
||||
is_banned = tables.banIPv4(net)
|
||||
else:
|
||||
with lock:
|
||||
if unban:
|
||||
is_unbanned = tables.unbanIPv6(net)
|
||||
elif int(f2boptions['manage_external']) != 1:
|
||||
is_banned = tables.banIPv6(net)
|
||||
|
||||
|
||||
if is_unbanned:
|
||||
r.hdel('F2B_PERM_BANS', '%s' % net)
|
||||
logger.logCrit('Removed host/network %s from denylist' % net)
|
||||
elif is_banned:
|
||||
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
||||
logger.logCrit('Added host/network %s to denylist' % net)
|
||||
|
||||
def clear():
|
||||
global lock
|
||||
logger.logInfo('Clearing all bans')
|
||||
for net in bans.copy():
|
||||
logdebug("Unbanning net: %s" % net)
|
||||
unban(net)
|
||||
with lock:
|
||||
logdebug("Clearing IPv4/IPv6 table")
|
||||
tables.clearIPv4Table()
|
||||
tables.clearIPv6Table()
|
||||
try:
|
||||
if r is not None:
|
||||
r.delete('F2B_ACTIVE_BANS')
|
||||
r.delete('F2B_PERM_BANS')
|
||||
except Exception as ex:
|
||||
logger.logWarn('Error clearing redis keys F2B_ACTIVE_BANS and F2B_PERM_BANS: %s' % ex)
|
||||
|
||||
def watch():
|
||||
global pubsub
|
||||
global quit_now
|
||||
global exit_code
|
||||
|
||||
logger.logInfo('Watching Redis channel F2B_CHANNEL')
|
||||
pubsub.subscribe('F2B_CHANNEL')
|
||||
|
||||
while not quit_now:
|
||||
try:
|
||||
for item in pubsub.listen():
|
||||
refreshF2bregex()
|
||||
for rule_id, rule_regex in f2bregex.items():
|
||||
if item['data'] and item['type'] == 'message':
|
||||
try:
|
||||
result = re.search(rule_regex, item['data'])
|
||||
except re.error:
|
||||
result = False
|
||||
if result:
|
||||
addr = result.group(1)
|
||||
ip = ipaddress.ip_address(addr)
|
||||
if ip.is_private or ip.is_loopback:
|
||||
continue
|
||||
logger.logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
|
||||
ban(addr)
|
||||
except Exception as ex:
|
||||
logger.logWarn('Error reading log line from pubsub: %s' % ex)
|
||||
pubsub = None
|
||||
quit_now = True
|
||||
exit_code = 2
|
||||
|
||||
def snat4(snat_target):
|
||||
global lock
|
||||
global quit_now
|
||||
|
||||
while not quit_now:
|
||||
time.sleep(10)
|
||||
with lock:
|
||||
tables.snat4(snat_target, os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24')
|
||||
|
||||
def snat6(snat_target):
|
||||
global lock
|
||||
global quit_now
|
||||
|
||||
while not quit_now:
|
||||
time.sleep(10)
|
||||
with lock:
|
||||
tables.snat6(snat_target, os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64'))
|
||||
|
||||
def autopurge():
|
||||
global f2boptions
|
||||
logdebug("autopurge thread started")
|
||||
while not quit_now:
|
||||
logdebug("autopurge tick")
|
||||
time.sleep(10)
|
||||
refreshF2boptions()
|
||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
|
||||
logdebug("QUEUE_UNBAN: %s" % QUEUE_UNBAN)
|
||||
if QUEUE_UNBAN:
|
||||
for net in QUEUE_UNBAN:
|
||||
logdebug("Autopurge: unbanning queued net: %s" % net)
|
||||
unban(str(net))
|
||||
# Only check expiry for actively banned IPs:
|
||||
active_bans = r.hgetall('F2B_ACTIVE_BANS')
|
||||
now = time.time()
|
||||
for net_str, expire_str in active_bans.items():
|
||||
logdebug("Checking ban expiry for (actively banned): %s" % net_str)
|
||||
# Defensive: always process if timer missing or expired
|
||||
try:
|
||||
expire = float(expire_str)
|
||||
except Exception:
|
||||
logdebug("Invalid expire time for %s; unbanning" % net_str)
|
||||
unban(net_str)
|
||||
continue
|
||||
time_left = expire - now
|
||||
logdebug("Time left for %s: %.1f seconds" % (net_str, time_left))
|
||||
if time_left <= 0:
|
||||
logdebug("Ban expired for %s" % net_str)
|
||||
unban(net_str)
|
||||
|
||||
def mailcowChainOrder():
|
||||
global lock
|
||||
global quit_now
|
||||
global exit_code
|
||||
while not quit_now:
|
||||
time.sleep(10)
|
||||
with lock:
|
||||
quit_now, exit_code = tables.checkIPv4ChainOrder()
|
||||
if quit_now: return
|
||||
quit_now, exit_code = tables.checkIPv6ChainOrder()
|
||||
|
||||
def calcNetBanTime(ban_counter):
|
||||
global f2boptions
|
||||
|
||||
BAN_TIME = int(f2boptions['ban_time'])
|
||||
MAX_BAN_TIME = int(f2boptions['max_ban_time'])
|
||||
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
|
||||
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** ban_counter
|
||||
NET_BAN_TIME = max([BAN_TIME, min([NET_BAN_TIME, MAX_BAN_TIME])])
|
||||
return NET_BAN_TIME
|
||||
|
||||
def isIpNetwork(address):
|
||||
try:
|
||||
ipaddress.ip_network(address, False)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def genNetworkList(list):
|
||||
resolver = dns.resolver.Resolver()
|
||||
hostnames = []
|
||||
networks = []
|
||||
for key in list:
|
||||
if isIpNetwork(key):
|
||||
networks.append(key)
|
||||
else:
|
||||
hostnames.append(key)
|
||||
for hostname in hostnames:
|
||||
hostname_ips = []
|
||||
for rdtype in ['A', 'AAAA']:
|
||||
try:
|
||||
answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
|
||||
except dns.exception.Timeout:
|
||||
logger.logInfo('Hostname %s timedout on resolve' % hostname)
|
||||
break
|
||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||
continue
|
||||
except dns.exception.DNSException as dnsexception:
|
||||
logger.logInfo('%s' % dnsexception)
|
||||
continue
|
||||
for rdata in answer:
|
||||
hostname_ips.append(rdata.to_text())
|
||||
networks.extend(hostname_ips)
|
||||
return set(networks)
|
||||
|
||||
def whitelistUpdate():
|
||||
global lock
|
||||
global quit_now
|
||||
global WHITELIST
|
||||
while not quit_now:
|
||||
start_time = time.time()
|
||||
list = r.hgetall('F2B_WHITELIST')
|
||||
new_whitelist = []
|
||||
if list:
|
||||
new_whitelist = genNetworkList(list)
|
||||
with lock:
|
||||
if Counter(new_whitelist) != Counter(WHITELIST):
|
||||
WHITELIST = new_whitelist
|
||||
logger.logInfo('Allowlist was changed, it has %s entries' % len(WHITELIST))
|
||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||
|
||||
def blacklistUpdate():
|
||||
global quit_now
|
||||
global BLACKLIST
|
||||
while not quit_now:
|
||||
start_time = time.time()
|
||||
list = r.hgetall('F2B_BLACKLIST')
|
||||
new_blacklist = []
|
||||
if list:
|
||||
new_blacklist = genNetworkList(list)
|
||||
if Counter(new_blacklist) != Counter(BLACKLIST):
|
||||
addban = set(new_blacklist).difference(BLACKLIST)
|
||||
delban = set(BLACKLIST).difference(new_blacklist)
|
||||
BLACKLIST = new_blacklist
|
||||
logger.logInfo('Denylist was changed, it has %s entries' % len(BLACKLIST))
|
||||
if addban:
|
||||
for net in addban:
|
||||
permBan(net=net)
|
||||
if delban:
|
||||
for net in delban:
|
||||
permBan(net=net, unban=True)
|
||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||
|
||||
def sigterm_quit(signum, frame):
|
||||
global clear_before_quit
|
||||
logdebug("SIGTERM received, setting clear_before_quit to True and exiting")
|
||||
clear_before_quit = True
|
||||
sys.exit(exit_code)
|
||||
|
||||
def before_quit():
|
||||
logdebug("before_quit called, clear_before_quit=%s" % clear_before_quit)
|
||||
if clear_before_quit:
|
||||
clear()
|
||||
if pubsub is not None:
|
||||
pubsub.unsubscribe()
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger = Logger()
|
||||
logdebug("Sys.argv: %s" % sys.argv)
|
||||
atexit.register(before_quit)
|
||||
signal.signal(signal.SIGTERM, sigterm_quit)
|
||||
|
||||
backend = sys.argv[1]
|
||||
logdebug("Backend: %s" % backend)
|
||||
if backend == "nftables":
|
||||
logger.logInfo('Using NFTables backend')
|
||||
tables = NFTables(chain_name, logger)
|
||||
else:
|
||||
logger.logInfo('Using IPTables backend')
|
||||
logger.logWarn(
|
||||
"DEPRECATION: iptables-legacy is deprecated and will be removed in future releases. "
|
||||
"Please switch to nftables on your host to ensure complete compatibility."
|
||||
)
|
||||
time.sleep(5)
|
||||
tables = IPTables(chain_name, logger)
|
||||
|
||||
clear()
|
||||
logger.logInfo("Initializing mailcow netfilter chain")
|
||||
tables.initChainIPv4()
|
||||
tables.initChainIPv6()
|
||||
|
||||
if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE", "").lower() in ("y", "yes"):
|
||||
logger.logInfo(f"Skipping {chain_name} isolation")
|
||||
else:
|
||||
logger.logInfo(f"Setting {chain_name} isolation")
|
||||
tables.create_mailcow_isolation_rule("br-mailcow", [3306, 6379, 8983, 12345], os.getenv("MAILCOW_REPLICA_IP"))
|
||||
|
||||
# connect to redis
|
||||
while True:
|
||||
try:
|
||||
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
|
||||
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
|
||||
logdebug(
|
||||
"Connecting redis (SLAVEOF_IP:%s, PORT:%s)" % (redis_slaveof_ip, redis_slaveof_port))
|
||||
if "".__eq__(redis_slaveof_ip):
|
||||
r = redis.StrictRedis(
|
||||
host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS'])
|
||||
else:
|
||||
r = redis.StrictRedis(
|
||||
host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS'])
|
||||
r.ping()
|
||||
pubsub = r.pubsub()
|
||||
except Exception as ex:
|
||||
logdebug(
|
||||
'Redis connection failed: %s - trying again in 3 seconds' % (ex))
|
||||
time.sleep(3)
|
||||
else:
|
||||
break
|
||||
logger.set_redis(r)
|
||||
logdebug("Redis connection established, setting up F2B keys")
|
||||
|
||||
if r.exists('F2B_LOG'):
|
||||
logdebug("Renaming F2B_LOG to NETFILTER_LOG")
|
||||
r.rename('F2B_LOG', 'NETFILTER_LOG')
|
||||
r.delete('F2B_ACTIVE_BANS')
|
||||
r.delete('F2B_PERM_BANS')
|
||||
|
||||
refreshF2boptions()
|
||||
|
||||
watch_thread = Thread(target=watch)
|
||||
watch_thread.daemon = True
|
||||
watch_thread.start()
|
||||
|
||||
if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':
|
||||
try:
|
||||
snat_ip = os.getenv('SNAT_TO_SOURCE')
|
||||
snat_ipo = ipaddress.ip_address(snat_ip)
|
||||
if type(snat_ipo) is ipaddress.IPv4Address:
|
||||
snat4_thread = Thread(target=snat4, args=(snat_ip,))
|
||||
snat4_thread.daemon = True
|
||||
snat4_thread.start()
|
||||
except ValueError:
|
||||
print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
|
||||
|
||||
if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':
|
||||
try:
|
||||
snat_ip = os.getenv('SNAT6_TO_SOURCE')
|
||||
snat_ipo = ipaddress.ip_address(snat_ip)
|
||||
if type(snat_ipo) is ipaddress.IPv6Address:
|
||||
snat6_thread = Thread(target=snat6,args=(snat_ip,))
|
||||
snat6_thread.daemon = True
|
||||
snat6_thread.start()
|
||||
except ValueError:
|
||||
print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')
|
||||
|
||||
autopurge_thread = Thread(target=autopurge)
|
||||
autopurge_thread.daemon = True
|
||||
autopurge_thread.start()
|
||||
|
||||
mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
|
||||
mailcowchainwatch_thread.daemon = True
|
||||
mailcowchainwatch_thread.start()
|
||||
|
||||
blacklistupdate_thread = Thread(target=blacklistUpdate)
|
||||
blacklistupdate_thread.daemon = True
|
||||
blacklistupdate_thread.start()
|
||||
|
||||
whitelistupdate_thread = Thread(target=whitelistUpdate)
|
||||
whitelistupdate_thread.daemon = True
|
||||
whitelistupdate_thread.start()
|
||||
|
||||
while not quit_now:
|
||||
time.sleep(0.5)
|
||||
|
||||
logdebug("Exiting with code %s" % exit_code)
|
||||
sys.exit(exit_code)
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import redis
|
||||
from modules.Logger import Logger
|
||||
from modules.Netfilter import Netfilter
|
||||
|
||||
chain_name = "MAILCOW"
|
||||
|
||||
def connect_redis(logger):
|
||||
if os.getenv('REDIS_SLAVEOF_IP', ''):
|
||||
host, port = os.getenv('REDIS_SLAVEOF_IP'), os.getenv('REDIS_SLAVEOF_PORT', '')
|
||||
else:
|
||||
host, port = os.getenv('IPV4_NETWORK', '172.22.1') + '.249', 6379
|
||||
while True:
|
||||
try:
|
||||
# log lines are not always valid UTF-8
|
||||
r = redis.StrictRedis(host=host, port=port, db=0, password=os.environ['REDISPASS'], decode_responses=True,
|
||||
encoding_errors='replace', socket_connect_timeout=5, socket_timeout=30)
|
||||
r.ping()
|
||||
return r
|
||||
except Exception as ex:
|
||||
logger.logWarn('Redis connection failed: %s - trying again in 3 seconds' % ex)
|
||||
time.sleep(3)
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger = Logger()
|
||||
|
||||
backend = sys.argv[1]
|
||||
if backend == "nftables":
|
||||
from modules.NFTables import NFTables
|
||||
logger.logInfo('Using NFTables backend')
|
||||
tables = NFTables(chain_name, logger)
|
||||
else:
|
||||
from modules.IPTables import IPTables
|
||||
logger.logInfo('Using IPTables backend')
|
||||
logger.logWarn(
|
||||
"DEPRECATION: iptables-legacy is deprecated and will be removed in future releases. "
|
||||
"Please switch to nftables on your host to ensure complete compatibility."
|
||||
)
|
||||
time.sleep(5)
|
||||
tables = IPTables(chain_name, logger)
|
||||
|
||||
r = connect_redis(logger)
|
||||
logger.set_redis(r)
|
||||
|
||||
netfilter = Netfilter(r, tables, logger, os.environ)
|
||||
signal.signal(signal.SIGTERM, lambda signum, frame: netfilter.stop.set())
|
||||
signal.signal(signal.SIGINT, lambda signum, frame: netfilter.stop.set())
|
||||
|
||||
netfilter.setup()
|
||||
netfilter.run()
|
||||
netfilter.shutdown()
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,218 @@
|
||||
import ipaddress
|
||||
import time
|
||||
|
||||
|
||||
class BanManager:
|
||||
"""Failed attempts and automatic bans (Redis F2B_ACTIVE_BANS: network => expiry), the denylist (F2B_PERM_BANS)
|
||||
and the allowlist. Every firewall change runs under lock, which also guards the other firewall work of netfilter
|
||||
(chains, SNAT), because the backends must not run in parallel."""
|
||||
|
||||
def __init__(self, r, tables, options, logger, lock, clock=time.time):
|
||||
self.r = r
|
||||
self.tables = tables
|
||||
self.options = options
|
||||
self.logger = logger
|
||||
self.lock = lock
|
||||
self.clock = clock
|
||||
self.counters = {}
|
||||
self.banned = set()
|
||||
self.denied = {}
|
||||
self.allowlist = []
|
||||
|
||||
def attempt(self, ip):
|
||||
"""Counts a failed attempt of ip and bans its network after max_attempts within retry_window"""
|
||||
options = self.options.values
|
||||
prefix = options['netban_ipv4'] if ip.version == 4 else options['netban_ipv6']
|
||||
net = str(ipaddress.ip_network('%s/%d' % (ip, prefix), strict=False))
|
||||
with self.lock:
|
||||
now = self.clock()
|
||||
allowed = self.allowed_by(ip)
|
||||
if allowed is not None:
|
||||
self.logger.logInfo('Address %s is allowlisted by rule %s' % (ip, allowed))
|
||||
return
|
||||
if net in self.banned:
|
||||
# log lines of connections from before the ban
|
||||
return
|
||||
counter = self.counter(net)
|
||||
if now - counter['last_attempt'] > options['retry_window']:
|
||||
counter['attempts'] = 0
|
||||
counter['attempts'] += 1
|
||||
counter['last_attempt'] = now
|
||||
if counter['attempts'] < options['max_attempts']:
|
||||
self.logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (
|
||||
options['max_attempts'] - counter['attempts'], options['retry_window'], net))
|
||||
return
|
||||
|
||||
ban_time = self.options.ban_time(counter['ban_counter'])
|
||||
self.logger.logCrit('Banning %s for %d minutes' % (net, ban_time / 60))
|
||||
# Redis first: a firewall rule without expiry would never be lifted
|
||||
self.r.hset('F2B_ACTIVE_BANS', net, int(round(now + ban_time)))
|
||||
self.banned.add(net)
|
||||
if options['manage_external'] != 1:
|
||||
self.apply([net])
|
||||
|
||||
def unban(self, net):
|
||||
"""Lifts the automatic ban of a network; the rule stays while the denylist bans the same network"""
|
||||
with self.lock:
|
||||
normalized = self.normalize(net)
|
||||
if normalized is None:
|
||||
self.logger.logWarn('Removing invalid network %s from the active bans' % net)
|
||||
else:
|
||||
self.logger.logInfo('Unbanning %s' % net)
|
||||
if normalized not in self.denied:
|
||||
self.remove([normalized])
|
||||
self.r.hdel('F2B_ACTIVE_BANS', net)
|
||||
self.r.hdel('F2B_QUEUE_UNBAN', net)
|
||||
if net in self.banned:
|
||||
self.banned.discard(net)
|
||||
counter = self.counter(net)
|
||||
counter['attempts'] = 0
|
||||
counter['ban_counter'] += 1
|
||||
|
||||
def unban_queued(self, entry):
|
||||
"""Unban request of the UI: a banned network, or a network that contains banned ones (allowlisting)"""
|
||||
network = self.network(entry)
|
||||
if network is None:
|
||||
self.logger.logWarn('Removing invalid network %s from the unban queue' % entry)
|
||||
self.r.hdel('F2B_QUEUE_UNBAN', entry)
|
||||
return
|
||||
with self.lock:
|
||||
banned = set(self.r.hkeys('F2B_ACTIVE_BANS')) | self.banned
|
||||
targets = [net for net in banned if self.overlaps(net, network)]
|
||||
for net in targets:
|
||||
self.unban(net)
|
||||
if not targets and str(network) not in self.denied:
|
||||
# a rule without known ban, e.g. left by an older netfilter
|
||||
if self.remove([str(network)]):
|
||||
self.logger.logInfo('Unbanning %s' % network)
|
||||
else:
|
||||
self.logger.logInfo('%s is not banned, removing it from the unban queue' % network)
|
||||
self.r.hdel('F2B_QUEUE_UNBAN', entry)
|
||||
|
||||
def purge(self):
|
||||
"""Processes the unban queue of the UI and lifts expired bans"""
|
||||
for entry in self.r.hkeys('F2B_QUEUE_UNBAN'):
|
||||
self.unban_queued(entry)
|
||||
now = self.clock()
|
||||
for net, expires in self.r.hgetall('F2B_ACTIVE_BANS').items():
|
||||
try:
|
||||
expired = self.network(net) is None or float(expires) <= now
|
||||
except (TypeError, ValueError):
|
||||
expired = True
|
||||
if expired:
|
||||
self.unban(net)
|
||||
with self.lock:
|
||||
# bans whose Redis entry is gone, e.g. deleted by hand
|
||||
for net in self.banned - set(self.r.hkeys('F2B_ACTIVE_BANS')):
|
||||
self.unban(net)
|
||||
self.forget_idle(now)
|
||||
|
||||
def restore(self):
|
||||
"""Applies the unexpired automatic bans of Redis again after a start of netfilter and drops the expired ones"""
|
||||
now = self.clock()
|
||||
restored = []
|
||||
with self.lock:
|
||||
for net, expires in self.r.hgetall('F2B_ACTIVE_BANS').items():
|
||||
try:
|
||||
valid = self.network(net) is not None and float(expires) > now
|
||||
except (TypeError, ValueError):
|
||||
valid = False
|
||||
if not valid:
|
||||
self.r.hdel('F2B_ACTIVE_BANS', net)
|
||||
continue
|
||||
self.banned.add(net)
|
||||
self.counter(net)
|
||||
restored.append(net)
|
||||
if restored and self.options.values['manage_external'] != 1:
|
||||
self.apply(restored)
|
||||
if restored:
|
||||
self.logger.logInfo('Restored %d active bans' % len(restored))
|
||||
|
||||
def reapply(self):
|
||||
"""Applies all bans again, e.g. after the MAILCOW chain had to be recreated"""
|
||||
with self.lock:
|
||||
if self.options.values['manage_external'] != 1:
|
||||
self.apply(list(self.banned) + list(self.denied))
|
||||
|
||||
def set_denylist(self, entries):
|
||||
"""Bans the networks of the denylist permanently and lifts the bans of removed entries"""
|
||||
networks = {}
|
||||
for entry in entries:
|
||||
net = self.normalize(entry)
|
||||
if net is None:
|
||||
self.logger.logWarn('Skipping invalid denylist entry %s' % entry)
|
||||
continue
|
||||
networks[net] = entry
|
||||
with self.lock:
|
||||
if networks.keys() == self.denied.keys():
|
||||
return
|
||||
self.logger.logInfo('Denylist was changed, it has %d entries' % len(networks))
|
||||
added = [net for net in networks if net not in self.denied]
|
||||
removed = [net for net in self.denied if net not in networks]
|
||||
if added and self.options.values['manage_external'] != 1:
|
||||
self.apply(added)
|
||||
for net in added:
|
||||
self.r.hset('F2B_PERM_BANS', networks[net], int(round(self.clock())))
|
||||
self.logger.logCrit('Added host/network %s to denylist' % networks[net])
|
||||
# an automatic ban of the same network keeps its rule
|
||||
auto_banned = {self.normalize(net) for net in self.banned}
|
||||
self.remove([net for net in removed if net not in auto_banned])
|
||||
for net in removed:
|
||||
self.r.hdel('F2B_PERM_BANS', self.denied[net])
|
||||
self.logger.logCrit('Removed host/network %s from denylist' % self.denied[net])
|
||||
self.denied = networks
|
||||
|
||||
def set_allowlist(self, entries):
|
||||
networks = []
|
||||
for entry in entries:
|
||||
network = self.network(entry)
|
||||
if network is None:
|
||||
self.logger.logWarn('Skipping invalid allowlist entry %s' % entry)
|
||||
continue
|
||||
networks.append(network)
|
||||
with self.lock:
|
||||
if set(networks) != set(self.allowlist):
|
||||
self.allowlist = networks
|
||||
self.logger.logInfo('Allowlist was changed, it has %d entries' % len(networks))
|
||||
|
||||
def allowed_by(self, ip):
|
||||
address = ipaddress.ip_network(ip)
|
||||
for network in self.allowlist:
|
||||
if network.version == address.version and network.overlaps(address):
|
||||
return network
|
||||
return None
|
||||
|
||||
def counter(self, net):
|
||||
return self.counters.setdefault(net, {'attempts': 0, 'last_attempt': 0.0, 'ban_counter': 0})
|
||||
|
||||
def forget_idle(self, now):
|
||||
"""Drops counters of networks that were never banned and had no attempt within the retry window"""
|
||||
window = self.options.values['retry_window']
|
||||
with self.lock:
|
||||
for net in [net for net, counter in self.counters.items()
|
||||
if net not in self.banned and counter['ban_counter'] == 0 and now - counter['last_attempt'] > window]:
|
||||
del self.counters[net]
|
||||
|
||||
def apply(self, nets):
|
||||
for net in self.tables.ban(nets):
|
||||
self.logger.logWarn('Could not add the firewall rule for %s' % net)
|
||||
|
||||
def remove(self, nets):
|
||||
return self.tables.unban(nets) if nets else []
|
||||
|
||||
@staticmethod
|
||||
def network(entry):
|
||||
try:
|
||||
return ipaddress.ip_network(entry, strict=False)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def normalize(cls, entry):
|
||||
network = cls.network(entry)
|
||||
return str(network) if network is not None else None
|
||||
|
||||
@classmethod
|
||||
def overlaps(cls, net, network):
|
||||
other = cls.network(net)
|
||||
return other is not None and other.version == network.version and other.overlaps(network)
|
||||
@@ -1,6 +1,7 @@
|
||||
import iptc
|
||||
import time
|
||||
import os
|
||||
import ipaddress
|
||||
import contextlib
|
||||
|
||||
class IPTables:
|
||||
def __init__(self, chain_name, logger):
|
||||
@@ -8,62 +9,10 @@ class IPTables:
|
||||
self.logger = logger
|
||||
|
||||
def initChainIPv4(self):
|
||||
if not iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name) in iptc.Table(iptc.Table.FILTER).chains:
|
||||
iptc.Table(iptc.Table.FILTER).create_chain(self.chain_name)
|
||||
for c in ['FORWARD', 'INPUT']:
|
||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
|
||||
rule = iptc.Rule()
|
||||
rule.src = '0.0.0.0/0'
|
||||
rule.dst = '0.0.0.0/0'
|
||||
target = iptc.Target(rule, self.chain_name)
|
||||
rule.target = target
|
||||
if rule not in chain.rules:
|
||||
chain.insert_rule(rule)
|
||||
self.init_chain(iptc.Table(iptc.Table.FILTER))
|
||||
|
||||
def initChainIPv6(self):
|
||||
if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name) in iptc.Table6(iptc.Table6.FILTER).chains:
|
||||
iptc.Table6(iptc.Table6.FILTER).create_chain(self.chain_name)
|
||||
for c in ['FORWARD', 'INPUT']:
|
||||
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
|
||||
rule = iptc.Rule6()
|
||||
rule.src = '::/0'
|
||||
rule.dst = '::/0'
|
||||
target = iptc.Target(rule, self.chain_name)
|
||||
rule.target = target
|
||||
if rule not in chain.rules:
|
||||
chain.insert_rule(rule)
|
||||
|
||||
def checkIPv4ChainOrder(self):
|
||||
filter_table = iptc.Table(iptc.Table.FILTER)
|
||||
filter_table.refresh()
|
||||
return self.checkChainOrder(filter_table)
|
||||
|
||||
def checkIPv6ChainOrder(self):
|
||||
filter_table = iptc.Table6(iptc.Table6.FILTER)
|
||||
filter_table.refresh()
|
||||
return self.checkChainOrder(filter_table)
|
||||
|
||||
def checkChainOrder(self, filter_table):
|
||||
err = False
|
||||
exit_code = None
|
||||
|
||||
forward_chain = iptc.Chain(filter_table, 'FORWARD')
|
||||
input_chain = iptc.Chain(filter_table, 'INPUT')
|
||||
for chain in [forward_chain, input_chain]:
|
||||
target_found = False
|
||||
for position, item in enumerate(chain.rules):
|
||||
if item.target.name == self.chain_name:
|
||||
target_found = True
|
||||
if position > 2:
|
||||
self.logger.logCrit('Error in %s chain: %s target not found, restarting container' % (chain.name, self.chain_name))
|
||||
err = True
|
||||
exit_code = 2
|
||||
if not target_found:
|
||||
self.logger.logCrit('Error in %s chain: %s target not found, restarting container' % (chain.name, self.chain_name))
|
||||
err = True
|
||||
exit_code = 2
|
||||
|
||||
return err, exit_code
|
||||
self.init_chain(iptc.Table6(iptc.Table6.FILTER))
|
||||
|
||||
def clearIPv4Table(self):
|
||||
self.clearTable(iptc.Table(iptc.Table.FILTER))
|
||||
@@ -71,68 +20,133 @@ class IPTables:
|
||||
def clearIPv6Table(self):
|
||||
self.clearTable(iptc.Table6(iptc.Table6.FILTER))
|
||||
|
||||
def clearTable(self, filter_table):
|
||||
filter_table.autocommit = False
|
||||
forward_chain = iptc.Chain(filter_table, "FORWARD")
|
||||
input_chain = iptc.Chain(filter_table, "INPUT")
|
||||
mailcow_chain = iptc.Chain(filter_table, self.chain_name)
|
||||
if mailcow_chain in filter_table.chains:
|
||||
for rule in mailcow_chain.rules:
|
||||
mailcow_chain.delete_rule(rule)
|
||||
for rule in forward_chain.rules:
|
||||
if rule.target.name == self.chain_name:
|
||||
forward_chain.delete_rule(rule)
|
||||
for rule in input_chain.rules:
|
||||
if rule.target.name == self.chain_name:
|
||||
input_chain.delete_rule(rule)
|
||||
filter_table.delete_chain(self.chain_name)
|
||||
filter_table.commit()
|
||||
@contextlib.contextmanager
|
||||
def transaction(self, table):
|
||||
# One commit for all changes, autocommit rewrites the whole table for every change
|
||||
table.autocommit = False
|
||||
table.refresh()
|
||||
try:
|
||||
yield
|
||||
table.commit()
|
||||
finally:
|
||||
table.refresh()
|
||||
table.autocommit = True
|
||||
|
||||
def new_rule(self, table):
|
||||
return iptc.Rule6() if isinstance(table, iptc.Table6) else iptc.Rule()
|
||||
|
||||
def family(self, table):
|
||||
return 'IPv6' if isinstance(table, iptc.Table6) else 'IPv4'
|
||||
|
||||
def init_chain(self, filter_table):
|
||||
# Empty MAILCOW chain with one jump at the top of the input and forward chains
|
||||
filter_table.refresh()
|
||||
filter_table.autocommit = True
|
||||
if filter_table.is_chain(self.chain_name):
|
||||
filter_table.flush_entries(self.chain_name)
|
||||
else:
|
||||
filter_table.create_chain(self.chain_name)
|
||||
self.ensure_jumps(filter_table)
|
||||
|
||||
def banIPv4(self, source):
|
||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name)
|
||||
rule = iptc.Rule()
|
||||
rule.src = source
|
||||
target = iptc.Target(rule, "REJECT")
|
||||
rule.target = target
|
||||
if rule in chain.rules:
|
||||
return False
|
||||
chain.insert_rule(rule)
|
||||
return True
|
||||
def ensure_chains(self):
|
||||
# Recreates missing MAILCOW chains and moves the jumps back to the top, True if a chain was recreated
|
||||
recreated = False
|
||||
for filter_table in [iptc.Table(iptc.Table.FILTER), iptc.Table6(iptc.Table6.FILTER)]:
|
||||
filter_table.refresh()
|
||||
if not filter_table.is_chain(self.chain_name):
|
||||
filter_table.create_chain(self.chain_name)
|
||||
self.logger.logWarn(f'MAILCOW {self.family(filter_table)} chain was missing and has been recreated')
|
||||
recreated = True
|
||||
self.ensure_jumps(filter_table)
|
||||
return recreated
|
||||
|
||||
def banIPv6(self, source):
|
||||
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name)
|
||||
rule = iptc.Rule6()
|
||||
rule.src = source
|
||||
target = iptc.Target(rule, "REJECT")
|
||||
rule.target = target
|
||||
if rule in chain.rules:
|
||||
return False
|
||||
chain.insert_rule(rule)
|
||||
return True
|
||||
def ensure_jumps(self, filter_table):
|
||||
# Exactly one jump to MAILCOW as first rule: docker inserts its jump (DOCKER-USER) at the top
|
||||
for name in ['INPUT', 'FORWARD']:
|
||||
chain = iptc.Chain(filter_table, name)
|
||||
positions = [position for position, rule in enumerate(chain.rules) if rule.target.name == self.chain_name]
|
||||
if positions == [0]: continue
|
||||
|
||||
def unbanIPv4(self, source):
|
||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name)
|
||||
rule = iptc.Rule()
|
||||
rule.src = source
|
||||
target = iptc.Target(rule, "REJECT")
|
||||
rule.target = target
|
||||
if rule not in chain.rules:
|
||||
return False
|
||||
chain.delete_rule(rule)
|
||||
return True
|
||||
with self.transaction(filter_table):
|
||||
for rule in [rule for rule in chain.rules if rule.target.name == self.chain_name]:
|
||||
chain.delete_rule(rule)
|
||||
rule = self.new_rule(filter_table)
|
||||
rule.target = iptc.Target(rule, self.chain_name)
|
||||
chain.insert_rule(rule)
|
||||
if positions:
|
||||
self.logger.logInfo(f'Moved MAILCOW jump to the top of the {self.family(filter_table)} {name} chain, it was at position {positions[0] + 1}')
|
||||
else:
|
||||
self.logger.logInfo(f'Added MAILCOW jump to the {self.family(filter_table)} {name} chain')
|
||||
|
||||
def unbanIPv6(self, source):
|
||||
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name)
|
||||
rule = iptc.Rule6()
|
||||
rule.src = source
|
||||
target = iptc.Target(rule, "REJECT")
|
||||
rule.target = target
|
||||
if rule not in chain.rules:
|
||||
return False
|
||||
chain.delete_rule(rule)
|
||||
return True
|
||||
def clearTable(self, filter_table):
|
||||
try:
|
||||
with self.transaction(filter_table):
|
||||
if not filter_table.is_chain(self.chain_name):
|
||||
return
|
||||
for chain in filter_table.chains:
|
||||
for rule in chain.rules:
|
||||
if rule.target.name == self.chain_name:
|
||||
chain.delete_rule(rule)
|
||||
filter_table.flush_entries(self.chain_name)
|
||||
filter_table.delete_chain(self.chain_name)
|
||||
self.logger.logInfo(f'Clear completed: {self.family(filter_table)}')
|
||||
except Exception as ex:
|
||||
self.logger.logWarn(f'Error clearing the {self.family(filter_table)} MAILCOW chain: {ex}')
|
||||
|
||||
def ban_rule_network(self, rule):
|
||||
# Source network of a ban rule, None for every other rule
|
||||
if rule.target.name not in ('REJECT', 'DROP') or rule.matches or rule.in_interface or rule.out_interface:
|
||||
return None
|
||||
try:
|
||||
return ipaddress.ip_network(rule.src, strict=False)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def split_tables(self, sources):
|
||||
networks = {4: [], 6: []}
|
||||
for source in sources:
|
||||
network = ipaddress.ip_network(source, strict=False)
|
||||
networks[network.version].append(network)
|
||||
tables = [(iptc.Table(iptc.Table.FILTER), networks[4]), (iptc.Table6(iptc.Table6.FILTER), networks[6])]
|
||||
return [(table, table_networks) for table, table_networks in tables if table_networks]
|
||||
|
||||
def ban(self, sources):
|
||||
# One REJECT rule per network, existing rules are kept. Returns the networks that could not be banned
|
||||
failed = []
|
||||
for filter_table, networks in self.split_tables(sources):
|
||||
try:
|
||||
with self.transaction(filter_table):
|
||||
chain = iptc.Chain(filter_table, self.chain_name)
|
||||
existing = {self.ban_rule_network(rule) for rule in chain.rules}
|
||||
for network in dict.fromkeys(networks):
|
||||
if network in existing: continue
|
||||
rule = self.new_rule(filter_table)
|
||||
rule.src = str(network)
|
||||
rule.target = iptc.Target(rule, "REJECT")
|
||||
chain.insert_rule(rule)
|
||||
except Exception as ex:
|
||||
self.logger.logWarn(f'iptables: {ex}')
|
||||
failed += networks
|
||||
return [str(network) for network in failed]
|
||||
|
||||
def unban(self, sources):
|
||||
# Removes every rule of the networks, also duplicates of older versions. Returns the unbanned networks
|
||||
removed = []
|
||||
for filter_table, networks in self.split_tables(sources):
|
||||
targets = set(networks)
|
||||
try:
|
||||
with self.transaction(filter_table):
|
||||
chain = iptc.Chain(filter_table, self.chain_name)
|
||||
found = set()
|
||||
for rule in chain.rules:
|
||||
network = self.ban_rule_network(rule)
|
||||
if network in targets:
|
||||
chain.delete_rule(rule)
|
||||
found.add(network)
|
||||
except Exception as ex:
|
||||
self.logger.logWarn(f'iptables: {ex}')
|
||||
continue
|
||||
removed += found
|
||||
return [str(network) for network in removed]
|
||||
|
||||
def snat4(self, snat_target, source):
|
||||
try:
|
||||
@@ -188,7 +202,7 @@ class IPTables:
|
||||
if item == new_rule:
|
||||
if position != 0:
|
||||
chain.delete_rule(new_rule)
|
||||
|
||||
|
||||
table.commit()
|
||||
table.autocommit = True
|
||||
except:
|
||||
@@ -215,7 +229,9 @@ class IPTables:
|
||||
|
||||
def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""):
|
||||
try:
|
||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name)
|
||||
table = iptc.Table(iptc.Table.FILTER)
|
||||
table.refresh()
|
||||
chain = iptc.Chain(table, self.chain_name)
|
||||
|
||||
# insert mailcow isolation rule
|
||||
rule = iptc.Rule()
|
||||
@@ -231,7 +247,7 @@ class IPTables:
|
||||
chain.insert_rule(rule, position=0)
|
||||
|
||||
# insert mailcow isolation exception rule
|
||||
if _allow != "":
|
||||
if _allow:
|
||||
rule = iptc.Rule()
|
||||
rule.src = _allow
|
||||
rule.in_interface = f'!{_interface}'
|
||||
@@ -249,4 +265,4 @@ class IPTables:
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.logCrit(f"Error adding {self.chain_name} isolation: {e}")
|
||||
return False
|
||||
return False
|
||||
@@ -1,8 +1,9 @@
|
||||
import nftables
|
||||
import ipaddress
|
||||
import os
|
||||
|
||||
class NFTables:
|
||||
ISOLATION_COMMENTS = ('mailcow isolation', 'mailcow isolation allow')
|
||||
|
||||
def __init__(self, chain_name, logger):
|
||||
self.chain_name = chain_name
|
||||
self.logger = logger
|
||||
@@ -12,41 +13,15 @@ class NFTables:
|
||||
self.nft.set_handle_output(True)
|
||||
self.nft_chain_names = {'ip': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} },
|
||||
'ip6': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} } }
|
||||
self.mailcow_chains = set()
|
||||
|
||||
self.search_current_chains()
|
||||
|
||||
def initChainIPv4(self):
|
||||
self.insert_mailcow_chains("ip")
|
||||
self.init_chain("ip")
|
||||
|
||||
def initChainIPv6(self):
|
||||
self.insert_mailcow_chains("ip6")
|
||||
|
||||
def checkIPv4ChainOrder(self):
|
||||
return self.checkChainOrder("ip")
|
||||
|
||||
def checkIPv6ChainOrder(self):
|
||||
return self.checkChainOrder("ip6")
|
||||
|
||||
def checkChainOrder(self, filter_table):
|
||||
err = False
|
||||
exit_code = None
|
||||
|
||||
for chain in ['input', 'forward']:
|
||||
chain_position = self.check_mailcow_chains(filter_table, chain)
|
||||
if chain_position is None: continue
|
||||
|
||||
if chain_position is False:
|
||||
self.logger.logCrit(f'MAILCOW target not found in {filter_table} {chain} table, restarting container to fix it...')
|
||||
err = True
|
||||
exit_code = 2
|
||||
|
||||
if chain_position > 0:
|
||||
chain_position += 1
|
||||
self.logger.logCrit(f'MAILCOW target is in position {chain_position} in the {filter_table} {chain} table, restarting container to fix it...')
|
||||
err = True
|
||||
exit_code = 2
|
||||
|
||||
return err, exit_code
|
||||
self.init_chain("ip6")
|
||||
|
||||
def clearIPv4Table(self):
|
||||
self.clearTable("ip")
|
||||
@@ -54,84 +29,19 @@ class NFTables:
|
||||
def clearIPv6Table(self):
|
||||
self.clearTable("ip6")
|
||||
|
||||
def clearTable(self, _family):
|
||||
is_empty_dict = True
|
||||
json_command = self.get_base_dict()
|
||||
chain_handle = self.get_chain_handle(_family, "filter", self.chain_name)
|
||||
# if no handle, the chain doesn't exists
|
||||
if chain_handle is not None:
|
||||
is_empty_dict = False
|
||||
# flush chain
|
||||
mailcow_chain = {'family': _family, 'table': 'filter', 'name': self.chain_name}
|
||||
flush_chain = {'flush': {'chain': mailcow_chain}}
|
||||
json_command["nftables"].append(flush_chain)
|
||||
|
||||
# remove rule in forward chain
|
||||
# remove rule in input chain
|
||||
chains_family = [self.nft_chain_names[_family]['filter']['input'],
|
||||
self.nft_chain_names[_family]['filter']['forward'] ]
|
||||
|
||||
for chain_base in chains_family:
|
||||
if not chain_base: continue
|
||||
|
||||
rules_handle = self.get_rules_handle(_family, "filter", chain_base)
|
||||
if rules_handle is not None:
|
||||
for r_handle in rules_handle:
|
||||
is_empty_dict = False
|
||||
mailcow_rule = {'family':_family,
|
||||
'table': 'filter',
|
||||
'chain': chain_base,
|
||||
'handle': r_handle }
|
||||
delete_rules = {'delete': {'rule': mailcow_rule} }
|
||||
json_command["nftables"].append(delete_rules)
|
||||
|
||||
# remove chain
|
||||
# after delete all rules referencing this chain
|
||||
if chain_handle is not None:
|
||||
mc_chain_handle = {'family':_family,
|
||||
'table': 'filter',
|
||||
'name': self.chain_name,
|
||||
'handle': chain_handle }
|
||||
delete_chain = {'delete': {'chain': mc_chain_handle} }
|
||||
json_command["nftables"].append(delete_chain)
|
||||
|
||||
if is_empty_dict == False:
|
||||
if self.nft_exec_dict(json_command):
|
||||
self.logger.logInfo(f"Clear completed: {_family}")
|
||||
|
||||
def banIPv4(self, source):
|
||||
ban_dict = self.get_ban_ip_dict(source, "ip")
|
||||
return self.nft_exec_dict(ban_dict)
|
||||
|
||||
def banIPv6(self, source):
|
||||
ban_dict = self.get_ban_ip_dict(source, "ip6")
|
||||
return self.nft_exec_dict(ban_dict)
|
||||
|
||||
def unbanIPv4(self, source):
|
||||
unban_dict = self.get_unban_ip_dict(source, "ip")
|
||||
if not unban_dict:
|
||||
return False
|
||||
return self.nft_exec_dict(unban_dict)
|
||||
|
||||
def unbanIPv6(self, source):
|
||||
unban_dict = self.get_unban_ip_dict(source, "ip6")
|
||||
if not unban_dict:
|
||||
return False
|
||||
return self.nft_exec_dict(unban_dict)
|
||||
|
||||
def snat4(self, snat_target, source):
|
||||
self.snat_rule("ip", snat_target, source)
|
||||
|
||||
def snat6(self, snat_target, source):
|
||||
self.snat_rule("ip6", snat_target, source)
|
||||
|
||||
|
||||
def nft_exec_dict(self, query: dict):
|
||||
def nft_exec_dict(self, query: dict, quiet: bool = False):
|
||||
if not query: return False
|
||||
|
||||
rc, output, error = self.nft.json_cmd(query)
|
||||
if rc != 0:
|
||||
#self.logger.logCrit(f"Nftables Error: {error}")
|
||||
if not quiet:
|
||||
self.logger.logWarn(f"nftables: {str(error).strip() or f'error {rc}'}")
|
||||
return False
|
||||
|
||||
# Prevent returning False or empty string on commands that do not produce output
|
||||
@@ -144,118 +54,165 @@ class NFTables:
|
||||
return {'nftables': [{ 'metainfo': { 'json_schema_version': 1} } ] }
|
||||
|
||||
def search_current_chains(self):
|
||||
nft_chain_names = {'ip': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} },
|
||||
'ip6': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} } }
|
||||
nft_chain_priority = {'ip': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} },
|
||||
'ip6': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} } }
|
||||
'ip6': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} } }
|
||||
mailcow_chains = set()
|
||||
|
||||
# Command: 'nft list chains'
|
||||
_list = {'list' : {'chains': 'null'} }
|
||||
command = self.get_base_dict()
|
||||
command['nftables'].append(_list)
|
||||
kernel_ruleset = self.nft_exec_dict(command)
|
||||
if kernel_ruleset:
|
||||
for _object in kernel_ruleset['nftables']:
|
||||
chain = _object.get("chain")
|
||||
if not chain: continue
|
||||
if not kernel_ruleset:
|
||||
return
|
||||
|
||||
_family = chain['family']
|
||||
_table = chain['table']
|
||||
_hook = chain.get("hook")
|
||||
_priority = chain.get("prio")
|
||||
_name = chain['name']
|
||||
|
||||
if _family not in self.nft_chain_names: continue
|
||||
if _table not in self.nft_chain_names[_family]: continue
|
||||
if _hook not in self.nft_chain_names[_family][_table]: continue
|
||||
if _priority is None: continue
|
||||
|
||||
_saved_priority = nft_chain_priority[_family][_table][_hook]
|
||||
if _saved_priority is None or _priority < _saved_priority:
|
||||
# at this point, we know the chain has:
|
||||
# hook and priority set
|
||||
# and it has the lowest priority
|
||||
nft_chain_priority[_family][_table][_hook] = _priority
|
||||
self.nft_chain_names[_family][_table][_hook] = _name
|
||||
|
||||
def search_for_chain(self, kernel_ruleset: dict, chain_name: str):
|
||||
found = False
|
||||
for _object in kernel_ruleset["nftables"]:
|
||||
for _object in kernel_ruleset['nftables']:
|
||||
chain = _object.get("chain")
|
||||
if not chain:
|
||||
continue
|
||||
ch_name = chain.get("name")
|
||||
if ch_name == chain_name:
|
||||
found = True
|
||||
break
|
||||
return found
|
||||
if not chain: continue
|
||||
|
||||
_family = chain['family']
|
||||
_table = chain['table']
|
||||
_hook = chain.get("hook")
|
||||
_priority = chain.get("prio")
|
||||
_name = chain['name']
|
||||
|
||||
if _family not in nft_chain_names: continue
|
||||
if _table not in nft_chain_names[_family]: continue
|
||||
if _table == 'filter' and _name == self.chain_name:
|
||||
mailcow_chains.add(_family)
|
||||
if _hook not in nft_chain_names[_family][_table]: continue
|
||||
if _priority is None: continue
|
||||
|
||||
_saved_priority = nft_chain_priority[_family][_table][_hook]
|
||||
if _saved_priority is None or _priority < _saved_priority:
|
||||
# at this point, we know the chain has:
|
||||
# hook and priority set
|
||||
# and it has the lowest priority
|
||||
nft_chain_priority[_family][_table][_hook] = _priority
|
||||
nft_chain_names[_family][_table][_hook] = _name
|
||||
|
||||
self.nft_chain_names = nft_chain_names
|
||||
self.mailcow_chains = mailcow_chains
|
||||
|
||||
def has_filter_chains(self, _family: str):
|
||||
# no base chains: the filter table does not exist, e.g. ip6 if docker has ipv6 disabled
|
||||
return any(self.nft_chain_names[_family]['filter'].values())
|
||||
|
||||
def list_rules(self, _family: str, _table: str, _chain: str):
|
||||
# Command: 'nft list chain {family} {table} {chain}', None if the chain does not exist
|
||||
_list = {'list': {'chain': {'family': _family, 'table': _table, 'name': _chain}}}
|
||||
command = self.get_base_dict()
|
||||
command['nftables'].append(_list)
|
||||
kernel_ruleset = self.nft_exec_dict(command, quiet=True)
|
||||
if not kernel_ruleset or not isinstance(kernel_ruleset, dict):
|
||||
return None
|
||||
return [_object["rule"] for _object in kernel_ruleset["nftables"] if _object.get("rule")]
|
||||
|
||||
def get_chain_dict(self, _family: str, _name: str):
|
||||
# nft (add | create) chain [<family>] <table> <name>
|
||||
# nft (add | create) chain [<family>] <table> <name>
|
||||
_chain_opts = {'family': _family, 'table': 'filter', 'name': _name }
|
||||
_add = {'add': {'chain': _chain_opts} }
|
||||
final_chain = self.get_base_dict()
|
||||
final_chain["nftables"].append(_add)
|
||||
return final_chain
|
||||
|
||||
def get_mailcow_jump_rule_dict(self, _family: str, _chain: str):
|
||||
_jump_rule = self.get_base_dict()
|
||||
_expr_opt=[]
|
||||
_expr_counter = {'family': _family, 'table': 'filter', 'packets': 0, 'bytes': 0}
|
||||
_counter_dict = {'counter': _expr_counter}
|
||||
_expr_opt.append(_counter_dict)
|
||||
|
||||
_jump_opts = {'jump': {'target': self.chain_name} }
|
||||
|
||||
_expr_opt.append(_jump_opts)
|
||||
|
||||
def get_mailcow_jump_rule(self, _family: str, _chain: str):
|
||||
_expr_opt = [{'counter': {'family': _family, 'table': 'filter', 'packets': 0, 'bytes': 0}},
|
||||
{'jump': {'target': self.chain_name}}]
|
||||
_rule_params = {'family': _family,
|
||||
'table': 'filter',
|
||||
'chain': _chain,
|
||||
'expr': _expr_opt,
|
||||
'comment': "mailcow" }
|
||||
return {'insert': {'rule': _rule_params} }
|
||||
|
||||
_add_rule = {'insert': {'rule': _rule_params} }
|
||||
def is_mailcow_jump(self, rule: dict):
|
||||
# by target, jumps restored by iptables-restore carry no nft comment
|
||||
return any(isinstance(expr, dict) and isinstance(expr.get('jump'), dict) and expr['jump'].get('target') == self.chain_name
|
||||
for expr in rule.get('expr', []))
|
||||
|
||||
_jump_rule["nftables"].append(_add_rule)
|
||||
def init_chain(self, _family: str):
|
||||
# Empty MAILCOW chain with one jump at the top of the input and forward chains
|
||||
if not self.has_filter_chains(_family): return
|
||||
|
||||
return _jump_rule
|
||||
if self.list_rules(_family, 'filter', self.chain_name) is None:
|
||||
if self.nft_exec_dict(self.get_chain_dict(_family, self.chain_name)):
|
||||
self.logger.logInfo(f"MAILCOW {_family} chain created successfully.")
|
||||
else:
|
||||
flush_chain = {'flush': {'chain': {'family': _family, 'table': 'filter', 'name': self.chain_name}}}
|
||||
command = self.get_base_dict()
|
||||
command['nftables'].append(flush_chain)
|
||||
self.nft_exec_dict(command)
|
||||
|
||||
def insert_mailcow_chains(self, _family: str):
|
||||
nft_input_chain = self.nft_chain_names[_family]['filter']['input']
|
||||
nft_forward_chain = self.nft_chain_names[_family]['filter']['forward']
|
||||
# Command: 'nft list table <family> filter'
|
||||
_table_opts = {'family': _family, 'name': 'filter'}
|
||||
_list = {'list': {'table': _table_opts} }
|
||||
self.ensure_jumps(_family)
|
||||
|
||||
def ensure_chains(self):
|
||||
# Recreates missing MAILCOW chains and moves the jumps back to the top, True if a chain was recreated
|
||||
self.search_current_chains()
|
||||
recreated = False
|
||||
for _family in ['ip', 'ip6']:
|
||||
if not self.has_filter_chains(_family): continue
|
||||
|
||||
if _family not in self.mailcow_chains:
|
||||
if self.nft_exec_dict(self.get_chain_dict(_family, self.chain_name)):
|
||||
self.logger.logWarn(f"MAILCOW {_family} chain was missing and has been recreated")
|
||||
recreated = True
|
||||
self.ensure_jumps(_family)
|
||||
return recreated
|
||||
|
||||
def ensure_jumps(self, _family: str):
|
||||
# Exactly one jump to MAILCOW as first rule: docker inserts its jumps (DOCKER-USER, DOCKER-FORWARD) at the top
|
||||
for hook in ['input', 'forward']:
|
||||
base_chain = self.nft_chain_names[_family]['filter'][hook]
|
||||
if not base_chain: continue
|
||||
|
||||
rules = self.list_rules(_family, 'filter', base_chain)
|
||||
if rules is None: continue
|
||||
|
||||
positions = [position for position, rule in enumerate(rules) if self.is_mailcow_jump(rule)]
|
||||
if positions == [0]: continue
|
||||
|
||||
# delete and insert in one transaction, the chain is never without jump
|
||||
command = self.get_base_dict()
|
||||
for position in positions:
|
||||
_rule_opts = {'family': _family, 'table': 'filter', 'chain': base_chain, 'handle': rules[position]['handle']}
|
||||
command['nftables'].append({'delete': {'rule': _rule_opts}})
|
||||
command['nftables'].append(self.get_mailcow_jump_rule(_family, base_chain))
|
||||
if self.nft_exec_dict(command):
|
||||
if positions:
|
||||
self.logger.logInfo(f"Moved MAILCOW jump to the top of the {_family} {base_chain} chain, it was at position {positions[0] + 1}")
|
||||
else:
|
||||
self.logger.logInfo(f"Added MAILCOW jump to the {_family} {base_chain} chain")
|
||||
|
||||
def clearTable(self, _family: str):
|
||||
# Jumps first, the chain can only be deleted without references. Separate steps, so one failing step
|
||||
# does not roll back the others
|
||||
_list = {'list': {'table': {'family': _family, 'name': 'filter'}}}
|
||||
command = self.get_base_dict()
|
||||
command['nftables'].append(_list)
|
||||
kernel_ruleset = self.nft_exec_dict(command)
|
||||
if kernel_ruleset:
|
||||
# chain
|
||||
if not self.search_for_chain(kernel_ruleset, self.chain_name):
|
||||
cadena = self.get_chain_dict(_family, self.chain_name)
|
||||
if self.nft_exec_dict(cadena):
|
||||
self.logger.logInfo(f"MAILCOW {_family} chain created successfully.")
|
||||
kernel_ruleset = self.nft_exec_dict(command, quiet=True)
|
||||
if not kernel_ruleset or not isinstance(kernel_ruleset, dict):
|
||||
return
|
||||
|
||||
input_jump_found, forward_jump_found = False, False
|
||||
delete_jumps = self.get_base_dict()
|
||||
for _object in kernel_ruleset["nftables"]:
|
||||
rule = _object.get("rule")
|
||||
if rule and self.is_mailcow_jump(rule):
|
||||
_rule_opts = {'family': _family, 'table': 'filter', 'chain': rule['chain'], 'handle': rule['handle']}
|
||||
delete_jumps['nftables'].append({'delete': {'rule': _rule_opts}})
|
||||
if len(delete_jumps['nftables']) > 1:
|
||||
self.nft_exec_dict(delete_jumps)
|
||||
|
||||
for _object in kernel_ruleset["nftables"]:
|
||||
if not _object.get("rule"):
|
||||
continue
|
||||
|
||||
rule = _object["rule"]
|
||||
if nft_input_chain and rule["chain"] == nft_input_chain:
|
||||
if rule.get("comment") and rule["comment"] == "mailcow":
|
||||
input_jump_found = True
|
||||
if nft_forward_chain and rule["chain"] == nft_forward_chain:
|
||||
if rule.get("comment") and rule["comment"] == "mailcow":
|
||||
forward_jump_found = True
|
||||
|
||||
if not input_jump_found:
|
||||
command = self.get_mailcow_jump_rule_dict(_family, nft_input_chain)
|
||||
self.nft_exec_dict(command)
|
||||
|
||||
if not forward_jump_found:
|
||||
command = self.get_mailcow_jump_rule_dict(_family, nft_forward_chain)
|
||||
self.nft_exec_dict(command)
|
||||
if self.list_rules(_family, 'filter', self.chain_name) is None:
|
||||
return
|
||||
mailcow_chain = {'family': _family, 'table': 'filter', 'name': self.chain_name}
|
||||
command = self.get_base_dict()
|
||||
command['nftables'].append({'flush': {'chain': mailcow_chain}})
|
||||
command['nftables'].append({'delete': {'chain': mailcow_chain}})
|
||||
if self.nft_exec_dict(command):
|
||||
self.logger.logInfo(f"Clear completed: {_family}")
|
||||
|
||||
def delete_nat_rule(self, _family:str, _chain: str, _handle:str):
|
||||
delete_command = self.get_base_dict()
|
||||
@@ -285,23 +242,14 @@ class NFTables:
|
||||
# no postrouting chain, may occur if docker has ipv6 disabled.
|
||||
if not chain_name: return
|
||||
|
||||
# Command: nft list chain <family> nat <chain_name>
|
||||
_chain_opts = {'family': _family, 'table': 'nat', 'name': chain_name}
|
||||
_list = {'list':{'chain': _chain_opts} }
|
||||
command = self.get_base_dict()
|
||||
command['nftables'].append(_list)
|
||||
kernel_ruleset = self.nft_exec_dict(command)
|
||||
if not kernel_ruleset:
|
||||
rules = self.list_rules(_family, 'nat', chain_name)
|
||||
if rules is None:
|
||||
return
|
||||
|
||||
rule_position = 0
|
||||
rule_handle = None
|
||||
rule_found = False
|
||||
for _object in kernel_ruleset["nftables"]:
|
||||
if not _object.get("rule"):
|
||||
continue
|
||||
|
||||
rule = _object["rule"]
|
||||
for rule in rules:
|
||||
if not rule.get("comment") or not rule["comment"] == "mailcow":
|
||||
rule_position +=1
|
||||
continue
|
||||
@@ -314,23 +262,28 @@ class NFTables:
|
||||
target_net = ipaddress.ip_network(snat_target, strict=False)
|
||||
|
||||
if rule_found:
|
||||
saddr_ip = rule["expr"][0]["match"]["right"]["prefix"]["addr"]
|
||||
saddr_len = int(rule["expr"][0]["match"]["right"]["prefix"]["len"])
|
||||
try:
|
||||
saddr_ip = rule["expr"][0]["match"]["right"]["prefix"]["addr"]
|
||||
saddr_len = int(rule["expr"][0]["match"]["right"]["prefix"]["len"])
|
||||
|
||||
daddr_ip = rule["expr"][1]["match"]["right"]["prefix"]["addr"]
|
||||
daddr_len = int(rule["expr"][1]["match"]["right"]["prefix"]["len"])
|
||||
daddr_ip = rule["expr"][1]["match"]["right"]["prefix"]["addr"]
|
||||
daddr_len = int(rule["expr"][1]["match"]["right"]["prefix"]["len"])
|
||||
|
||||
target_ip = rule["expr"][3]["snat"]["addr"]
|
||||
target_ip = rule["expr"][3]["snat"]["addr"]
|
||||
|
||||
saddr_net = ipaddress.ip_network(saddr_ip + '/' + str(saddr_len), strict=False)
|
||||
daddr_net = ipaddress.ip_network(daddr_ip + '/' + str(daddr_len), strict=False)
|
||||
current_target_net = ipaddress.ip_network(target_ip, strict=False)
|
||||
saddr_net = ipaddress.ip_network(saddr_ip + '/' + str(saddr_len), strict=False)
|
||||
daddr_net = ipaddress.ip_network(daddr_ip + '/' + str(daddr_len), strict=False)
|
||||
current_target_net = ipaddress.ip_network(target_ip, strict=False)
|
||||
|
||||
match = all((
|
||||
dest_net == saddr_net,
|
||||
dest_net == daddr_net,
|
||||
target_net == current_target_net
|
||||
))
|
||||
match = all((
|
||||
dest_net == saddr_net,
|
||||
dest_net == daddr_net,
|
||||
target_net == current_target_net
|
||||
))
|
||||
except (KeyError, IndexError, TypeError, ValueError):
|
||||
# rule of another format, replace it
|
||||
saddr_net = dest_net
|
||||
match = False
|
||||
try:
|
||||
if rule_position == 0:
|
||||
if not match:
|
||||
@@ -377,49 +330,13 @@ class NFTables:
|
||||
except:
|
||||
self.logger.logCrit(f"Error running SNAT on {_family}, retrying...")
|
||||
|
||||
def get_chain_handle(self, _family: str, _table: str, chain_name: str):
|
||||
chain_handle = None
|
||||
# Command: 'nft list chains {family}'
|
||||
_list = {'list': {'chains': {'family': _family} } }
|
||||
command = self.get_base_dict()
|
||||
command['nftables'].append(_list)
|
||||
kernel_ruleset = self.nft_exec_dict(command)
|
||||
if kernel_ruleset:
|
||||
for _object in kernel_ruleset["nftables"]:
|
||||
if not _object.get("chain"):
|
||||
continue
|
||||
chain = _object["chain"]
|
||||
if chain["family"] == _family and chain["table"] == _table and chain["name"] == chain_name:
|
||||
chain_handle = chain["handle"]
|
||||
break
|
||||
return chain_handle
|
||||
|
||||
def get_rules_handle(self, _family: str, _table: str, chain_name: str, _comment_filter = "mailcow"):
|
||||
rule_handle = []
|
||||
# Command: 'nft list chain {family} {table} {chain_name}'
|
||||
_chain_opts = {'family': _family, 'table': _table, 'name': chain_name}
|
||||
_list = {'list': {'chain': _chain_opts} }
|
||||
command = self.get_base_dict()
|
||||
command['nftables'].append(_list)
|
||||
|
||||
kernel_ruleset = self.nft_exec_dict(command)
|
||||
if kernel_ruleset:
|
||||
for _object in kernel_ruleset["nftables"]:
|
||||
if not _object.get("rule"):
|
||||
continue
|
||||
|
||||
rule = _object["rule"]
|
||||
if rule["family"] == _family and rule["table"] == _table and rule["chain"] == chain_name:
|
||||
if rule.get("comment") and rule["comment"] == _comment_filter:
|
||||
rule_handle.append(rule["handle"])
|
||||
return rule_handle
|
||||
|
||||
def get_ban_ip_dict(self, ipaddr: str, _family: str):
|
||||
json_command = self.get_base_dict()
|
||||
rules = self.list_rules(_family, _table, chain_name) or []
|
||||
return [rule["handle"] for rule in rules if rule.get("comment") == _comment_filter]
|
||||
|
||||
def get_ban_rule(self, network, _family: str):
|
||||
expr_opt = []
|
||||
ipaddr_net = ipaddress.ip_network(ipaddr, strict=False)
|
||||
right_dict = {'prefix': {'addr': str(ipaddr_net.network_address), 'len': int(ipaddr_net.prefixlen) } }
|
||||
right_dict = {'prefix': {'addr': str(network.network_address), 'len': int(network.prefixlen) } }
|
||||
|
||||
left_dict = {'payload': {'protocol': _family, 'field': 'saddr'} }
|
||||
match_dict = {'op': '==', 'left': left_dict, 'right': right_dict }
|
||||
@@ -432,228 +349,115 @@ class NFTables:
|
||||
|
||||
rule_dict = {'family': _family, 'table': "filter", 'chain': self.chain_name, 'expr': expr_opt}
|
||||
|
||||
base_dict = {'insert': {'rule': rule_dict} }
|
||||
json_command["nftables"].append(base_dict)
|
||||
return {'insert': {'rule': rule_dict} }
|
||||
|
||||
return json_command
|
||||
def ban_rule_network(self, rule: dict, _family: str):
|
||||
# Source network of a ban rule (saddr match and drop), None for every other rule
|
||||
if rule.get("comment") in self.ISOLATION_COMMENTS:
|
||||
return None
|
||||
exprs = [expr for expr in rule.get("expr", []) if isinstance(expr, dict)]
|
||||
if not any('drop' in expr or 'reject' in expr for expr in exprs):
|
||||
return None
|
||||
for expr in exprs:
|
||||
match = expr.get("match")
|
||||
if not isinstance(match, dict) or match.get("op") != "==":
|
||||
continue
|
||||
left = match.get("left")
|
||||
payload = left.get("payload") if isinstance(left, dict) else None
|
||||
if not isinstance(payload, dict) or payload.get("protocol") != _family or payload.get("field") != "saddr":
|
||||
continue
|
||||
right = match.get("right")
|
||||
try:
|
||||
if isinstance(right, dict) and isinstance(right.get("prefix"), dict):
|
||||
return ipaddress.ip_network(f'{right["prefix"]["addr"]}/{right["prefix"]["len"]}', strict=False)
|
||||
if isinstance(right, str):
|
||||
return ipaddress.ip_network(right, strict=False)
|
||||
except (KeyError, ValueError):
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_unban_ip_dict(self, ipaddr:str, _family: str):
|
||||
json_command = self.get_base_dict()
|
||||
# Command: 'nft list chain {s_family} filter MAILCOW'
|
||||
_chain_opts = {'family': _family, 'table': 'filter', 'name': self.chain_name}
|
||||
_list = {'list': {'chain': _chain_opts} }
|
||||
command = self.get_base_dict()
|
||||
command['nftables'].append(_list)
|
||||
kernel_ruleset = self.nft_exec_dict(command)
|
||||
rule_handle = None
|
||||
if kernel_ruleset:
|
||||
for _object in kernel_ruleset["nftables"]:
|
||||
if not _object.get("rule"):
|
||||
continue
|
||||
def split_families(self, sources):
|
||||
families = {}
|
||||
for source in sources:
|
||||
network = ipaddress.ip_network(source, strict=False)
|
||||
families.setdefault('ip' if network.version == 4 else 'ip6', []).append(network)
|
||||
return families
|
||||
|
||||
rule = _object["rule"]["expr"][0]["match"]
|
||||
if not "payload" in rule["left"]:
|
||||
continue
|
||||
left_opt = rule["left"]["payload"]
|
||||
if not left_opt["protocol"] == _family:
|
||||
continue
|
||||
if not left_opt["field"] =="saddr":
|
||||
continue
|
||||
def ban(self, sources):
|
||||
# One drop rule per network, existing rules are kept. Returns the networks that could not be banned
|
||||
failed = []
|
||||
for _family, networks in self.split_families(sources).items():
|
||||
rules = self.list_rules(_family, 'filter', self.chain_name)
|
||||
if rules is None:
|
||||
failed += networks
|
||||
continue
|
||||
existing = {self.ban_rule_network(rule, _family) for rule in rules}
|
||||
missing = [network for network in dict.fromkeys(networks) if network not in existing]
|
||||
if not missing: continue
|
||||
|
||||
# ip currently banned
|
||||
rule_right = rule["right"]
|
||||
if isinstance(rule_right, dict):
|
||||
current_rule_ip = rule_right["prefix"]["addr"] + '/' + str(rule_right["prefix"]["len"])
|
||||
else:
|
||||
current_rule_ip = rule_right
|
||||
current_rule_net = ipaddress.ip_network(current_rule_ip)
|
||||
command = self.get_base_dict()
|
||||
for network in missing:
|
||||
command['nftables'].append(self.get_ban_rule(network, _family))
|
||||
if not self.nft_exec_dict(command):
|
||||
failed += missing
|
||||
return [str(network) for network in failed]
|
||||
|
||||
# ip to ban
|
||||
candidate_net = ipaddress.ip_network(ipaddr, strict=False)
|
||||
def unban(self, sources):
|
||||
# Removes every rule of the networks, also duplicates of older versions. Returns the unbanned networks
|
||||
removed = []
|
||||
for _family, networks in self.split_families(sources).items():
|
||||
rules = self.list_rules(_family, 'filter', self.chain_name)
|
||||
if not rules: continue
|
||||
|
||||
if current_rule_net == candidate_net:
|
||||
rule_handle = _object["rule"]["handle"]
|
||||
break
|
||||
|
||||
if rule_handle is not None:
|
||||
mailcow_rule = {'family': _family, 'table': 'filter', 'chain': self.chain_name, 'handle': rule_handle}
|
||||
delete_rule = {'delete': {'rule': mailcow_rule} }
|
||||
json_command["nftables"].append(delete_rule)
|
||||
else:
|
||||
return False
|
||||
|
||||
return json_command
|
||||
|
||||
def check_mailcow_chains(self, family: str, chain: str):
|
||||
position = 0
|
||||
rule_found = False
|
||||
chain_name = self.nft_chain_names[family]['filter'][chain]
|
||||
|
||||
if not chain_name: return None
|
||||
|
||||
_chain_opts = {'family': family, 'table': 'filter', 'name': chain_name}
|
||||
_list = {'list': {'chain': _chain_opts}}
|
||||
command = self.get_base_dict()
|
||||
command['nftables'].append(_list)
|
||||
kernel_ruleset = self.nft_exec_dict(command)
|
||||
if kernel_ruleset:
|
||||
for _object in kernel_ruleset["nftables"]:
|
||||
if not _object.get("rule"):
|
||||
continue
|
||||
rule = _object["rule"]
|
||||
if rule.get("comment") and rule["comment"] == "mailcow":
|
||||
rule_found = True
|
||||
break
|
||||
|
||||
position+=1
|
||||
|
||||
return position if rule_found else False
|
||||
targets = set(networks)
|
||||
command = self.get_base_dict()
|
||||
found = set()
|
||||
for rule in rules:
|
||||
network = self.ban_rule_network(rule, _family)
|
||||
if network in targets:
|
||||
_rule_opts = {'family': _family, 'table': 'filter', 'chain': self.chain_name, 'handle': rule['handle']}
|
||||
command['nftables'].append({'delete': {'rule': _rule_opts}})
|
||||
found.add(network)
|
||||
if found and self.nft_exec_dict(command):
|
||||
removed += found
|
||||
return [str(network) for network in removed]
|
||||
|
||||
def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""):
|
||||
family = "ip"
|
||||
table = "filter"
|
||||
comment_filter_drop = "mailcow isolation"
|
||||
comment_filter_allow = "mailcow isolation allow"
|
||||
json_command = self.get_base_dict()
|
||||
# One rule per port: iptables-nft cannot show an anonymous set, a host that saves and restores the ruleset
|
||||
# with iptables-save (e.g. netfilter-persistent) would get a drop for all ports
|
||||
for comment in self.ISOLATION_COMMENTS:
|
||||
for handle in self.get_rules_handle("ip", "filter", self.chain_name, comment):
|
||||
self.delete_filter_rule("ip", self.chain_name, handle)
|
||||
|
||||
# Delete old mailcow isolation rules
|
||||
handles = self.get_rules_handle(family, table, self.chain_name, comment_filter_drop)
|
||||
for handle in handles:
|
||||
self.delete_filter_rule(family, self.chain_name, handle)
|
||||
handles = self.get_rules_handle(family, table, self.chain_name, comment_filter_allow)
|
||||
for handle in handles:
|
||||
self.delete_filter_rule(family, self.chain_name, handle)
|
||||
|
||||
# insert mailcow isolation rule
|
||||
_match_dict_drop = [
|
||||
{
|
||||
"match": {
|
||||
"op": "!=",
|
||||
"left": {
|
||||
"meta": {
|
||||
"key": "iifname"
|
||||
}
|
||||
},
|
||||
"right": _interface
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"op": "==",
|
||||
"left": {
|
||||
"meta": {
|
||||
"key": "oifname"
|
||||
}
|
||||
},
|
||||
"right": _interface
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"op": "==",
|
||||
"left": {
|
||||
"payload": {
|
||||
"protocol": "tcp",
|
||||
"field": "dport"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
"set": _dports
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"counter": {
|
||||
"packets": 0,
|
||||
"bytes": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"drop": None
|
||||
}
|
||||
]
|
||||
rule_drop = { "insert": { "rule": {
|
||||
"family": family,
|
||||
"table": table,
|
||||
"chain": self.chain_name,
|
||||
"comment": comment_filter_drop,
|
||||
"expr": _match_dict_drop
|
||||
}}}
|
||||
json_command["nftables"].append(rule_drop)
|
||||
|
||||
# insert mailcow isolation allow rule
|
||||
if _allow != "":
|
||||
_match_dict_allow = [
|
||||
{
|
||||
"match": {
|
||||
"op": "==",
|
||||
"left": {
|
||||
"payload": {
|
||||
"protocol": "ip",
|
||||
"field": "saddr"
|
||||
}
|
||||
},
|
||||
"right": _allow
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"op": "!=",
|
||||
"left": {
|
||||
"meta": {
|
||||
"key": "iifname"
|
||||
}
|
||||
},
|
||||
"right": _interface
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"op": "==",
|
||||
"left": {
|
||||
"meta": {
|
||||
"key": "oifname"
|
||||
}
|
||||
},
|
||||
"right": _interface
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": {
|
||||
"op": "==",
|
||||
"left": {
|
||||
"payload": {
|
||||
"protocol": "tcp",
|
||||
"field": "dport"
|
||||
}
|
||||
},
|
||||
"right": {
|
||||
"set": _dports
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"counter": {
|
||||
"packets": 0,
|
||||
"bytes": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"accept": None
|
||||
}
|
||||
]
|
||||
rule_allow = { "insert": { "rule": {
|
||||
"family": family,
|
||||
"table": table,
|
||||
"chain": self.chain_name,
|
||||
"comment": comment_filter_allow,
|
||||
"expr": _match_dict_allow
|
||||
}}}
|
||||
json_command["nftables"].append(rule_allow)
|
||||
|
||||
success = self.nft_exec_dict(json_command)
|
||||
if success == False:
|
||||
if not self.nft_exec_dict(self.get_isolation_dict(_interface, _dports, self.ISOLATION_COMMENTS[0], {"drop": None})):
|
||||
self.logger.logCrit(f"Error adding {self.chain_name} isolation")
|
||||
return False
|
||||
|
||||
return True
|
||||
# separate, an invalid address must not prevent the drop rules. Inserted last, so they are above them
|
||||
if _allow and not self.nft_exec_dict(self.get_isolation_dict(_interface, _dports, self.ISOLATION_COMMENTS[1], {"accept": None}, _allow)):
|
||||
self.logger.logCrit(f"Error adding {self.chain_name} isolation exception for {_allow}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_isolation_dict(self, _interface:str, _dports:list, _comment:str, _verdict:dict, _saddr:str = ""):
|
||||
json_command = self.get_base_dict()
|
||||
for port in _dports:
|
||||
expr = []
|
||||
if _saddr:
|
||||
expr.append({"match": {"op": "==", "left": {"payload": {"protocol": "ip", "field": "saddr"}}, "right": _saddr}})
|
||||
expr += [
|
||||
{"match": {"op": "!=", "left": {"meta": {"key": "iifname"}}, "right": _interface}},
|
||||
{"match": {"op": "==", "left": {"meta": {"key": "oifname"}}, "right": _interface}},
|
||||
{"match": {"op": "==", "left": {"payload": {"protocol": "tcp", "field": "dport"}}, "right": int(port)}},
|
||||
{"counter": {"packets": 0, "bytes": 0}},
|
||||
_verdict
|
||||
]
|
||||
json_command["nftables"].append({"insert": {"rule": {
|
||||
"family": "ip",
|
||||
"table": "filter",
|
||||
"chain": self.chain_name,
|
||||
"comment": _comment,
|
||||
"expr": expr
|
||||
}}})
|
||||
return json_command
|
||||
@@ -0,0 +1,194 @@
|
||||
import ipaddress
|
||||
import threading
|
||||
import dns.exception
|
||||
import dns.resolver
|
||||
from modules.BanManager import BanManager
|
||||
from modules.Options import Options
|
||||
from modules.Regexes import Regexes
|
||||
|
||||
|
||||
class Netfilter:
|
||||
"""Runs the workers of netfilter. Every worker is a loop that logs errors and goes on, so neither a broken log
|
||||
line nor a Redis restart can stop the expiry of bans."""
|
||||
|
||||
ISOLATION_INTERFACE = 'br-mailcow'
|
||||
ISOLATION_PORTS = [3306, 6379, 8983, 12345]
|
||||
|
||||
def __init__(self, r, tables, logger, env):
|
||||
self.r = r
|
||||
self.tables = tables
|
||||
self.logger = logger
|
||||
self.env = env
|
||||
# reentrant, bans.reapply() runs within the chain check
|
||||
self.lock = threading.RLock()
|
||||
self.stop = threading.Event()
|
||||
self.options = Options(logger)
|
||||
self.regexes = Regexes(logger)
|
||||
self.bans = BanManager(r, tables, self.options, logger, self.lock)
|
||||
self.snat4_target = self.snat_target('SNAT_TO_SOURCE', 4)
|
||||
self.snat6_target = self.snat_target('SNAT6_TO_SOURCE', 6)
|
||||
self.resolved = {}
|
||||
self.workers = {
|
||||
'watch': (self.watch, 3),
|
||||
'purge': (self.purge, 10),
|
||||
'chains': (self.check_chains, 10),
|
||||
'lists': (self.update_lists, 60),
|
||||
}
|
||||
if self.snat4_target or self.snat6_target:
|
||||
self.workers['snat'] = (self.snat, 10)
|
||||
self.threads = {}
|
||||
|
||||
def setup(self):
|
||||
if self.r.exists('F2B_LOG'):
|
||||
self.r.rename('F2B_LOG', 'NETFILTER_LOG')
|
||||
# filled again by the lists worker
|
||||
self.r.delete('F2B_PERM_BANS')
|
||||
self.options.refresh(self.r)
|
||||
self.regexes.refresh(self.r)
|
||||
with self.lock:
|
||||
self.tables.clearIPv4Table()
|
||||
self.tables.clearIPv6Table()
|
||||
self.logger.logInfo('Initializing mailcow netfilter chain')
|
||||
self.tables.initChainIPv4()
|
||||
self.tables.initChainIPv6()
|
||||
self.isolate()
|
||||
self.bans.restore()
|
||||
|
||||
def run(self):
|
||||
for name in self.workers:
|
||||
self.start(name)
|
||||
while not self.stop.wait(5):
|
||||
for name, thread in list(self.threads.items()):
|
||||
if not thread.is_alive():
|
||||
self.logger.logCrit('Worker %s stopped unexpectedly, restarting it' % name)
|
||||
self.start(name)
|
||||
|
||||
def shutdown(self):
|
||||
"""Removes the firewall rules, the active bans stay in Redis and are restored on the next start"""
|
||||
self.stop.set()
|
||||
for thread in self.threads.values():
|
||||
thread.join(timeout=5)
|
||||
with self.lock:
|
||||
self.logger.logInfo('Clearing all bans')
|
||||
self.tables.clearIPv4Table()
|
||||
self.tables.clearIPv6Table()
|
||||
try:
|
||||
self.r.delete('F2B_PERM_BANS')
|
||||
except Exception as ex:
|
||||
self.logger.logWarn('Error clearing redis key F2B_PERM_BANS: %s' % ex)
|
||||
|
||||
def start(self, name):
|
||||
task, interval = self.workers[name]
|
||||
thread = threading.Thread(target=self.loop, args=(name, task, interval), name=name, daemon=True)
|
||||
self.threads[name] = thread
|
||||
thread.start()
|
||||
|
||||
def loop(self, name, task, interval):
|
||||
while not self.stop.is_set():
|
||||
try:
|
||||
task()
|
||||
except Exception as ex:
|
||||
self.logger.logWarn('Error in worker %s: %s' % (name, ex))
|
||||
self.stop.wait(interval)
|
||||
|
||||
def watch(self):
|
||||
pubsub = self.r.pubsub(ignore_subscribe_messages=True)
|
||||
try:
|
||||
pubsub.subscribe('F2B_CHANNEL')
|
||||
self.logger.logInfo('Watching Redis channel F2B_CHANNEL')
|
||||
while not self.stop.is_set():
|
||||
message = pubsub.get_message(timeout=1.0)
|
||||
if message and message['type'] == 'message' and message['data']:
|
||||
self.handle_line(message['data'])
|
||||
finally:
|
||||
try:
|
||||
pubsub.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def handle_line(self, line):
|
||||
# a broken line must not end the watcher
|
||||
try:
|
||||
result = self.regexes.match(line)
|
||||
if result is None:
|
||||
return
|
||||
rule_id, address = result
|
||||
ip = Regexes.parse_ip(address)
|
||||
if ip is None:
|
||||
self.logger.logWarn('Rule id %s matched an invalid address %s (%s)' % (rule_id, address, line))
|
||||
return
|
||||
if not self.external(ip):
|
||||
return
|
||||
self.logger.logWarn('%s matched rule id %s (%s)' % (ip, rule_id, line))
|
||||
self.bans.attempt(ip)
|
||||
except Exception as ex:
|
||||
self.logger.logWarn('Error processing log line %r: %s' % (line, ex))
|
||||
|
||||
@staticmethod
|
||||
def external(ip):
|
||||
# private and loopback addresses are never banned
|
||||
return ip is not None and not (ip.is_private or ip.is_loopback)
|
||||
|
||||
def purge(self):
|
||||
self.options.refresh(self.r)
|
||||
self.regexes.refresh(self.r)
|
||||
self.bans.purge()
|
||||
|
||||
def check_chains(self):
|
||||
with self.lock:
|
||||
if self.tables.ensure_chains():
|
||||
self.isolate()
|
||||
self.bans.reapply()
|
||||
|
||||
def snat(self):
|
||||
with self.lock:
|
||||
if self.snat4_target:
|
||||
self.tables.snat4(self.snat4_target, self.env.get('IPV4_NETWORK', '172.22.1') + '.0/24')
|
||||
if self.snat6_target:
|
||||
self.tables.snat6(self.snat6_target, self.env.get('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64'))
|
||||
|
||||
def update_lists(self):
|
||||
self.bans.set_allowlist(self.resolve(self.r.hkeys('F2B_WHITELIST')))
|
||||
self.bans.set_denylist(self.resolve(self.r.hkeys('F2B_BLACKLIST')))
|
||||
|
||||
def isolate(self):
|
||||
if self.env.get('DISABLE_NETFILTER_ISOLATION_RULE', '').lower() in ('y', 'yes'):
|
||||
self.logger.logInfo('Skipping MAILCOW isolation')
|
||||
return
|
||||
self.logger.logInfo('Setting MAILCOW isolation')
|
||||
self.tables.create_mailcow_isolation_rule(self.ISOLATION_INTERFACE, self.ISOLATION_PORTS, self.env.get('MAILCOW_REPLICA_IP', ''))
|
||||
|
||||
def resolve(self, entries):
|
||||
"""Networks of the entries; hostnames are resolved, on DNS errors to their last known addresses"""
|
||||
resolver = dns.resolver.Resolver()
|
||||
networks = set()
|
||||
for entry in entries:
|
||||
if BanManager.network(entry) is not None:
|
||||
networks.add(entry)
|
||||
continue
|
||||
addresses = set()
|
||||
for rdtype in ['A', 'AAAA']:
|
||||
try:
|
||||
answer = resolver.resolve(qname=entry, rdtype=rdtype, lifetime=3)
|
||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||
continue
|
||||
except dns.exception.DNSException as ex:
|
||||
self.logger.logInfo('Could not resolve %s: %s' % (entry, ex))
|
||||
addresses = self.resolved.get(entry, set())
|
||||
break
|
||||
addresses.update(rdata.to_text() for rdata in answer)
|
||||
self.resolved[entry] = addresses
|
||||
networks |= addresses
|
||||
return networks
|
||||
|
||||
def snat_target(self, name, version):
|
||||
value = self.env.get(name, '')
|
||||
if not value or value == 'n':
|
||||
return None
|
||||
try:
|
||||
if ipaddress.ip_address(value).version == version:
|
||||
return value
|
||||
except ValueError:
|
||||
pass
|
||||
self.logger.logWarn('%s is not a valid IPv%d address' % (value, version))
|
||||
return None
|
||||
@@ -0,0 +1,120 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
|
||||
class Options:
|
||||
"""Fail2ban options of the mailcow UI (Redis F2B_OPTIONS). The values are replaced as a whole and never changed
|
||||
in place, so a thread that took them (options.values) keeps a consistent set."""
|
||||
|
||||
DEFAULTS = {
|
||||
'ban_time': 1800,
|
||||
'max_ban_time': 10000,
|
||||
'ban_time_increment': True,
|
||||
'max_attempts': 10,
|
||||
'retry_window': 600,
|
||||
'netban_ipv4': 32,
|
||||
'netban_ipv6': 128,
|
||||
'manage_external': 0,
|
||||
}
|
||||
# Single keys of old installations, read while F2B_OPTIONS does not exist
|
||||
LEGACY_KEYS = {
|
||||
'ban_time': 'F2B_BAN_TIME',
|
||||
'max_ban_time': 'F2B_MAX_BAN_TIME',
|
||||
'ban_time_increment': 'F2B_BAN_TIME_INCREMENT',
|
||||
'max_attempts': 'F2B_MAX_ATTEMPTS',
|
||||
'retry_window': 'F2B_RETRY_WINDOW',
|
||||
'netban_ipv4': 'F2B_NETBAN_IPV4',
|
||||
'netban_ipv6': 'F2B_NETBAN_IPV6',
|
||||
}
|
||||
|
||||
def __init__(self, logger):
|
||||
self.logger = logger
|
||||
self.values = self.normalize({})
|
||||
|
||||
def refresh(self, r):
|
||||
"""Reads the options of the UI; options it lacks are added to Redis once, existing ones are never overwritten"""
|
||||
raw = r.get('F2B_OPTIONS')
|
||||
if raw:
|
||||
try:
|
||||
stored = json.loads(raw)
|
||||
except ValueError as ex:
|
||||
self.logger.logWarn('F2B_OPTIONS is no valid JSON, keeping the current options: %s' % ex)
|
||||
return
|
||||
if not isinstance(stored, dict):
|
||||
self.logger.logWarn('F2B_OPTIONS is no JSON object, keeping the current options')
|
||||
return
|
||||
else:
|
||||
stored = {key: r.get(name) for key, name in self.LEGACY_KEYS.items()}
|
||||
stored = {key: value for key, value in stored.items() if value is not None}
|
||||
|
||||
values = self.normalize(stored)
|
||||
self.values = values
|
||||
# First start or old installation: store the complete set, else only what the UI has not saved yet
|
||||
missing = {key: value for key, value in values.items() if not raw or stored.get(key) is None}
|
||||
if missing:
|
||||
self.add_missing(r, missing)
|
||||
|
||||
def ban_time(self, ban_counter):
|
||||
"""Seconds of the next ban of a network that was banned ban_counter times before"""
|
||||
values = self.values
|
||||
ban_time = values['ban_time']
|
||||
if values['ban_time_increment']:
|
||||
# the exponent is capped, max_ban_time limits the result anyway
|
||||
ban_time = ban_time * 2 ** min(ban_counter, 32)
|
||||
return max(values['ban_time'], min(ban_time, values['max_ban_time']))
|
||||
|
||||
@classmethod
|
||||
def normalize(cls, stored):
|
||||
values = dict(stored)
|
||||
for key, default in cls.DEFAULTS.items():
|
||||
if isinstance(default, bool):
|
||||
values[key] = cls.to_bool(stored.get(key), default)
|
||||
else:
|
||||
values[key] = cls.to_int(stored.get(key), default)
|
||||
values['ban_time'] = max(1, values['ban_time'])
|
||||
values['max_ban_time'] = max(1, values['max_ban_time'])
|
||||
values['max_attempts'] = max(1, values['max_attempts'])
|
||||
values['retry_window'] = max(1, values['retry_window'])
|
||||
# A short prefix would ban whole regions or everything (/0)
|
||||
values['netban_ipv4'] = min(32, max(8, values['netban_ipv4']))
|
||||
values['netban_ipv6'] = min(128, max(8, values['netban_ipv6']))
|
||||
values['manage_external'] = 1 if values['manage_external'] == 1 else 0
|
||||
if not values.get('banlist_id'):
|
||||
values['banlist_id'] = str(uuid.uuid4())
|
||||
return values
|
||||
|
||||
@staticmethod
|
||||
def to_int(value, default):
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def to_bool(value, default):
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if isinstance(value, (int, float)):
|
||||
return value != 0
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in ('1', 'true', 'yes', 'on')
|
||||
return default
|
||||
|
||||
def add_missing(self, r, missing):
|
||||
# WATCH keeps a save of the UI between reading and writing
|
||||
def fill(pipe):
|
||||
raw = pipe.get('F2B_OPTIONS')
|
||||
current = json.loads(raw) if raw else {}
|
||||
if not isinstance(current, dict):
|
||||
return
|
||||
added = {key: value for key, value in missing.items() if current.get(key) is None}
|
||||
if not added:
|
||||
return
|
||||
current.update(added)
|
||||
pipe.multi()
|
||||
pipe.set('F2B_OPTIONS', json.dumps(current, ensure_ascii=False))
|
||||
|
||||
try:
|
||||
r.transaction(fill, 'F2B_OPTIONS')
|
||||
except Exception as ex:
|
||||
self.logger.logWarn('Could not add default options to F2B_OPTIONS: %s' % ex)
|
||||
@@ -0,0 +1,141 @@
|
||||
import ipaddress
|
||||
import json
|
||||
import re
|
||||
|
||||
|
||||
class Regexes:
|
||||
"""Rules for log lines (Redis F2B_REGEX): rule id => regex whose first group captures the client address"""
|
||||
|
||||
# Anchored to the line start: text clients control in other lines (login names, SMTP commands) must not match.
|
||||
# Failed IMAP, POP3, Sieve and SMTP logins are reported by mailcowauth.php (rule 3), not by Dovecot or Postfix.
|
||||
# Only the master passdb is checked by Dovecot itself (rule 8); the last address is the one Dovecot added.
|
||||
DEFAULTS = {
|
||||
1: r'^mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)$',
|
||||
2: r'^Rspamd UI: Invalid password by ([0-9a-f\.:]+)$',
|
||||
3: r'^MAILCOWAUTH: Login failed for user .+ with service \w* from IP ([0-9a-f\.:]+)$',
|
||||
4: r'^warning: non-SMTP command from [^\[\]\s]*\[([0-9a-f\.:]+)\]:.+',
|
||||
5: r'^NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+',
|
||||
6: r'^(?:\[\d+\]: )?SOGo\w* Login from \'(?:[0-9a-f\.:]+, )*([0-9a-f\.:]+)\' for user .+ might not have worked',
|
||||
7: r'([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+',
|
||||
8: r'^auth: passwd-file\(.*,([0-9a-f\.:]+),master(?:,<[^<>]*>)?\): (?:Password mismatch|unknown user)',
|
||||
}
|
||||
# Former default rules (with any rule id) => rule id of their replacement, None: the rule is removed.
|
||||
# Rules the admin changed are kept.
|
||||
FORMER_DEFAULTS = {
|
||||
r'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)': 1,
|
||||
r'Rspamd UI: Invalid password by ([0-9a-f\.:]+)': 2,
|
||||
r'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed': 3,
|
||||
r'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+': 3,
|
||||
r'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+': 4,
|
||||
r'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+': 5,
|
||||
r"SOGo.+ Login from '([0-9a-f\.:]+)' for user .+ might not have worked": 6,
|
||||
r'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked': 6,
|
||||
r'-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),': None,
|
||||
r'-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),': None,
|
||||
r'-login: Aborted login \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+': None,
|
||||
r'-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+': None,
|
||||
r'-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+': None,
|
||||
r'-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+': None,
|
||||
r'auth: static\([^,]+,([0-9a-f\.:]+),<[^>]+>\): Password mismatch \(SHA1 of given password: [a-f0-9]+\)': 8,
|
||||
r'auth: \w+\([^,]+,([0-9a-f\.:]+),<[^>]+>\): Password mismatch \(SHA1 of given password: [a-f0-9]+\)': 8,
|
||||
r'\w+\([^,]+,([0-9a-f\.:]+),<[^>]+>\): Password mismatch \(SHA1 of given password: [a-f0-9]+\)': 8,
|
||||
r'\w+\([^,]+,([0-9a-f\.:]+),<[^>]+>\): unknown user \(SHA1 of given password: [a-f0-9]+\)': None,
|
||||
}
|
||||
# Lines of the web UI are not sanitized like syslog lines: a newline in a login name must not hide the address
|
||||
CONTROL_CHARS = re.compile(r'[\x00-\x1f\x7f]')
|
||||
|
||||
def __init__(self, logger):
|
||||
self.logger = logger
|
||||
self.rules = []
|
||||
self.source = None
|
||||
|
||||
def refresh(self, r):
|
||||
"""Compiles the rules of the UI again when they changed; invalid rules are skipped"""
|
||||
raw = r.get('F2B_REGEX')
|
||||
if not raw:
|
||||
raw = json.dumps(self.DEFAULTS, ensure_ascii=False)
|
||||
r.set('F2B_REGEX', raw, nx=True)
|
||||
if raw == self.source:
|
||||
return
|
||||
try:
|
||||
stored = json.loads(raw)
|
||||
except ValueError as ex:
|
||||
self.logger.logWarn('F2B_REGEX is no valid JSON, keeping the current rules: %s' % ex)
|
||||
return
|
||||
if not isinstance(stored, dict):
|
||||
self.logger.logWarn('F2B_REGEX is no JSON object, keeping the current rules')
|
||||
return
|
||||
migrated = self.migrate(stored)
|
||||
if migrated != stored:
|
||||
self.store_migrated(r)
|
||||
stored = migrated
|
||||
|
||||
rules = []
|
||||
for rule_id, regex in sorted(stored.items(), key=lambda item: self.sort_key(item[0])):
|
||||
try:
|
||||
rules.append((str(rule_id), re.compile(regex)))
|
||||
except (re.error, TypeError) as ex:
|
||||
self.logger.logWarn('Skipping regex of rule id %s: %s' % (rule_id, ex))
|
||||
self.rules = rules
|
||||
self.source = raw
|
||||
|
||||
def match(self, line):
|
||||
"""(rule id, captured address) of the first rule that matches the line, None if no rule matches"""
|
||||
line = self.CONTROL_CHARS.sub('_', line)
|
||||
for rule_id, regex in self.rules:
|
||||
result = regex.search(line)
|
||||
if result and result.lastindex and result.group(1):
|
||||
return rule_id, result.group(1)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def migrate(cls, stored):
|
||||
"""The stored rules with former default rules replaced by the current ones (or removed)"""
|
||||
migrated = {}
|
||||
for rule_id, regex in stored.items():
|
||||
if isinstance(regex, str) and regex in cls.FORMER_DEFAULTS:
|
||||
replacement = cls.FORMER_DEFAULTS[regex]
|
||||
if replacement is None:
|
||||
continue
|
||||
regex = cls.DEFAULTS[replacement]
|
||||
migrated[rule_id] = regex
|
||||
return migrated
|
||||
|
||||
def store_migrated(self, r):
|
||||
# WATCH keeps a save of the UI between reading and writing
|
||||
def replace(pipe):
|
||||
current = json.loads(pipe.get('F2B_REGEX') or '{}')
|
||||
if not isinstance(current, dict):
|
||||
return
|
||||
migrated = self.migrate(current)
|
||||
if migrated == current:
|
||||
return
|
||||
pipe.multi()
|
||||
pipe.set('F2B_REGEX', json.dumps(migrated, ensure_ascii=False))
|
||||
|
||||
try:
|
||||
r.transaction(replace, 'F2B_REGEX')
|
||||
self.logger.logInfo('Replaced former default rules in F2B_REGEX')
|
||||
except Exception as ex:
|
||||
self.logger.logWarn('Could not replace former default rules in F2B_REGEX: %s' % ex)
|
||||
|
||||
@staticmethod
|
||||
def parse_ip(text):
|
||||
"""IP address of a captured address, IPv4-mapped IPv6 as IPv4; a trailing colon (e.g. "1.2.3.4:") is
|
||||
tolerated. None if the text is no address."""
|
||||
for candidate in (text, text.rstrip(':')):
|
||||
try:
|
||||
ip = ipaddress.ip_address(candidate)
|
||||
except ValueError:
|
||||
continue
|
||||
if ip.version == 6 and ip.ipv4_mapped:
|
||||
return ip.ipv4_mapped
|
||||
return ip
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def sort_key(rule_id):
|
||||
try:
|
||||
return (0, int(rule_id))
|
||||
except (TypeError, ValueError):
|
||||
return (1, str(rule_id))
|
||||
@@ -3,7 +3,8 @@ LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
|
||||
RUN apk add --no-cache nginx \
|
||||
RUN apk upgrade --no-cache && \
|
||||
apk add --no-cache nginx \
|
||||
python3 \
|
||||
py3-pip && \
|
||||
pip install --upgrade pip && \
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
@@ -44,6 +45,11 @@ def prepare_template_vars():
|
||||
ipv4_network = os.getenv("IPV4_NETWORK", "172.22.1")
|
||||
additional_server_names = os.getenv("ADDITIONAL_SERVER_NAMES", "")
|
||||
trusted_proxies = os.getenv("TRUSTED_PROXIES", "")
|
||||
# nginx size syntax, invalid values fall back to the default
|
||||
max_body_size = os.getenv("NGINX_MAX_BODY_SIZE", "128m").strip()
|
||||
if not re.fullmatch(r'[0-9]+[kKmMgG]?', max_body_size):
|
||||
print(f"Invalid NGINX_MAX_BODY_SIZE '{max_body_size}', using 128m")
|
||||
max_body_size = "128m"
|
||||
|
||||
template_vars = {
|
||||
'IPV4_NETWORK': ipv4_network,
|
||||
@@ -60,6 +66,7 @@ def prepare_template_vars():
|
||||
'PHPFPMHOST': os.getenv("PHPFPMHOST", "php-fpm-mailcow"),
|
||||
'ENABLE_IPV6': os.getenv("ENABLE_IPV6", "true").lower() != "false",
|
||||
'HTTP_REDIRECT': os.getenv("HTTP_REDIRECT", "n").lower() in ("y", "yes"),
|
||||
'NGINX_MAX_BODY_SIZE': max_body_size,
|
||||
}
|
||||
|
||||
ssl_dir = '/etc/ssl/mail/'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.24
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ logger.debug('olefy listen address string: {} (type {})'.format(olefy_listen_add
|
||||
if not olefy_listen_addr_string:
|
||||
olefy_listen_addr = ""
|
||||
else:
|
||||
addr_re = re.compile('[\[" \]]')
|
||||
addr_re = re.compile(r'[\[" \]]')
|
||||
olefy_listen_addr = addr_re.sub('', olefy_listen_addr_string.replace("'", "")).split(',')
|
||||
|
||||
# log runtime variables
|
||||
@@ -206,8 +206,9 @@ class AIO(asyncio.Protocol):
|
||||
self.transport.close()
|
||||
|
||||
|
||||
# start the listeners
|
||||
loop = asyncio.get_event_loop()
|
||||
# start the listeners, Python >= 3.14 no longer creates a loop in get_event_loop()
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
# each client connection will create a new protocol instance
|
||||
coro = loop.create_server(AIO, olefy_listen_addr, olefy_listen_port)
|
||||
server = loop.run_until_complete(coro)
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
function array_by_comma { local IFS=","; echo "$*"; }
|
||||
|
||||
# Wait for containers
|
||||
while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
||||
echo "Waiting for SQL..."
|
||||
@@ -132,33 +130,6 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Set API options if env vars are not empty
|
||||
if [[ ${API_ALLOW_FROM} != "invalid" ]] && [[ ! -z ${API_ALLOW_FROM} ]]; then
|
||||
IFS=',' read -r -a API_ALLOW_FROM_ARR <<< "${API_ALLOW_FROM}"
|
||||
declare -a VALIDATED_API_ALLOW_FROM_ARR
|
||||
REGEX_IP6='^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}(/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$'
|
||||
REGEX_IP4='^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/([0-9]|[1-2][0-9]|3[0-2]))?$'
|
||||
for IP in "${API_ALLOW_FROM_ARR[@]}"; do
|
||||
if [[ ${IP} =~ ${REGEX_IP6} ]] || [[ ${IP} =~ ${REGEX_IP4} ]]; then
|
||||
VALIDATED_API_ALLOW_FROM_ARR+=("${IP}")
|
||||
fi
|
||||
done
|
||||
VALIDATED_IPS=$(array_by_comma ${VALIDATED_API_ALLOW_FROM_ARR[*]})
|
||||
if [[ ! -z ${VALIDATED_IPS} ]]; then
|
||||
if [[ ${API_KEY} != "invalid" ]] && [[ ! -z ${API_KEY} ]]; then
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
DELETE FROM api WHERE access = 'rw';
|
||||
INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY}", "1", "${VALIDATED_IPS}", "rw");
|
||||
EOF
|
||||
fi
|
||||
if [[ ${API_KEY_READ_ONLY} != "invalid" ]] && [[ ! -z ${API_KEY_READ_ONLY} ]]; then
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
DELETE FROM api WHERE access = 'ro';
|
||||
INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY_READ_ONLY}", "1", "${VALIDATED_IPS}", "ro");
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create events (master only, STATUS for event on slave will be SLAVESIDE_DISABLED)
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
|
||||
@@ -4,7 +4,7 @@ WORKDIR /src
|
||||
ENV CGO_ENABLED=0 \
|
||||
GO111MODULE=on \
|
||||
NOOPT=1 \
|
||||
VERSION=1.14.0
|
||||
VERSION=1.14.5
|
||||
|
||||
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
|
||||
cd /src/postfix-tlspol && \
|
||||
|
||||
@@ -2,7 +2,7 @@ FROM debian:trixie-slim
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG RSPAMD_VER=rspamd_4.2.1-1~b7a16ae
|
||||
ARG RSPAMD_VER=rspamd_4.2.3-1~50bde75
|
||||
ARG CODENAME=trixie
|
||||
ENV LC_ALL=C
|
||||
|
||||
|
||||
@@ -277,9 +277,34 @@ if [[ ! -z ${SPAMHAUS_DQS_KEY} ]]; then
|
||||
}
|
||||
}
|
||||
"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";
|
||||
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" {
|
||||
ignore_defaults = true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# SOGo built from source to enable security patch application
|
||||
# Repository: https://github.com/Alinto/sogo
|
||||
# Version: SOGo-5.12.12
|
||||
# Version: SOGo-5.12.13
|
||||
#
|
||||
# Applied security patches:
|
||||
# -
|
||||
@@ -12,12 +12,12 @@ FROM debian:bookworm
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG SOGO_VERSION=SOGo-5.12.12
|
||||
ARG SOPE_VERSION=SOPE-5.12.12
|
||||
ARG SOGO_VERSION=SOGo-5.12.13
|
||||
ARG SOPE_VERSION=SOPE-5.12.13
|
||||
# Security patches to apply (space-separated commit hashes)
|
||||
ARG SOGO_SECURITY_PATCHES=""
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||
ARG GOSU_VERSION=1.19
|
||||
ARG GOSU_VERSION=1.20
|
||||
ENV LC_ALL=C
|
||||
|
||||
# Install dependencies, build SOPE and SOGo, then clean up (all in one layer to minimize image size)
|
||||
|
||||
@@ -13,13 +13,11 @@ do
|
||||
sleep 3
|
||||
done
|
||||
|
||||
# Wait for updated schema
|
||||
# Wait for updated schema (DB_VERSION from docker-compose.yml)
|
||||
DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
|
||||
DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
|
||||
while [[ "${DBV_NOW}" != "${DBV_NEW}" ]]; do
|
||||
while [[ "${DBV_NOW}" != "${DB_VERSION}" ]]; do
|
||||
echo "Waiting for schema update..."
|
||||
DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
|
||||
DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
|
||||
sleep 5
|
||||
done
|
||||
echo "DB schema is ${DBV_NOW}"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.24
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# install unbound from alpine:edge to get security patches
|
||||
# minimum version is pinned
|
||||
RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main "unbound>=1.26.1-r0"
|
||||
RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main "unbound>=1.26.2-r0"
|
||||
|
||||
# install other packages from regular alpine stable repo
|
||||
RUN apk add --update --no-cache \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.24
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -43,6 +43,19 @@ while ! mariadb-admin status --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u$
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Wait for the schema update (DB_VERSION) at startup, do not restart them meanwhile.
|
||||
DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
|
||||
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 --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
|
||||
done
|
||||
if [[ "${DBV_NOW}" != "${DB_VERSION}" ]]; then
|
||||
echo -e "\e[31m$(date) - DB schema is ${DBV_NOW}, expected ${DB_VERSION} - starting checks anyway, check the php-fpm log (init_db)\e[0m"
|
||||
fi
|
||||
|
||||
# Do not attempt to write to slave
|
||||
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
|
||||
REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
|
||||
|
||||
@@ -68,13 +68,17 @@ require_once 'functions.acl.inc.php';
|
||||
|
||||
|
||||
$isSOGoRequest = $post['real_rip'] == getenv('IPV4_NETWORK') . '.248';
|
||||
// real_rip is the address that connected to Dovecot (for proxied connections the proxy), client_ip the client (for logs)
|
||||
$client_ip = filter_var($post['client_ip'] ?? '', FILTER_VALIDATE_IP) ? $post['client_ip'] : $post['real_rip'];
|
||||
$result = false;
|
||||
// set when the password is right, but the account does not allow this login
|
||||
$denied = false;
|
||||
if ($isSOGoRequest) {
|
||||
// This is a SOGo Auth request. First check for SSO password.
|
||||
$sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
|
||||
if ($sogo_sso_pass === $post['password']){
|
||||
error_log('MAILCOWAUTH: SOGo SSO auth for user ' . $post['username']);
|
||||
set_sasl_log($post['username'], $post['real_rip'], "SOGO");
|
||||
set_sasl_log($post['username'], $client_ip, "SOGO");
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
@@ -91,28 +95,48 @@ if ($result === false){
|
||||
'service' => $post['service'],
|
||||
'is_internal' => true,
|
||||
'remote_addr' => $post['real_rip']
|
||||
));
|
||||
), $denied);
|
||||
if ($result) {
|
||||
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $service . " from IP " . $post['real_rip']);
|
||||
set_sasl_log($post['username'], $post['real_rip'], $service);
|
||||
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $service . " from IP " . $client_ip);
|
||||
set_sasl_log($post['username'], $client_ip, $service);
|
||||
}
|
||||
}
|
||||
if ($result === false){
|
||||
// Init Identity Provider
|
||||
$iam_provider = identity_provider('init');
|
||||
$iam_settings = identity_provider('get');
|
||||
$result = user_login($post['username'], $post['password'], array('is_internal' => true, 'service' => $post['service']));
|
||||
try {
|
||||
$result = user_login($post['username'], $post['password'], array('is_internal' => true, 'service' => $post['service']), $denied);
|
||||
} catch (\Mailcow\Auth\IdpUnavailable $e) {
|
||||
// temporary failure for Dovecot, no failed login
|
||||
error_log("MAILCOWAUTH: Identity provider unavailable for user " . $post['username'] . " with service " . $post['service'] . " from IP " . $client_ip . ": " . $e->getMessage());
|
||||
http_response_code(503); // Service Unavailable
|
||||
echo json_encode($return);
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
if ($result) {
|
||||
error_log('MAILCOWAUTH: User auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
|
||||
set_sasl_log($post['username'], $post['real_rip'], $post['service']);
|
||||
error_log('MAILCOWAUTH: User auth for user ' . $post['username'] . " with service " . $service . " from IP " . $client_ip);
|
||||
set_sasl_log($post['username'], $client_ip, $service);
|
||||
}
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
http_response_code(200); // OK
|
||||
$return['success'] = true;
|
||||
} elseif ($denied) {
|
||||
// Logged, but no failed login for netfilter: e.g. a client of a shared mailbox or without IMAP access
|
||||
error_log("MAILCOWAUTH: Login denied for user " . $post['username'] . " with service " . $service . " from IP " . $client_ip);
|
||||
http_response_code(401); // Unauthorized
|
||||
} else {
|
||||
error_log("MAILCOWAUTH: Login failed for user " . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
|
||||
// Same line for the log and netfilter, which counts it for IMAP, POP3, Sieve and SMTP (Postfix asks Dovecot);
|
||||
// SOGo logins have their own rule
|
||||
$failed = "MAILCOWAUTH: Login failed for user " . $post['username'] . " with service " .
|
||||
preg_replace('/[^a-z0-9]/', '', strtolower((string)$service)) . " from IP " . $client_ip;
|
||||
error_log($failed);
|
||||
if (!$isSOGoRequest) {
|
||||
$redis->publish("F2B_CHANNEL", $failed);
|
||||
}
|
||||
http_response_code(401); // Unauthorized
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
function auth_password_verify(request, password)
|
||||
if request.domain == nil then
|
||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
|
||||
end
|
||||
|
||||
local json = require "cjson"
|
||||
local ltn12 = require "ltn12"
|
||||
local https = require "ssl.https"
|
||||
@@ -11,7 +7,10 @@ function auth_password_verify(request, password)
|
||||
local req = {
|
||||
username = request.user,
|
||||
password = password,
|
||||
-- address that connected to Dovecot, for proxied connections the proxy
|
||||
real_rip = request.real_rip,
|
||||
-- address of the client, also for proxied connections
|
||||
client_ip = request.rip,
|
||||
service = request.service
|
||||
}
|
||||
local req_json = json.encode(req)
|
||||
@@ -41,6 +40,12 @@ function auth_password_verify(request, password)
|
||||
return dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE, "Upstream unreachable"
|
||||
end
|
||||
|
||||
-- 5xx: the backend or the identity provider cannot answer, a temporary failure and no failed login
|
||||
if c >= 500 then
|
||||
dovecot.i_info("HTTP request to auth backend failed with " .. tostring(c) .. " for user " .. request.user)
|
||||
return dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE, "Upstream unavailable"
|
||||
end
|
||||
|
||||
if c ~= 200 and c ~= 401 then
|
||||
dovecot.i_info("HTTP request failed with " .. tostring(c) .. " for user " .. request.user)
|
||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error"
|
||||
|
||||
@@ -51,13 +51,6 @@ recipient_delimiter = +
|
||||
auth_master_user_separator = *
|
||||
mail_shared_explicit_inbox = yes
|
||||
mail_prefetch_count = 30
|
||||
passdb {
|
||||
driver = lua
|
||||
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes cache_key=%s:%u:%w
|
||||
result_success = return-ok
|
||||
result_failure = continue
|
||||
result_internalfail = continue
|
||||
}
|
||||
# try a master passwd
|
||||
passdb {
|
||||
driver = passwd-file
|
||||
@@ -67,9 +60,12 @@ passdb {
|
||||
}
|
||||
# check for regular password - if empty (e.g. force-passwd-reset), previous pass=yes passdbs also fail
|
||||
# a return of the following passdb is mandatory
|
||||
# each check asks mailcowauth.php, which reports a failed login to netfilter once
|
||||
# cache_key with password: the lua default (%u) would let a temporary failure fall back to the negative entry
|
||||
# of a passdb lookup (master logins) and reject the login as unknown user
|
||||
passdb {
|
||||
driver = lua
|
||||
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes
|
||||
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes cache_key=%s:%u:%w
|
||||
}
|
||||
# Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
|
||||
service doveadm {
|
||||
|
||||
@@ -47,6 +47,13 @@ http {
|
||||
default "";
|
||||
}
|
||||
|
||||
# Client address for PHP: on the internal sogo-auth listener nginx itself is the client,
|
||||
# the real address comes from the X-Real-IP header set by /sogo-auth-verify
|
||||
map $sogo_auth_internal $mailcow_client_ip {
|
||||
"1" $http_x_real_ip;
|
||||
default $remote_addr;
|
||||
}
|
||||
|
||||
{% if HTTP_REDIRECT %}
|
||||
# HTTP to HTTPS redirect
|
||||
server {
|
||||
|
||||
@@ -22,7 +22,8 @@ add_header Referrer-Policy strict-origin;
|
||||
|
||||
index index.php index.html;
|
||||
|
||||
client_max_body_size 0;
|
||||
# PHP accepts no larger requests (post_max_size), SOGo webmail, CalDAV/CardDAV and ActiveSync use NGINX_MAX_BODY_SIZE
|
||||
client_max_body_size 64m;
|
||||
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
@@ -93,11 +94,11 @@ location / {
|
||||
}
|
||||
|
||||
location /qhandler {
|
||||
rewrite ^/qhandler/(.*)/(.*) /qhandler.php?action=$1&hash=$2;
|
||||
rewrite ^/qhandler/([^/]+)/(.+?)/?$ /qhandler.php?action=$1&hash=$2;
|
||||
}
|
||||
|
||||
location /edit {
|
||||
rewrite ^/edit/(.*)/(.*) /edit.php?$1=$2;
|
||||
rewrite ^/edit/([^/]+)/(.+?)/?$ /edit.php?$1=$2;
|
||||
}
|
||||
|
||||
location ~ ^/api/v1/(.*)$ {
|
||||
@@ -116,7 +117,8 @@ location ~ \.php$ {
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param HTTP_X_REAL_IP $remote_addr;
|
||||
fastcgi_param REMOTE_ADDR $mailcow_client_ip;
|
||||
fastcgi_param HTTP_X_REAL_IP $mailcow_client_ip;
|
||||
# trusted internal-auth marker; empty for external clients (see nginx.conf map)
|
||||
fastcgi_param SOGO_AUTH_INTERNAL $sogo_auth_internal;
|
||||
fastcgi_read_timeout 3600;
|
||||
@@ -181,9 +183,16 @@ location /sogo-auth-verify {
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_pass http://127.0.0.1:65510/sogo-auth;
|
||||
proxy_pass_request_body off;
|
||||
client_max_body_size {{ NGINX_MAX_BODY_SIZE }};
|
||||
}
|
||||
|
||||
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_set $user $upstream_http_x_user;
|
||||
auth_request_set $auth $upstream_http_x_auth;
|
||||
@@ -204,7 +213,7 @@ location ^~ /Microsoft-Server-ActiveSync {
|
||||
proxy_busy_buffers_size 512k;
|
||||
proxy_set_header Host $http_host;
|
||||
client_body_buffer_size 512k;
|
||||
client_max_body_size 0;
|
||||
client_max_body_size {{ NGINX_MAX_BODY_SIZE }};
|
||||
}
|
||||
|
||||
location ^~ /SOGo {
|
||||
@@ -255,7 +264,7 @@ location ^~ /SOGo {
|
||||
proxy_send_timeout 3600;
|
||||
proxy_read_timeout 3600;
|
||||
client_body_buffer_size 128k;
|
||||
client_max_body_size 0;
|
||||
client_max_body_size {{ NGINX_MAX_BODY_SIZE }};
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
require_once(__DIR__ . '/../web/inc/vars.inc.php');
|
||||
if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
|
||||
include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
|
||||
}
|
||||
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
|
||||
|
||||
$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) {
|
||||
fwrite(STDERR, "DB connect failed: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$redis = new Redis();
|
||||
try {
|
||||
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"));
|
||||
} catch (Exception $e) {
|
||||
fwrite(STDERR, "Redis connect failed: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function logMsg($priority, $message, $task = "Group Shares") {
|
||||
global $redis;
|
||||
$redis->lPush('CRON_LOG', json_encode(array(
|
||||
"time" => time(),
|
||||
"priority" => $priority,
|
||||
"task" => $task,
|
||||
"message" => $message,
|
||||
)));
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../web/inc/functions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.docker.inc.php';
|
||||
|
||||
// One run at a time, released when the process ends
|
||||
$lock = fopen('/tmp/group-grants.lock', 'c');
|
||||
if ($lock === false || !flock($lock, LOCK_EX | LOCK_NB)) {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$sharing = \Mailcow\MailboxGroup\MailboxGroupModule::create($pdo, \Mailcow\Core\Context::system())->sharing;
|
||||
|
||||
// Every minute: subscriptions of changed groups; failed ones are retried and logged by the full run
|
||||
if ((int)date('i') % 15 !== 0) {
|
||||
$sharing->syncSubscriptions();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Every 15 minutes: IMAP ACLs on new top-level folders and failed grants, SOGo grants of owners that opened SOGo meanwhile
|
||||
foreach ($sharing->reconcile() as $warning) {
|
||||
logMsg("warning", $warning[0] . ": " . html_entity_decode(implode(", ", array_slice($warning, 1))));
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
require_once(__DIR__ . '/../web/inc/vars.inc.php');
|
||||
if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
|
||||
include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
|
||||
}
|
||||
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
|
||||
|
||||
$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) {
|
||||
fwrite(STDERR, "DB connect failed: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$redis = new Redis();
|
||||
try {
|
||||
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"));
|
||||
} catch (Exception $e) {
|
||||
fwrite(STDERR, "Redis connect failed: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function logMsg($priority, $message, $task = "Identity Sync") {
|
||||
global $redis;
|
||||
$redis->lPush('CRON_LOG', json_encode(array(
|
||||
"time" => time(),
|
||||
"priority" => $priority,
|
||||
"task" => $task,
|
||||
"message" => $message,
|
||||
)));
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../web/inc/functions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/sessions.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
|
||||
$_SESSION['mailcow_cc_username'] = "admin";
|
||||
$_SESSION['mailcow_cc_role'] = "admin";
|
||||
$_SESSION['acl']['tls_policy'] = "1";
|
||||
$_SESSION['acl']['quarantine_notification'] = "1";
|
||||
$_SESSION['acl']['quarantine_category'] = "1";
|
||||
$_SESSION['acl']['ratelimit'] = "1";
|
||||
$_SESSION['acl']['sogo_access'] = "1";
|
||||
$_SESSION['acl']['protocol_access'] = "1";
|
||||
$_SESSION['acl']['mailbox_relayhost'] = "1";
|
||||
$_SESSION['acl']['unlimited_quota'] = "1";
|
||||
|
||||
// One run at a time, released when the process ends
|
||||
$lock = fopen('/tmp/identity-sync.lock', 'c');
|
||||
if ($lock === false || !flock($lock, LOCK_EX | LOCK_NB)) {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Users, offboarding and groups of Keycloak or LDAP; requested runs first, else every sync_interval minutes.
|
||||
// The IdP is only contacted when a run is due.
|
||||
$iam_settings = identity_provider('get');
|
||||
$identity = \Mailcow\Identity\IdentityModule::create($pdo, \Mailcow\Core\Context::system(), $iam_settings, function () {
|
||||
return identity_provider('init');
|
||||
});
|
||||
$run = $identity->sync->runDue();
|
||||
if ($run !== null) {
|
||||
$summary = $run['summary'];
|
||||
if ($run['status'] == 'error') {
|
||||
logMsg("err", ucfirst($run['mode']) . " failed: " . $summary['error']);
|
||||
} else {
|
||||
$counts = array();
|
||||
foreach ($summary['counts'] as $key => $count) {
|
||||
if ($count > 0) $counts[] = $key . "=" . $count;
|
||||
}
|
||||
logMsg(($run['status'] == 'blocked') ? "warning" : "info", ucfirst($run['mode']) . " " . $run['status'] . ": " . (empty($counts) ? "no changes" : implode(", ", $counts)));
|
||||
foreach ($summary['details']['conflicts'] as $conflict) {
|
||||
logMsg("warning", $conflict);
|
||||
}
|
||||
foreach ($summary['errors'] ?? array() as $error) {
|
||||
logMsg("err", $error);
|
||||
}
|
||||
}
|
||||
}
|
||||
session_destroy();
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
require_once(__DIR__ . '/../web/inc/vars.inc.php');
|
||||
if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
|
||||
include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
|
||||
}
|
||||
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
|
||||
|
||||
$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) {
|
||||
fwrite(STDERR, "DB connect failed: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$redis = new Redis();
|
||||
try {
|
||||
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"));
|
||||
} catch (Exception $e) {
|
||||
fwrite(STDERR, "Redis connect failed: " . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
function logMsg($priority, $message, $task = "imapsync OAuth refresh") {
|
||||
global $redis;
|
||||
$redis->lPush('CRON_LOG', json_encode(array(
|
||||
"time" => time(),
|
||||
"priority" => $priority,
|
||||
"task" => $task,
|
||||
"message" => $message,
|
||||
)));
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../web/inc/functions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/sessions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php';
|
||||
|
||||
// File-lock to prevent concurrent runs
|
||||
$lock_file = '/tmp/imapsync-oauth-refresh.lock';
|
||||
if (file_exists($lock_file)) {
|
||||
$pid = (int)trim(@file_get_contents($lock_file));
|
||||
if ($pid > 0 && posix_kill($pid, 0)) {
|
||||
logMsg("info", "Previous refresh still running (pid=$pid), exiting");
|
||||
exit(0);
|
||||
}
|
||||
@unlink($lock_file);
|
||||
}
|
||||
file_put_contents($lock_file, getmypid());
|
||||
|
||||
try {
|
||||
// client_credentials source tokens, unused per-user tokens (1 day grace), authorization_code per-user tokens;
|
||||
// failing tokens are retried 15 minutes after their last attempt (C2), errors are sanitized
|
||||
$imapsync = \Mailcow\Imapsync\ImapsyncModule::create($pdo, \Mailcow\Core\Context::system(), [
|
||||
'hostname' => (string)getenv('MAILCOW_HOSTNAME'),
|
||||
]);
|
||||
foreach ($imapsync->oauth->refreshDue() as $e) {
|
||||
logMsg($e['priority'], $e['message']);
|
||||
}
|
||||
} finally {
|
||||
@unlink($lock_file);
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once(__DIR__ . '/../web/inc/vars.inc.php');
|
||||
if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
|
||||
include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
|
||||
}
|
||||
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
|
||||
|
||||
// Init database
|
||||
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
|
||||
$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) {
|
||||
logMsg("err", $e->getMessage());
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Init Redis
|
||||
$redis = new Redis();
|
||||
try {
|
||||
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"));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
echo "Exiting: " . $e->getMessage();
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
function logMsg($priority, $message, $task = "Keycloak Sync") {
|
||||
global $redis;
|
||||
|
||||
$finalMsg = array(
|
||||
"time" => time(),
|
||||
"priority" => $priority,
|
||||
"task" => $task,
|
||||
"message" => $message
|
||||
);
|
||||
$redis->lPush('CRON_LOG', json_encode($finalMsg));
|
||||
}
|
||||
|
||||
// Load core functions first
|
||||
require_once __DIR__ . '/../web/inc/functions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/sessions.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';
|
||||
|
||||
$_SESSION['mailcow_cc_username'] = "admin";
|
||||
$_SESSION['mailcow_cc_role'] = "admin";
|
||||
$_SESSION['acl']['tls_policy'] = "1";
|
||||
$_SESSION['acl']['quarantine_notification'] = "1";
|
||||
$_SESSION['acl']['quarantine_category'] = "1";
|
||||
$_SESSION['acl']['ratelimit'] = "1";
|
||||
$_SESSION['acl']['sogo_access'] = "1";
|
||||
$_SESSION['acl']['protocol_access'] = "1";
|
||||
$_SESSION['acl']['mailbox_relayhost'] = "1";
|
||||
$_SESSION['acl']['unlimited_quota'] = "1";
|
||||
|
||||
$iam_settings = identity_provider('get');
|
||||
if ($iam_settings['authsource'] != "keycloak" || (intval($iam_settings['periodic_sync']) != 1 && intval($iam_settings['import_users']) != 1)) {
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set pagination variables
|
||||
$start = 0;
|
||||
$max = 100;
|
||||
|
||||
// lock sync if already running
|
||||
$lock_file = '/tmp/iam-sync.lock';
|
||||
if (file_exists($lock_file)) {
|
||||
$lock_file_parts = explode("\n", file_get_contents($lock_file));
|
||||
$pid = $lock_file_parts[0];
|
||||
if (count($lock_file_parts) > 1){
|
||||
$last_execution = $lock_file_parts[1];
|
||||
$elapsed_time = (time() - $last_execution) / 60;
|
||||
if ($elapsed_time < intval($iam_settings['sync_interval'])) {
|
||||
logMsg("warning", "Sync not ready (".number_format((float)$elapsed_time, 2, '.', '')."min / ".$iam_settings['sync_interval']."min)");
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (posix_kill($pid, 0)) {
|
||||
logMsg("warning", "Sync is already running");
|
||||
session_destroy();
|
||||
exit;
|
||||
} else {
|
||||
unlink($lock_file);
|
||||
}
|
||||
}
|
||||
$lock_file_handle = fopen($lock_file, 'w');
|
||||
fwrite($lock_file_handle, getmypid());
|
||||
fclose($lock_file_handle);
|
||||
|
||||
// Init Keycloak Provider
|
||||
$iam_provider = identity_provider('init');
|
||||
|
||||
// Loop until all users have been retrieved
|
||||
while (true) {
|
||||
// Get admin access token
|
||||
$admin_token = identity_provider("get-keycloak-admin-token");
|
||||
|
||||
// Make the API request to retrieve the users
|
||||
$url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users?first=$start&max=$max";
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Content-Type: application/json",
|
||||
"Authorization: Bearer " . $admin_token
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($code != 200){
|
||||
logMsg("err", "Received HTTP {$code}");
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
try {
|
||||
$response = json_decode($response, true);
|
||||
} catch (Exception $e) {
|
||||
logMsg("err", $e->getMessage());
|
||||
break;
|
||||
}
|
||||
if (!is_array($response)){
|
||||
logMsg("err", "Received malformed response from keycloak api");
|
||||
break;
|
||||
}
|
||||
if (count($response) == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Process the batch of users
|
||||
foreach ($response as $user) {
|
||||
if (empty($user['email'])){
|
||||
logMsg("warning", "No email address in keycloak found for user " . $user['name']);
|
||||
continue;
|
||||
}
|
||||
|
||||
// try get mailbox user
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
mailbox.*,
|
||||
domain.active AS d_active
|
||||
FROM `mailbox`
|
||||
INNER JOIN domain on mailbox.domain = domain.domain
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $user['email']));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// check if matching attribute mapping exists
|
||||
$user_template = $user['attributes']['mailcow_template'][0];
|
||||
$mapper_key = array_search($user_template, $iam_settings['mappers']);
|
||||
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
if (!$row && intval($iam_settings['import_users']) == 1){
|
||||
if ($mapper_key === false){
|
||||
if (!empty($iam_settings['default_template'])) {
|
||||
$mbox_template = $iam_settings['default_template'];
|
||||
logMsg("warning", "Using default template for user " . $user['email']);
|
||||
} else {
|
||||
logMsg("warning", "No matching attribute mapping found for user " . $user['email']);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
}
|
||||
// mailbox user does not exist, create...
|
||||
logMsg("info", "Creating user " . $user['email']);
|
||||
$create_res = mailbox('add', 'mailbox_from_template', array(
|
||||
'domain' => explode('@', $user['email'])[1],
|
||||
'local_part' => explode('@', $user['email'])[0],
|
||||
'name' => $user['firstName'] . " " . $user['lastName'],
|
||||
'authsource' => 'keycloak',
|
||||
'template' => $mbox_template
|
||||
));
|
||||
if (!$create_res){
|
||||
logMsg("err", "Could not create user " . $user['email']);
|
||||
continue;
|
||||
}
|
||||
} else if ($row && intval($iam_settings['periodic_sync']) == 1 && $row['authsource'] == "keycloak") {
|
||||
if ($mapper_key === false){
|
||||
logMsg("warning", "No matching attribute mapping found for user " . $user['email']);
|
||||
continue;
|
||||
}
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
// mailbox user does exist, sync attribtues...
|
||||
logMsg("info", "Syncing attributes for user " . $user['email']);
|
||||
mailbox('edit', 'mailbox_from_template', array(
|
||||
'username' => $user['email'],
|
||||
'name' => $user['firstName'] . " " . $user['lastName'],
|
||||
'template' => $mbox_template
|
||||
));
|
||||
} else {
|
||||
// skip mailbox user
|
||||
logMsg("info", "Skipping user " . $user['email']);
|
||||
}
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
|
||||
sleep(0.025);
|
||||
}
|
||||
|
||||
// Update the pagination variables for the next batch
|
||||
$start += $max;
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
logMsg("info", "DONE!");
|
||||
// add last execution time to lock file
|
||||
$lock_file_handle = fopen($lock_file, 'w');
|
||||
fwrite($lock_file_handle, getmypid() . "\n" . time());
|
||||
fclose($lock_file_handle);
|
||||
session_destroy();
|
||||
@@ -1,198 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once(__DIR__ . '/../web/inc/vars.inc.php');
|
||||
if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
|
||||
include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
|
||||
}
|
||||
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
|
||||
|
||||
// Init database
|
||||
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
|
||||
$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) {
|
||||
logMsg("err", $e->getMessage());
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Init Redis
|
||||
$redis = new Redis();
|
||||
try {
|
||||
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"));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
echo "Exiting: " . $e->getMessage();
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
function logMsg($priority, $message, $task = "LDAP Sync") {
|
||||
global $redis;
|
||||
|
||||
$finalMsg = array(
|
||||
"time" => time(),
|
||||
"priority" => $priority,
|
||||
"task" => $task,
|
||||
"message" => $message
|
||||
);
|
||||
$redis->lPush('CRON_LOG', json_encode($finalMsg));
|
||||
}
|
||||
|
||||
// Load core functions first
|
||||
require_once __DIR__ . '/../web/inc/functions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/sessions.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';
|
||||
|
||||
$_SESSION['mailcow_cc_username'] = "admin";
|
||||
$_SESSION['mailcow_cc_role'] = "admin";
|
||||
$_SESSION['acl']['tls_policy'] = "1";
|
||||
$_SESSION['acl']['quarantine_notification'] = "1";
|
||||
$_SESSION['acl']['quarantine_category'] = "1";
|
||||
$_SESSION['acl']['ratelimit'] = "1";
|
||||
$_SESSION['acl']['sogo_access'] = "1";
|
||||
$_SESSION['acl']['protocol_access'] = "1";
|
||||
$_SESSION['acl']['mailbox_relayhost'] = "1";
|
||||
$_SESSION['acl']['unlimited_quota'] = "1";
|
||||
|
||||
$iam_settings = identity_provider('get');
|
||||
if ($iam_settings['authsource'] != "ldap" || (intval($iam_settings['periodic_sync']) != 1 && intval($iam_settings['import_users']) != 1)) {
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set pagination variables
|
||||
$start = 0;
|
||||
$max = 100;
|
||||
|
||||
// lock sync if already running
|
||||
$lock_file = '/tmp/iam-sync.lock';
|
||||
if (file_exists($lock_file)) {
|
||||
$lock_file_parts = explode("\n", file_get_contents($lock_file));
|
||||
$pid = $lock_file_parts[0];
|
||||
if (count($lock_file_parts) > 1){
|
||||
$last_execution = $lock_file_parts[1];
|
||||
$elapsed_time = (time() - $last_execution) / 60;
|
||||
if ($elapsed_time < intval($iam_settings['sync_interval'])) {
|
||||
logMsg("warning", "Sync not ready (".number_format((float)$elapsed_time, 2, '.', '')."min / ".$iam_settings['sync_interval']."min)");
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (posix_kill($pid, 0)) {
|
||||
logMsg("warning", "Sync is already running");
|
||||
session_destroy();
|
||||
exit;
|
||||
} else {
|
||||
unlink($lock_file);
|
||||
}
|
||||
}
|
||||
$lock_file_handle = fopen($lock_file, 'w');
|
||||
fwrite($lock_file_handle, getmypid());
|
||||
fclose($lock_file_handle);
|
||||
|
||||
// Init Provider
|
||||
$iam_provider = identity_provider('init');
|
||||
|
||||
// Get ldap users
|
||||
$ldap_query = $iam_provider->query();
|
||||
if (!empty($iam_settings['filter'])) {
|
||||
$ldap_query = $ldap_query->rawFilter($iam_settings['filter']);
|
||||
}
|
||||
$response = $ldap_query->where($iam_settings['username_field'], "*")
|
||||
->where($iam_settings['attribute_field'], "*")
|
||||
->select([$iam_settings['username_field'], $iam_settings['attribute_field'], 'displayname'])
|
||||
->paginate($max);
|
||||
|
||||
// Process the users
|
||||
foreach ($response as $user) {
|
||||
// try get mailbox user
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
mailbox.*,
|
||||
domain.active AS d_active
|
||||
FROM `mailbox`
|
||||
INNER JOIN domain on mailbox.domain = domain.domain
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $user[$iam_settings['username_field']][0]));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// check if matching attribute mapping exists
|
||||
$user_template = $user[$iam_settings['attribute_field']][0];
|
||||
$mapper_key = array_search($user_template, $iam_settings['mappers']);
|
||||
|
||||
if (empty($user[$iam_settings['username_field']][0])){
|
||||
logMsg("warning", "Skipping user " . $user['displayname'][0] . " due to empty LDAP ". $iam_settings['username_field'] . " property.");
|
||||
continue;
|
||||
}
|
||||
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
if (!$row && intval($iam_settings['import_users']) == 1){
|
||||
if ($mapper_key === false){
|
||||
if (!empty($iam_settings['default_template'])) {
|
||||
$mbox_template = $iam_settings['default_template'];
|
||||
} else {
|
||||
logMsg("warning", "No matching attribute mapping found for user " . $user[$iam_settings['username_field']][0]);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
}
|
||||
// mailbox user does not exist, create...
|
||||
logMsg("info", "Creating user " . $user[$iam_settings['username_field']][0]);
|
||||
$create_res = mailbox('add', 'mailbox_from_template', array(
|
||||
'domain' => explode('@', $user[$iam_settings['username_field']][0])[1],
|
||||
'local_part' => explode('@', $user[$iam_settings['username_field']][0])[0],
|
||||
'name' => $user['displayname'][0],
|
||||
'authsource' => 'ldap',
|
||||
'template' => $mbox_template
|
||||
));
|
||||
if (!$create_res){
|
||||
logMsg("err", "Could not create user " . $user[$iam_settings['username_field']][0]);
|
||||
continue;
|
||||
}
|
||||
} else if ($row && intval($iam_settings['periodic_sync']) == 1 && $row['authsource'] == "ldap") {
|
||||
if ($mapper_key === false){
|
||||
logMsg("warning", "No matching attribute mapping found for user " . $user[$iam_settings['username_field']][0]);
|
||||
continue;
|
||||
}
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
// mailbox user does exist, sync attribtues...
|
||||
logMsg("info", "Syncing attributes for user " . $user[$iam_settings['username_field']][0]);
|
||||
mailbox('edit', 'mailbox_from_template', array(
|
||||
'username' => $user[$iam_settings['username_field']][0],
|
||||
'name' => $user['displayname'][0],
|
||||
'template' => $mbox_template
|
||||
));
|
||||
} else {
|
||||
// skip mailbox user
|
||||
logMsg("info", "Skipping user " . $user[$iam_settings['username_field']][0]);
|
||||
}
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
|
||||
sleep(0.025);
|
||||
}
|
||||
|
||||
logMsg("info", "DONE!");
|
||||
// add last execution time to lock file
|
||||
$lock_file_handle = fopen($lock_file, 'w');
|
||||
fwrite($lock_file_handle, getmypid() . "\n" . time());
|
||||
fclose($lock_file_handle);
|
||||
session_destroy();
|
||||
@@ -1,6 +1,6 @@
|
||||
# Whitelist generated by Postwhite v3.4 on Thu Oct 1 00:35:20 UTC 2026
|
||||
# Whitelist generated by Postwhite v3.4 on Sun Nov 1 00:23:09 UTC 2026
|
||||
# https://github.com/stevejenkins/postwhite/
|
||||
# 2246 total rules
|
||||
# 2237 total rules
|
||||
2a00:1450:4000::/36 permit
|
||||
2a00:1450:4864::/56 permit
|
||||
2a01:111:f400::/48 permit
|
||||
@@ -197,6 +197,9 @@
|
||||
44.246.68.102 permit
|
||||
44.246.77.92 permit
|
||||
45.14.148.0/22 permit
|
||||
45.128.172.192/27 permit
|
||||
45.128.172.224/28 permit
|
||||
45.128.174.0/25 permit
|
||||
45.143.132.0/24 permit
|
||||
45.143.133.0/24 permit
|
||||
45.143.134.0/24 permit
|
||||
@@ -672,7 +675,6 @@
|
||||
85.215.255.47 permit
|
||||
85.215.255.48 permit
|
||||
85.215.255.49 permit
|
||||
86.61.88.25 permit
|
||||
87.238.80.0/21 permit
|
||||
87.248.103.12 permit
|
||||
87.248.103.21 permit
|
||||
@@ -1345,11 +1347,11 @@
|
||||
107.20.210.250 permit
|
||||
107.22.191.150 permit
|
||||
108.174.0.0/24 permit
|
||||
108.174.0.215 permit
|
||||
108.174.0.93 permit
|
||||
108.174.3.0/24 permit
|
||||
108.174.3.215 permit
|
||||
108.174.3.93 permit
|
||||
108.174.6.0/24 permit
|
||||
108.174.6.215 permit
|
||||
108.174.6.93 permit
|
||||
108.175.18.45 permit
|
||||
108.175.30.45 permit
|
||||
108.179.144.0/20 permit
|
||||
@@ -1532,11 +1534,6 @@
|
||||
147.154.63.0/24 permit
|
||||
147.154.126.0/24 permit
|
||||
147.154.191.0/24 permit
|
||||
147.243.1.47 permit
|
||||
147.243.1.48 permit
|
||||
147.243.1.153 permit
|
||||
147.243.128.24 permit
|
||||
147.243.128.26 permit
|
||||
148.105.0.0/16 permit
|
||||
148.105.8.0/21 permit
|
||||
148.116.32.128/25 permit
|
||||
@@ -1546,7 +1543,7 @@
|
||||
149.97.173.180 permit
|
||||
149.118.160.128/25 permit
|
||||
150.136.21.199 permit
|
||||
150.171.109.178 permit
|
||||
150.171.109.66 permit
|
||||
150.230.98.160 permit
|
||||
151.145.38.14 permit
|
||||
152.67.105.195 permit
|
||||
@@ -1562,7 +1559,6 @@
|
||||
155.248.237.141 permit
|
||||
157.56.120.128/26 permit
|
||||
157.58.30.128/25 permit
|
||||
157.58.196.96/29 permit
|
||||
157.58.249.3 permit
|
||||
157.137.32.128/25 permit
|
||||
157.137.96.128/25 permit
|
||||
@@ -1651,7 +1647,6 @@
|
||||
167.89.75.126 permit
|
||||
167.89.75.136 permit
|
||||
167.89.75.164 permit
|
||||
167.220.67.232/29 permit
|
||||
168.107.248.128/25 permit
|
||||
168.110.160.128/25 permit
|
||||
168.110.248.128/25 permit
|
||||
@@ -1979,8 +1974,6 @@
|
||||
207.46.22.35 permit
|
||||
207.46.50.72 permit
|
||||
207.46.50.82 permit
|
||||
207.46.52.71 permit
|
||||
207.46.52.79 permit
|
||||
207.67.38.0/24 permit
|
||||
207.67.98.192/27 permit
|
||||
207.97.204.96/29 permit
|
||||
@@ -2138,8 +2131,6 @@
|
||||
212.227.126.227 permit
|
||||
213.199.128.139 permit
|
||||
213.199.128.145 permit
|
||||
213.199.138.181 permit
|
||||
213.199.138.191 permit
|
||||
216.17.150.242 permit
|
||||
216.17.150.251 permit
|
||||
216.24.224.0/20 permit
|
||||
@@ -2228,23 +2219,23 @@
|
||||
2001:748:400:3301::4 permit
|
||||
2404:6800:4000::/36 permit
|
||||
2404:6800:4864::/56 permit
|
||||
2603:1061:14:1d0::1 permit
|
||||
2603:1061:14:41::1 permit
|
||||
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
|
||||
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
|
||||
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
|
||||
2607:f8b0:4000::/36 permit
|
||||
2607:f8b0:4864::/56 permit
|
||||
2620:109:c003:104::/64 permit
|
||||
2620:109:c003:104::215 permit
|
||||
2620:109:c003:104::5d permit
|
||||
2620:109:c006:104::/64 permit
|
||||
2620:109:c006:104::215 permit
|
||||
2620:109:c006:104::5d permit
|
||||
2620:109:c00d:104::/64 permit
|
||||
2620:10d:c090:400::8:1 permit
|
||||
2620:10d:c091:400::8:1 permit
|
||||
2620:10d:c09b:400::8:1 permit
|
||||
2620:10d:c09c:400::8:1 permit
|
||||
2620:119:50c0:207::/64 permit
|
||||
2620:119:50c0:207::215 permit
|
||||
2620:119:50c0:207::5d permit
|
||||
2800:3f0:4000::/36 permit
|
||||
2800:3f0:4864::/56 permit
|
||||
49.12.4.251 permit # checks.mailcow.email
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
MX_IMPLICIT {
|
||||
expression = "MX_GOOD & MX_MISSING";
|
||||
score = -0.01;
|
||||
}
|
||||
VIRUS_FOUND {
|
||||
expression = "CLAM_VIRUS & !MAILCOW_WHITE";
|
||||
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" {
|
||||
symbols = {
|
||||
"IP_REPUTATION_HAM" {
|
||||
|
||||
@@ -5,12 +5,6 @@ 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 {
|
||||
type = "content";
|
||||
map = "${LOCAL_CONFDIR}/custom/bulk_header.map";
|
||||
@@ -151,20 +145,6 @@ BAD_LANG {
|
||||
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 {
|
||||
type = "user";
|
||||
map = "redis://SMTP_LIMITED_ACCESS";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
timeout = 8.0;
|
||||
symbol_bad_mx = "MX_INVALID";
|
||||
symbol_no_mx = "MX_MISSING";
|
||||
symbol_good_mx = "MX_GOOD";
|
||||
expire = 86400;
|
||||
key_prefix = "rmx";
|
||||
|
||||
@@ -9,3 +9,5 @@ disable_monitoring = true;
|
||||
# instead of silently accepting the message without further processing.
|
||||
soft_reject_on_timeout = true;
|
||||
local_addrs = /etc/rspamd/custom/mailcow_networks.map;
|
||||
# Must not be lower than message_size_limit in postfix main.cf
|
||||
max_message = 134217728;
|
||||
@@ -150,41 +150,41 @@ rspamd_config:register_symbol({
|
||||
local function ipv6_to_segments(ip_str)
|
||||
-- Remove zone identifier if present (e.g., %eth0)
|
||||
ip_str = ip_str:gsub("%%.*$", "")
|
||||
|
||||
|
||||
local segments = {}
|
||||
|
||||
|
||||
-- Handle :: compression
|
||||
if ip_str:find('::') then
|
||||
local before, after = ip_str:match('^(.*)::(.*)$')
|
||||
before = before or ''
|
||||
after = after or ''
|
||||
|
||||
|
||||
local before_parts = {}
|
||||
local after_parts = {}
|
||||
|
||||
|
||||
if before ~= '' then
|
||||
for seg in before:gmatch('[^:]+') do
|
||||
table.insert(before_parts, tonumber(seg, 16) or 0)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if after ~= '' then
|
||||
for seg in after:gmatch('[^:]+') do
|
||||
table.insert(after_parts, tonumber(seg, 16) or 0)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Add before segments
|
||||
for _, seg in ipairs(before_parts) do
|
||||
table.insert(segments, seg)
|
||||
end
|
||||
|
||||
|
||||
-- Add compressed zeros
|
||||
local zeros_needed = 8 - #before_parts - #after_parts
|
||||
for i = 1, zeros_needed do
|
||||
table.insert(segments, 0)
|
||||
end
|
||||
|
||||
|
||||
-- Add after segments
|
||||
for _, seg in ipairs(after_parts) do
|
||||
table.insert(segments, seg)
|
||||
@@ -195,12 +195,12 @@ rspamd_config:register_symbol({
|
||||
table.insert(segments, tonumber(seg, 16) or 0)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Ensure we have exactly 8 segments
|
||||
while #segments < 8 do
|
||||
table.insert(segments, 0)
|
||||
end
|
||||
|
||||
|
||||
return segments
|
||||
end
|
||||
|
||||
@@ -208,46 +208,46 @@ rspamd_config:register_symbol({
|
||||
local function get_ipv6_variants(ip_str)
|
||||
local variants = {}
|
||||
local seen = {}
|
||||
|
||||
|
||||
local function add_variant(v)
|
||||
if v and not seen[v] then
|
||||
table.insert(variants, v)
|
||||
seen[v] = true
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- For IPv4, just return the original
|
||||
if not ip_str:find(':') then
|
||||
add_variant(ip_str)
|
||||
return variants
|
||||
end
|
||||
|
||||
|
||||
local segments = ipv6_to_segments(ip_str)
|
||||
|
||||
|
||||
-- 1. Fully expanded form (all zeros shown as 0000)
|
||||
local expanded_parts = {}
|
||||
for _, seg in ipairs(segments) do
|
||||
table.insert(expanded_parts, string.format('%04x', seg))
|
||||
end
|
||||
add_variant(table.concat(expanded_parts, ':'))
|
||||
|
||||
|
||||
-- 2. Standard form (no leading zeros, but all segments present)
|
||||
local standard_parts = {}
|
||||
for _, seg in ipairs(segments) do
|
||||
table.insert(standard_parts, string.format('%x', seg))
|
||||
end
|
||||
add_variant(table.concat(standard_parts, ':'))
|
||||
|
||||
|
||||
-- 3. Find all possible :: compressions
|
||||
-- RFC 5952: compress the longest run of consecutive zeros
|
||||
-- But we need to check all possibilities since Redis might have any form
|
||||
|
||||
|
||||
-- Find all zero runs
|
||||
local zero_runs = {}
|
||||
local in_run = false
|
||||
local run_start = 0
|
||||
local run_length = 0
|
||||
|
||||
|
||||
for i = 1, 8 do
|
||||
if segments[i] == 0 then
|
||||
if not in_run then
|
||||
@@ -266,21 +266,21 @@ rspamd_config:register_symbol({
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Don't forget the last run
|
||||
if in_run and run_length >= 1 then
|
||||
table.insert(zero_runs, {start = run_start, length = run_length})
|
||||
end
|
||||
|
||||
|
||||
-- Generate variant for each zero run compression
|
||||
for _, run in ipairs(zero_runs) do
|
||||
local parts = {}
|
||||
|
||||
|
||||
-- Before compression
|
||||
for i = 1, run.start - 1 do
|
||||
table.insert(parts, string.format('%x', segments[i]))
|
||||
end
|
||||
|
||||
|
||||
-- The compression
|
||||
if run.start == 1 then
|
||||
table.insert(parts, '')
|
||||
@@ -291,22 +291,22 @@ rspamd_config:register_symbol({
|
||||
else
|
||||
table.insert(parts, '')
|
||||
end
|
||||
|
||||
|
||||
-- After compression
|
||||
for i = run.start + run.length, 8 do
|
||||
table.insert(parts, string.format('%x', segments[i]))
|
||||
end
|
||||
|
||||
|
||||
local compressed = table.concat(parts, ':'):gsub('::+', '::')
|
||||
add_variant(compressed)
|
||||
end
|
||||
|
||||
|
||||
return variants
|
||||
end
|
||||
|
||||
local from_ip_string = tostring(ip)
|
||||
local ip_check_table = {}
|
||||
|
||||
|
||||
-- Add all variants of the exact IP
|
||||
for _, variant in ipairs(get_ipv6_variants(from_ip_string)) do
|
||||
table.insert(ip_check_table, variant)
|
||||
@@ -318,18 +318,18 @@ rspamd_config:register_symbol({
|
||||
maxbits = 32
|
||||
minbits = 8
|
||||
end
|
||||
|
||||
|
||||
-- Add all CIDR notations with variants
|
||||
for i=maxbits,minbits,-1 do
|
||||
local masked_ip = ip:apply_mask(i)
|
||||
local cidr_base = masked_ip:to_string()
|
||||
|
||||
|
||||
for _, variant in ipairs(get_ipv6_variants(cidr_base)) do
|
||||
local cidr = variant .. "/" .. i
|
||||
table.insert(ip_check_table, cidr)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function keep_spam_cb(err, data)
|
||||
if err then
|
||||
rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
|
||||
@@ -345,7 +345,7 @@ rspamd_config:register_symbol({
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
table.insert(ip_check_table, 1, 'KEEP_SPAM')
|
||||
local redis_ret_user = rspamd_redis_make_request(task,
|
||||
redis_params, -- connect params
|
||||
@@ -362,6 +362,61 @@ rspamd_config:register_symbol({
|
||||
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({
|
||||
name = 'TLS_HEADER',
|
||||
type = 'postfilter',
|
||||
@@ -390,9 +445,8 @@ rspamd_config:register_symbol({
|
||||
local rspamd_logger = require "rspamd_logger"
|
||||
local redis_params = rspamd_parse_redis_server('taghandler')
|
||||
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 tagged_rcpt = task:get_symbol("TAGGED_RCPT")
|
||||
|
||||
local function remove_moo_tag()
|
||||
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 '+')
|
||||
local tag = nil
|
||||
if tagged_rcpt ~= nil then
|
||||
tag = tagged_rcpt
|
||||
local base_user, tag_part = rcpt_user:match('^([^%+]+)%+(.+)$')
|
||||
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)
|
||||
end
|
||||
|
||||
@@ -441,7 +496,7 @@ rspamd_config:register_symbol({
|
||||
|
||||
-- Check if we have a pre-result (e.g., from KEEP_SPAM or POSTMASTER_HANDLER)
|
||||
local allow_processing = false
|
||||
|
||||
|
||||
if task.has_pre_result then
|
||||
local has_pre, pre_action = task:has_pre_result()
|
||||
if has_pre then
|
||||
@@ -498,8 +553,7 @@ rspamd_config:register_symbol({
|
||||
else
|
||||
rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
|
||||
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_value .. '] ' .. sbj)) .. '?='
|
||||
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
|
||||
task:set_milter_reply({
|
||||
remove_headers = {
|
||||
['Subject'] = 1,
|
||||
@@ -765,13 +819,22 @@ rspamd_config:register_symbol({
|
||||
end
|
||||
})
|
||||
|
||||
-- Expand a domain footer: replace only "{= key =}" placeholders with their values.
|
||||
-- Unknown placeholders render empty, matching the previous engine behaviour.
|
||||
local function expand_footer(template, replacements)
|
||||
return (template:gsub('{=%s*([%w_]+)%s*=}', function(key)
|
||||
local value = replacements[key]
|
||||
if value == nil then return '' end
|
||||
return tostring(value)
|
||||
end))
|
||||
end
|
||||
|
||||
rspamd_config:register_symbol({
|
||||
name = 'MOO_FOOTER',
|
||||
type = 'prefilter',
|
||||
callback = function(task)
|
||||
local cjson = require "cjson"
|
||||
local lua_mime = require "lua_mime"
|
||||
local lua_util = require "lua_util"
|
||||
local rspamd_logger = require "rspamd_logger"
|
||||
local rspamd_http = require "rspamd_http"
|
||||
local envfrom = task:get_from(1)
|
||||
@@ -844,10 +907,10 @@ rspamd_config:register_symbol({
|
||||
end
|
||||
end
|
||||
if footer.html and footer.html ~= "" then
|
||||
footer.html = lua_util.jinja_template(footer.html, replacements, true)
|
||||
footer.html = expand_footer(footer.html, replacements)
|
||||
end
|
||||
if footer.plain and footer.plain ~= "" then
|
||||
footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
|
||||
footer.plain = expand_footer(footer.plain, replacements)
|
||||
end
|
||||
|
||||
-- add footer
|
||||
@@ -898,6 +961,9 @@ rspamd_config:register_symbol({
|
||||
|
||||
if not seen_cte and rewrite.need_rewrite_ct then
|
||||
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 of headers
|
||||
|
||||
@@ -109,7 +109,7 @@ foreach ($rcpts as $rcpt) {
|
||||
}
|
||||
|
||||
// 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);
|
||||
continue;
|
||||
}
|
||||
@@ -216,8 +216,9 @@ foreach ($rcpts as $rcpt) {
|
||||
foreach ($rcpt_final_mailboxes as $rcpt_final) {
|
||||
error_log("QUARANTINE: quarantine pipe: processing quarantine message for rcpt " . $rcpt_final . PHP_EOL);
|
||||
try {
|
||||
$stmt = $pdo->prepare("INSERT INTO `quarantine` (`qid`, `subject`, `score`, `sender`, `rcpt`, `symbols`, `user`, `ip`, `msg`, `action`, `fuzzy_hashes`)
|
||||
VALUES (:qid, :subject, :score, :sender, :rcpt, :symbols, :user, :ip, :msg, :action, :fuzzy_hashes)");
|
||||
// qhash is the secret of the release/delete links in notifications, so it must not be derivable
|
||||
$stmt = $pdo->prepare("INSERT INTO `quarantine` (`qid`, `subject`, `score`, `sender`, `rcpt`, `symbols`, `user`, `ip`, `msg`, `action`, `fuzzy_hashes`, `qhash`)
|
||||
VALUES (:qid, :subject, :score, :sender, :rcpt, :symbols, :user, :ip, :msg, :action, :fuzzy_hashes, :qhash)");
|
||||
$stmt->execute(array(
|
||||
':qid' => $qid,
|
||||
':subject' => $subject,
|
||||
@@ -229,11 +230,9 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) {
|
||||
':ip' => $ip,
|
||||
':msg' => $raw_data,
|
||||
':action' => $action,
|
||||
':fuzzy_hashes' => $fuzzy
|
||||
':fuzzy_hashes' => $fuzzy,
|
||||
':qhash' => bin2hex(random_bytes(32))
|
||||
));
|
||||
$lastId = $pdo->lastInsertId();
|
||||
$stmt_update = $pdo->prepare("UPDATE `quarantine` SET `qhash` = SHA2(CONCAT(`id`, `qid`), 256) WHERE `id` = :id");
|
||||
$stmt_update->execute(array(':id' => $lastId));
|
||||
$stmt = $pdo->prepare('DELETE FROM `quarantine` WHERE `rcpt` = :rcpt AND `id` NOT IN (
|
||||
SELECT `id`
|
||||
FROM (
|
||||
|
||||
@@ -9,6 +9,8 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
|
||||
|
||||
$js_minifier->add('/web/js/site/imapsync.js');
|
||||
$js_minifier->add('/web/js/site/groups.js');
|
||||
$js_minifier->add('/web/js/site/mailbox.js');
|
||||
$js_minifier->add('/web/js/presets/sieveMailbox.js');
|
||||
$js_minifier->add('/web/js/site/pwgen.js');
|
||||
@@ -38,10 +40,12 @@ $template_data = [
|
||||
'global_filters' => mailbox('get', 'global_filter_details'),
|
||||
'domains' => $domains,
|
||||
'mailboxes' => $mailboxes,
|
||||
'mbox_templates' => mailbox('get', 'mailbox_templates'),
|
||||
'lang_mailbox' => json_encode($lang['mailbox']),
|
||||
'lang_rl' => json_encode($lang['ratelimit']),
|
||||
'lang_edit' => json_encode($lang['edit']),
|
||||
'lang_datatables' => json_encode($lang['datatables']),
|
||||
'imapsync_redirect_uri' => \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->oauth->redirectUri(),
|
||||
];
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
@@ -12,6 +12,7 @@ $fido2_data = fido2(array("action" => "get_friendly_names"));
|
||||
$js_minifier->add('/web/js/site/admin.js');
|
||||
$js_minifier->add('/web/js/presets/rspamd.js');
|
||||
$js_minifier->add('/web/js/site/pwgen.js');
|
||||
$js_minifier->add('/web/js/site/identity.js');
|
||||
|
||||
// all domains
|
||||
$domains = mailbox('get', 'domains');
|
||||
@@ -93,10 +94,6 @@ $template_data = [
|
||||
'tfa_id' => @$_SESSION['tfa_id'],
|
||||
'fido2_cid' => @$_SESSION['fido2_cid'],
|
||||
'fido2_data' => $fido2_data,
|
||||
'api' => [
|
||||
'ro' => admin_api('ro', 'get'),
|
||||
'rw' => admin_api('rw', 'get'),
|
||||
],
|
||||
'dkim_domains' => $dkim_domains,
|
||||
'dkim_domains_with_keys' => $dkim_domains_with_keys,
|
||||
'dkim_blind_domains' => $dkim_blind_domains,
|
||||
@@ -118,6 +115,7 @@ $template_data = [
|
||||
'ui_themes' => customize('get', 'ui_themes'),
|
||||
'ui_theme' => customize('get', 'ui_theme'),
|
||||
'password_complexity' => password_complexity('get'),
|
||||
'imapsync_settings' => \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getSettings(),
|
||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||
'cors_settings' => $cors_settings,
|
||||
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
|
||||
|
||||
@@ -14,3 +14,19 @@ body {
|
||||
margin: 0;
|
||||
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>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<title>Swagger UI: OAuth2 Redirect</title>
|
||||
</head>
|
||||
<body>
|
||||
<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>
|
||||
<script src="oauth2-redirect.js"></script>
|
||||
</body>
|
||||
</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()}));
|
||||
+13973
-5686
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 */
|
||||
!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
|
||||
//# sourceMappingURL=swagger-ui-bundle.js.map
|
||||
!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
|
||||
@@ -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 */
|
||||
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
|
||||
@@ -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 */
|
||||
(()=>{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
|
||||
//# sourceMappingURL=swagger-ui-es-bundle.js.map
|
||||
(()=>{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
|
||||
@@ -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":"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 truncated
|
||||
@@ -1,3 +1,2 @@
|
||||
/*! 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
|
||||
//# sourceMappingURL=swagger-ui-standalone-preset.js.map
|
||||
!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
|
||||
+29
-16
@@ -1,25 +1,30 @@
|
||||
<?php
|
||||
/*!
|
||||
* 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> */
|
||||
|
||||
/**
|
||||
* Random_* Compatibility Library
|
||||
* for using the new PHP 7 random_* API in PHP 5 projects
|
||||
*
|
||||
* @version 2.99.99
|
||||
* @released 2018-06-06
|
||||
*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2015 - 2018 Paragon Initiative Enterprises
|
||||
*
|
||||
* @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 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
|
||||
@@ -29,4 +34,12 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
// NOP
|
||||
/**
|
||||
* @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":"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 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*/
|
||||
@@ -1 +1 @@
|
||||
{"version":3,"file":"swagger-ui.css","mappings":"AAAA,YCII,aCYU,CDdV,sBEKJ,4EDSc,CCEd,iBAEE,0BACA,8BAFA,gBAEA,CAUF,iBACE,SAOF,gHAME,cAQF,eACE,cACA,eAWF,2DAGE,cAOF,mBACE,gBAQF,eACE,uBACA,SACA,iBAQF,gBACE,gCACA,cAWF,cAEE,qCADA,4BACA,CAQF,wBACE,mBAEA,oGAOF,iCAEE,oBASA,kBATA,CAiBF,kDAGE,gCACA,cAOF,gBACE,kBAOF,iBACE,sBACA,WAOF,kBACE,cAQF,gCAEE,cACA,cACA,kBACA,wBAGF,gBACE,cAGF,gBACE,UAUF,oCAEE,qBAOF,kCACE,aACA,SAOF,gBACE,kBAOF,2BACE,gBAWF,kGAKE,uBACA,eACA,iBACA,SAQF,qCAEE,iBAQF,sCAEE,oBASF,qGAIE,0BAOF,wKAIE,kBACA,UAOF,4JAIE,8BAOF,qBACE,2BAUF,mBACE,sBACA,cACA,cACA,eACA,UACA,mBAQF,qBACE,qBACA,wBAOF,qBACE,cAQF,qDAEE,sBACA,UAOF,0GAEE,YAQF,0BACE,6BACA,oBAOF,6GAEE,wBAQF,yCACE,0BACA,aAWF,qCAEE,cAOF,oBACE,kBAUF,mBACE,qBAkBF,0CACE,aCnbF,4CACA,kDACA,kDCDA,wBACE,0EAGF,2BACE,0EAGF,gCACE,mEAGF,iCACE,mEClBF,0tBAkCE,sBCrBF,0BACE,SACA,kBAGF,sDACA,uDAEA,kDACA,sDAEA,oDACA,mDAEA,oDACA,mDAEA,qDACA,mDAEA,mDAEA,kCAII,SAGA,YAFA,OAJA,kBAEA,QADA,MAIA,WAEA,YAGJ,mCACI,6BACE,SACA,kBAEF,yDACA,0DACA,qDACA,yDACA,uDACA,sDACA,uDACA,sDACA,wDACA,sDACA,sDACA,qCAII,SAGA,YAFA,OAJA,kBAEA,QADA,MAIA,WAEA,aAIR,uDACI,4BACE,SACA,kBAEF,wDACA,yDACA,oDACA,wDACA,sDACA,qDACA,sDACA,qDACA,uDACA,qDACA,qDACA,oCAII,SAGA,YAFA,OAJA,kBAEA,QADA,MAIA,WAEA,aAIR,mCACI,4BACE,SACA,kBAEF,wDACA,yDACA,oDACA,wDACA,sDACA,qDACA,sDACA,qDACA,uDACA,qDACA,qDACA,oCAII,SAGA,YAFA,OAJA,kBAEA,QADA,MAIA,WAEA,aC3HR,+BCQE,mDACA,uDAEF,mCACE,sDACA,2DAGF,uDACE,qDACA,0DAGF,mCACE,qDACA,0DCXF,uBAEE,wBADA,2BACA,CAGF,oBAEE,wBADA,2BACA,CAGF,sBAEE,yBADA,2BACA,CAGF,uBAEE,2BADA,2BACA,CAGF,qBAEE,sBADA,2BACA,CAGF,mCACE,0BAEE,wBADA,2BACA,CAGF,uBAEE,wBADA,2BACA,CAGF,yBAEE,yBADA,2BACA,CAGF,0BAEE,2BADA,2BACA,CAGF,wBAEE,sBADA,2BACA,EAIJ,uDACE,yBAEE,wBADA,2BACA,CAGF,sBAEE,wBADA,2BACA,CAGF,wBAEE,yBADA,2BACA,CAGF,yBAEE,2BADA,2BACA,CAGF,uBAEE,sBADA,2BACA,EAIJ,mCACE,yBAEE,wBADA,2BACA,CAGF,sBAEE,wBADA,2BACA,CAGF,wBAEE,yBADA,2BACA,CAGF,yBAEE,2BADA,2BACA,CAGF,uBAEE,sBADA,2BACA,EChHJ,uCACA,+DACA,iCAEA,mCACE,0CACA,kEACA,qCAGF,uDACE,yCACA,iEACA,oCAGF,mCACE,yCACA,iEACA,oCCPA,oDACA,4DACA,gEACA,kEACA,8DACA,iDAGF,mCACE,uDACA,+DACA,mEACA,qEACA,iEACA,qDAGF,uDACE,sDACA,8DACA,kEACA,oEACA,gEACA,oDAGF,mCACE,sDACA,8DACA,kEACA,oEACA,gEACA,oDCnCF,uCC2CQ,CD1CR,4CC2Ca,CD1Cb,2CC2CY,CD1CZ,0CC2CW,CD1CX,sCC2CO,CD1CP,wCC2CS,CD1CT,8CC2Ce,CD1Cf,2CC2CY,CD1CZ,4CC2Ca,CD1Cb,+CC2Ca,CD1Cb,uCC2CQ,CDzCR,wDCuDW,CDtDX,wDCuDW,CDtDX,wDCuDW,CDtDX,wDCuDW,CDtDX,wDCuDW,CDtDX,wDCuDW,CDtDX,wDCuDW,CDtDX,wDCuDW,CDtDX,wDCuDW,CDtDX,yDCuDW,CDtDX,2DCuDY,CDtDZ,4DCuDa,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
|
||||
@@ -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);
|
||||
|
||||
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'] . '"}';
|
||||
}
|
||||
elseif (strtolower($_GET['Protocol']) == 'autodiscoverv1') {
|
||||
|
||||
+43
-44
@@ -31,7 +31,7 @@ error_reporting(0);
|
||||
|
||||
$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';
|
||||
if ($autodiscover_config['useEASforOutlook'] == 'yes' &&
|
||||
// Office for macOS does not support EAS
|
||||
@@ -144,41 +144,15 @@ try {
|
||||
}
|
||||
|
||||
$username = trim((string)$email);
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT `mailbox`.`name`, `mailbox`.`active` FROM `mailbox`
|
||||
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
|
||||
WHERE `mailbox`.`username` = :username
|
||||
AND `mailbox`.`active` = '1'
|
||||
AND `domain`.`active` = '1'");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
catch(PDOException $e) {
|
||||
// Database error - return error response with complete XML
|
||||
list($usec, $sec) = explode(' ', microtime());
|
||||
?>
|
||||
<Response>
|
||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||
<ErrorCode>500</ErrorCode>
|
||||
<Message>Database Error</Message>
|
||||
<DebugData />
|
||||
</Error>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
<?php
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Mailbox not found or not active - return generic error to prevent user enumeration
|
||||
if (empty($MailboxData)) {
|
||||
if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
|
||||
try {
|
||||
$json = json_encode(
|
||||
array(
|
||||
"time" => time(),
|
||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||
"user" => $email,
|
||||
"user" => "none",
|
||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"service" => "Error: mailbox not found or inactive"
|
||||
"service" => "Error: invalid email address in request"
|
||||
)
|
||||
);
|
||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||
@@ -202,13 +176,33 @@ if (empty($MailboxData)) {
|
||||
<?php
|
||||
exit(0);
|
||||
}
|
||||
try {
|
||||
$stmt = $pdo->prepare("SELECT `mailbox`.`name`, `mailbox`.`active` FROM `mailbox`
|
||||
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
|
||||
WHERE `mailbox`.`username` = :username
|
||||
AND `mailbox`.`active` = '1'
|
||||
AND `domain`.`active` = '1'");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
catch(PDOException $e) {
|
||||
// Database error - return error response with complete XML
|
||||
list($usec, $sec) = explode(' ', microtime());
|
||||
?>
|
||||
<Response>
|
||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||
<ErrorCode>500</ErrorCode>
|
||||
<Message>Database Error</Message>
|
||||
<DebugData />
|
||||
</Error>
|
||||
</Response>
|
||||
</Autodiscover>
|
||||
<?php
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (!empty($MailboxData['name'])) {
|
||||
$displayname = $MailboxData['name'];
|
||||
}
|
||||
else {
|
||||
$displayname = $email;
|
||||
}
|
||||
$email = $username;
|
||||
$displayname = $email;
|
||||
try {
|
||||
$json = json_encode(
|
||||
array(
|
||||
@@ -216,11 +210,16 @@ try {
|
||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||
"user" => $email,
|
||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||
"service" => $autodiscover_config['autodiscoverType']
|
||||
// The answer is the same, only the log tells unknown or inactive mailboxes apart
|
||||
"service" => (empty($MailboxData)) ? "Error: mailbox not found or inactive" : $autodiscover_config['autodiscoverType']
|
||||
)
|
||||
);
|
||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||
if (empty($MailboxData)) {
|
||||
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
|
||||
}
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -243,7 +242,7 @@ if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
||||
<Server><?=$autodiscover_config['imap']['server'];?></Server>
|
||||
<Port><?=$autodiscover_config['imap']['port'];?></Port>
|
||||
<DomainRequired>off</DomainRequired>
|
||||
<LoginName><?=$email;?></LoginName>
|
||||
<LoginName><?=htmlspecialchars($email, ENT_XML1 | ENT_QUOTES, 'UTF-8');?></LoginName>
|
||||
<SPA>off</SPA>
|
||||
<SSL>on</SSL>
|
||||
<AuthRequired>on</AuthRequired>
|
||||
@@ -253,7 +252,7 @@ if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
||||
<Server><?=$autodiscover_config['smtp']['server'];?></Server>
|
||||
<Port><?=$autodiscover_config['smtp']['port'];?></Port>
|
||||
<DomainRequired>off</DomainRequired>
|
||||
<LoginName><?=$email;?></LoginName>
|
||||
<LoginName><?=htmlspecialchars($email, ENT_XML1 | ENT_QUOTES, 'UTF-8');?></LoginName>
|
||||
<SPA>off</SPA>
|
||||
<SSL>on</SSL>
|
||||
<AuthRequired>on</AuthRequired>
|
||||
@@ -265,15 +264,15 @@ if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
||||
?>
|
||||
<Protocol>
|
||||
<Type>CalDAV</Type>
|
||||
<Server>https://<?=$autodiscover_config['caldav']['server'];?><?php if ($autodiscover_config['caldav']['port'] != 443) echo ':'.$autodiscover_config['caldav']['port']; ?>/SOGo/dav/<?=$email;?>/</Server>
|
||||
<Server>https://<?=$autodiscover_config['caldav']['server'];?><?php if ($autodiscover_config['caldav']['port'] != 443) echo ':'.$autodiscover_config['caldav']['port']; ?>/SOGo/dav/<?=htmlspecialchars($email, ENT_XML1 | ENT_QUOTES, 'UTF-8');?>/</Server>
|
||||
<DomainRequired>off</DomainRequired>
|
||||
<LoginName><?=$email;?></LoginName>
|
||||
<LoginName><?=htmlspecialchars($email, ENT_XML1 | ENT_QUOTES, 'UTF-8');?></LoginName>
|
||||
</Protocol>
|
||||
<Protocol>
|
||||
<Type>CardDAV</Type>
|
||||
<Server>https://<?=$autodiscover_config['carddav']['server'];?><?php if ($autodiscover_config['caldav']['port'] != 443) echo ':'.$autodiscover_config['carddav']['port']; ?>/SOGo/dav/<?=$email;?>/</Server>
|
||||
<Server>https://<?=$autodiscover_config['carddav']['server'];?><?php if ($autodiscover_config['caldav']['port'] != 443) echo ':'.$autodiscover_config['carddav']['port']; ?>/SOGo/dav/<?=htmlspecialchars($email, ENT_XML1 | ENT_QUOTES, 'UTF-8');?>/</Server>
|
||||
<DomainRequired>off</DomainRequired>
|
||||
<LoginName><?=$email;?></LoginName>
|
||||
<LoginName><?=htmlspecialchars($email, ENT_XML1 | ENT_QUOTES, 'UTF-8');?></LoginName>
|
||||
</Protocol>
|
||||
<?php
|
||||
}
|
||||
@@ -288,7 +287,7 @@ if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
||||
<Culture>en:en</Culture>
|
||||
<User>
|
||||
<DisplayName><?=htmlspecialchars($displayname, ENT_XML1 | ENT_QUOTES, 'UTF-8');?></DisplayName>
|
||||
<EMailAddress><?=$email;?></EMailAddress>
|
||||
<EMailAddress><?=htmlspecialchars($email, ENT_XML1 | ENT_QUOTES, 'UTF-8');?></EMailAddress>
|
||||
</User>
|
||||
<Action>
|
||||
<Settings>
|
||||
|
||||
@@ -15,6 +15,10 @@ table.dataTable>tbody>tr.child ul.dtr-details>li {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.129);
|
||||
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>th.dtr-control:before:hover {
|
||||
background-color: var(--mc-dtr-hover-bg);
|
||||
@@ -28,7 +32,8 @@ table.dataTable td.dt-control:before {
|
||||
color: var(--mc-dtr-color);
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
line-height: 1.25em;
|
||||
line-height: 1em;
|
||||
margin-top: calc(-0.5em - 1.5px);
|
||||
border-radius: 0px;
|
||||
box-shadow: none;
|
||||
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 {
|
||||
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 {
|
||||
vertical-align: middle;
|
||||
}
|
||||
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 {
|
||||
min-width: 350px;
|
||||
|
||||
@@ -139,6 +139,13 @@ body {
|
||||
display: inline-block;
|
||||
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 {
|
||||
padding: .25rem .4rem;
|
||||
font-size: .875rem;
|
||||
@@ -343,6 +350,7 @@ code {
|
||||
}
|
||||
.tag-badge .bi {
|
||||
font-size: 12px;
|
||||
line-height: 0;
|
||||
}
|
||||
.tag-badge.btn-badge:hover {
|
||||
filter: brightness(0.9);
|
||||
|
||||
@@ -191,7 +191,7 @@
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
line-height: 2rem;
|
||||
margin-top: -15px;
|
||||
margin-top: calc(-1rem - 1.5px);
|
||||
}
|
||||
|
||||
li .dtr-data {
|
||||
|
||||
@@ -9,6 +9,8 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
|
||||
|
||||
$js_minifier->add('/web/js/site/imapsync.js');
|
||||
$js_minifier->add('/web/js/site/groups.js');
|
||||
$js_minifier->add('/web/js/site/mailbox.js');
|
||||
$js_minifier->add('/web/js/presets/sieveMailbox.js');
|
||||
$js_minifier->add('/web/js/site/pwgen.js');
|
||||
@@ -42,6 +44,7 @@ $template_data = [
|
||||
'lang_rl' => json_encode($lang['ratelimit']),
|
||||
'lang_edit' => json_encode($lang['edit']),
|
||||
'lang_datatables' => json_encode($lang['datatables']),
|
||||
'imapsync_redirect_uri' => \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->oauth->redirectUri(),
|
||||
];
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
+34
-1
@@ -99,6 +99,27 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
$template = 'edit/oauth2client.twig';
|
||||
$template_data = ['oauth2client' => $oauth2client];
|
||||
}
|
||||
elseif (isset($_GET['mailbox-group']) &&
|
||||
is_numeric($_GET['mailbox-group'])) {
|
||||
$result = \Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->api->getGroup($_GET['mailbox-group']);
|
||||
$template = 'edit/mailbox-group.twig';
|
||||
// Member candidates: mailboxes of the domains the caller manages
|
||||
$mailboxes = [];
|
||||
foreach (mailbox('get', 'domains') as $domain) {
|
||||
foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) {
|
||||
$mailboxes[] = $mailbox;
|
||||
}
|
||||
}
|
||||
$template_data = [
|
||||
'mailboxes' => $mailboxes,
|
||||
'mbox_templates' => ($_SESSION['mailcow_cc_role'] == 'admin') ? mailbox('get', 'mailbox_templates') : [],
|
||||
];
|
||||
}
|
||||
elseif (isset($_GET['api-key']) &&
|
||||
is_numeric($_GET['api-key'])) {
|
||||
$result = \Mailcow\ApiKey\ApiKeyModule::fromGlobals()->api->getKey($_GET['api-key']);
|
||||
$template = 'edit/api-key.twig';
|
||||
}
|
||||
elseif (isset($_GET['aliasdomain']) &&
|
||||
is_valid_domain_name(html_entity_decode(rawurldecode($_GET["aliasdomain"]))) &&
|
||||
!empty($_GET["aliasdomain"])) {
|
||||
@@ -136,6 +157,8 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
'get_tls_policy' => $get_tls_policy,
|
||||
'rlyhosts' => $rlyhosts,
|
||||
'sender_acl_handles' => mailbox('get', 'sender_acl_handles', $mailbox),
|
||||
'mailbox_groups' => \Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->api->getMemberships($mailbox),
|
||||
'identity_link' => \Mailcow\Identity\IdentityModule::fromGlobals()->api->getLink($mailbox),
|
||||
'user_acls' => acl('get', 'user', $mailbox),
|
||||
'mailbox_details' => $result,
|
||||
'iam_settings' => $iam_settings,
|
||||
@@ -204,9 +227,16 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
if (isset($_GET['syncjob']) &&
|
||||
is_numeric($_GET['syncjob'])) {
|
||||
$id = $_GET["syncjob"];
|
||||
$result = mailbox('get', 'syncjob_details', $id);
|
||||
// Without the syncjobs ACL a job cannot be saved, so do not show the form
|
||||
$result = (($_SESSION['acl']['syncjobs'] ?? '0') == '1') ? \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getJob($id) : [];
|
||||
$template = 'edit/syncjob.twig';
|
||||
}
|
||||
elseif (isset($_GET['syncjob_source']) &&
|
||||
is_numeric($_GET['syncjob_source'])) {
|
||||
$id = $_GET["syncjob_source"];
|
||||
$result = \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getSource($id);
|
||||
$template = 'edit/imapsync_source.twig';
|
||||
}
|
||||
elseif (isset($_GET['filter']) &&
|
||||
is_numeric($_GET['filter'])) {
|
||||
$id = $_GET["filter"];
|
||||
@@ -225,6 +255,8 @@ else {
|
||||
$template_data['access_denied'] = true;
|
||||
}
|
||||
|
||||
$js_minifier->add('/web/js/site/imapsync.js');
|
||||
$js_minifier->add('/web/js/site/groups.js');
|
||||
$js_minifier->add('/web/js/site/edit.js');
|
||||
$js_minifier->add('/web/js/site/pwgen.js');
|
||||
|
||||
@@ -233,5 +265,6 @@ $template_data['return_to'] = $_SESSION['return_to'];
|
||||
$template_data['lang_user'] = json_encode($lang['user']);
|
||||
$template_data['lang_admin'] = json_encode($lang['admin']);
|
||||
$template_data['lang_datatables'] = json_encode($lang['datatables']);
|
||||
$template_data['imapsync_redirect_uri'] = \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->oauth->redirectUri();
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
@@ -10,6 +10,10 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != 'admin') {
|
||||
exit();
|
||||
}
|
||||
// Read-only API keys must not control containers
|
||||
if (!empty($_SESSION['mailcow_cc_api']) && ($_SESSION['mailcow_cc_api_access'] ?? '') != 'rw') {
|
||||
exit();
|
||||
}
|
||||
|
||||
if (preg_match('/^[a-z\-]{0,}-mailcow/', $_GET['service'])) {
|
||||
if ($_GET['action'] == "start") {
|
||||
|
||||
@@ -6,7 +6,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
|
||||
}
|
||||
|
||||
if (isset($_GET['id']) && is_numeric($_GET['id'])) {
|
||||
if ($details = mailbox('get', 'syncjob_details', intval($_GET['id']))) {
|
||||
if ($details = \Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->getJob(intval($_GET['id']))) {
|
||||
echo (empty($details['log'])) ? '-' : $details['log'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,11 @@ if (array_key_exists('pending_tfa_methods', $_SESSION)) {
|
||||
if (isset($pending_tfa_authmechs['webauthn'])) {
|
||||
$pending_tfa_authmechs['webauthn'] = true;
|
||||
}
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
&& isset($pending_tfa_authmechs['yubi_otp'])) {
|
||||
$pending_tfa_authmechs['yubi_otp'] = true;
|
||||
}
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
&& !isset($pending_tfa_authmechs['yubi_otp'])
|
||||
&& isset($pending_tfa_authmechs['totp'])) {
|
||||
$pending_tfa_authmechs['totp'] = true;
|
||||
@@ -72,6 +72,8 @@ $globalVariables = [
|
||||
'lang_fido2' => json_encode($lang['fido2']),
|
||||
'lang_success' => json_encode($lang['success']),
|
||||
'lang_danger' => json_encode($lang['danger']),
|
||||
'lang_syncjobs' => json_encode($lang['syncjobs']),
|
||||
'lang_groups' => json_encode($lang['groups']),
|
||||
'docker_timeout' => $DOCKER_TIMEOUT,
|
||||
'session_lifetime' => (int)$SESSION_LIFETIME,
|
||||
'csrf_token' => $_SESSION['CSRF']['TOKEN'],
|
||||
|
||||
@@ -193,6 +193,10 @@ function acl($_action, $_scope = null, $_data = null, $_extra = null) {
|
||||
while ($row = array_shift($acl_all)) {
|
||||
$acl['acl'][$row['Field']] = 1;
|
||||
}
|
||||
// Read-only API keys must not log in as other accounts (SOGo SSO, dual login)
|
||||
if (!empty($_SESSION['mailcow_cc_api']) && ($_SESSION['mailcow_cc_api_access'] ?? '') != 'rw') {
|
||||
$acl['acl']['login_as'] = 0;
|
||||
}
|
||||
}
|
||||
elseif ($_SESSION['mailcow_cc_role'] == 'domainadmin' ||
|
||||
(isset($_SESSION["dual-login"]["role"]) && $_SESSION["dual-login"]["role"] == 'domainadmin')) {
|
||||
|
||||
@@ -74,6 +74,15 @@ function admin($_action, $_data = null) {
|
||||
$usernames = $_data['username'];
|
||||
}
|
||||
foreach ($usernames as $username) {
|
||||
// A pending password update only allows changing the own password
|
||||
if (!empty($_SESSION['pending_pw_update']) && $username != $_SESSION['mailcow_cc_username']) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$is_now = admin('details', $username);
|
||||
if (!empty($is_now)) {
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
@@ -103,7 +112,7 @@ function admin($_action, $_data = null) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) {
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new)) || $username_new == 'API') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
@@ -179,6 +188,9 @@ function admin($_action, $_data = null) {
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
||||
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
||||
}
|
||||
if ($username_new != $username) {
|
||||
\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username, $username_new);
|
||||
}
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
@@ -211,6 +223,7 @@ function admin($_action, $_data = null) {
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username);
|
||||
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
|
||||
@@ -5,6 +5,15 @@ function app_passwd($_action, $_data = null) {
|
||||
$_data_log = $_data;
|
||||
!isset($_data_log['app_passwd']) ?: $_data_log['app_passwd'] = '*';
|
||||
!isset($_data_log['app_passwd2']) ?: $_data_log['app_passwd2'] = '*';
|
||||
if (in_array($_action, array('add', 'edit', 'delete')) &&
|
||||
(!isset($_SESSION['acl']['app_passwds']) || $_SESSION['acl']['app_passwds'] != "1")) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (isset($_data['username']) && filter_var($_data['username'], FILTER_VALIDATE_EMAIL)) {
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data['username'])) {
|
||||
$_SESSION['return'][] = array(
|
||||
|
||||
@@ -6,6 +6,8 @@ function check_login($user, $pass, $extra = null) {
|
||||
$is_internal = $extra['is_internal'];
|
||||
$role = $extra['role'];
|
||||
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
|
||||
// set when the password is right, but the account does not allow this login
|
||||
$denied = false;
|
||||
|
||||
// Try validate admin
|
||||
if (!isset($role) || $role == "admin") {
|
||||
@@ -26,7 +28,7 @@ function check_login($user, $pass, $extra = null) {
|
||||
|
||||
// Try validate app password
|
||||
if (!isset($role) || $role == "app") {
|
||||
$result = apppass_login($user, $pass, $extra);
|
||||
$result = apppass_login($user, $pass, $extra, $denied);
|
||||
if ($result !== false) {
|
||||
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
|
||||
set_sasl_log($user, $real_rip, $extra['service'], $pass);
|
||||
@@ -36,26 +38,38 @@ function check_login($user, $pass, $extra = null) {
|
||||
|
||||
// Try validate user
|
||||
if (!isset($role) || $role == "user") {
|
||||
$result = user_login($user, $pass, $extra);
|
||||
try {
|
||||
$result = user_login($user, $pass, $extra, $denied);
|
||||
} catch (\Mailcow\Auth\IdpUnavailable $e) {
|
||||
// no failed login, netfilter must not count it
|
||||
error_log("mailcow UI: Identity provider unavailable for " . $user . " by " . $_SERVER['REMOTE_ADDR'] . ": " . $e->getMessage());
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
|
||||
'msg' => 'iam_unavailable'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($result !== false) {
|
||||
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
|
||||
set_sasl_log($user, $real_rip, $extra['service']);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// skip log and only return false if it's an internal request
|
||||
if ($is_internal == true) return false;
|
||||
|
||||
// A denied login is logged, but not counted by netfilter (e.g. EAS/DAV clients of accounts without access)
|
||||
$msg = ($denied ? "mailcow UI: Login denied for " : "mailcow UI: Invalid password for ") . $user . " by " . $_SERVER['REMOTE_ADDR'];
|
||||
if (!isset($_SESSION['ldelay'])) {
|
||||
$_SESSION['ldelay'] = "0";
|
||||
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
||||
if (!$denied) $redis->publish("F2B_CHANNEL", $msg);
|
||||
error_log($msg);
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_username'])) {
|
||||
$_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
|
||||
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
||||
if (!$denied) $redis->publish("F2B_CHANNEL", $msg);
|
||||
error_log($msg);
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
@@ -196,7 +210,7 @@ function domainadmin_login($user, $pass){
|
||||
|
||||
return false;
|
||||
}
|
||||
function user_login($user, $pass, $extra = null){
|
||||
function user_login($user, $pass, $extra = null, &$denied = false){
|
||||
global $pdo;
|
||||
global $iam_provider;
|
||||
global $iam_settings;
|
||||
@@ -250,11 +264,14 @@ function user_login($user, $pass, $extra = null){
|
||||
if ($extra['service'] != 'NONE') {
|
||||
$key = strtolower($extra['service']) . "_access";
|
||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||
$denied = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// the IdP accepted the password, the mailbox is inactive
|
||||
$denied = true;
|
||||
}
|
||||
clear_session();
|
||||
return false;
|
||||
@@ -262,11 +279,17 @@ function user_login($user, $pass, $extra = null){
|
||||
|
||||
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
|
||||
$row['attributes'] = json_decode($row['attributes'], true);
|
||||
if ($extra['service'] != 'NONE') {
|
||||
$key = strtolower($extra['service']) . "_access";
|
||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||
return false;
|
||||
$key = strtolower($extra['service']) . "_access";
|
||||
// Shared mailboxes have no login of their own, members open them through group shares.
|
||||
// Checked before asking an IdP (no template side effects).
|
||||
if (($row['attributes']['shared'] ?? '0') == '1'
|
||||
|| ($extra['service'] != 'NONE' && isset($row['attributes'][$key]) && $row['attributes'][$key] != '1')
|
||||
|| $row['active'] != '1' || $row['d_active'] != '1') {
|
||||
// Denied, not failed, unless the password is wrong; IdP passwords are not checked for that
|
||||
if ($row['authsource'] != 'mailcow' || verify_hash($row['password'], $pass) !== false) {
|
||||
$denied = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
switch ($row['authsource']) {
|
||||
case 'keycloak':
|
||||
@@ -284,6 +307,7 @@ function user_login($user, $pass, $extra = null){
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($row)) {
|
||||
$denied = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -342,6 +366,7 @@ function user_login($user, $pass, $extra = null){
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($row)) {
|
||||
$denied = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -384,9 +409,6 @@ function user_login($user, $pass, $extra = null){
|
||||
return $result;
|
||||
break;
|
||||
case 'mailcow':
|
||||
if ($row['active'] != 1 || $row['d_active'] != 1) {
|
||||
return false;
|
||||
}
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass) !== false) {
|
||||
|
||||
@@ -427,13 +449,15 @@ function user_login($user, $pass, $extra = null){
|
||||
}
|
||||
return "user";
|
||||
}
|
||||
// TFA accounts use app passwords in mail clients
|
||||
$denied = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
function apppass_login($user, $pass, $extra = null){
|
||||
function apppass_login($user, $pass, $extra = null, &$denied = false){
|
||||
global $pdo;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
@@ -454,14 +478,12 @@ function apppass_login($user, $pass, $extra = null){
|
||||
return false;
|
||||
}
|
||||
|
||||
// fetch app password data
|
||||
$stmt = $pdo->prepare("SELECT `app_passwd`.*, `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
|
||||
// fetch app password data, inactive ones too: their right password is denied, not failed
|
||||
$stmt = $pdo->prepare("SELECT `app_passwd`.*, `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id`,
|
||||
`mailbox`.`active` as `mailbox_active`, `domain`.`active` as `domain_active`, `mailbox`.`attributes` as `mailbox_attributes` FROM `app_passwd`
|
||||
INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
|
||||
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
|
||||
WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
|
||||
AND `mailbox`.`active` = '1'
|
||||
AND `domain`.`active` = '1'
|
||||
AND `app_passwd`.`active` = '1'
|
||||
AND `app_passwd`.`mailbox` = :user"
|
||||
);
|
||||
// fetch password data
|
||||
@@ -471,16 +493,25 @@ function apppass_login($user, $pass, $extra = null){
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if ($extra['service'] != 'NONE' && $row[strtolower($extra['service']) . '_access'] != '1'){
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass) !== false) {
|
||||
$_SESSION['app_passwd_id'] = $row['app_passwd_id'];
|
||||
unset($_SESSION['ldelay']);
|
||||
return "user";
|
||||
$key = strtolower($extra['service']) . '_access';
|
||||
$mailbox_attributes = json_decode($row['mailbox_attributes'], true);
|
||||
// the protocols of the mailbox only limit direct user access, app passwords have their own
|
||||
if ($row['active'] != '1' || $row['mailbox_active'] != '1' || $row['domain_active'] != '1'
|
||||
|| ($mailbox_attributes['shared'] ?? '0') == '1'
|
||||
|| ($extra['service'] != 'NONE' && $row[$key] != '1')) {
|
||||
// right password, but the app password does not allow this login
|
||||
$denied = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$_SESSION['app_passwd_id'] = $row['app_passwd_id'];
|
||||
unset($_SESSION['ldelay']);
|
||||
return "user";
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -507,15 +538,19 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
|
||||
return false;
|
||||
}
|
||||
if (!$iam_provider) {
|
||||
return false;
|
||||
throw new \Mailcow\Auth\IdpUnavailable('Keycloak is not configured completely');
|
||||
}
|
||||
|
||||
// get access_token for service account of mailcow client
|
||||
$admin_token = identity_provider("get-keycloak-admin-token");
|
||||
if (!$admin_token) {
|
||||
throw new \Mailcow\Auth\IdpUnavailable('Could not get an admin token from Keycloak');
|
||||
}
|
||||
|
||||
// get the mailcow_password attribute from keycloak user
|
||||
$url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users";
|
||||
$queryParams = array('email' => $user, 'exact' => true);
|
||||
// as string, http_build_query() would send exact=1 and Keycloak would search for substrings
|
||||
$queryParams = array('email' => $user, 'exact' => 'true');
|
||||
$queryString = http_build_query($queryParams);
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 7);
|
||||
@@ -525,15 +560,26 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
|
||||
'Authorization: Bearer ' . $admin_token,
|
||||
'Content-Type: application/json'
|
||||
));
|
||||
$user_res = json_decode(curl_exec($curl), true)[0];
|
||||
if (intval($iam_settings['ignore_ssl_error']) == 1) {
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
}
|
||||
$users = json_decode(curl_exec($curl), true);
|
||||
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
curl_close($curl);
|
||||
// the password is checked below, a failed request is never a wrong password
|
||||
if ($code != 200) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Identity Provider returned HTTP ' . $code),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
throw new \Mailcow\Auth\IdpUnavailable('Keycloak returned HTTP ' . $code);
|
||||
}
|
||||
// exact=true can still return several users with that email, only the exact address counts
|
||||
if (is_array($users)) {
|
||||
$users = array_values(array_filter($users, fn($u) => is_array($u) && strcasecmp((string)($u['email'] ?? ''), $user) === 0));
|
||||
}
|
||||
if (!is_array($users) || count($users) != 1) {
|
||||
return false;
|
||||
}
|
||||
$user_res = $users[0];
|
||||
if (empty($user_res['enabled'])) {
|
||||
return false;
|
||||
}
|
||||
if (!isset($user_res['attributes']['mailcow_password']) || !is_array($user_res['attributes']['mailcow_password'])){
|
||||
@@ -555,23 +601,40 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
|
||||
|
||||
// validate mailcow_password
|
||||
$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)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// get mapped template
|
||||
$user_template = $user_res['attributes']['mailcow_template'][0];
|
||||
$mapper_key = array_search($user_template, $iam_settings['mappers']);
|
||||
$name = trim(($user_res['firstName'] ?? '') . ' ' . ($user_res['lastName'] ?? ''));
|
||||
$idp_login = \Mailcow\Auth\AuthModule::fromGlobals()->idp;
|
||||
$mapped_template = $idp_login->mappedTemplate($user_res['attributes']['mailcow_template'] ?? array());
|
||||
|
||||
if (!$create) {
|
||||
if (!$idp_login->verifyLink('keycloak', $user, (string)($user_res['id'] ?? ''))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'The mailbox is linked to another IdP user'),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// login success
|
||||
if ($mapper_key !== false) {
|
||||
$update_template = $idp_login->templateFor($user, $mapped_template);
|
||||
if ($update_template !== null) {
|
||||
// update user
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
mailbox('edit', 'mailbox_from_template', array(
|
||||
'username' => $user,
|
||||
'name' => $user_res['name'],
|
||||
'template' => $iam_settings['templates'][$mapper_key]
|
||||
'name' => $name,
|
||||
'template' => $update_template
|
||||
));
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
}
|
||||
@@ -590,7 +653,7 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
|
||||
return false;
|
||||
}
|
||||
// check if matching attribute exist
|
||||
if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) {
|
||||
if ($mapped_template === null) {
|
||||
if (!empty($iam_settings['default_template'])) {
|
||||
$mbox_template = $iam_settings['default_template'];
|
||||
} else {
|
||||
@@ -602,7 +665,16 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
$mbox_template = $mapped_template;
|
||||
}
|
||||
|
||||
if (!$idp_login->mayCreate('keycloak', $user, (string)($user_res['id'] ?? ''))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'The IdP user is linked to another mailbox'),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// create mailbox
|
||||
@@ -610,7 +682,7 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
|
||||
$create_res = mailbox('add', 'mailbox_from_template', array(
|
||||
'domain' => explode('@', $user)[1],
|
||||
'local_part' => explode('@', $user)[0],
|
||||
'name' => $user_res['name'],
|
||||
'name' => $name,
|
||||
'authsource' => 'keycloak',
|
||||
'template' => $mbox_template
|
||||
));
|
||||
@@ -624,6 +696,7 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$idp_login->storeLink('keycloak', $user, (string)($user_res['id'] ?? ''));
|
||||
|
||||
return 'user';
|
||||
}
|
||||
@@ -646,7 +719,8 @@ function ldap_mbox_login($user, $pass, $extra = null){
|
||||
return false;
|
||||
}
|
||||
if (!$iam_provider) {
|
||||
return false;
|
||||
// identity_provider('init') could not connect
|
||||
throw new \Mailcow\Auth\IdpUnavailable('No connection to the LDAP server');
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -655,47 +729,45 @@ function ldap_mbox_login($user, $pass, $extra = null){
|
||||
$ldap_query = $ldap_query->rawFilter($iam_settings['filter']);
|
||||
}
|
||||
$ldap_query = $ldap_query->where($iam_settings['username_field'], '=', $user)
|
||||
->select([$iam_settings['username_field'], $iam_settings['attribute_field'], 'displayname', 'distinguishedname', 'dn']);
|
||||
->select(array_values(array_filter(array_merge([$iam_settings['username_field'], $iam_settings['attribute_field'], 'displayname', 'distinguishedname', 'dn'],
|
||||
\Mailcow\Identity\Directory\LdapDirectory::idAttributes($iam_settings['id_attribute'] ?? '')))));
|
||||
|
||||
$user_res = $ldap_query->firstOrFail();
|
||||
$entries = $ldap_query->get();
|
||||
} catch (Exception $e) {
|
||||
// clear $_SESSION['return'] to not leak data
|
||||
$_SESSION['return'] = array();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
throw new \Mailcow\Auth\IdpUnavailable('LDAP search failed: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
// the login name must identify exactly one entry, several matches are ambiguous
|
||||
if (!is_array($entries) || count($entries) != 1) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (!$iam_provider->auth()->attempt($user_res['dn'], $pass)) {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// clear $_SESSION['return'] to not leak data
|
||||
$_SESSION['return'] = array();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
$user_res = reset($entries);
|
||||
$idp_login = \Mailcow\Auth\AuthModule::fromGlobals()->idp;
|
||||
if (!$idp_login->verifyLdapPassword($iam_provider, (string)$user_res['dn'], (string)$pass)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// get mapped template
|
||||
$user_template = $user_res[$iam_settings['attribute_field']][0];
|
||||
$mapper_key = array_search($user_template, $iam_settings['mappers']);
|
||||
$mapped_template = $idp_login->mappedTemplate($user_res[$iam_settings['attribute_field']] ?? array());
|
||||
$external_id = \Mailcow\Identity\Directory\LdapDirectory::entryId($user_res, $iam_settings['id_attribute'] ?? '');
|
||||
|
||||
if (!$create) {
|
||||
if (!$idp_login->verifyLink('ldap', $user, $external_id)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'The mailbox is linked to another IdP user'),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// login success
|
||||
if ($mapper_key !== false) {
|
||||
$update_template = $idp_login->templateFor($user, $mapped_template);
|
||||
if ($update_template !== null) {
|
||||
// update user
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
mailbox('edit', 'mailbox_from_template', array(
|
||||
'username' => $user,
|
||||
'name' => $user_res['displayname'][0],
|
||||
'template' => $iam_settings['templates'][$mapper_key]
|
||||
'template' => $update_template
|
||||
));
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
}
|
||||
@@ -714,7 +786,7 @@ function ldap_mbox_login($user, $pass, $extra = null){
|
||||
return false;
|
||||
}
|
||||
// check if matching attribute exist
|
||||
if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) {
|
||||
if ($mapped_template === null) {
|
||||
if (!empty($iam_settings['default_template'])) {
|
||||
$mbox_template = $iam_settings['default_template'];
|
||||
} else {
|
||||
@@ -726,7 +798,16 @@ function ldap_mbox_login($user, $pass, $extra = null){
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
$mbox_template = $mapped_template;
|
||||
}
|
||||
|
||||
if (!$idp_login->mayCreate('ldap', $user, $external_id)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'The IdP user is linked to another mailbox'),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// create mailbox
|
||||
@@ -748,6 +829,7 @@ function ldap_mbox_login($user, $pass, $extra = null){
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$idp_login->storeLink('ldap', $user, $external_id);
|
||||
|
||||
return 'user';
|
||||
}
|
||||
@@ -156,7 +156,7 @@ function domain_admin($_action, $_data = null) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) {
|
||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new)) || $username_new == 'API') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
@@ -183,6 +183,9 @@ function domain_admin($_action, $_data = null) {
|
||||
':username_new' => $username_new,
|
||||
':username' => $username
|
||||
));
|
||||
if ($username_new != $username) {
|
||||
\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username, $username_new);
|
||||
}
|
||||
if (!empty($domains)) {
|
||||
foreach ($domains as $domain) {
|
||||
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
|
||||
@@ -331,6 +334,8 @@ function domain_admin($_action, $_data = null) {
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
// sync sources created by this account must not pass to a re-created account of the same name
|
||||
\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
|
||||
@@ -60,13 +60,14 @@ function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
$queue_unban = $redis->hGetAll('F2B_QUEUE_UNBAN');
|
||||
if (is_array($active_bans)) {
|
||||
foreach ($active_bans as $network => $banned_until) {
|
||||
$queued_for_unban = (isset($queue_unban[$network]) && $queue_unban[$network] == 1) ? 1 : 0;
|
||||
$difference = $banned_until - time();
|
||||
// Expired bans are lifted by netfilter within seconds
|
||||
$difference = max(0, intval($banned_until) - time());
|
||||
$queued_for_unban = ($difference == 0 || (isset($queue_unban[$network]) && $queue_unban[$network] == 1)) ? 1 : 0;
|
||||
$f2b_options['active_bans'][] = array(
|
||||
'queued_for_unban' => $queued_for_unban,
|
||||
'network' => $network,
|
||||
'ip' => strtok($network,'/'),
|
||||
'banned_until' => sprintf('%02dh %02dm %02ds', ($difference/3600), ($difference/60%60), $difference%60)
|
||||
'banned_until' => sprintf('%02dh %02dm %02ds', intdiv($difference, 3600), intdiv($difference, 60) % 60, $difference % 60)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -108,16 +109,15 @@ function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// Rules will also be recreated on log events, but rules may seem empty for a second in the UI
|
||||
docker('post', 'netfilter-mailcow', 'restart');
|
||||
// netfilter recreates the default rules within 10 seconds
|
||||
$fail_count = 0;
|
||||
$regex_result = json_decode($redis->Get('F2B_REGEX'), true);
|
||||
while (empty($regex_result) && $fail_count < 10) {
|
||||
while (empty($regex_result) && $fail_count < 15) {
|
||||
$regex_result = json_decode($redis->Get('F2B_REGEX'), true);
|
||||
$fail_count++;
|
||||
sleep(1);
|
||||
}
|
||||
if ($fail_count >= 10) {
|
||||
if (empty($regex_result)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
@@ -141,7 +141,29 @@ function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('object_modified', htmlspecialchars($network))
|
||||
'msg' => 'f2b_modified'
|
||||
);
|
||||
return true;
|
||||
}
|
||||
// Queue all automatic bans for unbanning, the denylist stays
|
||||
elseif ($_data['action'] == "unban-all") {
|
||||
try {
|
||||
foreach ($redis->hKeys('F2B_ACTIVE_BANS') as $network) {
|
||||
$redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
|
||||
}
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'f2b_modified'
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@@ -172,7 +194,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
if (valid_network($network)) {
|
||||
try {
|
||||
$redis->hSet('F2B_WHITELIST', $network, 1);
|
||||
$redis->hDel('F2B_BLACKLIST', $network, 1);
|
||||
$redis->hDel('F2B_BLACKLIST', $network);
|
||||
$redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
@@ -199,14 +221,13 @@ function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
if (valid_network($network) && !in_array($network, array(
|
||||
'0.0.0.0',
|
||||
'0.0.0.0/0',
|
||||
getenv('IPV4_NETWORK') . '0/24',
|
||||
getenv('IPV4_NETWORK') . '0',
|
||||
getenv('IPV4_NETWORK') . '.0/24',
|
||||
getenv('IPV4_NETWORK') . '.0',
|
||||
getenv('IPV6_NETWORK')
|
||||
))) {
|
||||
try {
|
||||
$redis->hSet('F2B_BLACKLIST', $network, 1);
|
||||
$redis->hDel('F2B_WHITELIST', $network, 1);
|
||||
//$response = docker('post', 'netfilter-mailcow', 'restart');
|
||||
$redis->hDel('F2B_WHITELIST', $network);
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -261,62 +282,60 @@ function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
$f2b_options['ban_time'] = ($ban_time < 60) ? 60 : $ban_time;
|
||||
$f2b_options['ban_time_increment'] = ($ban_time_increment == 1) ? true : false;
|
||||
$f2b_options['max_ban_time'] = ($max_ban_time < 60) ? 60 : $max_ban_time;
|
||||
$f2b_options['netban_ipv4'] = ($netban_ipv4 < 8) ? 8 : $netban_ipv4;
|
||||
$f2b_options['netban_ipv6'] = ($netban_ipv6 < 8) ? 8 : $netban_ipv6;
|
||||
$f2b_options['netban_ipv4'] = ($netban_ipv4 > 32) ? 32 : $netban_ipv4;
|
||||
$f2b_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
|
||||
$f2b_options['netban_ipv4'] = min(32, max(8, $netban_ipv4));
|
||||
$f2b_options['netban_ipv6'] = min(128, max(8, $netban_ipv6));
|
||||
$f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
|
||||
$f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
|
||||
$f2b_options['banlist_id'] = $is_now['banlist_id'];
|
||||
$f2b_options['banlist_id'] = (!empty($is_now['banlist_id'])) ? $is_now['banlist_id'] : uuid4();
|
||||
$f2b_options['manage_external'] = ($manage_external > 0) ? 1 : 0;
|
||||
$wl_items = array();
|
||||
if (!empty($wl)) {
|
||||
foreach (array_filter(array_map('trim', preg_split("/( |,|;|\n)/", $wl))) as $wl_item) {
|
||||
if (valid_network($wl_item) || valid_hostname($wl_item)) {
|
||||
$wl_items[$wl_item] = 1;
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('network_host_invalid', $wl_item)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
$bl_items = array();
|
||||
if (!empty($bl)) {
|
||||
foreach (array_filter(array_map('trim', preg_split("/( |,|;|\n)/", $bl))) as $bl_item) {
|
||||
if (valid_network($bl_item) && !in_array($bl_item, array(
|
||||
'0.0.0.0',
|
||||
'0.0.0.0/0',
|
||||
getenv('IPV4_NETWORK') . '.0/24',
|
||||
getenv('IPV4_NETWORK') . '.0',
|
||||
getenv('IPV6_NETWORK')
|
||||
))) {
|
||||
$bl_items[$bl_item] = 1;
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('network_host_invalid', $bl_item)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
// One transaction, netfilter must never see empty lists in between
|
||||
$redis->multi();
|
||||
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
|
||||
$redis->Del('F2B_WHITELIST');
|
||||
$redis->Del('F2B_BLACKLIST');
|
||||
if(!empty($wl)) {
|
||||
$wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
|
||||
$wl_array = array_filter($wl_array);
|
||||
if (is_array($wl_array)) {
|
||||
foreach ($wl_array as $wl_item) {
|
||||
if (valid_network($wl_item) || valid_hostname($wl_item)) {
|
||||
$redis->hSet('F2B_WHITELIST', $wl_item, 1);
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('network_host_invalid', $wl_item)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
$redis->Del('F2B_WHITELIST', 'F2B_BLACKLIST');
|
||||
if (!empty($wl_items)) {
|
||||
$redis->hMSet('F2B_WHITELIST', $wl_items);
|
||||
}
|
||||
if(!empty($bl)) {
|
||||
$bl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $bl));
|
||||
$bl_array = array_filter($bl_array);
|
||||
if (is_array($bl_array)) {
|
||||
foreach ($bl_array as $bl_item) {
|
||||
if (valid_network($bl_item) && !in_array($bl_item, array(
|
||||
'0.0.0.0',
|
||||
'0.0.0.0/0',
|
||||
getenv('IPV4_NETWORK') . '0/24',
|
||||
getenv('IPV4_NETWORK') . '0',
|
||||
getenv('IPV6_NETWORK')
|
||||
))) {
|
||||
$redis->hSet('F2B_BLACKLIST', $bl_item, 1);
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => array('network_host_invalid', $bl_item)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($bl_items)) {
|
||||
$redis->hMSet('F2B_BLACKLIST', $bl_items);
|
||||
}
|
||||
$redis->exec();
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -348,7 +367,9 @@ function fail2ban($_action, $_data = null, $_extra = null) {
|
||||
if (is_array($_extra)) {
|
||||
$_extra = $_extra[0];
|
||||
}
|
||||
if ($_extra != $f2b_options['banlist_id']){
|
||||
// Timing safe, a missing or empty id never matches
|
||||
$banlist_id = (string)($f2b_options['banlist_id'] ?? '');
|
||||
if ($banlist_id === '' || !is_string($_extra) || !hash_equals($banlist_id, $_extra)) {
|
||||
http_response_code(404);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,13 @@ function fwdhost($_action, $_data = null) {
|
||||
);
|
||||
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) {
|
||||
try {
|
||||
$redis->hSet('WHITELISTED_FWD_HOST', $host, $source);
|
||||
|
||||
+180
-154
@@ -1292,8 +1292,13 @@ function set_tfa($_data) {
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row) {
|
||||
if ($row['authsource'] == 'ldap'){
|
||||
if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
try {
|
||||
if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
} catch (\Mailcow\Auth\IdpUnavailable $e) {
|
||||
$_SESSION['return'][] = array('type' => 'danger', 'log' => array(__FUNCTION__, $_data_log, $e->getMessage()), 'msg' => 'iam_unavailable');
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
|
||||
else $access_denied = false;
|
||||
@@ -1389,6 +1394,18 @@ function set_tfa($_data) {
|
||||
case "webauthn":
|
||||
$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`)
|
||||
VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')");
|
||||
$stmt->execute(array(
|
||||
@@ -1411,11 +1428,11 @@ function set_tfa($_data) {
|
||||
// Block TFA removal if force_tfa policy is active
|
||||
$is_forced_tfa = false;
|
||||
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));
|
||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||
} 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));
|
||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||
}
|
||||
@@ -1682,11 +1699,11 @@ function unset_tfa_key($_data) {
|
||||
// Block key removal if force_tfa policy is active
|
||||
$is_forced_tfa = false;
|
||||
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));
|
||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||
} 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));
|
||||
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
|
||||
}
|
||||
@@ -1852,6 +1869,40 @@ function get_tfa($username = null, $id = null) {
|
||||
}
|
||||
}
|
||||
function verify_tfa_login($username, $_data) {
|
||||
global $redis;
|
||||
global $TFA_MAX_FAILS;
|
||||
global $TFA_FAIL_WINDOW;
|
||||
|
||||
if (empty($username)) {
|
||||
return false;
|
||||
}
|
||||
// Count failed second factors per account, so codes cannot be guessed from many addresses
|
||||
$fails_key = 'TFA_FAILS_' . $username;
|
||||
if (intval($redis->get($fails_key)) >= $TFA_MAX_FAILS) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $username, '*'),
|
||||
'msg' => 'tfa_login_locked'
|
||||
);
|
||||
$verified = false;
|
||||
}
|
||||
else {
|
||||
$verified = verify_tfa_token($username, $_data);
|
||||
}
|
||||
if ($verified !== true) {
|
||||
// The window starts with the first failure and is not extended by further attempts
|
||||
if ($redis->incr($fails_key) == 1) {
|
||||
$redis->expire($fails_key, $TFA_FAIL_WINDOW);
|
||||
}
|
||||
// Same message format as failed passwords, so the default fail2ban rule counts it
|
||||
error_log("mailcow UI: Invalid password for " . $username . " (second factor) by " . $_SERVER['REMOTE_ADDR']);
|
||||
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $username . " (second factor) by " . $_SERVER['REMOTE_ADDR']);
|
||||
return false;
|
||||
}
|
||||
$redis->del($fails_key);
|
||||
return true;
|
||||
}
|
||||
function verify_tfa_token($username, $_data) {
|
||||
global $pdo;
|
||||
global $yubi;
|
||||
global $u2f;
|
||||
@@ -2054,128 +2105,6 @@ function verify_tfa_login($username, $_data) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
function admin_api($access, $action, $data = null) {
|
||||
global $pdo;
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($access !== "ro" && $access !== "rw") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__),
|
||||
'msg' => 'invalid access type'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($action == "edit") {
|
||||
$active = (!empty($data['active'])) ? 1 : 0;
|
||||
$skip_ip_check = (isset($data['skip_ip_check'])) ? 1 : 0;
|
||||
$allow_from = array();
|
||||
if (isset($data['allow_from'])) {
|
||||
$allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $data['allow_from']));
|
||||
}
|
||||
foreach ($allow_from as $key => $val) {
|
||||
if (empty($val)) {
|
||||
unset($allow_from[$key]);
|
||||
continue;
|
||||
}
|
||||
if (valid_network($val) !== true) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
'log' => array(__FUNCTION__, $data),
|
||||
'msg' => array('ip_invalid', htmlspecialchars($allow_from[$key]))
|
||||
);
|
||||
unset($allow_from[$key]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$allow_from = implode(',', array_unique(array_filter($allow_from)));
|
||||
if (empty($allow_from) && $skip_ip_check == 0) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $data),
|
||||
'msg' => 'ip_list_empty'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$api_key = implode('-', array(
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3)))
|
||||
));
|
||||
$stmt = $pdo->query("SELECT `api_key` FROM `api` WHERE `access` = '" . $access . "'");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
if (empty($num_results)) {
|
||||
$stmt = $pdo->prepare("INSERT INTO `api` (`api_key`, `skip_ip_check`, `active`, `allow_from`, `access`)
|
||||
VALUES (:api_key, :skip_ip_check, :active, :allow_from, :access);");
|
||||
$stmt->execute(array(
|
||||
':api_key' => $api_key,
|
||||
':skip_ip_check' => $skip_ip_check,
|
||||
':active' => $active,
|
||||
':allow_from' => $allow_from,
|
||||
':access' => $access
|
||||
));
|
||||
}
|
||||
else {
|
||||
if ($skip_ip_check == 0) {
|
||||
$stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
|
||||
`active` = :active,
|
||||
`allow_from` = :allow_from
|
||||
WHERE `access` = :access;");
|
||||
$stmt->execute(array(
|
||||
':active' => $active,
|
||||
':skip_ip_check' => $skip_ip_check,
|
||||
':allow_from' => $allow_from,
|
||||
':access' => $access
|
||||
));
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
|
||||
`active` = :active
|
||||
WHERE `access` = :access;");
|
||||
$stmt->execute(array(
|
||||
':active' => $active,
|
||||
':skip_ip_check' => $skip_ip_check,
|
||||
':access' => $access
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif ($action == "regen_key") {
|
||||
$api_key = implode('-', array(
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3))),
|
||||
strtoupper(bin2hex(random_bytes(3)))
|
||||
));
|
||||
$stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key WHERE `access` = :access");
|
||||
$stmt->execute(array(
|
||||
':api_key' => $api_key,
|
||||
':access' => $access
|
||||
));
|
||||
}
|
||||
elseif ($action == "get") {
|
||||
$stmt = $pdo->query("SELECT * FROM `api` WHERE `access` = '" . $access . "'");
|
||||
$apidata = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if ($apidata !== false) {
|
||||
$apidata['allow_from'] = str_replace(',', PHP_EOL, $apidata['allow_from']);
|
||||
}
|
||||
return $apidata;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $data),
|
||||
'msg' => 'admin_api_modified'
|
||||
);
|
||||
}
|
||||
function license($action, $data = null) {
|
||||
global $pdo;
|
||||
global $redis;
|
||||
@@ -2482,6 +2411,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
$data_log = $_data;
|
||||
if (isset($data_log['client_secret'])) $data_log['client_secret'] = '*';
|
||||
if (isset($data_log['access_token'])) $data_log['access_token'] = '*';
|
||||
if (isset($data_log['bindpass'])) $data_log['bindpass'] = '*';
|
||||
|
||||
switch ($_action) {
|
||||
case 'get':
|
||||
@@ -2492,14 +2422,17 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
foreach($rows as $row){
|
||||
switch ($row["key"]) {
|
||||
case "redirect_url_extra":
|
||||
$settings[$row["key"]] = json_decode($row["value"]);
|
||||
break;
|
||||
case "mappers":
|
||||
case "templates":
|
||||
$settings[$row["key"]] = json_decode($row["value"]);
|
||||
// Parallel lists; older rows can be JSON objects, decode as arrays to keep the keys aligned
|
||||
$settings[$row["key"]] = (array)json_decode($row["value"], true);
|
||||
break;
|
||||
case "use_ssl":
|
||||
case "use_tls":
|
||||
case "login_provisioning":
|
||||
case "ignore_ssl_errors":
|
||||
case "ignore_ssl_error":
|
||||
$settings[$row["key"]] = boolval($row["value"]);
|
||||
break;
|
||||
default:
|
||||
@@ -2523,7 +2456,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
if ($settings["authsource"] == "ldap"){
|
||||
$settings['use_ssl'] = !isset($settings['use_ssl']) ? false : $settings['use_ssl'];
|
||||
$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;
|
||||
break;
|
||||
@@ -2577,6 +2510,13 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
|
||||
$_data['ignore_ssl_error'] = isset($_data['ignore_ssl_error']) ? boolval($_data['ignore_ssl_error']) : false;
|
||||
$_data['login_provisioning'] = isset($_data['login_provisioning']) ? boolval($_data['login_provisioning']) : false;
|
||||
// Identity sync: offboarding and groups (Keycloak and LDAP)
|
||||
$sync_settings = array('offboarding', 'offboarding_limit', 'group_sync', 'group_autocreate', 'group_default_domain');
|
||||
$_data['offboarding'] = isset($_data['offboarding']) ? intval($_data['offboarding']) : 0;
|
||||
$_data['offboarding_limit'] = isset($_data['offboarding_limit']) && is_numeric($_data['offboarding_limit']) ? min(100, max(0, intval($_data['offboarding_limit']))) : 10;
|
||||
$_data['group_sync'] = isset($_data['group_sync']) ? intval($_data['group_sync']) : 0;
|
||||
$_data['group_autocreate'] = isset($_data['group_autocreate']) ? intval($_data['group_autocreate']) : 0;
|
||||
$_data['group_default_domain'] = isset($_data['group_default_domain']) ? strtolower(trim($_data['group_default_domain'])) : "";
|
||||
switch ($_data['authsource']) {
|
||||
case "keycloak":
|
||||
$_data['server_url'] = (!empty($_data['server_url'])) ? rtrim($_data['server_url'], '/') : null;
|
||||
@@ -2585,7 +2525,8 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
$_data['import_users'] = isset($_data['import_users']) ? intval($_data['import_users']) : 0;
|
||||
$_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15;
|
||||
$_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
|
||||
$required_settings = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval', 'ignore_ssl_error', 'login_provisioning');
|
||||
$_data['group_paths'] = isset($_data['group_paths']) ? trim($_data['group_paths']) : "";
|
||||
$required_settings = array_merge(array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval', 'ignore_ssl_error', 'login_provisioning', 'group_paths'), $sync_settings);
|
||||
break;
|
||||
case "generic-oidc":
|
||||
$_data['authorize_url'] = (!empty($_data['authorize_url'])) ? $_data['authorize_url'] : null;
|
||||
@@ -2606,7 +2547,14 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
$_data['use_tls'] = isset($_data['use_tls']) && !$_data['use_ssl'] ? boolval($_data['use_tls']) : false;
|
||||
$_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15;
|
||||
$_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
|
||||
$required_settings = array('authsource', 'host', 'port', 'basedn', 'username_field', 'filter', 'attribute_field', 'binddn', 'bindpass', 'periodic_sync', 'import_users', 'sync_interval', 'use_ssl', 'use_tls', 'ignore_ssl_error', 'login_provisioning');
|
||||
$_data['id_attribute'] = isset($_data['id_attribute']) ? strtolower(trim($_data['id_attribute'])) : "";
|
||||
$_data['disabled_filter'] = isset($_data['disabled_filter']) ? trim($_data['disabled_filter']) : "";
|
||||
$_data['group_basedn'] = isset($_data['group_basedn']) ? trim($_data['group_basedn']) : "";
|
||||
$_data['group_filter'] = isset($_data['group_filter']) ? trim($_data['group_filter']) : "";
|
||||
$_data['group_name_attribute'] = (!empty($_data['group_name_attribute'])) ? strtolower(trim($_data['group_name_attribute'])) : "cn";
|
||||
$_data['group_member_mode'] = isset($_data['group_member_mode']) && in_array($_data['group_member_mode'], array('memberof', 'member', 'ad_chain')) ? $_data['group_member_mode'] : "memberof";
|
||||
$_data['group_member_attribute'] = (!empty($_data['group_member_attribute'])) ? strtolower(trim($_data['group_member_attribute'])) : "member";
|
||||
$required_settings = array_merge(array('authsource', 'host', 'port', 'basedn', 'username_field', 'filter', 'attribute_field', 'binddn', 'bindpass', 'periodic_sync', 'import_users', 'sync_interval', 'use_ssl', 'use_tls', 'ignore_ssl_error', 'login_provisioning', 'id_attribute', 'disabled_filter', 'group_basedn', 'group_filter', 'group_name_attribute', 'group_member_mode', 'group_member_attribute'), $sync_settings);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2628,6 +2576,10 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
$stmt->bindParam(':value', $_data[$setting]);
|
||||
$stmt->execute();
|
||||
}
|
||||
// Stored LDAP ids of another id attribute never match again, the mailboxes are linked by email again
|
||||
if ($_data['authsource'] == 'ldap' && ($iam_settings['id_attribute'] ?? '') != $_data['id_attribute']) {
|
||||
$pdo->query("UPDATE `identity_link` SET `external_id` = NULL WHERE `provider` = 'ldap'");
|
||||
}
|
||||
$pdo->commit();
|
||||
|
||||
// add redirect_url_extra
|
||||
@@ -2655,19 +2607,26 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
$_data['mappers'] = (!is_array($_data['mappers'])) ? array($_data['mappers']) : $_data['mappers'];
|
||||
$_data['templates'] = (!is_array($_data['templates'])) ? array($_data['templates']) : $_data['templates'];
|
||||
|
||||
$mappers = array_filter($_data['mappers']);
|
||||
$templates = array_filter($_data['templates']);
|
||||
if (count($mappers) == count($templates)){
|
||||
$mappers = json_encode($mappers);
|
||||
$templates = json_encode($templates);
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES ('mappers', :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
|
||||
$stmt->bindParam(':value', $mappers);
|
||||
$stmt->execute();
|
||||
$stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES ('templates', :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
|
||||
$stmt->bindParam(':value', $templates);
|
||||
$stmt->execute();
|
||||
// Keep mapper/template pairs together, incomplete rows are dropped
|
||||
$mappers = array();
|
||||
$templates = array();
|
||||
$template_values = array_values($_data['templates']);
|
||||
foreach (array_values($_data['mappers']) as $i => $mapper) {
|
||||
if ((string)$mapper === '' || (string)($template_values[$i] ?? '') === '') {
|
||||
continue;
|
||||
}
|
||||
$mappers[] = $mapper;
|
||||
$templates[] = $template_values[$i];
|
||||
}
|
||||
$mappers = json_encode($mappers);
|
||||
$templates = json_encode($templates);
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES ('mappers', :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
|
||||
$stmt->bindParam(':value', $mappers);
|
||||
$stmt->execute();
|
||||
$stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES ('templates', :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
|
||||
$stmt->bindParam(':value', $templates);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
// delete old access_token
|
||||
@@ -2792,6 +2751,8 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
}
|
||||
|
||||
$stmt = $pdo->query("DELETE FROM identity_provider;");
|
||||
$stmt = $pdo->query("DELETE FROM identity_link;");
|
||||
$stmt = $pdo->query("DELETE FROM identity_sync_run;");
|
||||
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
@@ -2912,8 +2873,8 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
}
|
||||
|
||||
// get mapped template
|
||||
$user_template = $info['mailcow_template'];
|
||||
$mapper_key = array_search($user_template, $iam_settings['mappers']);
|
||||
$idp_login = \Mailcow\Auth\AuthModule::fromGlobals()->idp;
|
||||
$mapped_template = $idp_login->mappedTemplate($info['mailcow_template'] ?? array());
|
||||
|
||||
// token valid, get mailbox
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
@@ -2935,13 +2896,23 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($mapper_key !== false) {
|
||||
if (!$idp_login->verifyLink($iam_settings['authsource'], $info['email'], (string)($info['sub'] ?? ''))) {
|
||||
clear_session();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $info['email'], 'The mailbox is linked to another IdP user'),
|
||||
'msg' => 'login_failed'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$update_template = $idp_login->templateFor($info['email'], $mapped_template);
|
||||
if ($update_template !== null) {
|
||||
// update user
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
mailbox('edit', 'mailbox_from_template', array(
|
||||
'username' => $info['email'],
|
||||
'name' => $info['name'],
|
||||
'template' => $iam_settings['templates'][$mapper_key]
|
||||
'template' => $update_template
|
||||
));
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
|
||||
@@ -2956,11 +2927,11 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
$stmt->execute(array(':user' => $info['email']));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
if ($row['active'] != 1 || $row['d_active'] != 1) {
|
||||
if ($row['active'] != 1 || $row['d_active'] != 1 || (json_decode($row['attributes'], true)['shared'] ?? '0') == '1') {
|
||||
clear_session();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $info['email'], 'Domain or mailbox is inactive'),
|
||||
'log' => array(__FUNCTION__, $info['email'], 'Domain or mailbox is inactive or shared'),
|
||||
'msg' => 'login_failed'
|
||||
);
|
||||
return false;
|
||||
@@ -2986,7 +2957,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (empty($iam_settings['mappers']) || empty($user_template) || $mapper_key === false){
|
||||
if ($mapped_template === null){
|
||||
if (!empty($iam_settings['default_template'])) {
|
||||
$mbox_template = $iam_settings['default_template'];
|
||||
} else {
|
||||
@@ -2999,7 +2970,17 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
$mbox_template = $mapped_template;
|
||||
}
|
||||
|
||||
if (!$idp_login->mayCreate($iam_settings['authsource'], $info['email'], (string)($info['sub'] ?? ''))) {
|
||||
clear_session();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $info['email'], 'The IdP user is linked to another mailbox'),
|
||||
'msg' => 'login_failed'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// create mailbox
|
||||
@@ -3021,6 +3002,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$idp_login->storeLink($iam_settings['authsource'], $info['email'], (string)($info['sub'] ?? ''));
|
||||
|
||||
// double check if mailbox and domain is active
|
||||
$stmt = $pdo->prepare("SELECT * FROM `mailbox`
|
||||
@@ -3077,6 +3059,38 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A refresh keeps the logged-in mailbox: a changed IdP email or another IdP user ends the session
|
||||
if (strtolower($info['email']) !== strtolower($_SESSION['mailcow_cc_username'] ?? '') ||
|
||||
!\Mailcow\Auth\AuthModule::fromGlobals()->idp->verifyLink($iam_settings['authsource'], $info['email'], (string)($info['sub'] ?? ''))) {
|
||||
clear_session();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $info['email'], 'The IdP user does not match the logged-in mailbox'),
|
||||
'msg' => 'refresh_login_failed'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The mailbox may have been deleted or deactivated since the login
|
||||
$stmt = $pdo->prepare("SELECT `mailbox`.`authsource` FROM `mailbox`
|
||||
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `mailbox`.`active` = '1'
|
||||
AND `domain`.`active` = '1'
|
||||
AND COALESCE(JSON_UNQUOTE(JSON_VALUE(`mailbox`.`attributes`, '$.shared')), '0') != '1'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $info['email']));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$row || !in_array($row['authsource'], array('keycloak', 'generic-oidc'))) {
|
||||
clear_session();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $info['email'], 'Mailbox or domain is inactive or not of type OIDC'),
|
||||
'msg' => 'refresh_login_failed'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
set_user_loggedin_session($info['email']);
|
||||
$_SESSION['iam_token'] = $plain_token;
|
||||
$_SESSION['iam_refresh_token'] = $plain_refreshtoken;
|
||||
@@ -3104,7 +3118,17 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
case "get-keycloak-admin-token":
|
||||
// get access_token for service account of mailcow client
|
||||
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
|
||||
$url = "{$iam_settings['server_url']}/realms/{$iam_settings['realm']}/protocol/openid-connect/token/introspect";
|
||||
$req = http_build_query(array(
|
||||
@@ -3155,6 +3179,8 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
|
||||
':key' => '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'];
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -302,192 +302,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'msg' => array('mailbox_modified', $username)
|
||||
);
|
||||
break;
|
||||
case 'syncjob':
|
||||
if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (isset($_data['username']) && filter_var($_data['username'], FILTER_VALIDATE_EMAIL)) {
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data['username'])) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
$username = $_data['username'];
|
||||
}
|
||||
}
|
||||
elseif ($_SESSION['mailcow_cc_role'] == "user") {
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'no_user_defined'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$active = intval($_data['active']);
|
||||
$subscribeall = intval($_data['subscribeall']);
|
||||
$delete2duplicates = intval($_data['delete2duplicates']);
|
||||
$delete1 = intval($_data['delete1']);
|
||||
$delete2 = intval($_data['delete2']);
|
||||
$timeout1 = intval($_data['timeout1']);
|
||||
$timeout2 = intval($_data['timeout2']);
|
||||
$skipcrossduplicates = intval($_data['skipcrossduplicates']);
|
||||
$automap = intval($_data['automap']);
|
||||
$dry = intval($_data['dry']);
|
||||
$port1 = $_data['port1'];
|
||||
$host1 = strtolower($_data['host1']);
|
||||
$password1 = $_data['password1'];
|
||||
$exclude = $_data['exclude'];
|
||||
$maxage = $_data['maxage'];
|
||||
$maxbytespersecond = $_data['maxbytespersecond'];
|
||||
$subfolder2 = $_data['subfolder2'];
|
||||
$user1 = $_data['user1'];
|
||||
$mins_interval = $_data['mins_interval'];
|
||||
$enc1 = $_data['enc1'];
|
||||
$custom_params = (empty(trim($_data['custom_params']))) ? '' : trim($_data['custom_params']);
|
||||
|
||||
// validate custom params
|
||||
foreach (explode('-', $custom_params) as $param){
|
||||
if(empty($param)) continue;
|
||||
|
||||
// extract option
|
||||
if (str_contains($param, '=')) $param = explode('=', $param)[0];
|
||||
else $param = rtrim($param, ' ');
|
||||
// remove first char if first char is -
|
||||
if ($param[0] == '-') $param = ltrim($param, $param[0]);
|
||||
|
||||
if (str_contains($param, ' ')) {
|
||||
// bad char
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'bad character SPACE'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if param is whitelisted
|
||||
if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){
|
||||
// bad option
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'bad option '. $param
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (empty($subfolder2)) {
|
||||
$subfolder2 = "";
|
||||
}
|
||||
if (!isset($maxage) || !filter_var($maxage, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
|
||||
$maxage = "0";
|
||||
}
|
||||
if (!isset($timeout1) || !filter_var($timeout1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
|
||||
$timeout1 = "600";
|
||||
}
|
||||
if (!isset($timeout2) || !filter_var($timeout2, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
|
||||
$timeout2 = "600";
|
||||
}
|
||||
if (!isset($maxbytespersecond) || !filter_var($maxbytespersecond, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 125000000)))) {
|
||||
$maxbytespersecond = "0";
|
||||
}
|
||||
if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!filter_var($mins_interval, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 43800)))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// if (!is_valid_domain_name($host1)) {
|
||||
// $_SESSION['return'][] = array(
|
||||
// 'type' => 'danger',
|
||||
// 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
// 'msg' => 'access_denied'
|
||||
// );
|
||||
// return false;
|
||||
// }
|
||||
if ($enc1 != "TLS" && $enc1 != "SSL" && $enc1 != "PLAIN") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (@preg_match("/" . $exclude . "/", null) === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT '1' FROM `imapsync`
|
||||
WHERE `user2` = :user2 AND `user1` = :user1 AND `host1` = :host1");
|
||||
$stmt->execute(array(':user1' => $user1, ':user2' => $username, ':host1' => $host1));
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
if ($num_results != 0) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('object_exists', htmlspecialchars($host1 . ' / ' . $user1))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `timeout1`, `timeout2`, `automap`, `skipcrossduplicates`, `maxbytespersecond`, `subscribeall`, `dry`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `custom_params`, `active`)
|
||||
VALUES (:user2, :exclude, :delete1, :delete2, :timeout1, :timeout2, :automap, :skipcrossduplicates, :maxbytespersecond, :subscribeall, :dry, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :custom_params, :active)");
|
||||
$stmt->execute(array(
|
||||
':user2' => $username,
|
||||
':custom_params' => $custom_params,
|
||||
':exclude' => $exclude,
|
||||
':maxage' => $maxage,
|
||||
':delete1' => $delete1,
|
||||
':delete2' => $delete2,
|
||||
':timeout1' => $timeout1,
|
||||
':timeout2' => $timeout2,
|
||||
':automap' => $automap,
|
||||
':skipcrossduplicates' => $skipcrossduplicates,
|
||||
':maxbytespersecond' => $maxbytespersecond,
|
||||
':subscribeall' => $subscribeall,
|
||||
':dry' => $dry,
|
||||
':subfolder2' => $subfolder2,
|
||||
':host1' => $host1,
|
||||
':authmech1' => 'PLAIN',
|
||||
':user1' => $user1,
|
||||
':password1' => $password1,
|
||||
':mins_interval' => $mins_interval,
|
||||
':port1' => $port1,
|
||||
':enc1' => $enc1,
|
||||
':delete2duplicates' => $delete2duplicates,
|
||||
':active' => $active,
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('mailbox_modified', $username)
|
||||
);
|
||||
break;
|
||||
case 'domain':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -1124,6 +938,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$eas_access = (isset($_data['eas_access'])) ? intval($_data['eas_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['eas_access']);
|
||||
$dav_access = (isset($_data['dav_access'])) ? intval($_data['dav_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['dav_access']);
|
||||
$relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : 0;
|
||||
// Shared mailbox: no login of its own, members open it through group shares
|
||||
$shared = (isset($_data['shared'])) ? intval($_data['shared']) : 0;
|
||||
$quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
|
||||
$quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
|
||||
// Validate quarantine_category
|
||||
@@ -1157,6 +973,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'eas_access' => strval($eas_access),
|
||||
'dav_access' => strval($dav_access),
|
||||
'relayhost' => strval($relayhost),
|
||||
'shared' => strval($shared),
|
||||
'passwd_update' => time(),
|
||||
'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']),
|
||||
'quarantine_notification' => strval($quarantine_notification),
|
||||
@@ -1236,7 +1053,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($authsource == 'mailcow'){
|
||||
if ($authsource == 'mailcow' && $shared == 1 && empty($password) && empty($password2)) {
|
||||
// Nobody logs in to a shared mailbox, a random password keeps it unusable
|
||||
$password_hashed = hash_password(bin2hex(random_bytes(32)));
|
||||
}
|
||||
elseif ($authsource == 'mailcow'){
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
@@ -1454,7 +1275,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
}
|
||||
}
|
||||
|
||||
return mailbox('add', 'mailbox', $mailbox_attributes);
|
||||
$result = mailbox('add', 'mailbox', $mailbox_attributes);
|
||||
if ($result !== false) {
|
||||
// Remember the applied template, see edit mailbox_from_template
|
||||
$stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.template', :template) WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':template' => $_data['template'],
|
||||
':username' => strtolower(trim($_data['local_part'])) . '@' . idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46)
|
||||
));
|
||||
}
|
||||
return $result;
|
||||
break;
|
||||
case 'mta_sts':
|
||||
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
@@ -2280,202 +2110,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'syncjob':
|
||||
if (!is_array($_data['id'])) {
|
||||
$ids = array();
|
||||
$ids[] = $_data['id'];
|
||||
}
|
||||
else {
|
||||
$ids = $_data['id'];
|
||||
}
|
||||
if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
foreach ($ids as $id) {
|
||||
$is_now = mailbox('get', 'syncjob_details', $id, array('with_password'));
|
||||
if (!empty($is_now)) {
|
||||
$username = $is_now['user2'];
|
||||
$user1 = (!empty($_data['user1'])) ? $_data['user1'] : $is_now['user1'];
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
$last_run = (isset($_data['last_run'])) ? NULL : $is_now['last_run'];
|
||||
$success = (isset($_data['success'])) ? NULL : $is_now['success'];
|
||||
$delete2duplicates = (isset($_data['delete2duplicates'])) ? intval($_data['delete2duplicates']) : $is_now['delete2duplicates'];
|
||||
$subscribeall = (isset($_data['subscribeall'])) ? intval($_data['subscribeall']) : $is_now['subscribeall'];
|
||||
$dry = (isset($_data['dry'])) ? intval($_data['dry']) : $is_now['dry'];
|
||||
$delete1 = (isset($_data['delete1'])) ? intval($_data['delete1']) : $is_now['delete1'];
|
||||
$delete2 = (isset($_data['delete2'])) ? intval($_data['delete2']) : $is_now['delete2'];
|
||||
$automap = (isset($_data['automap'])) ? intval($_data['automap']) : $is_now['automap'];
|
||||
$skipcrossduplicates = (isset($_data['skipcrossduplicates'])) ? intval($_data['skipcrossduplicates']) : $is_now['skipcrossduplicates'];
|
||||
$port1 = (!empty($_data['port1'])) ? $_data['port1'] : $is_now['port1'];
|
||||
$password1 = (!empty($_data['password1'])) ? $_data['password1'] : $is_now['password1'];
|
||||
$host1 = (!empty($_data['host1'])) ? $_data['host1'] : $is_now['host1'];
|
||||
$subfolder2 = (isset($_data['subfolder2'])) ? $_data['subfolder2'] : $is_now['subfolder2'];
|
||||
$enc1 = (!empty($_data['enc1'])) ? $_data['enc1'] : $is_now['enc1'];
|
||||
$mins_interval = (!empty($_data['mins_interval'])) ? $_data['mins_interval'] : $is_now['mins_interval'];
|
||||
$exclude = (isset($_data['exclude'])) ? $_data['exclude'] : $is_now['exclude'];
|
||||
$custom_params = (isset($_data['custom_params'])) ? $_data['custom_params'] : $is_now['custom_params'];
|
||||
$maxage = (isset($_data['maxage']) && $_data['maxage'] != "") ? intval($_data['maxage']) : $is_now['maxage'];
|
||||
$maxbytespersecond = (isset($_data['maxbytespersecond']) && $_data['maxbytespersecond'] != "") ? intval($_data['maxbytespersecond']) : $is_now['maxbytespersecond'];
|
||||
$timeout1 = (isset($_data['timeout1']) && $_data['timeout1'] != "") ? intval($_data['timeout1']) : $is_now['timeout1'];
|
||||
$timeout2 = (isset($_data['timeout2']) && $_data['timeout2'] != "") ? intval($_data['timeout2']) : $is_now['timeout2'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// validate custom params
|
||||
foreach (explode('-', $custom_params) as $param){
|
||||
if(empty($param)) continue;
|
||||
|
||||
// extract option
|
||||
if (str_contains($param, '=')) $param = explode('=', $param)[0];
|
||||
else $param = rtrim($param, ' ');
|
||||
// remove first char if first char is -
|
||||
if ($param[0] == '-') $param = ltrim($param, $param[0]);
|
||||
|
||||
if (str_contains($param, ' ')) {
|
||||
// bad char
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'bad character SPACE'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if param is whitelisted
|
||||
if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){
|
||||
// bad option
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'bad option '. $param
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (empty($subfolder2)) {
|
||||
$subfolder2 = "";
|
||||
}
|
||||
if (!isset($maxage) || !filter_var($maxage, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
|
||||
$maxage = "0";
|
||||
}
|
||||
if (!isset($timeout1) || !filter_var($timeout1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
|
||||
$timeout1 = "600";
|
||||
}
|
||||
if (!isset($timeout2) || !filter_var($timeout2, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
|
||||
$timeout2 = "600";
|
||||
}
|
||||
if (!isset($maxbytespersecond) || !filter_var($maxbytespersecond, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 125000000)))) {
|
||||
$maxbytespersecond = "0";
|
||||
}
|
||||
if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!filter_var($mins_interval, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 43800)))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!is_valid_domain_name($host1)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ($enc1 != "TLS" && $enc1 != "SSL" && $enc1 != "PLAIN") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (@preg_match("/" . $exclude . "/", null) === false) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("UPDATE `imapsync` SET `delete1` = :delete1,
|
||||
`delete2` = :delete2,
|
||||
`automap` = :automap,
|
||||
`skipcrossduplicates` = :skipcrossduplicates,
|
||||
`maxage` = :maxage,
|
||||
`maxbytespersecond` = :maxbytespersecond,
|
||||
`subfolder2` = :subfolder2,
|
||||
`exclude` = :exclude,
|
||||
`host1` = :host1,
|
||||
`last_run` = :last_run,
|
||||
`success` = :success,
|
||||
`user1` = :user1,
|
||||
`password1` = :password1,
|
||||
`mins_interval` = :mins_interval,
|
||||
`port1` = :port1,
|
||||
`enc1` = :enc1,
|
||||
`delete2duplicates` = :delete2duplicates,
|
||||
`custom_params` = :custom_params,
|
||||
`timeout1` = :timeout1,
|
||||
`timeout2` = :timeout2,
|
||||
`subscribeall` = :subscribeall,
|
||||
`dry` = :dry,
|
||||
`active` = :active
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
':delete1' => $delete1,
|
||||
':delete2' => $delete2,
|
||||
':automap' => $automap,
|
||||
':skipcrossduplicates' => $skipcrossduplicates,
|
||||
':id' => $id,
|
||||
':exclude' => $exclude,
|
||||
':maxage' => $maxage,
|
||||
':maxbytespersecond' => $maxbytespersecond,
|
||||
':subfolder2' => $subfolder2,
|
||||
':host1' => $host1,
|
||||
':user1' => $user1,
|
||||
':password1' => $password1,
|
||||
':last_run' => $last_run,
|
||||
':success' => $success,
|
||||
':mins_interval' => $mins_interval,
|
||||
':port1' => $port1,
|
||||
':enc1' => $enc1,
|
||||
':delete2duplicates' => $delete2duplicates,
|
||||
':custom_params' => $custom_params,
|
||||
':timeout1' => $timeout1,
|
||||
':timeout2' => $timeout2,
|
||||
':subscribeall' => $subscribeall,
|
||||
':dry' => $dry,
|
||||
':active' => $active,
|
||||
));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('mailbox_modified', $username)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'filter':
|
||||
if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -2590,6 +2224,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!empty($is_now['group_id'])) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('alias_group_managed', htmlspecialchars($is_now['address']))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ($_data['expand_alias'] === true || $_data['expand_alias'] == 1) {
|
||||
$stmt = $pdo->prepare("SELECT `address` FROM `alias`
|
||||
WHERE `address` = :address
|
||||
@@ -2752,7 +2394,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
// Delete from sender_acl to prevent duplicates
|
||||
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE
|
||||
`logged_in_as` = :goto AND
|
||||
`send_as` = :address");
|
||||
`send_as` = :address AND
|
||||
`group_id` IS NULL");
|
||||
$stmt->execute(array(
|
||||
':goto' => $goto,
|
||||
':address' => $address
|
||||
@@ -2786,6 +2429,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':active' => $active,
|
||||
':id' => $is_now['id']
|
||||
));
|
||||
if ($address != $is_now['address']) {
|
||||
// Send-as grants of groups follow the address
|
||||
\Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->api->addressRenamed($is_now['address'], $address);
|
||||
}
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
@@ -2877,6 +2524,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':domain' => $domain,
|
||||
':tag_name' => $tag,
|
||||
));
|
||||
$stmt = $pdo->prepare("UPDATE `domain` SET `modified` = NOW() WHERE `domain` = :domain");
|
||||
$stmt->execute(array(
|
||||
':domain' => $domain,
|
||||
));
|
||||
}
|
||||
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -3045,6 +2696,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':domain' => $domain,
|
||||
':tag_name' => $tag,
|
||||
));
|
||||
$stmt = $pdo->prepare("UPDATE `domain` SET `modified` = NOW() WHERE `domain` = :domain");
|
||||
$stmt->execute(array(
|
||||
':domain' => $domain,
|
||||
));
|
||||
}
|
||||
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -3167,6 +2822,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
(int)$eas_access = (isset($_data['eas_access']) && hasACLAccess("protocol_access")) ? intval($_data['eas_access']) : intval($is_now['attributes']['eas_access']);
|
||||
(int)$dav_access = (isset($_data['dav_access']) && hasACLAccess("protocol_access")) ? intval($_data['dav_access']) : intval($is_now['attributes']['dav_access']);
|
||||
(int)$relayhost = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
|
||||
(int)$shared = (isset($_data['shared'])) ? intval($_data['shared']) : intval($is_now['attributes']['shared'] ?? 0);
|
||||
(int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
|
||||
$name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
|
||||
$domain = $is_now['domain'];
|
||||
@@ -3389,7 +3045,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$sender_acl_merged = array_merge($sender_acl_domain_admin, $sender_acl_admin);
|
||||
// If merged array still contains "*", set it as only value
|
||||
!in_array('*', $sender_acl_merged) ?: $sender_acl_merged = array('*');
|
||||
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 0 AND `logged_in_as` = :username");
|
||||
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 0 AND `group_id` IS NULL AND `logged_in_as` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
@@ -3421,7 +3077,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
}
|
||||
}
|
||||
else {
|
||||
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 0 AND `logged_in_as` = :username");
|
||||
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 0 AND `group_id` IS NULL AND `logged_in_as` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
@@ -3462,6 +3118,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
|
||||
`attributes` = JSON_SET(`attributes`, '$.shared', :shared),
|
||||
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.eas_access', :eas_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.dav_access', :dav_access),
|
||||
@@ -3484,6 +3141,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':dav_access' => $dav_access,
|
||||
':recovery_email' => $pw_recovery_email,
|
||||
':relayhost' => $relayhost,
|
||||
':shared' => strval($shared),
|
||||
':username' => $username,
|
||||
':authsource' => $authsource
|
||||
));
|
||||
@@ -3520,6 +3178,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':username' => $username,
|
||||
':tag_name' => $tag,
|
||||
));
|
||||
$stmt = $pdo->prepare("UPDATE `mailbox` SET `modified` = NOW() WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
@@ -3640,7 +3302,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
"imapsync" => ['user2'],
|
||||
'bcc_maps' => ['local_dest', 'bcc_dest'],
|
||||
'recipient_maps' => ['old_dest', 'new_dest'],
|
||||
'sender_acl' => ['logged_in_as', 'send_as']
|
||||
'sender_acl' => ['logged_in_as', 'send_as'],
|
||||
'identity_link' => ['username'],
|
||||
'mailbox_group_imap_grant' => ['owner'],
|
||||
'mailbox_group_sogo_grant' => ['owner'],
|
||||
'mailbox_group_sogo_acl' => ['c_uid'],
|
||||
'mailbox_group_subscription' => ['username', 'owner']
|
||||
];
|
||||
foreach ($tables as $table => $columns) {
|
||||
foreach ($columns as $column) {
|
||||
@@ -3652,6 +3319,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
}
|
||||
}
|
||||
|
||||
// Sync sources created by / targeting the mailbox and its OAuth tokens (owner); a PDOException rolls back below
|
||||
\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->sources->onAccountRenamed($old_username, $new_username);
|
||||
// Group memberships and send-as grants follow, group aliases are rebuilt
|
||||
\Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->hooks->onMailboxRenamed($old_username, $new_username);
|
||||
|
||||
// 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")
|
||||
->execute([
|
||||
@@ -3749,10 +3421,27 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attribute_hash = sha1(json_encode($mbox_template_data["attributes"]));
|
||||
$is_now = mailbox('get', 'mailbox_details', $_data['username']);
|
||||
$name = ltrim(rtrim($_data['name'], '>'), '<');
|
||||
if ($is_now['attributes']['attribute_hash'] == $attribute_hash && $is_now['name'] == $name)
|
||||
// Last applied template, null for mailboxes created before it was recorded
|
||||
$applied_template = $is_now['attributes']['template'] ?? null;
|
||||
$stmt_template = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.template', :template) WHERE `username` = :username");
|
||||
if ($is_now['attributes']['attribute_hash'] == $attribute_hash && $is_now['name'] == $name) {
|
||||
if ($applied_template === null) {
|
||||
$stmt_template->execute(array(':template' => $_data['template'], ':username' => $_data['username']));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$mbox_template_data = json_decode($mbox_template_data["attributes"], true);
|
||||
// Any manual edit clears attribute_hash: re-applying the same template must not undo a manual (de)activation
|
||||
if ($applied_template === null || $applied_template === $_data['template']) {
|
||||
unset($mbox_template_data['active']);
|
||||
}
|
||||
// Login disabled by the IdP sync is only lifted by the sync
|
||||
$stmt = $pdo->prepare("SELECT `idp_disabled` FROM `identity_link` WHERE `username` = :username");
|
||||
$stmt->execute(array(':username' => $_data['username']));
|
||||
if (intval($stmt->fetchColumn()) == 1) {
|
||||
unset($mbox_template_data['active']);
|
||||
}
|
||||
$mbox_template_data['attribute_hash'] = $attribute_hash;
|
||||
$mbox_template_data['name'] = $name;
|
||||
$quarantine_attributes = array('username' => $_data['username']);
|
||||
@@ -3795,6 +3484,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
if ($result === false) return $result;
|
||||
$result = acl('edit', 'user', $acl_attributes);
|
||||
if ($result === false) return $result;
|
||||
$stmt_template->execute(array(':template' => $_data['template'], ':username' => $_data['username']));
|
||||
|
||||
$_SESSION['return'] = array();
|
||||
return true;
|
||||
@@ -3907,6 +3597,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
":template" => $_data["template"] ,
|
||||
":attributes" => json_encode($attr)
|
||||
));
|
||||
// Mailboxes with this effective group template get the new settings
|
||||
\Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->api->templateChanged((int)$id);
|
||||
}
|
||||
|
||||
|
||||
@@ -4308,6 +4000,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)'));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($row = array_shift($rows)) {
|
||||
if (!hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['address'])) {
|
||||
continue;
|
||||
}
|
||||
// Keep old array for backward compatibility
|
||||
$data['fixed_sender_aliases'][] = $row['address'];
|
||||
// Split into allowed/blocked for proper display
|
||||
@@ -4338,7 +4033,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
}
|
||||
// Return array $data['sender_acl_domains/addresses']['ro'] with read-only objects
|
||||
// Return array $data['sender_acl_domains/addresses']['rw'] with read-write objects (can be deleted)
|
||||
$stmt = $pdo->prepare("SELECT REPLACE(`send_as`, '@', '') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `external` = '0' AND (`send_as` LIKE '@%' OR `send_as` = '*')");
|
||||
$stmt = $pdo->prepare("SELECT REPLACE(`send_as`, '@', '') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `external` = '0' AND `group_id` IS NULL AND (`send_as` LIKE '@%' OR `send_as` = '*')");
|
||||
$stmt->execute(array(':logged_in_as' => $_data));
|
||||
$domain_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($domain_row = array_shift($domain_rows)) {
|
||||
@@ -4357,7 +4052,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$data['sender_acl_domains']['rw'][] = $domain_row['send_as'];
|
||||
}
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `external` = '0' AND (`send_as` NOT LIKE '@%' AND `send_as` != '*')");
|
||||
$stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `external` = '0' AND `group_id` IS NULL AND (`send_as` NOT LIKE '@%' AND `send_as` != '*')");
|
||||
$stmt->execute(array(':logged_in_as' => $_data));
|
||||
$address_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($address_row = array_shift($address_rows)) {
|
||||
@@ -4603,68 +4298,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
}
|
||||
return false;
|
||||
break;
|
||||
case 'syncjob_details':
|
||||
$syncjobdetails = array();
|
||||
if (!is_numeric($_data)) {
|
||||
return false;
|
||||
}
|
||||
if (isset($_extra) && in_array('no_log', $_extra)) {
|
||||
$field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("returned_text", "password1")');
|
||||
$fields = $field_query->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($field = array_shift($fields)) {
|
||||
$shown_fields[] = $field['Field'];
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT " . implode(',', (array)$shown_fields) . ",
|
||||
`active`
|
||||
FROM `imapsync` WHERE id = :id");
|
||||
}
|
||||
elseif (isset($_extra) && in_array('with_password', $_extra)) {
|
||||
$stmt = $pdo->prepare("SELECT *,
|
||||
`active`
|
||||
FROM `imapsync` WHERE id = :id");
|
||||
}
|
||||
else {
|
||||
$field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("password1")');
|
||||
$fields = $field_query->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($field = array_shift($fields)) {
|
||||
$shown_fields[] = $field['Field'];
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT " . implode(',', (array)$shown_fields) . ",
|
||||
`active`
|
||||
FROM `imapsync` WHERE id = :id");
|
||||
}
|
||||
$stmt->execute(array(':id' => $_data));
|
||||
$syncjobdetails = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!empty($syncjobdetails['returned_text'])) {
|
||||
$syncjobdetails['log'] = $syncjobdetails['returned_text'];
|
||||
}
|
||||
else {
|
||||
$syncjobdetails['log'] = '';
|
||||
}
|
||||
unset($syncjobdetails['returned_text']);
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $syncjobdetails['user2'])) {
|
||||
return false;
|
||||
}
|
||||
return $syncjobdetails;
|
||||
break;
|
||||
case 'syncjobs':
|
||||
$syncjobdata = array();
|
||||
if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$_data = $_SESSION['mailcow_cc_username'];
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `id` FROM `imapsync` WHERE `user2` = :username");
|
||||
$stmt->execute(array(':username' => $_data));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($row = array_shift($rows)) {
|
||||
$syncjobdata[] = $row['id'];
|
||||
}
|
||||
return $syncjobdata;
|
||||
break;
|
||||
case 'spam_score':
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
|
||||
@@ -4877,7 +4510,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`sogo_visible`,
|
||||
`sender_allowed`,
|
||||
`created`,
|
||||
`modified`
|
||||
`modified`,
|
||||
`group_id`,
|
||||
(SELECT `name` FROM `mailbox_group` WHERE `mailbox_group`.`id` = `alias`.`group_id`) AS `group_name`
|
||||
FROM `alias`
|
||||
WHERE (`id` = :id OR `address` = :address) AND `address` != `goto`");
|
||||
$stmt->execute(array(
|
||||
@@ -4910,6 +4545,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$aliasdata['sogo_visible'] = $row['sogo_visible'];
|
||||
$aliasdata['sogo_visible_int'] = $row['sogo_visible'];
|
||||
$aliasdata['sender_allowed'] = $row['sender_allowed'];
|
||||
// Distribution address of a mailbox group, read-only here
|
||||
$aliasdata['group_id'] = $row['group_id'];
|
||||
$aliasdata['group_name'] = $row['group_name'];
|
||||
$aliasdata['created'] = $row['created'];
|
||||
$aliasdata['modified'] = $row['modified'];
|
||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) {
|
||||
@@ -5300,6 +4938,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
else if ($SaslLogs['service'] == 'SSO') {
|
||||
$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) {
|
||||
$last_imap_login = 0;
|
||||
@@ -5313,10 +4954,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
if (!isset($last_sso_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
|
||||
$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_smtp_login'] = $last_smtp_login;
|
||||
$mailboxdata['last_pop3_login'] = $last_pop3_login;
|
||||
$mailboxdata['last_sso_login'] = $last_sso_login;
|
||||
$mailboxdata['last_sogo_login'] = $last_sogo_login;
|
||||
|
||||
if (!isset($_extra) || $_extra != 'reduced') {
|
||||
$rl = ratelimit('get', 'mailbox', $_data);
|
||||
@@ -5350,11 +4995,20 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$mailboxdata['is_relayed'] = $row['backupmx'];
|
||||
// Internal send-as ACL for this mailbox, mirrors the sender_acl field accepted by edit/mailbox
|
||||
$mailboxdata['sender_acl'] = array();
|
||||
$stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `external` = '0'");
|
||||
$stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `external` = '0' AND `group_id` IS NULL");
|
||||
$stmt->execute(array(':username' => $_data));
|
||||
$sender_acl_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
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`
|
||||
@@ -5494,46 +5148,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
break;
|
||||
case 'delete':
|
||||
switch ($_type) {
|
||||
case 'syncjob':
|
||||
if (!is_array($_data['id'])) {
|
||||
$ids = array();
|
||||
$ids[] = $_data['id'];
|
||||
}
|
||||
else {
|
||||
$ids = $_data['id'];
|
||||
}
|
||||
if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
foreach ($ids as $id) {
|
||||
if (!is_numeric($id)) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `user2` FROM `imapsync` WHERE id = :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$user2 = $stmt->fetch(PDO::FETCH_ASSOC)['user2'];
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $user2)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `imapsync` WHERE `id`= :id");
|
||||
$stmt->execute(array(':id' => $id));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('deleted_syncjob', $id)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'filter':
|
||||
if (!is_array($_data['id'])) {
|
||||
$ids = array();
|
||||
@@ -5764,6 +5378,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$stmt->execute(array(
|
||||
':domain' => $domain,
|
||||
));
|
||||
\Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->api->domainDeleted($domain);
|
||||
$stmt = $pdo->prepare("DELETE FROM `imapsync_source_domain` WHERE `domain` = :domain");
|
||||
$stmt->execute(array(
|
||||
':domain' => $domain,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `domain` = :domain");
|
||||
$stmt->execute(array(
|
||||
':domain' => $domain,
|
||||
@@ -5887,6 +5506,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!empty($alias_data['group_id'])) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('alias_group_managed', htmlspecialchars($alias_data['address']))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track affected mailboxes for SOGo update (capture before deletion)
|
||||
if (!empty($alias_data['goto'])) {
|
||||
@@ -5907,6 +5534,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$stmt->execute(array(
|
||||
':alias_address' => $alias_data['address']
|
||||
));
|
||||
\Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->api->addressDeleted($alias_data['address']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
@@ -5947,6 +5575,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$stmt->execute(array(
|
||||
':alias_domain' => $alias_domain,
|
||||
));
|
||||
\Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->api->domainDeleted($alias_domain);
|
||||
$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `domain` = :alias_domain");
|
||||
$stmt->execute(array(
|
||||
':alias_domain' => $alias_domain,
|
||||
@@ -6029,7 +5658,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'msg' => 'Could not move maildir to garbage collector: variables local_part and/or domain empty'
|
||||
);
|
||||
}
|
||||
$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `goto` = :username");
|
||||
\Mailcow\MailboxGroup\MailboxGroupModule::fromGlobals()->api->mailboxDeleted($username);
|
||||
$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `goto` = :username AND `group_id` IS NULL");
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
@@ -6071,6 +5701,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
// Tokens, scope targets and own sync sources of the mailbox
|
||||
\Mailcow\Imapsync\ImapsyncModule::fromGlobals()->api->accountChanged($username);
|
||||
$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
@@ -6315,6 +5947,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':domain' => $domain,
|
||||
':tag_name' => $tag,
|
||||
));
|
||||
if ($stmt->rowCount() > 0) {
|
||||
$stmt = $pdo->prepare("UPDATE `domain` SET `modified` = NOW() WHERE `domain` = :domain");
|
||||
$stmt->execute(array(
|
||||
':domain' => $domain,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6367,6 +6005,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':username' => $username,
|
||||
':tag_name' => $tag,
|
||||
));
|
||||
if ($stmt->rowCount() > 0) {
|
||||
$stmt = $pdo->prepare("UPDATE `mailbox` SET `modified` = NOW() WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -767,8 +767,9 @@ function quarantine($_action, $_data = null) {
|
||||
else {
|
||||
$domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
|
||||
foreach ($domains as $domain) {
|
||||
$stmt = $pdo->prepare('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `score`, `rcpt`, `sender`, `action`, UNIX_TIMESTAMP(`created`) AS `created`, `notified` FROM `quarantine` WHERE `rcpt` REGEXP :domain');
|
||||
$stmt->execute(array(':domain' => '@' . $domain . '$'));
|
||||
// Compare the domain part exactly, a regex would treat dots in the domain as wildcards
|
||||
$stmt = $pdo->prepare('SELECT `id`, `qid`, `subject`, LOCATE("VIRUS_FOUND", `symbols`) AS `virus_flag`, `score`, `rcpt`, `sender`, `action`, UNIX_TIMESTAMP(`created`) AS `created`, `notified` FROM `quarantine` WHERE SUBSTRING_INDEX(`rcpt`, "@", -1) = :domain');
|
||||
$stmt->execute(array(':domain' => $domain));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while($row = array_shift($rows)) {
|
||||
$q_meta[] = $row;
|
||||
|
||||
+707
-66
@@ -4,7 +4,20 @@ function init_db_schema()
|
||||
try {
|
||||
global $pdo;
|
||||
|
||||
$db_version = "18082026_1200";
|
||||
// Defined once as DB_VERSION in docker-compose.yml
|
||||
$db_version = (string)getenv('DB_VERSION');
|
||||
if ($db_version === '') {
|
||||
if (php_sapi_name() == "cli") {
|
||||
echo "DB initialization failed: DB_VERSION is not set" . PHP_EOL;
|
||||
} else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__),
|
||||
'msg' => 'Database not initialized: DB_VERSION is not set.'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
@@ -174,19 +187,18 @@ function init_db_schema()
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"alias" => array(
|
||||
"api" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
"address" => "VARCHAR(255) NOT NULL",
|
||||
"goto" => "TEXT NOT NULL",
|
||||
"domain" => "VARCHAR(255) NOT NULL",
|
||||
"api_key" => "VARCHAR(255) NOT NULL",
|
||||
"description" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
"allow_from" => "VARCHAR(512) NOT NULL",
|
||||
"skip_ip_check" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
|
||||
"private_comment" => "TEXT",
|
||||
"public_comment" => "TEXT",
|
||||
"sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"internal" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"sender_allowed" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"modified" => "DATETIME ON UPDATE NOW(0)",
|
||||
"access" => "ENUM('ro', 'rw') NOT NULL DEFAULT 'rw'",
|
||||
// 1 = defined in mailcow.conf, read-only in UI and API
|
||||
"service" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||
),
|
||||
"keys" => array(
|
||||
@@ -194,41 +206,7 @@ function init_db_schema()
|
||||
"" => array("id")
|
||||
),
|
||||
"unique" => array(
|
||||
"address" => array("address")
|
||||
),
|
||||
"key" => array(
|
||||
"domain" => array("domain")
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"api" => array(
|
||||
"cols" => array(
|
||||
"api_key" => "VARCHAR(255) NOT NULL",
|
||||
"allow_from" => "VARCHAR(512) NOT NULL",
|
||||
"skip_ip_check" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE NOW(0)",
|
||||
"access" => "ENUM('ro', 'rw') NOT NULL DEFAULT 'rw'",
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("api_key")
|
||||
),
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"sender_acl" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
"logged_in_as" => "VARCHAR(255) NOT NULL",
|
||||
"send_as" => "VARCHAR(255) NOT NULL",
|
||||
"external" => "TINYINT(1) NOT NULL DEFAULT '0'"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("id")
|
||||
"api_key" => array("api_key")
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
@@ -411,6 +389,360 @@ function init_db_schema()
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"mailbox_group" => 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 ''",
|
||||
// distribution address, maintained as alias row with group_id
|
||||
"address" => "VARCHAR(255) NULL DEFAULT NULL",
|
||||
// members may send as the address
|
||||
"sender_allowed" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"internal" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"template_id" => "INT NULL DEFAULT NULL",
|
||||
// highest priority wins when a mailbox is in several groups with a template
|
||||
"priority" => "INT NOT NULL DEFAULT '0'",
|
||||
// IdP group id: set = memberships are synced from the IdP
|
||||
"idp_ref" => "VARCHAR(255) NULL DEFAULT NULL",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("id")
|
||||
),
|
||||
"unique" => array(
|
||||
"domain_name" => array("domain", "name"),
|
||||
"address" => array("address"),
|
||||
"idp_ref" => array("idp_ref")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_mailbox_group_domain" => array(
|
||||
"col" => "domain",
|
||||
"ref" => "domain.domain",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
),
|
||||
"fk_mailbox_group_template" => array(
|
||||
"col" => "template_id",
|
||||
"ref" => "templates.id",
|
||||
"delete" => "SET NULL",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"mailbox_group_member" => array(
|
||||
"cols" => array(
|
||||
"group_id" => "INT NOT NULL",
|
||||
"username" => "VARCHAR(255) NOT NULL",
|
||||
"source" => "ENUM('manual', 'idp') NOT NULL DEFAULT 'manual'",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("group_id", "username")
|
||||
),
|
||||
"key" => array(
|
||||
"username" => array("username")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_mailbox_group_member_group" => array(
|
||||
"col" => "group_id",
|
||||
"ref" => "mailbox_group.id",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
),
|
||||
"fk_mailbox_group_member_mailbox" => array(
|
||||
"col" => "username",
|
||||
"ref" => "mailbox.username",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"mailbox_group_send_as" => array(
|
||||
"cols" => array(
|
||||
"group_id" => "INT NOT NULL",
|
||||
// address or @domain the members may send as
|
||||
"address" => "VARCHAR(255) NOT NULL"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("group_id", "address")
|
||||
),
|
||||
"key" => array(
|
||||
"address" => array("address")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_mailbox_group_send_as_group" => array(
|
||||
"col" => "group_id",
|
||||
"ref" => "mailbox_group.id",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"mailbox_group_imap_grant" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
"group_id" => "INT NOT NULL",
|
||||
// mailbox whose folders the members may access
|
||||
"owner" => "VARCHAR(255) NOT NULL",
|
||||
"rights" => "ENUM('read', 'write', 'full') NOT NULL DEFAULT 'read'",
|
||||
// 1 = the group-grants cron subscribes the members to the folders (once per member)
|
||||
"subscribe" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
// removing = the ACL is deleted in Dovecot, then the row
|
||||
"state" => "ENUM('pending', 'applied', 'failed', 'removing') NOT NULL DEFAULT 'pending'",
|
||||
"last_error" => "TEXT",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("id")
|
||||
),
|
||||
"unique" => array(
|
||||
"group_owner" => array("group_id", "owner")
|
||||
),
|
||||
"key" => array(
|
||||
"owner" => array("owner")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_mailbox_group_imap_grant_group" => array(
|
||||
"col" => "group_id",
|
||||
"ref" => "mailbox_group.id",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
),
|
||||
"fk_mailbox_group_imap_grant_owner" => array(
|
||||
"col" => "owner",
|
||||
"ref" => "mailbox.username",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"mailbox_group_sogo_grant" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
"group_id" => "INT NOT NULL",
|
||||
"owner" => "VARCHAR(255) NOT NULL",
|
||||
"folder_type" => "ENUM('Calendar', 'Contacts') NOT NULL DEFAULT 'Calendar'",
|
||||
"folder" => "VARCHAR(255) NOT NULL DEFAULT 'personal'",
|
||||
"access" => "ENUM('read', 'write') NOT NULL DEFAULT 'read'",
|
||||
// 1 = the group-grants cron subscribes the members to the folder (once per member)
|
||||
"subscribe" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
// pending = the owner has not opened SOGo yet, the folder does not exist
|
||||
"state" => "ENUM('pending', 'applied', 'failed') NOT NULL DEFAULT 'pending'",
|
||||
"last_error" => "TEXT",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("id")
|
||||
),
|
||||
"unique" => array(
|
||||
"group_folder" => array("group_id", "owner", "folder_type", "folder")
|
||||
),
|
||||
"key" => array(
|
||||
"owner" => array("owner")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_mailbox_group_sogo_grant_group" => array(
|
||||
"col" => "group_id",
|
||||
"ref" => "mailbox_group.id",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
),
|
||||
"fk_mailbox_group_sogo_grant_owner" => array(
|
||||
"col" => "owner",
|
||||
"ref" => "mailbox.username",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
// sogo_acl rows written for a grant, so only those are ever removed again. No FK: rows of grants
|
||||
// removed by a cascade stay until their sogo_acl rows are deleted (GroupRepository::cleanupSogoAcl())
|
||||
"mailbox_group_sogo_acl" => array(
|
||||
"cols" => array(
|
||||
"acl_id" => "INT NOT NULL",
|
||||
"grant_id" => "INT NOT NULL",
|
||||
"c_uid" => "VARCHAR(255) NOT NULL",
|
||||
"c_role" => "VARCHAR(80) NOT NULL"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("acl_id")
|
||||
),
|
||||
"key" => array(
|
||||
"grant_id" => array("grant_id")
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
// IMAP and SOGo subscriptions made by the group-grants cron. No FK: rows of grants removed by a cascade stay
|
||||
// until the members are unsubscribed again
|
||||
"mailbox_group_subscription" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
"kind" => "ENUM('imap', 'sogo') NOT NULL",
|
||||
// mailbox_group_imap_grant.id or mailbox_group_sogo_grant.id
|
||||
"grant_id" => "INT NOT NULL",
|
||||
"username" => "VARCHAR(255) NOT NULL",
|
||||
"owner" => "VARCHAR(255) NOT NULL",
|
||||
// SOGo: Calendar/<id> or Contacts/<id>, IMAP: empty (all folders of the owner)
|
||||
"folder" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("id")
|
||||
),
|
||||
"unique" => array(
|
||||
"grant_username" => array("kind", "grant_id", "username")
|
||||
),
|
||||
"key" => array(
|
||||
"username" => array("username")
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"identity_link" => array(
|
||||
"cols" => array(
|
||||
"username" => "VARCHAR(255) NOT NULL",
|
||||
"provider" => "VARCHAR(20) NOT NULL",
|
||||
// Keycloak user id (= OIDC sub), LDAP objectGUID/entryUUID/... or dn:<dn>, OIDC sub; NULL = not linked yet
|
||||
"external_id" => "VARCHAR(255) NULL DEFAULT NULL",
|
||||
// 1 = the sync set the mailbox to login disabled (active = 2)
|
||||
"idp_disabled" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
// first complete sync run that did not list the user
|
||||
"missing_since" => "DATETIME NULL DEFAULT NULL",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("username")
|
||||
),
|
||||
"unique" => array(
|
||||
"provider_external_id" => array("provider", "external_id")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_identity_link_mailbox" => array(
|
||||
"col" => "username",
|
||||
"ref" => "mailbox.username",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"identity_sync_run" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
"provider" => "VARCHAR(20) NOT NULL DEFAULT ''",
|
||||
// run = scheduled or requested, preview = dry run, force = without the deactivation limit
|
||||
"mode" => "ENUM('run', 'preview', 'force') NOT NULL DEFAULT 'run'",
|
||||
"status" => "ENUM('pending', 'running', 'ok', 'blocked', 'error') NOT NULL DEFAULT 'pending'",
|
||||
"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",
|
||||
"summary" => "MEDIUMTEXT"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("id")
|
||||
),
|
||||
"key" => array(
|
||||
"status" => array("status")
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
// alias and sender_acl come after mailbox_group (foreign keys)
|
||||
"alias" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
"address" => "VARCHAR(255) NOT NULL",
|
||||
"goto" => "TEXT NOT NULL",
|
||||
"domain" => "VARCHAR(255) NOT NULL",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
|
||||
"private_comment" => "TEXT",
|
||||
"public_comment" => "TEXT",
|
||||
"sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"internal" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"sender_allowed" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
// set = distribution address of a mailbox group, read-only in the alias UI
|
||||
"group_id" => "INT NULL DEFAULT NULL"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("id")
|
||||
),
|
||||
"unique" => array(
|
||||
"address" => array("address"),
|
||||
"group_id" => array("group_id")
|
||||
),
|
||||
"key" => array(
|
||||
"domain" => array("domain")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_alias_mailbox_group" => array(
|
||||
"col" => "group_id",
|
||||
"ref" => "mailbox_group.id",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"sender_acl" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
"logged_in_as" => "VARCHAR(255) NOT NULL",
|
||||
"send_as" => "VARCHAR(255) NOT NULL",
|
||||
"external" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
// set = granted through a mailbox group, rebuilt by the group module
|
||||
"group_id" => "INT NULL DEFAULT NULL"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("id")
|
||||
),
|
||||
"key" => array(
|
||||
"group_id" => array("group_id")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_sender_acl_mailbox_group" => array(
|
||||
"col" => "group_id",
|
||||
"ref" => "mailbox_group.id",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"sieve_filters" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
@@ -720,7 +1052,8 @@ function init_db_schema()
|
||||
"mailbox_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"domain_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"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'"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
@@ -755,35 +1088,154 @@ function init_db_schema()
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"imapsync_source" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
"name" => "VARCHAR(100) NOT NULL",
|
||||
"description" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
"created_by" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
"scope" => "ENUM('all','domain','user') NOT NULL DEFAULT 'user'",
|
||||
"host1" => "VARCHAR(255) NOT NULL",
|
||||
"port1" => "SMALLINT UNSIGNED NOT NULL",
|
||||
"enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'",
|
||||
"verify_cert" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"auth_type" => "ENUM('PLAIN','LOGIN','CRAM-MD5','XOAUTH2') DEFAULT 'PLAIN'",
|
||||
"oauth_flow" => "ENUM('client_credentials','authorization_code') NOT NULL DEFAULT 'client_credentials'",
|
||||
"oauth_token_endpoint" => "VARCHAR(500) DEFAULT NULL",
|
||||
"oauth_authorize_endpoint" => "VARCHAR(500) DEFAULT NULL",
|
||||
"oauth_userinfo_endpoint" => "VARCHAR(500) DEFAULT NULL",
|
||||
"oauth_client_id" => "VARCHAR(500) DEFAULT NULL",
|
||||
"oauth_client_secret" => "VARCHAR(500) DEFAULT NULL",
|
||||
"oauth_scope" => "VARCHAR(500) DEFAULT NULL",
|
||||
"oauth_extra_params" => "TEXT DEFAULT NULL",
|
||||
"oauth_access_token" => "TEXT DEFAULT NULL",
|
||||
"oauth_token_expires" => "BIGINT UNSIGNED DEFAULT NULL",
|
||||
"oauth_last_refresh_error" => "TEXT DEFAULT NULL",
|
||||
"oauth_last_refresh_attempt" => "DATETIME NULL DEFAULT NULL",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("id")
|
||||
),
|
||||
"unique" => array(
|
||||
"uniq_creator_name" => array("created_by", "name")
|
||||
),
|
||||
"key" => array(
|
||||
"idx_created_by" => array("created_by"),
|
||||
"idx_scope" => array("scope")
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"imapsync_source_domain" => array(
|
||||
"cols" => array(
|
||||
"source_id" => "INT NOT NULL",
|
||||
"domain" => "VARCHAR(255) NOT NULL"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("source_id", "domain")
|
||||
),
|
||||
"key" => array(
|
||||
"idx_domain" => array("domain")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_imapsync_source_domain" => array(
|
||||
"col" => "source_id",
|
||||
"ref" => "imapsync_source.id",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"imapsync_source_user" => array(
|
||||
"cols" => array(
|
||||
"source_id" => "INT NOT NULL",
|
||||
"username" => "VARCHAR(255) NOT NULL"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("source_id", "username")
|
||||
),
|
||||
"key" => array(
|
||||
"idx_username" => array("username")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_imapsync_source_user" => array(
|
||||
"col" => "source_id",
|
||||
"ref" => "imapsync_source.id",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"imapsync_source_oauth_token" => array(
|
||||
"cols" => array(
|
||||
"source_id" => "INT NOT NULL",
|
||||
"owner" => "VARCHAR(255) NOT NULL",
|
||||
"username" => "VARCHAR(255) NOT NULL",
|
||||
"access_token" => "TEXT DEFAULT NULL",
|
||||
"refresh_token" => "TEXT DEFAULT NULL",
|
||||
"token_expires" => "BIGINT UNSIGNED DEFAULT NULL",
|
||||
"last_refresh_error" => "TEXT DEFAULT NULL",
|
||||
"oauth_last_refresh_attempt" => "DATETIME NULL DEFAULT NULL",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("source_id", "owner", "username")
|
||||
),
|
||||
"key" => array(
|
||||
"idx_owner" => array("owner")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_imapsync_source_oauth_token" => array(
|
||||
"col" => "source_id",
|
||||
"ref" => "imapsync_source.id",
|
||||
"delete" => "CASCADE",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"imapsync" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
"user2" => "VARCHAR(255) NOT NULL",
|
||||
"host1" => "VARCHAR(255) NOT NULL",
|
||||
"authmech1" => "ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN'",
|
||||
"source_id" => "INT NOT NULL",
|
||||
"regextrans2" => "VARCHAR(255) DEFAULT ''",
|
||||
"authmd51" => "TINYINT(1) NOT NULL DEFAULT 0",
|
||||
"domain2" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
"subfolder2" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
"user1" => "VARCHAR(255) NOT NULL",
|
||||
"password1" => "VARCHAR(255) NOT NULL",
|
||||
"password1" => "VARCHAR(255) NOT NULL DEFAULT ''",
|
||||
"exclude" => "VARCHAR(500) NOT NULL DEFAULT ''",
|
||||
"maxage" => "SMALLINT NOT NULL DEFAULT '0'",
|
||||
"mins_interval" => "SMALLINT UNSIGNED NOT NULL DEFAULT '0'",
|
||||
"maxbytespersecond" => "VARCHAR(50) NOT NULL DEFAULT '0'",
|
||||
"port1" => "SMALLINT UNSIGNED NOT NULL",
|
||||
"enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'",
|
||||
"delete2duplicates" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"delete1" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"delete2" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"automap" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"skipcrossduplicates" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"custom_params" => "VARCHAR(512) NOT NULL DEFAULT ''",
|
||||
"custom_params" => "TEXT",
|
||||
// original legacy custom_params string, only set when options were dropped during migration (never used by the runner)
|
||||
"custom_params_legacy" => "TEXT DEFAULT NULL",
|
||||
"timeout1" => "SMALLINT NOT NULL DEFAULT '600'",
|
||||
"timeout2" => "SMALLINT NOT NULL DEFAULT '600'",
|
||||
"subscribeall" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"dry" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"is_running" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"prio" => "INT NOT NULL DEFAULT 0",
|
||||
"returned_text" => "LONGTEXT",
|
||||
"last_run" => "TIMESTAMP NULL DEFAULT NULL",
|
||||
"success" => "TINYINT(1) UNSIGNED DEFAULT NULL",
|
||||
@@ -795,6 +1247,32 @@ function init_db_schema()
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("id")
|
||||
),
|
||||
"key" => array(
|
||||
"idx_source_id" => array("source_id"),
|
||||
"idx_prio" => array("prio")
|
||||
),
|
||||
"fkey" => array(
|
||||
"fk_imapsync_source" => array(
|
||||
"col" => "source_id",
|
||||
"ref" => "imapsync_source.id",
|
||||
"delete" => "RESTRICT",
|
||||
"update" => "NO ACTION"
|
||||
)
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"imapsync_settings" => array(
|
||||
"cols" => array(
|
||||
"name" => "VARCHAR(64) NOT NULL",
|
||||
"value" => "VARCHAR(255) NOT NULL",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("name")
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
@@ -1127,7 +1605,9 @@ function init_db_schema()
|
||||
"redirect_uri" => "VARCHAR(2000)",
|
||||
"expires" => "TIMESTAMP NOT NULL",
|
||||
"scope" => "VARCHAR(4000)",
|
||||
"id_token" => "VARCHAR(1000)"
|
||||
"id_token" => "VARCHAR(1000)",
|
||||
"code_challenge" => "VARCHAR(1000)",
|
||||
"code_challenge_method" => "VARCHAR(20)"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
@@ -1184,11 +1664,138 @@ function init_db_schema()
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate legacy imapsync jobs (host/port/enc per job) to domain-scoped legacy sources
|
||||
// Re-entrant: only rows with source_id IS NULL are migrated, the legacy columns are kept while any are left
|
||||
if ($table == 'imapsync') {
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'imapsync'");
|
||||
if (count($stmt->fetchAll(PDO::FETCH_ASSOC)) != 0) {
|
||||
$imapsync_cols = array_column($pdo->query("SHOW COLUMNS FROM `imapsync`")->fetchAll(PDO::FETCH_ASSOC), 'Field', 'Field');
|
||||
if (isset($imapsync_cols['host1'])) {
|
||||
if (!isset($imapsync_cols['source_id'])) {
|
||||
$pdo->exec("ALTER TABLE `imapsync` ADD COLUMN `source_id` INT DEFAULT NULL");
|
||||
}
|
||||
if (!isset($imapsync_cols['custom_params_legacy'])) {
|
||||
$pdo->exec("ALTER TABLE `imapsync` ADD COLUMN `custom_params_legacy` TEXT DEFAULT NULL");
|
||||
}
|
||||
// Converted JSON may not fit into the old VARCHAR(512)
|
||||
$pdo->exec("ALTER TABLE `imapsync` MODIFY COLUMN `custom_params` TEXT");
|
||||
$imapsync_allow = \Mailcow\Imapsync\Model\CustomParams::allowlist($GLOBALS['IMAPSYNC_OPTIONS'] ?? [], $GLOBALS['IMAPSYNC_OPTIONS_DEFAULT'] ?? []);
|
||||
// A legacy source is found by its connection and its only target domain (none for jobs without domain)
|
||||
$source_sel = $pdo->prepare("SELECT `id` FROM `imapsync_source` s WHERE `created_by` = '' AND `host1` = :host AND `port1` = :port
|
||||
AND `enc1` = :enc AND `auth_type` = :auth
|
||||
AND (SELECT GROUP_CONCAT(`domain`) FROM `imapsync_source_domain` d WHERE d.`source_id` = s.`id`) <=> NULLIF(:domain, '')
|
||||
ORDER BY `id` LIMIT 1");
|
||||
$source_name_sel = $pdo->prepare("SELECT COUNT(*) FROM `imapsync_source` WHERE `created_by` = '' AND `name` = :name");
|
||||
// The old runner never checked certificates, legacy sources keep that
|
||||
$source_ins = $pdo->prepare("INSERT IGNORE INTO `imapsync_source` (`name`, `description`, `created_by`, `scope`, `host1`, `port1`, `enc1`, `verify_cert`, `auth_type`, `active`)
|
||||
VALUES (:name, 'Migrated sync jobs', '', 'domain', :host, :port, :enc, 0, :auth, 1)");
|
||||
$source_domain_ins = $pdo->prepare("INSERT IGNORE INTO `imapsync_source_domain` (`source_id`, `domain`) VALUES (:id, :domain)");
|
||||
$job_upd = $pdo->prepare("UPDATE `imapsync` SET `source_id` = :source_id, `custom_params` = :cp, `custom_params_legacy` = :cp_legacy,
|
||||
`dry` = GREATEST(`dry`, :dry) WHERE `id` = :id AND `source_id` IS NULL");
|
||||
$stmt = $pdo->query("SELECT `id`, `user2`, `host1`, `port1`, `enc1`, `custom_params` FROM `imapsync` WHERE `source_id` IS NULL");
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
// Parse the legacy custom_params string like the old runner did (all tokens up to the next option are its value)
|
||||
// into pairs valid under $IMAPSYNC_OPTIONS, a token after a flag is never its value.
|
||||
// --authmech1 PLAIN|LOGIN|CRAM-MD5 becomes the source auth type, --dry the job's dry flag (never run a simulation for real).
|
||||
// Anything else that does not fit is dropped, the original string is then kept in custom_params_legacy.
|
||||
$pairs = array();
|
||||
$auth = null;
|
||||
$dry = false;
|
||||
$dropped = false;
|
||||
$tokens = preg_split('/\s+/', trim((string)$row['custom_params']), -1, PREG_SPLIT_NO_EMPTY);
|
||||
for ($i = 0, $n = count($tokens); $i < $n; $i++) {
|
||||
if ($tokens[$i][0] !== '-') {
|
||||
// stray value without option
|
||||
$dropped = true;
|
||||
continue;
|
||||
}
|
||||
@list($opt, $val) = explode('=', ltrim($tokens[$i], '-'), 2);
|
||||
$opt = strtolower($opt);
|
||||
$inline = ($val !== null);
|
||||
$rest = array();
|
||||
while ($i + 1 < $n && $tokens[$i + 1][0] !== '-') {
|
||||
$rest[] = $tokens[++$i];
|
||||
}
|
||||
$rule = ($opt === 'authmech1') ? true : ($imapsync_allow[$opt] ?? null);
|
||||
if ($opt === 'dry' || $rule === false) {
|
||||
if ($inline || !empty($rest)) $dropped = true;
|
||||
if ($opt === 'dry') {
|
||||
$dry = true;
|
||||
} elseif (!$inline) {
|
||||
$pairs[] = array('o' => $opt, 'v' => '');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// unknown option, or "--opt=a b" (the old runner cut the value at the first space)
|
||||
if ($rule === null || ($inline && !empty($rest))) {
|
||||
$dropped = true;
|
||||
continue;
|
||||
}
|
||||
$val = $inline ? $val : implode(' ', $rest);
|
||||
if ($opt === 'authmech1' && $auth === null && in_array(strtoupper($val), array('PLAIN', 'LOGIN', 'CRAM-MD5'), true)) {
|
||||
$auth = strtoupper($val);
|
||||
} elseif ($opt === 'authmech1' || trim($val) === ''
|
||||
|| (is_string($rule) && preg_match('~^(?:' . str_replace('~', '\~', $rule) . ')$~D', $val) !== 1)) {
|
||||
$dropped = true;
|
||||
} else {
|
||||
$pairs[] = array('o' => $opt, 'v' => $val);
|
||||
}
|
||||
}
|
||||
// Old runner ignored the authmech1 column, only a custom --authmech1 was effective
|
||||
$auth = $auth ?? 'PLAIN';
|
||||
$host = strtolower(trim((string)$row['host1']));
|
||||
$port = (intval($row['port1']) >= 1 && intval($row['port1']) <= 65535) ? intval($row['port1']) : 143;
|
||||
$enc = in_array($row['enc1'], array('TLS', 'SSL', 'PLAIN'), true) ? $row['enc1'] : 'TLS';
|
||||
$at = strrchr((string)$row['user2'], '@');
|
||||
$domain = ($at === false) ? '' : strtolower(substr($at, 1));
|
||||
// One source per (host, port, enc, auth, domain), named "host (domain)" plus " 2", " 3"... when taken (<= 100 chars)
|
||||
$conn = array(':host' => $host, ':port' => $port, ':enc' => $enc, ':auth' => $auth);
|
||||
$source_sel->execute($conn + array(':domain' => $domain));
|
||||
if (($source_id = $source_sel->fetchColumn()) === false) {
|
||||
$base = mb_substr($host . ($domain !== '' ? ' (' . $domain . ')' : ''), 0, 94);
|
||||
for ($i = 1, $name = $base; $i < 1000; $name = $base . ' ' . ++$i) {
|
||||
$source_name_sel->execute(array(':name' => $name));
|
||||
if ($source_name_sel->fetchColumn() == 0) break;
|
||||
}
|
||||
$source_ins->execute($conn + array(':name' => $name));
|
||||
if (($source_id = $pdo->lastInsertId()) == 0) continue;
|
||||
if ($domain !== '') {
|
||||
$source_domain_ins->execute(array(':id' => $source_id, ':domain' => $domain));
|
||||
}
|
||||
}
|
||||
$job_upd->execute(array(
|
||||
':source_id' => $source_id,
|
||||
':cp' => json_encode($pairs, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
|
||||
':cp_legacy' => $dropped ? $row['custom_params'] : null,
|
||||
':dry' => $dry ? 1 : 0,
|
||||
':id' => $row['id']
|
||||
));
|
||||
}
|
||||
|
||||
// Never throw on leftovers: keep source_id nullable and the legacy columns, retried with the next db_version
|
||||
$pending = intval($pdo->query("SELECT COUNT(*) FROM `imapsync` WHERE `source_id` IS NULL")->fetchColumn());
|
||||
if ($pending > 0) {
|
||||
$properties['cols']['source_id'] = "INT DEFAULT NULL";
|
||||
$legacy_col_types = array(
|
||||
'host1' => "VARCHAR(255) DEFAULT NULL",
|
||||
'port1' => "SMALLINT UNSIGNED DEFAULT NULL",
|
||||
'enc1' => "ENUM('TLS','SSL','PLAIN') DEFAULT NULL",
|
||||
'authmech1' => "ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT NULL"
|
||||
);
|
||||
foreach ($legacy_col_types as $col => $type) {
|
||||
if (isset($imapsync_cols[$col])) $properties['cols'][$col] = $type;
|
||||
}
|
||||
error_log('mailcow: imapsync migration: ' . $pending . ' sync job(s) could not be assigned to a source and will not run');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE '" . $table . "'");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
if ($num_results != 0) {
|
||||
$stmt = $pdo->prepare("SELECT CONCAT('ALTER TABLE `', `table_schema`, '`.', `table_name`, ' DROP FOREIGN KEY ', `constraint_name`, ';') AS `FKEY_DROP` FROM `information_schema`.`table_constraints`
|
||||
WHERE `constraint_type` = 'FOREIGN KEY' AND `table_name` = :table;");
|
||||
WHERE `constraint_type` = 'FOREIGN KEY' AND `table_schema` = DATABASE() AND `table_name` = :table;");
|
||||
$stmt->execute(array(':table' => $table));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($row = array_shift($rows)) {
|
||||
@@ -1349,20 +1956,29 @@ function init_db_schema()
|
||||
AND (JSON_CONTAINS_PATH(`call`, 'one', '$[2].password')
|
||||
OR JSON_CONTAINS_PATH(`call`, 'one', '$[2].password2'));");
|
||||
|
||||
// Mitigate imapsync argument injection issue
|
||||
$pdo->query("UPDATE `imapsync` SET `custom_params` = ''
|
||||
WHERE `custom_params` LIKE '%pipemess%'
|
||||
OR custom_params LIKE '%skipmess%'
|
||||
OR custom_params LIKE '%delete2foldersonly%'
|
||||
OR custom_params LIKE '%delete2foldersbutnot%'
|
||||
OR custom_params LIKE '%regexflag%'
|
||||
OR custom_params LIKE '%pipemess%'
|
||||
OR custom_params LIKE '%regextrans2%'
|
||||
OR custom_params LIKE '%maxlinelengthcmd%';");
|
||||
// Mask syncjob secrets in old log entries (data and attr arguments of the logged call)
|
||||
$mask_paths = array();
|
||||
$unmasked = array();
|
||||
foreach (array(3, 4) as $idx) {
|
||||
foreach (array('password1', 'password2', 'oauth_client_secret', 'oauth_extra_params') as $key) {
|
||||
$mask_paths[] = "'$[" . $idx . "]." . $key . "'";
|
||||
// only rows still holding a value other than the string '*' (objects/arrays/numbers included), missing paths are NULL
|
||||
$unmasked[] = "JSON_UNQUOTE(JSON_EXTRACT(`call`, '$[" . $idx . "]." . $key . "')) <> '*'";
|
||||
}
|
||||
}
|
||||
$pdo->exec("UPDATE `logs` SET `call` = JSON_REPLACE(`call`, " . implode(", '*', ", $mask_paths) . ", '*')
|
||||
WHERE JSON_VALID(`call`)
|
||||
AND (JSON_VALUE(`call`, '$[0]') = 'syncjob'
|
||||
OR JSON_VALUE(`call`, '$[0]') LIKE 'imapsync%'
|
||||
OR (JSON_VALUE(`call`, '$[0]') = 'mailbox' AND JSON_VALUE(`call`, '$[2]') = 'syncjob'))
|
||||
AND (" . implode(" OR ", $unmasked) . ");");
|
||||
|
||||
// Migrate webauthn tfa
|
||||
$stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')");
|
||||
|
||||
// Syncjobs: seed global settings
|
||||
$pdo->query("INSERT IGNORE INTO `imapsync_settings` (`name`, `value`) VALUES ('max_parallel', '1')");
|
||||
|
||||
// Inject admin if not exists
|
||||
$stmt = $pdo->query("SELECT NULL FROM `admin`");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
@@ -1530,6 +2146,18 @@ function init_db_schema()
|
||||
// fill quarantine.qhash
|
||||
$pdo->query("UPDATE `quarantine` SET `qhash` = SHA2(CONCAT(`id`, `qid`), 256) WHERE ISNULL(`qhash`)");
|
||||
|
||||
// Group rows that lost their group_id (init_db of an older version ran) belong to their group again, so the
|
||||
// group can update its alias and revokes send-as rights of former members. An active group without rows of its
|
||||
// own for a grant only exists after such a downgrade; manual rows next to group rows stay manual.
|
||||
$pdo->query("UPDATE `alias` a JOIN `mailbox_group` g ON g.`address` = a.`address` SET a.`group_id` = g.`id` WHERE a.`group_id` IS NULL");
|
||||
$pdo->query("UPDATE `sender_acl` s
|
||||
JOIN `mailbox_group_member` m ON m.`username` = s.`logged_in_as`
|
||||
JOIN `mailbox_group` g ON g.`id` = m.`group_id` AND g.`active` = 1
|
||||
JOIN `mailbox_group_send_as` ga ON ga.`group_id` = m.`group_id` AND ga.`address` = s.`send_as`
|
||||
SET s.`group_id` = m.`group_id`
|
||||
WHERE s.`group_id` IS NULL AND s.`external` = 0
|
||||
AND NOT EXISTS (SELECT 1 FROM (SELECT `group_id` FROM `sender_acl` WHERE `group_id` IS NOT NULL) own WHERE own.`group_id` = m.`group_id`)");
|
||||
|
||||
} catch (PDOException $e) {
|
||||
if (php_sapi_name() == "cli") {
|
||||
echo "DB initialization failed: " . print_r($e, true) . PHP_EOL;
|
||||
@@ -1544,6 +2172,10 @@ function init_db_schema()
|
||||
}
|
||||
if (php_sapi_name() == "cli") {
|
||||
include '/web/inc/vars.inc.php';
|
||||
if (file_exists('/web/inc/vars.local.inc.php')) {
|
||||
include '/web/inc/vars.local.inc.php';
|
||||
}
|
||||
require_once '/web/inc/lib/vendor/autoload.php';
|
||||
include '/web/inc/functions.inc.php';
|
||||
include '/web/inc/functions.docker.inc.php';
|
||||
// $now = new DateTime();
|
||||
@@ -1561,7 +2193,7 @@ if (php_sapi_name() == "cli") {
|
||||
//PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '" . $offset . "', group_concat_max_len = 3423543543;",
|
||||
];
|
||||
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
||||
$stmt = $pdo->query("SELECT COUNT('OK') AS OK_C FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view' OR TABLE_NAME = '_sogo_static_view';");
|
||||
$stmt = $pdo->query("SELECT COUNT('OK') AS OK_C FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND (TABLE_NAME = 'sogo_view' OR TABLE_NAME = '_sogo_static_view');");
|
||||
$res = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (intval($res['OK_C']) === 2) {
|
||||
// Be more precise when replacing into _sogo_static_view, col orders may change
|
||||
@@ -1581,4 +2213,13 @@ if (php_sapi_name() == "cli") {
|
||||
// Dunno
|
||||
}
|
||||
init_db_schema();
|
||||
// Service API keys from mailcow.conf (API_KEY, API_KEY_READ_ONLY, API_ALLOW_FROM)
|
||||
try {
|
||||
$api_keys = \Mailcow\ApiKey\ApiKeyModule::create($pdo, \Mailcow\Core\Context::system())->keys;
|
||||
foreach ($api_keys->syncServiceKeys(getenv()) as $warning) {
|
||||
echo "API service keys: " . $warning . PHP_EOL;
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
echo "API service keys: sync failed: " . $e->getMessage() . PHP_EOL;
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,13 @@
|
||||
"mustangostang/spyc": "^0.6.3",
|
||||
"directorytree/ldaprecord": "^3.3",
|
||||
"twig/twig": "^3.0",
|
||||
"stevenmaguire/oauth2-keycloak": "^4.0",
|
||||
"stevenmaguire/oauth2-keycloak": "^6.1",
|
||||
"league/oauth2-client": "^2.7",
|
||||
"bacon/bacon-qr-code": "^2.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Mailcow\\": "mailcow/"
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+825
-472
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mailcow\ApiKey;
|
||||
|
||||
use Mailcow\ApiKey\Controller\ApiKeyApiController;
|
||||
use Mailcow\ApiKey\Repository\ApiKeyRepository;
|
||||
use Mailcow\ApiKey\Service\ApiKeyService;
|
||||
use Mailcow\Core\ApiResponse;
|
||||
use Mailcow\Core\Context;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Composition root of the API key module.
|
||||
*
|
||||
* process_add_return(ApiKeyModule::fromGlobals()->api->addKey($attr));
|
||||
* ApiKeyModule::create($pdo, Context::system())->keys->syncServiceKeys(getenv()); // php-fpm start
|
||||
*/
|
||||
class ApiKeyModule
|
||||
{
|
||||
/** Request keys whose values never reach a log */
|
||||
private const SECRET_KEYS = ['api_key'];
|
||||
|
||||
public function __construct(public readonly ApiKeyService $keys, public readonly ApiKeyApiController $api)
|
||||
{
|
||||
}
|
||||
|
||||
public static function create(PDO $pdo, Context $context): self
|
||||
{
|
||||
$keys = new ApiKeyService(new ApiKeyRepository($pdo), $context);
|
||||
return new self($keys, new ApiKeyApiController($keys, new ApiResponse(self::SECRET_KEYS)));
|
||||
}
|
||||
|
||||
/** Legacy entry point for procedural callers: reads $pdo and $_SESSION; $context overrides the session caller */
|
||||
public static function fromGlobals(?Context $context = null): self
|
||||
{
|
||||
return self::create($GLOBALS['pdo'], $context ?? Context::fromSession($_SESSION ?? []));
|
||||
}
|
||||
}
|
||||
Loaded 100 of 1631 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user