mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-09-01 11:47:15 +00:00
Compare commits
189
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
161f763b6c | ||
|
|
17d20599a7 | ||
|
|
d80b15b06c | ||
|
|
ed4fe41d7a | ||
|
|
6b9d875773 | ||
|
|
4d5b9d1c80 | ||
|
|
8abf453e1c | ||
|
|
8bd9e12a73 | ||
|
|
c665d430de | ||
|
|
2195a674ff | ||
|
|
089c44aee1 | ||
|
|
6644699e65 | ||
|
|
91d3b7246a | ||
|
|
b362c7105d | ||
|
|
555e7ed706 | ||
|
|
59e2a9ac77 | ||
|
|
feed5ad183 | ||
|
|
f1f7a9800b | ||
|
|
8c85044781 | ||
|
|
8fb32e4b59 | ||
|
|
b5fa926bc0 | ||
|
|
d1a2f4e168 | ||
|
|
f7a536d634 | ||
|
|
8b456a33e7 | ||
|
|
54da526d41 | ||
|
|
50be483c39 | ||
|
|
d64c923aca | ||
|
|
2d5f166ba8 | ||
|
|
94b5a623ab | ||
|
|
e406ecd461 | ||
|
|
68a95c3af3 | ||
|
|
bd037d5644 | ||
|
|
7036dbf4f8 | ||
|
|
2f10a4633f | ||
|
|
36c70db86c | ||
|
|
9cd16f0001 | ||
|
|
f4961c4023 | ||
|
|
38de21592c | ||
|
|
f44bd2f36a | ||
|
|
95a77f2dcb | ||
|
|
e856510fb2 | ||
|
|
d51d06d716 | ||
|
|
54170d075a | ||
|
|
b4bb1a625b | ||
|
|
2e5a29bf69 | ||
|
|
e245ac04d9 | ||
|
|
c877fdf0a5 | ||
|
|
fea38c8e1b | ||
|
|
cc9af65852 | ||
|
|
92cc8bec90 | ||
|
|
145745329e | ||
|
|
8e72d22c56 | ||
|
|
ddd76d99cd | ||
|
|
7e63061b30 | ||
|
|
52ca8f95c2 | ||
|
|
6758999cf3 | ||
|
|
5c5f251d35 | ||
|
|
1a10d0748e | ||
|
|
f4f9f72395 | ||
|
|
6f81778e5e | ||
|
|
8c9524a2fe | ||
|
|
2ebd32d2ee | ||
|
|
069b89629e | ||
|
|
496c3a51ac | ||
|
|
06a2a14c55 | ||
|
|
4b62af9d02 | ||
|
|
90ca1bf25a | ||
|
|
14772c3a20 | ||
|
|
e5d0ed8c4c | ||
|
|
c17cc8a792 | ||
|
|
4cba448671 | ||
|
|
7dab6c63d1 | ||
|
|
5408bc809d | ||
|
|
bd5c18b145 | ||
|
|
0f06c8e563 | ||
|
|
fffe1aec3d | ||
|
|
6f7fee49cd | ||
|
|
c1d75cf808 | ||
|
|
473fe2885d | ||
|
|
31e1550668 | ||
|
|
315f55bc65 | ||
|
|
e696ee2f6c | ||
|
|
d65aa11870 | ||
|
|
f6630e5b43 | ||
|
|
98e26c5603 | ||
|
|
3098caebbc | ||
|
|
94e23a3cd3 | ||
|
|
1840074991 | ||
|
|
99768e16f5 | ||
|
|
1f8b4679cc | ||
|
|
9e723e942d | ||
|
|
47e4552b44 | ||
|
|
ddcce811c1 | ||
|
|
7e6c36dce1 | ||
|
|
fa154c71f3 | ||
|
|
83a045ed3e | ||
|
|
064817ac70 | ||
|
|
2bd7a24b8c | ||
|
|
ecc2462e4c | ||
|
|
2d8db72d46 | ||
|
|
277a307fb9 | ||
|
|
d455555621 | ||
|
|
9ea2ff1dff | ||
|
|
24cc369d84 | ||
|
|
5f5367f2f9 | ||
|
|
03c31f825a | ||
|
|
c0050e8836 | ||
|
|
15891960f7 | ||
|
|
cce02e2b15 | ||
|
|
0fafda696b | ||
|
|
843db5854c | ||
|
|
23e4e4f373 | ||
|
|
175878f8f1 | ||
|
|
3fcda21c4e | ||
|
|
eac1bf02fc | ||
|
|
a7f2b2145f | ||
|
|
faac56a611 | ||
|
|
be37bc5a68 | ||
|
|
b227c76156 | ||
|
|
db4e3b4d54 | ||
|
|
ffbc37a00c | ||
|
|
e6fa0c5099 | ||
|
|
ce3ceaf5b8 | ||
|
|
efc2e76159 | ||
|
|
7d17715e2e | ||
|
|
aed440bcb3 | ||
|
|
4f2348631a | ||
|
|
da586682f6 | ||
|
|
22ae1f5363 | ||
|
|
c9cf8d7f38 | ||
|
|
cbd3d8b9bc | ||
|
|
f553f38635 | ||
|
|
07373907e3 | ||
|
|
104af58628 | ||
|
|
4ddcee28e4 | ||
|
|
b0d16bbcee | ||
|
|
9175e5f086 | ||
|
|
ff3d571054 | ||
|
|
a8e945f3da | ||
|
|
33547d1d73 | ||
|
|
539c32d99c | ||
|
|
d42c3823c4 | ||
|
|
8ead057ce9 | ||
|
|
886dbcc419 | ||
|
|
b0873edb6a | ||
|
|
694d751915 | ||
|
|
dc15994d40 | ||
|
|
ec24825280 | ||
|
|
08278a8be9 | ||
|
|
5a00b5124b | ||
|
|
8c039f694f | ||
|
|
95bf46c1e4 | ||
|
|
edde35156d | ||
|
|
84e3c32f13 | ||
|
|
ecb848493b | ||
|
|
8a65b9d1c6 | ||
|
|
ed9264fd2a | ||
|
|
7817dda43f | ||
|
|
018e292854 | ||
|
|
127fb1e8f5 | ||
|
|
09f09cb850 | ||
|
|
d4bf377a96 | ||
|
|
abd6fe8c79 | ||
|
|
5f8382ef44 | ||
|
|
03eccd4e42 | ||
|
|
1da8d1c894 | ||
|
|
d1feebf164 | ||
|
|
293b885a85 | ||
|
|
1e0850193a | ||
|
|
33acf56526 | ||
|
|
bea9ad7e8f | ||
|
|
e7ea3aa608 | ||
|
|
5888e248c3 | ||
|
|
2e176339ba | ||
|
|
8f883f3d37 | ||
|
|
709117fe19 | ||
|
|
82ea418423 | ||
|
|
fd24163c6e | ||
|
|
3caef45a12 | ||
|
|
8760e7e5db | ||
|
|
e848226062 | ||
|
|
efaeb77e13 | ||
|
|
569b4cf985 | ||
|
|
6c857243ec | ||
|
|
905e93627c | ||
|
|
598ea21827 | ||
|
|
6012cf4486 | ||
|
|
fe71f84c82 | ||
|
|
4e33c7143f |
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
||||
uses: actions/stale@v10.2.0
|
||||
uses: actions/stale@v11.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
||||
days-before-stale: 60
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- "watchdog-mailcow"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Setup Docker
|
||||
run: |
|
||||
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
|
||||
|
||||
@@ -8,11 +8,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Run the Action
|
||||
uses: devops-infra/action-pull-request@v1.0.2
|
||||
uses: devops-infra/action-pull-request@v1.4.0
|
||||
with:
|
||||
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
|
||||
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}
|
||||
|
||||
@@ -13,13 +13,13 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Generate postscreen_access.cidr
|
||||
run: |
|
||||
|
||||
@@ -23,10 +23,15 @@ A big thank you to everyone supporting us on GitHub Sponsors—your contribution
|
||||
<a href="https://www.maehdros.com/" target=_blank><img
|
||||
src="https://avatars.githubusercontent.com/u/173894712" height="58"
|
||||
/></a>
|
||||
<a href="https://garske-systems.de/" target=_blank><img
|
||||
src="https://garske-systems.de/img/logo_min.png" height="58"
|
||||
/></a>
|
||||
|
||||
### 50$/Month Sponsors
|
||||
<a href="https://github.com/vnukhr" target=_blank><img
|
||||
src="https://avatars.githubusercontent.com/u/7805987?s=52&v=4" height="58"
|
||||
/></a><a href="https://github.com/humiico" target=_blank><img
|
||||
src="https://avatars.githubusercontent.com/u/110134705?s=52&v=4" height="58"
|
||||
/></a>
|
||||
|
||||
## Info, documentation and support
|
||||
|
||||
@@ -64,7 +64,7 @@ adapt_new_options() {
|
||||
|
||||
sed -i --follow-symlinks '$a\' mailcow.conf
|
||||
for option in ${CONFIG_ARRAY[@]}; do
|
||||
if grep -q "${option}" mailcow.conf; then
|
||||
if grep -q "^#\?${option}=" mailcow.conf; then
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -302,7 +302,7 @@ adapt_new_options() {
|
||||
;;
|
||||
ACME_DNS_PROVIDER)
|
||||
echo '# DNS provider for DNS-01 challenge (e.g. dns_cf, dns_azure, dns_gd, etc.)' >> mailcow.conf
|
||||
echo '# See the dns-101 provider documentation for more information.' >> mailcow.conf
|
||||
echo '# See the dns-01 provider documentation for more information.' >> mailcow.conf
|
||||
echo 'ACME_DNS_PROVIDER=dns_xxx' >> mailcow.conf
|
||||
;;
|
||||
ACME_ACCOUNT_EMAIL)
|
||||
|
||||
@@ -249,14 +249,45 @@ while true; do
|
||||
done <<< "${SQL_DOMAINS}"
|
||||
|
||||
if [[ ${ONLY_MAILCOW_HOSTNAME} != "y" ]]; then
|
||||
# Fetch all domains with an active MTA-STS policy once.
|
||||
unset MTA_STS_ACTIVE_DOMAINS
|
||||
declare -A MTA_STS_ACTIVE_DOMAINS
|
||||
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
||||
SQL_MTA_STS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM mta_sts WHERE active = 1" -Bs)
|
||||
if [[ $? -eq 0 ]]; then
|
||||
while read mta_sts_domain; do
|
||||
if [[ -z "${mta_sts_domain}" ]]; then
|
||||
# ignore empty lines
|
||||
continue
|
||||
fi
|
||||
MTA_STS_ACTIVE_DOMAINS["${mta_sts_domain}"]=1
|
||||
done <<< "${SQL_MTA_STS_DOMAINS}"
|
||||
fi
|
||||
fi
|
||||
for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do
|
||||
unset VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
|
||||
declare -a VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
|
||||
for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do
|
||||
if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then
|
||||
if check_domain "${SUBDOMAIN}.${SQL_DOMAIN}"; then
|
||||
VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
|
||||
fi
|
||||
FULL_SUBDOMAIN="${SUBDOMAIN}.${SQL_DOMAIN}"
|
||||
|
||||
# Skip mta-sts subdomain unless MTA-STS is enabled (active) for this domain
|
||||
if [[ "${SUBDOMAIN}" == "mta-sts" && -z "${MTA_STS_ACTIVE_DOMAINS[${SQL_DOMAIN}]}" ]]; then
|
||||
log_f "MTA-STS is not enabled for ${SQL_DOMAIN} - skipping mta-sts subdomain certificate"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Skip if subdomain matches MAILCOW_HOSTNAME
|
||||
if [[ "${FULL_SUBDOMAIN}" == "${MAILCOW_HOSTNAME}" ]]; then
|
||||
continue
|
||||
fi
|
||||
# Skip if subdomain is covered by a wildcard in ADDITIONAL_SAN
|
||||
if is_covered_by_wildcard "${FULL_SUBDOMAIN}"; then
|
||||
log_f "Subdomain '${FULL_SUBDOMAIN}' is covered by wildcard - skipping explicit subdomain"
|
||||
continue
|
||||
fi
|
||||
# Validate and add subdomain
|
||||
if check_domain "${FULL_SUBDOMAIN}"; then
|
||||
VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${FULL_SUBDOMAIN}")
|
||||
fi
|
||||
done
|
||||
VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}")
|
||||
@@ -273,7 +304,10 @@ while true; do
|
||||
fi
|
||||
# Only add mta-sts subdomain for alias domains
|
||||
if [[ "mta-sts.${alias_domain}" != "${MAILCOW_HOSTNAME}" ]]; then
|
||||
if check_domain "mta-sts.${alias_domain}"; then
|
||||
# Skip if mta-sts subdomain is covered by a wildcard
|
||||
if is_covered_by_wildcard "mta-sts.${alias_domain}"; then
|
||||
log_f "Alias domain mta-sts subdomain 'mta-sts.${alias_domain}' is covered by wildcard - skipping"
|
||||
elif check_domain "mta-sts.${alias_domain}"; then
|
||||
VALIDATED_CONFIG_DOMAINS+=("mta-sts.${alias_domain}")
|
||||
fi
|
||||
fi
|
||||
@@ -308,13 +342,31 @@ while true; do
|
||||
done
|
||||
fi
|
||||
|
||||
# Check if MAILCOW_HOSTNAME is covered by a wildcard in ADDITIONAL_SAN
|
||||
MAILCOW_HOSTNAME_COVERED=0
|
||||
if [[ ! -z ${VALIDATED_MAILCOW_HOSTNAME} ]]; then
|
||||
if is_covered_by_wildcard "${VALIDATED_MAILCOW_HOSTNAME}"; then
|
||||
MAILCOW_PARENT_DOMAIN=$(echo ${VALIDATED_MAILCOW_HOSTNAME} | cut -d. -f2-)
|
||||
log_f "MAILCOW_HOSTNAME '${VALIDATED_MAILCOW_HOSTNAME}' is covered by wildcard '*.${MAILCOW_PARENT_DOMAIN}' - skipping explicit hostname"
|
||||
MAILCOW_HOSTNAME_COVERED=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Unique domains for server certificate
|
||||
if [[ ${ENABLE_SSL_SNI} == "y" ]]; then
|
||||
# create certificate for server name and fqdn SANs only
|
||||
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||
if [[ ${MAILCOW_HOSTNAME_COVERED} == "1" ]]; then
|
||||
SERVER_SAN_VALIDATED=($(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||
else
|
||||
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||
fi
|
||||
else
|
||||
# create certificate for all domains, including all subdomains from other domains [*]
|
||||
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||
if [[ ${MAILCOW_HOSTNAME_COVERED} == "1" ]]; then
|
||||
SERVER_SAN_VALIDATED=($(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||
else
|
||||
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
|
||||
fi
|
||||
fi
|
||||
if [[ ! -z ${SERVER_SAN_VALIDATED[*]} ]]; then
|
||||
CERT_NAME=${SERVER_SAN_VALIDATED[0]}
|
||||
|
||||
@@ -135,3 +135,32 @@ verify_challenge_path(){
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if a domain is covered by a wildcard (*.example.com) in ADDITIONAL_SAN
|
||||
# Usage: is_covered_by_wildcard "subdomain.example.com"
|
||||
# Returns: 0 if covered, 1 if not covered
|
||||
# Note: Only returns 0 (covered) when DNS-01 challenge is enabled,
|
||||
# as wildcards cannot be validated with HTTP-01 challenge
|
||||
is_covered_by_wildcard() {
|
||||
local DOMAIN=$1
|
||||
|
||||
# Only skip if DNS challenge is enabled (wildcards require DNS-01)
|
||||
if [[ ${ACME_DNS_CHALLENGE} != "y" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Return early if no ADDITIONAL_SAN is set
|
||||
if [[ -z ${ADDITIONAL_SAN} ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Extract parent domain (e.g., mail.example.com -> example.com)
|
||||
local PARENT_DOMAIN=$(echo ${DOMAIN} | cut -d. -f2-)
|
||||
|
||||
# Check if ADDITIONAL_SAN contains a wildcard for this parent domain
|
||||
if [[ "${ADDITIONAL_SAN}" == *"*.${PARENT_DOMAIN}"* ]]; then
|
||||
return 0 # Covered by wildcard
|
||||
fi
|
||||
|
||||
return 1 # Not covered
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ else
|
||||
__dns_loader_standalone=0
|
||||
fi
|
||||
|
||||
CONFIG_PATH="${ACME_DNS_CONFIG_FILE:-/etc/acme/dns-101.conf}"
|
||||
CONFIG_PATH="${ACME_DNS_CONFIG_FILE:-/etc/acme/dns-01.conf}"
|
||||
|
||||
if [[ ! -f "${CONFIG_PATH}" ]]; then
|
||||
if [[ $__dns_loader_standalone -eq 1 ]]; then
|
||||
|
||||
@@ -12,7 +12,7 @@ CERT_DOMAINS=(${DOMAINS[@]})
|
||||
CERT_DOMAIN=${CERT_DOMAINS[0]}
|
||||
ACME_BASE=/var/lib/acme
|
||||
|
||||
# Load optional DNS provider secrets from /etc/acme/dns-101.conf
|
||||
# Load optional DNS provider secrets from /etc/acme/dns-01.conf
|
||||
if [[ -f /srv/load-dns-config.sh ]]; then
|
||||
source /srv/load-dns-config.sh
|
||||
if declare -F log_f >/dev/null; then
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM alpine:3.21 AS builder
|
||||
FROM alpine:3.24 AS builder
|
||||
|
||||
WORKDIR /src
|
||||
ENV CLAMD_VERSION=1.4.2
|
||||
ENV CLAMD_VERSION=1.4.6
|
||||
|
||||
RUN apk upgrade --no-cache \
|
||||
&& apk add --update --no-cache \
|
||||
@@ -68,7 +68,7 @@ RUN wget -P /src https://www.clamav.net/downloads/production/clamav-${CLAMD_VERS
|
||||
"/clamav/etc/clamav/clamav-milter.conf.sample" > "/clamav/etc/clamav/clamav-milter.conf" || exit 1
|
||||
|
||||
|
||||
FROM alpine:3.21
|
||||
FROM alpine:3.24
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.22
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -44,109 +44,90 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
else
|
||||
QUOTA_TABLE=quota2replica
|
||||
fi
|
||||
|
||||
cat <<EOF > /etc/dovecot/conf.d/12-mysql.conf
|
||||
# Autogenerated by mailcow - DO NOT TOUCH!
|
||||
mysql /var/run/mysqld/mysqld.sock {
|
||||
dbname=${DBNAME}
|
||||
user=${DBUSER}
|
||||
password=${DBPASS}
|
||||
|
||||
ssl = no
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-quota.conf
|
||||
# Autogenerated by mailcow
|
||||
dict_map priv/quota/storage {
|
||||
sql_table = ${QUOTA_TABLE}
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
||||
map {
|
||||
pattern = priv/quota/storage
|
||||
table = ${QUOTA_TABLE}
|
||||
username_field = username
|
||||
value_field bytes {
|
||||
}
|
||||
value_field = bytes
|
||||
}
|
||||
|
||||
dict_map priv/quota/messages {
|
||||
sql_table = ${QUOTA_TABLE}
|
||||
map {
|
||||
pattern = priv/quota/messages
|
||||
table = ${QUOTA_TABLE}
|
||||
username_field = username
|
||||
value_field messages {
|
||||
}
|
||||
value_field = messages
|
||||
}
|
||||
EOF
|
||||
|
||||
# Create dict used for sieve pre and postfilters
|
||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
|
||||
# Autogenerated by mailcow
|
||||
|
||||
dict_map priv/sieve/name/\$script_name {
|
||||
sql_table = sieve_before
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
||||
map {
|
||||
pattern = priv/sieve/name/\$script_name
|
||||
table = sieve_before
|
||||
username_field = username
|
||||
value_field id {
|
||||
}
|
||||
|
||||
# The script name field in the table to query
|
||||
key_field script_name {
|
||||
value = \$script_name
|
||||
value_field = id
|
||||
fields {
|
||||
script_name = \$script_name
|
||||
}
|
||||
}
|
||||
|
||||
dict_map priv/sieve/data/\$id {
|
||||
sql_table = sieve_before
|
||||
map {
|
||||
pattern = priv/sieve/data/\$id
|
||||
table = sieve_before
|
||||
username_field = username
|
||||
value_field script_data {
|
||||
}
|
||||
key_field id {
|
||||
value = \$id
|
||||
value_field = script_data
|
||||
fields {
|
||||
id = \$id
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
|
||||
# Autogenerated by mailcow
|
||||
|
||||
dict_map priv/sieve/name/\$script_name {
|
||||
sql_table = sieve_after
|
||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
||||
map {
|
||||
pattern = priv/sieve/name/\$script_name
|
||||
table = sieve_after
|
||||
username_field = username
|
||||
value_field id {
|
||||
}
|
||||
key_field script_name {
|
||||
value = \$script_name
|
||||
value_field = id
|
||||
fields {
|
||||
script_name = \$script_name
|
||||
}
|
||||
}
|
||||
|
||||
dict_map priv/sieve/data/\$id {
|
||||
sql_table = sieve_after
|
||||
map {
|
||||
pattern = priv/sieve/data/\$id
|
||||
table = sieve_after
|
||||
username_field = username
|
||||
value_field script_data {
|
||||
}
|
||||
key_field id {
|
||||
value = \$id
|
||||
value_field = script_data
|
||||
fields {
|
||||
id = \$id
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
if [[ "${ACL_ANYONE}" == "allow" ]]; then
|
||||
echo -n "yes" > /etc/dovecot/acl_anyone
|
||||
else
|
||||
echo -n "no" > /etc/dovecot/acl_anyone
|
||||
fi
|
||||
echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
|
||||
|
||||
if [[ "${SKIP_FTS}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo -e "\e[33mDetecting SKIP_FTS=y... not enabling Flatcurve (FTS) then...\e[0m"
|
||||
echo -n 'quota quota_clone acl mail_crypt mail_crypt_acl mail_log mail_compress notify lazy_expunge' > /etc/dovecot/mail_plugins
|
||||
echo -n 'quota quota_clone imap_quota imap_acl acl imap_sieve mail_crypt mail_crypt_acl mail_compress notify mail_log' > /etc/dovecot/mail_plugins_imap
|
||||
echo -n 'quota quota_clone sieve acl mail_crypt mail_crypt_acl mail_compress notify' > /etc/dovecot/mail_plugins_lmtp
|
||||
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication lazy_expunge' > /etc/dovecot/mail_plugins
|
||||
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify listescape replication mail_log' > /etc/dovecot/mail_plugins_imap
|
||||
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
|
||||
else
|
||||
echo -e "\e[32mDetecting SKIP_FTS=n... enabling Flatcurve (FTS)\e[0m"
|
||||
echo -n 'quota quota_clone acl mail_crypt mail_crypt_acl mail_log mail_compress notify fts fts_flatcurve lazy_expunge' > /etc/dovecot/mail_plugins
|
||||
echo -n 'quota quota_clone imap_quota imap_acl acl imap_sieve mail_crypt mail_crypt_acl mail_compress notify mail_log fts fts_flatcurve' > /etc/dovecot/mail_plugins_imap
|
||||
echo -n 'quota quota_clone sieve acl mail_crypt mail_crypt_acl mail_compress fts fts_flatcurve notify' > /etc/dovecot/mail_plugins_lmtp
|
||||
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication lazy_expunge' > /etc/dovecot/mail_plugins
|
||||
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins_imap
|
||||
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_flatcurve notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
|
||||
fi
|
||||
chmod 644 /etc/dovecot/mail_plugins /etc/dovecot/mail_plugins_imap /etc/dovecot/mail_plugins_lmtp /templates/quarantine.tpl
|
||||
|
||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
|
||||
# Autogenerated by mailcow
|
||||
query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%{user | domain }}/%{user | username }/Maildir:VOLATILEDIR=/var/volatile/%{user}:INDEX=/var/vmail_index/%{user}') AS mail, '%{protocol}' AS protocol, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%{user}' AND (active = '1' OR active = '2')
|
||||
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')
|
||||
iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
|
||||
EOF
|
||||
|
||||
@@ -177,8 +158,8 @@ for cert_dir in /etc/ssl/mail/*/ ; do
|
||||
domains=($(cat ${cert_dir}domains))
|
||||
for domain in ${domains[@]}; do
|
||||
echo 'local_name '${domain}' {' >> /etc/dovecot/sni.conf;
|
||||
echo ' ssl_server_cert_file = '${cert_dir}'cert.pem' >> /etc/dovecot/sni.conf;
|
||||
echo ' ssl_server_key_file = '${cert_dir}'key.pem' >> /etc/dovecot/sni.conf;
|
||||
echo ' ssl_cert = <'${cert_dir}'cert.pem' >> /etc/dovecot/sni.conf;
|
||||
echo ' ssl_key = <'${cert_dir}'key.pem' >> /etc/dovecot/sni.conf;
|
||||
echo '}' >> /etc/dovecot/sni.conf;
|
||||
done
|
||||
done
|
||||
@@ -202,13 +183,11 @@ else
|
||||
fi
|
||||
cat <<EOF > /etc/dovecot/shared_namespace.conf
|
||||
# Autogenerated by mailcow
|
||||
namespace shared {
|
||||
namespace {
|
||||
type = shared
|
||||
separator = /
|
||||
prefix = Shared/\$user/
|
||||
mail_driver = maildir
|
||||
mail_path = %{owner_home}${MAILDIR_SUB_SHARED}
|
||||
mail_index_private_path = ~${MAILDIR_SUB_SHARED}/Shared/%{owner_user}
|
||||
prefix = Shared/%%u/
|
||||
location = maildir:%%h${MAILDIR_SUB_SHARED}:INDEX=~${MAILDIR_SUB_SHARED}/Shared/%%u
|
||||
subscriptions = no
|
||||
list = children
|
||||
}
|
||||
@@ -218,7 +197,7 @@ EOF
|
||||
cat <<EOF > /etc/dovecot/sogo_trusted_ip.conf
|
||||
# Autogenerated by mailcow
|
||||
remote ${IPV4_NETWORK}.248 {
|
||||
auth_allow_cleartext = yes
|
||||
disable_plaintext_auth = no
|
||||
}
|
||||
EOF
|
||||
|
||||
@@ -227,13 +206,9 @@ RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
|
||||
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
|
||||
cat <<EOF > /etc/dovecot/sogo-sso.conf
|
||||
# Autogenerated by mailcow
|
||||
passdb static {
|
||||
fields {
|
||||
allow_real_nets=${IPV4_NETWORK}.248/32
|
||||
}
|
||||
|
||||
password={plain}${RAND_PASS}
|
||||
|
||||
passdb {
|
||||
driver = static
|
||||
args = allow_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
|
||||
}
|
||||
EOF
|
||||
|
||||
@@ -261,9 +236,9 @@ fi
|
||||
if [[ "${SKIP_FTS}" =~ ^([nN][oO]|[nN])+$ ]]; then
|
||||
echo -e "\e[94mConfiguring FTS Settings...\e[0m"
|
||||
echo -e "\e[94mSetting FTS Memory Limit (per process) to ${FTS_HEAP} MB\e[0m"
|
||||
sed -i "s/vsz_limit\s*=\s*[0-9]*\s*MB*/vsz_limit=${FTS_HEAP} MB/" /etc/dovecot/conf.d/35-fts.conf
|
||||
sed -i "s/vsz_limit\s*=\s*[0-9]*\s*MB*/vsz_limit=${FTS_HEAP} MB/" /etc/dovecot/conf.d/fts.conf
|
||||
echo -e "\e[94mSetting FTS Process Limit to ${FTS_PROCS}\e[0m"
|
||||
sed -i "s/process_limit\s*=\s*[0-9]*/process_limit=${FTS_PROCS}/" /etc/dovecot/conf.d/35-fts.conf
|
||||
sed -i "s/process_limit\s*=\s*[0-9]*/process_limit=${FTS_PROCS}/" /etc/dovecot/conf.d/fts.conf
|
||||
fi
|
||||
|
||||
# 401 is user dovecot
|
||||
@@ -275,16 +250,16 @@ else
|
||||
chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem
|
||||
fi
|
||||
|
||||
# # Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
|
||||
# if grep -qE 'ssl_min_protocol\s*=\s*(TLSv1|TLSv1\.1)\s*$' /etc/dovecot/dovecot.conf /etc/dovecot/extra.conf; then
|
||||
# sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf
|
||||
# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
|
||||
if grep -qE 'ssl_min_protocol\s*=\s*(TLSv1|TLSv1\.1)\s*$' /etc/dovecot/dovecot.conf /etc/dovecot/extra.conf; then
|
||||
sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf
|
||||
|
||||
# echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf
|
||||
# echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf
|
||||
# echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
|
||||
# echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
|
||||
# echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
|
||||
# fi
|
||||
echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf
|
||||
echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf
|
||||
echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
|
||||
echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
|
||||
echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
|
||||
fi
|
||||
|
||||
# Compile sieve scripts
|
||||
sievec /var/vmail/sieve/global_sieve_before.sieve
|
||||
|
||||
@@ -5,35 +5,13 @@ use LockFile::Simple qw(lock trylock unlock);
|
||||
use Proc::ProcessTable;
|
||||
use Data::Dumper qw(Dumper);
|
||||
use IPC::Run 'run';
|
||||
use JSON::PP;
|
||||
use File::Temp;
|
||||
use Try::Tiny;
|
||||
use POSIX ();
|
||||
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';
|
||||
@@ -46,56 +24,92 @@ $dbh = DBI->connect($dsn, $ENV{'DBUSER'}, $ENV{'DBPASS'}, {
|
||||
});
|
||||
$dbh->do("UPDATE imapsync SET is_running = 0");
|
||||
|
||||
# Max concurrent imapsync processes (admin-configurable, stored in MySQL). Fallback 1.
|
||||
my $max_parallel = 1;
|
||||
my ($mp) = $dbh->selectrow_array("SELECT value FROM imapsync_settings WHERE name = 'max_parallel'");
|
||||
$max_parallel = int($mp) if defined($mp) && int($mp) >= 1;
|
||||
|
||||
# Global per-process bandwidth cap in bytes/s (0 = off). Applied to every imapsync run below.
|
||||
my $global_bw = 0;
|
||||
my ($gbw) = $dbh->selectrow_array("SELECT value FROM imapsync_settings WHERE name = 'max_bytes_per_second'");
|
||||
$global_bw = int($gbw) if defined($gbw) && int($gbw) > 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>;
|
||||
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
|
||||
# Source-based schema: join imapsync_source to read host/port/encryption/auth-type
|
||||
my $sth = $dbh->prepare("SELECT
|
||||
i.id,
|
||||
i.user1,
|
||||
i.user2,
|
||||
s.host1,
|
||||
s.auth_type,
|
||||
i.password1,
|
||||
i.exclude,
|
||||
s.port1,
|
||||
s.enc1,
|
||||
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,
|
||||
s.oauth_access_token,
|
||||
s.oauth_token_expires,
|
||||
s.oauth_flow,
|
||||
i.source_id
|
||||
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(last_run) > mins_interval * 60
|
||||
UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(i.last_run) > i.mins_interval * 60
|
||||
OR
|
||||
last_run IS NULL)
|
||||
ORDER BY last_run");
|
||||
i.last_run IS NULL)
|
||||
ORDER BY i.last_run ASC, i.prio DESC, i.id ASC
|
||||
LIMIT $max_parallel");
|
||||
|
||||
$sth->execute();
|
||||
my $row;
|
||||
my @rows = @{$sth->fetchall_arrayref()};
|
||||
$sth->finish();
|
||||
|
||||
while ($row = $sth->fetchrow_arrayref()) {
|
||||
# Only up to $max_parallel due jobs are pulled per run (LIMIT above)
|
||||
my $active = 0;
|
||||
foreach my $row (@rows) {
|
||||
# Throttle: once max children are running, reap one before starting the next
|
||||
if ($active >= $max_parallel) {
|
||||
wait();
|
||||
$active-- if $active > 0;
|
||||
}
|
||||
my $pid = fork();
|
||||
next if (!defined $pid);
|
||||
if ($pid != 0) { $active++; next; } # parent: one more child running, continue
|
||||
|
||||
# ---- CHILD ---- own DB connection so the parent's handle stays intact across forks
|
||||
$dbh->{InactiveDestroy} = 1;
|
||||
$dbh = DBI->connect($dsn, $ENV{'DBUSER'}, $ENV{'DBPASS'}, {
|
||||
mysql_auto_reconnect => 1,
|
||||
mysql_enable_utf8mb4 => 1
|
||||
});
|
||||
|
||||
$id = @$row[0];
|
||||
$user1 = @$row[1];
|
||||
$user2 = @$row[2];
|
||||
$host1 = @$row[3];
|
||||
$authmech1 = @$row[4];
|
||||
$auth_type = @$row[4];
|
||||
$password1 = @$row[5];
|
||||
$exclude = @$row[6];
|
||||
$port1 = @$row[7];
|
||||
@@ -113,21 +127,78 @@ while ($row = $sth->fetchrow_arrayref()) {
|
||||
$timeout1 = @$row[19];
|
||||
$timeout2 = @$row[20];
|
||||
$dry = @$row[21];
|
||||
$oauth_access_token = @$row[22];
|
||||
$oauth_token_expires = @$row[23];
|
||||
$oauth_flow = @$row[24];
|
||||
$source_id = @$row[25];
|
||||
|
||||
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);
|
||||
|
||||
my $tokenfile1;
|
||||
|
||||
binmode( $passfile1, ":utf8" );
|
||||
|
||||
print $passfile1 "$password1\n";
|
||||
|
||||
# Auth-mode-specific argument set
|
||||
my @auth_args;
|
||||
if (defined($auth_type) && $auth_type eq 'XOAUTH2') {
|
||||
# authorization_code sources use a per-user token (keyed by source + user1),
|
||||
# not the shared source-level token used by client_credentials.
|
||||
if (defined($oauth_flow) && $oauth_flow eq 'authorization_code') {
|
||||
my $tsth = $dbh->prepare("SELECT access_token, token_expires FROM imapsync_source_oauth_token WHERE source_id=? AND username=?");
|
||||
$tsth->execute($source_id, $user1);
|
||||
my $trow = $tsth->fetchrow_arrayref();
|
||||
$oauth_access_token = $trow ? @$trow[0] : undef;
|
||||
$oauth_token_expires = $trow ? @$trow[1] : undef;
|
||||
}
|
||||
# Skip sync if cached token is missing or about to expire (<2 min)
|
||||
if (!defined($oauth_access_token) || $oauth_access_token eq ''
|
||||
|| !defined($oauth_token_expires) || $oauth_token_expires - time() < 120) {
|
||||
my $upd = $dbh->prepare("UPDATE imapsync SET returned_text=?, success=0, exit_status='OAUTH_TOKEN_NOT_READY', last_run=NOW(), is_running=0 WHERE id=?");
|
||||
$upd->execute("OAuth access token for the configured source is missing or expires too soon; refresh cron will retry.", $id);
|
||||
$dbh->disconnect();
|
||||
unlink $passfile1->filename if defined $passfile1;
|
||||
unlink $passfile2->filename if defined $passfile2;
|
||||
POSIX::_exit(75); # EX_TEMPFAIL: did not actually sync (token not ready)
|
||||
}
|
||||
$tokenfile1 = File::Temp->new(TEMPLATE => $template);
|
||||
binmode($tokenfile1, ":utf8");
|
||||
print $tokenfile1 "$oauth_access_token\n";
|
||||
@auth_args = ("--authmech1", "XOAUTH2",
|
||||
"--oauthaccesstoken1", $tokenfile1->filename);
|
||||
} else {
|
||||
print $passfile1 "$password1\n";
|
||||
@auth_args = ("--passfile1", $passfile1->filename);
|
||||
# imapsync auto-picks PLAIN; only emit --authmech1 for LOGIN/CRAM-MD5
|
||||
if (defined($auth_type) && $auth_type ne 'PLAIN') {
|
||||
push @auth_args, "--authmech1", $auth_type;
|
||||
}
|
||||
}
|
||||
|
||||
print $passfile2 trim($master_pass) . "\n";
|
||||
|
||||
my @custom_params_a = qqw($custom_params);
|
||||
# Structured custom params: JSON array of {o,v}. Build one argv token per pair as "--opt=value"
|
||||
# (value glued to its option) so a value can never become a separate option; passed via
|
||||
# IPC::Run list form (execvp, no shell). Only allowlisted option names are stored (validated on write).
|
||||
my @custom_params_a;
|
||||
my $cp_pairs = eval { decode_json(defined($custom_params) && $custom_params ne '' ? $custom_params : '[]') };
|
||||
if (ref($cp_pairs) eq 'ARRAY') {
|
||||
for my $p (@$cp_pairs) {
|
||||
next unless ref($p) eq 'HASH' && defined $p->{o} && $p->{o} ne '';
|
||||
(my $ov = defined($p->{v}) ? $p->{v} : '') =~ s/\x00//g;
|
||||
push @custom_params_a, ($ov eq '' ? "--$p->{o}" : "--$p->{o}=$ov");
|
||||
}
|
||||
}
|
||||
my $custom_params_ref = \@custom_params_a;
|
||||
|
||||
# Effective per-process bandwidth cap (bytes/s): per-job value, capped by the global limit.
|
||||
my $eff_bw = int($maxbytespersecond);
|
||||
if ($global_bw > 0) {
|
||||
$eff_bw = ($eff_bw > 0 && $eff_bw < $global_bw) ? $eff_bw : $global_bw;
|
||||
}
|
||||
|
||||
my $generated_cmds = [ "/usr/local/bin/imapsync",
|
||||
"--tmpdir", "/tmp",
|
||||
"--nofoldersizes",
|
||||
@@ -137,7 +208,7 @@ while ($row = $sth->fetchrow_arrayref()) {
|
||||
($exclude eq "" ? () : ("--exclude", $exclude)),
|
||||
($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
|
||||
($maxage eq "0" ? () : ('--maxage', $maxage)),
|
||||
($maxbytespersecond eq "0" ? () : ('--maxbytespersecond', $maxbytespersecond)),
|
||||
($eff_bw <= 0 ? () : ('--maxbytespersecond', $eff_bw)),
|
||||
($delete2duplicates ne "1" ? () : ('--delete2duplicates')),
|
||||
($subscribeall ne "1" ? () : ('--subscribeall')),
|
||||
($delete1 ne "1" ? () : ('--delete')),
|
||||
@@ -147,7 +218,7 @@ while ($row = $sth->fetchrow_arrayref()) {
|
||||
(!defined($enc1) ? () : ($enc1)),
|
||||
"--host1", $host1,
|
||||
"--user1", $user1,
|
||||
"--passfile1", $passfile1->filename,
|
||||
@auth_args,
|
||||
"--port1", $port1,
|
||||
"--host2", "localhost",
|
||||
"--user2", $user2 . '*' . trim($master_user),
|
||||
@@ -187,10 +258,19 @@ while ($row = $sth->fetchrow_arrayref()) {
|
||||
$update->execute();
|
||||
};
|
||||
|
||||
|
||||
$dbh->disconnect();
|
||||
# Remove this child's own temp files, then _exit so END/DESTROY are skipped — this keeps
|
||||
# the parent's LockFile lock and DB handle intact (a plain exit() would run autoclean and
|
||||
# release the shared lock while the parent + siblings are still working).
|
||||
unlink $passfile1->filename if defined $passfile1;
|
||||
unlink $passfile2->filename if defined $passfile2;
|
||||
unlink $tokenfile1->filename if defined $tokenfile1;
|
||||
POSIX::_exit(0); # ---- end CHILD ----
|
||||
}
|
||||
|
||||
$sth->finish();
|
||||
# Parent: wait for all remaining children to finish
|
||||
while (wait() != -1) { }
|
||||
|
||||
$dbh->disconnect();
|
||||
|
||||
$lockmgr->unlock($lock_file);
|
||||
|
||||
@@ -47,7 +47,7 @@ try:
|
||||
if max_score == "":
|
||||
max_score = 9999.0
|
||||
|
||||
def query_mysql(query, headers = True, update = False):
|
||||
def query_mysql(query, params = None, headers = True, update = False):
|
||||
while True:
|
||||
try:
|
||||
cnx = MySQLdb.connect(user=os.environ.get('DBUSER'), password=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
|
||||
@@ -57,7 +57,10 @@ try:
|
||||
else:
|
||||
break
|
||||
cur = cnx.cursor()
|
||||
cur.execute(query)
|
||||
if params:
|
||||
cur.execute(query, params)
|
||||
else:
|
||||
cur.execute(query)
|
||||
if not update:
|
||||
result = []
|
||||
columns = tuple( [d[0] for d in cur.description] )
|
||||
@@ -76,7 +79,7 @@ try:
|
||||
|
||||
def notify_rcpt(rcpt, msg_count, quarantine_acl, category):
|
||||
if category == "add_header": category = "add header"
|
||||
meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category))
|
||||
meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = %s AND score < %s AND (action = %s OR "all" = %s)', (rcpt, max_score, category, category))
|
||||
print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count))
|
||||
if len(meta_query) == 0:
|
||||
return
|
||||
@@ -130,7 +133,7 @@ try:
|
||||
server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text)
|
||||
server.quit()
|
||||
for res in meta_query:
|
||||
query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True)
|
||||
query_mysql('UPDATE quarantine SET notified = 1 WHERE id = %s', (res['id'],), update = True)
|
||||
r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now)
|
||||
break
|
||||
except Exception as ex:
|
||||
@@ -138,7 +141,7 @@ try:
|
||||
print('%s' % (ex))
|
||||
time.sleep(3)
|
||||
|
||||
records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %f AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt' % (max_score))
|
||||
records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %s AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt', (max_score,))
|
||||
|
||||
for record in records:
|
||||
attrs = ''
|
||||
@@ -156,7 +159,7 @@ try:
|
||||
except Exception as ex:
|
||||
print('Could not determine last notification for %s, assuming never' % (record['rcpt']))
|
||||
last_notification = 0
|
||||
attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = "%s"' % (record['rcpt']))
|
||||
attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = %s', (record['rcpt'],))
|
||||
attrs = attrs_json[0]['attributes']
|
||||
if isinstance(attrs, str):
|
||||
# if attr is str then just load it
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM nginx:alpine
|
||||
FROM nginx:1.30.4-alpine
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ENV PIP_BREAK_SYSTEM_PACKAGES=1
|
||||
|
||||
@@ -7,13 +7,13 @@ ARG APCU_PECL_VERSION=5.1.28
|
||||
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG IMAGICK_PECL_VERSION=3.8.1
|
||||
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG MAILPARSE_PECL_VERSION=3.1.9
|
||||
ARG MAILPARSE_PECL_VERSION=3.2.0
|
||||
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG MEMCACHED_PECL_VERSION=3.4.0
|
||||
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG REDIS_PECL_VERSION=6.3.0
|
||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG COMPOSER_VERSION=2.9.5
|
||||
ARG COMPOSER_VERSION=2.10.2
|
||||
|
||||
RUN apk add -U --no-cache autoconf \
|
||||
aspell-dev \
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
FROM golang:1.26-trixie AS builder
|
||||
WORKDIR /src
|
||||
|
||||
ENV CGO_ENABLED=0 \
|
||||
GO111MODULE=on \
|
||||
NOOPT=1 \
|
||||
VERSION=1.8.22
|
||||
VERSION=1.11.0
|
||||
|
||||
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
|
||||
cd /src/postfix-tlspol && \
|
||||
scripts/build.sh build-only
|
||||
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
FROM debian:trixie-slim
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@version: 3.38
|
||||
@version: 4.8
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
@@ -7,7 +7,7 @@ options {
|
||||
dns_cache(no);
|
||||
use_fqdn(no);
|
||||
owner("root"); group("adm"); perm(0640);
|
||||
stats_freq(0);
|
||||
stats(freq(0));
|
||||
bad_hostname("^gconfd$");
|
||||
};
|
||||
source s_src {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@version: 3.38
|
||||
@version: 4.8
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
@@ -7,7 +7,7 @@ options {
|
||||
dns_cache(no);
|
||||
use_fqdn(no);
|
||||
owner("root"); group("adm"); perm(0640);
|
||||
stats_freq(0);
|
||||
stats(freq(0));
|
||||
bad_hostname("^gconfd$");
|
||||
};
|
||||
source s_src {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM debian:bookworm-slim
|
||||
FROM debian:trixie-slim
|
||||
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@version: 3.38
|
||||
@version: 4.8
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
@@ -7,7 +7,7 @@ options {
|
||||
dns_cache(no);
|
||||
use_fqdn(no);
|
||||
owner("root"); group("adm"); perm(0640);
|
||||
stats_freq(0);
|
||||
stats(freq(0));
|
||||
bad_hostname("^gconfd$");
|
||||
};
|
||||
source s_src {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@version: 3.38
|
||||
@version: 4.8
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
@@ -7,7 +7,7 @@ options {
|
||||
dns_cache(no);
|
||||
use_fqdn(no);
|
||||
owner("root"); group("adm"); perm(0640);
|
||||
stats_freq(0);
|
||||
stats(freq(0));
|
||||
bad_hostname("^gconfd$");
|
||||
};
|
||||
source s_src {
|
||||
|
||||
@@ -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_3.14.2-82~90302bc
|
||||
ARG RSPAMD_VER=rspamd_4.1.4-1~beb659b
|
||||
ARG CODENAME=trixie
|
||||
ENV LC_ALL=C
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# SOGo built from source to enable security patch application
|
||||
# Repository: https://github.com/Alinto/sogo
|
||||
# Version: SOGo-5.12.4
|
||||
# Version: SOGo-5.12.9
|
||||
#
|
||||
# Applied security patches:
|
||||
# - 16ab99e7cf8db2c30b211f0d5e338d7f9e3a9efb: XSS vulnerability in theme parameter
|
||||
# -
|
||||
#
|
||||
# To add new patches, modify SOGO_SECURITY_PATCHES ARG below with space-separated commit hashes
|
||||
|
||||
@@ -12,10 +12,10 @@ FROM debian:bookworm
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG SOGO_VERSION=SOGo-5.12.4
|
||||
ARG SOPE_VERSION=SOPE-5.12.4
|
||||
ARG SOGO_VERSION=SOGo-5.12.10
|
||||
ARG SOPE_VERSION=SOPE-5.12.10
|
||||
# Security patches to apply (space-separated commit hashes)
|
||||
ARG SOGO_SECURITY_PATCHES="16ab99e7cf8db2c30b211f0d5e338d7f9e3a9efb"
|
||||
ARG SOGO_SECURITY_PATCHES=""
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||
ARG GOSU_VERSION=1.19
|
||||
ENV LC_ALL=C
|
||||
@@ -26,6 +26,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
git \
|
||||
build-essential \
|
||||
gobjc \
|
||||
pkg-config \
|
||||
gnustep-make \
|
||||
gnustep-base-runtime \
|
||||
libgnustep-base-dev \
|
||||
@@ -40,6 +41,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libcurl4-openssl-dev \
|
||||
libzip-dev \
|
||||
libytnef0-dev \
|
||||
libwbxml2-dev \
|
||||
curl \
|
||||
ca-certificates \
|
||||
# Runtime dependencies
|
||||
@@ -68,6 +70,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libcurl4 \
|
||||
libzip4 \
|
||||
libytnef0 \
|
||||
libwbxml2-1 \
|
||||
# Download gosu
|
||||
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
|
||||
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
|
||||
@@ -97,6 +100,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
&& ./configure --disable-debug --disable-strip \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& cd /tmp/sogo/ActiveSync \
|
||||
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||
&& make -j$(nproc) install \
|
||||
&& cd / \
|
||||
&& rm -rf /tmp/sogo \
|
||||
# Strip binaries
|
||||
@@ -146,8 +152,8 @@ RUN echo "/usr/lib64" > /etc/ld.so.conf.d/sogo.conf \
|
||||
# Create sogo user and group
|
||||
RUN groupadd -r -g 999 sogo \
|
||||
&& useradd -r -u 999 -g sogo -d /var/lib/sogo -s /bin/bash -c "SOGo Daemon" sogo \
|
||||
&& mkdir -p /var/lib/sogo /var/run/sogo /var/log/sogo \
|
||||
&& chown -R sogo:sogo /var/lib/sogo /var/run/sogo /var/log/sogo
|
||||
&& mkdir -p /var/lib/sogo /var/run/sogo /var/log/sogo /var/spool/sogo \
|
||||
&& chown -R sogo:sogo /var/lib/sogo /var/run/sogo /var/log/sogo /var/spool/sogo
|
||||
|
||||
# Create symlinks for SOGo binaries
|
||||
RUN ln -s /usr/local/sbin/sogod /usr/sbin/sogod \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
--- /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200
|
||||
+++ /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200
|
||||
--- /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200
|
||||
+++ /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200
|
||||
@@ -46,7 +46,7 @@
|
||||
</md-item-template>
|
||||
</md-autocomplete>
|
||||
|
||||
@@ -130,18 +130,22 @@ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
# Patch ACLs
|
||||
#if [[ ${ACL_ANYONE} == 'allow' ]]; then
|
||||
# #enable any or authenticated targets for ACL
|
||||
# if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
||||
# patch -R /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
||||
# if patch -R -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
||||
# patch -R /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
||||
# fi
|
||||
#else
|
||||
# #disable any or authenticated targets for ACL
|
||||
# if patch -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
||||
# patch /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
||||
# if patch -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
|
||||
# patch /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
|
||||
# fi
|
||||
#fi
|
||||
|
||||
if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then
|
||||
patch -R /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
|
||||
# Apply custom UI patch (reverse patch to ADD buttons)
|
||||
if patch -R -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then
|
||||
echo "Applying navMailcowBtns patch (reverse to add buttons)..."
|
||||
patch -R /usr/local/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
|
||||
else
|
||||
echo "navMailcowBtns patch already applied or cannot be applied"
|
||||
fi
|
||||
|
||||
# Rename custom logo, if any
|
||||
@@ -149,7 +153,7 @@ fi
|
||||
|
||||
# Rsync web content
|
||||
echo "Syncing web content with named volume"
|
||||
rsync -a /usr/lib/GNUstep/SOGo/. /sogo_web/
|
||||
rsync -a /usr/local/lib/GNUstep/SOGo/. /sogo_web/
|
||||
|
||||
# Chown backup path
|
||||
chown -R sogo:sogo /sogo_backup
|
||||
|
||||
@@ -2,11 +2,14 @@ FROM alpine:3.23
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# install unbound from alpine:edge to get security patches
|
||||
RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main unbound
|
||||
|
||||
# install other packages from regular alpine stable repo
|
||||
RUN apk add --update --no-cache \
|
||||
curl \
|
||||
bind-tools \
|
||||
coreutils \
|
||||
unbound \
|
||||
bash \
|
||||
openssl \
|
||||
drill \
|
||||
|
||||
@@ -37,5 +37,6 @@ RUN apk add --update \
|
||||
COPY watchdog.sh /watchdog.sh
|
||||
COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
|
||||
COPY check_dns.sh /usr/lib/mailcow/check_dns.sh
|
||||
COPY client.cnf /etc/my.cnf.d/client.cnf
|
||||
|
||||
CMD ["/watchdog.sh"]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[client]
|
||||
ssl = false
|
||||
ssl-verify-server-cert = false
|
||||
@@ -38,7 +38,7 @@ if [[ ! -p /tmp/com_pipe ]]; then
|
||||
fi
|
||||
|
||||
# Wait for containers
|
||||
while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
||||
while ! mariadb-admin status --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
|
||||
echo "Waiting for SQL..."
|
||||
sleep 2
|
||||
done
|
||||
@@ -359,8 +359,8 @@ mysql_checks() {
|
||||
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
||||
touch /tmp/mysql-mailcow; echo "$(tail -50 /tmp/mysql-mailcow)" > /tmp/mysql-mailcow
|
||||
err_c_cur=${err_count}
|
||||
/usr/lib/nagios/plugins/check_mysql -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
/usr/lib/nagios/plugins/check_mysql_query -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
/usr/lib/nagios/plugins/check_mysql -f /etc/my.cnf.d/client.cnf -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
/usr/lib/nagios/plugins/check_mysql_query -f /etc/my.cnf.d/client.cnf -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
||||
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
|
||||
progress "MySQL/MariaDB" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
|
||||
@@ -384,7 +384,7 @@ mysql_repl_checks() {
|
||||
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
||||
touch /tmp/mysql_repl_checks; echo "$(tail -50 /tmp/mysql_repl_checks)" > /tmp/mysql_repl_checks
|
||||
err_c_cur=${err_count}
|
||||
/usr/lib/nagios/plugins/check_mysql_slavestatus.sh -S /var/run/mysqld/mysqld.sock -u root -p ${DBROOT} 2>> /tmp/mysql_repl_checks 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
/usr/lib/nagios/plugins/check_mysql_slavestatus.sh -o /etc/my.cnf.d/client.cnf -S /var/run/mysqld/mysqld.sock -u root -p ${DBROOT} 2>> /tmp/mysql_repl_checks 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
||||
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
|
||||
progress "MySQL/MariaDB replication" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
function auth_password_verify(request, password)
|
||||
request.domain = request.auth_user:match("@(.+)") or nil
|
||||
if request.domain == nil then
|
||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
|
||||
end
|
||||
@@ -10,10 +9,10 @@ function auth_password_verify(request, password)
|
||||
https.TIMEOUT = 30
|
||||
|
||||
local req = {
|
||||
username = request.auth_user,
|
||||
username = request.user,
|
||||
password = password,
|
||||
real_rip = request.remote_ip,
|
||||
service = request.protocol
|
||||
real_rip = request.real_rip,
|
||||
service = request.service
|
||||
}
|
||||
local req_json = json.encode(req)
|
||||
local res = {}
|
||||
@@ -33,7 +32,17 @@ function auth_password_verify(request, password)
|
||||
-- Returning PASSDB_RESULT_PASSWORD_MISMATCH will reset the user's auth cache entry.
|
||||
-- Returning PASSDB_RESULT_INTERNAL_FAILURE keeps the existing cache entry,
|
||||
-- even if the TTL has expired. Useful to avoid cache eviction during backend issues.
|
||||
|
||||
-- On a network-level failure (nginx unreachable, DNS failure, timeout) https.request
|
||||
-- returns nil plus an error string, so c is not a numeric HTTP status code. Treat this
|
||||
-- as a backend outage and keep the cache entry, rather than wiping it as a mismatch.
|
||||
if type(c) ~= "number" 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 unreachable"
|
||||
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"
|
||||
end
|
||||
|
||||
@@ -46,7 +55,7 @@ function auth_password_verify(request, password)
|
||||
end
|
||||
|
||||
if response_json.success == true then
|
||||
return dovecot.auth.PASSDB_RESULT_OK, { msg = "" }
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
end
|
||||
|
||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
|
||||
@@ -55,7 +64,3 @@ end
|
||||
function auth_passdb_lookup(req)
|
||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
|
||||
end
|
||||
|
||||
function auth_passdb_get_cache_key()
|
||||
return "%{protocol}:%{user | username}\t:%{password}"
|
||||
end
|
||||
@@ -1,3 +0,0 @@
|
||||
# /etc/dovecot/conf.d/05-core.conf
|
||||
# Core, single-line settings that don't fit elsewhere.
|
||||
recipient_delimiter = +
|
||||
@@ -1,13 +0,0 @@
|
||||
# /etc/dovecot/conf.d/10-logging.conf
|
||||
# Logging and debug.
|
||||
#mail_debug = yes
|
||||
#auth_debug = yes
|
||||
#log_debug = category=fts-flatcurve
|
||||
log_path = syslog
|
||||
log_timestamp = "%Y-%m-%d %H:%M:%S "
|
||||
login_log_format_elements = "user=<%{user}> method=%{mechanism} rip=%{remote_ip} lip=%{local_ip} mpid=%{mail_pid} %{secured} session=<%{session}>"
|
||||
|
||||
# Mail event logging.
|
||||
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
|
||||
mail_log_fields = uid box msgid size
|
||||
mail_log_cached_only = yes
|
||||
@@ -1,10 +0,0 @@
|
||||
# /etc/dovecot/conf.d/10-mail.conf
|
||||
# Mail storage paths and core mail settings.
|
||||
mail_home = /var/vmail/%{user | domain }/%{user | username }
|
||||
mail_driver = maildir
|
||||
mail_path = ~/Maildir
|
||||
mail_index_path = /var/vmail_index/%{user}
|
||||
mail_plugins = </etc/dovecot/mail_plugins
|
||||
mail_shared_explicit_inbox = yes
|
||||
mailbox_list_storage_escape_char = "\\"
|
||||
mail_prefetch_count = 30
|
||||
@@ -1,13 +0,0 @@
|
||||
# /etc/dovecot/conf.d/10-ssl.conf
|
||||
# TLS/SSL settings.
|
||||
ssl_min_protocol = TLSv1.2
|
||||
ssl_cipher_list = ALL:!ADH:!LOW:!SSLv2:!SSLv3:!EXP:!aNULL:!eNULL:!3DES:!MD5:!PSK:!DSS:!RC4:!SEED:!IDEA:+HIGH:+MEDIUM
|
||||
ssl_options = no_ticket
|
||||
#ssl_dh_parameters_length = 2048
|
||||
|
||||
ssl_server {
|
||||
prefer_ciphers = server
|
||||
dh_file = /etc/ssl/mail/dhparams.pem
|
||||
cert_file = /etc/ssl/mail/cert.pem
|
||||
key_file = /etc/ssl/mail/key.pem
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
# /etc/dovecot/conf.d/11-sql.conf
|
||||
# Default SQL driver used by SQL-based dicts/userdb.
|
||||
sql_driver = mysql
|
||||
@@ -1,8 +0,0 @@
|
||||
# Autogenerated by mailcow - DO NOT TOUCH!
|
||||
mysql /var/run/mysqld/mysqld.sock {
|
||||
dbname=mailcow
|
||||
user=mailcow
|
||||
password=D8O9BIivJc7Pb2VCfpAeLbAzUOZ0
|
||||
|
||||
ssl = no
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# /etc/dovecot/conf.d/12-storage-attachments.conf
|
||||
# External attachment storage.
|
||||
fs mail_ext_attachment {
|
||||
fs_driver = posix
|
||||
mail_ext_attachment_path = /var/attachments
|
||||
mail_ext_attachment_min_size = 128k
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
# /etc/dovecot/conf.d/15-performance.conf
|
||||
# Performance and mailbox tuning.
|
||||
# Enable only when you do not manually touch cur/.
|
||||
maildir_very_dirty_syncs = yes
|
||||
|
||||
# NFS examples | Only modify if using NFS!:
|
||||
#mm ap_disable = yes
|
||||
#mail_fsync = always
|
||||
#mail_nfs_index = yes
|
||||
#mail_nfs_storage = yes
|
||||
@@ -1,40 +0,0 @@
|
||||
# /etc/dovecot/conf.d/20-auth.conf
|
||||
# Authentication mechanisms, master/user separation, passdb chain, auth cache.
|
||||
auth_mechanisms = plain login
|
||||
auth_allow_cleartext = yes
|
||||
auth_master_user_separator = *
|
||||
|
||||
auth_cache_verify_password_with_worker = yes
|
||||
auth_cache_negative_ttl = 60s
|
||||
auth_cache_ttl = 300s
|
||||
auth_cache_size = 10M
|
||||
auth_verbose_passwords = sha1:6
|
||||
|
||||
# 1) Lua password verification (blocking, return mapping).
|
||||
passdb lua {
|
||||
driver = lua
|
||||
lua_file = /etc/dovecot/auth/passwd-verify.lua
|
||||
lua_settings {
|
||||
blocking=yes
|
||||
result_success = return-ok
|
||||
result_failure = continue
|
||||
result_internalfail = continue
|
||||
}
|
||||
}
|
||||
|
||||
# 2) Master password for master user logins.
|
||||
passdb master {
|
||||
driver = passwd-file
|
||||
passwd_file_path = /etc/dovecot/dovecot-master.passwd
|
||||
master = yes
|
||||
skip = authenticated
|
||||
}
|
||||
|
||||
# 3) Mandatory return layer: empty Lua (e.g. for forced reset).
|
||||
passdb empty-lua {
|
||||
driver = lua
|
||||
lua_file = /etc/dovecot/auth/passwd-verify.lua
|
||||
lua_settings {
|
||||
blocking = yes
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
# /etc/dovecot/conf.d/20-userdb.conf
|
||||
# User database chain.
|
||||
userdb passwd {
|
||||
driver = passwd-file
|
||||
passwd_file_path = /etc/dovecot/dovecot-master.userdb
|
||||
}
|
||||
|
||||
userdb sql {
|
||||
!include /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
|
||||
skip = found
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
# /etc/dovecot/conf.d/25-services.conf
|
||||
# All service listeners and workers.
|
||||
|
||||
# doveadm remote admin
|
||||
# Set doveadm_password in extra.conf.
|
||||
service doveadm {
|
||||
inet_listener doveadm {
|
||||
port = 12345
|
||||
}
|
||||
vsz_limit = 2048 MB
|
||||
}
|
||||
|
||||
# dict
|
||||
service dict {
|
||||
unix_listener dict {
|
||||
mode = 0660
|
||||
user = vmail
|
||||
group = vmail
|
||||
}
|
||||
}
|
||||
|
||||
# log
|
||||
service log {
|
||||
user = dovenull
|
||||
}
|
||||
|
||||
# config socket
|
||||
service config {
|
||||
unix_listener config {
|
||||
user = root
|
||||
group = vmail
|
||||
mode = 0660
|
||||
}
|
||||
}
|
||||
|
||||
# anvil socket
|
||||
service anvil {
|
||||
unix_listener anvil {
|
||||
user = vmail
|
||||
group = vmail
|
||||
mode = 0660
|
||||
}
|
||||
}
|
||||
|
||||
# auth sockets and inet
|
||||
service auth {
|
||||
inet_listener auth-inet {
|
||||
port = 10001
|
||||
}
|
||||
unix_listener auth-master {
|
||||
mode = 0600
|
||||
user = vmail
|
||||
}
|
||||
unix_listener auth-userdb {
|
||||
mode = 0600
|
||||
user = vmail
|
||||
}
|
||||
vsz_limit = 2G
|
||||
}
|
||||
|
||||
# managesieve login
|
||||
service managesieve-login {
|
||||
inet_listener sieve {
|
||||
port = 4190
|
||||
}
|
||||
inet_listener sieve_haproxy {
|
||||
port = 14190
|
||||
haproxy = yes
|
||||
}
|
||||
service_restart_request_count = 1
|
||||
process_min_avail = 2
|
||||
vsz_limit = 1G
|
||||
}
|
||||
|
||||
# imap login
|
||||
service imap-login {
|
||||
service_restart_request_count = 1
|
||||
process_min_avail = 2
|
||||
process_limit = 10000
|
||||
vsz_limit = 1G
|
||||
user = dovenull
|
||||
inet_listener imap_haproxy {
|
||||
port = 10143
|
||||
haproxy = yes
|
||||
}
|
||||
inet_listener imaps_haproxy {
|
||||
port = 10993
|
||||
ssl = yes
|
||||
haproxy = yes
|
||||
}
|
||||
}
|
||||
|
||||
# pop3 login
|
||||
service pop3-login {
|
||||
service_restart_request_count = 1
|
||||
process_min_avail = 1
|
||||
vsz_limit = 1G
|
||||
inet_listener pop3_haproxy {
|
||||
port = 10110
|
||||
haproxy = yes
|
||||
}
|
||||
inet_listener pop3s_haproxy {
|
||||
port = 10995
|
||||
ssl = yes
|
||||
haproxy = yes
|
||||
}
|
||||
}
|
||||
|
||||
# imap worker
|
||||
service imap {
|
||||
executable = imap
|
||||
user = vmail
|
||||
vsz_limit = 1G
|
||||
}
|
||||
|
||||
# managesieve worker
|
||||
service managesieve {
|
||||
process_limit = 256
|
||||
}
|
||||
|
||||
# lmtp
|
||||
service lmtp {
|
||||
inet_listener lmtp-inet {
|
||||
port = 24
|
||||
}
|
||||
user = vmail
|
||||
}
|
||||
|
||||
# quota warning hook
|
||||
service quota-warning {
|
||||
executable = script /usr/local/bin/quota_notify.py
|
||||
user = vmail
|
||||
unix_listener quota-warning {
|
||||
user = vmail
|
||||
}
|
||||
}
|
||||
|
||||
# stats
|
||||
service stats {
|
||||
unix_listener stats-writer {
|
||||
mode = 0660
|
||||
user = vmail
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
# /etc/dovecot/conf.d/30-protocols.conf
|
||||
# IMAP protocol specifics.
|
||||
protocol imap {
|
||||
mail_plugins = </etc/dovecot/mail_plugins_imap
|
||||
imap_metadata = yes
|
||||
}
|
||||
|
||||
# LMTP protocol specifics.
|
||||
protocol lmtp {
|
||||
mail_plugins = </etc/dovecot/mail_plugins_lmtp
|
||||
auth_socket_path = /var/run/dovecot/auth-master
|
||||
}
|
||||
|
||||
# ManageSieve protocol specifics.
|
||||
protocol sieve {
|
||||
managesieve_logout_format = bytes=%i/%o
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
# mailcow FTS Flatcurve Settings, change them as you like.
|
||||
|
||||
# Maximum term length can be set via the 'maxlen' argument (maxlen is
|
||||
# specified in bytes, not number of UTF-8 characters)
|
||||
language_tokenizer_address_token_maxlen = 100
|
||||
language_tokenizer_generic_algorithm = simple
|
||||
language_tokenizer_generic_token_maxlen = 30
|
||||
|
||||
# These are not flatcurve settings, but required for Dovecot FTS. See
|
||||
# Dovecot FTS Configuration link above for further information.
|
||||
language en {
|
||||
default = yes
|
||||
language_filters = lowercase snowball english-possessive stopwords
|
||||
}
|
||||
|
||||
language de {
|
||||
language_filters = lowercase snowball stopwords
|
||||
}
|
||||
|
||||
language es {
|
||||
language_filters = lowercase snowball stopwords
|
||||
}
|
||||
|
||||
language_tokenizers = generic email-address
|
||||
|
||||
fts_search_timeout = 300s
|
||||
|
||||
fts_autoindex = yes
|
||||
# Tweak this setting if you only want to ensure big and frequent folders are indexed, not all.
|
||||
fts_autoindex_max_recent_msgs = 20
|
||||
fts flatcurve {
|
||||
substring_search = no
|
||||
}
|
||||
|
||||
### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###
|
||||
|
||||
service indexer-worker {
|
||||
# Max amount of simultaniously running indexer jobs.
|
||||
process_limit=1
|
||||
|
||||
# Max amount of RAM used by EACH indexer process.
|
||||
vsz_limit=128 MB
|
||||
}
|
||||
|
||||
### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###
|
||||
@@ -1,12 +0,0 @@
|
||||
# /etc/dovecot/conf.d/40-acl.conf
|
||||
# ACL and shared mailboxes.
|
||||
imap_acl_allow_anyone = </etc/dovecot/acl_anyone
|
||||
|
||||
acl_sharing_map {
|
||||
dict file {
|
||||
path = /var/vmail/shared-mailboxes.db
|
||||
}
|
||||
}
|
||||
|
||||
acl_driver = vfile
|
||||
acl_user = %{user}
|
||||
@@ -1,7 +0,0 @@
|
||||
# /etc/dovecot/conf.d/40-attributes.conf
|
||||
# User/mail attributes.
|
||||
mail_attribute {
|
||||
dict file {
|
||||
path = /etc/dovecot/dovecot-attributes
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
# /etc/dovecot/conf.d/50-quota.conf
|
||||
# Quota configuration and notifications.
|
||||
quota "User quota" {
|
||||
driver = count
|
||||
|
||||
warning warn-95 {
|
||||
quota_storage_percentage = 95
|
||||
execute quota-warning {
|
||||
args = 95 %{user}
|
||||
}
|
||||
}
|
||||
|
||||
warning warn-80 {
|
||||
quota_storage_percentage = 80
|
||||
execute quota-warning {
|
||||
args = 80 %{user}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quota_clone {
|
||||
dict proxy {
|
||||
name = mysql_quota
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
# /etc/dovecot/conf.d/60-sieve-pipeline.conf
|
||||
# Complete Sieve pipeline: personal/global scripts, plugins, limits, training.
|
||||
|
||||
# Global before/after (file and dict)
|
||||
sieve_script before {
|
||||
type = before
|
||||
driver = file
|
||||
path = /var/vmail/sieve/global_sieve_before.sieve
|
||||
}
|
||||
|
||||
sieve_script before2 {
|
||||
type = before
|
||||
driver = dict
|
||||
name = active
|
||||
dict proxy {
|
||||
name = sieve_before
|
||||
}
|
||||
bin_path = /var/vmail/sieve_before_bindir/%{user}
|
||||
}
|
||||
|
||||
sieve_script after {
|
||||
type = after
|
||||
driver = file
|
||||
path = /var/vmail/sieve/global_sieve_after.sieve
|
||||
}
|
||||
|
||||
sieve_script after2 {
|
||||
type = after
|
||||
driver = dict
|
||||
name = active
|
||||
dict proxy {
|
||||
name = sieve_after
|
||||
}
|
||||
bin_path = /var/vmail/sieve_after_bindir/%{user}
|
||||
}
|
||||
|
||||
# Personal scripts
|
||||
sieve_script personal {
|
||||
type = personal
|
||||
driver = file
|
||||
path = ~/sieve
|
||||
active_path = ~/.dovecot.sieve
|
||||
}
|
||||
|
||||
# Plugins and behavior
|
||||
sieve_plugins = sieve_imapsieve sieve_extprograms
|
||||
sieve_vacation_send_from_recipient = yes
|
||||
sieve_redirect_envelope_from = recipient
|
||||
|
||||
# IMAPSieve training
|
||||
imapsieve_from Junk {
|
||||
sieve_script ham {
|
||||
type = before
|
||||
cause = copy
|
||||
path = /usr/lib/dovecot/sieve/report-ham.sieve
|
||||
}
|
||||
}
|
||||
mailbox Junk {
|
||||
sieve_script spam {
|
||||
type = before
|
||||
cause = copy
|
||||
path = /usr/lib/dovecot/sieve/report-spam.sieve
|
||||
}
|
||||
}
|
||||
|
||||
# Extprograms and extensions
|
||||
sieve_pipe_bin_dir = /usr/lib/dovecot/sieve
|
||||
sieve_plugins {
|
||||
sieve_extprograms = yes
|
||||
}
|
||||
sieve_global_extensions {
|
||||
vnd.dovecot.pipe = yes
|
||||
vnd.dovecot.execute = yes
|
||||
}
|
||||
|
||||
# Limits and duplicate handling
|
||||
sieve_max_script_size = 1M
|
||||
sieve_max_redirects = 100
|
||||
sieve_max_actions = 101
|
||||
sieve_quota_script_count = 0
|
||||
sieve_quota_storage_size = 0
|
||||
sieve_vacation_min_period = 5s
|
||||
sieve_vacation_max_period = 365d
|
||||
sieve_vacation_default_period = 60s
|
||||
sieve_duplicate_default_period = 1m
|
||||
sieve_duplicate_max_period = 7d
|
||||
|
||||
sieve_extensions {
|
||||
vacation-seconds = yes
|
||||
editheader = yes
|
||||
}
|
||||
|
||||
# pipe sockets in /var/run/dovecot/sieve-pipe
|
||||
sieve_pipe_socket_dir = sieve-pipe
|
||||
|
||||
# execute sockets in /var/run/dovecot/sieve-execute
|
||||
sieve_execute_socket_dir = sieve-execute
|
||||
@@ -1,6 +0,0 @@
|
||||
# /etc/dovecot/conf.d/70-crypto.conf
|
||||
# Global mail-crypt keys.
|
||||
crypt_global_private_key global {
|
||||
crypt_private_key_file = /mail_crypt/ecprivkey.pem
|
||||
}
|
||||
crypt_global_public_key_file = /mail_crypt/ecpubkey.pem
|
||||
@@ -1,3 +0,0 @@
|
||||
# /etc/dovecot/conf.d/80-compress.conf
|
||||
# Compression settings.
|
||||
mail_compress_write_method = lz4
|
||||
@@ -1,18 +0,0 @@
|
||||
# /etc/dovecot/conf.d/90-dict.conf
|
||||
# Dict declarations and SQL bindings.
|
||||
dict_server {
|
||||
dict sieve_after {
|
||||
driver = sql
|
||||
!include /etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
|
||||
}
|
||||
|
||||
dict sieve_before {
|
||||
driver = sql
|
||||
!include /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
|
||||
}
|
||||
|
||||
dict mysql_quota {
|
||||
driver = sql
|
||||
!include /etc/dovecot/sql/dovecot-dict-sql-quota.conf
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# /etc/dovecot/conf.d/90-limits.conf
|
||||
# Connection and memory limits; doveadm port.
|
||||
mail_max_userip_connections = 500
|
||||
imap_max_line_length = 2 M
|
||||
default_client_limit = 10400
|
||||
default_vsz_limit = 1024 M
|
||||
doveadm_port = 12345
|
||||
@@ -1,22 +0,0 @@
|
||||
# /etc/dovecot/conf.d/99-includes.conf
|
||||
# Late includes and site-specific bits.
|
||||
|
||||
# Mailbox layout includes (if used)
|
||||
!include /etc/dovecot/dovecot.folders.conf
|
||||
|
||||
# Optional replication
|
||||
!include_try /etc/dovecot/mail_replica.conf
|
||||
|
||||
# Existing includes you already had
|
||||
!include_try /etc/dovecot/sni.conf
|
||||
!include_try /etc/dovecot/sogo_trusted_ip.conf
|
||||
!include_try /etc/dovecot/shared_namespace.conf
|
||||
!include_try /etc/dovecot/conf.d/fts.conf
|
||||
|
||||
# Remote auth override
|
||||
remote 127.0.0.1 {
|
||||
auth_allow_cleartext = yes
|
||||
}
|
||||
|
||||
# Outbound submission target
|
||||
submission_host = postfix:588
|
||||
@@ -0,0 +1,37 @@
|
||||
# mailcow FTS Flatcurve Settings, change them as you like.
|
||||
plugin {
|
||||
fts_autoindex = yes
|
||||
fts_autoindex_exclude = \Junk
|
||||
fts_autoindex_exclude2 = \Trash
|
||||
# Tweak this setting if you only want to ensure big and frequent folders are indexed, not all.
|
||||
fts_autoindex_max_recent_msgs = 20
|
||||
fts = flatcurve
|
||||
|
||||
# Maximum term length can be set via the 'maxlen' argument (maxlen is
|
||||
# specified in bytes, not number of UTF-8 characters)
|
||||
fts_tokenizer_email_address = maxlen=100
|
||||
fts_tokenizer_generic = algorithm=simple maxlen=30
|
||||
|
||||
# These are not flatcurve settings, but required for Dovecot FTS. See
|
||||
# Dovecot FTS Configuration link above for further information.
|
||||
fts_languages = en es de
|
||||
fts_tokenizers = generic email-address
|
||||
|
||||
# OPTIONAL: Recommended default FTS core configuration
|
||||
fts_filters = normalizer-icu snowball stopwords
|
||||
fts_filters_en = lowercase snowball english-possessive stopwords
|
||||
|
||||
fts_index_timeout = 300s
|
||||
}
|
||||
|
||||
### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###
|
||||
|
||||
service indexer-worker {
|
||||
# Max amount of simultaniously running indexer jobs.
|
||||
process_limit=1
|
||||
|
||||
# Max amount of RAM used by EACH indexer process.
|
||||
vsz_limit=128 MB
|
||||
}
|
||||
|
||||
### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###
|
||||
+307
-30
@@ -1,34 +1,311 @@
|
||||
# /etc/dovecot/dovecot.conf
|
||||
# Base file kept minimal. All real config lives under conf.d/.
|
||||
dovecot_config_version = 2.4.0
|
||||
dovecot_storage_version = 2.4.0
|
||||
# --------------------------------------------------------------------------
|
||||
# Please create a file "extra.conf" for persistent overrides to dovecot.conf
|
||||
# --------------------------------------------------------------------------
|
||||
# LDAP example:
|
||||
#passdb {
|
||||
# args = /etc/dovecot/ldap/passdb.conf
|
||||
# driver = ldap
|
||||
#}
|
||||
|
||||
listen = *,[::]
|
||||
auth_mechanisms = plain login
|
||||
#mail_debug = yes
|
||||
#auth_debug = yes
|
||||
#log_debug = category=fts-flatcurve # Activate Logging for Flatcurve FTS Searchings
|
||||
log_path = syslog
|
||||
disable_plaintext_auth = yes
|
||||
# Uncomment on NFS share
|
||||
#mmap_disable = yes
|
||||
#mail_fsync = always
|
||||
#mail_nfs_index = yes
|
||||
#mail_nfs_storage = yes
|
||||
login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k"
|
||||
mail_home = /var/vmail/%d/%n
|
||||
mail_location = maildir:~/
|
||||
mail_plugins = </etc/dovecot/mail_plugins
|
||||
mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
|
||||
mail_attachment_dir = /var/attachments
|
||||
mail_attachment_min_size = 128k
|
||||
# Significantly speeds up very large mailboxes, but is only safe to enable if
|
||||
# you do not manually modify the files in the `cur` directories in
|
||||
# mailcowdockerized_vmail-vol-1.
|
||||
# https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-performance/
|
||||
maildir_very_dirty_syncs = yes
|
||||
|
||||
# Dovecot 2.2
|
||||
#ssl_protocols = !SSLv3
|
||||
# Dovecot 2.3
|
||||
ssl_min_protocol = TLSv1.2
|
||||
|
||||
ssl_prefer_server_ciphers = yes
|
||||
ssl_cipher_list = ALL:!ADH:!LOW:!SSLv2:!SSLv3:!EXP:!aNULL:!eNULL:!3DES:!MD5:!PSK:!DSS:!RC4:!SEED:!IDEA:+HIGH:+MEDIUM
|
||||
|
||||
# Default in Dovecot 2.3
|
||||
ssl_options = no_compression no_ticket
|
||||
|
||||
# New in Dovecot 2.3
|
||||
ssl_dh = </etc/ssl/mail/dhparams.pem
|
||||
# Dovecot 2.2
|
||||
#ssl_dh_parameters_length = 2048
|
||||
log_timestamp = "%Y-%m-%d %H:%M:%S "
|
||||
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
|
||||
args = /etc/dovecot/dovecot-master.passwd
|
||||
master = yes
|
||||
skip = authenticated
|
||||
}
|
||||
# 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
|
||||
passdb {
|
||||
driver = lua
|
||||
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes
|
||||
}
|
||||
# Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
|
||||
service doveadm {
|
||||
inet_listener {
|
||||
port = 12345
|
||||
}
|
||||
vsz_limit=2048 MB
|
||||
}
|
||||
!include /etc/dovecot/dovecot.folders.conf
|
||||
protocols = imap sieve lmtp pop3
|
||||
service dict {
|
||||
unix_listener dict {
|
||||
mode = 0660
|
||||
user = vmail
|
||||
group = vmail
|
||||
}
|
||||
}
|
||||
service log {
|
||||
user = dovenull
|
||||
}
|
||||
service config {
|
||||
unix_listener config {
|
||||
user = root
|
||||
group = vmail
|
||||
mode = 0660
|
||||
}
|
||||
}
|
||||
service auth {
|
||||
inet_listener auth-inet {
|
||||
port = 10001
|
||||
}
|
||||
unix_listener auth-master {
|
||||
mode = 0600
|
||||
user = vmail
|
||||
}
|
||||
unix_listener auth-userdb {
|
||||
mode = 0600
|
||||
user = vmail
|
||||
}
|
||||
vsz_limit = 2G
|
||||
}
|
||||
service managesieve-login {
|
||||
inet_listener sieve {
|
||||
port = 4190
|
||||
}
|
||||
inet_listener sieve_haproxy {
|
||||
port = 14190
|
||||
haproxy = yes
|
||||
}
|
||||
service_count = 1
|
||||
process_min_avail = 2
|
||||
vsz_limit = 1G
|
||||
}
|
||||
service imap-login {
|
||||
service_count = 1
|
||||
process_min_avail = 2
|
||||
process_limit = 10000
|
||||
vsz_limit = 1G
|
||||
user = dovenull
|
||||
inet_listener imap_haproxy {
|
||||
port = 10143
|
||||
haproxy = yes
|
||||
}
|
||||
inet_listener imaps_haproxy {
|
||||
port = 10993
|
||||
ssl = yes
|
||||
haproxy = yes
|
||||
}
|
||||
}
|
||||
service pop3-login {
|
||||
service_count = 1
|
||||
process_min_avail = 1
|
||||
vsz_limit = 1G
|
||||
inet_listener pop3_haproxy {
|
||||
port = 10110
|
||||
haproxy = yes
|
||||
}
|
||||
inet_listener pop3s_haproxy {
|
||||
port = 10995
|
||||
ssl = yes
|
||||
haproxy = yes
|
||||
}
|
||||
}
|
||||
service imap {
|
||||
executable = imap
|
||||
user = vmail
|
||||
vsz_limit = 1G
|
||||
}
|
||||
service managesieve {
|
||||
process_limit = 256
|
||||
}
|
||||
service lmtp {
|
||||
inet_listener lmtp-inet {
|
||||
port = 24
|
||||
}
|
||||
user = vmail
|
||||
}
|
||||
listen = *,[::]
|
||||
ssl_cert = </etc/ssl/mail/cert.pem
|
||||
ssl_key = </etc/ssl/mail/key.pem
|
||||
userdb {
|
||||
driver = passwd-file
|
||||
args = /etc/dovecot/dovecot-master.userdb
|
||||
}
|
||||
userdb {
|
||||
args = /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
|
||||
driver = sql
|
||||
skip = found
|
||||
}
|
||||
protocol imap {
|
||||
mail_plugins = </etc/dovecot/mail_plugins_imap
|
||||
imap_metadata = yes
|
||||
}
|
||||
mail_attribute_dict = file:%h/dovecot-attributes
|
||||
protocol lmtp {
|
||||
mail_plugins = </etc/dovecot/mail_plugins_lmtp
|
||||
auth_socket_path = /var/run/dovecot/auth-master
|
||||
}
|
||||
protocol sieve {
|
||||
managesieve_logout_format = bytes=%i/%o
|
||||
}
|
||||
plugin {
|
||||
# Allow "any" or "authenticated" to be used in ACLs
|
||||
acl_anyone = </etc/dovecot/acl_anyone
|
||||
acl_shared_dict = file:/var/vmail/shared-mailboxes.db
|
||||
acl = vfile
|
||||
acl_user = %u
|
||||
quota = dict:Userquota::proxy::sqlquota
|
||||
quota_rule2 = Trash:storage=+100%%
|
||||
sieve = /var/vmail/sieve/%u.sieve
|
||||
sieve_plugins = sieve_imapsieve sieve_extprograms
|
||||
sieve_vacation_send_from_recipient = yes
|
||||
sieve_redirect_envelope_from = recipient
|
||||
# From elsewhere to Spam folder
|
||||
imapsieve_mailbox1_name = Junk
|
||||
imapsieve_mailbox1_causes = COPY
|
||||
imapsieve_mailbox1_before = file:/usr/lib/dovecot/sieve/report-spam.sieve
|
||||
# END
|
||||
# From Spam folder to elsewhere
|
||||
imapsieve_mailbox2_name = *
|
||||
imapsieve_mailbox2_from = Junk
|
||||
imapsieve_mailbox2_causes = COPY
|
||||
imapsieve_mailbox2_before = file:/usr/lib/dovecot/sieve/report-ham.sieve
|
||||
# END
|
||||
master_user = %u
|
||||
quota_warning = storage=95%% quota-warning 95 %u
|
||||
quota_warning2 = storage=80%% quota-warning 80 %u
|
||||
sieve_pipe_bin_dir = /usr/lib/dovecot/sieve
|
||||
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.execute
|
||||
sieve_extensions = +notify +imapflags +vacation-seconds +editheader
|
||||
sieve_max_script_size = 1M
|
||||
sieve_max_redirects = 100
|
||||
sieve_max_actions = 101
|
||||
sieve_quota_max_scripts = 0
|
||||
sieve_quota_max_storage = 0
|
||||
listescape_char = "\\"
|
||||
sieve_vacation_min_period = 5s
|
||||
sieve_vacation_max_period = 0
|
||||
sieve_vacation_default_period = 60s
|
||||
sieve_before = /var/vmail/sieve/global_sieve_before.sieve
|
||||
sieve_before2 = dict:proxy::sieve_before;name=active;bindir=/var/vmail/sieve_before_bindir
|
||||
sieve_after = dict:proxy::sieve_after;name=active;bindir=/var/vmail/sieve_after_bindir
|
||||
sieve_after2 = /var/vmail/sieve/global_sieve_after.sieve
|
||||
sieve_duplicate_default_period = 1m
|
||||
sieve_duplicate_max_period = 7d
|
||||
|
||||
!include_try /etc/dovecot/conf.d/05-core.conf
|
||||
!include_try /etc/dovecot/conf.d/10-logging.conf
|
||||
!include_try /etc/dovecot/conf.d/10-mail.conf
|
||||
!include_try /etc/dovecot/conf.d/10-ssl.conf
|
||||
!include_try /etc/dovecot/conf.d/11-sql.conf
|
||||
!include_try /etc/dovecot/conf.d/12-mysql.conf
|
||||
!include_try /etc/dovecot/conf.d/12-storage-attachments.conf
|
||||
!include_try /etc/dovecot/conf.d/15-performance.conf
|
||||
!include_try /etc/dovecot/conf.d/20-auth.conf
|
||||
!include_try /etc/dovecot/conf.d/20-userdb.conf
|
||||
!include_try /etc/dovecot/conf.d/25-services.conf
|
||||
!include_try /etc/dovecot/conf.d/30-protocols.conf
|
||||
!include_try /etc/dovecot/conf.d/35-fts.conf
|
||||
!include_try /etc/dovecot/conf.d/40-acl.conf
|
||||
!include_try /etc/dovecot/conf.d/40-attributes.conf
|
||||
!include_try /etc/dovecot/conf.d/50-quota.conf
|
||||
!include_try /etc/dovecot/conf.d/60-sieve-pipeline.conf
|
||||
!include_try /etc/dovecot/conf.d/70-crypto.conf
|
||||
!include_try /etc/dovecot/conf.d/80-compress.conf
|
||||
!include_try /etc/dovecot/conf.d/80-mail-logging.conf
|
||||
!include_try /etc/dovecot/conf.d/90-limits.conf
|
||||
!include_try /etc/dovecot/conf.d/90-dict.conf
|
||||
!include_try /etc/dovecot/conf.d/99-includes.conf
|
||||
# -- Global keys
|
||||
mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem
|
||||
mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem
|
||||
mail_crypt_save_version = 2
|
||||
|
||||
# Last: local overrides
|
||||
!include_try /etc/dovecot/extra.conf
|
||||
# Enable compression while saving, lz4 Dovecot v2.3.17+
|
||||
zlib_save = lz4
|
||||
|
||||
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
|
||||
mail_log_fields = uid box msgid size
|
||||
mail_log_cached_only = yes
|
||||
|
||||
# Try set mail_replica
|
||||
!include_try /etc/dovecot/mail_replica.conf
|
||||
}
|
||||
service quota-warning {
|
||||
executable = script /usr/local/bin/quota_notify.py
|
||||
# use some unprivileged user for executing the quota warnings
|
||||
user = vmail
|
||||
unix_listener quota-warning {
|
||||
user = vmail
|
||||
}
|
||||
}
|
||||
dict {
|
||||
sqlquota = mysql:/etc/dovecot/sql/dovecot-dict-sql-quota.conf
|
||||
sieve_after = mysql:/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
|
||||
sieve_before = mysql:/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
|
||||
}
|
||||
remote 127.0.0.1 {
|
||||
disable_plaintext_auth = no
|
||||
}
|
||||
submission_host = postfix:588
|
||||
mail_max_userip_connections = 500
|
||||
service stats {
|
||||
unix_listener stats-writer {
|
||||
mode = 0660
|
||||
user = vmail
|
||||
}
|
||||
}
|
||||
imap_max_line_length = 2 M
|
||||
auth_cache_verify_password_with_worker = yes
|
||||
auth_cache_negative_ttl = 60s
|
||||
auth_cache_ttl = 300s
|
||||
auth_cache_size = 10M
|
||||
auth_verbose_passwords = sha1:6
|
||||
service replicator {
|
||||
process_min_avail = 1
|
||||
}
|
||||
service aggregator {
|
||||
fifo_listener replication-notify-fifo {
|
||||
user = vmail
|
||||
}
|
||||
unix_listener replication-notify {
|
||||
user = vmail
|
||||
}
|
||||
}
|
||||
service replicator {
|
||||
unix_listener replicator-doveadm {
|
||||
mode = 0666
|
||||
}
|
||||
}
|
||||
replication_max_conns = 10
|
||||
doveadm_port = 12345
|
||||
replication_dsync_parameters = -d -l 30 -U -n INBOX
|
||||
# <Includes>
|
||||
!include_try /etc/dovecot/sni.conf
|
||||
!include_try /etc/dovecot/sogo_trusted_ip.conf
|
||||
!include_try /etc/dovecot/extra.conf
|
||||
!include_try /etc/dovecot/shared_namespace.conf
|
||||
!include_try /etc/dovecot/conf.d/fts.conf
|
||||
# </Includes>
|
||||
default_client_limit = 10400
|
||||
default_vsz_limit = 1024 M
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
namespace inbox {
|
||||
inbox = yes
|
||||
location =
|
||||
separator = /
|
||||
mailbox storage/* {
|
||||
quota_storage_extra = 100M
|
||||
}
|
||||
mailbox "Trash" {
|
||||
auto = subscribe
|
||||
special_use = \Trash
|
||||
quota_storage_percentage = 100
|
||||
fts_autoindex = no
|
||||
}
|
||||
mailbox "Deleted Messages" {
|
||||
special_use = \Trash
|
||||
@@ -199,7 +195,6 @@ namespace inbox {
|
||||
mailbox "Junk" {
|
||||
auto = subscribe
|
||||
special_use = \Junk
|
||||
fts_autoindex = no
|
||||
}
|
||||
mailbox "Junk-E-Mail" {
|
||||
special_use = \Junk
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
# global_sieve_before script
|
||||
# global_sieve_before -> user sieve_before (mailcow UI) -> user sieve_after (mailcow UI) -> global_sieve_after
|
||||
|
||||
require ["mailbox", "fileinto"];
|
||||
|
||||
if header :contains ["Chat-Version"] [""] {
|
||||
if mailboxexists "DeltaChat" {
|
||||
fileinto "DeltaChat";
|
||||
} else {
|
||||
fileinto :create "DeltaChat";
|
||||
}
|
||||
stop;
|
||||
}
|
||||
|
||||
@@ -42,12 +42,19 @@ http {
|
||||
https https;
|
||||
}
|
||||
|
||||
map $server_port $sogo_auth_internal {
|
||||
65510 "1";
|
||||
default "";
|
||||
}
|
||||
|
||||
{% if HTTP_REDIRECT %}
|
||||
# HTTP to HTTPS redirect
|
||||
server {
|
||||
root /web;
|
||||
listen {{ HTTP_PORT }} default_server;
|
||||
{% if ENABLE_IPV6 %}
|
||||
listen [::]:{{ HTTP_PORT }} default_server;
|
||||
{%endif%}
|
||||
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
|
||||
|
||||
|
||||
@@ -116,6 +116,9 @@ 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;
|
||||
# trusted internal-auth marker; empty for external clients (see nginx.conf map)
|
||||
fastcgi_param SOGO_AUTH_INTERNAL $sogo_auth_internal;
|
||||
fastcgi_read_timeout 3600;
|
||||
fastcgi_send_timeout 3600;
|
||||
}
|
||||
@@ -261,19 +264,19 @@ location ~* /sogo$ {
|
||||
}
|
||||
|
||||
location /SOGo.woa/WebServerResources/ {
|
||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
||||
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||
}
|
||||
|
||||
location /.woa/WebServerResources/ {
|
||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
||||
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||
}
|
||||
|
||||
location /SOGo/WebServerResources/ {
|
||||
alias /usr/lib/GNUstep/SOGo/WebServerResources/;
|
||||
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
|
||||
}
|
||||
|
||||
location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) {
|
||||
alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
|
||||
alias /usr/local/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<?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';
|
||||
require_once __DIR__ . '/../web/inc/functions.syncjob.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 {
|
||||
// 1) client_credentials sources: refresh the shared source-level token (expires < 5 min or none)
|
||||
$stmt = $pdo->prepare("SELECT `id`, `name` FROM `imapsync_source`
|
||||
WHERE `active` = 1 AND `auth_type` = 'XOAUTH2' AND `oauth_flow` = 'client_credentials'
|
||||
AND (`oauth_token_expires` IS NULL OR `oauth_token_expires` < :soon)");
|
||||
$stmt->execute(array(':soon' => time() + 300));
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $src) {
|
||||
$ok = imapsync_source_refresh_token_internal($src['id']);
|
||||
if ($ok) {
|
||||
logMsg("info", "Refreshed token for source '" . $src['name'] . "' (id=" . $src['id'] . ")");
|
||||
} else {
|
||||
$e = $pdo->prepare("SELECT `oauth_last_refresh_error` FROM `imapsync_source` WHERE `id` = :id");
|
||||
$e->execute(array(':id' => $src['id']));
|
||||
logMsg("warning", "Refresh failed for source '" . $src['name'] . "' (id=" . $src['id'] . "): " . $e->fetchColumn());
|
||||
}
|
||||
}
|
||||
|
||||
// 2) authorization_code sources: refresh each per-user token via its stored refresh_token
|
||||
$stmt = $pdo->prepare("SELECT t.`source_id`, t.`username`, s.`name`
|
||||
FROM `imapsync_source_oauth_token` t
|
||||
JOIN `imapsync_source` s ON s.`id` = t.`source_id`
|
||||
WHERE s.`active` = 1 AND s.`auth_type` = 'XOAUTH2' AND s.`oauth_flow` = 'authorization_code'
|
||||
AND t.`refresh_token` <> ''
|
||||
AND (t.`token_expires` IS NULL OR t.`token_expires` < :soon)");
|
||||
$stmt->execute(array(':soon' => time() + 300));
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $tok) {
|
||||
$ok = imapsync_source_refresh_user_token_internal($tok['source_id'], $tok['username']);
|
||||
if ($ok) {
|
||||
logMsg("info", "Refreshed user token '" . $tok['username'] . "' on source '" . $tok['name'] . "' (id=" . $tok['source_id'] . ")");
|
||||
} else {
|
||||
$e = $pdo->prepare("SELECT `last_refresh_error` FROM `imapsync_source_oauth_token` WHERE `source_id` = :sid AND `username` = :user");
|
||||
$e->execute(array(':sid' => $tok['source_id'], ':user' => $tok['username']));
|
||||
logMsg("warning", "User-token refresh failed for '" . $tok['username'] . "' on source '" . $tok['name'] . "' (id=" . $tok['source_id'] . "): " . $e->fetchColumn());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@unlink($lock_file);
|
||||
}
|
||||
@@ -37,7 +37,7 @@ postscreen_access_list = permit_mynetworks,
|
||||
cidr:/opt/postfix/conf/postscreen_access.cidr,
|
||||
tcp:127.0.0.1:10027
|
||||
postscreen_bare_newline_enable = no
|
||||
postscreen_blacklist_action = drop
|
||||
postscreen_denylist_action = drop
|
||||
postscreen_cache_cleanup_interval = 24h
|
||||
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
|
||||
postscreen_dnsbl_action = enforce
|
||||
@@ -107,8 +107,6 @@ smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch,
|
||||
reject_unknown_sender_domain
|
||||
smtpd_soft_error_limit = 3
|
||||
smtpd_tls_auth_only = yes
|
||||
smtpd_tls_dh1024_param_file = /etc/ssl/mail/dhparams.pem
|
||||
smtpd_tls_eecdh_grade = auto
|
||||
smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA
|
||||
smtpd_tls_loglevel = 1
|
||||
|
||||
@@ -152,7 +150,7 @@ smtp_sasl_auth_enable = yes
|
||||
smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
|
||||
smtp_sasl_security_options =
|
||||
smtp_sasl_mechanism_filter = plain, login
|
||||
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf socketmap:inet:postfix-tlspol:8642:QUERY
|
||||
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf socketmap:inet:postfix-tlspol:8642:QUERYwithTLSRPT
|
||||
smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
|
||||
mail_name = Postcow
|
||||
# local_transport map catches local destinations and prevents routing local dests when the next map would route "*"
|
||||
|
||||
@@ -29,7 +29,6 @@ smtps inet n - n - - smtpd
|
||||
# TLS protocol can be modified by setting submission_smtpd_tls_mandatory_protocols in extra.cf
|
||||
submission inet n - n - - smtpd
|
||||
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
|
||||
-o smtpd_enforce_tls=yes
|
||||
-o smtpd_tls_security_level=encrypt
|
||||
-o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols
|
||||
-o tls_preempt_cipherlist=yes
|
||||
@@ -38,7 +37,6 @@ submission inet n - n - - smtpd
|
||||
10587 inet n - n - - smtpd
|
||||
-o smtpd_upstream_proxy_protocol=haproxy
|
||||
-o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
|
||||
-o smtpd_enforce_tls=yes
|
||||
-o smtpd_tls_security_level=encrypt
|
||||
-o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols
|
||||
-o tls_preempt_cipherlist=yes
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Whitelist generated by Postwhite v3.4 on Sun Mar 1 00:29:01 UTC 2026
|
||||
# Whitelist generated by Postwhite v3.4 on Sat Aug 1 00:35:20 UTC 2026
|
||||
# https://github.com/stevejenkins/postwhite/
|
||||
# 2174 total rules
|
||||
# 2256 total rules
|
||||
2a00:1450:4000::/36 permit
|
||||
2a00:1450:4864::/56 permit
|
||||
2a01:111:f400::/48 permit
|
||||
2a01:111:f403:2800::/53 permit
|
||||
2a01:111:f403:8000::/51 permit
|
||||
@@ -28,11 +29,15 @@
|
||||
2a01:b747:3004:200::/56 permit
|
||||
2a01:b747:3005:200::/56 permit
|
||||
2a01:b747:3006:200::/56 permit
|
||||
2a01:b747:3007::/56 permit
|
||||
2a02:a60:0:5::/64 permit
|
||||
2a0f:f640::/56 permit
|
||||
2c0f:fb50:4000::/36 permit
|
||||
2c0f:fb50:4864::/56 permit
|
||||
2.207.151.32/27 permit
|
||||
2.207.151.53 permit
|
||||
2.207.217.30 permit
|
||||
2.207.223.160/27 permit
|
||||
3.64.237.68 permit
|
||||
3.65.3.180 permit
|
||||
3.70.123.177 permit
|
||||
@@ -53,13 +58,14 @@
|
||||
8.25.196.0/23 permit
|
||||
8.36.116.0/24 permit
|
||||
8.39.54.0/23 permit
|
||||
8.39.54.240/29 permit
|
||||
8.39.54.250/31 permit
|
||||
8.39.144.0/24 permit
|
||||
8.40.222.0/23 permit
|
||||
8.40.222.240/29 permit
|
||||
8.40.222.250/31 permit
|
||||
8.47.10.9 permit
|
||||
12.130.86.238 permit
|
||||
13.107.213.51 permit
|
||||
13.107.246.51 permit
|
||||
13.108.16.0/20 permit
|
||||
13.110.208.0/21 permit
|
||||
13.110.209.0/24 permit
|
||||
@@ -277,6 +283,7 @@
|
||||
50.56.130.221 permit
|
||||
50.56.130.222 permit
|
||||
50.112.246.219 permit
|
||||
51.83.17.38 permit
|
||||
52.1.14.157 permit
|
||||
52.5.230.59 permit
|
||||
52.6.74.205 permit
|
||||
@@ -368,6 +375,7 @@
|
||||
64.132.88.0/23 permit
|
||||
64.132.92.0/24 permit
|
||||
64.181.194.190 permit
|
||||
64.181.213.254 permit
|
||||
64.207.219.7 permit
|
||||
64.207.219.8 permit
|
||||
64.207.219.9 permit
|
||||
@@ -453,7 +461,11 @@
|
||||
66.218.75.252/31 permit
|
||||
66.218.75.254 permit
|
||||
66.220.144.128/25 permit
|
||||
66.220.144.178 permit
|
||||
66.220.144.179 permit
|
||||
66.220.155.0/24 permit
|
||||
66.220.155.178 permit
|
||||
66.220.155.179 permit
|
||||
66.220.157.0/25 permit
|
||||
66.231.80.0/20 permit
|
||||
66.240.227.0/24 permit
|
||||
@@ -518,6 +530,8 @@
|
||||
69.162.98.0/24 permit
|
||||
69.169.224.0/20 permit
|
||||
69.171.232.0/24 permit
|
||||
69.171.232.180 permit
|
||||
69.171.232.181 permit
|
||||
69.171.244.0/23 permit
|
||||
70.42.149.35 permit
|
||||
72.3.185.0/24 permit
|
||||
@@ -639,6 +653,7 @@
|
||||
77.238.189.148/30 permit
|
||||
79.135.106.0/24 permit
|
||||
79.135.107.0/24 permit
|
||||
80.225.160.128/25 permit
|
||||
81.169.146.243 permit
|
||||
81.169.146.245 permit
|
||||
81.169.146.246 permit
|
||||
@@ -657,6 +672,9 @@
|
||||
82.165.159.45 permit
|
||||
82.165.159.130 permit
|
||||
82.165.159.131 permit
|
||||
84.8.68.0/25 permit
|
||||
84.8.192.128/25 permit
|
||||
84.8.224.128/25 permit
|
||||
85.9.206.169 permit
|
||||
85.9.210.45 permit
|
||||
85.158.136.0/21 permit
|
||||
@@ -1369,6 +1387,7 @@
|
||||
117.120.16.0/21 permit
|
||||
119.42.242.52/31 permit
|
||||
119.42.242.156 permit
|
||||
121.244.91.31 permit
|
||||
121.244.91.48 permit
|
||||
121.244.91.52 permit
|
||||
122.15.156.182 permit
|
||||
@@ -1408,6 +1427,20 @@
|
||||
129.146.88.28 permit
|
||||
129.146.147.105 permit
|
||||
129.146.236.58 permit
|
||||
129.148.135.0/25 permit
|
||||
129.148.148.0/25 permit
|
||||
129.148.164.0/25 permit
|
||||
129.148.180.0/25 permit
|
||||
129.148.215.0/25 permit
|
||||
129.149.6.0/25 permit
|
||||
129.149.22.0/25 permit
|
||||
129.149.38.0/25 permit
|
||||
129.149.52.0/25 permit
|
||||
129.149.68.0/25 permit
|
||||
129.149.84.0/25 permit
|
||||
129.149.100.0/25 permit
|
||||
129.149.118.0/25 permit
|
||||
129.149.126.0/25 permit
|
||||
129.151.67.221 permit
|
||||
129.153.62.216 permit
|
||||
129.153.104.71 permit
|
||||
@@ -1420,16 +1453,19 @@
|
||||
129.159.22.159 permit
|
||||
129.159.87.137 permit
|
||||
129.213.195.191 permit
|
||||
130.35.116.0/25 permit
|
||||
130.61.9.72 permit
|
||||
130.162.39.83 permit
|
||||
130.248.172.0/24 permit
|
||||
130.248.173.0/24 permit
|
||||
131.186.12.0/25 permit
|
||||
131.253.30.0/24 permit
|
||||
131.253.121.0/26 permit
|
||||
132.145.13.209 permit
|
||||
132.226.26.225 permit
|
||||
132.226.49.32 permit
|
||||
132.226.56.24 permit
|
||||
134.98.248.128/25 permit
|
||||
134.128.64.0/19 permit
|
||||
134.128.96.0/19 permit
|
||||
134.170.27.8 permit
|
||||
@@ -1460,6 +1496,10 @@
|
||||
136.147.182.0/24 permit
|
||||
136.147.224.0/20 permit
|
||||
136.179.50.206 permit
|
||||
136.248.224.128/25 permit
|
||||
136.248.232.128/25 permit
|
||||
138.1.108.0/25 permit
|
||||
138.1.170.0/24 permit
|
||||
139.60.152.0/22 permit
|
||||
139.138.35.44 permit
|
||||
139.138.46.121 permit
|
||||
@@ -1468,6 +1508,7 @@
|
||||
139.138.57.55 permit
|
||||
139.138.58.119 permit
|
||||
139.167.79.86 permit
|
||||
139.177.108.0/25 permit
|
||||
139.180.17.0/24 permit
|
||||
140.238.148.191 permit
|
||||
141.148.55.217 permit
|
||||
@@ -1506,6 +1547,9 @@
|
||||
146.88.28.0/24 permit
|
||||
146.148.116.76 permit
|
||||
147.154.32.0/25 permit
|
||||
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
|
||||
@@ -1513,16 +1557,22 @@
|
||||
147.243.128.26 permit
|
||||
148.105.0.0/16 permit
|
||||
148.105.8.0/21 permit
|
||||
148.116.32.128/25 permit
|
||||
149.72.0.0/16 permit
|
||||
149.72.234.184 permit
|
||||
149.72.248.236 permit
|
||||
149.97.173.180 permit
|
||||
149.118.160.128/25 permit
|
||||
150.136.21.199 permit
|
||||
150.171.109.72 permit
|
||||
150.230.98.160 permit
|
||||
151.145.38.14 permit
|
||||
152.67.105.195 permit
|
||||
152.69.200.236 permit
|
||||
152.70.155.126 permit
|
||||
155.248.135.128/25 permit
|
||||
155.248.140.0/25 permit
|
||||
155.248.148.0/25 permit
|
||||
155.248.208.51 permit
|
||||
155.248.220.138 permit
|
||||
155.248.234.149 permit
|
||||
@@ -1531,10 +1581,14 @@
|
||||
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
|
||||
157.151.208.65 permit
|
||||
157.255.1.64/29 permit
|
||||
158.101.211.207 permit
|
||||
158.247.16.0/20 permit
|
||||
158.247.100.0/25 permit
|
||||
159.13.4.0/25 permit
|
||||
159.92.154.0/24 permit
|
||||
159.92.155.0/24 permit
|
||||
159.92.157.0/24 permit
|
||||
@@ -1568,6 +1622,7 @@
|
||||
161.71.64.0/20 permit
|
||||
162.88.4.0/23 permit
|
||||
162.88.8.0/24 permit
|
||||
162.88.24.0/23 permit
|
||||
162.88.24.0/24 permit
|
||||
162.88.25.0/24 permit
|
||||
162.88.36.0/24 permit
|
||||
@@ -1585,10 +1640,13 @@
|
||||
164.152.23.32 permit
|
||||
164.152.25.241 permit
|
||||
164.177.132.168/30 permit
|
||||
165.1.100.0/25 permit
|
||||
165.173.128.0/24 permit
|
||||
165.173.180.1 permit
|
||||
165.173.180.0/24 permit
|
||||
165.173.180.250/31 permit
|
||||
165.173.182.0/24 permit
|
||||
165.173.182.250/31 permit
|
||||
165.173.189.205 permit
|
||||
166.78.68.0/22 permit
|
||||
166.78.68.221 permit
|
||||
166.78.69.169 permit
|
||||
@@ -1607,9 +1665,12 @@
|
||||
167.89.75.126 permit
|
||||
167.89.75.136 permit
|
||||
167.89.75.164 permit
|
||||
167.89.101.2 permit
|
||||
167.89.101.192/28 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
|
||||
168.129.184.128/25 permit
|
||||
168.129.248.128/25 permit
|
||||
168.138.5.36 permit
|
||||
168.138.73.51 permit
|
||||
168.138.77.31 permit
|
||||
@@ -1626,6 +1687,7 @@
|
||||
169.148.144.0/25 permit
|
||||
169.148.144.10 permit
|
||||
169.148.146.0/23 permit
|
||||
169.148.174.10 permit
|
||||
169.148.175.3 permit
|
||||
169.148.179.3 permit
|
||||
169.148.188.0/24 permit
|
||||
@@ -1644,6 +1706,7 @@
|
||||
173.203.81.39 permit
|
||||
173.224.161.128/25 permit
|
||||
173.224.165.0/26 permit
|
||||
173.224.166.48/28 permit
|
||||
174.36.84.8/29 permit
|
||||
174.36.84.16/29 permit
|
||||
174.36.84.32/29 permit
|
||||
@@ -1745,8 +1808,24 @@
|
||||
192.18.139.154 permit
|
||||
192.18.145.36 permit
|
||||
192.18.152.58 permit
|
||||
192.22.32.128/25 permit
|
||||
192.22.96.128/25 permit
|
||||
192.22.160.128/25 permit
|
||||
192.22.224.128/25 permit
|
||||
192.28.128.0/18 permit
|
||||
192.29.24.0/25 permit
|
||||
192.29.44.0/25 permit
|
||||
192.29.72.0/25 permit
|
||||
192.29.88.0/25 permit
|
||||
192.29.103.128/25 permit
|
||||
192.29.134.0/25 permit
|
||||
192.29.151.128/25 permit
|
||||
192.29.172.0/25 permit
|
||||
192.29.178.0/25 permit
|
||||
192.29.200.0/25 permit
|
||||
192.29.216.0/25 permit
|
||||
192.29.232.0/25 permit
|
||||
192.29.248.0/25 permit
|
||||
192.30.252.0/22 permit
|
||||
192.161.144.0/20 permit
|
||||
192.162.87.0/24 permit
|
||||
@@ -1754,11 +1833,6 @@
|
||||
192.237.159.42 permit
|
||||
192.237.159.43 permit
|
||||
192.254.112.0/20 permit
|
||||
192.254.112.60 permit
|
||||
192.254.112.98/31 permit
|
||||
192.254.113.10 permit
|
||||
192.254.113.101 permit
|
||||
192.254.114.176 permit
|
||||
193.109.254.0/23 permit
|
||||
193.122.128.100 permit
|
||||
193.123.56.63 permit
|
||||
@@ -1927,6 +2001,7 @@
|
||||
207.211.30.128/25 permit
|
||||
207.211.31.0/25 permit
|
||||
207.211.41.113 permit
|
||||
207.211.132.0/25 permit
|
||||
207.218.90.0/24 permit
|
||||
207.218.90.122 permit
|
||||
207.250.68.0/24 permit
|
||||
@@ -1934,6 +2009,8 @@
|
||||
208.43.21.28/30 permit
|
||||
208.43.21.64/29 permit
|
||||
208.43.21.72/30 permit
|
||||
208.56.9.224 permit
|
||||
208.56.13.196 permit
|
||||
208.64.132.0/22 permit
|
||||
208.71.40.63 permit
|
||||
208.71.40.64/31 permit
|
||||
@@ -1960,6 +2037,7 @@
|
||||
208.71.42.214 permit
|
||||
208.72.249.240/29 permit
|
||||
208.75.120.0/22 permit
|
||||
208.76.62.0/23 permit
|
||||
208.76.62.0/24 permit
|
||||
208.76.63.0/24 permit
|
||||
208.82.237.96/29 permit
|
||||
@@ -2120,6 +2198,7 @@
|
||||
216.136.168.80/28 permit
|
||||
216.139.64.0/19 permit
|
||||
216.145.221.0/24 permit
|
||||
216.146.32.0/23 permit
|
||||
216.146.32.0/24 permit
|
||||
216.146.33.0/24 permit
|
||||
216.198.0.0/18 permit
|
||||
@@ -2140,6 +2219,7 @@
|
||||
223.165.120.0/23 permit
|
||||
2001:0868:0100:0600::/64 permit
|
||||
2001:4860:4000::/36 permit
|
||||
2001:4860:4864::/56 permit
|
||||
2001:748:100:40::2:0/112 permit
|
||||
2001:748:400:1300::3 permit
|
||||
2001:748:400:1300::4 permit
|
||||
@@ -2157,10 +2237,13 @@
|
||||
2001:748:400:3301::3 permit
|
||||
2001:748:400:3301::4 permit
|
||||
2404:6800:4000::/36 permit
|
||||
2404:6800:4864::/56 permit
|
||||
2603:1061:14:75::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:c006:104::/64 permit
|
||||
@@ -2172,8 +2255,7 @@
|
||||
2620:10d:c09c:400::8:1 permit
|
||||
2620:119:50c0:207::/64 permit
|
||||
2620:119:50c0:207::215 permit
|
||||
2620:1ec:46::51 permit
|
||||
2620:1ec:bdf::51 permit
|
||||
2800:3f0:4000::/36 permit
|
||||
2800:3f0:4864::/56 permit
|
||||
49.12.4.251 permit # checks.mailcow.email
|
||||
2a01:4f8:c17:7906::10 permit # checks.mailcow.email
|
||||
|
||||
@@ -3,8 +3,7 @@ rules {
|
||||
backend = "http";
|
||||
url = "http://nginx:9081/pipe.php";
|
||||
selector = "reject_no_global_bl";
|
||||
formatter = "default";
|
||||
meta_headers = true;
|
||||
formatter = "multipart";
|
||||
}
|
||||
RLINFO {
|
||||
backend = "http";
|
||||
@@ -16,8 +15,7 @@ rules {
|
||||
backend = "http";
|
||||
url = "http://nginx:9081/pushover.php";
|
||||
selector = "mailcow_rcpt";
|
||||
formatter = "json";
|
||||
meta_headers = true;
|
||||
formatter = "multipart";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,47 +32,42 @@ function parse_email($email) {
|
||||
$a = strrpos($email, '@');
|
||||
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
||||
}
|
||||
if (!function_exists('getallheaders')) {
|
||||
function getallheaders() {
|
||||
if (!is_array($_SERVER)) {
|
||||
return array();
|
||||
}
|
||||
$headers = array();
|
||||
foreach ($_SERVER as $name => $value) {
|
||||
if (substr($name, 0, 5) == 'HTTP_') {
|
||||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
// rspamd metadata_exporter (multipart formatter):
|
||||
// - $_POST['metadata'] JSON with the rspamd metadata
|
||||
// - $_FILES['message'] raw RFC822 message
|
||||
if (empty($_POST['metadata']) || !isset($_FILES['message']) || $_FILES['message']['error'] !== UPLOAD_ERR_OK) {
|
||||
error_log("QUARANTINE: missing multipart parts from rspamd" . PHP_EOL);
|
||||
http_response_code(400);
|
||||
exit;
|
||||
}
|
||||
|
||||
$raw_data_content = file_get_contents('php://input');
|
||||
$meta = json_decode($_POST['metadata'], true);
|
||||
if (!is_array($meta)) {
|
||||
error_log("QUARANTINE: cannot decode metadata JSON" . PHP_EOL);
|
||||
http_response_code(400);
|
||||
exit;
|
||||
}
|
||||
|
||||
$raw_data_content = file_get_contents($_FILES['message']['tmp_name']);
|
||||
$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
|
||||
$headers = getallheaders();
|
||||
$raw_size = (int)$_FILES['message']['size'];
|
||||
|
||||
$qid = $headers['X-Rspamd-Qid'];
|
||||
$fuzzy = $headers['X-Rspamd-Fuzzy'];
|
||||
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
|
||||
$score = $headers['X-Rspamd-Score'];
|
||||
$rcpts = $headers['X-Rspamd-Rcpt'];
|
||||
$user = $headers['X-Rspamd-User'];
|
||||
$ip = $headers['X-Rspamd-Ip'];
|
||||
$action = $headers['X-Rspamd-Action'];
|
||||
$sender = $headers['X-Rspamd-From'];
|
||||
$symbols = $headers['X-Rspamd-Symbols'];
|
||||
|
||||
$raw_size = (int)$_SERVER['CONTENT_LENGTH'];
|
||||
$qid = $meta['qid'] ?? 'unknown';
|
||||
$subject = $meta['subject'] ?? '';
|
||||
$score = $meta['score'] ?? 0;
|
||||
$rcpts = $meta['rcpt'] ?? array();
|
||||
$user = $meta['user'] ?? 'unknown';
|
||||
$ip = $meta['ip'] ?? 'unknown';
|
||||
$action = $meta['action'] ?? 'no action';
|
||||
$sender = $meta['from'] ?? '';
|
||||
$symbols = json_encode($meta['symbols'] ?? array());
|
||||
$fuzzy = json_encode(is_array($meta['fuzzy'] ?? null) ? $meta['fuzzy'] : array());
|
||||
|
||||
if (empty($sender)) {
|
||||
error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL);
|
||||
$sender = 'empty-env-from@localhost';
|
||||
}
|
||||
|
||||
if ($fuzzy == 'unknown') {
|
||||
$fuzzy = '[]';
|
||||
}
|
||||
|
||||
try {
|
||||
$max_size = (int)$redis->Get('Q_MAX_SIZE');
|
||||
if (($max_size * 1048576) < $raw_size) {
|
||||
@@ -94,7 +89,7 @@ catch (RedisException $e) {
|
||||
$rcpt_final_mailboxes = array();
|
||||
|
||||
// Loop through all rcpts
|
||||
foreach (json_decode($rcpts, true) as $rcpt) {
|
||||
foreach ($rcpts as $rcpt) {
|
||||
// Remove tag
|
||||
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
||||
|
||||
|
||||
@@ -32,50 +32,46 @@ function parse_email($email) {
|
||||
$a = strrpos($email, '@');
|
||||
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
|
||||
}
|
||||
if (!function_exists('getallheaders')) {
|
||||
function getallheaders() {
|
||||
if (!is_array($_SERVER)) {
|
||||
return array();
|
||||
}
|
||||
$headers = array();
|
||||
foreach ($_SERVER as $name => $value) {
|
||||
if (substr($name, 0, 5) == 'HTTP_') {
|
||||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
// rspamd metadata_exporter (multipart formatter): metadata JSON arrives as $_POST['metadata'].
|
||||
if (empty($_POST['metadata'])) {
|
||||
error_log("NOTIFY: missing metadata part from rspamd" . PHP_EOL);
|
||||
http_response_code(400);
|
||||
exit;
|
||||
}
|
||||
|
||||
$headers = getallheaders();
|
||||
$json_body = json_decode(file_get_contents('php://input'));
|
||||
$meta = json_decode($_POST['metadata'], true);
|
||||
if (!is_array($meta)) {
|
||||
error_log("NOTIFY: cannot decode metadata JSON" . PHP_EOL);
|
||||
http_response_code(400);
|
||||
exit;
|
||||
}
|
||||
|
||||
$qid = $headers['X-Rspamd-Qid'];
|
||||
$rcpts = $headers['X-Rspamd-Rcpt'];
|
||||
$sender = $headers['X-Rspamd-From'];
|
||||
$ip = $headers['X-Rspamd-Ip'];
|
||||
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
|
||||
$messageid= $json_body->message_id;
|
||||
$qid = $meta['qid'] ?? 'unknown';
|
||||
$rcpts = $meta['rcpt'] ?? array();
|
||||
$sender = $meta['from'] ?? '';
|
||||
$ip = $meta['ip'] ?? 'unknown';
|
||||
$subject = $meta['subject'] ?? '';
|
||||
$messageid= $meta['message_id'] ?? '';
|
||||
$priority = 0;
|
||||
|
||||
$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true);
|
||||
$symbols_array = $meta['symbols'] ?? array();
|
||||
if (is_array($symbols_array)) {
|
||||
foreach ($symbols_array as $symbol) {
|
||||
if ($symbol['name'] == 'HAS_X_PRIO_ONE') {
|
||||
if (($symbol['name'] ?? null) == 'HAS_X_PRIO_ONE') {
|
||||
$priority = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sender_address = $json_body->header_from[0];
|
||||
$sender_address = $meta['header_from'][0] ?? '';
|
||||
$sender_name = '-';
|
||||
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $sender_address, $matches)) {
|
||||
$sender_address = $matches['address'];
|
||||
$sender_name = trim($matches['name'], '"\' ');
|
||||
}
|
||||
|
||||
$to_address = $json_body->header_to[0];
|
||||
$to_address = $meta['header_to'][0] ?? '';
|
||||
$to_name = '-';
|
||||
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
|
||||
$to_address = $matches['address'];
|
||||
@@ -85,7 +81,7 @@ if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
|
||||
$rcpt_final_mailboxes = array();
|
||||
|
||||
// Loop through all rcpts
|
||||
foreach (json_decode($rcpts, true) as $rcpt) {
|
||||
foreach ($rcpts as $rcpt) {
|
||||
// Remove tag
|
||||
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ $template_data = [
|
||||
'ip_check' => customize('get', 'ip_check'),
|
||||
'custom_login' => customize('get', 'custom_login'),
|
||||
'password_complexity' => password_complexity('get'),
|
||||
'imapsync_settings' => imapsync_get_settings(),
|
||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||
'cors_settings' => $cors_settings,
|
||||
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
|
||||
|
||||
+617
-8
@@ -185,6 +185,9 @@ paths:
|
||||
example:
|
||||
username: info@domain.tld
|
||||
domain: domain.tld
|
||||
description: my time limited alias
|
||||
validity: 8760
|
||||
permanent: false
|
||||
properties:
|
||||
username:
|
||||
description: 'the mailbox an alias should be created for'
|
||||
@@ -192,6 +195,15 @@ paths:
|
||||
domain:
|
||||
description: "the domain"
|
||||
type: string
|
||||
description:
|
||||
description: "a description for the alias, defaults to an empty string"
|
||||
type: string
|
||||
validity:
|
||||
description: "how many hours the alias stays valid, 1 to 87600, defaults to 8760 (one year)"
|
||||
type: integer
|
||||
permanent:
|
||||
description: "keep the alias after it expired, defaults to false"
|
||||
type: boolean
|
||||
type: object
|
||||
summary: Create time limited alias
|
||||
/api/v1/add/app-passwd:
|
||||
@@ -1072,6 +1084,7 @@ paths:
|
||||
password2: "*"
|
||||
quota: "3072"
|
||||
force_pw_update: "1"
|
||||
force_tfa: "1"
|
||||
tls_enforce_in: "1"
|
||||
tls_enforce_out: "1"
|
||||
tags: ["tag1", "tag2"]
|
||||
@@ -1118,6 +1131,7 @@ paths:
|
||||
password2: atedismonsin
|
||||
quota: "3072"
|
||||
force_pw_update: "1"
|
||||
force_tfa: "1"
|
||||
tls_enforce_in: "1"
|
||||
tls_enforce_out: "1"
|
||||
tags: ["tag1", "tag2"]
|
||||
@@ -1151,6 +1165,9 @@ paths:
|
||||
force_pw_update:
|
||||
description: forces the user to update its password on first login
|
||||
type: boolean
|
||||
force_tfa:
|
||||
description: force 2FA enrollment at login
|
||||
type: boolean
|
||||
tls_enforce_in:
|
||||
description: force inbound email tls encryption
|
||||
type: boolean
|
||||
@@ -1520,7 +1537,9 @@ paths:
|
||||
timeout1: "600"
|
||||
timeout2: "600"
|
||||
exclude: "(?i)spam|(?i)junk"
|
||||
custom_params: "--dry"
|
||||
custom_params:
|
||||
- {o: dry, v: ""}
|
||||
- {o: folder, v: INBOX}
|
||||
delete2duplicates: "1"
|
||||
delete1: "1"
|
||||
delete2: "0"
|
||||
@@ -1569,8 +1588,19 @@ paths:
|
||||
description: exclude objects (regex)
|
||||
type: string
|
||||
custom_params:
|
||||
description: custom parameters
|
||||
type: string
|
||||
description: >-
|
||||
Allowlisted imapsync options as option/value pairs. `o` is the
|
||||
option name (must be one from `GET /get/syncjob_options`); `v`
|
||||
is the value (empty string for pure flags). Values may contain
|
||||
any character and are passed to imapsync as a single argument.
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
o:
|
||||
type: string
|
||||
v:
|
||||
type: string
|
||||
delete2duplicates:
|
||||
description: delete duplicates on destination (--delete2duplicates)
|
||||
type: boolean
|
||||
@@ -1593,6 +1623,151 @@ paths:
|
||||
description: enables or disables the sync job
|
||||
type: boolean
|
||||
type: object
|
||||
/api/v1/add/syncjob_source:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- log:
|
||||
- syncjob
|
||||
- add
|
||||
- source
|
||||
- name: Example import
|
||||
description: ""
|
||||
host1: imap.example.org
|
||||
port1: 993
|
||||
enc1: SSL
|
||||
auth_type: PLAIN
|
||||
active: 1
|
||||
scope: all
|
||||
msg:
|
||||
- imapsync_source_added
|
||||
- Example import
|
||||
type: success
|
||||
schema:
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Create a reusable IMAP sync source. A sync job binds to a source by id;
|
||||
the source carries host/port/encryption and either static credentials
|
||||
(PLAIN/LOGIN/CRAM-MD5 — supplied per-job) or an OAuth2 token endpoint
|
||||
(XOAUTH2 — token cached on the source row).
|
||||
operationId: Create sync job source
|
||||
summary: Create sync job source
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
name: Example import
|
||||
description: ""
|
||||
host1: imap.example.org
|
||||
port1: "993"
|
||||
enc1: SSL
|
||||
auth_type: PLAIN
|
||||
active: "1"
|
||||
scope: all
|
||||
properties:
|
||||
name:
|
||||
description: source name, unique per owner
|
||||
type: string
|
||||
description:
|
||||
description: free text shown in the UI
|
||||
type: string
|
||||
host1:
|
||||
description: remote IMAP host
|
||||
type: string
|
||||
port1:
|
||||
description: remote IMAP port (1-65535)
|
||||
type: integer
|
||||
enc1:
|
||||
description: TLS / SSL / PLAIN
|
||||
type: string
|
||||
enum:
|
||||
- TLS
|
||||
- SSL
|
||||
- PLAIN
|
||||
auth_type:
|
||||
description: authentication mechanism
|
||||
type: string
|
||||
enum:
|
||||
- PLAIN
|
||||
- LOGIN
|
||||
- CRAM-MD5
|
||||
- XOAUTH2
|
||||
oauth_flow:
|
||||
description: XOAUTH2 grant type (default client_credentials)
|
||||
type: string
|
||||
enum:
|
||||
- client_credentials
|
||||
- authorization_code
|
||||
oauth_token_endpoint:
|
||||
description: required when auth_type is XOAUTH2 (token URL)
|
||||
type: string
|
||||
oauth_authorize_endpoint:
|
||||
description: required when oauth_flow is authorization_code (authorize URL)
|
||||
type: string
|
||||
oauth_userinfo_endpoint:
|
||||
description: optional identity endpoint for authorization_code
|
||||
type: string
|
||||
oauth_client_id:
|
||||
description: required when auth_type is XOAUTH2
|
||||
type: string
|
||||
oauth_client_secret:
|
||||
description: required when auth_type is XOAUTH2
|
||||
type: string
|
||||
oauth_scope:
|
||||
description: required when auth_type is XOAUTH2
|
||||
type: string
|
||||
oauth_extra_params:
|
||||
description: optional JSON object merged into the token request body
|
||||
type: string
|
||||
active:
|
||||
description: enables or disables the source
|
||||
type: boolean
|
||||
scope:
|
||||
description: >-
|
||||
admin/domainadmin — source visibility. client_credentials
|
||||
XOAUTH2 sources are always forced to a private scope.
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- domain
|
||||
- user
|
||||
domains:
|
||||
description: target domains when scope is domain
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
users:
|
||||
description: target users when scope is user
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
type: object
|
||||
/api/v1/add/tls-policy-map:
|
||||
post:
|
||||
responses:
|
||||
@@ -2510,7 +2685,7 @@ paths:
|
||||
description: >-
|
||||
Using this endpoint you can perform actions on quarantine items. It is possible to release
|
||||
emails from quarantine into to the inbox, or learn them as ham to improve Rspamd filtering.
|
||||
You must provide the quarantine item IDs. You can get the IDs using the GET method.
|
||||
You must provide the quarantine item IDs. You can get the IDs using the GET method.
|
||||
operationId: Edit mails in Quarantine
|
||||
requestBody:
|
||||
content:
|
||||
@@ -2765,6 +2940,62 @@ paths:
|
||||
type: object
|
||||
type: object
|
||||
summary: Delete sync job
|
||||
/api/v1/delete/syncjob_source:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- log:
|
||||
- syncjob
|
||||
- delete
|
||||
- source
|
||||
- id:
|
||||
- "3"
|
||||
msg:
|
||||
- imapsync_source_deleted
|
||||
- Example import
|
||||
type: success
|
||||
schema:
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Delete one or more sync job sources by id. Sources still referenced by
|
||||
a sync job cannot be deleted (FK ON DELETE RESTRICT) — delete or
|
||||
repoint the dependent jobs first.
|
||||
operationId: Delete sync job source
|
||||
summary: Delete sync job source
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
- "3"
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
/api/v1/delete/tls-policy-map:
|
||||
post:
|
||||
responses:
|
||||
@@ -3414,6 +3645,7 @@ paths:
|
||||
- mailbox
|
||||
- active: "1"
|
||||
force_pw_update: "0"
|
||||
force_tfa: "0"
|
||||
name: Full name
|
||||
password: "*"
|
||||
password2: "*"
|
||||
@@ -3464,6 +3696,7 @@ paths:
|
||||
attr:
|
||||
active: "1"
|
||||
force_pw_update: "0"
|
||||
force_tfa: "0"
|
||||
name: Full name
|
||||
authsource: mailcow
|
||||
password: ""
|
||||
@@ -3487,6 +3720,9 @@ paths:
|
||||
force_pw_update:
|
||||
description: force user to change password on next login
|
||||
type: boolean
|
||||
force_tfa:
|
||||
description: force 2FA enrollment at login
|
||||
type: boolean
|
||||
name:
|
||||
description: Full name of the mailbox user
|
||||
type: string
|
||||
@@ -3862,8 +4098,25 @@ paths:
|
||||
etc.)
|
||||
type: boolean
|
||||
custom_params:
|
||||
description: Custom parameters passed to imapsync command
|
||||
type: string
|
||||
description: >-
|
||||
Allowlisted imapsync options as option/value pairs. `o` is
|
||||
the option name (must be one from `GET /get/syncjob_options`);
|
||||
`v` is the value (empty string for flags). Values are passed to imapsync as a single
|
||||
argument.
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
o:
|
||||
type: string
|
||||
v:
|
||||
type: string
|
||||
prio:
|
||||
description: >-
|
||||
Priority (admin only; ignored otherwise). The runner processes due
|
||||
jobs least-recently-run first, priority breaks ties (0 = normal,
|
||||
higher wins).
|
||||
type: number
|
||||
delete1:
|
||||
description: Delete from source when completed
|
||||
type: boolean
|
||||
@@ -3931,6 +4184,269 @@ paths:
|
||||
type: object
|
||||
type: object
|
||||
summary: Update sync job
|
||||
/api/v1/edit/syncjob_source:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- log:
|
||||
- syncjob
|
||||
- edit
|
||||
- source
|
||||
- id:
|
||||
- "3"
|
||||
name: Example import
|
||||
host1: imap.example.org
|
||||
port1: 993
|
||||
msg:
|
||||
- imapsync_source_modified
|
||||
- Example import
|
||||
type: success
|
||||
schema:
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Update one or more sync job sources. Fields not provided are left
|
||||
unchanged. For XOAUTH2 sources, an empty `oauth_client_secret`
|
||||
preserves the stored value.
|
||||
operationId: Update sync job source
|
||||
summary: Update sync job source
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
items:
|
||||
- "3"
|
||||
attr:
|
||||
name: Example import
|
||||
description: ""
|
||||
host1: imap.example.org
|
||||
port1: "993"
|
||||
enc1: SSL
|
||||
auth_type: PLAIN
|
||||
active: "1"
|
||||
properties:
|
||||
attr:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
host1:
|
||||
type: string
|
||||
port1:
|
||||
type: integer
|
||||
enc1:
|
||||
type: string
|
||||
enum:
|
||||
- TLS
|
||||
- SSL
|
||||
- PLAIN
|
||||
auth_type:
|
||||
type: string
|
||||
enum:
|
||||
- PLAIN
|
||||
- LOGIN
|
||||
- CRAM-MD5
|
||||
- XOAUTH2
|
||||
oauth_flow:
|
||||
description: XOAUTH2 grant type
|
||||
type: string
|
||||
enum:
|
||||
- client_credentials
|
||||
- authorization_code
|
||||
oauth_token_endpoint:
|
||||
type: string
|
||||
oauth_authorize_endpoint:
|
||||
description: required when oauth_flow is authorization_code
|
||||
type: string
|
||||
oauth_userinfo_endpoint:
|
||||
description: optional identity endpoint for authorization_code
|
||||
type: string
|
||||
oauth_client_id:
|
||||
type: string
|
||||
oauth_client_secret:
|
||||
description: empty keeps the previously stored secret
|
||||
type: string
|
||||
oauth_scope:
|
||||
type: string
|
||||
oauth_extra_params:
|
||||
type: string
|
||||
active:
|
||||
type: boolean
|
||||
scope:
|
||||
description: source visibility (admin/domainadmin)
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- domain
|
||||
- user
|
||||
domains:
|
||||
description: target domains when scope is domain
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
users:
|
||||
description: target users when scope is user
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
type: object
|
||||
items:
|
||||
description: list of source ids to update
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
type: object
|
||||
/api/v1/edit/syncjob_source/refresh_token:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- log:
|
||||
- syncjob
|
||||
- edit
|
||||
- refresh_token
|
||||
- id:
|
||||
- "3"
|
||||
msg:
|
||||
- imapsync_source_token_refreshed
|
||||
- Example import
|
||||
type: success
|
||||
schema:
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Force an OAuth2 access-token refresh for a XOAUTH2 source. Performs a
|
||||
Client-Credentials grant against the source's `oauth_token_endpoint`
|
||||
and caches the new access token + expiry on the source row. On failure,
|
||||
the error is recorded in `oauth_last_refresh_error`.
|
||||
operationId: Refresh sync job source token
|
||||
summary: Refresh sync job source token
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
items:
|
||||
- "3"
|
||||
properties:
|
||||
items:
|
||||
description: list of source ids to refresh
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
type: object
|
||||
/api/v1/edit/imapsync_settings:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- log:
|
||||
- imapsync_settings
|
||||
- edit
|
||||
msg:
|
||||
- max_parallel_saved
|
||||
type: success
|
||||
schema:
|
||||
properties:
|
||||
log:
|
||||
description: contains request object
|
||||
items: {}
|
||||
type: array
|
||||
msg:
|
||||
items: {}
|
||||
type: array
|
||||
type:
|
||||
enum:
|
||||
- success
|
||||
- danger
|
||||
- error
|
||||
type: string
|
||||
type: object
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Update global sync job settings (admin only). `max_parallel` is the
|
||||
number of imapsync processes that may run concurrently; `max_kb_per_second`
|
||||
is a per-process bandwidth cap in KB/s (0 = unlimited), stored internally
|
||||
in bytes/s.
|
||||
operationId: Update sync job settings
|
||||
summary: Update sync job settings
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
example:
|
||||
attr:
|
||||
max_parallel: "4"
|
||||
max_kb_per_second: "2000"
|
||||
properties:
|
||||
attr:
|
||||
properties:
|
||||
max_parallel:
|
||||
description: Max. concurrent sync processes (>= 1)
|
||||
type: number
|
||||
max_kb_per_second:
|
||||
description: Bandwidth limit per process in KB/s (0 = unlimited)
|
||||
type: number
|
||||
type: object
|
||||
type: object
|
||||
/api/v1/edit/user-acl:
|
||||
post:
|
||||
responses:
|
||||
@@ -4881,6 +5397,7 @@ paths:
|
||||
- active: "1"
|
||||
attributes:
|
||||
force_pw_update: "0"
|
||||
force_tfa: "0"
|
||||
mailbox_format: "maildir:"
|
||||
quarantine_notification: never
|
||||
sogo_access: "1"
|
||||
@@ -4897,6 +5414,7 @@ paths:
|
||||
quota: 3221225472
|
||||
quota_used: 0
|
||||
rl: false
|
||||
sender_acl: ["otherbox@doman3.tld", "@aliasdomain.tld"]
|
||||
spam_aliases: 0
|
||||
username: info@doman3.tld
|
||||
tags: ["tag1", "tag2"]
|
||||
@@ -5629,6 +6147,95 @@ paths:
|
||||
description: You can list all syn jobs existing in system.
|
||||
operationId: Get sync jobs
|
||||
summary: Get sync jobs
|
||||
"/api/v1/get/syncjob_source/{id}":
|
||||
get:
|
||||
parameters:
|
||||
- description: source id, or `all` to list every source visible to the session
|
||||
example: all
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- description: e.g. api-key-string
|
||||
example: api-key-string
|
||||
in: header
|
||||
name: X-API-Key
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- id: 3
|
||||
name: Example import
|
||||
description: ""
|
||||
created_by: ""
|
||||
scope: all
|
||||
host1: imap.example.org
|
||||
port1: 993
|
||||
enc1: SSL
|
||||
auth_type: PLAIN
|
||||
oauth_token_endpoint: null
|
||||
oauth_client_id: null
|
||||
oauth_client_secret: ""
|
||||
oauth_scope: null
|
||||
oauth_extra_params: null
|
||||
oauth_access_token: ""
|
||||
oauth_token_expires: null
|
||||
oauth_last_refresh_error: null
|
||||
active: 1
|
||||
created: "2026-06-11 09:00:00"
|
||||
modified: "2026-06-11 09:00:00"
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Returns a single sync job source by id, or all sources visible to the
|
||||
caller when `id` is `all`. Secrets (`oauth_client_secret`,
|
||||
`oauth_access_token`) are blanked unless requested with internal
|
||||
flags — they are never returned over the JSON API.
|
||||
operationId: Get sync job sources
|
||||
summary: Get sync job sources
|
||||
/api/v1/get/syncjob_options:
|
||||
get:
|
||||
parameters:
|
||||
- description: e.g. api-key-string
|
||||
example: api-key-string
|
||||
in: header
|
||||
name: X-API-Key
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- dry
|
||||
- folder
|
||||
- delete2folders
|
||||
- maxsize
|
||||
description: OK
|
||||
headers: {}
|
||||
tags:
|
||||
- Sync jobs
|
||||
description: >-
|
||||
Returns the allowlist of imapsync option names accepted in a sync job's
|
||||
`custom_params`. Used by the UI to populate the option autocompletion.
|
||||
operationId: Get sync job options
|
||||
summary: Get sync job options
|
||||
"/api/v1/get/tls-policy-map/{id}":
|
||||
get:
|
||||
parameters:
|
||||
@@ -5805,6 +6412,7 @@ paths:
|
||||
- active: "1"
|
||||
attributes:
|
||||
force_pw_update: "0"
|
||||
force_tfa: "0"
|
||||
mailbox_format: "maildir:"
|
||||
quarantine_notification: never
|
||||
sogo_access: "1"
|
||||
@@ -5822,6 +6430,7 @@ paths:
|
||||
quota: 3221225472
|
||||
quota_used: 0
|
||||
rl: false
|
||||
sender_acl: ["otherbox@domain3.tld", "@aliasdomain.tld"]
|
||||
spam_aliases: 0
|
||||
username: info@domain3.tld
|
||||
tags: ["tag1", "tag2"]
|
||||
@@ -5844,7 +6453,7 @@ paths:
|
||||
response:
|
||||
value:
|
||||
- type: "success"
|
||||
log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
|
||||
log: ["cors", "edit", {"allowed_origins": ["*", "https://mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
|
||||
msg: "cors_headers_edited"
|
||||
description: OK
|
||||
headers: { }
|
||||
@@ -5861,7 +6470,7 @@ paths:
|
||||
schema:
|
||||
example:
|
||||
attr:
|
||||
allowed_origins: ["*", "mail.mailcow.tld"]
|
||||
allowed_origins: ["*", "https://mail.mailcow.tld"]
|
||||
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
|
||||
properties:
|
||||
attr:
|
||||
|
||||
+7
-1
@@ -204,9 +204,15 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
if (isset($_GET['syncjob']) &&
|
||||
is_numeric($_GET['syncjob'])) {
|
||||
$id = $_GET["syncjob"];
|
||||
$result = mailbox('get', 'syncjob_details', $id);
|
||||
$result = syncjob('get', 'job', $id);
|
||||
$template = 'edit/syncjob.twig';
|
||||
}
|
||||
elseif (isset($_GET['syncjob_source']) &&
|
||||
is_numeric($_GET['syncjob_source'])) {
|
||||
$id = $_GET["syncjob_source"];
|
||||
$result = syncjob('get', 'source', array('id' => $id, 'with_secret' => true));
|
||||
$template = 'edit/imapsync_source.twig';
|
||||
}
|
||||
elseif (isset($_GET['filter']) &&
|
||||
is_numeric($_GET['filter'])) {
|
||||
$id = $_GET["filter"];
|
||||
|
||||
@@ -380,13 +380,13 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
else {
|
||||
$state = state_nomatch;
|
||||
}
|
||||
$state .= '<br />' . $current[$data_field[$current['type']]];
|
||||
$state .= '<br />' . htmlspecialchars($current[$data_field[$current['type']]]);
|
||||
}
|
||||
if ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=dmarc') === 0 &&
|
||||
$record[2] == $dmarc_link) {
|
||||
$current['txt'] = str_replace(' ', '', $current['txt']);
|
||||
$state = $current[$data_field[$current['type']]] . state_optional;
|
||||
$state = htmlspecialchars($current[$data_field[$current['type']]]) . state_optional;
|
||||
}
|
||||
elseif ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=spf') === 0 &&
|
||||
@@ -396,7 +396,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
if (in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)) {
|
||||
$state = state_good;
|
||||
}
|
||||
$state .= '<br />' . $current[$data_field[$current['type']]] . state_optional;
|
||||
$state .= '<br />' . htmlspecialchars($current[$data_field[$current['type']]]) . state_optional;
|
||||
}
|
||||
elseif ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=dkim') === 0 &&
|
||||
@@ -426,7 +426,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
if ($state == state_nomatch) {
|
||||
$state = array();
|
||||
foreach ($currents as $current) {
|
||||
$state[] = $current[$data_field[$current['type']]];
|
||||
$state[] = htmlspecialchars($current[$data_field[$current['type']]]);
|
||||
}
|
||||
$state = implode('<br />', $state);
|
||||
}
|
||||
@@ -436,12 +436,22 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
<td>%s</td>
|
||||
<td class="dns-found">%s</td>
|
||||
<td class="dns-recommended">%s</td>
|
||||
</tr>', $record[0], $record[1], $record[2], $state);
|
||||
</tr>', htmlspecialchars($record[0]), htmlspecialchars($record[1]), $record[2], $state);
|
||||
$record[3] = explode('<br />', $state);
|
||||
}
|
||||
|
||||
unset($record);
|
||||
|
||||
// Make a hostname RHS absolute so it is not read relative to $ORIGIN.
|
||||
// Already-absolute names, the SRV root target "." and IP addresses are left alone.
|
||||
$absolutize = function($host) {
|
||||
$host = trim($host);
|
||||
if ($host === '' || $host === '.' || substr($host, -1) === '.' || filter_var($host, FILTER_VALIDATE_IP)) {
|
||||
return $host;
|
||||
}
|
||||
return $host . '.';
|
||||
};
|
||||
|
||||
$dns_data = sprintf("\$ORIGIN %s.\n", $domain);
|
||||
foreach ($records as $record) {
|
||||
if ($domain == substr($record[0], -strlen($domain))) {
|
||||
@@ -462,22 +472,32 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
$val = str_replace(state_optional, '', $val);
|
||||
$val = str_replace(state_good, '', $val);
|
||||
if (strlen($val) > 0) {
|
||||
// these are all TXT values, their RHS is a character string, not a name
|
||||
$vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ($record[1] == 'MX' || $record[1] == 'CNAME') {
|
||||
$val = $absolutize($val);
|
||||
}
|
||||
elseif ($record[1] == 'SRV') {
|
||||
// format here is "target port"; only the target is a name
|
||||
$parts = explode(' ', $val, 2);
|
||||
$parts[0] = $absolutize($parts[0]);
|
||||
$val = implode(' ', $parts);
|
||||
}
|
||||
$vals[] = sprintf("%s\tIN\t%s\t%s\n", $label, $record[1], $val);
|
||||
}
|
||||
|
||||
foreach ($vals as $val) {
|
||||
$dns_data .= str_replace($domain, $domain . '.', $val);
|
||||
$dns_data .= $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</table>
|
||||
<a id='download-zonefile' class="btn btn-sm btn-secondary visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline mb-4" style="margin-top:10px" data-zonefile="<?=base64_encode($dns_data);?>" download='<?=$_GET['domain'];?>.txt' type='text/csv'>Download</a>
|
||||
<a id='download-zonefile' class="btn btn-sm btn-secondary visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline mb-4" style="margin-top:10px" data-zonefile="<?=base64_encode($dns_data);?>" download='<?=htmlspecialchars($_GET['domain']);?>.txt' type='text/csv'>Download</a>
|
||||
<script>
|
||||
var zonefile_dl_link = document.getElementById('download-zonefile');
|
||||
var zonefile = atob(zonefile_dl_link.getAttribute('data-zonefile'));
|
||||
|
||||
@@ -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 = syncjob('get', 'job', intval($_GET['id']))) {
|
||||
echo (empty($details['log'])) ? '-' : $details['log'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Admins only
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] !== 'admin') {
|
||||
http_response_code(403);
|
||||
echo json_encode(array('status' => 'error', 'message' => 'access denied'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$owner = $GLOBALS['MAILCOW_GIT_OWNER'];
|
||||
$repo = $GLOBALS['MAILCOW_GIT_REPO'];
|
||||
$current = $GLOBALS['MAILCOW_GIT_VERSION'];
|
||||
|
||||
// Cache key is bound to the running version, so an update invalidates it naturally
|
||||
$cache_key = 'MAILCOW_UPDATE_CHECK/' . $current;
|
||||
|
||||
// Serve cached result if present (one GitHub call per TTL, not one per page load)
|
||||
$cached = $redis->get($cache_key);
|
||||
if ($cached !== false) {
|
||||
echo $cached;
|
||||
exit;
|
||||
}
|
||||
|
||||
// GitHub requires a User-Agent and rate-limits unauthenticated requests to 60/h per IP
|
||||
function github_get($url) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, array(
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_USERAGENT => 'mailcow-update-check',
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_HTTPHEADER => array('Accept: application/vnd.github+json'),
|
||||
));
|
||||
$body = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($body === false || $code !== 200) return false;
|
||||
$json = json_decode($body, true);
|
||||
return is_array($json) ? $json : false;
|
||||
}
|
||||
|
||||
$latest = github_get("https://api.github.com/repos/{$owner}/{$repo}/releases/latest");
|
||||
$release = github_get("https://api.github.com/repos/{$owner}/{$repo}/releases/tags/{$current}");
|
||||
|
||||
// Any failure (rate limit, offline, unexpected payload) -> honest error, cached briefly
|
||||
if ($latest === false || $release === false ||
|
||||
empty($latest['tag_name']) || empty($latest['created_at']) || empty($release['created_at'])) {
|
||||
$result = json_encode(array('status' => 'error', 'message' => 'could not reach GitHub API'));
|
||||
$redis->setex($cache_key, 300, $result);
|
||||
echo $result;
|
||||
exit;
|
||||
}
|
||||
|
||||
$date_latest = strtotime($latest['created_at']);
|
||||
$date_current = strtotime($release['created_at']);
|
||||
|
||||
if ($date_latest <= $date_current) {
|
||||
$result = json_encode(array('status' => 'no_update'));
|
||||
} else {
|
||||
$result = json_encode(array('status' => 'update_available', 'latest_tag' => $latest['tag_name']));
|
||||
}
|
||||
|
||||
// Cache the positive result for an hour
|
||||
$redis->setex($cache_key, 3600, $result);
|
||||
echo $result;
|
||||
@@ -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,7 @@ $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']),
|
||||
'docker_timeout' => $DOCKER_TIMEOUT,
|
||||
'session_lifetime' => (int)$SESSION_LIFETIME,
|
||||
'csrf_token' => $_SESSION['CSRF']['TOKEN'],
|
||||
|
||||
@@ -287,6 +287,8 @@ function user_login($user, $pass, $extra = null){
|
||||
return false;
|
||||
}
|
||||
|
||||
$row['attributes'] = json_decode($row['attributes'], true);
|
||||
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
|
||||
@@ -343,6 +345,8 @@ function user_login($user, $pass, $extra = null){
|
||||
return false;
|
||||
}
|
||||
|
||||
$row['attributes'] = json_decode($row['attributes'], true);
|
||||
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
|
||||
|
||||
@@ -429,14 +429,25 @@ function domain_admin_sso($_action, $_data) {
|
||||
|
||||
switch ($_action) {
|
||||
case 'check':
|
||||
$token = $_data;
|
||||
$token = preg_replace('/[^a-zA-Z0-9-]/', '', $_data);
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `t1`.`username` FROM `da_sso` AS `t1` JOIN `admin` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL '30' SECOND) AND `t2`.`active` = 1 AND `t2`.`superadmin` = 0;");
|
||||
$stmt->execute(array(
|
||||
':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token)
|
||||
':token' => $token
|
||||
));
|
||||
$return = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
return empty($return['username']) ? false : $return['username'];
|
||||
if (empty($return['username'])) {
|
||||
return false;
|
||||
}
|
||||
// single-use: consume the token; if a concurrent request already used it, deny
|
||||
$del = $pdo->prepare("DELETE FROM `da_sso` WHERE `token` = :token");
|
||||
$del->execute(array(
|
||||
':token' => $token
|
||||
));
|
||||
if ($del->rowCount() < 1) {
|
||||
return false;
|
||||
}
|
||||
return $return['username'];
|
||||
case 'issue':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
|
||||
@@ -108,6 +108,14 @@ function fwdhost($_action, $_data = null) {
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$hosts = (array)$_data['forwardinghost'];
|
||||
foreach ($hosts as $host) {
|
||||
try {
|
||||
|
||||
@@ -53,6 +53,39 @@ function valid_network($network) {
|
||||
function valid_hostname($hostname) {
|
||||
return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
|
||||
}
|
||||
// Validates a browser-style Origin: scheme://host[:port], no path/query/fragment/credentials
|
||||
function valid_origin($origin) {
|
||||
$parts = parse_url($origin);
|
||||
if ($parts === false || !isset($parts['scheme']) || !isset($parts['host'])) {
|
||||
return false;
|
||||
}
|
||||
if (!in_array(strtolower($parts['scheme']), array('http', 'https'), true)) {
|
||||
return false;
|
||||
}
|
||||
if (isset($parts['user']) || isset($parts['pass']) || isset($parts['path']) || isset($parts['query']) || isset($parts['fragment'])) {
|
||||
return false;
|
||||
}
|
||||
$host = $parts['host'];
|
||||
$host_without_brackets = trim($host, '[]');
|
||||
if (!valid_hostname($host_without_brackets) && !filter_var($host_without_brackets, FILTER_VALIDATE_IP)) {
|
||||
return false;
|
||||
}
|
||||
// Reject anything that doesn't round-trip to the exact same origin (e.g. stray trailing slash)
|
||||
$port = isset($parts['port']) ? ':' . $parts['port'] : '';
|
||||
$rebuilt = strtolower($parts['scheme']) . '://' . strtolower($host) . $port;
|
||||
return strtolower($origin) === $rebuilt;
|
||||
}
|
||||
// Bring an origin into the exact form a browser sends it (scheme://host[:port], lowercase).
|
||||
function normalize_cors_origin($origin) {
|
||||
$origin = trim($origin);
|
||||
if ($origin === '*') {
|
||||
return '*';
|
||||
}
|
||||
if ($origin !== '' && !preg_match('~^[a-z][a-z0-9+.-]*://~i', $origin)) {
|
||||
$origin = 'https://' . $origin;
|
||||
}
|
||||
return valid_origin($origin) ? strtolower($origin) : false;
|
||||
}
|
||||
// Thanks to https://stackoverflow.com/a/49373789
|
||||
// Validates exact ip matches and ip-in-cidr, ipv4 and ipv6
|
||||
function ip_acl($ip, $networks) {
|
||||
@@ -558,7 +591,7 @@ function logger($_data = false) {
|
||||
$type = $return['type'];
|
||||
$msg = null;
|
||||
if (isset($return['msg'])) {
|
||||
$msg = json_encode($return['msg'], JSON_UNESCAPED_UNICODE);
|
||||
$msg = json_encode($return['msg'], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
|
||||
}
|
||||
$call = null;
|
||||
if (isset($return['log'])) {
|
||||
@@ -600,6 +633,18 @@ function logger($_data = false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
function is_local_mailcow_domain($domain) {
|
||||
// True if domain is a locally managed, active primary or alias domain
|
||||
global $pdo;
|
||||
$domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46);
|
||||
if (empty($domain)) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT 1 FROM `domain` WHERE `domain` = :d AND `active` = 1
|
||||
UNION SELECT 1 FROM `alias_domain` WHERE `alias_domain` = :d2 AND `active` = 1 LIMIT 1");
|
||||
$stmt->execute(array(':d' => $domain, ':d2' => $domain));
|
||||
return (bool)$stmt->fetchColumn();
|
||||
}
|
||||
function hasDomainAccess($username, $role, $domain) {
|
||||
global $pdo;
|
||||
if (empty($domain) || !is_valid_domain_name($domain)) {
|
||||
@@ -2283,10 +2328,13 @@ function cors($action, $data = null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']);
|
||||
$allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array(getBaseURL());
|
||||
$allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins;
|
||||
foreach ($allowed_origins as $origin) {
|
||||
if (!filter_var($origin, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) && $origin != '*') {
|
||||
foreach ($allowed_origins as &$origin) {
|
||||
if ($origin === '*') {
|
||||
continue;
|
||||
}
|
||||
if (!valid_origin($origin)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $action, $data),
|
||||
@@ -2294,7 +2342,10 @@ function cors($action, $data = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// browsers always send a lowercase scheme/host in the Origin header, so normalize to match
|
||||
$origin = strtolower($origin);
|
||||
}
|
||||
unset($origin);
|
||||
|
||||
$allowed_methods = isset($data['allowed_methods']) ? $data['allowed_methods'] : array('GET', 'POST', 'PUT', 'DELETE');
|
||||
$allowed_methods = !is_array($allowed_methods) ? array_map('trim', preg_split( "/( |,|;|\n)/", $allowed_methods)) : $allowed_methods;
|
||||
@@ -2342,26 +2393,35 @@ function cors($action, $data = null) {
|
||||
);
|
||||
}
|
||||
|
||||
$cors_settings = !$cors_settings ? array('allowed_origins' => $_SERVER['SERVER_NAME'], 'allowed_methods' => 'GET, POST, PUT, DELETE') : $cors_settings;
|
||||
$cors_settings['allowed_origins'] = empty($cors_settings['allowed_origins']) ? $_SERVER['SERVER_NAME'] : $cors_settings['allowed_origins'];
|
||||
$cors_settings = !$cors_settings ? array('allowed_origins' => getBaseURL(), 'allowed_methods' => 'GET, POST, PUT, DELETE') : $cors_settings;
|
||||
$cors_settings['allowed_origins'] = empty($cors_settings['allowed_origins']) ? getBaseURL() : $cors_settings['allowed_origins'];
|
||||
$cors_settings['allowed_methods'] = empty($cors_settings['allowed_methods']) ? 'GET, POST, PUT, DELETE, OPTION' : $cors_settings['allowed_methods'];
|
||||
|
||||
return $cors_settings;
|
||||
break;
|
||||
case "set_headers":
|
||||
$cors_settings = cors('get');
|
||||
// normalize the stored list; it may still hold bare hostnames written before origins were validated as origins
|
||||
$allowed_origins = array_filter(array_map('normalize_cors_origin', explode(',', $cors_settings['allowed_origins'])));
|
||||
$origin = isset($_SERVER['HTTP_ORIGIN']) ? normalize_cors_origin($_SERVER['HTTP_ORIGIN']) : false;
|
||||
// check if requested origin is in allowed origins
|
||||
$allowed_origins = explode(', ', $cors_settings['allowed_origins']);
|
||||
$cors_settings['allowed_origins'] = $allowed_origins[0];
|
||||
if (in_array('*', $allowed_origins)){
|
||||
$cors_settings['allowed_origins'] = '*';
|
||||
} else if (array_key_exists('HTTP_ORIGIN', $_SERVER) && in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
|
||||
$cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN'];
|
||||
$allow_origin = null;
|
||||
if (in_array('*', $allowed_origins, true)) {
|
||||
$allow_origin = '*';
|
||||
} else if ($origin !== false && in_array($origin, $allowed_origins, true)) {
|
||||
$allow_origin = $origin;
|
||||
}
|
||||
// always allow OPTIONS for preflight request
|
||||
$cors_settings["allowed_methods"] = empty($cors_settings["allowed_methods"]) ? 'OPTIONS' : $cors_settings["allowed_methods"] . ', ' . 'OPTIONS';
|
||||
|
||||
header('Access-Control-Allow-Origin: ' . $cors_settings['allowed_origins']);
|
||||
// a disallowed origin gets no Access-Control-Allow-Origin at all; echoing a different origin than the requesting one tells a browser nothing
|
||||
if ($allow_origin !== null) {
|
||||
header('Access-Control-Allow-Origin: ' . $allow_origin);
|
||||
}
|
||||
if ($allow_origin !== '*') {
|
||||
// the response depends on the request origin, keep caches from mixing them up
|
||||
header('Vary: Origin', false);
|
||||
}
|
||||
header('Access-Control-Allow-Methods: '. $cors_settings['allowed_methods']);
|
||||
header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin');
|
||||
|
||||
@@ -3503,7 +3563,16 @@ function protect_route($allowed_roles = ['admin', 'domainadmin', 'user'], $redir
|
||||
if (isset($redirects['unauthenticated'])) {
|
||||
header('Location: ' . $redirects['unauthenticated']);
|
||||
} else {
|
||||
header('Location: /');
|
||||
// Send a deep link to the login page for its area instead of the user login at /,
|
||||
// e.g. /admin/dashboard -> /admin rather than /
|
||||
$request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
|
||||
if (strpos($request_uri, '/admin/') === 0) {
|
||||
header('Location: /admin');
|
||||
} elseif (strpos($request_uri, '/domainadmin/') === 0) {
|
||||
header('Location: /domainadmin');
|
||||
} else {
|
||||
header('Location: /');
|
||||
}
|
||||
}
|
||||
exit();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$_data_log = $_data;
|
||||
!isset($_data_log['password']) ?: $_data_log['password'] = '*';
|
||||
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
|
||||
|
||||
// Track mailboxes affected by alias operations for incremental SOGo updates
|
||||
$update_sogo_mailboxes = array();
|
||||
|
||||
switch ($_action) {
|
||||
case 'add':
|
||||
switch ($_type) {
|
||||
@@ -37,13 +41,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
else {
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
}
|
||||
if (isset($_data["validity"]) && !filter_var($_data["validity"], FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 87600)))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'validity_missing'
|
||||
);
|
||||
return false;
|
||||
if (isset($_data["validity"])) {
|
||||
if (!filter_var($_data["validity"], FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 87600)))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'validity_missing'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Default to 1 yr
|
||||
@@ -56,7 +62,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$permanent = 0;
|
||||
}
|
||||
$domain = $_data['domain'];
|
||||
$description = $_data['description'];
|
||||
// spamalias.description is NOT NULL, an absent description must not become a null insert
|
||||
$description = $_data['description'] ?? '';
|
||||
$valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain'];
|
||||
$valid_alias_domains = user_get_alias_details($username)['alias_domains'];
|
||||
if (!empty($valid_alias_domains)) {
|
||||
@@ -295,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(
|
||||
@@ -743,6 +564,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$goto_domain = idn_to_ascii(substr(strstr($goto, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
$goto_local_part = strstr($goto, '@', true);
|
||||
$goto = $goto_local_part.'@'.$goto_domain;
|
||||
// Deny external goto domains: global switch (all roles) overrides the per-DA ACL
|
||||
if (($GLOBALS['ALIAS_DISABLE_EXTERNAL_DOMAINS'] === true ||
|
||||
(isset($_SESSION['acl']['alias_external_goto']) && $_SESSION['acl']['alias_external_goto'] != "1")) &&
|
||||
!is_local_mailcow_domain($goto_domain)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('external_goto_denied', htmlspecialchars($goto))
|
||||
);
|
||||
unset($gotos[$i]);
|
||||
continue;
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
|
||||
WHERE `kind` REGEXP 'location|thing|group'
|
||||
AND `username`= :goto");
|
||||
@@ -886,6 +719,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('alias_added', $address, $id)
|
||||
);
|
||||
|
||||
// Track affected mailboxes for SOGo update
|
||||
if (!empty($goto)) {
|
||||
$gotos = array_map('trim', explode(',', $goto));
|
||||
foreach ($gotos as $g) {
|
||||
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
|
||||
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
|
||||
$update_sogo_mailboxes[] = $g;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'alias_domain':
|
||||
@@ -1096,10 +940,21 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : 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
|
||||
if (!in_array($quarantine_category, array('add_header', 'reject', 'all'))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'quarantine_category_invalid'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$quota_b = ($quota_m * 1048576);
|
||||
$attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
|
||||
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
|
||||
$force_pw_update = 0;
|
||||
}
|
||||
if ($authsource == 'generic-oidc'){
|
||||
$force_tfa = 0;
|
||||
}
|
||||
$mailbox_attrs = json_encode(
|
||||
@@ -1368,15 +1223,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
), $_extra);
|
||||
}
|
||||
|
||||
try {
|
||||
update_sogo_static_view($username);
|
||||
} catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => $e->getMessage()
|
||||
);
|
||||
}
|
||||
// Track affected mailboxes for SOGo update
|
||||
$update_sogo_mailboxes[] = $username;
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
@@ -1607,6 +1455,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('resource_added', htmlspecialchars($name))
|
||||
);
|
||||
|
||||
// Track affected mailboxes for SOGo update
|
||||
$update_sogo_mailboxes[] = $name;
|
||||
break;
|
||||
case 'domain_templates':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
@@ -1720,6 +1571,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['tagged_mail_handler']);
|
||||
$attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
|
||||
$attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
|
||||
// Validate quarantine_category
|
||||
if (!in_array($attr["quarantine_category"], array('add_header', 'reject', 'all'))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
|
||||
'msg' => 'quarantine_category_invalid'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
|
||||
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : "";
|
||||
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
|
||||
@@ -2049,6 +1909,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
return false;
|
||||
}
|
||||
foreach ($usernames as $username) {
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ($_data['spam_score'] == "default") {
|
||||
$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username
|
||||
AND (`option` = 'lowspamlevel' OR `option` = 'highspamlevel')");
|
||||
@@ -2226,202 +2094,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(
|
||||
@@ -2673,6 +2345,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
unset($gotos[$i]);
|
||||
continue;
|
||||
}
|
||||
// Deny external goto domains: global switch (all roles) overrides the per-DA ACL
|
||||
$goto_domain = idn_to_ascii(substr(strstr($goto, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
if (($GLOBALS['ALIAS_DISABLE_EXTERNAL_DOMAINS'] === true ||
|
||||
(isset($_SESSION['acl']['alias_external_goto']) && $_SESSION['acl']['alias_external_goto'] != "1")) &&
|
||||
!is_local_mailcow_domain($goto_domain)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('external_goto_denied', htmlspecialchars($goto))
|
||||
);
|
||||
unset($gotos[$i]);
|
||||
continue;
|
||||
}
|
||||
if ($goto == $address) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
@@ -2725,6 +2410,28 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('alias_modified', htmlspecialchars($address))
|
||||
);
|
||||
|
||||
// Track affected mailboxes for SOGo update (both old and new goto addresses)
|
||||
// Old goto: to remove alias from their view
|
||||
if (!empty($is_now['goto'])) {
|
||||
$old_gotos = array_map('trim', explode(',', $is_now['goto']));
|
||||
foreach ($old_gotos as $g) {
|
||||
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
|
||||
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
|
||||
$update_sogo_mailboxes[] = $g;
|
||||
}
|
||||
}
|
||||
}
|
||||
// New goto: to add alias to their view
|
||||
if (!empty($goto)) {
|
||||
$new_gotos = array_map('trim', explode(',', $goto));
|
||||
foreach ($new_gotos as $g) {
|
||||
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
|
||||
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
|
||||
$update_sogo_mailboxes[] = $g;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'domain':
|
||||
@@ -3093,6 +2800,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
}
|
||||
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
|
||||
$force_pw_update = 0;
|
||||
}
|
||||
if ($authsource == 'generic-oidc'){
|
||||
$force_tfa = 0;
|
||||
}
|
||||
$pw_recovery_email = (isset($_data['pw_recovery_email']) && $authsource == 'mailcow') ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
|
||||
@@ -3439,17 +3148,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'msg' => array('mailbox_modified', $username)
|
||||
);
|
||||
|
||||
try {
|
||||
update_sogo_static_view($username);
|
||||
} catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => $e->getMessage()
|
||||
);
|
||||
}
|
||||
// Track affected mailboxes for SOGo update
|
||||
$update_sogo_mailboxes[] = $username;
|
||||
}
|
||||
return true;
|
||||
break;
|
||||
case 'mailbox_rename':
|
||||
$domain = $_data['domain'];
|
||||
@@ -3760,9 +3461,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : $is_now['tagged_mail_handler'];
|
||||
$attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification'];
|
||||
$attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category'];
|
||||
// Validate quarantine_category
|
||||
if (!in_array($attr["quarantine_category"], array('add_header', 'reject', 'all'))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_extra),
|
||||
'msg' => 'quarantine_category_invalid'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : $is_now['rl_frame'];
|
||||
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : $is_now['rl_value'];
|
||||
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : $is_now['force_pw_update'];
|
||||
$attr["force_tfa"] = isset($_data['force_tfa']) ? intval($_data['force_tfa']) : $is_now['force_tfa'];
|
||||
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : $is_now['sogo_access'];
|
||||
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : $is_now['active'];
|
||||
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in'];
|
||||
@@ -4076,6 +3787,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('resource_modified', htmlspecialchars($name))
|
||||
);
|
||||
|
||||
// Track affected mailboxes for SOGo update
|
||||
$update_sogo_mailboxes[] = $name;
|
||||
}
|
||||
break;
|
||||
case 'domain_wide_footer':
|
||||
@@ -4507,68 +4221,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');
|
||||
@@ -5252,6 +4904,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$mailboxdata['rl_scope'] = 'domain';
|
||||
}
|
||||
$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->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'];
|
||||
}
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `tag_name`
|
||||
FROM `tags_mailbox` WHERE `username`= :username");
|
||||
@@ -5390,46 +5050,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();
|
||||
@@ -5579,8 +5199,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $username . "/%' OR `c_uid` = :username");
|
||||
// bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match)
|
||||
$c_object_like = '%/' . addcslashes($username, '\\%_') . '/%';
|
||||
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username");
|
||||
$stmt->execute(array(
|
||||
':c_object_like' => $c_object_like,
|
||||
':username' => $username
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
|
||||
@@ -5780,6 +5403,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track affected mailboxes for SOGo update (capture before deletion)
|
||||
if (!empty($alias_data['goto'])) {
|
||||
$gotos = array_map('trim', explode(',', $alias_data['goto']));
|
||||
foreach ($gotos as $g) {
|
||||
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
|
||||
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
|
||||
$update_sogo_mailboxes[] = $g;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
':id' => $alias_data['id']
|
||||
@@ -5964,8 +5599,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . str_replace('%', '\%', $username) . "/%' OR `c_uid` = :username");
|
||||
// bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match)
|
||||
$c_object_like = '%/' . addcslashes($username, '\\%_') . '/%';
|
||||
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username");
|
||||
$stmt->execute(array(
|
||||
':c_object_like' => $c_object_like,
|
||||
':username' => $username
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
|
||||
@@ -6038,22 +5676,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
update_sogo_static_view($username);
|
||||
}catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => $e->getMessage()
|
||||
);
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('mailbox_removed', htmlspecialchars($username))
|
||||
);
|
||||
|
||||
// Track affected mailboxes for SOGo update
|
||||
$update_sogo_mailboxes[] = $username;
|
||||
}
|
||||
return true;
|
||||
break;
|
||||
case 'mailbox_templates':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
@@ -6128,8 +5759,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$stmt->execute(array(
|
||||
':username' => $name
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $name . "/%' OR `c_uid` = :username");
|
||||
// bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match)
|
||||
$c_object_like = '%/' . addcslashes($name, '\\%_') . '/%';
|
||||
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username");
|
||||
$stmt->execute(array(
|
||||
':c_object_like' => $c_object_like,
|
||||
':username' => $name
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
|
||||
@@ -6153,6 +5787,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('resource_removed', htmlspecialchars($name))
|
||||
);
|
||||
|
||||
// Track affected mailboxes for SOGo update
|
||||
$update_sogo_mailboxes[] = $name;
|
||||
}
|
||||
break;
|
||||
case 'tags_domain':
|
||||
@@ -6259,9 +5896,21 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'resource')) && getenv('SKIP_SOGO') != "y") {
|
||||
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'resource', 'mailbox')) && getenv('SKIP_SOGO') != "y") {
|
||||
try {
|
||||
update_sogo_static_view();
|
||||
if (($_type == 'alias' || $_type == 'resource' || $_type == 'mailbox') && !empty($update_sogo_mailboxes)) {
|
||||
// INCREMENTAL UPDATE: Update only affected mailboxes/resources
|
||||
$update_sogo_mailboxes = array_unique($update_sogo_mailboxes);
|
||||
foreach ($update_sogo_mailboxes as $mailbox) {
|
||||
update_sogo_static_view($mailbox);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// FULL REBUILD: For domain and alias_domain operations or if no tracked mailboxes
|
||||
// Domain operations affect all mailboxes
|
||||
// Alias_domain operations affect entire target domain
|
||||
update_sogo_static_view();
|
||||
}
|
||||
}catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
|
||||
@@ -22,7 +22,7 @@ function quarantine($_action, $_data = null) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
|
||||
WHERE `qhash` = :hash
|
||||
WHERE `qhash` = :hash
|
||||
AND user_acl.quarantine = 1
|
||||
AND rcpt IN (SELECT username FROM mailbox)');
|
||||
$stmt->execute(array(':hash' => $hash));
|
||||
@@ -65,7 +65,7 @@ function quarantine($_action, $_data = null) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
|
||||
WHERE `qhash` = :hash
|
||||
WHERE `qhash` = :hash
|
||||
AND `user_acl`.`quarantine` = 1
|
||||
AND `username` IN (SELECT `username` FROM `mailbox`)');
|
||||
$stmt->execute(array(':hash' => $hash));
|
||||
@@ -170,6 +170,8 @@ function quarantine($_action, $_data = null) {
|
||||
}
|
||||
elseif ($release_format == 'raw') {
|
||||
$detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $detail_row['msg']);
|
||||
// dot-stuffing per RFC5321 4.5.2: escape leading dots
|
||||
$detail_row['msg'] = preg_replace('~^\.~m', '..', $detail_row['msg']);
|
||||
$postfix_talk = array(
|
||||
array('220', 'HELO quarantine' . chr(10)),
|
||||
array('250', 'MAIL FROM: ' . $sender . chr(10)),
|
||||
@@ -180,7 +182,7 @@ function quarantine($_action, $_data = null) {
|
||||
array('221', '')
|
||||
);
|
||||
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
|
||||
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
|
||||
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
|
||||
if (!$smtp_connection) {
|
||||
logger(array('return' => array(
|
||||
array(
|
||||
@@ -192,7 +194,7 @@ function quarantine($_action, $_data = null) {
|
||||
return false;
|
||||
}
|
||||
for ($i=0; $i < count($postfix_talk); $i++) {
|
||||
$smtp_resource = fgets($smtp_connection, 256);
|
||||
$smtp_resource = fgets($smtp_connection, 256);
|
||||
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
|
||||
$ret = substr($smtp_resource, 0, 3);
|
||||
$ret = (empty($ret)) ? '-' : $ret;
|
||||
@@ -465,17 +467,19 @@ function quarantine($_action, $_data = null) {
|
||||
}
|
||||
elseif ($release_format == 'raw') {
|
||||
$row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $row['msg']);
|
||||
// dot-stuffing per RFC5321 4.5.2: escape leading dots
|
||||
$row['msg'] = preg_replace('~^\.~m', '..', $row['msg']);
|
||||
$postfix_talk = array(
|
||||
array('220', 'HELO quarantine' . chr(10)),
|
||||
array('250', 'MAIL FROM: ' . $sender . chr(10)),
|
||||
array('250', 'RCPT TO: ' . $row['rcpt'] . chr(10)),
|
||||
array('250', 'DATA' . chr(10)),
|
||||
array('354', str_replace("\n.", '', $row['msg']) . chr(10) . '.' . chr(10)),
|
||||
array('354', $row['msg'] . chr(10) . '.' . chr(10)),
|
||||
array('250', 'QUIT' . chr(10)),
|
||||
array('221', '')
|
||||
);
|
||||
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
|
||||
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
|
||||
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
|
||||
if (!$smtp_connection) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'warning',
|
||||
@@ -485,7 +489,7 @@ function quarantine($_action, $_data = null) {
|
||||
return false;
|
||||
}
|
||||
for ($i=0; $i < count($postfix_talk); $i++) {
|
||||
$smtp_resource = fgets($smtp_connection, 256);
|
||||
$smtp_resource = fgets($smtp_connection, 256);
|
||||
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
|
||||
$ret = substr($smtp_resource, 0, 3);
|
||||
$ret = (empty($ret)) ? '-' : $ret;
|
||||
@@ -833,7 +837,7 @@ function quarantine($_action, $_data = null) {
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash');
|
||||
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash');
|
||||
$stmt->execute(array(':hash' => $hash));
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
break;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -89,7 +89,7 @@ $globalVariables = [
|
||||
'app_links' => $app_links,
|
||||
'app_links_processed' => $app_links_processed,
|
||||
'is_root_uri' => (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) == '/'),
|
||||
'uri' => $_SERVER['REQUEST_URI'],
|
||||
'uri' => parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?: '/',
|
||||
];
|
||||
|
||||
foreach ($globalVariables as $globalVariableName => $globalVariableValue) {
|
||||
|
||||
+199
-19
@@ -4,7 +4,7 @@ function init_db_schema()
|
||||
try {
|
||||
global $pdo;
|
||||
|
||||
$db_version = "19022026_1220";
|
||||
$db_version = "19082026_0900";
|
||||
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
@@ -719,7 +719,8 @@ function init_db_schema()
|
||||
"alias_domains" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"mailbox_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"domain_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'"
|
||||
"domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"alias_external_goto" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
@@ -754,35 +755,145 @@ 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'",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("source_id", "username")
|
||||
),
|
||||
"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",
|
||||
"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",
|
||||
@@ -794,6 +905,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"
|
||||
@@ -1183,6 +1320,33 @@ function init_db_schema()
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate imapsync to source-based schema
|
||||
if ($table == 'imapsync') {
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'imapsync'");
|
||||
if (count($stmt->fetchAll(PDO::FETCH_ASSOC)) != 0) {
|
||||
$stmt = $pdo->query("SHOW COLUMNS FROM `imapsync` LIKE 'host1'");
|
||||
$has_legacy_cols = (count($stmt->fetchAll(PDO::FETCH_ASSOC)) != 0);
|
||||
$stmt = $pdo->query("SHOW COLUMNS FROM `imapsync` LIKE 'source_id'");
|
||||
$has_source_id = (count($stmt->fetchAll(PDO::FETCH_ASSOC)) != 0);
|
||||
|
||||
if ($has_legacy_cols && !$has_source_id) {
|
||||
$pdo->exec("INSERT IGNORE INTO `imapsync_source` (`name`, `created_by`, `scope`, `host1`, `port1`, `enc1`, `auth_type`, `active`)
|
||||
SELECT CONCAT('legacy-', `host1`, '-', `port1`, '-', `enc1`, '-', `authmech1`) AS name,
|
||||
'' AS created_by, 'all' AS scope, `host1`, `port1`, `enc1`, `authmech1`, 1
|
||||
FROM `imapsync`
|
||||
GROUP BY `host1`, `port1`, `enc1`, `authmech1`");
|
||||
$pdo->exec("ALTER TABLE `imapsync` ADD COLUMN `source_id` INT DEFAULT NULL");
|
||||
$pdo->exec("UPDATE `imapsync` i
|
||||
JOIN `imapsync_source` s
|
||||
ON s.`host1` = i.`host1` AND s.`port1` = i.`port1`
|
||||
AND s.`enc1` = i.`enc1` AND s.`auth_type` = i.`authmech1`
|
||||
AND s.`created_by` = '' AND s.`scope` = 'all'
|
||||
SET i.`source_id` = s.`id`");
|
||||
$pdo->exec("ALTER TABLE `imapsync` MODIFY COLUMN `source_id` INT NOT NULL");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE '" . $table . "'");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
if ($num_results != 0) {
|
||||
@@ -1348,20 +1512,34 @@ 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%';");
|
||||
|
||||
// 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')");
|
||||
|
||||
// Syncjobs: migrate custom_params from raw imapsync string to structured JSON pairs.
|
||||
// Old values could not contain spaces (they were rejected), so a whitespace split is safe.
|
||||
$allow = $GLOBALS['IMAPSYNC_OPTIONS'];
|
||||
$cp_upd = $pdo->prepare("UPDATE `imapsync` SET `custom_params` = :cp WHERE `id` = :id");
|
||||
foreach ($pdo->query("SELECT `id`, `custom_params` FROM `imapsync`")->fetchAll(PDO::FETCH_ASSOC) as $r) {
|
||||
$cp = trim((string)$r['custom_params']);
|
||||
if ($cp !== '' && $cp[0] === '[') continue; // already migrated (JSON)
|
||||
$pairs = array();
|
||||
if ($cp !== '') {
|
||||
$tokens = preg_split('/\s+/', $cp, -1, PREG_SPLIT_NO_EMPTY);
|
||||
for ($i = 0; $i < count($tokens); $i++) {
|
||||
if (strpos($tokens[$i], '--') !== 0) continue;
|
||||
$opt = ltrim($tokens[$i], '-'); $val = '';
|
||||
if (strpos($opt, '=') !== false) { list($opt, $val) = explode('=', $opt, 2); }
|
||||
elseif ($i + 1 < count($tokens) && strpos($tokens[$i + 1], '--') !== 0) { $val = $tokens[++$i]; }
|
||||
if (!array_key_exists(strtolower($opt), $allow)) continue; // drop options no longer allowed
|
||||
$pairs[] = array('o' => strtolower($opt), 'v' => $val);
|
||||
}
|
||||
}
|
||||
$cp_upd->execute(array(':cp' => json_encode($pairs), ':id' => $r['id']));
|
||||
}
|
||||
|
||||
// Inject admin if not exists
|
||||
$stmt = $pdo->query("SELECT NULL FROM `admin`");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
@@ -1464,6 +1642,8 @@ function init_db_schema()
|
||||
"pop3_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['pop3_access']),
|
||||
"smtp_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['smtp_access']),
|
||||
"sieve_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sieve_access']),
|
||||
"eas_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['eas_access']),
|
||||
"dav_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['dav_access']),
|
||||
"acl_spam_alias" => 1,
|
||||
"acl_tls_policy" => 1,
|
||||
"acl_spam_score" => 1,
|
||||
|
||||
@@ -285,6 +285,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.syncjob.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailq.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.oauth2.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php';
|
||||
|
||||
@@ -13,7 +13,9 @@ $twig = new Environment($loader, [
|
||||
|
||||
// functions
|
||||
$twig->addFunction(new TwigFunction('query_string', function (array $params = []) {
|
||||
return http_build_query(array_merge($_GET, $params));
|
||||
$allowed = ['lang', 'mobileconfig'];
|
||||
$filtered = array_intersect_key($_GET, array_flip($allowed));
|
||||
return http_build_query(array_merge($filtered, $params));
|
||||
}));
|
||||
|
||||
$twig->addFunction(new TwigFunction('is_uri', function (string $uri, string $where = null) {
|
||||
|
||||
+89
-126
@@ -112,6 +112,7 @@ $AVAILABLE_LANGUAGES = array(
|
||||
'sv-se' => 'Svenska (Swedish)',
|
||||
'tr-tr' => 'Türkçe (Turkish)',
|
||||
'uk-ua' => 'Українська (Ukrainian)',
|
||||
'uz-uz' => 'Oʻzbekcha (Uzbek)',
|
||||
'vi-vn' => 'Tiếng Việt (Vietnamese)',
|
||||
'zh-cn' => '简体中文 (Simplified Chinese)',
|
||||
'zh-tw' => '繁體中文 (Traditional Chinese)',
|
||||
@@ -245,6 +246,10 @@ $PW_RESET_TOKEN_LIMIT = 3;
|
||||
// Maximum time in minutes a password reset token is valid
|
||||
$PW_RESET_TOKEN_LIFETIME = 15;
|
||||
|
||||
// Globally forbid aliases with external (non-local) goto domains for ALL roles (incl. admins),
|
||||
// overriding the per-domain-admin da_acl. false = defer to da_acl.
|
||||
$ALIAS_DISABLE_EXTERNAL_DOMAINS = false;
|
||||
|
||||
// UV flag handling in FIDO2/WebAuthn - defaults to false to allow iOS logins
|
||||
// true = required
|
||||
// false = preferred
|
||||
@@ -280,131 +285,89 @@ $RSPAMD_MAPS = array(
|
||||
);
|
||||
|
||||
|
||||
// Allowed imapsync options for a sync job's custom_params, mapped to whether they take a value:
|
||||
// true = option takes a value
|
||||
// false = pure flag, no value
|
||||
// Anything not listed here is rejected.
|
||||
// NEVER add (RCE): 'delete2foldersonly', 'delete2foldersbutnot', 'regexflag', 'regexmess', 'pipemess', 'regextrans2', 'maxlinelengthcmd'
|
||||
$IMAPSYNC_OPTIONS = array(
|
||||
'whitelist' => array(
|
||||
'abort',
|
||||
'authmd51',
|
||||
'authmd52',
|
||||
'authmech1',
|
||||
'authmech2',
|
||||
'authuser1',
|
||||
'authuser2',
|
||||
'debug',
|
||||
'debugcontent',
|
||||
'debugcrossduplicates',
|
||||
'debugflags',
|
||||
'debugfolders',
|
||||
'debugimap',
|
||||
'debugimap1',
|
||||
'debugimap2',
|
||||
'debugmemory',
|
||||
'debugssl',
|
||||
'delete1emptyfolders',
|
||||
'delete2folders',
|
||||
'disarmreadreceipts',
|
||||
'domain1',
|
||||
'domain2',
|
||||
'domino1',
|
||||
'domino2',
|
||||
'dry',
|
||||
'errorsmax',
|
||||
'exchange1',
|
||||
'exchange2',
|
||||
'exitwhenover',
|
||||
'expunge1',
|
||||
'f1f2',
|
||||
'filterbuggyflags',
|
||||
'folder',
|
||||
'folderfirst',
|
||||
'folderlast',
|
||||
'folderrec',
|
||||
'gmail1',
|
||||
'gmail2',
|
||||
'idatefromheader',
|
||||
'include',
|
||||
'inet4',
|
||||
'inet6',
|
||||
'justconnect',
|
||||
'justfolders',
|
||||
'justfoldersizes',
|
||||
'justlogin',
|
||||
'keepalive1',
|
||||
'keepalive2',
|
||||
'log',
|
||||
'logdir',
|
||||
'logfile',
|
||||
'maxbytesafter',
|
||||
'maxlinelength',
|
||||
'maxmessagespersecond',
|
||||
'maxsize',
|
||||
'maxsleep',
|
||||
'minage',
|
||||
'minsize',
|
||||
'noabletosearch',
|
||||
'noabletosearch1',
|
||||
'noabletosearch2',
|
||||
'noexpunge1',
|
||||
'noexpunge2',
|
||||
'nofoldersizesatend',
|
||||
'noid',
|
||||
'nolog',
|
||||
'nomixfolders',
|
||||
'noresyncflags',
|
||||
'nossl1',
|
||||
'nossl2',
|
||||
'nosyncacls',
|
||||
'notls1',
|
||||
'notls2',
|
||||
'nouidexpunge2',
|
||||
'nousecache',
|
||||
'oauthaccesstoken1',
|
||||
'oauthaccesstoken2',
|
||||
'oauthdirect1',
|
||||
'oauthdirect2',
|
||||
'office1',
|
||||
'office2',
|
||||
'pidfile',
|
||||
'pidfilelocking',
|
||||
'prefix1',
|
||||
'prefix2',
|
||||
'proxyauth1',
|
||||
'proxyauth2',
|
||||
'resyncflags',
|
||||
'resynclabels',
|
||||
'search',
|
||||
'search1',
|
||||
'search2',
|
||||
'sep1',
|
||||
'sep2',
|
||||
'showpasswords',
|
||||
'skipemptyfolders',
|
||||
'ssl2',
|
||||
'sslargs1',
|
||||
'sslargs2',
|
||||
'subfolder1',
|
||||
'subscribe',
|
||||
'subscribed',
|
||||
'syncacls',
|
||||
'syncduplicates',
|
||||
'syncinternaldates',
|
||||
'synclabels',
|
||||
'tests',
|
||||
'testslive',
|
||||
'testslive6',
|
||||
'tls2',
|
||||
'truncmess',
|
||||
'usecache',
|
||||
'useheader',
|
||||
'useuid'
|
||||
),
|
||||
'blacklist' => array(
|
||||
'skipmess',
|
||||
'delete2foldersonly',
|
||||
'delete2foldersbutnot',
|
||||
'regexflag',
|
||||
'regexmess',
|
||||
'pipemess',
|
||||
'regextrans2',
|
||||
'maxlinelengthcmd'
|
||||
)
|
||||
'abort' => false,
|
||||
'authmd51' => false,
|
||||
'authuser1' => true,
|
||||
'debug' => false,
|
||||
'debugcontent' => false,
|
||||
'debugcrossduplicates' => false,
|
||||
'debugflags' => false,
|
||||
'debugfolders' => false,
|
||||
'debugimap' => false,
|
||||
'debugimap1' => false,
|
||||
'debugmemory' => false,
|
||||
'debugssl' => true,
|
||||
'delete1emptyfolders' => false,
|
||||
'delete2folders' => false,
|
||||
'disarmreadreceipts' => false,
|
||||
'domain1' => true,
|
||||
'domino1' => false,
|
||||
'errorsmax' => true,
|
||||
'exchange1' => false,
|
||||
'exitwhenover' => true,
|
||||
'expunge1' => false,
|
||||
'f1f2' => true,
|
||||
'filterbuggyflags' => false,
|
||||
'folder' => true,
|
||||
'folderfirst' => true,
|
||||
'folderlast' => true,
|
||||
'folderrec' => true,
|
||||
'gmail1' => false,
|
||||
'idatefromheader' => false,
|
||||
'include' => true,
|
||||
'inet4' => false,
|
||||
'inet6' => false,
|
||||
'justconnect' => false,
|
||||
'justfolders' => false,
|
||||
'justfoldersizes' => false,
|
||||
'justlogin' => false,
|
||||
'keepalive1' => false,
|
||||
'log' => false,
|
||||
'maxbytesafter' => true,
|
||||
'maxlinelength' => true,
|
||||
'maxmessagespersecond' => true,
|
||||
'maxsize' => true,
|
||||
'maxsleep' => true,
|
||||
'minage' => true,
|
||||
'minsize' => true,
|
||||
'noabletosearch' => false,
|
||||
'noabletosearch1' => false,
|
||||
'noexpunge1' => false,
|
||||
'nofoldersizesatend' => false,
|
||||
'noid' => false,
|
||||
'nolog' => false,
|
||||
'nomixfolders' => false,
|
||||
'noresyncflags' => false,
|
||||
'nossl1' => false,
|
||||
'nosyncacls' => false,
|
||||
'notls1' => false,
|
||||
'nousecache' => false,
|
||||
'office1' => false,
|
||||
'prefix1' => true,
|
||||
'resyncflags' => false,
|
||||
'resynclabels' => false,
|
||||
'search' => true,
|
||||
'search1' => true,
|
||||
'sep1' => true,
|
||||
'skipemptyfolders' => false,
|
||||
'subfolder1' => true,
|
||||
'subscribe' => false,
|
||||
'subscribed' => false,
|
||||
'syncacls' => false,
|
||||
'syncduplicates' => false,
|
||||
'syncinternaldates' => false,
|
||||
'synclabels' => false,
|
||||
'tests' => false,
|
||||
'testslive' => false,
|
||||
'testslive6' => false,
|
||||
'truncmess' => true,
|
||||
'usecache' => false,
|
||||
'useheader' => true,
|
||||
'useuid' => false,
|
||||
);
|
||||
|
||||
@@ -345,7 +345,7 @@ $(document).ready(function() {
|
||||
$('.main-logo-dark').addClass('d-none');
|
||||
if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_dark.png');
|
||||
if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_dark.png');
|
||||
localStorage.setItem('theme', 'light');
|
||||
localStorage.setItem('mailcow_theme', 'light');
|
||||
}else{
|
||||
$('head').append('<link id="dark-mode-theme" rel="stylesheet" type="text/css" href="/css/themes/mailcow-darkmode.css">');
|
||||
$('#dark-mode-toggle').prop('checked', true);
|
||||
@@ -353,7 +353,7 @@ $(document).ready(function() {
|
||||
$('.main-logo-dark').removeClass('d-none');
|
||||
if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_light.png');
|
||||
if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_light.png');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
localStorage.setItem('mailcow_theme', 'dark');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,3 +414,279 @@ function copyToClipboard(id) {
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
mailcow_alert_box(lang.copy_to_clipboard, "success");
|
||||
}
|
||||
|
||||
// ===== IMAP sync sources: shared helpers + delegated listeners =====
|
||||
// Used by the mailbox/user/edit pages (each still owns its own draw_imapsync_source_table).
|
||||
// Kept here because 013-mailcow.js is loaded globally, so the logic lives in one place.
|
||||
|
||||
// Human-readable visibility label for a source row (used by the per-page source tables)
|
||||
function imapsyncScopeDisplay(item) {
|
||||
if (item.scope === 'all') return '<i class="bi bi-globe"></i> ' + lang.syncjobs.source_scope_all;
|
||||
if (item.scope === 'domain') return escapeHtml(lang.syncjobs.source_scope_domain + ': ' + (item.domains || []).join(', '));
|
||||
var us = item.users || [];
|
||||
if (us.length === 0) return escapeHtml(item.created_by || '');
|
||||
return escapeHtml(lang.syncjobs.source_scope_user + ': ' + us.join(', '));
|
||||
}
|
||||
|
||||
// Auth column for a source row: auth type, and for XOAUTH2 the flow + a token-status badge.
|
||||
function imapsyncAuthDisplay(item) {
|
||||
var out = escapeHtml(item.auth_type);
|
||||
if (item.auth_type !== 'XOAUTH2') return out;
|
||||
var flow = item.oauth_flow || 'client_credentials';
|
||||
var flowLabel = (flow === 'authorization_code')
|
||||
? lang.syncjobs.source_flow_badge_user
|
||||
: lang.syncjobs.source_flow_badge_app;
|
||||
out += ' <span class="text-muted">(' + escapeHtml(flowLabel) + ')</span>';
|
||||
if (flow === 'authorization_code') {
|
||||
// token lives per user+source (created via the syncjob "connect"), not on the source row
|
||||
return out;
|
||||
}
|
||||
var badge;
|
||||
if (item.oauth_token_expires && item.oauth_token_expires * 1000 > Date.now()) {
|
||||
badge = '<span class="badge bg-success">' + lang.syncjobs.source_token_status + ': ' + new Date(item.oauth_token_expires * 1000).toLocaleString() + '</span>';
|
||||
} else if (item.oauth_last_refresh_error) {
|
||||
badge = '<span class="badge bg-danger" title="' + escapeHtml(item.oauth_last_refresh_error) + '">' + lang.syncjobs.source_token_error + '</span>';
|
||||
} else {
|
||||
badge = '<span class="badge bg-warning">' + lang.waiting + '</span>';
|
||||
}
|
||||
// badge on its own line, below "XOAUTH2 (Client Credentials)"
|
||||
out += '<div class="mt-1">' + badge + '</div>';
|
||||
return out;
|
||||
}
|
||||
|
||||
// Per-syncjob OAuth token badge for authorization_code sources: the token is issued
|
||||
// per user (via "connect"), so a job can be pending / issued / failed. Returns '' for
|
||||
// non-XOAUTH2 or client_credentials jobs (those have no per-user token).
|
||||
function imapsyncJobTokenBadge(item) {
|
||||
if (item.source_auth_type !== 'XOAUTH2' || item.source_oauth_flow !== 'authorization_code') return '';
|
||||
if (item.user_token_error) {
|
||||
return '<span class="badge bg-danger" title="' + escapeHtml(item.user_token_error) + '">' + lang.syncjobs.token_state_failed + '</span>';
|
||||
}
|
||||
if (item.user_token_expires && item.user_token_expires * 1000 > Date.now()) {
|
||||
return '<span class="badge bg-success" title="' + lang.syncjobs.source_token_status + ': ' + new Date(item.user_token_expires * 1000).toLocaleString() + '">' + lang.syncjobs.token_state_issued + '</span>';
|
||||
}
|
||||
return '<span class="badge bg-warning">' + lang.syncjobs.token_state_pending + '</span>';
|
||||
}
|
||||
|
||||
// (Re)build every imapsync-source dropdown from the ACL-filtered API (fresh on modal open)
|
||||
function populateImapsyncSourceSelects() {
|
||||
$.get("/api/v1/get/syncjob_source/all", function(sources) {
|
||||
$('select.imapsync-source-select').each(function() {
|
||||
var $sel = $(this);
|
||||
var keepId = $sel.data('current-source-id');
|
||||
$sel.empty();
|
||||
$sel.append('<option value="">' + (lang.syncjobs ? lang.syncjobs.source_select : '') + '</option>');
|
||||
$.each(sources, function(_, src) {
|
||||
var label = src.name + ' — ' + src.host1 + ':' + src.port1 + ' (' + src.auth_type + ')';
|
||||
if (src.scope === 'all') label += ' [' + (lang.syncjobs ? lang.syncjobs.source_scope_all : 'all') + ']';
|
||||
var $opt = $('<option/>').val(src.id).text(label)
|
||||
.data('auth-type', src.auth_type).data('oauth-flow', src.oauth_flow);
|
||||
if (keepId && parseInt(keepId) === parseInt(src.id)) $opt.attr('selected', 'selected');
|
||||
$sel.append($opt);
|
||||
});
|
||||
if (typeof $sel.selectpicker === 'function') {
|
||||
$sel.selectpicker('destroy');
|
||||
$sel.selectpicker();
|
||||
}
|
||||
$sel.trigger('change');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Populate a scope domain/user multiselect from the ACL-filtered endpoint (preserves existing options)
|
||||
function fillImapsyncScopeSelect($sel, url, field) {
|
||||
$.get('/api/v1/get/' + url, function(rows) {
|
||||
var have = {};
|
||||
$sel.find('option').each(function() { have[$(this).val()] = true; });
|
||||
$.each(rows, function(_, r) {
|
||||
var v = r[field];
|
||||
if (!v || have[v]) return;
|
||||
$sel.append($('<option/>').val(v).text(v));
|
||||
});
|
||||
// Rebuild the bootstrap-select wrapper (destroy + re-init)
|
||||
if (typeof $sel.selectpicker === 'function') {
|
||||
$sel.selectpicker('destroy');
|
||||
$sel.selectpicker();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Source editor: OAuth block visibility + authorization_code-only fields + redirect URI
|
||||
function imapsyncSyncSourceOauthToggle($form) {
|
||||
var isOauth = $form.find('select.imapsync-source-auth-type').val() === 'XOAUTH2';
|
||||
var isAuthCode = $form.find('select.imapsync-source-oauth-flow').val() === 'authorization_code';
|
||||
$form.find('.imapsync-source-oauth-block').toggle(isOauth);
|
||||
$form.find('.imapsync-source-authcode-block').toggle(isOauth && isAuthCode);
|
||||
$form.find('.imapsync-source-redirect-uri').val(window.location.origin + '/syncjob-oauth');
|
||||
}
|
||||
|
||||
// Allowlisted imapsync options as a map { name: takesValue }. Loaded once and cached.
|
||||
var imapsyncCpOptions = null;
|
||||
function imapsyncCpEnsureOptions(cb) {
|
||||
if (imapsyncCpOptions !== null) { cb(); return; }
|
||||
$.get("/api/v1/get/syncjob_options", function(opts) {
|
||||
imapsyncCpOptions = (opts && typeof opts === 'object') ? opts : {};
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
// A imapsync flag option takes no value
|
||||
function imapsyncCpTakesValue(name) {
|
||||
return !(imapsyncCpOptions && imapsyncCpOptions[name] === false);
|
||||
}
|
||||
|
||||
// Enable/disable/require a row's value field based on the selected option.
|
||||
function imapsyncCpApplyValueState($row) {
|
||||
var name = $row.find('.imapsync-cp-opt').val() || '';
|
||||
var isFlag = (name !== '') && !imapsyncCpTakesValue(name);
|
||||
var needsValue = (name !== '') && !isFlag;
|
||||
var $val = $row.find('.imapsync-cp-val');
|
||||
$val.prop('disabled', isFlag);
|
||||
if (isFlag) $val.val('');
|
||||
$val.prop('required', needsValue);
|
||||
$val.attr('placeholder', isFlag
|
||||
? ((lang.syncjobs && lang.syncjobs.custom_param_no_value) || 'no value')
|
||||
: ((lang.syncjobs && lang.syncjobs.custom_param_value) || 'value'));
|
||||
}
|
||||
|
||||
// Append one row to an editor; option is a native <select> (not free-text), value is arbitrary.
|
||||
function imapsyncCpAddRow($editor, o, v) {
|
||||
var $row = $(
|
||||
'<div class="input-group mb-2 imapsync-cp-row">' +
|
||||
'<select class="form-control imapsync-cp-opt" style="flex:0 0 35%"></select>' +
|
||||
'<input type="text" class="form-control imapsync-cp-val">' +
|
||||
'<button type="button" class="btn btn-secondary imapsync-cp-remove"><i class="bi bi-trash"></i></button>' +
|
||||
'</div>'
|
||||
);
|
||||
var $sel = $row.find('.imapsync-cp-opt');
|
||||
$sel.append($('<option></option>').attr('value', '').text('— ' + ((lang.syncjobs && lang.syncjobs.custom_param_option) || 'option') + ' —'));
|
||||
Object.keys(imapsyncCpOptions || {}).forEach(function(name) {
|
||||
$sel.append($('<option></option>').attr('value', name).text(name));
|
||||
});
|
||||
// keep a stored value that is no longer allowlisted visible instead of silently dropping it
|
||||
if (o && !(imapsyncCpOptions && (o in imapsyncCpOptions))) {
|
||||
$sel.append($('<option></option>').attr('value', o).text(o));
|
||||
}
|
||||
$sel.val(o || '');
|
||||
$row.find('.imapsync-cp-val').val(v || '');
|
||||
$editor.find('.imapsync-cp-rows').append($row);
|
||||
imapsyncCpApplyValueState($row);
|
||||
}
|
||||
|
||||
// Build the rows of an editor from its hidden JSON value.
|
||||
function imapsyncCpBuild($editor) {
|
||||
var $hidden = $editor.siblings('.imapsync-cp-value');
|
||||
$editor.find('.imapsync-cp-rows').empty();
|
||||
var pairs = [];
|
||||
try { pairs = JSON.parse($hidden.val() || '[]'); } catch (e) { pairs = []; }
|
||||
if (!Array.isArray(pairs)) pairs = [];
|
||||
pairs.forEach(function(p) {
|
||||
if (p && typeof p === 'object') imapsyncCpAddRow($editor, p.o, p.v);
|
||||
});
|
||||
}
|
||||
|
||||
// Re-serialize an editor's rows back into its hidden JSON value (option name required).
|
||||
function imapsyncCpSerialize($editor) {
|
||||
var out = [];
|
||||
$editor.find('.imapsync-cp-row').each(function() {
|
||||
var o = $.trim($(this).find('.imapsync-cp-opt').val() || '').replace(/^-+/, '');
|
||||
if (o === '') return;
|
||||
out.push({ o: o, v: $(this).find('.imapsync-cp-val').val() || '' });
|
||||
});
|
||||
$editor.siblings('.imapsync-cp-value').val(JSON.stringify(out));
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
// Custom-params editor: keep the hidden JSON in sync with the rows
|
||||
$(document).on('change', '.imapsync-cp-opt', function() {
|
||||
imapsyncCpApplyValueState($(this).closest('.imapsync-cp-row'));
|
||||
imapsyncCpSerialize($(this).closest('.imapsync-cp-editor'));
|
||||
});
|
||||
$(document).on('input', '.imapsync-cp-val', function() {
|
||||
imapsyncCpSerialize($(this).closest('.imapsync-cp-editor'));
|
||||
});
|
||||
$(document).on('click', '.imapsync-cp-add', function() {
|
||||
var $editor = $(this).closest('.imapsync-cp-editor');
|
||||
imapsyncCpEnsureOptions(function() { imapsyncCpAddRow($editor); });
|
||||
});
|
||||
$(document).on('click', '.imapsync-cp-remove', function() {
|
||||
var $editor = $(this).closest('.imapsync-cp-editor');
|
||||
$(this).closest('.imapsync-cp-row').remove();
|
||||
imapsyncCpSerialize($editor);
|
||||
});
|
||||
|
||||
// Edit page (syncjob.twig): build rows from the stored value on load
|
||||
imapsyncCpEnsureOptions(function() {
|
||||
$('.imapsync-cp-editor').each(function() { imapsyncCpBuild($(this)); });
|
||||
});
|
||||
|
||||
// Syncjob form: toggle password row / OAuth "connect" button by the selected source's flow
|
||||
$(document).on('change', 'select.imapsync-source-select', function() {
|
||||
var $sel = $(this);
|
||||
var $opt = $sel.find('option:selected');
|
||||
var $form = $sel.closest('form');
|
||||
var isOauth = ($opt.data('auth-type') === 'XOAUTH2');
|
||||
var isAuthCode = isOauth && ($opt.data('oauth-flow') === 'authorization_code');
|
||||
var isEdit = ($form.data('id') === 'editsyncjob');
|
||||
$form.find('.password1-row').toggle(!isOauth);
|
||||
$form.find('input[name="password1"]').prop('required', !isOauth && !isEdit);
|
||||
$form.find('.imapsync-oauth-connect-row').toggle(isAuthCode);
|
||||
$form.find('.imapsync-user1-row').toggle(!isAuthCode);
|
||||
$form.find('input[name="user1"]').prop('required', !isAuthCode && !isEdit);
|
||||
});
|
||||
|
||||
// Source editor: OAuth block + authorization_code fields
|
||||
$(document).on('change', 'select.imapsync-source-auth-type, select.imapsync-source-oauth-flow', function() {
|
||||
imapsyncSyncSourceOauthToggle($(this).closest('form'));
|
||||
});
|
||||
|
||||
// Source editor: scope -> show + populate the domain/user pickers
|
||||
$(document).on('change', 'select.imapsync-source-scope', function() {
|
||||
var $form = $(this).closest('form');
|
||||
var scope = $(this).val();
|
||||
$form.find('.imapsync-source-domains-row').toggle(scope === 'domain');
|
||||
$form.find('.imapsync-source-users-row').toggle(scope === 'user');
|
||||
if (scope === 'domain') fillImapsyncScopeSelect($form.find('.imapsync-source-domains'), 'domain/all', 'domain_name');
|
||||
if (scope === 'user') fillImapsyncScopeSelect($form.find('.imapsync-source-users'), 'mailbox/all', 'username');
|
||||
});
|
||||
|
||||
// "Connect with the provider": open the per-user OAuth popup for the selected source
|
||||
$(document).on('click', '.imapsync-oauth-connect-btn', function() {
|
||||
var $form = $(this).closest('form');
|
||||
var sourceId = $form.find('select.imapsync-source-select').val();
|
||||
if (!sourceId) return;
|
||||
window.open('/syncjob-oauth?action=start&source_id=' + encodeURIComponent(sourceId),
|
||||
'syncjob_oauth', 'width=600,height=700');
|
||||
});
|
||||
|
||||
// Result from the OAuth popup: fill + lock user1, mark connected (or show the error)
|
||||
window.addEventListener('message', function(ev) {
|
||||
if (ev.origin !== window.location.origin || !ev.data || ev.data.type !== 'syncjob_oauth') return;
|
||||
$('.imapsync-oauth-connect-row:visible').each(function() {
|
||||
var $form = $(this).closest('form');
|
||||
var $status = $form.find('.imapsync-oauth-connect-status');
|
||||
if (ev.data.ok) {
|
||||
$form.find('input[name="user1"]').val(ev.data.user1).prop('readonly', true);
|
||||
$status.removeClass('text-danger').addClass('text-success')
|
||||
.text((lang.syncjobs ? lang.syncjobs.syncjob_oauth_connected : 'Connected') + ': ' + ev.data.user1);
|
||||
} else {
|
||||
$status.removeClass('text-success').addClass('text-danger').text(ev.data.error || 'error');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add-source modal opens: init scope pickers + OAuth block (auth_type may be cached-form preselected)
|
||||
$(document).on('shown.bs.modal', '#addImapsyncSourceModal', function() {
|
||||
$(this).find('select.imapsync-source-scope').trigger('change');
|
||||
imapsyncSyncSourceOauthToggle($(this).find('form'));
|
||||
});
|
||||
|
||||
// Add-syncjob modal opens: (re)load the source dropdown from the API
|
||||
$(document).on('shown.bs.modal', '#addSyncJobModalAdmin, #addSyncJobModal', function() {
|
||||
populateImapsyncSourceSelects();
|
||||
var $modal = $(this);
|
||||
imapsyncCpEnsureOptions(function() {
|
||||
$modal.find('.imapsync-cp-editor').each(function() { imapsyncCpBuild($(this)); });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ $(document).ready(function() {
|
||||
// create host cpu and mem charts
|
||||
createHostCpuAndMemChart();
|
||||
// check for new version
|
||||
if (mailcow_info.branch === "master"){
|
||||
if (mailcow_info.branch === "master" && mailcow_cc_role === "admin"){
|
||||
check_update(mailcow_info.version_tag, mailcow_info.project_url);
|
||||
}
|
||||
$("#mailcow_version").click(function(){
|
||||
@@ -1089,6 +1089,8 @@ jQuery(function($){
|
||||
return str;
|
||||
}).join('<br>\n');
|
||||
item.subject = escapeHtml(item.subject);
|
||||
if (item.sender_mime != null) item.sender_mime = escapeHtml(item.sender_mime);
|
||||
if (item['message-id'] != null) item['message-id'] = escapeHtml(item['message-id']);
|
||||
var scan_time = item.time_real.toFixed(3);
|
||||
if (item.time_virtual) {
|
||||
scan_time += ' / ' + item.time_virtual.toFixed(3);
|
||||
@@ -1128,6 +1130,11 @@ jQuery(function($){
|
||||
item.ua = escapeHtml(item.ua);
|
||||
}
|
||||
item.ua = '<span style="font-size:small">' + item.ua + '</span>';
|
||||
if (item.user == null) {
|
||||
item.user = 'unknown';
|
||||
} else {
|
||||
item.user = escapeHtml(item.user);
|
||||
}
|
||||
if (item.service == "activesync") {
|
||||
item.service = '<span class="badge fs-6 bg-info">ActiveSync</span>';
|
||||
}
|
||||
@@ -1162,6 +1169,7 @@ jQuery(function($){
|
||||
if (item === null) { return true; }
|
||||
item.user = escapeHtml(item.user);
|
||||
item.call = escapeHtml(item.call);
|
||||
if (item.msg) item.msg = escapeHtml(item.msg);
|
||||
item.task = '<code>' + item.task + '</code>';
|
||||
item.type = '<span class="badge fs-6 bg-' + item.type + '">' + item.type + '</span>';
|
||||
});
|
||||
@@ -1169,6 +1177,7 @@ jQuery(function($){
|
||||
$.each(data, function (i, item) {
|
||||
if (item === null) { return true; }
|
||||
item.username = escapeHtml(item.username);
|
||||
item.real_rip = escapeHtml(item.real_rip);
|
||||
item.service = '<div class="badge fs-6 bg-secondary">' + item.service.toUpperCase() + '</div>';
|
||||
});
|
||||
} else if (table == 'general_syslog') {
|
||||
@@ -1207,6 +1216,11 @@ jQuery(function($){
|
||||
});
|
||||
} else if (table == 'rllog') {
|
||||
$.each(data, function (i, item) {
|
||||
if (item.header_from != null) item.header_from = escapeHtml(item.header_from);
|
||||
if (item.header_subject != null) item.header_subject = escapeHtml(item.header_subject);
|
||||
if (item.from != null) item.from = escapeHtml(item.from);
|
||||
if (item.rcpt != null) item.rcpt = escapeHtml(item.rcpt);
|
||||
if (item.message_id != null) item.message_id = escapeHtml(item.message_id);
|
||||
if (item.user == null) {
|
||||
item.user = "none";
|
||||
}
|
||||
@@ -1664,41 +1678,26 @@ function createHostCpuAndMemChart(){
|
||||
function check_update(current_version, github_repo_url){
|
||||
if (!current_version || !github_repo_url) return false;
|
||||
|
||||
var github_account = github_repo_url.split("/")[3];
|
||||
var github_repo_name = github_repo_url.split("/")[4];
|
||||
|
||||
// get details about latest release
|
||||
window.fetch("https://api.github.com/repos/"+github_account+"/"+github_repo_name+"/releases/latest", {method:'GET',cache:'no-cache'}).then(function(response) {
|
||||
window.fetch("/inc/ajax/update_check.php", {method:'GET',cache:'no-cache'}).then(function(response) {
|
||||
if (!response.ok) throw new Error("update check returned " + response.status);
|
||||
return response.json();
|
||||
}).then(function(latest_data) {
|
||||
// get details about current release
|
||||
window.fetch("https://api.github.com/repos/"+github_account+"/"+github_repo_name+"/releases/tags/"+current_version, {method:'GET',cache:'no-cache'}).then(function(response) {
|
||||
return response.json();
|
||||
}).then(function(current_data) {
|
||||
// compare releases
|
||||
var date_current = new Date(current_data.created_at);
|
||||
var date_latest = new Date(latest_data.created_at);
|
||||
if (date_latest.getTime() <= date_current.getTime()){
|
||||
// no update available
|
||||
$("#mailcow_update").removeClass("text-warning text-danger").addClass("text-success");
|
||||
$("#mailcow_update").html("<b>" + lang_debug.no_update_available + "</b>");
|
||||
} else {
|
||||
// update available
|
||||
$("#mailcow_update").removeClass("text-danger text-success").addClass("text-warning");
|
||||
$("#mailcow_update").html(lang_debug.update_available + ` <a href="#" id="mailcow_update_changelog">`+latest_data.tag_name+`</a>`);
|
||||
$("#mailcow_update_changelog").click(function(){
|
||||
if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin")
|
||||
return;
|
||||
}).then(function(data) {
|
||||
if (data.status === "no_update") {
|
||||
$("#mailcow_update").removeClass("text-warning text-danger").addClass("text-success");
|
||||
$("#mailcow_update").html("<b>" + lang_debug.no_update_available + "</b>");
|
||||
} else if (data.status === "update_available") {
|
||||
$("#mailcow_update").removeClass("text-danger text-success").addClass("text-warning");
|
||||
$("#mailcow_update").html(lang_debug.update_available + ` <a href="#" id="mailcow_update_changelog">`+data.latest_tag+`</a>`);
|
||||
$("#mailcow_update_changelog").click(function(){
|
||||
if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin")
|
||||
return;
|
||||
|
||||
showVersionModal("New Release " + latest_data.tag_name, latest_data.tag_name);
|
||||
})
|
||||
}
|
||||
}).catch(err => {
|
||||
// err
|
||||
console.log(err);
|
||||
$("#mailcow_update").removeClass("text-success text-warning").addClass("text-danger");
|
||||
$("#mailcow_update").html("<b>"+ lang_debug.update_failed +"</b>");
|
||||
});
|
||||
showVersionModal("New Release " + data.latest_tag, data.latest_tag);
|
||||
})
|
||||
} else {
|
||||
throw new Error(data.message || "update check failed");
|
||||
}
|
||||
}).catch(err => {
|
||||
// err
|
||||
console.log(err);
|
||||
|
||||
@@ -240,4 +240,15 @@ jQuery(function($){
|
||||
// Draw Table if tab is active
|
||||
onVisible("[id^=wl_policy_domain_table]", () => draw_wl_policy_domain_table());
|
||||
onVisible("[id^=bl_policy_domain_table]", () => draw_bl_policy_domain_table());
|
||||
|
||||
// initialise imapsync-source form on load.
|
||||
if ($('select.imapsync-source-oauth-flow').length) {
|
||||
imapsyncSyncSourceOauthToggle($('select.imapsync-source-oauth-flow').closest('form'));
|
||||
}
|
||||
if ($('select.imapsync-source-scope').length) {
|
||||
$('select.imapsync-source-scope').trigger('change');
|
||||
}
|
||||
if ($('select.imapsync-source-select').length) {
|
||||
populateImapsyncSourceSelects();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
$(document).ready(function() {
|
||||
var theme = localStorage.getItem("theme");
|
||||
localStorage.clear();
|
||||
localStorage.setItem("theme", theme);
|
||||
var theme = localStorage.getItem("mailcow_theme");
|
||||
if (theme !== null) {
|
||||
localStorage.setItem("mailcow_theme", theme);
|
||||
}
|
||||
});
|
||||
|
||||
+78
-12
@@ -424,6 +424,11 @@ $(document).ready(function() {
|
||||
} else {
|
||||
$('#force_pw_update').prop('checked', false);
|
||||
}
|
||||
if (template.force_tfa == 1){
|
||||
$('#force_tfa').prop('checked', true);
|
||||
} else {
|
||||
$('#force_tfa').prop('checked', false);
|
||||
}
|
||||
if (template.sogo_access == 1){
|
||||
$('#sogo_access').prop('checked', true);
|
||||
} else {
|
||||
@@ -991,6 +996,7 @@ jQuery(function($){
|
||||
'style="min-width:2em;width:' + item.percent_in_use + '%">' + item.percent_in_use + '%' + '</div></div>'
|
||||
};
|
||||
item.username = escapeHtml(item.username);
|
||||
item.name = escapeHtml(item.name);
|
||||
|
||||
if (Array.isArray(item.tags)){
|
||||
var tags = '';
|
||||
@@ -1242,6 +1248,7 @@ jQuery(function($){
|
||||
item.attributes.eas_access = '<i class="text-' + (item.attributes.eas_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.eas_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.eas_access == 1 ? '1' : '0') + '</span></i>';
|
||||
item.attributes.dav_access = '<i class="text-' + (item.attributes.dav_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.dav_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.dav_access == 1 ? '1' : '0') + '</span></i>';
|
||||
item.attributes.sogo_access = '<i class="text-' + (item.attributes.sogo_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sogo_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.sogo_access == 1 ? '1' : '0') + '</span></i>';
|
||||
item.attributes.force_tfa = '<i class="text-' + (item.attributes.force_tfa == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.force_tfa == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.force_tfa == 1 ? '1' : '0') + '</span></i>';
|
||||
if (item.attributes.quarantine_notification === 'never') {
|
||||
item.attributes.quarantine_notification = lang.never;
|
||||
} else if (item.attributes.quarantine_notification === 'hourly') {
|
||||
@@ -1385,6 +1392,11 @@ jQuery(function($){
|
||||
return 1==data?'<i class="bi bi-check-lg"></i>':'<i class="bi bi-x-lg"></i>';
|
||||
}
|
||||
},
|
||||
{
|
||||
title: lang.force_tfa,
|
||||
data: 'attributes.force_tfa',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang_edit.ratelimit,
|
||||
data: 'attributes.ratelimit',
|
||||
@@ -2157,7 +2169,7 @@ jQuery(function($){
|
||||
"tr" +
|
||||
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
language: lang_datatables,
|
||||
order: [[2, 'desc']],
|
||||
order: [[3, 'asc']],
|
||||
initComplete: function(){
|
||||
hideTableExpandCollapseBtn('#tab-syncjobs', '#sync_job_table');
|
||||
},
|
||||
@@ -2166,16 +2178,18 @@ jQuery(function($){
|
||||
url: "/api/v1/get/syncjobs/all/no_log",
|
||||
dataSrc: function(json){
|
||||
$.each(json, function (i, item) {
|
||||
item.log = '<a href="#syncjobLogModal" data-bs-toggle="modal" data-syncjob-id="' + encodeURIComponent(item.id) + '">' + lang.open_logs + '</a>'
|
||||
item.user2 = escapeHtml(item.user2);
|
||||
if (!item.exclude > 0) {
|
||||
item.exclude = '-';
|
||||
} else {
|
||||
item.exclude = '<code>' + escapeHtml(item.exclude) + '</code>';
|
||||
}
|
||||
item.server_w_port = escapeHtml(item.user1) + '@' + escapeHtml(item.host1) + ':' + escapeHtml(item.port1);
|
||||
item.server_w_port = escapeHtml(item.user1) + '@' + escapeHtml(item.source_name || '?') + ' (' + escapeHtml(item.source_host || '?') + ':' + escapeHtml(item.source_port || '?') + ')';
|
||||
var tokenBadge = imapsyncJobTokenBadge(item);
|
||||
if (tokenBadge) item.server_w_port += '<div class="mt-1">' + tokenBadge + '</div>';
|
||||
item.action = '<div class="btn-group">' +
|
||||
'<a href="/edit/syncjob/' + item.id + '" class="btn btn-sm btn-xs-lg btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
|
||||
'<a href="#syncjobLogModal" data-bs-toggle="modal" data-syncjob-id="' + encodeURIComponent(item.id) + '" class="btn btn-sm btn-xs-lg btn-xs-half btn-info"><i class="bi bi-card-list"></i> ' + lang.open_logs + '</a>' +
|
||||
'<a href="#" data-action="delete_selected" data-id="single-syncjob" data-api-url="delete/syncjob" data-item="' + item.id + '" class="btn btn-sm btn-xs-lg btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
|
||||
'</div>';
|
||||
item.chkbox = '<input type="checkbox" class="form-check-input" data-id="syncjob" name="multi_select" value="' + item.id + '" />';
|
||||
@@ -2193,10 +2207,10 @@ jQuery(function($){
|
||||
} else {
|
||||
item.success = '<i class="text-' + (item.success == 1 ? 'success' : 'danger') + ' bi bi-' + (item.success == 1 ? 'check-lg' : 'x-lg') + '"></i>';
|
||||
}
|
||||
if (lang['syncjob_'+item.exit_status]) {
|
||||
item.exit_status = lang['syncjob_'+item.exit_status];
|
||||
if (lang.syncjobs['syncjob_'+item.exit_status]) {
|
||||
item.exit_status = lang.syncjobs['syncjob_'+item.exit_status];
|
||||
} else if (item.success != '-') {
|
||||
item.exit_status = lang.syncjob_check_log;
|
||||
item.exit_status = lang.syncjobs.syncjob_check_log;
|
||||
}
|
||||
item.exit_status = item.success + ' ' + item.exit_status;
|
||||
});
|
||||
@@ -2228,6 +2242,12 @@ jQuery(function($){
|
||||
responsivePriority: 3,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.syncjobs.prio,
|
||||
data: 'prio',
|
||||
responsivePriority: 3,
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.owner,
|
||||
data: 'user2',
|
||||
@@ -2245,15 +2265,10 @@ jQuery(function($){
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.syncjob_last_run_result,
|
||||
title: lang.syncjobs.syncjob_last_run_result,
|
||||
data: 'exit_status',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'Log',
|
||||
data: 'log',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.active,
|
||||
data: 'active',
|
||||
@@ -2465,6 +2480,56 @@ jQuery(function($){
|
||||
});
|
||||
}
|
||||
|
||||
function draw_imapsync_source_table() {
|
||||
if ($.fn.DataTable.isDataTable('#imapsync_source_table')) {
|
||||
$('#imapsync_source_table').DataTable().columns.adjust().responsive.recalc();
|
||||
return;
|
||||
}
|
||||
$('#imapsync_source_table').DataTable({
|
||||
responsive: true,
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
stateSave: true,
|
||||
pageLength: pagination_size,
|
||||
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + "tr" +
|
||||
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
language: lang_datatables,
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: "/api/v1/get/syncjob_source/all",
|
||||
dataSrc: function(json) {
|
||||
$.each(json, function(i, item) {
|
||||
item.endpoint = escapeHtml(item.host1 + ':' + item.port1 + ' (' + item.enc1 + ')');
|
||||
item.auth_display = imapsyncAuthDisplay(item);
|
||||
item.visibility_display = imapsyncScopeDisplay(item);
|
||||
if (item.can_edit) {
|
||||
item.action = '<div class="btn-group">' +
|
||||
'<a href="/edit/syncjob_source/' + item.id + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
|
||||
'<a href="#" data-action="delete_selected" data-id="single-syncjob_source" data-api-url="delete/syncjob_source" data-item="' + item.id + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
|
||||
'</div>';
|
||||
} else {
|
||||
item.action = '<span class="text-muted">-</span>';
|
||||
}
|
||||
});
|
||||
return json;
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ title: 'ID', data: 'id', defaultContent: '' },
|
||||
{ title: lang.syncjobs.source_name, data: 'name', defaultContent: '', render: (d) => escapeHtml(d || '') },
|
||||
{ title: lang.syncjobs.source_scope, data: 'visibility_display', defaultContent: '' },
|
||||
{ title: 'Endpoint', data: 'endpoint', defaultContent: '' },
|
||||
{ title: lang.syncjobs.source_auth_type, data: 'auth_display', defaultContent: '' },
|
||||
{ title: lang.active, data: 'active', defaultContent: '',
|
||||
render: function(d) { return d == 1 ? '<i class="bi bi-check-lg"></i>' : '<i class="bi bi-x-lg"></i>'; }},
|
||||
{ title: lang.action, data: 'action', defaultContent: '', className: 'dt-text-right' }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Shared imapsync helpers + delegated listeners live in js/build/013-mailcow.js
|
||||
// (draw_imapsync_source_table stays per-page and calls the global imapsyncScopeDisplay).
|
||||
|
||||
// Load only if the tab is visible
|
||||
onVisible("[id^=domain_table]", () => draw_domain_table());
|
||||
onVisible("[id^=templates_domain_table]", () => draw_templates_domain_table());
|
||||
@@ -2474,6 +2539,7 @@ jQuery(function($){
|
||||
onVisible("[id^=alias_table]", () => draw_alias_table());
|
||||
onVisible("[id^=aliasdomain_table]", () => draw_aliasdomain_table());
|
||||
onVisible("[id^=sync_job_table]", () => draw_sync_job_table());
|
||||
onVisible("[id^=imapsync_source_table]", () => draw_imapsync_source_table());
|
||||
onVisible("[id^=filter_table]", () => draw_filter_table());
|
||||
onVisible("[id^=bcc_table]", () => draw_bcc_table());
|
||||
onVisible("[id^=recipient_map_table]", () => draw_recipient_map_table());
|
||||
|
||||
@@ -102,18 +102,21 @@ jQuery(function($){
|
||||
{
|
||||
title: 'ID',
|
||||
data: 'id',
|
||||
defaultContent: ''
|
||||
defaultContent: '',
|
||||
render: $.fn.dataTable.render.text()
|
||||
},
|
||||
{
|
||||
title: lang.qid,
|
||||
data: 'qid',
|
||||
defaultContent: ''
|
||||
defaultContent: '',
|
||||
render: $.fn.dataTable.render.text()
|
||||
},
|
||||
{
|
||||
title: lang.sender,
|
||||
data: 'sender',
|
||||
className: 'senders-mw220',
|
||||
defaultContent: ''
|
||||
defaultContent: '',
|
||||
render: $.fn.dataTable.render.text()
|
||||
},
|
||||
{
|
||||
title: lang.subj,
|
||||
@@ -128,7 +131,8 @@ jQuery(function($){
|
||||
{
|
||||
title: lang.rcpt,
|
||||
data: 'rcpt',
|
||||
defaultContent: ''
|
||||
defaultContent: '',
|
||||
render: $.fn.dataTable.render.text()
|
||||
},
|
||||
{
|
||||
title: lang.danger,
|
||||
@@ -226,18 +230,18 @@ jQuery(function($){
|
||||
}
|
||||
if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
|
||||
$.each(data.fuzzy_hashes, function (index, value) {
|
||||
$('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
|
||||
$('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + escapeHtml(value) + '</p>');
|
||||
});
|
||||
} else {
|
||||
$('#qid_detail_fuzzy').append('-');
|
||||
}
|
||||
if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
|
||||
if (data.action == "add header") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.junk_folder + '</span>');
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + escapeHtml(data.score) + '</b> - ' + lang.junk_folder + '</span>');
|
||||
} else if (data.action == "reject") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + data.score + '</b> - ' + lang.rejected + '</span>');
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + escapeHtml(data.score) + '</b> - ' + lang.rejected + '</span>');
|
||||
} else if (data.action == "rewrite subject") {
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
|
||||
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + escapeHtml(data.score) + '</b> - ' + lang.rewrite_subject + '</span>');
|
||||
}
|
||||
}
|
||||
if (typeof data.recipients !== 'undefined') {
|
||||
@@ -254,8 +258,8 @@ jQuery(function($){
|
||||
qAtts.text('');
|
||||
$.each(data.attachments, function(index, value) {
|
||||
qAtts.append(
|
||||
'<p><a href="/inc/ajax/qitem_details.php?id=' + qitem + '&att=' + index + '" target="_blank">' + value[0] + '</a> (' + value[1] + ')' +
|
||||
' - <small><a href="' + value[3] + '" target="_blank">' + lang.check_hash + '</a></small></p>'
|
||||
'<p><a href="/inc/ajax/qitem_details.php?id=' + escapeHtml(qitem) + '&att=' + index + '" target="_blank">' + escapeHtml(value[0]) + '</a> (' + escapeHtml(value[1]) + ')' +
|
||||
' - <small><a href="' + escapeHtml(value[3]) + '" target="_blank">' + lang.check_hash + '</a></small></p>'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,13 +48,13 @@ jQuery(function($){
|
||||
url: "/api/v1/get/mailq/all",
|
||||
dataSrc: function(data){
|
||||
$.each(data, function (i, item) {
|
||||
item.chkbox = '<input type="checkbox" class="form-check-input" data-id="mailqitems" name="multi_select" value="' + item.queue_id + '" />';
|
||||
item.chkbox = '<input type="checkbox" class="form-check-input" data-id="mailqitems" name="multi_select" value="' + escapeHtml(item.queue_id) + '" />';
|
||||
rcpts = $.map(item.recipients, function(i) {
|
||||
return escapeHtml(i);
|
||||
});
|
||||
item.recipients = rcpts.join('<hr style="margin:1px!important">');
|
||||
item.action = '<div class="btn-group">' +
|
||||
'<a href="#" data-bs-toggle="modal" data-bs-target="#showQueuedMsg" data-queue-id="' + encodeURI(item.queue_id) + '" class="btn btn-xs btn-secondary">' + lang.show_message + '</a>' +
|
||||
'<a href="#" data-bs-toggle="modal" data-bs-target="#showQueuedMsg" data-queue-id="' + escapeHtml(item.queue_id) + '" class="btn btn-xs btn-secondary">' + lang.show_message + '</a>' +
|
||||
'</div>';
|
||||
});
|
||||
return data;
|
||||
@@ -79,12 +79,14 @@ jQuery(function($){
|
||||
{
|
||||
title: 'QID',
|
||||
data: 'queue_id',
|
||||
defaultContent: ''
|
||||
defaultContent: '',
|
||||
render: $.fn.dataTable.render.text()
|
||||
},
|
||||
{
|
||||
title: 'Queue',
|
||||
data: 'queue_name',
|
||||
defaultContent: ''
|
||||
defaultContent: '',
|
||||
render: $.fn.dataTable.render.text()
|
||||
},
|
||||
{
|
||||
title: lang_admin.arrival_time,
|
||||
@@ -106,7 +108,8 @@ jQuery(function($){
|
||||
{
|
||||
title: lang_admin.sender,
|
||||
data: 'sender',
|
||||
defaultContent: ''
|
||||
defaultContent: '',
|
||||
render: $.fn.dataTable.render.text()
|
||||
},
|
||||
{
|
||||
title: lang_admin.recipients,
|
||||
|
||||
+68
-13
@@ -98,8 +98,8 @@ jQuery(function($){
|
||||
var local_datetime = datetime.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
||||
var service = '<div class="badge bg-secondary">' + item.service.toUpperCase() + '</div>';
|
||||
var app_password = item.app_password ? ' <a href="/edit/app-passwd/' + item.app_password + '"><i class="bi bi-key-fill"></i><span class="ms-1">' + escapeHtml(item.app_password_name || "App") + '</span></a>' : '';
|
||||
var real_rip = item.real_rip.startsWith("Web") ? item.real_rip : '<a href="https://bgp.tools/prefix/' + item.real_rip + '" target="_blank">' + item.real_rip + "</a>";
|
||||
var ip_location = item.location ? ' <span class="flag-icon flag-icon-' + item.location.toLowerCase() + '"></span>' : '';
|
||||
var real_rip = item.real_rip.startsWith("Web") ? escapeHtml(item.real_rip) : '<a href="https://bgp.tools/prefix/' + escapeHtml(item.real_rip) + '" target="_blank">' + escapeHtml(item.real_rip) + "</a>";
|
||||
var ip_location = item.location ? ' <span class="flag-icon flag-icon-' + escapeHtml(item.location.toLowerCase()) + '"></span>' : '';
|
||||
var ip_data = real_rip + ip_location + app_password;
|
||||
|
||||
$(".last-sasl-login").append(`
|
||||
@@ -272,22 +272,25 @@ jQuery(function($){
|
||||
"tr" +
|
||||
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
language: lang_datatables,
|
||||
order: [[3, 'asc']],
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: '/api/v1/get/syncjobs/' + encodeURIComponent(mailcow_cc_username) + '/no_log',
|
||||
dataSrc: function(data){
|
||||
$.each(data, function (i, item) {
|
||||
item.user1 = escapeHtml(item.user1);
|
||||
item.log = '<a href="#syncjobLogModal" data-bs-toggle="modal" data-syncjob-id="' + item.id + '">' + lang.open_logs + '</a>'
|
||||
if (!item.exclude > 0) {
|
||||
item.exclude = '-';
|
||||
} else {
|
||||
item.exclude = '<code>' + escapeHtml(item.exclude) + '</code>';
|
||||
}
|
||||
item.server_w_port = escapeHtml(item.user1 + '@' + item.host1 + ':' + item.port1);
|
||||
item.server_w_port = escapeHtml(item.user1 + '@' + (item.source_name || '?') + ' (' + (item.source_host || '?') + ':' + (item.source_port || '?') + ')');
|
||||
var tokenBadge = imapsyncJobTokenBadge(item);
|
||||
if (tokenBadge) item.server_w_port += '<div class="mt-1">' + tokenBadge + '</div>';
|
||||
if (acl_data.syncjobs === 1) {
|
||||
item.action = '<div class="btn-group">' +
|
||||
'<a href="/edit/syncjob/' + item.id + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
|
||||
'<a href="#syncjobLogModal" data-bs-toggle="modal" data-syncjob-id="' + item.id + '" class="btn btn-xs btn-xs-half btn-info"><i class="bi bi-card-list"></i> ' + lang.open_logs + '</a>' +
|
||||
'<a href="#" data-action="delete_selected" data-id="single-syncjob" data-api-url="delete/syncjob" data-item="' + item.id + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
|
||||
'</div>';
|
||||
item.chkbox = '<input type="checkbox" class="form-check-input" data-id="syncjob" name="multi_select" value="' + item.id + '" />';
|
||||
@@ -310,10 +313,10 @@ jQuery(function($){
|
||||
} else {
|
||||
item.success = '<i class="text-' + (item.success == 1 ? 'success' : 'danger') + ' bi bi-' + (item.success == 1 ? 'check-lg' : 'x-lg') + '"></i>';
|
||||
}
|
||||
if (lang['syncjob_'+item.exit_status]) {
|
||||
item.exit_status = lang['syncjob_'+item.exit_status];
|
||||
if (lang.syncjobs['syncjob_'+item.exit_status]) {
|
||||
item.exit_status = lang.syncjobs['syncjob_'+item.exit_status];
|
||||
} else if (item.success != '-') {
|
||||
item.exit_status = lang.syncjob_check_log;
|
||||
item.exit_status = lang.syncjobs.syncjob_check_log;
|
||||
}
|
||||
item.exit_status = item.success + ' ' + item.exit_status;
|
||||
});
|
||||
@@ -345,6 +348,12 @@ jQuery(function($){
|
||||
defaultContent: '',
|
||||
responsivePriority: 3
|
||||
},
|
||||
{
|
||||
title: lang.syncjobs.prio,
|
||||
data: 'prio',
|
||||
defaultContent: '',
|
||||
responsivePriority: 3
|
||||
},
|
||||
{
|
||||
title: 'Server',
|
||||
data: 'server_w_port',
|
||||
@@ -362,15 +371,10 @@ jQuery(function($){
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.syncjob_last_run_result,
|
||||
title: lang.syncjobs.syncjob_last_run_result,
|
||||
data: 'exit_status',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: 'Log',
|
||||
data: 'log',
|
||||
defaultContent: ''
|
||||
},
|
||||
{
|
||||
title: lang.active,
|
||||
data: 'active',
|
||||
@@ -694,11 +698,62 @@ jQuery(function($){
|
||||
});
|
||||
}
|
||||
|
||||
function draw_imapsync_source_table() {
|
||||
if ($.fn.DataTable.isDataTable('#imapsync_source_table')) {
|
||||
$('#imapsync_source_table').DataTable().columns.adjust().responsive.recalc();
|
||||
return;
|
||||
}
|
||||
$('#imapsync_source_table').DataTable({
|
||||
responsive: true,
|
||||
processing: true,
|
||||
serverSide: false,
|
||||
stateSave: true,
|
||||
pageLength: pagination_size,
|
||||
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + "tr" +
|
||||
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
|
||||
language: lang_datatables,
|
||||
ajax: {
|
||||
type: "GET",
|
||||
url: "/api/v1/get/syncjob_source/all",
|
||||
dataSrc: function(json) {
|
||||
$.each(json, function(i, item) {
|
||||
item.endpoint = escapeHtml(item.host1 + ':' + item.port1 + ' (' + item.enc1 + ')');
|
||||
item.auth_display = imapsyncAuthDisplay(item);
|
||||
item.visibility_display = imapsyncScopeDisplay(item);
|
||||
if (acl_data.syncjobs === 1 && item.can_edit) {
|
||||
item.action = '<div class="btn-group">' +
|
||||
'<a href="/edit/syncjob_source/' + item.id + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
|
||||
'<a href="#" data-action="delete_selected" data-id="single-syncjob_source" data-api-url="delete/syncjob_source" data-item="' + item.id + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
|
||||
'</div>';
|
||||
} else {
|
||||
item.action = '<span class="text-muted">-</span>';
|
||||
}
|
||||
});
|
||||
return json;
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{ title: 'ID', data: 'id', defaultContent: '' },
|
||||
{ title: lang.syncjobs.source_name, data: 'name', defaultContent: '', render: (d) => escapeHtml(d || '') },
|
||||
{ title: lang.syncjobs.source_scope, data: 'visibility_display', defaultContent: '' },
|
||||
{ title: 'Endpoint', data: 'endpoint', defaultContent: '' },
|
||||
{ title: lang.syncjobs.source_auth_type, data: 'auth_display', defaultContent: '' },
|
||||
{ title: lang.active, data: 'active', defaultContent: '',
|
||||
render: function(d) { return d == 1 ? '<i class="bi bi-check-lg"></i>' : '<i class="bi bi-x-lg"></i>'; }},
|
||||
{ title: lang.action, data: 'action', defaultContent: '', className: 'dt-text-right' }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Shared imapsync helpers + delegated listeners live in js/build/013-mailcow.js
|
||||
// (draw_imapsync_source_table stays per-page and calls the global imapsyncScopeDisplay).
|
||||
|
||||
// Load only if the tab is visible
|
||||
onVisible("[id^=tla_table]", () => draw_tla_table());
|
||||
onVisible("[id^=bl_policy_mailbox_table]", () => draw_bl_policy_mailbox_table());
|
||||
onVisible("[id^=wl_policy_mailbox_table]", () => draw_wl_policy_mailbox_table());
|
||||
onVisible("[id^=sync_job_table]", () => draw_sync_job_table());
|
||||
onVisible("[id^=imapsync_source_table]", () => draw_imapsync_source_table());
|
||||
onVisible("[id^=app_passwd_table]", () => draw_app_passwd_table());
|
||||
onVisible("[id^=recent-logins]", () => last_logins('get'));
|
||||
});
|
||||
|
||||
+79
-11
@@ -317,7 +317,10 @@ if (isset($_GET['query'])) {
|
||||
process_add_return(admin('add', $attr));
|
||||
break;
|
||||
case "syncjob":
|
||||
process_add_return(mailbox('add', 'syncjob', $attr));
|
||||
process_add_return(syncjob('add', 'job', $attr));
|
||||
break;
|
||||
case "syncjob_source":
|
||||
process_add_return(syncjob('add', 'source', $attr));
|
||||
break;
|
||||
case "bcc":
|
||||
process_add_return(bcc('add', $attr));
|
||||
@@ -1046,6 +1049,36 @@ if (isset($_GET['query'])) {
|
||||
}
|
||||
process_get_return($data);
|
||||
break;
|
||||
case "syncjob_options":
|
||||
process_get_return($GLOBALS['IMAPSYNC_OPTIONS']);
|
||||
break;
|
||||
case "syncjob_source":
|
||||
switch ($object) {
|
||||
case "all":
|
||||
$ids = syncjob('get', 'sources');
|
||||
$data = array();
|
||||
if (is_array($ids)) {
|
||||
foreach ($ids as $sid) {
|
||||
$row = syncjob('get', 'source', array('id' => $sid));
|
||||
if ($row) $data[] = $row;
|
||||
}
|
||||
}
|
||||
process_get_return($data);
|
||||
break;
|
||||
default:
|
||||
if (is_numeric($object)) {
|
||||
$row = syncjob('get', 'source', array('id' => $object));
|
||||
if ($row) {
|
||||
process_get_return($row);
|
||||
} else {
|
||||
echo '{}';
|
||||
}
|
||||
} else {
|
||||
echo '{}';
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "syncjobs":
|
||||
switch ($object) {
|
||||
case "all":
|
||||
@@ -1055,14 +1088,14 @@ if (isset($_GET['query'])) {
|
||||
$mailboxes = mailbox('get', 'mailboxes', $domain);
|
||||
if (!empty($mailboxes)) {
|
||||
foreach ($mailboxes as $mailbox) {
|
||||
$syncjobs = mailbox('get', 'syncjobs', $mailbox);
|
||||
$syncjobs = syncjob('get', 'jobs', $mailbox);
|
||||
if (!empty($syncjobs)) {
|
||||
foreach ($syncjobs as $syncjob) {
|
||||
foreach ($syncjobs as $sj) {
|
||||
if (isset($extra)) {
|
||||
$details = mailbox('get', 'syncjob_details', $syncjob, explode(',', $extra));
|
||||
$details = syncjob('get', 'job', $sj, explode(',', $extra));
|
||||
}
|
||||
else {
|
||||
$details = mailbox('get', 'syncjob_details', $syncjob);
|
||||
$details = syncjob('get', 'job', $sj);
|
||||
}
|
||||
if ($details) {
|
||||
$data[] = $details;
|
||||
@@ -1083,14 +1116,14 @@ if (isset($_GET['query'])) {
|
||||
break;
|
||||
|
||||
default:
|
||||
$syncjobs = mailbox('get', 'syncjobs', $object);
|
||||
$syncjobs = syncjob('get', 'jobs', $object);
|
||||
if (!empty($syncjobs)) {
|
||||
foreach ($syncjobs as $syncjob) {
|
||||
foreach ($syncjobs as $sj) {
|
||||
if (isset($extra)) {
|
||||
$details = mailbox('get', 'syncjob_details', $syncjob, explode(',', $extra));
|
||||
$details = syncjob('get', 'job', $sj, explode(',', $extra));
|
||||
}
|
||||
else {
|
||||
$details = mailbox('get', 'syncjob_details', $syncjob);
|
||||
$details = syncjob('get', 'job', $sj);
|
||||
}
|
||||
if ($details) {
|
||||
$data[] = $details;
|
||||
@@ -1742,7 +1775,10 @@ if (isset($_GET['query'])) {
|
||||
process_delete_return(rsettings('delete', array('id' => $items)));
|
||||
break;
|
||||
case "syncjob":
|
||||
process_delete_return(mailbox('delete', 'syncjob', array('id' => $items)));
|
||||
process_delete_return(syncjob('delete', 'job', array('id' => $items)));
|
||||
break;
|
||||
case "syncjob_source":
|
||||
process_delete_return(syncjob('delete', 'source', array('id' => $items)));
|
||||
break;
|
||||
case "filter":
|
||||
process_delete_return(mailbox('delete', 'filter', array('id' => $items)));
|
||||
@@ -1968,7 +2004,39 @@ if (isset($_GET['query'])) {
|
||||
}
|
||||
break;
|
||||
case "syncjob":
|
||||
process_edit_return(mailbox('edit', 'syncjob', array_merge(array('id' => $items), $attr)));
|
||||
switch ($object) {
|
||||
case "run_next":
|
||||
process_edit_return(syncjob('edit', 'run_next', array('id' => $items)));
|
||||
break;
|
||||
default:
|
||||
process_edit_return(syncjob('edit', 'job', array_merge(array('id' => $items), $attr)));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "imapsync_settings":
|
||||
$mp = intval($attr['max_parallel'] ?? 0);
|
||||
// UI sends KB/s; store bytes/s (runner + per-job field are bytes/s)
|
||||
$bw = max(0, intval($attr['max_kb_per_second'] ?? 0)) * 1024;
|
||||
if ($mp < 1) {
|
||||
$_SESSION['return'][] = array('type' => 'danger', 'log' => array('imapsync_settings', 'edit', $attr), 'msg' => 'imapsync_max_parallel_invalid');
|
||||
process_edit_return(false);
|
||||
} elseif (imapsync_set_setting('max_parallel', $mp) && imapsync_set_setting('max_bytes_per_second', $bw)) {
|
||||
$_SESSION['return'][] = array('type' => 'success', 'log' => array('imapsync_settings', 'edit'), 'msg' => 'max_parallel_saved');
|
||||
process_edit_return(true);
|
||||
} else {
|
||||
$_SESSION['return'][] = array('type' => 'danger', 'log' => array('imapsync_settings', 'edit'), 'msg' => 'access_denied');
|
||||
process_edit_return(false);
|
||||
}
|
||||
break;
|
||||
case "syncjob_source":
|
||||
switch ($object) {
|
||||
case "refresh_token":
|
||||
process_edit_return(syncjob('edit', 'refresh_token', array('id' => $items)));
|
||||
break;
|
||||
default:
|
||||
process_edit_return(syncjob('edit', 'source', array_merge(array('id' => $items), $attr)));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "filter":
|
||||
process_edit_return(mailbox('edit', 'filter', array_merge(array('id' => $items), $attr)));
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"acl": {
|
||||
"login_as": "E-poçt qutusu istifadəçisi olaraq daxil ol",
|
||||
"alias_domains": "Alternativ domenlər əlavə et",
|
||||
"app_passwds": "Tətbiq parollarını idarə et",
|
||||
"bcc_maps": "BCC yönləndirmə xəritələri",
|
||||
"delimiter_action": "Ayırıcı əməliyyatı",
|
||||
"domain_desc": "Domen təsvirini dəyiş",
|
||||
"domain_relayhost": "Domen üçün relay serveri dəyiş",
|
||||
"eas_reset": "EAS cihazlarını sıfırla",
|
||||
"extend_sender_acl": "Göndərən ACL-ni xarici ünvanlarla genişləndirməyə icazə ver",
|
||||
"filters": "Filtrlər",
|
||||
"mailbox_relayhost": "E-poçt qutusu üçün relay serveri dəyiş",
|
||||
"prohibited": "ACL tərəfindən məhdudlaşdırılıb",
|
||||
"protocol_access": "Protokol girişini dəyiş"
|
||||
}
|
||||
}
|
||||
+108
-54
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"acl": {
|
||||
"alias_domains": "Alias-Domains hinzufügen",
|
||||
"alias_external_goto": "Aliase mit externen Ziel-Domains erlauben",
|
||||
"app_passwds": "App-Passwörter verwalten",
|
||||
"bcc_maps": "BCC-Maps",
|
||||
"delimiter_action": "Delimiter-Aktionen (tags)",
|
||||
@@ -44,24 +45,16 @@
|
||||
"app_name": "App-Name",
|
||||
"app_password": "App-Passwort hinzufügen",
|
||||
"app_passwd_protocols": "Zugelassene Protokolle für App-Passwort",
|
||||
"automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
|
||||
"backup_mx_options": "Relay-Optionen",
|
||||
"bcc_dest_format": "BCC-Ziel muss eine gültige E-Mail-Adresse sein.",
|
||||
"comment_info": "Ein privater Kommentar ist für den Benutzer nicht einsehbar. Ein öffentlicher Kommentar wird als Tooltip im Interface des Benutzers angezeigt.",
|
||||
"custom_params": "Eigene Parameter",
|
||||
"custom_params_hint": "Richtig: --param=xy, falsch: --param xy",
|
||||
"delete1": "Lösche Nachricht nach Übertragung vom Quell-Server.",
|
||||
"delete2": "Lösche Nachrichten von Ziel-Server, die nicht auf dem Quell-Server vorhanden sind.",
|
||||
"delete2duplicates": "Lösche Duplikate im Ziel",
|
||||
"description": "Beschreibung",
|
||||
"destination": "Ziel",
|
||||
"disable_login": "Login verbieten (Mails werden weiterhin angenommen)",
|
||||
"domain": "Domain",
|
||||
"domain_matches_hostname": "Domain %s darf nicht dem Hostnamen entsprechen",
|
||||
"domain_quota_m": "Domain-Speicherplatz gesamt (MiB)",
|
||||
"dry": "Synchronisation simulieren",
|
||||
"enc_method": "Verschlüsselung",
|
||||
"exclude": "Elemente ausschließen (Regex)",
|
||||
"full_name": "Vor- und Nachname",
|
||||
"gal": "Globales Adressbuch",
|
||||
"gal_info": "Das globale Adressbuch enthält alle Objekte einer Domain und kann durch keinen Benutzer geändert werden. Die Verfügbarkeitsinformation in SOGo ist nur bei eingeschaltetem globalen Adressbuch ersichtlich! <b>Zum Anwenden einer Änderung muss SOGo neugestartet werden.</b>",
|
||||
@@ -80,7 +73,6 @@
|
||||
"mailbox_username": "Benutzername (linker Teil der E-Mail-Adresse)",
|
||||
"max_aliases": "Max. mögliche Aliasse",
|
||||
"max_mailboxes": "Max. mögliche Mailboxen",
|
||||
"mins_interval": "Abrufintervall (Minuten)",
|
||||
"multiple_bookings": "Mehrfaches Buchen möglich",
|
||||
"nexthop": "Next Hop",
|
||||
"password": "Passwort",
|
||||
@@ -100,15 +92,9 @@
|
||||
"select_domain": "Bitte zuerst eine Domain auswählen",
|
||||
"sieve_desc": "Kurze Beschreibung",
|
||||
"sieve_type": "Filtertyp",
|
||||
"skipcrossduplicates": "Duplikate auch über Ordner hinweg überspringen (\"first come, first serve\")",
|
||||
"subscribeall": "Alle synchronisierten Ordner abonnieren",
|
||||
"syncjob": "Sync-Job hinzufügen",
|
||||
"syncjob_hint": "Passwörter werden unverschlüsselt abgelegt!",
|
||||
"target_address": "Ziel-Adresse(n)",
|
||||
"target_address_info": "<small>Vollständige E-Mail-Adresse(n). Getrennt durch Komma.</small>",
|
||||
"target_domain": "Ziel-Domain",
|
||||
"timeout1": "Timeout für Verbindung zum Remote-Host",
|
||||
"timeout2": "Timeout für Verbindung zum lokalen Host",
|
||||
"username": "Benutzername",
|
||||
"validate": "Validieren",
|
||||
"validation_success": "Erfolgreich validiert",
|
||||
@@ -150,7 +136,7 @@
|
||||
"arrival_time": "Ankunftszeit (Serverzeit)",
|
||||
"authed_user": "Auth. Benutzer",
|
||||
"ays": "Soll der Vorgang wirklich ausgeführt werden?",
|
||||
"ban_list_info": "Übersicht ausgesperrter Netzwerke: <b>Netzwerk (verbleibende Bannzeit) - [Aktionen]</b>.<br />IPs, die zum Entsperren eingereiht werden, verlassen die Liste aktiver Banns nach wenigen Sekunden.<br />Rote Labels sind Indikatoren für aktive Allowlist-Einträge.",
|
||||
"ban_list_info": "Übersicht ausgesperrter Netzwerke: <b>Netzwerk (verbleibende Bannzeit) - [Aktionen]</b>.<br />IPs, die zum Entsperren eingereiht werden, verlassen die Liste aktiver Banns nach wenigen Sekunden.<br />Rote Labels kennzeichnen aktive permanente Sperren durch Denylisting.",
|
||||
"change_logo": "Logo ändern",
|
||||
"configuration": "Konfiguration",
|
||||
"convert_html_to_text": "Konvertiere HTML zu reinem Text",
|
||||
@@ -412,7 +398,12 @@
|
||||
"logo_dark_label": "Invertiert für den Darkmode",
|
||||
"logo_normal_label": "Normal",
|
||||
"user_link": "Nutzer-Link",
|
||||
"filter": "Filter"
|
||||
"filter": "Filter",
|
||||
"syncjobs": "Sync Jobs",
|
||||
"syncjob_max_parallel": "Max. parallele Sync-Prozesse",
|
||||
"syncjob_max_parallel_info": "Wie viele Sync-Jobs gleichzeitig laufen dürfen.",
|
||||
"syncjob_max_kb_per_second": "Bandbreiten-Limit pro Prozess (KB/s)",
|
||||
"syncjob_max_kb_per_second_info": "0 = unbegrenzt. Kappt jeden Sync-Prozess; ein per-Job-Wert wird hierdurch begrenzt. Gesamt ≈ dieser Wert × parallele Prozesse."
|
||||
},
|
||||
"danger": {
|
||||
"access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten",
|
||||
@@ -443,6 +434,7 @@
|
||||
"domain_not_found": "Domain %s nicht gefunden",
|
||||
"domain_quota_m_in_use": "Domain-Speicherplatzlimit muss größer oder gleich %d MiB sein",
|
||||
"extended_sender_acl_denied": "Keine Rechte zum Setzen von externen Absenderadressen",
|
||||
"external_goto_denied": "Externe Ziel-Adresse %s ist nicht erlaubt",
|
||||
"extra_acl_invalid": "Externe Absenderadresse \"%s\" ist ungültig",
|
||||
"extra_acl_invalid_domain": "Externe Absenderadresse \"%s\" verwendet eine ungültige Domain",
|
||||
"fido2_verification_failed": "FIDO2-Verifizierung fehlgeschlagen: %s",
|
||||
@@ -512,6 +504,7 @@
|
||||
"pushover_credentials_missing": "Pushover Token und/oder Key fehlen",
|
||||
"pushover_key": "Pushover Key hat das falsche Format",
|
||||
"pushover_token": "Pushover Token hat das falsche Format",
|
||||
"quarantine_category_invalid": "Quarantäne-Kategorie muss eine der folgenden sein: add_header, reject, all",
|
||||
"quota_not_0_not_numeric": "Speicherplatz muss numerisch und >= 0 sein",
|
||||
"recipient_map_entry_exists": "Eine Empfängerumschreibung für Objekt \"%s\" existiert bereits",
|
||||
"recovery_email_failed": "E-Mail zur Wiederherstellung konnte nicht gesendet werden. Bitte wenden Sie sich an Ihren Administrator.",
|
||||
@@ -558,7 +551,24 @@
|
||||
"template_exists": "Vorlage %s existiert bereits",
|
||||
"template_id_invalid": "Vorlagen-ID %s ungültig",
|
||||
"template_name_invalid": "Name der Vorlage ungültig",
|
||||
"required_data_missing": "Die benötigten Daten: %s fehlen"
|
||||
"required_data_missing": "Die benötigten Daten: %s fehlen",
|
||||
"imapsync_source_in_use": "Sync-Quelle %s wird noch von einem Sync-Job verwendet und kann nicht gelöscht werden",
|
||||
"imapsync_source_token_refresh_failed": "OAuth-Token-Erneuerung für %s fehlgeschlagen",
|
||||
"imapsync_source_scope_empty": "Für die gewählte Sichtbarkeit mindestens eine Domain wählen",
|
||||
"imapsync_source_unavailable": "Die gewählte Sync-Quelle ist nicht verfügbar",
|
||||
"imapsync_source_oauth_connect_required": "Vor dem Speichern dieses Sync-Jobs beim Provider anmelden",
|
||||
"imapsync_source_name_invalid": "Ungültiger Name der Sync-Quelle",
|
||||
"imapsync_source_host_invalid": "Ungültiger Host",
|
||||
"imapsync_source_port_invalid": "Ungültiger Port",
|
||||
"imapsync_source_enc_invalid": "Ungültige Verschlüsselungsmethode",
|
||||
"imapsync_source_auth_invalid": "Ungültige Authentifizierungsmethode",
|
||||
"imapsync_custom_param_invalid": "Eigene Parameter enthalten eine nicht erlaubte imapsync-Option",
|
||||
"imapsync_source_oauth_required": "OAuth2 erfordert Token-Endpoint, Client-ID, Client-Secret und Scope",
|
||||
"imapsync_source_oauth_endpoint_invalid": "Ungültige OAuth2-Endpoint-URL",
|
||||
"imapsync_source_oauth_extra_invalid": "OAuth2-Extra-Parameter müssen gültiges JSON sein",
|
||||
"imapsync_source_invalid": "Ungültige Daten der Sync-Quelle",
|
||||
"imapsync_source_oauth_invalid": "Ungültige OAuth2-Konfiguration",
|
||||
"imapsync_max_parallel_invalid": "Anzahl paralleler Prozesse muss mindestens 1 sein"
|
||||
},
|
||||
"datatables": {
|
||||
"collapse_all": "Alle Einklappen",
|
||||
@@ -647,7 +657,6 @@
|
||||
"app_name": "App-Name",
|
||||
"app_passwd": "App-Passwörter",
|
||||
"app_passwd_protocols": "Zugelassene Protokolle für App-Passwort",
|
||||
"automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
|
||||
"backup_mx_options": "Relay-Optionen",
|
||||
"bcc_dest_format": "BCC-Ziel muss eine gültige E-Mail-Adresse sein.",
|
||||
"client_id": "Client-ID",
|
||||
@@ -655,9 +664,6 @@
|
||||
"comment_info": "Ein privater Kommentar ist für den Benutzer nicht einsehbar. Ein öffentlicher Kommentar wird als Tooltip im Interface des Benutzers angezeigt.",
|
||||
"created_on": "Erstellt am",
|
||||
"custom_attributes": "benutzerdefinierte Attribute",
|
||||
"delete1": "Lösche Nachricht nach Übertragung vom Quell-Server",
|
||||
"delete2": "Lösche Nachrichten von Ziel-Server, die nicht auf Quell-Server vorhanden sind",
|
||||
"delete2duplicates": "Lösche Duplikate im Ziel",
|
||||
"delete_ays": "Soll der Löschvorgang wirklich ausgeführt werden?",
|
||||
"description": "Beschreibung",
|
||||
"disable_login": "Login verbieten (Mails werden weiterhin angenommen)",
|
||||
@@ -681,7 +687,6 @@
|
||||
"dont_check_sender_acl": "Absender für Domain %s u. Alias-Domain nicht prüfen",
|
||||
"edit_alias_domain": "Alias-Domain bearbeiten",
|
||||
"encryption": "Verschlüsselung",
|
||||
"exclude": "Elemente ausschließen (Regex)",
|
||||
"extended_sender_acl": "Externe Absenderadressen",
|
||||
"extended_sender_acl_info": "Der DKIM-Domainkey der externen Absenderdomain sollte in diesen Server importiert werden, falls vorhanden.<br>\r\n Wird SPF verwendet, muss diesem Server der Versand gestattet werden.<br>\r\n Wird eine Domain oder Alias-Domain zu diesem Server hinzugefügt, die sich mit der externen Absenderadresse überschneidet, wird der externe Absender hier entfernt.<br>\r\n Ein Eintrag @domain.tld erlaubt den Versand als *@domain.tld",
|
||||
"force_pw_update": "Erzwinge Passwortänderung bei nächstem Login",
|
||||
@@ -712,10 +717,7 @@
|
||||
"max_aliases": "Max. Aliasse",
|
||||
"max_mailboxes": "Max. Mailboxanzahl",
|
||||
"max_quota": "Max. Größe per Mailbox (MiB)",
|
||||
"maxage": "Maximales Alter in Tagen einer Nachricht, die kopiert werden soll<br><small>(0 = alle Nachrichten kopieren)</small>",
|
||||
"maxbytespersecond": "Max. Übertragungsrate in Bytes/s (0 für unlimitiert)",
|
||||
"mbox_rl_info": "Dieses Limit wird auf den SASL Loginnamen angewendet und betrifft daher alle Absenderadressen, die der eingeloggte Benutzer verwendet. Bei Mailbox Ratelimit überwiegt ein Domain-weites Ratelimit.",
|
||||
"mins_interval": "Intervall (min)",
|
||||
"mta_sts": "MTA-STS",
|
||||
"mta_sts_info": "<a href='https://de.wikipedia.org/wiki/STARTTLS#MTA-STS' target='_blank'>MTA-STS</a> ist ein Standard, der den E-Mail-Versand zwischen Mailservern zwingt, TLS mit gültigen Zertifikaten zu verwenden. <br>Er wird verwendet, wenn <a target='_blank' href='https://de.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities'>DANE</a> aufgrund fehlender oder nicht unterstützter DNSSEC nicht möglich ist.<br><b>Hinweis</b>: Wenn die empfangende Domain DANE mit DNSSEC unterstützt, wird DANE <b>immer</b> bevorzugt – MTA-STS fungiert nur als Fallback.",
|
||||
"mta_sts_version": "Version",
|
||||
@@ -727,6 +729,7 @@
|
||||
"mta_sts_mx": "MX-Server",
|
||||
"mta_sts_mx_info": "Erlaubt das Senden nur an explizit aufgeführte Mailserver-Hostnamen; der sendende MTA überprüft, ob der DNS-MX-Hostname mit der Richtlinienliste übereinstimmt, und erlaubt die Zustellung nur mit einem gültigen TLS-Zertifikat (schützt vor MITM).",
|
||||
"mta_sts_mx_notice": "Es können mehrere MX-Server angegeben werden (durch Kommas getrennt).",
|
||||
"mta_sts_active_info": "Wenn nicht angehakt, wird die MTA-STS-Richtlinie nicht veröffentlicht und für die mta-sts-Subdomain kein Zertifikat über ACME angefordert.",
|
||||
"multiple_bookings": "Mehrfaches Buchen",
|
||||
"nexthop": "Next Hop",
|
||||
"none_inherit": "Keine Auswahl / Erben",
|
||||
@@ -766,7 +769,6 @@
|
||||
"sender_acl_info": "Wird einem Mailbox-Benutzer A der Versand als Mailbox-Benutzer B gestattet, so erscheint der Absender <b>nicht</b> automatisch in SOGo zur Auswahl.<br>\r\n In SOGo muss zusätzlich eine Delegation eingerichtet werden. Dieses Verhalten trifft nicht auf Alias-Adressen zu.",
|
||||
"sieve_desc": "Kurze Beschreibung",
|
||||
"sieve_type": "Filtertyp",
|
||||
"skipcrossduplicates": "Duplikate auch über Ordner hinweg überspringen (\"first come, first serve\")",
|
||||
"sogo_access": "Direktes weiterleiten an SOGo",
|
||||
"sogo_access_info": "Nach dem Einloggen wird der Benutzer automatisch an SOGo weitergeleitet.",
|
||||
"sogo_visible": "Alias in SOGo sichtbar",
|
||||
@@ -775,12 +777,8 @@
|
||||
"spam_filter": "Spamfilter",
|
||||
"spam_policy": "Hinzufügen und Entfernen von Einträgen in Allow- und Denylisten",
|
||||
"spam_score": "Einen benutzerdefiniterten Spam-Score festlegen",
|
||||
"subfolder2": "Ziel-Ordner<br><small>(leer = kein Unterordner)</small>",
|
||||
"syncjob": "Sync-Job bearbeiten",
|
||||
"target_address": "Ziel-Adresse(n)",
|
||||
"target_domain": "Ziel-Domain",
|
||||
"timeout1": "Timeout für Verbindung zum Remote-Host",
|
||||
"timeout2": "Timeout für Verbindung zum lokalen Host",
|
||||
"title": "Objekt bearbeiten",
|
||||
"unchanged_if_empty": "Unverändert, wenn leer",
|
||||
"username": "Benutzername",
|
||||
@@ -994,7 +992,6 @@
|
||||
"spam_aliases": "Spam-Alias",
|
||||
"stats": "Statistik",
|
||||
"status": "Status",
|
||||
"sync_jobs": "Synchronisationen",
|
||||
"table_size": "Tabellengröße",
|
||||
"table_size_show_n": "Zeige %s Einträge",
|
||||
"target_address": "Ziel-Adresse",
|
||||
@@ -1022,17 +1019,8 @@
|
||||
"open_logs": "Öffne Logs",
|
||||
"recipient": "Empfänger",
|
||||
"sender": "Sender",
|
||||
"syncjob_EX_OK": "Erfolg",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort",
|
||||
"syncjob_check_log": "Logs überprüfen",
|
||||
"all_domains": "Alle Domains",
|
||||
"catch_all": "Catch-All",
|
||||
"syncjob_last_run_result": "Letztes Ausführungsergebnis",
|
||||
"syncjob_EXIT_CONNECTION_FAILURE": "Verbindungsproblem",
|
||||
"syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem",
|
||||
"syncjob_EXIT_OVERQUOTA": "Ziel Mailbox ist über dem Limit",
|
||||
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kann keine Verbindung zum Zielserver herstellen",
|
||||
"iam": "Identitätsanbieter"
|
||||
},
|
||||
"oauth2": {
|
||||
@@ -1085,7 +1073,7 @@
|
||||
"rspamd_result": "Rspamd-Ergebnis",
|
||||
"sender": "Sender (SMTP)",
|
||||
"sender_header": "Sender (\"From\"-Header)",
|
||||
"settings_info": "Maximale Anzahl der zurückgehaltenen E-Mails: %s<br>Maximale Größe einer zu speichernden E-Mail: %s MiB",
|
||||
"settings_info": "Maximale Anzahl der zurückgehaltenen E-Mails (pro Mailbox): %s<br>Maximale Größe einer zu speichernden E-Mail: %s MiB",
|
||||
"show_item": "Details",
|
||||
"spam": "Spam",
|
||||
"spam_score": "Bewertung",
|
||||
@@ -1206,7 +1194,13 @@
|
||||
"verified_fido2_login": "FIDO2-Anmeldung verifiziert",
|
||||
"verified_totp_login": "TOTP-Anmeldung verifiziert",
|
||||
"verified_webauthn_login": "WebAuthn-Anmeldung verifiziert",
|
||||
"verified_yotp_login": "Yubico-OTP-Anmeldung verifiziert"
|
||||
"verified_yotp_login": "Yubico-OTP-Anmeldung verifiziert",
|
||||
"imapsync_source_added": "Sync-Quelle %s wurde hinzugefügt",
|
||||
"imapsync_source_modified": "Sync-Quelle %s wurde gespeichert",
|
||||
"imapsync_source_deleted": "Sync-Quelle %s wurde gelöscht",
|
||||
"imapsync_source_token_refreshed": "OAuth-Token für %s wurde erneuert",
|
||||
"max_parallel_saved": "Sync-Job-Einstellungen gespeichert",
|
||||
"imapsync_run_next_done": "%s Sync-Job(s) an den Anfang der Warteschlange gesetzt"
|
||||
},
|
||||
"tfa": {
|
||||
"authenticators": "Authentikatoren",
|
||||
@@ -1241,6 +1235,77 @@
|
||||
"setup_required": "Ihr Konto erfordert Zwei-Faktor-Authentifizierung. Bitte richten Sie eine 2FA-Methode ein, um fortzufahren.",
|
||||
"cancel_setup": "Abbrechen und abmelden"
|
||||
},
|
||||
"syncjobs": {
|
||||
"sources_panel": "Sync-Quellen",
|
||||
"sources_hint": "Sync-Quellen definieren IMAP-Server (mit ihrer Authentifizierungsmethode), die von Sync-Jobs wiederverwendet werden können. Definiere jede Quelle einmal und wähle sie dann beim Anlegen eines Sync-Jobs aus dem Dropdown aus.",
|
||||
"source_add": "Quelle hinzufügen",
|
||||
"source": "Quelle",
|
||||
"source_name": "Name",
|
||||
"source_description": "Beschreibung",
|
||||
"source_scope": "Sichtbarkeit",
|
||||
"source_scope_all": "Alle Nutzer",
|
||||
"source_scope_domain": "Bestimmte Domains",
|
||||
"source_scope_user": "Bestimmte Nutzer",
|
||||
"source_domains": "Domains",
|
||||
"source_users": "Nutzer",
|
||||
"source_select": "Quelle wählen",
|
||||
"source_auth_type": "Authentifizierung",
|
||||
"source_oauth_block": "OAuth2-Konfiguration",
|
||||
"source_token_status": "Token gültig bis",
|
||||
"source_token_error": "Letzter Refresh-Fehler",
|
||||
"source_refresh_now": "Token jetzt erneuern",
|
||||
"source_secret_unchanged_hint": "Leer lassen, um das gespeicherte Secret beizubehalten.",
|
||||
"source_oauth_flow": "OAuth2-Flow",
|
||||
"source_oauth_flow_app": "App (Client Credentials)",
|
||||
"source_oauth_flow_user": "Benutzer-Login (Authorization Code)",
|
||||
"source_flow_badge_app": "Client Credentials",
|
||||
"source_flow_badge_user": "Benutzer-Login",
|
||||
"token_state_issued": "Token ausgestellt",
|
||||
"token_state_pending": "Token wird ausgestellt",
|
||||
"token_state_failed": "Token fehlgeschlagen",
|
||||
"source_authorize_endpoint": "Authorize-Endpoint",
|
||||
"source_userinfo_endpoint": "Userinfo-Endpoint",
|
||||
"source_redirect_uri": "Redirect-URI",
|
||||
"source_redirect_uri_hint": "Diese URI in der Provider-App (z.B. Azure) eintragen.",
|
||||
"syncjob_oauth_connect": "Beim Provider anmelden",
|
||||
"syncjob_oauth_connected": "Verbunden",
|
||||
"sync_jobs": "Sync Jobs",
|
||||
"syncjob_check_log": "Logs überprüfen",
|
||||
"syncjob_last_run_result": "Letztes Ausführungsergebnis",
|
||||
"syncjob_EX_OK": "Erfolg",
|
||||
"syncjob_EXIT_CONNECTION_FAILURE": "Verbindungsproblem",
|
||||
"syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem",
|
||||
"syncjob_EXIT_OVERQUOTA": "Ziel Mailbox ist über dem Limit",
|
||||
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kann keine Verbindung zum Zielserver herstellen",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort",
|
||||
"add": "Sync-Job hinzufügen",
|
||||
"add_hint": "Passwörter werden unverschlüsselt abgelegt!",
|
||||
"edit": "Sync-Job bearbeiten",
|
||||
"create": "Neuen Sync-Job erstellen",
|
||||
"dry": "Synchronisation simulieren",
|
||||
"subfolder2": "Ziel-Ordner<br><small>(leer = kein Unterordner)</small>",
|
||||
"mins_interval": "Abrufintervall (Minuten)",
|
||||
"maxage": "Maximales Alter in Tagen einer Nachricht, die kopiert werden soll<br><small>(0 = alle Nachrichten kopieren)</small>",
|
||||
"exclude": "Elemente ausschließen (Regex)",
|
||||
"automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
|
||||
"delete1": "Lösche Nachricht nach Übertragung vom Quell-Server.",
|
||||
"delete2": "Lösche Nachrichten von Ziel-Server, die nicht auf dem Quell-Server vorhanden sind.",
|
||||
"delete2duplicates": "Lösche Duplikate im Ziel",
|
||||
"skipcrossduplicates": "Duplikate auch über Ordner hinweg überspringen (\"first come, first serve\")",
|
||||
"subscribeall": "Alle synchronisierten Ordner abonnieren",
|
||||
"timeout1": "Timeout für Verbindung zum Remote-Host",
|
||||
"timeout2": "Timeout für Verbindung zum lokalen Host",
|
||||
"maxbytespersecond": "Max. Übertragungsrate in Bytes/s (0 für unlimitiert)",
|
||||
"custom_params": "Eigene Parameter",
|
||||
"custom_params_hint": "Pro Zeile eine imapsync-Option. Es werden nur erlaubte Optionen akzeptiert; der Wert darf beliebige Zeichen enthalten (Leerzeichen, Regex, …) und wird als ein einzelnes Argument übergeben.",
|
||||
"custom_param_option": "Option",
|
||||
"custom_param_value": "Wert",
|
||||
"custom_param_no_value": "kein Wert nötig",
|
||||
"custom_param_add": "Parameter hinzufügen",
|
||||
"prio": "Priorität",
|
||||
"prio_hint": "Fällige Jobs laufen am längsten-nicht-gelaufen zuerst; bei gleichem Stand entscheidet die höhere Priorität (0 = normal)."
|
||||
},
|
||||
"user": {
|
||||
"action": "Aktion",
|
||||
"active": "Aktiv",
|
||||
@@ -1271,7 +1336,6 @@
|
||||
"clear_recent_successful_connections": "Alle erfolgreichen Verbindungen bereinigen",
|
||||
"client_configuration": "Konfigurationsanleitungen für E-Mail-Programme und Smartphones anzeigen",
|
||||
"create_app_passwd": "Erstelle App-Passwort",
|
||||
"create_syncjob": "Neuen Sync-Job erstellen",
|
||||
"created_on": "Erstellt am",
|
||||
"daily": "Täglich",
|
||||
"day": "Tag",
|
||||
@@ -1379,7 +1443,6 @@
|
||||
"spamfilter_wl_desc": "Für E-Mail-Adressen, die vom Spamfilter <b>nicht</b> erfasst werden sollen. Die Verwendung von Wildcards ist gestattet. Ein Filter funktioniert lediglich für direkte Nicht-„Catch-All“-Alias-Adressen (Alias-Adressen mit lediglich einer Mailbox als Ziel-Adresse) sowie die Mailbox-Adresse selbst.",
|
||||
"spamfilter_yellow": "Gelb: Die Nachricht ist vielleicht Spam, wird als Spam markiert und in den Junk-Ordner verschoben",
|
||||
"status": "Status",
|
||||
"sync_jobs": "Sync Jobs",
|
||||
"tag_handling": "Umgang mit getaggten E-Mails steuern",
|
||||
"tag_help_example": "Beispiel für eine getaggte E-Mail-Adresse: ich<b>+Facebook</b>@example.org",
|
||||
"tag_help_explain": "Als Unterordner: Es wird ein Ordner mit dem Namen des Tags unterhalb der Inbox erstellt (\"INBOX/Facebook\").<br>\r\nIn Betreff: Der Name des Tags wird dem Betreff angefügt, etwa \"[Facebook] Meine Neuigkeiten\".",
|
||||
@@ -1404,16 +1467,7 @@
|
||||
"with_app_password": "mit App-Passwort",
|
||||
"year": "Jahr",
|
||||
"years": "Jahren",
|
||||
"syncjob_EX_OK": "Erfolg",
|
||||
"open_logs": "Öffne Logs",
|
||||
"syncjob_check_log": "Logs überprüfen",
|
||||
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kann keine Verbindung zum Zielserver herstellen",
|
||||
"syncjob_EXIT_OVERQUOTA": "Ziel Mailbox ist über dem Limit",
|
||||
"syncjob_last_run_result": "Letztes Ausführungsergebnis",
|
||||
"syncjob_EXIT_CONNECTION_FAILURE": "Verbindungsproblem",
|
||||
"syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort",
|
||||
"pushover_sound": "Ton"
|
||||
},
|
||||
"warning": {
|
||||
|
||||
+108
-53
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"acl": {
|
||||
"alias_domains": "Add alias domains",
|
||||
"alias_external_goto": "Allow aliases with external goto domains",
|
||||
"app_passwds": "Manage app passwords",
|
||||
"bcc_maps": "BCC maps",
|
||||
"delimiter_action": "Delimiter action",
|
||||
@@ -44,24 +45,16 @@
|
||||
"app_name": "App name",
|
||||
"app_password": "Add app password",
|
||||
"app_passwd_protocols": "Allowed protocols for app password",
|
||||
"automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
|
||||
"backup_mx_options": "Relay options",
|
||||
"bcc_dest_format": "BCC destination must be a single valid email address.<br>If you need to send a copy to multiple addresses, create an alias and use it here.",
|
||||
"comment_info": "A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a user's overview",
|
||||
"custom_params": "Custom parameters",
|
||||
"custom_params_hint": "Right: --param=xy, wrong: --param xy",
|
||||
"delete1": "Delete from source when completed",
|
||||
"delete2": "Delete messages on destination that are not on source",
|
||||
"delete2duplicates": "Delete duplicates on destination",
|
||||
"description": "Description",
|
||||
"destination": "Destination",
|
||||
"disable_login": "Disallow login (incoming mail is still accepted)",
|
||||
"domain": "Domain",
|
||||
"domain_matches_hostname": "Domain %s matches hostname",
|
||||
"domain_quota_m": "Total domain quota (MiB)",
|
||||
"dry": "Simulate synchronization",
|
||||
"enc_method": "Encryption method",
|
||||
"exclude": "Exclude objects (regex)",
|
||||
"full_name": "Full name",
|
||||
"gal": "Global Address List",
|
||||
"gal_info": "The GAL contains all objects of a domain and cannot be edited by any user. Free/busy information in SOGo is missing, if disabled! <b>Restart SOGo to apply changes.</b>",
|
||||
@@ -80,7 +73,6 @@
|
||||
"mailbox_username": "Username (left part of an email address)",
|
||||
"max_aliases": "Max. possible aliases",
|
||||
"max_mailboxes": "Max. possible mailboxes",
|
||||
"mins_interval": "Polling interval (minutes)",
|
||||
"multiple_bookings": "Multiple bookings",
|
||||
"nexthop": "Next hop",
|
||||
"password": "Password",
|
||||
@@ -100,16 +92,10 @@
|
||||
"select_domain": "Please select a domain first",
|
||||
"sieve_desc": "Short description",
|
||||
"sieve_type": "Filter type",
|
||||
"skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)",
|
||||
"subscribeall": "Subscribe all folders",
|
||||
"syncjob": "Add sync job",
|
||||
"syncjob_hint": "Be aware that passwords need to be saved plain-text!",
|
||||
"tags": "Tags",
|
||||
"target_address": "Goto addresses",
|
||||
"target_address_info": "<small>Full email address/es (comma-separated).</small>",
|
||||
"target_domain": "Target domain",
|
||||
"timeout1": "Timeout for connection to remote host",
|
||||
"timeout2": "Timeout for connection to local host",
|
||||
"username": "Username",
|
||||
"validate": "Validate",
|
||||
"validation_success": "Validated successfully"
|
||||
@@ -412,7 +398,12 @@
|
||||
"user_quicklink": "Hide Quicklink to User Login Page",
|
||||
"validate_license_now": "Validate GUID against license server",
|
||||
"verify": "Verify",
|
||||
"yes": "✓"
|
||||
"yes": "✓",
|
||||
"syncjobs": "Sync jobs",
|
||||
"syncjob_max_parallel": "Max. parallel sync processes",
|
||||
"syncjob_max_parallel_info": "How many sync jobs may run at the same time.",
|
||||
"syncjob_max_kb_per_second": "Bandwidth limit per process (KB/s)",
|
||||
"syncjob_max_kb_per_second_info": "0 = unlimited. Caps every sync process; a per-job value is capped by this. Total ≈ this × max parallel."
|
||||
},
|
||||
"danger": {
|
||||
"access_denied": "Access denied or invalid form data",
|
||||
@@ -444,6 +435,7 @@
|
||||
"domain_not_found": "Domain %s not found",
|
||||
"domain_quota_m_in_use": "Domain quota must be greater or equal to %s MiB",
|
||||
"extended_sender_acl_denied": "missing ACL to set external sender addresses",
|
||||
"external_goto_denied": "External goto address %s is not allowed",
|
||||
"extra_acl_invalid": "External sender address \"%s\" is invalid",
|
||||
"extra_acl_invalid_domain": "External sender \"%s\" uses an invalid domain",
|
||||
"fido2_verification_failed": "FIDO2 verification failed: %s",
|
||||
@@ -513,6 +505,7 @@
|
||||
"pushover_credentials_missing": "Pushover token and or key missing",
|
||||
"pushover_key": "Pushover key has a wrong format",
|
||||
"pushover_token": "Pushover token has a wrong format",
|
||||
"quarantine_category_invalid": "Quarantine category must be one of: add_header, reject, all",
|
||||
"quota_not_0_not_numeric": "Quota must be numeric and >= 0",
|
||||
"recipient_map_entry_exists": "A Recipient map entry \"%s\" exists",
|
||||
"recovery_email_failed": "Could not send a recovery email. Please contact your administrator.",
|
||||
@@ -558,7 +551,24 @@
|
||||
"validity_missing": "Please assign a period of validity",
|
||||
"value_missing": "Please provide all values",
|
||||
"version_invalid": "Version %s is invalid",
|
||||
"yotp_verification_failed": "Yubico OTP verification failed: %s"
|
||||
"yotp_verification_failed": "Yubico OTP verification failed: %s",
|
||||
"imapsync_source_in_use": "Sync source %s is still used by a sync job and cannot be deleted",
|
||||
"imapsync_source_token_refresh_failed": "OAuth token refresh for %s failed",
|
||||
"imapsync_source_scope_empty": "Select at least one domain for the chosen visibility",
|
||||
"imapsync_source_unavailable": "The selected sync source is unavailable",
|
||||
"imapsync_source_oauth_connect_required": "Sign in with the provider before saving this sync job",
|
||||
"imapsync_source_name_invalid": "Invalid sync source name",
|
||||
"imapsync_source_host_invalid": "Invalid host",
|
||||
"imapsync_source_port_invalid": "Invalid port",
|
||||
"imapsync_source_enc_invalid": "Invalid encryption method",
|
||||
"imapsync_source_auth_invalid": "Invalid authentication method",
|
||||
"imapsync_custom_param_invalid": "Custom parameters contain a disallowed imapsync option",
|
||||
"imapsync_source_oauth_required": "OAuth2 requires token endpoint, client ID, client secret and scope",
|
||||
"imapsync_source_oauth_endpoint_invalid": "Invalid OAuth2 endpoint URL",
|
||||
"imapsync_source_oauth_extra_invalid": "OAuth2 extra parameters must be valid JSON",
|
||||
"imapsync_source_invalid": "Invalid sync source data",
|
||||
"imapsync_source_oauth_invalid": "Invalid OAuth2 configuration",
|
||||
"imapsync_max_parallel_invalid": "Parallel process count must be at least 1"
|
||||
},
|
||||
"datatables": {
|
||||
"collapse_all": "Collapse All",
|
||||
@@ -647,7 +657,6 @@
|
||||
"app_name": "App name",
|
||||
"app_passwd": "App password",
|
||||
"app_passwd_protocols": "Allowed protocols for app password",
|
||||
"automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
|
||||
"backup_mx_options": "Relay options",
|
||||
"bcc_dest_format": "BCC destination must be a single valid email address.<br>If you need to send a copy to multiple addresses, create an alias and use it here.",
|
||||
"client_id": "Client ID",
|
||||
@@ -655,9 +664,6 @@
|
||||
"comment_info": "A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a user's overview",
|
||||
"created_on": "Created on",
|
||||
"custom_attributes": "Custom attributes",
|
||||
"delete1": "Delete from source when completed",
|
||||
"delete2": "Delete messages on destination that are not on source",
|
||||
"delete2duplicates": "Delete duplicates on destination",
|
||||
"delete_ays": "Please confirm the deletion process.",
|
||||
"description": "Description",
|
||||
"disable_login": "Disallow login (incoming mail is still accepted)",
|
||||
@@ -681,7 +687,6 @@
|
||||
"dont_check_sender_acl": "Disable sender check for domain %s (+ alias domains)",
|
||||
"edit_alias_domain": "Edit Alias domain",
|
||||
"encryption": "Encryption",
|
||||
"exclude": "Exclude objects (regex)",
|
||||
"extended_sender_acl": "External sender addresses",
|
||||
"extended_sender_acl_info": "A DKIM domain key should be imported, if available.<br>\r\n Remember to add this server to the corresponding SPF TXT record.<br>\r\n Whenever a domain or alias domain is added to this server, that overlaps with an external address, the external address is removed.<br>\r\n Use @domain.tld to allow to send as *@domain.tld.",
|
||||
"force_pw_update": "Force password update at next login",
|
||||
@@ -712,10 +717,7 @@
|
||||
"max_aliases": "Max. aliases",
|
||||
"max_mailboxes": "Max. possible mailboxes",
|
||||
"max_quota": "Max. quota per mailbox (MiB)",
|
||||
"maxage": "Maximum age of messages in days that will be polled from remote<br><small>(0 = ignore age)</small>",
|
||||
"maxbytespersecond": "Max. bytes per second <br><small>(0 = unlimited)</small>",
|
||||
"mbox_rl_info": "This rate limit is applied on the SASL login name, it matches any \"from\" address used by the logged-in user. A mailbox rate limit overrides a domain-wide rate limit.",
|
||||
"mins_interval": "Interval (min)",
|
||||
"mta_sts": "MTA-STS",
|
||||
"mta_sts_info": "<a href='https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security' target='_blank'>MTA-STS</a> is a standard that enforces email delivery between mail servers to use TLS with valid certificates. <br>It is used when <a target='_blank' href='https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities'>DANE</a> is not possible due to missing or unsupported DNSSEC.<br><b>Note</b>: If the receiving domain supports DANE with DNSSEC, DANE is <b>always</b> preferred – MTA-STS only acts as a fallback.",
|
||||
"mta_sts_version": "Version",
|
||||
@@ -727,6 +729,7 @@
|
||||
"mta_sts_mx": "MX server",
|
||||
"mta_sts_mx_info": "Allows sending only to explicitly listed mail server hostnames; the sending MTA checks if the DNS MX hostname matches the policy list, and only allows delivery with a valid TLS certificate (guards against MITM).",
|
||||
"mta_sts_mx_notice": "Multiple MX servers can be specified (separated by commas).",
|
||||
"mta_sts_active_info": "If unchecked, the MTA-STS policy will not be published and no certificate will be requested for the mta-sts subdomain via ACME.",
|
||||
"multiple_bookings": "Multiple bookings",
|
||||
"none_inherit": "None / Inherit",
|
||||
"nexthop": "Next hop",
|
||||
@@ -767,7 +770,6 @@
|
||||
"sender_acl_info": "If mailbox user A is allowed to send as mailbox user B, the sender address is not automatically displayed as selectable \"from\" field in SOGo.<br>\r\n Mailbox user B needs to create a delegation in SOGo to allow mailbox user A to select their address as sender. To delegate a mailbox in SOGo, use the menu (three dots) to the right of your mailbox name in the upper left while in mail view. This behaviour does not apply to alias addresses.",
|
||||
"sieve_desc": "Short description",
|
||||
"sieve_type": "Filter type",
|
||||
"skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)",
|
||||
"sogo_access": "Direct forwarding to SOGo",
|
||||
"sogo_access_info": "After logging in, the user is automatically redirected to SOGo.",
|
||||
"sogo_visible": "Alias is visible in SOGo",
|
||||
@@ -776,12 +778,8 @@
|
||||
"spam_filter": "Spam filter",
|
||||
"spam_policy": "Add or remove items to allow-/denylist",
|
||||
"spam_score": "Set a custom spam score",
|
||||
"subfolder2": "Sync into subfolder on destination<br><small>(empty = do not use subfolder)</small>",
|
||||
"syncjob": "Edit sync job",
|
||||
"target_address": "Goto address/es <small>(comma-separated)</small>",
|
||||
"target_domain": "Target domain",
|
||||
"timeout1": "Timeout for connection to remote host",
|
||||
"timeout2": "Timeout for connection to local host",
|
||||
"title": "Edit object",
|
||||
"unchanged_if_empty": "If unchanged leave blank",
|
||||
"username": "Username",
|
||||
@@ -928,6 +926,7 @@
|
||||
"filters": "Filters",
|
||||
"fname": "Full name",
|
||||
"force_pw_update": "Force password update at next login",
|
||||
"force_tfa": "TFA",
|
||||
"gal": "Global Address List",
|
||||
"goto_ham": "Learn as <b>ham</b>",
|
||||
"goto_spam": "Learn as <b>spam</b>",
|
||||
@@ -1002,16 +1001,6 @@
|
||||
"spam_aliases": "Temp. alias",
|
||||
"stats": "Statistics",
|
||||
"status": "Status",
|
||||
"sync_jobs": "Sync jobs",
|
||||
"syncjob_check_log": "Check log",
|
||||
"syncjob_last_run_result": "Last run result",
|
||||
"syncjob_EX_OK": "Success",
|
||||
"syncjob_EXIT_CONNECTION_FAILURE": "Connection problem",
|
||||
"syncjob_EXIT_TLS_FAILURE": "Problem with encrypted connection",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentication problem",
|
||||
"syncjob_EXIT_OVERQUOTA": "Target mailbox is over quota",
|
||||
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Can't connect to remote server",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Wrong username or password",
|
||||
"table_size": "Table size",
|
||||
"table_size_show_n": "Show %s items",
|
||||
"target_address": "Goto address",
|
||||
@@ -1085,7 +1074,7 @@
|
||||
"rspamd_result": "Rspamd result",
|
||||
"sender": "Sender (SMTP)",
|
||||
"sender_header": "Sender (\"From\" header)",
|
||||
"settings_info": "Maximum amount of elements to be quarantined: %s<br>Maximum email size: %s MiB",
|
||||
"settings_info": "Maximum amount of elements to be quarantined (per mailbox): %s<br>Maximum email size: %s MiB",
|
||||
"show_item": "Show item",
|
||||
"spam": "Spam",
|
||||
"spam_score": "Score",
|
||||
@@ -1213,7 +1202,13 @@
|
||||
"verified_fido2_login": "Verified FIDO2 login",
|
||||
"verified_totp_login": "Verified TOTP login",
|
||||
"verified_webauthn_login": "Verified WebAuthn login",
|
||||
"verified_yotp_login": "Verified Yubico OTP login"
|
||||
"verified_yotp_login": "Verified Yubico OTP login",
|
||||
"imapsync_source_added": "Sync source %s has been added",
|
||||
"imapsync_source_modified": "Sync source %s has been saved",
|
||||
"imapsync_source_deleted": "Sync source %s has been deleted",
|
||||
"imapsync_source_token_refreshed": "OAuth token for %s has been refreshed",
|
||||
"max_parallel_saved": "Sync job settings saved",
|
||||
"imapsync_run_next_done": "%s sync job(s) moved to the front of the queue"
|
||||
},
|
||||
"tfa": {
|
||||
"authenticators": "Authenticators",
|
||||
@@ -1248,6 +1243,77 @@
|
||||
"waiting_usb_register": "<i>Waiting for USB device...</i><br><br>Please enter your password above and confirm your registration by tapping the button on your USB device.",
|
||||
"yubi_otp": "Yubico OTP authentication"
|
||||
},
|
||||
"syncjobs": {
|
||||
"sources_panel": "Sync Sources",
|
||||
"sources_hint": "Sync sources define IMAP servers (and their authentication method) that sync jobs can reuse. Define each source once, then pick it from the dropdown when creating sync jobs.",
|
||||
"source_add": "Add source",
|
||||
"source": "Source",
|
||||
"source_name": "Name",
|
||||
"source_description": "Description",
|
||||
"source_scope": "Visibility",
|
||||
"source_scope_all": "All users",
|
||||
"source_scope_domain": "Specific domains",
|
||||
"source_scope_user": "Specific users",
|
||||
"source_domains": "Domains",
|
||||
"source_users": "Users",
|
||||
"source_select": "Select source",
|
||||
"source_auth_type": "Authentication",
|
||||
"source_oauth_block": "OAuth2 configuration",
|
||||
"source_token_status": "Token valid until",
|
||||
"source_token_error": "Last refresh error",
|
||||
"source_refresh_now": "Refresh token now",
|
||||
"source_secret_unchanged_hint": "Leave blank to keep the stored secret unchanged.",
|
||||
"source_oauth_flow": "OAuth2 flow",
|
||||
"source_oauth_flow_app": "App (client credentials)",
|
||||
"source_oauth_flow_user": "User login (authorization code)",
|
||||
"source_flow_badge_app": "Client credentials",
|
||||
"source_flow_badge_user": "User login",
|
||||
"token_state_issued": "Token issued",
|
||||
"token_state_pending": "Token pending",
|
||||
"token_state_failed": "Token failed",
|
||||
"source_authorize_endpoint": "Authorize endpoint",
|
||||
"source_userinfo_endpoint": "Userinfo endpoint",
|
||||
"source_redirect_uri": "Redirect URI",
|
||||
"source_redirect_uri_hint": "Register this URI in the provider app (e.g. Azure).",
|
||||
"syncjob_oauth_connect": "Sign in with the provider",
|
||||
"syncjob_oauth_connected": "Connected",
|
||||
"sync_jobs": "Sync jobs",
|
||||
"syncjob_check_log": "Check log",
|
||||
"syncjob_last_run_result": "Last run result",
|
||||
"syncjob_EX_OK": "Success",
|
||||
"syncjob_EXIT_CONNECTION_FAILURE": "Connection problem",
|
||||
"syncjob_EXIT_TLS_FAILURE": "Problem with encrypted connection",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentication problem",
|
||||
"syncjob_EXIT_OVERQUOTA": "Target mailbox is over quota",
|
||||
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Can't connect to remote server",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Wrong username or password",
|
||||
"add": "Add sync job",
|
||||
"add_hint": "Be aware that passwords need to be saved plain-text!",
|
||||
"edit": "Edit sync job",
|
||||
"create": "Create new sync job",
|
||||
"dry": "Simulate synchronization",
|
||||
"subfolder2": "Sync into subfolder on destination<br><small>(empty = do not use subfolder)</small>",
|
||||
"mins_interval": "Polling interval (minutes)",
|
||||
"maxage": "Maximum age of messages in days that will be polled from remote<br><small>(0 = ignore age)</small>",
|
||||
"exclude": "Exclude objects (regex)",
|
||||
"automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
|
||||
"delete1": "Delete from source when completed",
|
||||
"delete2": "Delete messages on destination that are not on source",
|
||||
"delete2duplicates": "Delete duplicates on destination",
|
||||
"skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)",
|
||||
"subscribeall": "Subscribe all folders",
|
||||
"timeout1": "Timeout for connection to remote host",
|
||||
"timeout2": "Timeout for connection to local host",
|
||||
"maxbytespersecond": "Max. bytes per second <br><small>(0 = unlimited)</small>",
|
||||
"custom_params": "Custom parameters",
|
||||
"custom_params_hint": "Add one imapsync option per row. Only allowlisted options are accepted; the value may contain any character (spaces, regex, …) and is passed as a single argument.",
|
||||
"custom_param_option": "Option",
|
||||
"custom_param_value": "Value",
|
||||
"custom_param_no_value": "no value needed",
|
||||
"custom_param_add": "Add parameter",
|
||||
"prio": "Priority",
|
||||
"prio_hint": "Due jobs run least-recently-run first; on a tie the higher priority wins (0 = normal)."
|
||||
},
|
||||
"user": {
|
||||
"action": "Action",
|
||||
"active": "Active",
|
||||
@@ -1278,7 +1344,6 @@
|
||||
"clear_recent_successful_connections": "Clear seen successful connections",
|
||||
"client_configuration": "Show configuration guides for email clients and smartphones",
|
||||
"create_app_passwd": "Create app password",
|
||||
"create_syncjob": "Create new sync job",
|
||||
"created_on": "Created on",
|
||||
"daily": "Daily",
|
||||
"day": "day",
|
||||
@@ -1388,16 +1453,6 @@
|
||||
"spamfilter_wl_desc": "Allowlisted email addresses are programmed to <b>never</b> classify as spam. Wildcards may be used. A filter is only applied to direct aliases (aliases with a single target mailbox) excluding catch-all aliases and a mailbox itself.",
|
||||
"spamfilter_yellow": "Yellow: this message may be spam, will be tagged as spam and moved to your junk folder",
|
||||
"status": "Status",
|
||||
"sync_jobs": "Sync jobs",
|
||||
"syncjob_check_log": "Check log",
|
||||
"syncjob_last_run_result": "Last run result",
|
||||
"syncjob_EX_OK": "Success",
|
||||
"syncjob_EXIT_CONNECTION_FAILURE": "Connection problem",
|
||||
"syncjob_EXIT_TLS_FAILURE": "Problem with encrypted connection",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentication problem",
|
||||
"syncjob_EXIT_OVERQUOTA": "Target mailbox is over quota",
|
||||
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Can't connect to remote server",
|
||||
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Wrong username or password",
|
||||
"tag_handling": "Set handling for tagged mail",
|
||||
"tag_help_example": "Example for a tagged email address: me<b>+Facebook</b>@example.org",
|
||||
"tag_help_explain": "In subfolder: a new subfolder named after the tag will be created below INBOX (\"INBOX/Facebook\").<br>\r\nIn subject: the tags name will be prepended to the mails subject, example: \"[Facebook] My News\".",
|
||||
|
||||
@@ -111,7 +111,8 @@
|
||||
"app_passwd_protocols": "Protocoles autorisés pour le mot de passe de l'application",
|
||||
"dry": "Simuler la synchronisation",
|
||||
"internal": "Interne",
|
||||
"internal_info": "Les alias internes sont accessibles uniquement depuis le domaine ou les alias du domaine."
|
||||
"internal_info": "Les alias internes sont accessibles uniquement depuis le domaine ou les alias du domaine.",
|
||||
"sender_allowed": "Autoriser l'envoi sous cet alias"
|
||||
},
|
||||
"admin": {
|
||||
"access": "Accès",
|
||||
@@ -556,7 +557,9 @@
|
||||
"max_age_invalid": "L'âge maximum %s est invalide",
|
||||
"mode_invalid": "Le mode %s est invalide",
|
||||
"mx_invalid": "L'enregistrement MX %s est invalide",
|
||||
"version_invalid": "La version %s est invalide"
|
||||
"version_invalid": "La version %s est invalide",
|
||||
"tfa_removal_blocked": "L’authentification à deux facteurs ne peut pas être supprimée, car elle est requise pour votre compte.",
|
||||
"quarantine_category_invalid": "La catégorie de quarantaine doit être l’une des suivantes : add_header, reject, all\""
|
||||
},
|
||||
"debug": {
|
||||
"chart_this_server": "Graphique (ce serveur)",
|
||||
@@ -755,7 +758,9 @@
|
||||
"mta_sts_info": "<a href='https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security' target='_blank'>MTA-STS</a> est un standard qui oblige la délivrance des courriels entre les serveurs de courriels à utiliser TLS avec des certificats valides. <br>Il est utilisé quand <a target='_blank' href='https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities'>DANE</a> n'est pas possible à cause d'un manque ou d'un non support de DNSSEC.<br><b>Note</b> : Si le domaine du destinataire supporte DANE avec DNSSEC, DANE est <b>toujours</b> préféré – MTA-STS sert seulement en secours.",
|
||||
"mta_sts_mode_info": "Il y a trois modes parmi lesquels choisir :<ul><li><em>testing</em> – la politique est seulement surveillée, les violations n'ont pas d'impact.</li><li><em>enforce</em> – la politique est appliquée strictement, les connexions sans TLS valide sont rejetées.</li><li><em>none</em> – la politique est publiée mais non appliquée.</li></ul>",
|
||||
"mta_sts_max_age_info": "Durée en secondes pendant laquelle les serveurs de courriel peuvent mettre en cache cette politique avant de revérifier.",
|
||||
"mta_sts_mx_info": "Autoriser l'envoi uniquement aux noms d'hôtes des serveurs de courriels indiqués explicitement ; le MTA émetteur vérifie si le nom d'hôte DNS du MX correspond à la liste de la politique, et autorise la délivrance seulement avec un certificat TLS valide (protège contre le MITM)."
|
||||
"mta_sts_mx_info": "Autoriser l'envoi uniquement aux noms d'hôtes des serveurs de courriels indiqués explicitement ; le MTA émetteur vérifie si le nom d'hôte DNS du MX correspond à la liste de la politique, et autorise la délivrance seulement avec un certificat TLS valide (protège contre le MITM).",
|
||||
"sender_allowed": "Autoriser l'envoi sous cet alias",
|
||||
"sender_allowed_info": "Si cette option est désactivée, cet alias peut uniquement recevoir des courriels. Utilisez les ACL d’expéditeur pour autoriser certaines boîtes aux lettres à envoyer des messages via cet alias."
|
||||
},
|
||||
"footer": {
|
||||
"cancel": "Annuler",
|
||||
@@ -988,7 +993,8 @@
|
||||
"recipient": "Destinataire",
|
||||
"open_logs": "Afficher les journaux",
|
||||
"iam": "Fournisseur d'identité",
|
||||
"internal": "Interne"
|
||||
"internal": "Interne",
|
||||
"force_tfa": "TFA"
|
||||
},
|
||||
"oauth2": {
|
||||
"access_denied": "Veuillez vous connecter en tant que propriétaire de la boîte de réception pour accorder l’accès via Oauth2.",
|
||||
@@ -1189,7 +1195,11 @@
|
||||
"yubi_otp": "Authentification OTP Yubico",
|
||||
"authenticators": "Authentificateurs",
|
||||
"u2f_deprecated_important": "Veuillez enregistrer votre clé dans le panneau d'administration avec la nouvelle méthode WebAuthn.",
|
||||
"u2f_deprecated": "Il semble que votre clé ait été enregistrée à l'aide de la méthode U2F obsolète. Nous allons désactiver l'authentification à deux facteurs pour vous et supprimer votre clé."
|
||||
"u2f_deprecated": "Il semble que votre clé ait été enregistrée à l'aide de la méthode U2F obsolète. Nous allons désactiver l'authentification à deux facteurs pour vous et supprimer votre clé.",
|
||||
"force_tfa": "Imposer l'authentification à deux facteurs lors de la connexion",
|
||||
"force_tfa_info": "L’utilisateur doit configurer l’authentification à deux facteurs avant de pouvoir accéder à l’interface.",
|
||||
"setup_title": "Authentification à deux facteurs requise",
|
||||
"setup_required": "Votre compte nécessite l’authentification à deux facteurs. Veuillez configurer une méthode 2FA pour continuer."
|
||||
},
|
||||
"fido2": {
|
||||
"set_fn": "Définir un nom",
|
||||
@@ -1378,7 +1388,8 @@
|
||||
"forever": "Pour toujours",
|
||||
"spam_aliases_info": "Un alias de spam est une adresse de courriel temporaire qui peut être utilisée pour protéger les véritables adresses de courriel. <br> De manière optionnelle, une durée d'expiration peut être définie afin que l'alias soit automatiquement désactivé après la période définie, éliminant ainsi les adresses étant abusées ou ayant fuité.",
|
||||
"authentication": "Authentification",
|
||||
"protocols": "Protocoles"
|
||||
"protocols": "Protocoles",
|
||||
"pw_update_required": "Votre compte nécessite un changement de mot de passe. Veuillez définir un nouveau mot de passe pour continuer."
|
||||
},
|
||||
"warning": {
|
||||
"cannot_delete_self": "Impossible de supprimer l’utilisateur connecté",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user