mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-06-17 03:50:30 +00:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 816b5cdb60 | |||
| fb7bbc3fd8 | |||
| 8e93a48ac0 | |||
| 05fbd549f8 | |||
| 58a26dc966 | |||
| 9d54fbcc3e | |||
| 8063b671cb | |||
| 763ecbc93e | |||
| 97312c1a9d | |||
| baf0e7e658 | |||
| a4ca5f9363 | |||
| dcbea71e67 | |||
| 524f19ff77 | |||
| 4c184b25f4 | |||
| ddc2309f1e | |||
| bccfcefd17 | |||
| d40252ccce | |||
| 4abb5cbfab | |||
| b695936273 | |||
| d9463c7950 | |||
| 579542381a | |||
| ce5659f300 | |||
| 1967cb642f | |||
| ba0eb04ebe | |||
| 609ce6b0d6 | |||
| af61e2d303 | |||
| c6e3f517e1 | |||
| 6eaa8d43e9 | |||
| c033cd6254 | |||
| 7562578b74 | |||
| 43f570e761 | |||
| 5e478a32df | |||
| 22c94a4d2f | |||
| 99dc0f6616 | |||
| 4732f568fa | |||
| 96d4802cb2 | |||
| ad5b94af5e | |||
| 7fce984cac | |||
| 404e2f0190 | |||
| 1c52eaa3a4 | |||
| c7e04b4146 | |||
| 075959aea9 | |||
| 885ba2510e | |||
| 87563249f1 | |||
| fb1686065d | |||
| 0428f5c9bd | |||
| a70c23065c | |||
| 2e8897c2cf | |||
| 5ca900749c | |||
| b005803fe0 | |||
| ec77406dba | |||
| ee15721550 | |||
| 890295bbfc | |||
| a52e977b89 |
@@ -14,7 +14,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
||||||
uses: actions/stale@v10.1.1
|
uses: actions/stale@v10.2.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
images:
|
images:
|
||||||
- "acme-mailcow"
|
- "acme-mailcow"
|
||||||
- "clamd-mailcow"
|
- "clamd-mailcow"
|
||||||
- "controller-mailcow"
|
- "dockerapi-mailcow"
|
||||||
- "dovecot-mailcow"
|
- "dovecot-mailcow"
|
||||||
- "netfilter-mailcow"
|
- "netfilter-mailcow"
|
||||||
- "olefy-mailcow"
|
- "olefy-mailcow"
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ data/conf/sogo/cron.creds
|
|||||||
data/conf/sogo/custom-fulllogo.svg
|
data/conf/sogo/custom-fulllogo.svg
|
||||||
data/conf/sogo/custom-shortlogo.svg
|
data/conf/sogo/custom-shortlogo.svg
|
||||||
data/conf/sogo/custom-fulllogo.png
|
data/conf/sogo/custom-fulllogo.png
|
||||||
|
data/conf/acme/dns-01.conf
|
||||||
data/gitea/
|
data/gitea/
|
||||||
data/gogs/
|
data/gogs/
|
||||||
data/hooks/dovecot/*
|
data/hooks/dovecot/*
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ adapt_new_options() {
|
|||||||
"DISABLE_NETFILTER_ISOLATION_RULE"
|
"DISABLE_NETFILTER_ISOLATION_RULE"
|
||||||
"HTTP_REDIRECT"
|
"HTTP_REDIRECT"
|
||||||
"ENABLE_IPV6"
|
"ENABLE_IPV6"
|
||||||
|
"ACME_DNS_CHALLENGE"
|
||||||
|
"ACME_DNS_PROVIDER"
|
||||||
|
"ACME_ACCOUNT_EMAIL"
|
||||||
)
|
)
|
||||||
|
|
||||||
sed -i --follow-symlinks '$a\' mailcow.conf
|
sed -i --follow-symlinks '$a\' mailcow.conf
|
||||||
@@ -292,6 +295,20 @@ adapt_new_options() {
|
|||||||
echo '# This key is used to encrypt email addresses within SOGo URLs' >> mailcow.conf
|
echo '# This key is used to encrypt email addresses within SOGo URLs' >> mailcow.conf
|
||||||
echo "SOGO_URL_ENCRYPTION_KEY=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 16)" >> mailcow.conf
|
echo "SOGO_URL_ENCRYPTION_KEY=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 16)" >> mailcow.conf
|
||||||
;;
|
;;
|
||||||
|
ACME_DNS_CHALLENGE)
|
||||||
|
echo '# Enable DNS-01 challenge for ACME (acme-mailcow) - y/n' >> mailcow.conf
|
||||||
|
echo '# This requires you to set ACME_DNS_PROVIDER and ACME_ACCOUNT_EMAIL below' >> mailcow.conf
|
||||||
|
echo 'ACME_DNS_CHALLENGE=n' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
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 'ACME_DNS_PROVIDER=dns_xxx' >> mailcow.conf
|
||||||
|
;;
|
||||||
|
ACME_ACCOUNT_EMAIL)
|
||||||
|
echo '# Account email for ACME DNS-01 challenge registration' >> mailcow.conf
|
||||||
|
echo 'ACME_ACCOUNT_EMAIL=me@example.com' >> mailcow.conf
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "${option}=" >> mailcow.conf
|
echo "${option}=" >> mailcow.conf
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
@@ -14,11 +14,22 @@ RUN apk upgrade --no-cache \
|
|||||||
tini \
|
tini \
|
||||||
tzdata \
|
tzdata \
|
||||||
python3 \
|
python3 \
|
||||||
acme-tiny
|
acme-tiny \
|
||||||
|
git \
|
||||||
|
socat \
|
||||||
|
&& git clone --depth 1 https://github.com/acmesh-official/acme.sh.git /opt/acme.sh \
|
||||||
|
&& chmod +x /opt/acme.sh/acme.sh \
|
||||||
|
&& mkdir -p /var/lib/acme/acme-sh
|
||||||
|
|
||||||
|
ENV ACME_SH_BIN=/opt/acme.sh/acme.sh \
|
||||||
|
ACME_SH_HOME=/opt/acme.sh \
|
||||||
|
ACME_SH_CONFIG_HOME=/var/lib/acme/acme-sh
|
||||||
|
|
||||||
COPY acme.sh /srv/acme.sh
|
COPY acme.sh /srv/acme.sh
|
||||||
COPY functions.sh /srv/functions.sh
|
COPY functions.sh /srv/functions.sh
|
||||||
COPY obtain-certificate.sh /srv/obtain-certificate.sh
|
COPY obtain-certificate.sh /srv/obtain-certificate.sh
|
||||||
|
COPY obtain-certificate-dns.sh /srv/obtain-certificate-dns.sh
|
||||||
|
COPY load-dns-config.sh /srv/load-dns-config.sh
|
||||||
COPY reload-configurations.sh /srv/reload-configurations.sh
|
COPY reload-configurations.sh /srv/reload-configurations.sh
|
||||||
COPY expand6.sh /srv/expand6.sh
|
COPY expand6.sh /srv/expand6.sh
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,17 @@ until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
|
|||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Create DNS-01 configuration template if it doesn't exist
|
||||||
|
if [[ ! -f /etc/acme/dns-01.conf ]]; then
|
||||||
|
mkdir -p /etc/acme
|
||||||
|
cat > /etc/acme/dns-01.conf <<'EOF'
|
||||||
|
# Add here your DNS-01 challenge configuration
|
||||||
|
# For more information, visit the acme.sh documentation:
|
||||||
|
# https://github.com/acmesh-official/acme.sh/wiki/dnsapi
|
||||||
|
EOF
|
||||||
|
echo "Created DNS-01 configuration template at /etc/acme/dns-01.conf"
|
||||||
|
fi
|
||||||
|
|
||||||
source /srv/functions.sh
|
source /srv/functions.sh
|
||||||
# Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6
|
# Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6
|
||||||
source /srv/expand6.sh
|
source /srv/expand6.sh
|
||||||
@@ -42,17 +53,21 @@ if [[ "${ENABLE_SSL_SNI}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
|||||||
ENABLE_SSL_SNI=y
|
ENABLE_SSL_SNI=y
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${ACME_DNS_CHALLENGE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
|
ACME_DNS_CHALLENGE=y
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
|
log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
|
||||||
sleep 365d
|
sleep 365d
|
||||||
exec $(readlink -f "$0")
|
exec $(readlink -f "$0")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_f "Waiting for Controller .."
|
log_f "Waiting for Docker API..."
|
||||||
until ping controller -c1 > /dev/null; do
|
until ping dockerapi -c1 > /dev/null; do
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
log_f "Controller OK"
|
log_f "Docker API OK"
|
||||||
|
|
||||||
log_f "Waiting for Postfix..."
|
log_f "Waiting for Postfix..."
|
||||||
until ping postfix -c1 > /dev/null; do
|
until ping postfix -c1 > /dev/null; do
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ check_domain(){
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ ${ACME_DNS_CHALLENGE} == "y" ]]; then
|
||||||
|
log_f "ACME_DNS_CHALLENGE=y - skipping IP and HTTP validation for ${DOMAIN}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
# Check if CNAME without v6 enabled target
|
# Check if CNAME without v6 enabled target
|
||||||
if [[ ! -z ${AAAA_DOMAIN} ]] && [[ -z $(echo ${AAAA_DOMAIN} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") ]]; then
|
if [[ ! -z ${AAAA_DOMAIN} ]] && [[ -z $(echo ${AAAA_DOMAIN} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") ]]; then
|
||||||
AAAA_DOMAIN=
|
AAAA_DOMAIN=
|
||||||
|
|||||||
Executable
+57
@@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SCRIPT_SOURCE="${BASH_SOURCE[0]:-${0}}"
|
||||||
|
if [[ "${SCRIPT_SOURCE}" == "${0}" ]]; then
|
||||||
|
__dns_loader_standalone=1
|
||||||
|
else
|
||||||
|
__dns_loader_standalone=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
CONFIG_PATH="${ACME_DNS_CONFIG_FILE:-/etc/acme/dns-101.conf}"
|
||||||
|
|
||||||
|
if [[ ! -f "${CONFIG_PATH}" ]]; then
|
||||||
|
if [[ $__dns_loader_standalone -eq 1 ]]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
source /srv/functions.sh
|
||||||
|
|
||||||
|
log_f "Loading DNS-01 configuration from ${CONFIG_PATH}"
|
||||||
|
|
||||||
|
LINE_NO=0
|
||||||
|
while IFS= read -r line || [[ -n "${line}" ]]; do
|
||||||
|
LINE_NO=$((LINE_NO+1))
|
||||||
|
line="${line%$'\r'}"
|
||||||
|
line_trimmed="$(printf '%s' "${line}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||||
|
[[ -z "${line_trimmed}" ]] && continue
|
||||||
|
[[ "${line_trimmed:0:1}" == "#" ]] && continue
|
||||||
|
if [[ "${line_trimmed}" != *=* ]]; then
|
||||||
|
log_f "Skipping invalid DNS config line ${LINE_NO} (missing key=value)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
KEY="${line_trimmed%%=*}"
|
||||||
|
VALUE="${line_trimmed#*=}"
|
||||||
|
KEY="$(printf '%s' "${KEY}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||||
|
VALUE="$(printf '%s' "${VALUE}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||||
|
if [[ -z "${KEY}" ]]; then
|
||||||
|
log_f "Skipping invalid DNS config line ${LINE_NO} (empty key)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "${VALUE}" =~ ^\".*\"$ ]]; then
|
||||||
|
VALUE="${VALUE:1:-1}"
|
||||||
|
elif [[ "${VALUE}" =~ ^\'.*\'$ ]]; then
|
||||||
|
VALUE="${VALUE:1:-1}"
|
||||||
|
fi
|
||||||
|
export "${KEY}"="${VALUE}"
|
||||||
|
log_f "Exported DNS config key ${KEY}"
|
||||||
|
|
||||||
|
done < "${CONFIG_PATH}"
|
||||||
|
|
||||||
|
if [[ $__dns_loader_standalone -eq 1 ]]; then
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Return values / exit codes
|
||||||
|
# 0 = cert created successfully
|
||||||
|
# 1 = cert renewed successfully
|
||||||
|
# 2 = cert not due for renewal
|
||||||
|
# * = errors
|
||||||
|
|
||||||
|
source /srv/functions.sh
|
||||||
|
|
||||||
|
CERT_DOMAINS=(${DOMAINS[@]})
|
||||||
|
CERT_DOMAIN=${CERT_DOMAINS[0]}
|
||||||
|
ACME_BASE=/var/lib/acme
|
||||||
|
|
||||||
|
# Load optional DNS provider secrets from /etc/acme/dns-101.conf
|
||||||
|
if [[ -f /srv/load-dns-config.sh ]]; then
|
||||||
|
source /srv/load-dns-config.sh
|
||||||
|
if declare -F log_f >/dev/null; then
|
||||||
|
log_f "ACME_DNS_CHALLENGE is enabled, DNS provider secrets loaded"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
TYPE=${1}
|
||||||
|
PREFIX=""
|
||||||
|
# only support rsa certificates for now
|
||||||
|
if [[ "${TYPE}" != "rsa" ]]; then
|
||||||
|
log_f "Unknown certificate type '${TYPE}' requested"
|
||||||
|
exit 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${ACME_DNS_PROVIDER}" ]]; then
|
||||||
|
log_f "ACME_DNS_PROVIDER is required when ACME_DNS_CHALLENGE is enabled"
|
||||||
|
exit 6
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
|
||||||
|
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
|
||||||
|
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
|
||||||
|
KEY=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}key.pem
|
||||||
|
CSR=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}acme.csr
|
||||||
|
|
||||||
|
if [[ -z ${CERT_DOMAINS[*]} ]]; then
|
||||||
|
log_f "Missing CERT_DOMAINS to obtain a certificate"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${LE_STAGING}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
|
if [[ ! -z "${DIRECTORY_URL}" ]]; then
|
||||||
|
log_f "Cannot use DIRECTORY_URL with LE_STAGING=y - ignoring DIRECTORY_URL"
|
||||||
|
fi
|
||||||
|
log_f "Using Let's Encrypt staging servers"
|
||||||
|
ACME_SH_SERVER_ARGS=("--staging")
|
||||||
|
elif [[ ! -z "${DIRECTORY_URL}" ]]; then
|
||||||
|
log_f "Using custom directory URL ${DIRECTORY_URL}"
|
||||||
|
ACME_SH_SERVER_ARGS=("--server" "${DIRECTORY_URL}")
|
||||||
|
else
|
||||||
|
log_f "Using Let's Encrypt production servers"
|
||||||
|
ACME_SH_SERVER_ARGS=("--server" "letsencrypt")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f ${DOMAINS_FILE} && "$(cat ${DOMAINS_FILE})" == "${CERT_DOMAINS[*]}" ]]; then
|
||||||
|
if [[ ! -f ${CERT} || ! -f "${KEY}" || -f "${ACME_BASE}/force_renew" ]]; then
|
||||||
|
log_f "Certificate ${CERT} doesn't exist yet or forced renewal - start obtaining"
|
||||||
|
elif ! openssl x509 -checkend 2592000 -noout -in ${CERT} > /dev/null; then
|
||||||
|
log_f "Certificate ${CERT} is due for renewal (< 30 days) - start renewing"
|
||||||
|
else
|
||||||
|
log_f "Certificate ${CERT} validation done, neither changed nor due for renewal."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log_f "Certificate ${CERT} missing or changed domains '${CERT_DOMAINS[*]}' - start obtaining"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make backup
|
||||||
|
if [[ -f ${CERT} ]]; then
|
||||||
|
DATE=$(date +%Y-%m-%d_%H_%M_%S)
|
||||||
|
BACKUP_DIR=${ACME_BASE}/backups/${CERT_DOMAIN}/${PREFIX}${DATE}
|
||||||
|
log_f "Creating backups in ${BACKUP_DIR} ..."
|
||||||
|
mkdir -p ${BACKUP_DIR}/
|
||||||
|
[[ -f ${DOMAINS_FILE} ]] && cp ${DOMAINS_FILE} ${BACKUP_DIR}/
|
||||||
|
[[ -f ${CERT} ]] && cp ${CERT} ${BACKUP_DIR}/
|
||||||
|
[[ -f ${KEY} ]] && cp ${KEY} ${BACKUP_DIR}/
|
||||||
|
[[ -f ${CSR} ]] && cp ${CSR} ${BACKUP_DIR}/
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p ${ACME_BASE}/${CERT_DOMAIN}
|
||||||
|
if [[ ! -f ${KEY} ]]; then
|
||||||
|
log_f "Copying shared private key for this certificate..."
|
||||||
|
cp ${SHARED_KEY} ${KEY}
|
||||||
|
chmod 600 ${KEY}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Generating CSR to keep layout parity with HTTP challenge flow
|
||||||
|
printf "[SAN]\nsubjectAltName=" > /tmp/_SAN
|
||||||
|
printf "DNS:%s," "${CERT_DOMAINS[@]}" >> /tmp/_SAN
|
||||||
|
sed -i '$s/,$//' /tmp/_SAN
|
||||||
|
openssl req -new -sha256 -key ${KEY} -subj "/" -reqexts SAN -config <(cat "$(openssl version -d | sed 's/.*\"\(.*\)\"/\1/g')/openssl.cnf" /tmp/_SAN) > ${CSR}
|
||||||
|
|
||||||
|
log_f "Checking resolver..."
|
||||||
|
until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
log_f "Resolver OK"
|
||||||
|
|
||||||
|
ACME_SH_BIN_PATH=${ACME_SH_BIN:-/opt/acme.sh/acme.sh}
|
||||||
|
ACME_SH_WORK_HOME=${ACME_SH_CONFIG_HOME:-/var/lib/acme/acme-sh}
|
||||||
|
mkdir -p ${ACME_SH_WORK_HOME}
|
||||||
|
|
||||||
|
if [[ ! -x ${ACME_SH_BIN_PATH} ]]; then
|
||||||
|
log_f "acme.sh binary not found at ${ACME_SH_BIN_PATH}"
|
||||||
|
exit 7
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f ${ACME_SH_WORK_HOME}/account.conf ]]; then
|
||||||
|
if [[ -z "${ACME_ACCOUNT_EMAIL}" ]]; then
|
||||||
|
log_f "ACME_ACCOUNT_EMAIL is required to register a new acme.sh account"
|
||||||
|
exit 8
|
||||||
|
fi
|
||||||
|
log_f "Registering acme.sh account for ${ACME_ACCOUNT_EMAIL}"
|
||||||
|
REGISTER_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}" "--register-account" "-m" "${ACME_ACCOUNT_EMAIL}")
|
||||||
|
REGISTER_CMD+=("${ACME_SH_SERVER_ARGS[@]}")
|
||||||
|
REGISTER_RESPONSE=$("${REGISTER_CMD[@]}" 2>&1)
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
log_f "Failed to register acme.sh account: ${REGISTER_RESPONSE}"
|
||||||
|
exit 9
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_CERT=$(mktemp /tmp/acme-cert.XXXXXX)
|
||||||
|
TMP_FULLCHAIN=$(mktemp /tmp/acme-fullchain.XXXXXX)
|
||||||
|
|
||||||
|
ACME_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}")
|
||||||
|
ACME_CMD+=("${ACME_SH_SERVER_ARGS[@]}")
|
||||||
|
ACME_CMD+=("--issue" "--dns" "${ACME_DNS_PROVIDER}" "--key-file" "${KEY}" "--cert-file" "${TMP_CERT}" "--fullchain-file" "${TMP_FULLCHAIN}" "--force")
|
||||||
|
for domain in "${CERT_DOMAINS[@]}"; do
|
||||||
|
ACME_CMD+=("-d" "${domain}")
|
||||||
|
done
|
||||||
|
|
||||||
|
log_f "Using command ${ACME_CMD[*]}"
|
||||||
|
if [[ -n "${ACME_DNS_PROVIDER}" ]]; then
|
||||||
|
log_f "DNS provider: ${ACME_DNS_PROVIDER}"
|
||||||
|
fi
|
||||||
|
if compgen -A variable | grep -Eq "^DNS_|^ACME_"; then
|
||||||
|
LOG_KEYS=$(env | grep -E "^(DNS_|ACME_)" | cut -d= -f1 | tr '\n' ' ')
|
||||||
|
log_f "Available DNS/ACME env keys: ${LOG_KEYS}" redis_only
|
||||||
|
fi
|
||||||
|
ACME_RESPONSE=$("${ACME_CMD[@]}" 2>&1 | tee /dev/fd/5; exit ${PIPESTATUS[0]})
|
||||||
|
SUCCESS="$?"
|
||||||
|
ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
|
||||||
|
log_f "${ACME_RESPONSE_B64}" redis_only b64
|
||||||
|
|
||||||
|
case "$SUCCESS" in
|
||||||
|
0)
|
||||||
|
log_f "Deploying certificate ${CERT}..."
|
||||||
|
if verify_hash_match ${TMP_FULLCHAIN} ${KEY}; then
|
||||||
|
RETURN=0
|
||||||
|
if [[ -f ${CERT} ]]; then
|
||||||
|
RETURN=1
|
||||||
|
fi
|
||||||
|
mv -f ${TMP_FULLCHAIN} ${CERT}
|
||||||
|
rm -f ${TMP_CERT}
|
||||||
|
echo -n ${CERT_DOMAINS[*]} > ${DOMAINS_FILE}
|
||||||
|
log_f "Certificate successfully obtained via DNS challenge"
|
||||||
|
exit ${RETURN}
|
||||||
|
else
|
||||||
|
log_f "Certificate was requested, but key and certificate hashes do not match"
|
||||||
|
rm -f ${TMP_CERT} ${TMP_FULLCHAIN}
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}' via DNS challenge"
|
||||||
|
redis-cli -h redis -a ${REDISPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)"
|
||||||
|
rm -f ${TMP_CERT} ${TMP_FULLCHAIN}
|
||||||
|
exit 100${SUCCESS}
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -20,6 +20,10 @@ if [[ "${TYPE}" != "rsa" ]]; then
|
|||||||
log_f "Unknown certificate type '${TYPE}' requested"
|
log_f "Unknown certificate type '${TYPE}' requested"
|
||||||
exit 5
|
exit 5
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${ACME_DNS_CHALLENGE}" == "y" ]]; then
|
||||||
|
exec /srv/obtain-certificate-dns.sh "$@"
|
||||||
|
fi
|
||||||
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
|
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
|
||||||
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
|
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
|
||||||
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
|
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
|
||||||
|
|||||||
@@ -2,32 +2,32 @@
|
|||||||
|
|
||||||
# Reading container IDs
|
# Reading container IDs
|
||||||
# Wrapping as array to ensure trimmed content when calling $NGINX etc.
|
# Wrapping as array to ensure trimmed content when calling $NGINX etc.
|
||||||
NGINX=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"nginx-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
|
NGINX=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"nginx-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
|
||||||
DOVECOT=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"dovecot-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
|
DOVECOT=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"dovecot-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
|
||||||
POSTFIX=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
|
POSTFIX=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
|
||||||
|
|
||||||
reload_nginx(){
|
reload_nginx(){
|
||||||
echo "Reloading Nginx..."
|
echo "Reloading Nginx..."
|
||||||
NGINX_RELOAD_RET=$(curl -X POST --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${NGINX}/exec -d '{"cmd":"reload", "task":"nginx"}' --silent -H 'Content-type: application/json' | jq -r .type)
|
NGINX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${NGINX}/exec -d '{"cmd":"reload", "task":"nginx"}' --silent -H 'Content-type: application/json' | jq -r .type)
|
||||||
[[ ${NGINX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Nginx, restarting container..."; restart_container ${NGINX} ; }
|
[[ ${NGINX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Nginx, restarting container..."; restart_container ${NGINX} ; }
|
||||||
}
|
}
|
||||||
|
|
||||||
reload_dovecot(){
|
reload_dovecot(){
|
||||||
echo "Reloading Dovecot..."
|
echo "Reloading Dovecot..."
|
||||||
DOVECOT_RELOAD_RET=$(curl -X POST --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${DOVECOT}/exec -d '{"cmd":"reload", "task":"dovecot"}' --silent -H 'Content-type: application/json' | jq -r .type)
|
DOVECOT_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${DOVECOT}/exec -d '{"cmd":"reload", "task":"dovecot"}' --silent -H 'Content-type: application/json' | jq -r .type)
|
||||||
[[ ${DOVECOT_RELOAD_RET} != 'success' ]] && { echo "Could not reload Dovecot, restarting container..."; restart_container ${DOVECOT} ; }
|
[[ ${DOVECOT_RELOAD_RET} != 'success' ]] && { echo "Could not reload Dovecot, restarting container..."; restart_container ${DOVECOT} ; }
|
||||||
}
|
}
|
||||||
|
|
||||||
reload_postfix(){
|
reload_postfix(){
|
||||||
echo "Reloading Postfix..."
|
echo "Reloading Postfix..."
|
||||||
POSTFIX_RELOAD_RET=$(curl -X POST --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/exec -d '{"cmd":"reload", "task":"postfix"}' --silent -H 'Content-type: application/json' | jq -r .type)
|
POSTFIX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/exec -d '{"cmd":"reload", "task":"postfix"}' --silent -H 'Content-type: application/json' | jq -r .type)
|
||||||
[[ ${POSTFIX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Postfix, restarting container..."; restart_container ${POSTFIX} ; }
|
[[ ${POSTFIX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Postfix, restarting container..."; restart_container ${POSTFIX} ; }
|
||||||
}
|
}
|
||||||
|
|
||||||
restart_container(){
|
restart_container(){
|
||||||
for container in $*; do
|
for container in $*; do
|
||||||
echo "Restarting ${container}..."
|
echo "Restarting ${container}..."
|
||||||
C_REST_OUT=$(curl -X POST --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${container}/restart --silent | jq -r '.msg')
|
C_REST_OUT=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${container}/restart --silent | jq -r '.msg')
|
||||||
echo "${C_REST_OUT}"
|
echo "${C_REST_OUT}"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
`openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
|
|
||||||
-keyout /app/controller_key.pem \
|
|
||||||
-out /app/controller_cert.pem \
|
|
||||||
-subj /CN=controller/O=mailcow \
|
|
||||||
-addext subjectAltName=DNS:controller`
|
|
||||||
|
|
||||||
exec "$@"
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from models.AliasModel import AliasModel
|
|
||||||
from models.MailboxModel import MailboxModel
|
|
||||||
from models.SyncjobModel import SyncjobModel
|
|
||||||
from models.CalendarModel import CalendarModel
|
|
||||||
from models.MailerModel import MailerModel
|
|
||||||
from models.AddressbookModel import AddressbookModel
|
|
||||||
from models.MaildirModel import MaildirModel
|
|
||||||
from models.DomainModel import DomainModel
|
|
||||||
from models.DomainadminModel import DomainadminModel
|
|
||||||
from models.StatusModel import StatusModel
|
|
||||||
|
|
||||||
from modules.Utils import Utils
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
utils = Utils()
|
|
||||||
|
|
||||||
model_map = {
|
|
||||||
MailboxModel.parser_command: MailboxModel,
|
|
||||||
AliasModel.parser_command: AliasModel,
|
|
||||||
SyncjobModel.parser_command: SyncjobModel,
|
|
||||||
CalendarModel.parser_command: CalendarModel,
|
|
||||||
AddressbookModel.parser_command: AddressbookModel,
|
|
||||||
MailerModel.parser_command: MailerModel,
|
|
||||||
MaildirModel.parser_command: MaildirModel,
|
|
||||||
DomainModel.parser_command: DomainModel,
|
|
||||||
DomainadminModel.parser_command: DomainadminModel,
|
|
||||||
StatusModel.parser_command: StatusModel
|
|
||||||
}
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="mailcow Admin Tool")
|
|
||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
||||||
|
|
||||||
for model in model_map.values():
|
|
||||||
model.add_parser(subparsers)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
for cmd, model_cls in model_map.items():
|
|
||||||
if args.command == cmd and model_cls.has_required_args(args):
|
|
||||||
instance = model_cls(**vars(args))
|
|
||||||
action = getattr(instance, args.object, None)
|
|
||||||
if callable(action):
|
|
||||||
res = action()
|
|
||||||
utils.pprint(res)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
parser.print_help()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
from modules.Sogo import Sogo
|
|
||||||
from models.BaseModel import BaseModel
|
|
||||||
|
|
||||||
class AddressbookModel(BaseModel):
|
|
||||||
parser_command = "addressbook"
|
|
||||||
required_args = {
|
|
||||||
"add": [["username", "name"]],
|
|
||||||
"delete": [["username", "name"]],
|
|
||||||
"get": [["username", "name"]],
|
|
||||||
"set_acl": [["username", "name", "sharee_email", "acl"]],
|
|
||||||
"get_acl": [["username", "name"]],
|
|
||||||
"delete_acl": [["username", "name", "sharee_email"]],
|
|
||||||
"add_contact": [["username", "name", "contact_name", "contact_email", "type"]],
|
|
||||||
"delete_contact": [["username", "name", "contact_name"]],
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
username=None,
|
|
||||||
name=None,
|
|
||||||
sharee_email=None,
|
|
||||||
acl=None,
|
|
||||||
subscribe=None,
|
|
||||||
ics=None,
|
|
||||||
contact_name=None,
|
|
||||||
contact_email=None,
|
|
||||||
type=None,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
self.sogo = Sogo(username)
|
|
||||||
|
|
||||||
self.name = name
|
|
||||||
self.acl = acl
|
|
||||||
self.sharee_email = sharee_email
|
|
||||||
self.subscribe = subscribe
|
|
||||||
self.ics = ics
|
|
||||||
self.contact_name = contact_name
|
|
||||||
self.contact_email = contact_email
|
|
||||||
self.type = type
|
|
||||||
|
|
||||||
def add(self):
|
|
||||||
"""
|
|
||||||
Add a new addressbook.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
return self.sogo.addAddressbook(self.name)
|
|
||||||
|
|
||||||
def set_acl(self):
|
|
||||||
"""
|
|
||||||
Set ACL for the addressbook.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
addressbook_id = self.sogo.getAddressbookIdByName(self.name)
|
|
||||||
if not addressbook_id:
|
|
||||||
print(f"Addressbook '{self.name}' not found for user '{self.username}'.")
|
|
||||||
return None
|
|
||||||
return self.sogo.setAddressbookACL(addressbook_id, self.sharee_email, self.acl, self.subscribe)
|
|
||||||
|
|
||||||
def delete_acl(self):
|
|
||||||
"""
|
|
||||||
Delete the addressbook ACL.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
addressbook_id = self.sogo.getAddressbookIdByName(self.name)
|
|
||||||
if not addressbook_id:
|
|
||||||
print(f"Addressbook '{self.name}' not found for user '{self.username}'.")
|
|
||||||
return None
|
|
||||||
return self.sogo.deleteAddressbookACL(addressbook_id, self.sharee_email)
|
|
||||||
|
|
||||||
def get_acl(self):
|
|
||||||
"""
|
|
||||||
Get the ACL for the addressbook.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
addressbook_id = self.sogo.getAddressbookIdByName(self.name)
|
|
||||||
if not addressbook_id:
|
|
||||||
print(f"Addressbook '{self.name}' not found for user '{self.username}'.")
|
|
||||||
return None
|
|
||||||
return self.sogo.getAddressbookACL(addressbook_id)
|
|
||||||
|
|
||||||
def add_contact(self):
|
|
||||||
"""
|
|
||||||
Add a new contact to the addressbook.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
addressbook_id = self.sogo.getAddressbookIdByName(self.name)
|
|
||||||
if not addressbook_id:
|
|
||||||
print(f"Addressbook '{self.name}' not found for user '{self.username}'.")
|
|
||||||
return None
|
|
||||||
if self.type == "card":
|
|
||||||
return self.sogo.addAddressbookContact(addressbook_id, self.contact_name, self.contact_email)
|
|
||||||
elif self.type == "list":
|
|
||||||
return self.sogo.addAddressbookContactList(addressbook_id, self.contact_name, self.contact_email)
|
|
||||||
|
|
||||||
def delete_contact(self):
|
|
||||||
"""
|
|
||||||
Delete a contact or contactlist from the addressbook.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
addressbook_id = self.sogo.getAddressbookIdByName(self.name)
|
|
||||||
if not addressbook_id:
|
|
||||||
print(f"Addressbook '{self.name}' not found for user '{self.username}'.")
|
|
||||||
return None
|
|
||||||
return self.sogo.deleteAddressbookItem(addressbook_id, self.contact_name)
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""
|
|
||||||
Retrieve addressbooks list.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
return self.sogo.getAddressbookList()
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
"""
|
|
||||||
Delete the addressbook.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
addressbook_id = self.sogo.getAddressbookIdByName(self.name)
|
|
||||||
if not addressbook_id:
|
|
||||||
print(f"Addressbook '{self.name}' not found for user '{self.username}'.")
|
|
||||||
return None
|
|
||||||
return self.sogo.deleteAddressbook(addressbook_id)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_parser(cls, subparsers):
|
|
||||||
parser = subparsers.add_parser(
|
|
||||||
cls.parser_command,
|
|
||||||
help="Manage addressbooks (add, delete, get, set_acl, get_acl, delete_acl, add_contact, delete_contact)"
|
|
||||||
)
|
|
||||||
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, set_acl, get_acl, delete_acl, add_contact, delete_contact")
|
|
||||||
parser.add_argument("--username", required=True, help="Username of the addressbook owner (e.g. user@example.com)")
|
|
||||||
parser.add_argument("--name", help="Addressbook name")
|
|
||||||
parser.add_argument("--sharee-email", help="Email address to share the addressbook with")
|
|
||||||
parser.add_argument("--acl", help="ACL rights for the sharee (e.g. r, w, rw)")
|
|
||||||
parser.add_argument("--subscribe", action='store_true', help="Subscribe the sharee to the addressbook")
|
|
||||||
parser.add_argument("--contact-name", help="Name of the contact or contactlist to add or delete")
|
|
||||||
parser.add_argument("--contact-email", help="Email address of the contact to add")
|
|
||||||
parser.add_argument("--type", choices=["card", "list"], help="Type of contact to add: card (single contact) or list (distribution list)")
|
|
||||||
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
from modules.Mailcow import Mailcow
|
|
||||||
from models.BaseModel import BaseModel
|
|
||||||
|
|
||||||
class AliasModel(BaseModel):
|
|
||||||
parser_command = "alias"
|
|
||||||
required_args = {
|
|
||||||
"add": [["address", "goto"]],
|
|
||||||
"delete": [["id"]],
|
|
||||||
"get": [["id"]],
|
|
||||||
"edit": [["id"]]
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
id=None,
|
|
||||||
address=None,
|
|
||||||
goto=None,
|
|
||||||
active=None,
|
|
||||||
sogo_visible=None,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
self.mailcow = Mailcow()
|
|
||||||
|
|
||||||
self.id = id
|
|
||||||
self.address = address
|
|
||||||
self.goto = goto
|
|
||||||
self.active = active
|
|
||||||
self.sogo_visible = sogo_visible
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data):
|
|
||||||
return cls(
|
|
||||||
address=data.get("address"),
|
|
||||||
goto=data.get("goto"),
|
|
||||||
active=data.get("active", None),
|
|
||||||
sogo_visible=data.get("sogo_visible", None)
|
|
||||||
)
|
|
||||||
|
|
||||||
def getAdd(self):
|
|
||||||
"""
|
|
||||||
Get the alias details as a dictionary for adding, sets default values.
|
|
||||||
:return: Dictionary containing alias details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
alias = {
|
|
||||||
"address": self.address,
|
|
||||||
"goto": self.goto,
|
|
||||||
"active": self.active if self.active is not None else 1,
|
|
||||||
"sogo_visible": self.sogo_visible if self.sogo_visible is not None else 0
|
|
||||||
}
|
|
||||||
return {key: value for key, value in alias.items() if value is not None}
|
|
||||||
|
|
||||||
def getEdit(self):
|
|
||||||
"""
|
|
||||||
Get the alias details as a dictionary for editing, sets no default values.
|
|
||||||
:return: Dictionary containing mailbox details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
alias = {
|
|
||||||
"address": self.address,
|
|
||||||
"goto": self.goto,
|
|
||||||
"active": self.active,
|
|
||||||
"sogo_visible": self.sogo_visible
|
|
||||||
}
|
|
||||||
return {key: value for key, value in alias.items() if value is not None}
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""
|
|
||||||
Get the mailbox details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.getAlias(self.id)
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
"""
|
|
||||||
Get the mailbox details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.deleteAlias(self.id)
|
|
||||||
|
|
||||||
def add(self):
|
|
||||||
"""
|
|
||||||
Get the mailbox details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.addAlias(self.getAdd())
|
|
||||||
|
|
||||||
def edit(self):
|
|
||||||
"""
|
|
||||||
Get the mailbox details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.editAlias(self.id, self.getEdit())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_parser(cls, subparsers):
|
|
||||||
parser = subparsers.add_parser(
|
|
||||||
cls.parser_command,
|
|
||||||
help="Manage aliases (add, delete, get, edit)"
|
|
||||||
)
|
|
||||||
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit")
|
|
||||||
parser.add_argument("--id", help="Alias object ID (required for get, edit, delete)")
|
|
||||||
parser.add_argument("--address", help="Alias email address (e.g. alias@example.com)")
|
|
||||||
parser.add_argument("--goto", help="Destination address(es), comma-separated (e.g. user1@example.com,user2@example.com)")
|
|
||||||
parser.add_argument("--active", choices=["1", "0"], help="Activate (1) or deactivate (0) the alias")
|
|
||||||
parser.add_argument("--sogo-visible", choices=["1", "0"], help="Show alias in SOGo addressbook (1 = yes, 0 = no)")
|
|
||||||
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
class BaseModel:
|
|
||||||
parser_command = ""
|
|
||||||
required_args = {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def has_required_args(cls, args):
|
|
||||||
"""
|
|
||||||
Validate that all required arguments are present.
|
|
||||||
"""
|
|
||||||
object_name = args.object if hasattr(args, "object") else args.get("object")
|
|
||||||
required_lists = cls.required_args.get(object_name, False)
|
|
||||||
|
|
||||||
if not required_lists:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for required_set in required_lists:
|
|
||||||
result = True
|
|
||||||
for required_args in required_set:
|
|
||||||
if isinstance(args, dict):
|
|
||||||
if not args.get(required_args):
|
|
||||||
result = False
|
|
||||||
break
|
|
||||||
elif not hasattr(args, required_args):
|
|
||||||
result = False
|
|
||||||
break
|
|
||||||
if result:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
print(f"Required arguments for '{object_name}': {required_lists}")
|
|
||||||
return result
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_parser(cls, subparsers):
|
|
||||||
pass
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
from modules.Sogo import Sogo
|
|
||||||
from models.BaseModel import BaseModel
|
|
||||||
|
|
||||||
class CalendarModel(BaseModel):
|
|
||||||
parser_command = "calendar"
|
|
||||||
required_args = {
|
|
||||||
"add": [["username", "name"]],
|
|
||||||
"delete": [["username", "name"]],
|
|
||||||
"get": [["username"]],
|
|
||||||
"import_ics": [["username", "name", "ics"]],
|
|
||||||
"set_acl": [["username", "name", "sharee_email", "acl"]],
|
|
||||||
"get_acl": [["username", "name"]],
|
|
||||||
"delete_acl": [["username", "name", "sharee_email"]],
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
username=None,
|
|
||||||
name=None,
|
|
||||||
sharee_email=None,
|
|
||||||
acl=None,
|
|
||||||
subscribe=None,
|
|
||||||
ics=None,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
self.sogo = Sogo(username)
|
|
||||||
|
|
||||||
self.name = name
|
|
||||||
self.acl = acl
|
|
||||||
self.sharee_email = sharee_email
|
|
||||||
self.subscribe = subscribe
|
|
||||||
self.ics = ics
|
|
||||||
|
|
||||||
def add(self):
|
|
||||||
"""
|
|
||||||
Add a new calendar.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
return self.sogo.addCalendar(self.name)
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
"""
|
|
||||||
Delete a calendar.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
calendar_id = self.sogo.getCalendarIdByName(self.name)
|
|
||||||
if not calendar_id:
|
|
||||||
print(f"Calendar '{self.name}' not found for user '{self.username}'.")
|
|
||||||
return None
|
|
||||||
return self.sogo.deleteCalendar(calendar_id)
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""
|
|
||||||
Get the calendar details.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
return self.sogo.getCalendar()
|
|
||||||
|
|
||||||
def set_acl(self):
|
|
||||||
"""
|
|
||||||
Set ACL for the calendar.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
calendar_id = self.sogo.getCalendarIdByName(self.name)
|
|
||||||
if not calendar_id:
|
|
||||||
print(f"Calendar '{self.name}' not found for user '{self.username}'.")
|
|
||||||
return None
|
|
||||||
return self.sogo.setCalendarACL(calendar_id, self.sharee_email, self.acl, self.subscribe)
|
|
||||||
|
|
||||||
def delete_acl(self):
|
|
||||||
"""
|
|
||||||
Delete the calendar ACL.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
calendar_id = self.sogo.getCalendarIdByName(self.name)
|
|
||||||
if not calendar_id:
|
|
||||||
print(f"Calendar '{self.name}' not found for user '{self.username}'.")
|
|
||||||
return None
|
|
||||||
return self.sogo.deleteCalendarACL(calendar_id, self.sharee_email)
|
|
||||||
|
|
||||||
def get_acl(self):
|
|
||||||
"""
|
|
||||||
Get the ACL for the calendar.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
calendar_id = self.sogo.getCalendarIdByName(self.name)
|
|
||||||
if not calendar_id:
|
|
||||||
print(f"Calendar '{self.name}' not found for user '{self.username}'.")
|
|
||||||
return None
|
|
||||||
return self.sogo.getCalendarACL(calendar_id)
|
|
||||||
|
|
||||||
def import_ics(self):
|
|
||||||
"""
|
|
||||||
Import a calendar from an ICS file.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
return self.sogo.importCalendar(self.name, self.ics)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_parser(cls, subparsers):
|
|
||||||
parser = subparsers.add_parser(
|
|
||||||
cls.parser_command,
|
|
||||||
help="Manage calendars (add, delete, get, import_ics, set_acl, get_acl, delete_acl)"
|
|
||||||
)
|
|
||||||
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, import_ics, set_acl, get_acl, delete_acl")
|
|
||||||
parser.add_argument("--username", required=True, help="Username of the calendar owner (e.g. user@example.com)")
|
|
||||||
parser.add_argument("--name", help="Calendar name")
|
|
||||||
parser.add_argument("--ics", help="Path to ICS file for import")
|
|
||||||
parser.add_argument("--sharee-email", help="Email address to share the calendar with")
|
|
||||||
parser.add_argument("--acl", help="ACL rights for the sharee (e.g. r, w, rw)")
|
|
||||||
parser.add_argument("--subscribe", action='store_true', help="Subscribe the sharee to the calendar")
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
from modules.Mailcow import Mailcow
|
|
||||||
from models.BaseModel import BaseModel
|
|
||||||
|
|
||||||
class DomainModel(BaseModel):
|
|
||||||
parser_command = "domain"
|
|
||||||
required_args = {
|
|
||||||
"add": [["domain"]],
|
|
||||||
"delete": [["domain"]],
|
|
||||||
"get": [["domain"]],
|
|
||||||
"edit": [["domain"]]
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
domain=None,
|
|
||||||
active=None,
|
|
||||||
aliases=None,
|
|
||||||
backupmx=None,
|
|
||||||
defquota=None,
|
|
||||||
description=None,
|
|
||||||
mailboxes=None,
|
|
||||||
maxquota=None,
|
|
||||||
quota=None,
|
|
||||||
relay_all_recipients=None,
|
|
||||||
rl_frame=None,
|
|
||||||
rl_value=None,
|
|
||||||
restart_sogo=None,
|
|
||||||
tags=None,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
self.mailcow = Mailcow()
|
|
||||||
|
|
||||||
self.domain = domain
|
|
||||||
self.active = active
|
|
||||||
self.aliases = aliases
|
|
||||||
self.backupmx = backupmx
|
|
||||||
self.defquota = defquota
|
|
||||||
self.description = description
|
|
||||||
self.mailboxes = mailboxes
|
|
||||||
self.maxquota = maxquota
|
|
||||||
self.quota = quota
|
|
||||||
self.relay_all_recipients = relay_all_recipients
|
|
||||||
self.rl_frame = rl_frame
|
|
||||||
self.rl_value = rl_value
|
|
||||||
self.restart_sogo = restart_sogo
|
|
||||||
self.tags = tags
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data):
|
|
||||||
return cls(
|
|
||||||
domain=data.get("domain"),
|
|
||||||
active=data.get("active", None),
|
|
||||||
aliases=data.get("aliases", None),
|
|
||||||
backupmx=data.get("backupmx", None),
|
|
||||||
defquota=data.get("defquota", None),
|
|
||||||
description=data.get("description", None),
|
|
||||||
mailboxes=data.get("mailboxes", None),
|
|
||||||
maxquota=data.get("maxquota", None),
|
|
||||||
quota=data.get("quota", None),
|
|
||||||
relay_all_recipients=data.get("relay_all_recipients", None),
|
|
||||||
rl_frame=data.get("rl_frame", None),
|
|
||||||
rl_value=data.get("rl_value", None),
|
|
||||||
restart_sogo=data.get("restart_sogo", None),
|
|
||||||
tags=data.get("tags", None)
|
|
||||||
)
|
|
||||||
|
|
||||||
def getAdd(self):
|
|
||||||
"""
|
|
||||||
Get the domain details as a dictionary for adding, sets default values.
|
|
||||||
:return: Dictionary containing domain details.
|
|
||||||
"""
|
|
||||||
domain = {
|
|
||||||
"domain": self.domain,
|
|
||||||
"active": self.active if self.active is not None else 1,
|
|
||||||
"aliases": self.aliases if self.aliases is not None else 400,
|
|
||||||
"backupmx": self.backupmx if self.backupmx is not None else 0,
|
|
||||||
"defquota": self.defquota if self.defquota is not None else 3072,
|
|
||||||
"description": self.description if self.description is not None else "",
|
|
||||||
"mailboxes": self.mailboxes if self.mailboxes is not None else 10,
|
|
||||||
"maxquota": self.maxquota if self.maxquota is not None else 10240,
|
|
||||||
"quota": self.quota if self.quota is not None else 10240,
|
|
||||||
"relay_all_recipients": self.relay_all_recipients if self.relay_all_recipients is not None else 0,
|
|
||||||
"rl_frame": self.rl_frame,
|
|
||||||
"rl_value": self.rl_value,
|
|
||||||
"restart_sogo": self.restart_sogo if self.restart_sogo is not None else 0,
|
|
||||||
"tags": self.tags if self.tags is not None else []
|
|
||||||
}
|
|
||||||
return {key: value for key, value in domain.items() if value is not None}
|
|
||||||
|
|
||||||
def getEdit(self):
|
|
||||||
"""
|
|
||||||
Get the domain details as a dictionary for editing, sets no default values.
|
|
||||||
:return: Dictionary containing domain details.
|
|
||||||
"""
|
|
||||||
domain = {
|
|
||||||
"domain": self.domain,
|
|
||||||
"active": self.active,
|
|
||||||
"aliases": self.aliases,
|
|
||||||
"backupmx": self.backupmx,
|
|
||||||
"defquota": self.defquota,
|
|
||||||
"description": self.description,
|
|
||||||
"mailboxes": self.mailboxes,
|
|
||||||
"maxquota": self.maxquota,
|
|
||||||
"quota": self.quota,
|
|
||||||
"relay_all_recipients": self.relay_all_recipients,
|
|
||||||
"rl_frame": self.rl_frame,
|
|
||||||
"rl_value": self.rl_value,
|
|
||||||
"restart_sogo": self.restart_sogo,
|
|
||||||
"tags": self.tags
|
|
||||||
}
|
|
||||||
return {key: value for key, value in domain.items() if value is not None}
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""
|
|
||||||
Get the domain details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.getDomain(self.domain)
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
"""
|
|
||||||
Delete the domain from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.deleteDomain(self.domain)
|
|
||||||
|
|
||||||
def add(self):
|
|
||||||
"""
|
|
||||||
Add the domain to the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.addDomain(self.getAdd())
|
|
||||||
|
|
||||||
def edit(self):
|
|
||||||
"""
|
|
||||||
Edit the domain in the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.editDomain(self.domain, self.getEdit())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_parser(cls, subparsers):
|
|
||||||
parser = subparsers.add_parser(
|
|
||||||
cls.parser_command,
|
|
||||||
help="Manage domains (add, delete, get, edit)"
|
|
||||||
)
|
|
||||||
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit")
|
|
||||||
parser.add_argument("--domain", required=True, help="Domain name (e.g. domain.tld)")
|
|
||||||
parser.add_argument("--active", choices=["1", "0"], help="Activate (1) or deactivate (0) the domain")
|
|
||||||
parser.add_argument("--aliases", help="Number of aliases allowed for the domain")
|
|
||||||
parser.add_argument("--backupmx", choices=["1", "0"], help="Enable (1) or disable (0) backup MX")
|
|
||||||
parser.add_argument("--defquota", help="Default quota for mailboxes in MB")
|
|
||||||
parser.add_argument("--description", help="Description of the domain")
|
|
||||||
parser.add_argument("--mailboxes", help="Number of mailboxes allowed for the domain")
|
|
||||||
parser.add_argument("--maxquota", help="Maximum quota for the domain in MB")
|
|
||||||
parser.add_argument("--quota", help="Quota used by the domain in MB")
|
|
||||||
parser.add_argument("--relay-all-recipients", choices=["1", "0"], help="Relay all recipients (1 = yes, 0 = no)")
|
|
||||||
parser.add_argument("--rl-frame", help="Rate limit frame (e.g., s, m, h)")
|
|
||||||
parser.add_argument("--rl-value", help="Rate limit value")
|
|
||||||
parser.add_argument("--restart-sogo", help="Restart SOGo after changes (1 = yes, 0 = no)")
|
|
||||||
parser.add_argument("--tags", nargs="*", help="Tags for the domain")
|
|
||||||
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
from modules.Mailcow import Mailcow
|
|
||||||
from models.BaseModel import BaseModel
|
|
||||||
|
|
||||||
class DomainadminModel(BaseModel):
|
|
||||||
parser_command = "domainadmin"
|
|
||||||
required_args = {
|
|
||||||
"add": [["username", "domains", "password"]],
|
|
||||||
"delete": [["username"]],
|
|
||||||
"get": [["username"]],
|
|
||||||
"edit": [["username"]]
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
username=None,
|
|
||||||
domains=None,
|
|
||||||
password=None,
|
|
||||||
active=None,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
self.mailcow = Mailcow()
|
|
||||||
|
|
||||||
self.username = username
|
|
||||||
self.domains = domains
|
|
||||||
self.password = password
|
|
||||||
self.password2 = password
|
|
||||||
self.active = active
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data):
|
|
||||||
return cls(
|
|
||||||
username=data.get("username"),
|
|
||||||
domains=data.get("domains"),
|
|
||||||
password=data.get("password"),
|
|
||||||
password2=data.get("password"),
|
|
||||||
active=data.get("active", None),
|
|
||||||
)
|
|
||||||
|
|
||||||
def getAdd(self):
|
|
||||||
"""
|
|
||||||
Get the domain admin details as a dictionary for adding, sets default values.
|
|
||||||
:return: Dictionary containing domain admin details.
|
|
||||||
"""
|
|
||||||
domainadmin = {
|
|
||||||
"username": self.username,
|
|
||||||
"domains": self.domains,
|
|
||||||
"password": self.password,
|
|
||||||
"password2": self.password2,
|
|
||||||
"active": self.active if self.active is not None else "1"
|
|
||||||
}
|
|
||||||
return {key: value for key, value in domainadmin.items() if value is not None}
|
|
||||||
|
|
||||||
def getEdit(self):
|
|
||||||
"""
|
|
||||||
Get the domain admin details as a dictionary for editing, sets no default values.
|
|
||||||
:return: Dictionary containing domain admin details.
|
|
||||||
"""
|
|
||||||
domainadmin = {
|
|
||||||
"username": self.username,
|
|
||||||
"domains": self.domains,
|
|
||||||
"password": self.password,
|
|
||||||
"password2": self.password2,
|
|
||||||
"active": self.active
|
|
||||||
}
|
|
||||||
return {key: value for key, value in domainadmin.items() if value is not None}
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""
|
|
||||||
Get the domain admin details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.getDomainadmin(self.username)
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
"""
|
|
||||||
Delete the domain admin from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.deleteDomainadmin(self.username)
|
|
||||||
|
|
||||||
def add(self):
|
|
||||||
"""
|
|
||||||
Add the domain admin to the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.addDomainadmin(self.getAdd())
|
|
||||||
|
|
||||||
def edit(self):
|
|
||||||
"""
|
|
||||||
Edit the domain admin in the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.editDomainadmin(self.username, self.getEdit())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_parser(cls, subparsers):
|
|
||||||
parser = subparsers.add_parser(
|
|
||||||
cls.parser_command,
|
|
||||||
help="Manage domain admins (add, delete, get, edit)"
|
|
||||||
)
|
|
||||||
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit")
|
|
||||||
parser.add_argument("--username", help="Username for the domain admin")
|
|
||||||
parser.add_argument("--domains", help="Comma-separated list of domains")
|
|
||||||
parser.add_argument("--password", help="Password for the domain admin")
|
|
||||||
parser.add_argument("--active", choices=["1", "0"], help="Activate (1) or deactivate (0) the domain admin")
|
|
||||||
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
from modules.Mailcow import Mailcow
|
|
||||||
from models.BaseModel import BaseModel
|
|
||||||
|
|
||||||
class MailboxModel(BaseModel):
|
|
||||||
parser_command = "mailbox"
|
|
||||||
required_args = {
|
|
||||||
"add": [["username", "password"]],
|
|
||||||
"delete": [["username"]],
|
|
||||||
"get": [["username"]],
|
|
||||||
"edit": [["username"]]
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
password=None,
|
|
||||||
username=None,
|
|
||||||
domain=None,
|
|
||||||
local_part=None,
|
|
||||||
active=None,
|
|
||||||
sogo_access=None,
|
|
||||||
name=None,
|
|
||||||
authsource=None,
|
|
||||||
quota=None,
|
|
||||||
force_pw_update=None,
|
|
||||||
tls_enforce_in=None,
|
|
||||||
tls_enforce_out=None,
|
|
||||||
tags=None,
|
|
||||||
sender_acl=None,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
self.mailcow = Mailcow()
|
|
||||||
|
|
||||||
if username is not None and "@" in username:
|
|
||||||
self.username = username
|
|
||||||
self.local_part, self.domain = username.split("@")
|
|
||||||
else:
|
|
||||||
self.username = f"{local_part}@{domain}"
|
|
||||||
self.local_part = local_part
|
|
||||||
self.domain = domain
|
|
||||||
|
|
||||||
self.password = password
|
|
||||||
self.password2 = password
|
|
||||||
self.active = active
|
|
||||||
self.sogo_access = sogo_access
|
|
||||||
self.name = name
|
|
||||||
self.authsource = authsource
|
|
||||||
self.quota = quota
|
|
||||||
self.force_pw_update = force_pw_update
|
|
||||||
self.tls_enforce_in = tls_enforce_in
|
|
||||||
self.tls_enforce_out = tls_enforce_out
|
|
||||||
self.tags = tags
|
|
||||||
self.sender_acl = sender_acl
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data):
|
|
||||||
return cls(
|
|
||||||
domain=data.get("domain"),
|
|
||||||
local_part=data.get("local_part"),
|
|
||||||
password=data.get("password"),
|
|
||||||
password2=data.get("password"),
|
|
||||||
active=data.get("active", None),
|
|
||||||
sogo_access=data.get("sogo_access", None),
|
|
||||||
name=data.get("name", None),
|
|
||||||
authsource=data.get("authsource", None),
|
|
||||||
quota=data.get("quota", None),
|
|
||||||
force_pw_update=data.get("force_pw_update", None),
|
|
||||||
tls_enforce_in=data.get("tls_enforce_in", None),
|
|
||||||
tls_enforce_out=data.get("tls_enforce_out", None),
|
|
||||||
tags=data.get("tags", None),
|
|
||||||
sender_acl=data.get("sender_acl", None)
|
|
||||||
)
|
|
||||||
|
|
||||||
def getAdd(self):
|
|
||||||
"""
|
|
||||||
Get the mailbox details as a dictionary for adding, sets default values.
|
|
||||||
:return: Dictionary containing mailbox details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
mailbox = {
|
|
||||||
"domain": self.domain,
|
|
||||||
"local_part": self.local_part,
|
|
||||||
"password": self.password,
|
|
||||||
"password2": self.password2,
|
|
||||||
"active": self.active if self.active is not None else 1,
|
|
||||||
"name": self.name if self.name is not None else "",
|
|
||||||
"authsource": self.authsource if self.authsource is not None else "mailcow",
|
|
||||||
"quota": self.quota if self.quota is not None else 0,
|
|
||||||
"force_pw_update": self.force_pw_update if self.force_pw_update is not None else 0,
|
|
||||||
"tls_enforce_in": self.tls_enforce_in if self.tls_enforce_in is not None else 0,
|
|
||||||
"tls_enforce_out": self.tls_enforce_out if self.tls_enforce_out is not None else 0,
|
|
||||||
"tags": self.tags if self.tags is not None else []
|
|
||||||
}
|
|
||||||
return {key: value for key, value in mailbox.items() if value is not None}
|
|
||||||
|
|
||||||
def getEdit(self):
|
|
||||||
"""
|
|
||||||
Get the mailbox details as a dictionary for editing, sets no default values.
|
|
||||||
:return: Dictionary containing mailbox details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
mailbox = {
|
|
||||||
"domain": self.domain,
|
|
||||||
"local_part": self.local_part,
|
|
||||||
"password": self.password,
|
|
||||||
"password2": self.password2,
|
|
||||||
"active": self.active,
|
|
||||||
"name": self.name,
|
|
||||||
"authsource": self.authsource,
|
|
||||||
"quota": self.quota,
|
|
||||||
"force_pw_update": self.force_pw_update,
|
|
||||||
"tls_enforce_in": self.tls_enforce_in,
|
|
||||||
"tls_enforce_out": self.tls_enforce_out,
|
|
||||||
"tags": self.tags
|
|
||||||
}
|
|
||||||
return {key: value for key, value in mailbox.items() if value is not None}
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""
|
|
||||||
Get the mailbox details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.getMailbox(self.username)
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
"""
|
|
||||||
Get the mailbox details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.deleteMailbox(self.username)
|
|
||||||
|
|
||||||
def add(self):
|
|
||||||
"""
|
|
||||||
Get the mailbox details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.addMailbox(self.getAdd())
|
|
||||||
|
|
||||||
def edit(self):
|
|
||||||
"""
|
|
||||||
Get the mailbox details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.editMailbox(self.username, self.getEdit())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_parser(cls, subparsers):
|
|
||||||
parser = subparsers.add_parser(
|
|
||||||
cls.parser_command,
|
|
||||||
help="Manage mailboxes (add, delete, get, edit)"
|
|
||||||
)
|
|
||||||
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit")
|
|
||||||
parser.add_argument("--username", help="Full email address of the mailbox (e.g. user@example.com)")
|
|
||||||
parser.add_argument("--password", help="Password for the mailbox (required for add)")
|
|
||||||
parser.add_argument("--active", choices=["1", "0"], help="Activate (1) or deactivate (0) the mailbox")
|
|
||||||
parser.add_argument("--sogo-access", choices=["1", "0"], help="Redirect mailbox to SOGo after web login (1 = yes, 0 = no)")
|
|
||||||
parser.add_argument("--name", help="Display name of the mailbox owner")
|
|
||||||
parser.add_argument("--authsource", help="Authentication source (default: mailcow)")
|
|
||||||
parser.add_argument("--quota", help="Mailbox quota in bytes (0 = unlimited)")
|
|
||||||
parser.add_argument("--force-pw-update", choices=["1", "0"], help="Force password update on next login (1 = yes, 0 = no)")
|
|
||||||
parser.add_argument("--tls-enforce-in", choices=["1", "0"], help="Enforce TLS for incoming emails (1 = yes, 0 = no)")
|
|
||||||
parser.add_argument("--tls-enforce-out", choices=["1", "0"], help="Enforce TLS for outgoing emails (1 = yes, 0 = no)")
|
|
||||||
parser.add_argument("--tags", help="Comma-separated list of tags for the mailbox")
|
|
||||||
parser.add_argument("--sender-acl", help="Comma-separated list of allowed sender addresses for this mailbox")
|
|
||||||
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
from modules.Dovecot import Dovecot
|
|
||||||
from models.BaseModel import BaseModel
|
|
||||||
|
|
||||||
class MaildirModel(BaseModel):
|
|
||||||
parser_command = "maildir"
|
|
||||||
required_args = {
|
|
||||||
"encrypt": [],
|
|
||||||
"decrypt": [],
|
|
||||||
"restore": [["username", "item"], ["list"]]
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
username=None,
|
|
||||||
source=None,
|
|
||||||
item=None,
|
|
||||||
overwrite=None,
|
|
||||||
list=None,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
self.dovecot = Dovecot()
|
|
||||||
|
|
||||||
for key, value in kwargs.items():
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
self.username = username
|
|
||||||
self.source = source
|
|
||||||
self.item = item
|
|
||||||
self.overwrite = overwrite
|
|
||||||
self.list = list
|
|
||||||
|
|
||||||
def encrypt(self):
|
|
||||||
"""
|
|
||||||
Encrypt the maildir for the specified user or all.
|
|
||||||
:return: Response from Dovecot.
|
|
||||||
"""
|
|
||||||
return self.dovecot.encryptMaildir(self.source_dir, self.output_dir)
|
|
||||||
|
|
||||||
def decrypt(self):
|
|
||||||
"""
|
|
||||||
Decrypt the maildir for the specified user or all.
|
|
||||||
:return: Response from Dovecot.
|
|
||||||
"""
|
|
||||||
return self.dovecot.decryptMaildir(self.source_dir, self.output_dir)
|
|
||||||
|
|
||||||
def restore(self):
|
|
||||||
"""
|
|
||||||
Restore or List maildir data for the specified user.
|
|
||||||
:return: Response from Dovecot.
|
|
||||||
"""
|
|
||||||
if self.list:
|
|
||||||
return self.dovecot.listDeletedMaildirs()
|
|
||||||
return self.dovecot.restoreMaildir(self.username, self.item)
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_parser(cls, subparsers):
|
|
||||||
parser = subparsers.add_parser(
|
|
||||||
cls.parser_command,
|
|
||||||
help="Manage maildir (encrypt, decrypt, restore)"
|
|
||||||
)
|
|
||||||
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: encrypt, decrypt, restore")
|
|
||||||
parser.add_argument("--item", help="Item to restore")
|
|
||||||
parser.add_argument("--username", help="Username to restore the item to")
|
|
||||||
parser.add_argument("--list", action="store_true", help="List items to restore")
|
|
||||||
parser.add_argument("--source-dir", help="Path to the source maildir to import/encrypt/decrypt")
|
|
||||||
parser.add_argument("--output-dir", help="Directory to store encrypted/decrypted files inside the Dovecot container")
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import json
|
|
||||||
from models.BaseModel import BaseModel
|
|
||||||
from modules.Mailer import Mailer
|
|
||||||
|
|
||||||
class MailerModel(BaseModel):
|
|
||||||
parser_command = "mail"
|
|
||||||
required_args = {
|
|
||||||
"send": [["sender", "recipient", "subject", "body"]]
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
sender=None,
|
|
||||||
recipient=None,
|
|
||||||
subject=None,
|
|
||||||
body=None,
|
|
||||||
context=None,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
self.sender = sender
|
|
||||||
self.recipient = recipient
|
|
||||||
self.subject = subject
|
|
||||||
self.body = body
|
|
||||||
self.context = context
|
|
||||||
|
|
||||||
def send(self):
|
|
||||||
if self.context is not None:
|
|
||||||
try:
|
|
||||||
self.context = json.loads(self.context)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
return f"Invalid context JSON: {e}"
|
|
||||||
else:
|
|
||||||
self.context = {}
|
|
||||||
|
|
||||||
mailer = Mailer(
|
|
||||||
smtp_host="postfix-mailcow",
|
|
||||||
smtp_port=25,
|
|
||||||
username=self.sender,
|
|
||||||
password="",
|
|
||||||
use_tls=True
|
|
||||||
)
|
|
||||||
res = mailer.send_mail(
|
|
||||||
subject=self.subject,
|
|
||||||
from_addr=self.sender,
|
|
||||||
to_addrs=self.recipient.split(","),
|
|
||||||
template=self.body,
|
|
||||||
context=self.context
|
|
||||||
)
|
|
||||||
return res
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_parser(cls, subparsers):
|
|
||||||
parser = subparsers.add_parser(
|
|
||||||
cls.parser_command,
|
|
||||||
help="Send emails via SMTP"
|
|
||||||
)
|
|
||||||
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: send")
|
|
||||||
parser.add_argument("--sender", required=True, help="Email sender address")
|
|
||||||
parser.add_argument("--recipient", required=True, help="Email recipient address (comma-separated for multiple)")
|
|
||||||
parser.add_argument("--subject", required=True, help="Email subject")
|
|
||||||
parser.add_argument("--body", required=True, help="Email body (Jinja2 template supported)")
|
|
||||||
parser.add_argument("--context", help="Context for Jinja2 template rendering (JSON format)")
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
from modules.Mailcow import Mailcow
|
|
||||||
from models.BaseModel import BaseModel
|
|
||||||
|
|
||||||
class StatusModel(BaseModel):
|
|
||||||
parser_command = "status"
|
|
||||||
required_args = {
|
|
||||||
"version": [[]],
|
|
||||||
"vmail": [[]],
|
|
||||||
"containers": [[]]
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
self.mailcow = Mailcow()
|
|
||||||
|
|
||||||
def version(self):
|
|
||||||
"""
|
|
||||||
Get the version of the mailcow instance.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.getStatusVersion()
|
|
||||||
|
|
||||||
def vmail(self):
|
|
||||||
"""
|
|
||||||
Get the vmail details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.getStatusVmail()
|
|
||||||
|
|
||||||
def containers(self):
|
|
||||||
"""
|
|
||||||
Get the status of containers in the mailcow instance.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.getStatusContainers()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_parser(cls, subparsers):
|
|
||||||
parser = subparsers.add_parser(
|
|
||||||
cls.parser_command,
|
|
||||||
help="Get information about mailcow (version, vmail, containers)"
|
|
||||||
)
|
|
||||||
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: version, vmail, containers")
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
from modules.Mailcow import Mailcow
|
|
||||||
from models.BaseModel import BaseModel
|
|
||||||
|
|
||||||
class SyncjobModel(BaseModel):
|
|
||||||
parser_command = "syncjob"
|
|
||||||
required_args = {
|
|
||||||
"add": [["username", "host1", "port1", "user1", "password1", "enc1"]],
|
|
||||||
"delete": [["id"]],
|
|
||||||
"get": [["username"]],
|
|
||||||
"edit": [["id"]],
|
|
||||||
"run": [["id"]]
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
id=None,
|
|
||||||
username=None,
|
|
||||||
host1=None,
|
|
||||||
port1=None,
|
|
||||||
user1=None,
|
|
||||||
password1=None,
|
|
||||||
enc1=None,
|
|
||||||
mins_interval=None,
|
|
||||||
subfolder2=None,
|
|
||||||
maxage=None,
|
|
||||||
maxbytespersecond=None,
|
|
||||||
timeout1=None,
|
|
||||||
timeout2=None,
|
|
||||||
exclude=None,
|
|
||||||
custom_parameters=None,
|
|
||||||
delete2duplicates=None,
|
|
||||||
delete1=None,
|
|
||||||
delete2=None,
|
|
||||||
automap=None,
|
|
||||||
skipcrossduplicates=None,
|
|
||||||
subscribeall=None,
|
|
||||||
active=None,
|
|
||||||
force=None,
|
|
||||||
**kwargs
|
|
||||||
):
|
|
||||||
self.mailcow = Mailcow()
|
|
||||||
|
|
||||||
for key, value in kwargs.items():
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
self.id = id
|
|
||||||
self.username = username
|
|
||||||
self.host1 = host1
|
|
||||||
self.port1 = port1
|
|
||||||
self.user1 = user1
|
|
||||||
self.password1 = password1
|
|
||||||
self.enc1 = enc1
|
|
||||||
self.mins_interval = mins_interval
|
|
||||||
self.subfolder2 = subfolder2
|
|
||||||
self.maxage = maxage
|
|
||||||
self.maxbytespersecond = maxbytespersecond
|
|
||||||
self.timeout1 = timeout1
|
|
||||||
self.timeout2 = timeout2
|
|
||||||
self.exclude = exclude
|
|
||||||
self.custom_parameters = custom_parameters
|
|
||||||
self.delete2duplicates = delete2duplicates
|
|
||||||
self.delete1 = delete1
|
|
||||||
self.delete2 = delete2
|
|
||||||
self.automap = automap
|
|
||||||
self.skipcrossduplicates = skipcrossduplicates
|
|
||||||
self.subscribeall = subscribeall
|
|
||||||
self.active = active
|
|
||||||
self.force = force
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data):
|
|
||||||
return cls(
|
|
||||||
username=data.get("username"),
|
|
||||||
host1=data.get("host1"),
|
|
||||||
port1=data.get("port1"),
|
|
||||||
user1=data.get("user1"),
|
|
||||||
password1=data.get("password1"),
|
|
||||||
enc1=data.get("enc1"),
|
|
||||||
mins_interval=data.get("mins_interval", None),
|
|
||||||
subfolder2=data.get("subfolder2", None),
|
|
||||||
maxage=data.get("maxage", None),
|
|
||||||
maxbytespersecond=data.get("maxbytespersecond", None),
|
|
||||||
timeout1=data.get("timeout1", None),
|
|
||||||
timeout2=data.get("timeout2", None),
|
|
||||||
exclude=data.get("exclude", None),
|
|
||||||
custom_parameters=data.get("custom_parameters", None),
|
|
||||||
delete2duplicates=data.get("delete2duplicates", None),
|
|
||||||
delete1=data.get("delete1", None),
|
|
||||||
delete2=data.get("delete2", None),
|
|
||||||
automap=data.get("automap", None),
|
|
||||||
skipcrossduplicates=data.get("skipcrossduplicates", None),
|
|
||||||
subscribeall=data.get("subscribeall", None),
|
|
||||||
active=data.get("active", None),
|
|
||||||
)
|
|
||||||
|
|
||||||
def getAdd(self):
|
|
||||||
"""
|
|
||||||
Get the sync job details as a dictionary for adding, sets default values.
|
|
||||||
:return: Dictionary containing sync job details.
|
|
||||||
"""
|
|
||||||
syncjob = {
|
|
||||||
"username": self.username,
|
|
||||||
"host1": self.host1,
|
|
||||||
"port1": self.port1,
|
|
||||||
"user1": self.user1,
|
|
||||||
"password1": self.password1,
|
|
||||||
"enc1": self.enc1,
|
|
||||||
"mins_interval": self.mins_interval if self.mins_interval is not None else 20,
|
|
||||||
"subfolder2": self.subfolder2 if self.subfolder2 is not None else "",
|
|
||||||
"maxage": self.maxage if self.maxage is not None else 0,
|
|
||||||
"maxbytespersecond": self.maxbytespersecond if self.maxbytespersecond is not None else 0,
|
|
||||||
"timeout1": self.timeout1 if self.timeout1 is not None else 600,
|
|
||||||
"timeout2": self.timeout2 if self.timeout2 is not None else 600,
|
|
||||||
"exclude": self.exclude if self.exclude is not None else "(?i)spam|(?i)junk",
|
|
||||||
"custom_parameters": self.custom_parameters if self.custom_parameters is not None else "",
|
|
||||||
"delete2duplicates": 1 if self.delete2duplicates else 0,
|
|
||||||
"delete1": 1 if self.delete1 else 0,
|
|
||||||
"delete2": 1 if self.delete2 else 0,
|
|
||||||
"automap": 1 if self.automap else 0,
|
|
||||||
"skipcrossduplicates": 1 if self.skipcrossduplicates else 0,
|
|
||||||
"subscribeall": 1 if self.subscribeall else 0,
|
|
||||||
"active": 1 if self.active else 0
|
|
||||||
}
|
|
||||||
return {key: value for key, value in syncjob.items() if value is not None}
|
|
||||||
|
|
||||||
def getEdit(self):
|
|
||||||
"""
|
|
||||||
Get the sync job details as a dictionary for editing, sets no default values.
|
|
||||||
:return: Dictionary containing sync job details.
|
|
||||||
"""
|
|
||||||
syncjob = {
|
|
||||||
"username": self.username,
|
|
||||||
"host1": self.host1,
|
|
||||||
"port1": self.port1,
|
|
||||||
"user1": self.user1,
|
|
||||||
"password1": self.password1,
|
|
||||||
"enc1": self.enc1,
|
|
||||||
"mins_interval": self.mins_interval,
|
|
||||||
"subfolder2": self.subfolder2,
|
|
||||||
"maxage": self.maxage,
|
|
||||||
"maxbytespersecond": self.maxbytespersecond,
|
|
||||||
"timeout1": self.timeout1,
|
|
||||||
"timeout2": self.timeout2,
|
|
||||||
"exclude": self.exclude,
|
|
||||||
"custom_parameters": self.custom_parameters,
|
|
||||||
"delete2duplicates": self.delete2duplicates,
|
|
||||||
"delete1": self.delete1,
|
|
||||||
"delete2": self.delete2,
|
|
||||||
"automap": self.automap,
|
|
||||||
"skipcrossduplicates": self.skipcrossduplicates,
|
|
||||||
"subscribeall": self.subscribeall,
|
|
||||||
"active": self.active
|
|
||||||
}
|
|
||||||
return {key: value for key, value in syncjob.items() if value is not None}
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""
|
|
||||||
Get the sync job details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.getSyncjob(self.username)
|
|
||||||
|
|
||||||
def delete(self):
|
|
||||||
"""
|
|
||||||
Get the sync job details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.deleteSyncjob(self.id)
|
|
||||||
|
|
||||||
def add(self):
|
|
||||||
"""
|
|
||||||
Get the sync job details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.addSyncjob(self.getAdd())
|
|
||||||
|
|
||||||
def edit(self):
|
|
||||||
"""
|
|
||||||
Get the sync job details from the mailcow API.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.editSyncjob(self.id, self.getEdit())
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""
|
|
||||||
Run the sync job.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.mailcow.runSyncjob(self.id, force=self.force)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_parser(cls, subparsers):
|
|
||||||
parser = subparsers.add_parser(
|
|
||||||
cls.parser_command,
|
|
||||||
help="Manage sync jobs (add, delete, get, edit)"
|
|
||||||
)
|
|
||||||
parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit")
|
|
||||||
parser.add_argument("--id", help="Syncjob object ID (required for edit, delete, run)")
|
|
||||||
parser.add_argument("--username", help="Target mailbox username (e.g. user@example.com)")
|
|
||||||
parser.add_argument("--host1", help="Source IMAP server hostname")
|
|
||||||
parser.add_argument("--port1", help="Source IMAP server port")
|
|
||||||
parser.add_argument("--user1", help="Source IMAP account username")
|
|
||||||
parser.add_argument("--password1", help="Source IMAP account password")
|
|
||||||
parser.add_argument("--enc1", choices=["PLAIN", "SSL", "TLS"], help="Encryption for source server connection")
|
|
||||||
parser.add_argument("--mins-interval", help="Sync interval in minutes (default: 20)")
|
|
||||||
parser.add_argument("--subfolder2", help="Destination subfolder (default: empty)")
|
|
||||||
parser.add_argument("--maxage", help="Maximum mail age in days (default: 0 = unlimited)")
|
|
||||||
parser.add_argument("--maxbytespersecond", help="Maximum bandwidth in bytes/sec (default: 0 = unlimited)")
|
|
||||||
parser.add_argument("--timeout1", help="Timeout for source server in seconds (default: 600)")
|
|
||||||
parser.add_argument("--timeout2", help="Timeout for destination server in seconds (default: 600)")
|
|
||||||
parser.add_argument("--exclude", help="Regex pattern to exclude folders (default: (?i)spam|(?i)junk)")
|
|
||||||
parser.add_argument("--custom-parameters", help="Additional imapsync parameters")
|
|
||||||
parser.add_argument("--delete2duplicates", choices=["1", "0"], help="Delete duplicates on destination (1 = yes, 0 = no)")
|
|
||||||
parser.add_argument("--del1", choices=["1", "0"], help="Delete mails on source after sync (1 = yes, 0 = no)")
|
|
||||||
parser.add_argument("--del2", choices=["1", "0"], help="Delete mails on destination after sync (1 = yes, 0 = no)")
|
|
||||||
parser.add_argument("--automap", choices=["1", "0"], help="Enable folder automapping (1 = yes, 0 = no)")
|
|
||||||
parser.add_argument("--skipcrossduplicates", choices=["1", "0"], help="Skip cross-account duplicates (1 = yes, 0 = no)")
|
|
||||||
parser.add_argument("--subscribeall", choices=["1", "0"], help="Subscribe to all folders (1 = yes, 0 = no)")
|
|
||||||
parser.add_argument("--active", choices=["1", "0"], help="Activate syncjob (1 = yes, 0 = no)")
|
|
||||||
parser.add_argument("--force", action="store_true", help="Force the syncjob to run even if it is not active")
|
|
||||||
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import docker
|
|
||||||
from docker.errors import APIError
|
|
||||||
|
|
||||||
class Docker:
|
|
||||||
def __init__(self):
|
|
||||||
self.client = docker.from_env()
|
|
||||||
|
|
||||||
def exec_command(self, container_name, cmd, user=None):
|
|
||||||
"""
|
|
||||||
Execute a command in a container by its container name.
|
|
||||||
:param container_name: The name of the container.
|
|
||||||
:param cmd: The command to execute as a list (e.g., ["ls", "-la"]).
|
|
||||||
:param user: The user to execute the command as (optional).
|
|
||||||
:return: A standardized response with status, output, and exit_code.
|
|
||||||
"""
|
|
||||||
|
|
||||||
filters = {"name": container_name}
|
|
||||||
|
|
||||||
try:
|
|
||||||
for container in self.client.containers.list(filters=filters):
|
|
||||||
exec_result = container.exec_run(cmd, user=user)
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"exit_code": exec_result.exit_code,
|
|
||||||
"output": exec_result.output.decode("utf-8")
|
|
||||||
}
|
|
||||||
except APIError as e:
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"exit_code": "APIError",
|
|
||||||
"output": str(e)
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"exit_code": "Exception",
|
|
||||||
"output": str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
def start_container(self, container_name):
|
|
||||||
"""
|
|
||||||
Start a container by its container name.
|
|
||||||
:param container_name: The name of the container.
|
|
||||||
:return: A standardized response with status, output, and exit_code.
|
|
||||||
"""
|
|
||||||
|
|
||||||
filters = {"name": container_name}
|
|
||||||
|
|
||||||
try:
|
|
||||||
for container in self.client.containers.list(filters=filters):
|
|
||||||
container.start()
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"exit_code": "0",
|
|
||||||
"output": f"Container '{container_name}' started successfully."
|
|
||||||
}
|
|
||||||
except APIError as e:
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"exit_code": "APIError",
|
|
||||||
"output": str(e)
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"error_type": "Exception",
|
|
||||||
"output": str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
def stop_container(self, container_name):
|
|
||||||
"""
|
|
||||||
Stop a container by its container name.
|
|
||||||
:param container_name: The name of the container.
|
|
||||||
:return: A standardized response with status, output, and exit_code.
|
|
||||||
"""
|
|
||||||
|
|
||||||
filters = {"name": container_name}
|
|
||||||
|
|
||||||
try:
|
|
||||||
for container in self.client.containers.list(filters=filters):
|
|
||||||
container.stop()
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"exit_code": "0",
|
|
||||||
"output": f"Container '{container_name}' stopped successfully."
|
|
||||||
}
|
|
||||||
except APIError as e:
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"exit_code": "APIError",
|
|
||||||
"output": str(e)
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"exit_code": "Exception",
|
|
||||||
"output": str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
def restart_container(self, container_name):
|
|
||||||
"""
|
|
||||||
Restart a container by its container name.
|
|
||||||
:param container_name: The name of the container.
|
|
||||||
:return: A standardized response with status, output, and exit_code.
|
|
||||||
"""
|
|
||||||
|
|
||||||
filters = {"name": container_name}
|
|
||||||
|
|
||||||
try:
|
|
||||||
for container in self.client.containers.list(filters=filters):
|
|
||||||
container.restart()
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"exit_code": "0",
|
|
||||||
"output": f"Container '{container_name}' restarted successfully."
|
|
||||||
}
|
|
||||||
except APIError as e:
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"exit_code": "APIError",
|
|
||||||
"output": str(e)
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
"status": "error",
|
|
||||||
"exit_code": "Exception",
|
|
||||||
"output": str(e)
|
|
||||||
}
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from modules.Docker import Docker
|
|
||||||
|
|
||||||
class Dovecot:
|
|
||||||
def __init__(self):
|
|
||||||
self.docker = Docker()
|
|
||||||
|
|
||||||
def decryptMaildir(self, source_dir="/var/vmail/", output_dir=None):
|
|
||||||
"""
|
|
||||||
Decrypt files in /var/vmail using doveadm if they are encrypted.
|
|
||||||
:param output_dir: Directory inside the Dovecot container to store decrypted files, Default overwrite.
|
|
||||||
"""
|
|
||||||
private_key = "/mail_crypt/ecprivkey.pem"
|
|
||||||
public_key = "/mail_crypt/ecpubkey.pem"
|
|
||||||
|
|
||||||
if output_dir:
|
|
||||||
# Ensure the output directory exists inside the container
|
|
||||||
mkdir_result = self.docker.exec_command("dovecot-mailcow", f"bash -c 'mkdir -p {output_dir} && chown vmail:vmail {output_dir}'")
|
|
||||||
if mkdir_result.get("status") != "success":
|
|
||||||
print(f"Error creating output directory: {mkdir_result.get('output')}")
|
|
||||||
return
|
|
||||||
|
|
||||||
find_command = [
|
|
||||||
"find", source_dir, "-type", "f", "-regextype", "egrep", "-regex", ".*S=.*W=.*"
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
find_result = self.docker.exec_command("dovecot-mailcow", " ".join(find_command))
|
|
||||||
if find_result.get("status") != "success":
|
|
||||||
print(f"Error finding files: {find_result.get('output')}")
|
|
||||||
return
|
|
||||||
|
|
||||||
files = find_result.get("output", "").splitlines()
|
|
||||||
|
|
||||||
for file in files:
|
|
||||||
head_command = f"head -c7 {file}"
|
|
||||||
head_result = self.docker.exec_command("dovecot-mailcow", head_command)
|
|
||||||
if head_result.get("status") == "success" and head_result.get("output", "").strip() == "CRYPTED":
|
|
||||||
if output_dir:
|
|
||||||
# Preserve the directory structure in the output directory
|
|
||||||
relative_path = os.path.relpath(file, source_dir)
|
|
||||||
output_file = os.path.join(output_dir, relative_path)
|
|
||||||
current_path = output_dir
|
|
||||||
for part in os.path.dirname(relative_path).split(os.sep):
|
|
||||||
current_path = os.path.join(current_path, part)
|
|
||||||
mkdir_result = self.docker.exec_command("dovecot-mailcow", f"bash -c '[ ! -d {current_path} ] && mkdir {current_path} && chown vmail:vmail {current_path}'")
|
|
||||||
if mkdir_result.get("status") != "success":
|
|
||||||
print(f"Error creating directory {current_path}: {mkdir_result.get('output')}")
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
# Overwrite the original file
|
|
||||||
output_file = file
|
|
||||||
|
|
||||||
decrypt_command = (
|
|
||||||
f"bash -c 'doveadm fs get compress lz4:1:crypt:private_key_path={private_key}:public_key_path={public_key}:posix:prefix=/ {file} > {output_file}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
decrypt_result = self.docker.exec_command("dovecot-mailcow", decrypt_command)
|
|
||||||
if decrypt_result.get("status") == "success":
|
|
||||||
print(f"Decrypted {file}")
|
|
||||||
|
|
||||||
# Verify the file size and set permissions
|
|
||||||
size_check_command = f"bash -c '[ -s {output_file} ] && chmod 600 {output_file} && chown vmail:vmail {output_file} || rm -f {output_file}'"
|
|
||||||
size_check_result = self.docker.exec_command("dovecot-mailcow", size_check_command)
|
|
||||||
if size_check_result.get("status") != "success":
|
|
||||||
print(f"Error setting permissions for {output_file}: {size_check_result.get('output')}\n")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error during decryption: {e}")
|
|
||||||
|
|
||||||
return "Done"
|
|
||||||
|
|
||||||
def encryptMaildir(self, source_dir="/var/vmail/", output_dir=None):
|
|
||||||
"""
|
|
||||||
Encrypt files in /var/vmail using doveadm if they are not already encrypted.
|
|
||||||
:param source_dir: Directory inside the Dovecot container to encrypt files.
|
|
||||||
:param output_dir: Directory inside the Dovecot container to store encrypted files, Default overwrite.
|
|
||||||
"""
|
|
||||||
private_key = "/mail_crypt/ecprivkey.pem"
|
|
||||||
public_key = "/mail_crypt/ecpubkey.pem"
|
|
||||||
|
|
||||||
if output_dir:
|
|
||||||
# Ensure the output directory exists inside the container
|
|
||||||
mkdir_result = self.docker.exec_command("dovecot-mailcow", f"mkdir -p {output_dir}")
|
|
||||||
if mkdir_result.get("status") != "success":
|
|
||||||
print(f"Error creating output directory: {mkdir_result.get('output')}")
|
|
||||||
return
|
|
||||||
|
|
||||||
find_command = [
|
|
||||||
"find", source_dir, "-type", "f", "-regextype", "egrep", "-regex", ".*S=.*W=.*"
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
find_result = self.docker.exec_command("dovecot-mailcow", " ".join(find_command))
|
|
||||||
if find_result.get("status") != "success":
|
|
||||||
print(f"Error finding files: {find_result.get('output')}")
|
|
||||||
return
|
|
||||||
|
|
||||||
files = find_result.get("output", "").splitlines()
|
|
||||||
|
|
||||||
for file in files:
|
|
||||||
head_command = f"head -c7 {file}"
|
|
||||||
head_result = self.docker.exec_command("dovecot-mailcow", head_command)
|
|
||||||
if head_result.get("status") == "success" and head_result.get("output", "").strip() != "CRYPTED":
|
|
||||||
if output_dir:
|
|
||||||
# Preserve the directory structure in the output directory
|
|
||||||
relative_path = os.path.relpath(file, source_dir)
|
|
||||||
output_file = os.path.join(output_dir, relative_path)
|
|
||||||
current_path = output_dir
|
|
||||||
for part in os.path.dirname(relative_path).split(os.sep):
|
|
||||||
current_path = os.path.join(current_path, part)
|
|
||||||
mkdir_result = self.docker.exec_command("dovecot-mailcow", f"bash -c '[ ! -d {current_path} ] && mkdir {current_path} && chown vmail:vmail {current_path}'")
|
|
||||||
if mkdir_result.get("status") != "success":
|
|
||||||
print(f"Error creating directory {current_path}: {mkdir_result.get('output')}")
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
# Overwrite the original file
|
|
||||||
output_file = file
|
|
||||||
|
|
||||||
encrypt_command = (
|
|
||||||
f"bash -c 'doveadm fs put crypt private_key_path={private_key}:public_key_path={public_key}:posix:prefix=/ {file} {output_file}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
encrypt_result = self.docker.exec_command("dovecot-mailcow", encrypt_command)
|
|
||||||
if encrypt_result.get("status") == "success":
|
|
||||||
print(f"Encrypted {file}")
|
|
||||||
|
|
||||||
# Set permissions
|
|
||||||
permissions_command = f"bash -c 'chmod 600 {output_file} && chown 5000:5000 {output_file}'"
|
|
||||||
permissions_result = self.docker.exec_command("dovecot-mailcow", permissions_command)
|
|
||||||
if permissions_result.get("status") != "success":
|
|
||||||
print(f"Error setting permissions for {output_file}: {permissions_result.get('output')}\n")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error during encryption: {e}")
|
|
||||||
|
|
||||||
return "Done"
|
|
||||||
|
|
||||||
def listDeletedMaildirs(self, source_dir="/var/vmail/_garbage"):
|
|
||||||
"""
|
|
||||||
List deleted maildirs in the specified garbage directory.
|
|
||||||
:param source_dir: Directory to search for deleted maildirs.
|
|
||||||
:return: List of maildirs.
|
|
||||||
"""
|
|
||||||
list_command = ["bash", "-c", f"ls -la {source_dir}"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = self.docker.exec_command("dovecot-mailcow", list_command)
|
|
||||||
if result.get("status") != "success":
|
|
||||||
print(f"Error listing deleted maildirs: {result.get('output')}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
lines = result.get("output", "").splitlines()
|
|
||||||
maildirs = {}
|
|
||||||
|
|
||||||
for idx, line in enumerate(lines):
|
|
||||||
parts = line.split()
|
|
||||||
if "_" in line:
|
|
||||||
folder_name = parts[-1]
|
|
||||||
time, maildir = folder_name.split("_", 1)
|
|
||||||
|
|
||||||
if maildir.endswith("_index"):
|
|
||||||
main_item = maildir[:-6]
|
|
||||||
if main_item in maildirs:
|
|
||||||
maildirs[main_item]["has_index"] = True
|
|
||||||
else:
|
|
||||||
maildirs[maildir] = {"item": idx, "time": time, "name": maildir, "has_index": False}
|
|
||||||
|
|
||||||
return list(maildirs.values())
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error during listing deleted maildirs: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
def restoreMaildir(self, username, item, source_dir="/var/vmail/_garbage"):
|
|
||||||
"""
|
|
||||||
Restore a maildir item for a specific user from the deleted maildirs.
|
|
||||||
:param username: Username to restore the item to.
|
|
||||||
:param item: Item to restore (e.g., mailbox, folder).
|
|
||||||
:param source_dir: Directory containing deleted maildirs.
|
|
||||||
:return: Response from Dovecot.
|
|
||||||
"""
|
|
||||||
username_splitted = username.split("@")
|
|
||||||
maildirs = self.listDeletedMaildirs()
|
|
||||||
|
|
||||||
maildir = None
|
|
||||||
for mdir in maildirs:
|
|
||||||
if mdir["item"] == int(item):
|
|
||||||
maildir = mdir
|
|
||||||
break
|
|
||||||
if not maildir:
|
|
||||||
return {"status": "error", "message": "Maildir not found."}
|
|
||||||
|
|
||||||
restore_command = f"mv {source_dir}/{maildir['time']}_{maildir['name']} /var/vmail/{username_splitted[1]}/{username_splitted[0]}"
|
|
||||||
restore_index_command = f"mv {source_dir}/{maildir['time']}_{maildir['name']}_index /var/vmail_index/{username}"
|
|
||||||
|
|
||||||
result = self.docker.exec_command("dovecot-mailcow", ["bash", "-c", restore_command])
|
|
||||||
if result.get("status") != "success":
|
|
||||||
return {"status": "error", "message": "Failed to restore maildir."}
|
|
||||||
|
|
||||||
result = self.docker.exec_command("dovecot-mailcow", ["bash", "-c", restore_index_command])
|
|
||||||
if result.get("status") != "success":
|
|
||||||
return {"status": "error", "message": "Failed to restore maildir index."}
|
|
||||||
|
|
||||||
return "Done"
|
|
||||||
@@ -1,457 +0,0 @@
|
|||||||
import requests
|
|
||||||
import urllib3
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
|
||||||
import mysql.connector
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from datetime import datetime
|
|
||||||
from modules.Docker import Docker
|
|
||||||
|
|
||||||
|
|
||||||
class Mailcow:
|
|
||||||
def __init__(self):
|
|
||||||
self.apiUrl = "/api/v1"
|
|
||||||
self.ignore_ssl_errors = True
|
|
||||||
|
|
||||||
self.baseUrl = f"https://{os.getenv('IPv4_NETWORK', '172.22.1')}.247:{os.getenv('HTTPS_PORT', '443')}"
|
|
||||||
self.host = os.getenv("MAILCOW_HOSTNAME", "")
|
|
||||||
self.apiKey = ""
|
|
||||||
if self.ignore_ssl_errors:
|
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
||||||
|
|
||||||
self.db_config = {
|
|
||||||
'user': os.getenv('DBUSER'),
|
|
||||||
'password': os.getenv('DBPASS'),
|
|
||||||
'database': os.getenv('DBNAME'),
|
|
||||||
'unix_socket': '/var/run/mysqld/mysqld.sock',
|
|
||||||
}
|
|
||||||
|
|
||||||
self.docker = Docker()
|
|
||||||
|
|
||||||
|
|
||||||
# API Functions
|
|
||||||
def addDomain(self, domain):
|
|
||||||
"""
|
|
||||||
Add a domain to the mailcow instance.
|
|
||||||
:param domain: Dictionary containing domain details.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.post('/add/domain', domain)
|
|
||||||
|
|
||||||
def addMailbox(self, mailbox):
|
|
||||||
"""
|
|
||||||
Add a mailbox to the mailcow instance.
|
|
||||||
:param mailbox: Dictionary containing mailbox details.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.post('/add/mailbox', mailbox)
|
|
||||||
|
|
||||||
def addAlias(self, alias):
|
|
||||||
"""
|
|
||||||
Add an alias to the mailcow instance.
|
|
||||||
:param alias: Dictionary containing alias details.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.post('/add/alias', alias)
|
|
||||||
|
|
||||||
def addSyncjob(self, syncjob):
|
|
||||||
"""
|
|
||||||
Add a sync job to the mailcow instance.
|
|
||||||
:param syncjob: Dictionary containing sync job details.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.post('/add/syncjob', syncjob)
|
|
||||||
|
|
||||||
def addDomainadmin(self, domainadmin):
|
|
||||||
"""
|
|
||||||
Add a domain admin to the mailcow instance.
|
|
||||||
:param domainadmin: Dictionary containing domain admin details.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.post('/add/domain-admin', domainadmin)
|
|
||||||
|
|
||||||
def deleteDomain(self, domain):
|
|
||||||
"""
|
|
||||||
Delete a domain from the mailcow instance.
|
|
||||||
:param domain: Name of the domain to delete.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
items = [domain]
|
|
||||||
return self.post('/delete/domain', items)
|
|
||||||
|
|
||||||
def deleteAlias(self, id):
|
|
||||||
"""
|
|
||||||
Delete an alias from the mailcow instance.
|
|
||||||
:param id: ID of the alias to delete.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
items = [id]
|
|
||||||
return self.post('/delete/alias', items)
|
|
||||||
|
|
||||||
def deleteSyncjob(self, id):
|
|
||||||
"""
|
|
||||||
Delete a sync job from the mailcow instance.
|
|
||||||
:param id: ID of the sync job to delete.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
items = [id]
|
|
||||||
return self.post('/delete/syncjob', items)
|
|
||||||
|
|
||||||
def deleteMailbox(self, mailbox):
|
|
||||||
"""
|
|
||||||
Delete a mailbox from the mailcow instance.
|
|
||||||
:param mailbox: Name of the mailbox to delete.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
items = [mailbox]
|
|
||||||
return self.post('/delete/mailbox', items)
|
|
||||||
|
|
||||||
def deleteDomainadmin(self, username):
|
|
||||||
"""
|
|
||||||
Delete a domain admin from the mailcow instance.
|
|
||||||
:param username: Username of the domain admin to delete.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
items = [username]
|
|
||||||
return self.post('/delete/domain-admin', items)
|
|
||||||
|
|
||||||
def post(self, endpoint, data):
|
|
||||||
"""
|
|
||||||
Make a POST request to the mailcow API.
|
|
||||||
:param endpoint: The API endpoint to post to.
|
|
||||||
:param data: Data to be sent in the POST request.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}"
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Host": self.host
|
|
||||||
}
|
|
||||||
if self.apiKey:
|
|
||||||
headers["X-Api-Key"] = self.apiKey
|
|
||||||
response = requests.post(
|
|
||||||
url,
|
|
||||||
json=data,
|
|
||||||
headers=headers,
|
|
||||||
verify=not self.ignore_ssl_errors
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def getDomain(self, domain):
|
|
||||||
"""
|
|
||||||
Get a domain from the mailcow instance.
|
|
||||||
:param domain: Name of the domain to get.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return self.get(f'/get/domain/{domain}')
|
|
||||||
|
|
||||||
def getMailbox(self, username):
|
|
||||||
"""
|
|
||||||
Get a mailbox from the mailcow instance.
|
|
||||||
:param mailbox: Dictionary containing mailbox details (e.g. {"username": "user@example.com"})
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.get(f'/get/mailbox/{username}')
|
|
||||||
|
|
||||||
def getAlias(self, id):
|
|
||||||
"""
|
|
||||||
Get an alias from the mailcow instance.
|
|
||||||
:param alias: Dictionary containing alias details (e.g. {"address": "alias@example.com"})
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.get(f'/get/alias/{id}')
|
|
||||||
|
|
||||||
def getSyncjob(self, id):
|
|
||||||
"""
|
|
||||||
Get a sync job from the mailcow instance.
|
|
||||||
:param syncjob: Dictionary containing sync job details (e.g. {"id": "123"})
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.get(f'/get/syncjobs/{id}')
|
|
||||||
|
|
||||||
def getDomainadmin(self, username):
|
|
||||||
"""
|
|
||||||
Get a domain admin from the mailcow instance.
|
|
||||||
:param username: Username of the domain admin to get.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.get(f'/get/domain-admin/{username}')
|
|
||||||
|
|
||||||
def getStatusVersion(self):
|
|
||||||
"""
|
|
||||||
Get the version of the mailcow instance.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.get('/get/status/version')
|
|
||||||
|
|
||||||
def getStatusVmail(self):
|
|
||||||
"""
|
|
||||||
Get the vmail status from the mailcow instance.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.get('/get/status/vmail')
|
|
||||||
|
|
||||||
def getStatusContainers(self):
|
|
||||||
"""
|
|
||||||
Get the status of containers from the mailcow instance.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
return self.get('/get/status/containers')
|
|
||||||
|
|
||||||
def get(self, endpoint, params=None):
|
|
||||||
"""
|
|
||||||
Make a GET request to the mailcow API.
|
|
||||||
:param endpoint: The API endpoint to get from.
|
|
||||||
:param params: Parameters to be sent in the GET request.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}"
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Host": self.host
|
|
||||||
}
|
|
||||||
if self.apiKey:
|
|
||||||
headers["X-Api-Key"] = self.apiKey
|
|
||||||
response = requests.get(
|
|
||||||
url,
|
|
||||||
params=params,
|
|
||||||
headers=headers,
|
|
||||||
verify=not self.ignore_ssl_errors
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def editDomain(self, domain, attributes):
|
|
||||||
"""
|
|
||||||
Edit an existing domain in the mailcow instance.
|
|
||||||
:param domain: Name of the domain to edit
|
|
||||||
:param attributes: Dictionary containing the new domain attributes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
items = [domain]
|
|
||||||
return self.edit('/edit/domain', items, attributes)
|
|
||||||
|
|
||||||
def editMailbox(self, mailbox, attributes):
|
|
||||||
"""
|
|
||||||
Edit an existing mailbox in the mailcow instance.
|
|
||||||
:param mailbox: Name of the mailbox to edit
|
|
||||||
:param attributes: Dictionary containing the new mailbox attributes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
items = [mailbox]
|
|
||||||
return self.edit('/edit/mailbox', items, attributes)
|
|
||||||
|
|
||||||
def editAlias(self, alias, attributes):
|
|
||||||
"""
|
|
||||||
Edit an existing alias in the mailcow instance.
|
|
||||||
:param alias: Name of the alias to edit
|
|
||||||
:param attributes: Dictionary containing the new alias attributes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
items = [alias]
|
|
||||||
return self.edit('/edit/alias', items, attributes)
|
|
||||||
|
|
||||||
def editSyncjob(self, syncjob, attributes):
|
|
||||||
"""
|
|
||||||
Edit an existing sync job in the mailcow instance.
|
|
||||||
:param syncjob: Name of the sync job to edit
|
|
||||||
:param attributes: Dictionary containing the new sync job attributes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
items = [syncjob]
|
|
||||||
return self.edit('/edit/syncjob', items, attributes)
|
|
||||||
|
|
||||||
def editDomainadmin(self, username, attributes):
|
|
||||||
"""
|
|
||||||
Edit an existing domain admin in the mailcow instance.
|
|
||||||
:param username: Username of the domain admin to edit
|
|
||||||
:param attributes: Dictionary containing the new domain admin attributes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
items = [username]
|
|
||||||
return self.edit('/edit/domain-admin', items, attributes)
|
|
||||||
|
|
||||||
def edit(self, endpoint, items, attributes):
|
|
||||||
"""
|
|
||||||
Make a POST request to edit items in the mailcow API.
|
|
||||||
:param items: List of items to edit.
|
|
||||||
:param attributes: Dictionary containing the new attributes for the items.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}"
|
|
||||||
headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Host": self.host
|
|
||||||
}
|
|
||||||
if self.apiKey:
|
|
||||||
headers["X-Api-Key"] = self.apiKey
|
|
||||||
data = {
|
|
||||||
"items": items,
|
|
||||||
"attr": attributes
|
|
||||||
}
|
|
||||||
response = requests.post(
|
|
||||||
url,
|
|
||||||
json=data,
|
|
||||||
headers=headers,
|
|
||||||
verify=not self.ignore_ssl_errors
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
|
|
||||||
# System Functions
|
|
||||||
def runSyncjob(self, id, force=False):
|
|
||||||
"""
|
|
||||||
Run a sync job.
|
|
||||||
:param id: ID of the sync job to run.
|
|
||||||
:return: Response from the imapsync script.
|
|
||||||
"""
|
|
||||||
|
|
||||||
creds_path = "/app/sieve.creds"
|
|
||||||
|
|
||||||
conn = mysql.connector.connect(**self.db_config)
|
|
||||||
cursor = conn.cursor(dictionary=True)
|
|
||||||
|
|
||||||
with open(creds_path, 'r') as file:
|
|
||||||
master_user, master_pass = file.read().strip().split(':')
|
|
||||||
|
|
||||||
query = ("SELECT * FROM imapsync WHERE id = %s")
|
|
||||||
cursor.execute(query, (id,))
|
|
||||||
|
|
||||||
success = False
|
|
||||||
syncjob = cursor.fetchone()
|
|
||||||
if not syncjob:
|
|
||||||
cursor.close()
|
|
||||||
conn.close()
|
|
||||||
return f"Sync job with ID {id} not found."
|
|
||||||
if syncjob['active'] == 0 and not force:
|
|
||||||
cursor.close()
|
|
||||||
conn.close()
|
|
||||||
return f"Sync job with ID {id} is not active."
|
|
||||||
|
|
||||||
enc1_flag = "--tls1" if syncjob['enc1'] == "TLS" else "--ssl1" if syncjob['enc1'] == "SSL" else None
|
|
||||||
|
|
||||||
|
|
||||||
passfile1_path = f"/tmp/passfile1_{id}.txt"
|
|
||||||
passfile2_path = f"/tmp/passfile2_{id}.txt"
|
|
||||||
passfile1_cmd = [
|
|
||||||
"sh", "-c",
|
|
||||||
f"echo {syncjob['password1']} > {passfile1_path}"
|
|
||||||
]
|
|
||||||
passfile2_cmd = [
|
|
||||||
"sh", "-c",
|
|
||||||
f"echo {master_pass} > {passfile2_path}"
|
|
||||||
]
|
|
||||||
|
|
||||||
self.docker.exec_command("dovecot-mailcow", passfile1_cmd)
|
|
||||||
self.docker.exec_command("dovecot-mailcow", passfile2_cmd)
|
|
||||||
|
|
||||||
imapsync_cmd = [
|
|
||||||
"/usr/local/bin/imapsync",
|
|
||||||
"--tmpdir", "/tmp",
|
|
||||||
"--nofoldersizes",
|
|
||||||
"--addheader"
|
|
||||||
]
|
|
||||||
|
|
||||||
if int(syncjob['timeout1']) > 0:
|
|
||||||
imapsync_cmd.extend(['--timeout1', str(syncjob['timeout1'])])
|
|
||||||
if int(syncjob['timeout2']) > 0:
|
|
||||||
imapsync_cmd.extend(['--timeout2', str(syncjob['timeout2'])])
|
|
||||||
if syncjob['exclude']:
|
|
||||||
imapsync_cmd.extend(['--exclude', syncjob['exclude']])
|
|
||||||
if syncjob['subfolder2']:
|
|
||||||
imapsync_cmd.extend(['--subfolder2', syncjob['subfolder2']])
|
|
||||||
if int(syncjob['maxage']) > 0:
|
|
||||||
imapsync_cmd.extend(['--maxage', str(syncjob['maxage'])])
|
|
||||||
if int(syncjob['maxbytespersecond']) > 0:
|
|
||||||
imapsync_cmd.extend(['--maxbytespersecond', str(syncjob['maxbytespersecond'])])
|
|
||||||
if int(syncjob['delete2duplicates']) == 1:
|
|
||||||
imapsync_cmd.append("--delete2duplicates")
|
|
||||||
if int(syncjob['subscribeall']) == 1:
|
|
||||||
imapsync_cmd.append("--subscribeall")
|
|
||||||
if int(syncjob['delete1']) == 1:
|
|
||||||
imapsync_cmd.append("--delete")
|
|
||||||
if int(syncjob['delete2']) == 1:
|
|
||||||
imapsync_cmd.append("--delete2")
|
|
||||||
if int(syncjob['automap']) == 1:
|
|
||||||
imapsync_cmd.append("--automap")
|
|
||||||
if int(syncjob['skipcrossduplicates']) == 1:
|
|
||||||
imapsync_cmd.append("--skipcrossduplicates")
|
|
||||||
if enc1_flag:
|
|
||||||
imapsync_cmd.append(enc1_flag)
|
|
||||||
|
|
||||||
imapsync_cmd.extend([
|
|
||||||
"--host1", syncjob['host1'],
|
|
||||||
"--user1", syncjob['user1'],
|
|
||||||
"--passfile1", passfile1_path,
|
|
||||||
"--port1", str(syncjob['port1']),
|
|
||||||
"--host2", "localhost",
|
|
||||||
"--user2", f"{syncjob['user2']}*{master_user}",
|
|
||||||
"--passfile2", passfile2_path
|
|
||||||
])
|
|
||||||
|
|
||||||
if syncjob['dry'] == 1:
|
|
||||||
imapsync_cmd.append("--dry")
|
|
||||||
|
|
||||||
imapsync_cmd.extend([
|
|
||||||
"--no-modulesversion",
|
|
||||||
"--noreleasecheck"
|
|
||||||
])
|
|
||||||
|
|
||||||
try:
|
|
||||||
cursor.execute("UPDATE imapsync SET is_running = 1, success = NULL, exit_status = NULL WHERE id = %s", (id,))
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
result = self.docker.exec_command("dovecot-mailcow", imapsync_cmd)
|
|
||||||
print(result)
|
|
||||||
|
|
||||||
success = result['status'] == "success" and result['exit_code'] == 0
|
|
||||||
cursor.execute(
|
|
||||||
"UPDATE imapsync SET returned_text = %s, success = %s, exit_status = %s WHERE id = %s",
|
|
||||||
(result['output'], int(success), result['exit_code'], id)
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
cursor.execute(
|
|
||||||
"UPDATE imapsync SET returned_text = %s, success = 0 WHERE id = %s",
|
|
||||||
(str(e), id)
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
finally:
|
|
||||||
cursor.execute("UPDATE imapsync SET last_run = NOW(), is_running = 0 WHERE id = %s", (id,))
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
delete_passfile1_cmd = [
|
|
||||||
"sh", "-c",
|
|
||||||
f"rm -f {passfile1_path}"
|
|
||||||
]
|
|
||||||
delete_passfile2_cmd = [
|
|
||||||
"sh", "-c",
|
|
||||||
f"rm -f {passfile2_path}"
|
|
||||||
]
|
|
||||||
self.docker.exec_command("dovecot-mailcow", delete_passfile1_cmd)
|
|
||||||
self.docker.exec_command("dovecot-mailcow", delete_passfile2_cmd)
|
|
||||||
|
|
||||||
cursor.close()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return "Sync job completed successfully." if success else "Sync job failed."
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import smtplib
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from email.mime.text import MIMEText
|
|
||||||
from email.mime.multipart import MIMEMultipart
|
|
||||||
from jinja2 import Environment, BaseLoader
|
|
||||||
|
|
||||||
class Mailer:
|
|
||||||
def __init__(self, smtp_host, smtp_port, username, password, use_tls=True):
|
|
||||||
self.smtp_host = smtp_host
|
|
||||||
self.smtp_port = smtp_port
|
|
||||||
self.username = username
|
|
||||||
self.password = password
|
|
||||||
self.use_tls = use_tls
|
|
||||||
self.server = None
|
|
||||||
self.env = Environment(loader=BaseLoader())
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
print("Connecting to the SMTP server...")
|
|
||||||
self.server = smtplib.SMTP(self.smtp_host, self.smtp_port)
|
|
||||||
if self.use_tls:
|
|
||||||
self.server.starttls()
|
|
||||||
print("TLS activated!")
|
|
||||||
if self.username and self.password:
|
|
||||||
self.server.login(self.username, self.password)
|
|
||||||
print("Authenticated!")
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
if self.server:
|
|
||||||
try:
|
|
||||||
if self.server.sock:
|
|
||||||
self.server.quit()
|
|
||||||
except smtplib.SMTPServerDisconnected:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
self.server = None
|
|
||||||
|
|
||||||
def render_inline_template(self, template_string, context):
|
|
||||||
template = self.env.from_string(template_string)
|
|
||||||
return template.render(context)
|
|
||||||
|
|
||||||
def send_mail(self, subject, from_addr, to_addrs, template, context = {}):
|
|
||||||
try:
|
|
||||||
if template == "":
|
|
||||||
print("Cannot send email, template is empty!")
|
|
||||||
return "Failed: Template is empty."
|
|
||||||
|
|
||||||
body = self.render_inline_template(template, context)
|
|
||||||
|
|
||||||
msg = MIMEMultipart()
|
|
||||||
msg['From'] = from_addr
|
|
||||||
msg['To'] = ', '.join(to_addrs) if isinstance(to_addrs, list) else to_addrs
|
|
||||||
msg['Subject'] = subject
|
|
||||||
msg.attach(MIMEText(body, 'html'))
|
|
||||||
|
|
||||||
self.connect()
|
|
||||||
self.server.sendmail(from_addr, to_addrs, msg.as_string())
|
|
||||||
self.disconnect()
|
|
||||||
return f"Success: Email sent to {msg['To']}"
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error during send_mail: {type(e).__name__}: {e}")
|
|
||||||
return f"Failed: {type(e).__name__}: {e}"
|
|
||||||
finally:
|
|
||||||
self.disconnect()
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
from jinja2 import Environment, Template
|
|
||||||
import csv
|
|
||||||
|
|
||||||
def split_at(value, sep, idx):
|
|
||||||
try:
|
|
||||||
return value.split(sep)[idx]
|
|
||||||
except Exception:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
class Reader:
|
|
||||||
"""
|
|
||||||
Reader class to handle reading and processing of CSV and JSON files for mailcow.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def read_csv(self, file_path, delimiter=',', encoding='iso-8859-1'):
|
|
||||||
"""
|
|
||||||
Read a CSV file and return a list of dictionaries.
|
|
||||||
Each dictionary represents a row in the CSV file.
|
|
||||||
:param file_path: Path to the CSV file.
|
|
||||||
:param delimiter: Delimiter used in the CSV file (default: ',').
|
|
||||||
"""
|
|
||||||
with open(file_path, mode='r', encoding=encoding) as file:
|
|
||||||
reader = csv.DictReader(file, delimiter=delimiter)
|
|
||||||
reader.fieldnames = [h.replace(" ", "_") if h else h for h in reader.fieldnames]
|
|
||||||
return [row for row in reader]
|
|
||||||
|
|
||||||
def map_csv_data(self, data, mapping_file_path, encoding='iso-8859-1'):
|
|
||||||
"""
|
|
||||||
Map CSV data to a specific structure based on the provided Jinja2 template file.
|
|
||||||
:param data: List of dictionaries representing CSV rows.
|
|
||||||
:param mapping_file_path: Path to the Jinja2 template file.
|
|
||||||
:return: List of dictionaries with mapped data.
|
|
||||||
"""
|
|
||||||
with open(mapping_file_path, 'r', encoding=encoding) as tpl_file:
|
|
||||||
template_content = tpl_file.read()
|
|
||||||
env = Environment()
|
|
||||||
env.filters['split_at'] = split_at
|
|
||||||
template = env.from_string(template_content)
|
|
||||||
|
|
||||||
mapped_data = []
|
|
||||||
for row in data:
|
|
||||||
rendered = template.render(**row)
|
|
||||||
try:
|
|
||||||
mapped_row = eval(rendered)
|
|
||||||
except Exception:
|
|
||||||
mapped_row = rendered
|
|
||||||
mapped_data.append(mapped_row)
|
|
||||||
return mapped_data
|
|
||||||
@@ -1,512 +0,0 @@
|
|||||||
import requests
|
|
||||||
import urllib3
|
|
||||||
import os
|
|
||||||
from uuid import uuid4
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
|
|
||||||
class Sogo:
|
|
||||||
def __init__(self, username, password=""):
|
|
||||||
self.apiUrl = "/SOGo/so"
|
|
||||||
self.davUrl = "/SOGo/dav"
|
|
||||||
self.ignore_ssl_errors = True
|
|
||||||
|
|
||||||
self.baseUrl = f"https://{os.getenv('IPv4_NETWORK', '172.22.1')}.247:{os.getenv('HTTPS_PORT', '443')}"
|
|
||||||
self.host = os.getenv("MAILCOW_HOSTNAME", "")
|
|
||||||
if self.ignore_ssl_errors:
|
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
||||||
self.username = username
|
|
||||||
self.password = password
|
|
||||||
|
|
||||||
def addCalendar(self, calendar_name):
|
|
||||||
"""
|
|
||||||
Add a new calendar to the sogo instance.
|
|
||||||
:param calendar_name: Name of the calendar to be created
|
|
||||||
:return: Response from the sogo API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
res = self.post(f"/{self.username}/Calendar/createFolder", {
|
|
||||||
"name": calendar_name
|
|
||||||
})
|
|
||||||
try:
|
|
||||||
return res.json()
|
|
||||||
except ValueError:
|
|
||||||
return res.text
|
|
||||||
|
|
||||||
def getCalendarIdByName(self, calendar_name):
|
|
||||||
"""
|
|
||||||
Get the calendar ID by its name.
|
|
||||||
:param calendar_name: Name of the calendar to find
|
|
||||||
:return: Calendar ID if found, otherwise None.
|
|
||||||
"""
|
|
||||||
|
|
||||||
res = self.get(f"/{self.username}/Calendar/calendarslist")
|
|
||||||
try:
|
|
||||||
for calendar in res.json()["calendars"]:
|
|
||||||
if calendar['name'] == calendar_name:
|
|
||||||
return calendar['id']
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
def getCalendar(self):
|
|
||||||
"""
|
|
||||||
Get calendar list.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
res = self.get(f"/{self.username}/Calendar/calendarslist")
|
|
||||||
try:
|
|
||||||
return res.json()
|
|
||||||
except ValueError:
|
|
||||||
return res.text
|
|
||||||
|
|
||||||
def deleteCalendar(self, calendar_id):
|
|
||||||
"""
|
|
||||||
Delete a calendar.
|
|
||||||
:param calendar_id: ID of the calendar to be deleted
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
res = self.get(f"/{self.username}/Calendar/{calendar_id}/delete")
|
|
||||||
return res.status_code == 204
|
|
||||||
|
|
||||||
def importCalendar(self, calendar_name, ics_file):
|
|
||||||
"""
|
|
||||||
Import a calendar from an ICS file.
|
|
||||||
:param calendar_name: Name of the calendar to import into
|
|
||||||
:param ics_file: Path to the ICS file to import
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(ics_file, "rb") as f:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Could not open ICS file '{ics_file}': {e}")
|
|
||||||
return {"status": "error", "message": str(e)}
|
|
||||||
|
|
||||||
new_calendar = self.addCalendar(calendar_name)
|
|
||||||
selected_calendar = new_calendar.json()["id"]
|
|
||||||
|
|
||||||
url = f"{self.baseUrl}{self.apiUrl}/{self.username}/Calendar/{selected_calendar}/import"
|
|
||||||
auth = (self.username, self.password)
|
|
||||||
with open(ics_file, "rb") as f:
|
|
||||||
files = {'icsFile': (ics_file, f, 'text/calendar')}
|
|
||||||
res = requests.post(
|
|
||||||
url,
|
|
||||||
files=files,
|
|
||||||
auth=auth,
|
|
||||||
verify=not self.ignore_ssl_errors
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
return res.json()
|
|
||||||
except ValueError:
|
|
||||||
return res.text
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def setCalendarACL(self, calendar_id, sharee_email, acl="r", subscribe=False):
|
|
||||||
"""
|
|
||||||
Set CalDAV calendar permissions for a user (sharee).
|
|
||||||
:param calendar_id: ID of the calendar to share
|
|
||||||
:param sharee_email: Email of the user to share with
|
|
||||||
:param acl: "w" for write, "r" for read-only or combination "rw" for read-write
|
|
||||||
:param subscribe: True will scubscribe the sharee to the calendar
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Access rights
|
|
||||||
if acl == "" or len(acl) > 2:
|
|
||||||
return "Invalid acl level specified. Use 'w', 'r' or combinations like 'rw'."
|
|
||||||
rights = [{
|
|
||||||
"c_email": sharee_email,
|
|
||||||
"uid": sharee_email,
|
|
||||||
"userClass": "normal-user",
|
|
||||||
"rights": {
|
|
||||||
"Public": "None",
|
|
||||||
"Private": "None",
|
|
||||||
"Confidential": "None",
|
|
||||||
"canCreateObjects": 0,
|
|
||||||
"canEraseObjects": 0
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
if "w" in acl:
|
|
||||||
rights[0]["rights"]["canCreateObjects"] = 1
|
|
||||||
rights[0]["rights"]["canEraseObjects"] = 1
|
|
||||||
if "r" in acl:
|
|
||||||
rights[0]["rights"]["Public"] = "Viewer"
|
|
||||||
rights[0]["rights"]["Private"] = "Viewer"
|
|
||||||
rights[0]["rights"]["Confidential"] = "Viewer"
|
|
||||||
|
|
||||||
r_add = self.get(f"/{self.username}/Calendar/{calendar_id}/addUserInAcls?uid={sharee_email}")
|
|
||||||
if r_add.status_code < 200 or r_add.status_code > 299:
|
|
||||||
try:
|
|
||||||
return r_add.json()
|
|
||||||
except ValueError:
|
|
||||||
return r_add.text
|
|
||||||
|
|
||||||
r_save = self.post(f"/{self.username}/Calendar/{calendar_id}/saveUserRights", rights)
|
|
||||||
if r_save.status_code < 200 or r_save.status_code > 299:
|
|
||||||
try:
|
|
||||||
return r_save.json()
|
|
||||||
except ValueError:
|
|
||||||
return r_save.text
|
|
||||||
|
|
||||||
if subscribe:
|
|
||||||
r_subscribe = self.get(f"/{self.username}/Calendar/{calendar_id}/subscribeUsers?uids={sharee_email}")
|
|
||||||
if r_subscribe.status_code < 200 or r_subscribe.status_code > 299:
|
|
||||||
try:
|
|
||||||
return r_subscribe.json()
|
|
||||||
except ValueError:
|
|
||||||
return r_subscribe.text
|
|
||||||
|
|
||||||
return r_save.status_code == 200
|
|
||||||
|
|
||||||
def getCalendarACL(self, calendar_id):
|
|
||||||
"""
|
|
||||||
Get CalDAV calendar permissions for a user (sharee).
|
|
||||||
:param calendar_id: ID of the calendar to get ACL from
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
res = self.get(f"/{self.username}/Calendar/{calendar_id}/acls")
|
|
||||||
try:
|
|
||||||
return res.json()
|
|
||||||
except ValueError:
|
|
||||||
return res.text
|
|
||||||
|
|
||||||
def deleteCalendarACL(self, calendar_id, sharee_email):
|
|
||||||
"""
|
|
||||||
Delete a calendar ACL for a user (sharee).
|
|
||||||
:param calendar_id: ID of the calendar to delete ACL from
|
|
||||||
:param sharee_email: Email of the user whose ACL to delete
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
res = self.get(f"/{self.username}/Calendar/{calendar_id}/removeUserFromAcls?uid={sharee_email}")
|
|
||||||
return res.status_code == 204
|
|
||||||
|
|
||||||
def addAddressbook(self, addressbook_name):
|
|
||||||
"""
|
|
||||||
Add a new addressbook to the sogo instance.
|
|
||||||
:param addressbook_name: Name of the addressbook to be created
|
|
||||||
:return: Response from the sogo API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
res = self.post(f"/{self.username}/Contacts/createFolder", {
|
|
||||||
"name": addressbook_name
|
|
||||||
})
|
|
||||||
try:
|
|
||||||
return res.json()
|
|
||||||
except ValueError:
|
|
||||||
return res.text
|
|
||||||
|
|
||||||
def getAddressbookIdByName(self, addressbook_name):
|
|
||||||
"""
|
|
||||||
Get the addressbook ID by its name.
|
|
||||||
:param addressbook_name: Name of the addressbook to find
|
|
||||||
:return: Addressbook ID if found, otherwise None.
|
|
||||||
"""
|
|
||||||
|
|
||||||
res = self.get(f"/{self.username}/Contacts/addressbooksList")
|
|
||||||
try:
|
|
||||||
for addressbook in res.json()["addressbooks"]:
|
|
||||||
if addressbook['name'] == addressbook_name:
|
|
||||||
return addressbook['id']
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
def deleteAddressbook(self, addressbook_id):
|
|
||||||
"""
|
|
||||||
Delete an addressbook.
|
|
||||||
:param addressbook_id: ID of the addressbook to be deleted
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/delete")
|
|
||||||
return res.status_code == 204
|
|
||||||
|
|
||||||
def getAddressbookList(self):
|
|
||||||
"""
|
|
||||||
Get addressbook list.
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
res = self.get(f"/{self.username}/Contacts/addressbooksList")
|
|
||||||
try:
|
|
||||||
return res.json()
|
|
||||||
except ValueError:
|
|
||||||
return res.text
|
|
||||||
|
|
||||||
def setAddressbookACL(self, addressbook_id, sharee_email, acl="r", subscribe=False):
|
|
||||||
"""
|
|
||||||
Set CalDAV addressbook permissions for a user (sharee).
|
|
||||||
:param addressbook_id: ID of the addressbook to share
|
|
||||||
:param sharee_email: Email of the user to share with
|
|
||||||
:param acl: "w" for write, "r" for read-only or combination "rw" for read-write
|
|
||||||
:param subscribe: True will subscribe the sharee to the addressbook
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Access rights
|
|
||||||
if acl == "" or len(acl) > 2:
|
|
||||||
print("Invalid acl level specified. Use 's', 'w', 'r' or combinations like 'rws'.")
|
|
||||||
return "Invalid acl level specified. Use 'w', 'r' or combinations like 'rw'."
|
|
||||||
rights = [{
|
|
||||||
"c_email": sharee_email,
|
|
||||||
"uid": sharee_email,
|
|
||||||
"userClass": "normal-user",
|
|
||||||
"rights": {
|
|
||||||
"canCreateObjects": 0,
|
|
||||||
"canEditObjects": 0,
|
|
||||||
"canEraseObjects": 0,
|
|
||||||
"canViewObjects": 0,
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
if "w" in acl:
|
|
||||||
rights[0]["rights"]["canCreateObjects"] = 1
|
|
||||||
rights[0]["rights"]["canEditObjects"] = 1
|
|
||||||
rights[0]["rights"]["canEraseObjects"] = 1
|
|
||||||
if "r" in acl:
|
|
||||||
rights[0]["rights"]["canViewObjects"] = 1
|
|
||||||
|
|
||||||
r_add = self.get(f"/{self.username}/Contacts/{addressbook_id}/addUserInAcls?uid={sharee_email}")
|
|
||||||
if r_add.status_code < 200 or r_add.status_code > 299:
|
|
||||||
try:
|
|
||||||
return r_add.json()
|
|
||||||
except ValueError:
|
|
||||||
return r_add.text
|
|
||||||
|
|
||||||
r_save = self.post(f"/{self.username}/Contacts/{addressbook_id}/saveUserRights", rights)
|
|
||||||
if r_save.status_code < 200 or r_save.status_code > 299:
|
|
||||||
try:
|
|
||||||
return r_save.json()
|
|
||||||
except ValueError:
|
|
||||||
return r_save.text
|
|
||||||
|
|
||||||
if subscribe:
|
|
||||||
r_subscribe = self.get(f"/{self.username}/Contacts/{addressbook_id}/subscribeUsers?uids={sharee_email}")
|
|
||||||
if r_subscribe.status_code < 200 or r_subscribe.status_code > 299:
|
|
||||||
try:
|
|
||||||
return r_subscribe.json()
|
|
||||||
except ValueError:
|
|
||||||
return r_subscribe.text
|
|
||||||
|
|
||||||
return r_save.status_code == 200
|
|
||||||
|
|
||||||
def getAddressbookACL(self, addressbook_id):
|
|
||||||
"""
|
|
||||||
Get CalDAV addressbook permissions for a user (sharee).
|
|
||||||
:param addressbook_id: ID of the addressbook to get ACL from
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/acls")
|
|
||||||
try:
|
|
||||||
return res.json()
|
|
||||||
except ValueError:
|
|
||||||
return res.text
|
|
||||||
|
|
||||||
def deleteAddressbookACL(self, addressbook_id, sharee_email):
|
|
||||||
"""
|
|
||||||
Delete an addressbook ACL for a user (sharee).
|
|
||||||
:param addressbook_id: ID of the addressbook to delete ACL from
|
|
||||||
:param sharee_email: Email of the user whose ACL to delete
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/removeUserFromAcls?uid={sharee_email}")
|
|
||||||
return res.status_code == 204
|
|
||||||
|
|
||||||
def getAddressbookNewGuid(self, addressbook_id):
|
|
||||||
"""
|
|
||||||
Request a new GUID for a SOGo addressbook.
|
|
||||||
:param addressbook_id: ID of the addressbook
|
|
||||||
:return: JSON response from SOGo or None if not found
|
|
||||||
"""
|
|
||||||
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/newguid")
|
|
||||||
try:
|
|
||||||
return res.json()
|
|
||||||
except ValueError:
|
|
||||||
return res.text
|
|
||||||
|
|
||||||
def addAddressbookContact(self, addressbook_id, contact_name, contact_email):
|
|
||||||
"""
|
|
||||||
Save a vCard as a contact in the specified addressbook.
|
|
||||||
:param addressbook_id: ID of the addressbook
|
|
||||||
:param contact_name: Name of the contact
|
|
||||||
:param contact_email: Email of the contact
|
|
||||||
:return: JSON response from SOGo or None if not found
|
|
||||||
"""
|
|
||||||
vcard_id = self.getAddressbookNewGuid(addressbook_id)
|
|
||||||
contact_data = {
|
|
||||||
"id": vcard_id["id"],
|
|
||||||
"pid": vcard_id["pid"],
|
|
||||||
"c_cn": contact_name,
|
|
||||||
"emails": [{
|
|
||||||
"type": "pref",
|
|
||||||
"value": contact_email
|
|
||||||
}],
|
|
||||||
"isNew": True,
|
|
||||||
"c_component": "vcard",
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint = f"/{self.username}/Contacts/{addressbook_id}/{vcard_id['id']}/saveAsContact"
|
|
||||||
res = self.post(endpoint, contact_data)
|
|
||||||
try:
|
|
||||||
return res.json()
|
|
||||||
except ValueError:
|
|
||||||
return res.text
|
|
||||||
|
|
||||||
def getAddressbookContacts(self, addressbook_id, contact_email=None):
|
|
||||||
"""
|
|
||||||
Get all contacts from the specified addressbook.
|
|
||||||
:param addressbook_id: ID of the addressbook
|
|
||||||
:return: JSON response with contacts or None if not found
|
|
||||||
"""
|
|
||||||
res = self.get(f"/{self.username}/Contacts/{addressbook_id}/view")
|
|
||||||
try:
|
|
||||||
res_json = res.json()
|
|
||||||
headers = res_json.get("headers", [])
|
|
||||||
if not headers or len(headers) < 2:
|
|
||||||
return []
|
|
||||||
|
|
||||||
field_names = headers[0]
|
|
||||||
contacts = []
|
|
||||||
for row in headers[1:]:
|
|
||||||
contact = dict(zip(field_names, row))
|
|
||||||
contacts.append(contact)
|
|
||||||
|
|
||||||
if contact_email:
|
|
||||||
contact = {}
|
|
||||||
for c in contacts:
|
|
||||||
if c["c_mail"] == contact_email or c["c_cn"] == contact_email:
|
|
||||||
contact = c
|
|
||||||
break
|
|
||||||
return contact
|
|
||||||
|
|
||||||
return contacts
|
|
||||||
except ValueError:
|
|
||||||
return res.text
|
|
||||||
|
|
||||||
def addAddressbookContactList(self, addressbook_id, contact_name, contact_email=None):
|
|
||||||
"""
|
|
||||||
Add a new contact list to the addressbook.
|
|
||||||
:param addressbook_id: ID of the addressbook
|
|
||||||
:param contact_name: Name of the contact list
|
|
||||||
:param contact_email: Comma-separated emails to include in the list
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
gal_domain = self.username.split("@")[-1]
|
|
||||||
vlist_id = self.getAddressbookNewGuid(addressbook_id)
|
|
||||||
contact_emails = contact_email.split(",") if contact_email else []
|
|
||||||
contacts = self.getAddressbookContacts(addressbook_id)
|
|
||||||
|
|
||||||
refs = []
|
|
||||||
for contact in contacts:
|
|
||||||
if contact['c_mail'] in contact_emails:
|
|
||||||
refs.append({
|
|
||||||
"refs": [],
|
|
||||||
"categories": [],
|
|
||||||
"c_screenname": contact.get("c_screenname", ""),
|
|
||||||
"pid": contact.get("pid", vlist_id["pid"]),
|
|
||||||
"id": contact.get("id", ""),
|
|
||||||
"notes": [""],
|
|
||||||
"empty": " ",
|
|
||||||
"hasphoto": contact.get("hasphoto", 0),
|
|
||||||
"c_cn": contact.get("c_cn", ""),
|
|
||||||
"c_uid": contact.get("c_uid", None),
|
|
||||||
"containername": contact.get("containername", f"GAL {gal_domain}"), # or your addressbook name
|
|
||||||
"sourceid": contact.get("sourceid", gal_domain),
|
|
||||||
"c_component": contact.get("c_component", "vcard"),
|
|
||||||
"c_sn": contact.get("c_sn", ""),
|
|
||||||
"c_givenname": contact.get("c_givenname", ""),
|
|
||||||
"c_name": contact.get("c_name", contact.get("id", "")),
|
|
||||||
"c_telephonenumber": contact.get("c_telephonenumber", ""),
|
|
||||||
"fn": contact.get("fn", ""),
|
|
||||||
"c_mail": contact.get("c_mail", ""),
|
|
||||||
"emails": contact.get("emails", []),
|
|
||||||
"c_o": contact.get("c_o", ""),
|
|
||||||
"reference": contact.get("id", ""),
|
|
||||||
"birthday": contact.get("birthday", "")
|
|
||||||
})
|
|
||||||
|
|
||||||
contact_data = {
|
|
||||||
"refs": refs,
|
|
||||||
"categories": [],
|
|
||||||
"c_screenname": None,
|
|
||||||
"pid": vlist_id["pid"],
|
|
||||||
"c_component": "vlist",
|
|
||||||
"notes": [""],
|
|
||||||
"empty": " ",
|
|
||||||
"isNew": True,
|
|
||||||
"id": vlist_id["id"],
|
|
||||||
"c_cn": contact_name,
|
|
||||||
"birthday": ""
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint = f"/{self.username}/Contacts/{addressbook_id}/{vlist_id['id']}/saveAsList"
|
|
||||||
res = self.post(endpoint, contact_data)
|
|
||||||
try:
|
|
||||||
return res.json()
|
|
||||||
except ValueError:
|
|
||||||
return res.text
|
|
||||||
|
|
||||||
def deleteAddressbookItem(self, addressbook_id, contact_name):
|
|
||||||
"""
|
|
||||||
Delete an addressbook item by its ID.
|
|
||||||
:param addressbook_id: ID of the addressbook item to delete
|
|
||||||
:param contact_name: Name of the contact to delete
|
|
||||||
:return: Response from SOGo API.
|
|
||||||
"""
|
|
||||||
res = self.getAddressbookContacts(addressbook_id, contact_name)
|
|
||||||
|
|
||||||
if "id" not in res:
|
|
||||||
print(f"Contact '{contact_name}' not found in addressbook '{addressbook_id}'.")
|
|
||||||
return None
|
|
||||||
res = self.post(f"/{self.username}/Contacts/{addressbook_id}/batchDelete", {
|
|
||||||
"uids": [res["id"]],
|
|
||||||
})
|
|
||||||
return res.status_code == 204
|
|
||||||
|
|
||||||
def get(self, endpoint, params=None):
|
|
||||||
"""
|
|
||||||
Make a GET request to the mailcow API.
|
|
||||||
:param endpoint: The API endpoint to get.
|
|
||||||
:param params: Optional parameters for the GET request.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
url = f"{self.baseUrl}{self.apiUrl}{endpoint}"
|
|
||||||
auth = (self.username, self.password)
|
|
||||||
headers = {"Host": self.host}
|
|
||||||
|
|
||||||
response = requests.get(
|
|
||||||
url,
|
|
||||||
params=params,
|
|
||||||
auth=auth,
|
|
||||||
headers=headers,
|
|
||||||
verify=not self.ignore_ssl_errors
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
def post(self, endpoint, data):
|
|
||||||
"""
|
|
||||||
Make a POST request to the mailcow API.
|
|
||||||
:param endpoint: The API endpoint to post to.
|
|
||||||
:param data: Data to be sent in the POST request.
|
|
||||||
:return: Response from the mailcow API.
|
|
||||||
"""
|
|
||||||
url = f"{self.baseUrl}{self.apiUrl}{endpoint}"
|
|
||||||
auth = (self.username, self.password)
|
|
||||||
headers = {"Host": self.host}
|
|
||||||
|
|
||||||
response = requests.post(
|
|
||||||
url,
|
|
||||||
json=data,
|
|
||||||
auth=auth,
|
|
||||||
headers=headers,
|
|
||||||
verify=not self.ignore_ssl_errors
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import json
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
|
|
||||||
|
|
||||||
class Utils:
|
|
||||||
def __init(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def normalize_email(self, email):
|
|
||||||
replacements = {
|
|
||||||
"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss",
|
|
||||||
"Ä": "Ae", "Ö": "Oe", "Ü": "Ue"
|
|
||||||
}
|
|
||||||
for orig, repl in replacements.items():
|
|
||||||
email = email.replace(orig, repl)
|
|
||||||
return email
|
|
||||||
|
|
||||||
def generate_password(self, length=8):
|
|
||||||
chars = string.ascii_letters + string.digits
|
|
||||||
return ''.join(random.choices(chars, k=length))
|
|
||||||
|
|
||||||
def pprint(self, data=""):
|
|
||||||
"""
|
|
||||||
Pretty print a dictionary, list, or text.
|
|
||||||
If data is a text containing JSON, it will be printed in a formatted way.
|
|
||||||
"""
|
|
||||||
if isinstance(data, (dict, list)):
|
|
||||||
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
||||||
elif isinstance(data, str):
|
|
||||||
try:
|
|
||||||
json_data = json.loads(data)
|
|
||||||
print(json.dumps(json_data, indent=2, ensure_ascii=False))
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
print(data)
|
|
||||||
else:
|
|
||||||
print(data)
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
jinja2
|
|
||||||
requests
|
|
||||||
mysql-connector-python
|
|
||||||
pytest
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import pytest
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
|
|
||||||
from models.DomainModel import DomainModel
|
|
||||||
from models.AliasModel import AliasModel
|
|
||||||
|
|
||||||
|
|
||||||
def test_model():
|
|
||||||
# Generate random alias
|
|
||||||
random_alias = f"alias_test{os.urandom(4).hex()}@mailcow.local"
|
|
||||||
|
|
||||||
# Create an instance of AliasModel
|
|
||||||
model = AliasModel(
|
|
||||||
address=random_alias,
|
|
||||||
goto="test@mailcow.local,test2@mailcow.local"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test the parser_command attribute
|
|
||||||
assert model.parser_command == "alias", "Parser command should be 'alias'"
|
|
||||||
|
|
||||||
# add Domain for testing
|
|
||||||
domain_model = DomainModel(domain="mailcow.local")
|
|
||||||
domain_model.add()
|
|
||||||
|
|
||||||
# 1. Alias add tests, should success
|
|
||||||
r_add = model.add()
|
|
||||||
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['type'] == "success", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
|
|
||||||
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 3, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['msg'][0] == "alias_added", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'alias_added'\n{json.dumps(r_add, indent=2)}"
|
|
||||||
|
|
||||||
# Assign created alias ID for further tests
|
|
||||||
model.id = r_add[0]['msg'][2]
|
|
||||||
|
|
||||||
# 2. Alias add tests, should fail because the alias already exists
|
|
||||||
r_add = model.add()
|
|
||||||
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
|
|
||||||
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['msg'][0] == "is_alias_or_mailbox", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'is_alias_or_mailbox'\n{json.dumps(r_add, indent=2)}"
|
|
||||||
|
|
||||||
# 3. Alias get tests
|
|
||||||
r_get = model.get()
|
|
||||||
assert isinstance(r_get, dict), f"Expected a dict but received: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert "domain" in r_get, f"'domain' key missing in response: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert "goto" in r_get, f"'goto' key missing in response: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert "address" in r_get, f"'address' key missing in response: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert r_get['domain'] == model.address.split("@")[1], f"Wrong 'domain' received: {r_get['domain']}, expected: {model.address.split('@')[1]}\n{json.dumps(r_get, indent=2)}"
|
|
||||||
assert r_get['goto'] == model.goto, f"Wrong 'goto' received: {r_get['goto']}, expected: {model.goto}\n{json.dumps(r_get, indent=2)}"
|
|
||||||
assert r_get['address'] == model.address, f"Wrong 'address' received: {r_get['address']}, expected: {model.address}\n{json.dumps(r_get, indent=2)}"
|
|
||||||
|
|
||||||
# 4. Alias edit tests
|
|
||||||
model.goto = "test@mailcow.local"
|
|
||||||
model.active = 0
|
|
||||||
r_edit = model.edit()
|
|
||||||
assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}"
|
|
||||||
assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert r_edit[0]['msg'][0] == "alias_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'alias_modified'\n{json.dumps(r_edit, indent=2)}"
|
|
||||||
|
|
||||||
# 5. Alias delete tests
|
|
||||||
r_delete = model.delete()
|
|
||||||
assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}"
|
|
||||||
assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert r_delete[0]['msg'][0] == "alias_removed", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'alias_removed'\n{json.dumps(r_delete, indent=2)}"
|
|
||||||
|
|
||||||
# delete testing Domain
|
|
||||||
domain_model.delete()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("Running AliasModel tests...")
|
|
||||||
test_model()
|
|
||||||
print("All tests passed!")
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import pytest
|
|
||||||
from models.BaseModel import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class Args:
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
for key, value in kwargs.items():
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
|
|
||||||
def test_has_required_args():
|
|
||||||
BaseModel.required_args = {
|
|
||||||
"test_object": [["arg1"], ["arg2", "arg3"]],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test cases with Args object
|
|
||||||
args = Args(object="non_existent_object")
|
|
||||||
assert BaseModel.has_required_args(args) == False
|
|
||||||
|
|
||||||
args = Args(object="test_object")
|
|
||||||
assert BaseModel.has_required_args(args) == False
|
|
||||||
|
|
||||||
args = Args(object="test_object", arg1="value")
|
|
||||||
assert BaseModel.has_required_args(args) == True
|
|
||||||
|
|
||||||
args = Args(object="test_object", arg2="value")
|
|
||||||
assert BaseModel.has_required_args(args) == False
|
|
||||||
|
|
||||||
args = Args(object="test_object", arg3="value")
|
|
||||||
assert BaseModel.has_required_args(args) == False
|
|
||||||
|
|
||||||
args = Args(object="test_object", arg2="value", arg3="value")
|
|
||||||
assert BaseModel.has_required_args(args) == True
|
|
||||||
|
|
||||||
# Test cases with dict object
|
|
||||||
args = {"object": "non_existent_object"}
|
|
||||||
assert BaseModel.has_required_args(args) == False
|
|
||||||
|
|
||||||
args = {"object": "test_object"}
|
|
||||||
assert BaseModel.has_required_args(args) == False
|
|
||||||
|
|
||||||
args = {"object": "test_object", "arg1": "value"}
|
|
||||||
assert BaseModel.has_required_args(args) == True
|
|
||||||
|
|
||||||
args = {"object": "test_object", "arg2": "value"}
|
|
||||||
assert BaseModel.has_required_args(args) == False
|
|
||||||
|
|
||||||
args = {"object": "test_object", "arg3": "value"}
|
|
||||||
assert BaseModel.has_required_args(args) == False
|
|
||||||
|
|
||||||
args = {"object": "test_object", "arg2": "value", "arg3": "value"}
|
|
||||||
assert BaseModel.has_required_args(args) == True
|
|
||||||
|
|
||||||
|
|
||||||
BaseModel.required_args = {
|
|
||||||
"test_object": [[]],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test cases with Args object
|
|
||||||
args = Args(object="non_existent_object")
|
|
||||||
assert BaseModel.has_required_args(args) == False
|
|
||||||
|
|
||||||
args = Args(object="test_object")
|
|
||||||
assert BaseModel.has_required_args(args) == True
|
|
||||||
|
|
||||||
# Test cases with dict object
|
|
||||||
args = {"object": "non_existent_object"}
|
|
||||||
assert BaseModel.has_required_args(args) == False
|
|
||||||
|
|
||||||
args = {"object": "test_object"}
|
|
||||||
assert BaseModel.has_required_args(args) == True
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import pytest
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
|
|
||||||
from models.DomainModel import DomainModel
|
|
||||||
|
|
||||||
|
|
||||||
def test_model():
|
|
||||||
# Create an instance of DomainModel
|
|
||||||
model = DomainModel(
|
|
||||||
domain="mailcow.local",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test the parser_command attribute
|
|
||||||
assert model.parser_command == "domain", "Parser command should be 'domain'"
|
|
||||||
|
|
||||||
# 1. Domain add tests, should success
|
|
||||||
r_add = model.add()
|
|
||||||
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add) > 0 and len(r_add) >= 2, f"Wrong array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert "type" in r_add[1], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[1]['type'] == "success", f"Wrong 'type' received: {r_add[1]['type']}\n{json.dumps(r_add, indent=2)}"
|
|
||||||
assert "msg" in r_add[1], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert isinstance(r_add[1]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add[1]['msg']) > 0 and len(r_add[1]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[1]['msg'][0] == "domain_added", f"Wrong 'msg' received: {r_add[1]['msg'][0]}, expected: 'domain_added'\n{json.dumps(r_add, indent=2)}"
|
|
||||||
|
|
||||||
# 2. Domain add tests, should fail because the domain already exists
|
|
||||||
r_add = model.add()
|
|
||||||
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
|
|
||||||
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['msg'][0] == "domain_exists", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'domain_exists'\n{json.dumps(r_add, indent=2)}"
|
|
||||||
|
|
||||||
# 3. Domain get tests
|
|
||||||
r_get = model.get()
|
|
||||||
assert isinstance(r_get, dict), f"Expected a dict but received: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert "domain_name" in r_get, f"'domain_name' key missing in response: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert r_get['domain_name'] == model.domain, f"Wrong 'domain_name' received: {r_get['domain_name']}, expected: {model.domain}\n{json.dumps(r_get, indent=2)}"
|
|
||||||
|
|
||||||
# 4. Domain edit tests
|
|
||||||
model.active = 0
|
|
||||||
r_edit = model.edit()
|
|
||||||
assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}"
|
|
||||||
assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert r_edit[0]['msg'][0] == "domain_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'domain_modified'\n{json.dumps(r_edit, indent=2)}"
|
|
||||||
|
|
||||||
# 5. Domain delete tests
|
|
||||||
r_delete = model.delete()
|
|
||||||
assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}"
|
|
||||||
assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert r_delete[0]['msg'][0] == "domain_removed", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'domain_removed'\n{json.dumps(r_delete, indent=2)}"
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("Running DomainModel tests...")
|
|
||||||
test_model()
|
|
||||||
print("All tests passed!")
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import pytest
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
|
|
||||||
from models.DomainModel import DomainModel
|
|
||||||
from models.DomainadminModel import DomainadminModel
|
|
||||||
|
|
||||||
|
|
||||||
def test_model():
|
|
||||||
# Generate random domainadmin
|
|
||||||
random_username = f"dadmin_test{os.urandom(4).hex()}"
|
|
||||||
random_password = f"{os.urandom(4).hex()}"
|
|
||||||
|
|
||||||
# Create an instance of DomainadminModel
|
|
||||||
model = DomainadminModel(
|
|
||||||
username=random_username,
|
|
||||||
password=random_password,
|
|
||||||
domains="mailcow.local",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test the parser_command attribute
|
|
||||||
assert model.parser_command == "domainadmin", "Parser command should be 'domainadmin'"
|
|
||||||
|
|
||||||
# add Domain for testing
|
|
||||||
domain_model = DomainModel(domain="mailcow.local")
|
|
||||||
domain_model.add()
|
|
||||||
|
|
||||||
# 1. Domainadmin add tests, should success
|
|
||||||
r_add = model.add()
|
|
||||||
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['type'] == "success", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
|
|
||||||
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 3, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['msg'][0] == "domain_admin_added", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'domain_admin_added'\n{json.dumps(r_add, indent=2)}"
|
|
||||||
|
|
||||||
# 2. Domainadmin add tests, should fail because the domainadmin already exists
|
|
||||||
r_add = model.add()
|
|
||||||
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
|
|
||||||
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['msg'][0] == "object_exists", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'object_exists'\n{json.dumps(r_add, indent=2)}"
|
|
||||||
|
|
||||||
# 3. Domainadmin get tests
|
|
||||||
r_get = model.get()
|
|
||||||
assert isinstance(r_get, dict), f"Expected a dict but received: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert "selected_domains" in r_get, f"'selected_domains' key missing in response: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert "username" in r_get, f"'username' key missing in response: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert set(model.domains.replace(" ", "").split(",")) == set(r_get['selected_domains']), f"Wrong 'selected_domains' received: {r_get['selected_domains']}, expected: {model.domains}\n{json.dumps(r_get, indent=2)}"
|
|
||||||
assert r_get['username'] == model.username, f"Wrong 'username' received: {r_get['username']}, expected: {model.username}\n{json.dumps(r_get, indent=2)}"
|
|
||||||
|
|
||||||
# 4. Domainadmin edit tests
|
|
||||||
model.active = 0
|
|
||||||
r_edit = model.edit()
|
|
||||||
assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}"
|
|
||||||
assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert r_edit[0]['msg'][0] == "domain_admin_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'domain_admin_modified'\n{json.dumps(r_edit, indent=2)}"
|
|
||||||
|
|
||||||
# 5. Domainadmin delete tests
|
|
||||||
r_delete = model.delete()
|
|
||||||
assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}"
|
|
||||||
assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert r_delete[0]['msg'][0] == "domain_admin_removed", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'domain_admin_removed'\n{json.dumps(r_delete, indent=2)}"
|
|
||||||
|
|
||||||
# delete testing Domain
|
|
||||||
domain_model.delete()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("Running DomainadminModel tests...")
|
|
||||||
test_model()
|
|
||||||
print("All tests passed!")
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import pytest
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
|
|
||||||
from models.DomainModel import DomainModel
|
|
||||||
from models.MailboxModel import MailboxModel
|
|
||||||
|
|
||||||
|
|
||||||
def test_model():
|
|
||||||
# Generate random mailbox
|
|
||||||
random_username = f"mbox_test{os.urandom(4).hex()}@mailcow.local"
|
|
||||||
random_password = f"{os.urandom(4).hex()}"
|
|
||||||
|
|
||||||
# Create an instance of MailboxModel
|
|
||||||
model = MailboxModel(
|
|
||||||
username=random_username,
|
|
||||||
password=random_password
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test the parser_command attribute
|
|
||||||
assert model.parser_command == "mailbox", "Parser command should be 'mailbox'"
|
|
||||||
|
|
||||||
# add Domain for testing
|
|
||||||
domain_model = DomainModel(domain="mailcow.local")
|
|
||||||
domain_model.add()
|
|
||||||
|
|
||||||
# 1. Mailbox add tests, should success
|
|
||||||
r_add = model.add()
|
|
||||||
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add) > 0 and len(r_add) <= 2, f"Wrong array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert "type" in r_add[1], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[1]['type'] == "success", f"Wrong 'type' received: {r_add[1]['type']}\n{json.dumps(r_add, indent=2)}"
|
|
||||||
assert "msg" in r_add[1], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert isinstance(r_add[1]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add[1]['msg']) > 0 and len(r_add[1]['msg']) <= 3, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[1]['msg'][0] == "mailbox_added", f"Wrong 'msg' received: {r_add[1]['msg'][0]}, expected: 'mailbox_added'\n{json.dumps(r_add, indent=2)}"
|
|
||||||
|
|
||||||
# 2. Mailbox add tests, should fail because the mailbox already exists
|
|
||||||
r_add = model.add()
|
|
||||||
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
|
|
||||||
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['msg'][0] == "object_exists", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'object_exists'\n{json.dumps(r_add, indent=2)}"
|
|
||||||
|
|
||||||
# 3. Mailbox get tests
|
|
||||||
r_get = model.get()
|
|
||||||
assert isinstance(r_get, dict), f"Expected a dict but received: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert "domain" in r_get, f"'domain' key missing in response: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert "local_part" in r_get, f"'local_part' key missing in response: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert r_get['domain'] == model.domain, f"Wrong 'domain' received: {r_get['domain']}, expected: {model.domain}\n{json.dumps(r_get, indent=2)}"
|
|
||||||
assert r_get['local_part'] == model.local_part, f"Wrong 'local_part' received: {r_get['local_part']}, expected: {model.local_part}\n{json.dumps(r_get, indent=2)}"
|
|
||||||
|
|
||||||
# 4. Mailbox edit tests
|
|
||||||
model.active = 0
|
|
||||||
r_edit = model.edit()
|
|
||||||
assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}"
|
|
||||||
assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert r_edit[0]['msg'][0] == "mailbox_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'mailbox_modified'\n{json.dumps(r_edit, indent=2)}"
|
|
||||||
|
|
||||||
# 5. Mailbox delete tests
|
|
||||||
r_delete = model.delete()
|
|
||||||
assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}"
|
|
||||||
assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert r_delete[0]['msg'][0] == "mailbox_removed", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'mailbox_removed'\n{json.dumps(r_delete, indent=2)}"
|
|
||||||
|
|
||||||
# delete testing Domain
|
|
||||||
domain_model.delete()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("Running MailboxModel tests...")
|
|
||||||
test_model()
|
|
||||||
print("All tests passed!")
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import pytest
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
|
|
||||||
from models.StatusModel import StatusModel
|
|
||||||
|
|
||||||
|
|
||||||
def test_model():
|
|
||||||
# Create an instance of StatusModel
|
|
||||||
model = StatusModel()
|
|
||||||
|
|
||||||
# Test the parser_command attribute
|
|
||||||
assert model.parser_command == "status", "Parser command should be 'status'"
|
|
||||||
|
|
||||||
# 1. Status version tests
|
|
||||||
r_version = model.version()
|
|
||||||
assert isinstance(r_version, dict), f"Expected a dict but received: {json.dumps(r_version, indent=2)}"
|
|
||||||
assert "version" in r_version, f"'version' key missing in response: {json.dumps(r_version, indent=2)}"
|
|
||||||
|
|
||||||
# 2. Status vmail tests
|
|
||||||
r_vmail = model.vmail()
|
|
||||||
assert isinstance(r_vmail, dict), f"Expected a dict but received: {json.dumps(r_vmail, indent=2)}"
|
|
||||||
assert "type" in r_vmail, f"'type' key missing in response: {json.dumps(r_vmail, indent=2)}"
|
|
||||||
assert "disk" in r_vmail, f"'disk' key missing in response: {json.dumps(r_vmail, indent=2)}"
|
|
||||||
assert "used" in r_vmail, f"'used' key missing in response: {json.dumps(r_vmail, indent=2)}"
|
|
||||||
assert "total" in r_vmail, f"'total' key missing in response: {json.dumps(r_vmail, indent=2)}"
|
|
||||||
assert "used_percent" in r_vmail, f"'used_percent' key missing in response: {json.dumps(r_vmail, indent=2)}"
|
|
||||||
|
|
||||||
# 3. Status containers tests
|
|
||||||
r_containers = model.containers()
|
|
||||||
assert isinstance(r_containers, dict), f"Expected a dict but received: {json.dumps(r_containers, indent=2)}"
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("Running StatusModel tests...")
|
|
||||||
test_model()
|
|
||||||
print("All tests passed!")
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import pytest
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
|
|
||||||
from models.DomainModel import DomainModel
|
|
||||||
from models.MailboxModel import MailboxModel
|
|
||||||
from models.SyncjobModel import SyncjobModel
|
|
||||||
|
|
||||||
|
|
||||||
def test_model():
|
|
||||||
# Generate random Mailbox
|
|
||||||
random_username = f"mbox_test@mailcow.local"
|
|
||||||
random_password = f"{os.urandom(4).hex()}"
|
|
||||||
|
|
||||||
# Create an instance of SyncjobModel
|
|
||||||
model = SyncjobModel(
|
|
||||||
username=random_username,
|
|
||||||
host1="mailcow.local",
|
|
||||||
port1=993,
|
|
||||||
user1="testuser@mailcow.local",
|
|
||||||
password1="testpassword",
|
|
||||||
enc1="SSL",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test the parser_command attribute
|
|
||||||
assert model.parser_command == "syncjob", "Parser command should be 'syncjob'"
|
|
||||||
|
|
||||||
# add Domain and Mailbox for testing
|
|
||||||
domain_model = DomainModel(domain="mailcow.local")
|
|
||||||
domain_model.add()
|
|
||||||
mbox_model = MailboxModel(username=random_username, password=random_password)
|
|
||||||
mbox_model.add()
|
|
||||||
|
|
||||||
# 1. Syncjob add tests, should success
|
|
||||||
r_add = model.add()
|
|
||||||
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add) > 0 and len(r_add) <= 2, f"Wrong array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['type'] == "success", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
|
|
||||||
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 3, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['msg'][0] == "mailbox_modified", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'mailbox_modified'\n{json.dumps(r_add, indent=2)}"
|
|
||||||
|
|
||||||
# Assign created syncjob ID for further tests
|
|
||||||
model.id = r_add[0]['msg'][2]
|
|
||||||
|
|
||||||
# 2. Syncjob add tests, should fail because the syncjob already exists
|
|
||||||
r_add = model.add()
|
|
||||||
assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}"
|
|
||||||
assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}"
|
|
||||||
assert r_add[0]['msg'][0] == "object_exists", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'object_exists'\n{json.dumps(r_add, indent=2)}"
|
|
||||||
|
|
||||||
# 3. Syncjob get tests
|
|
||||||
r_get = model.get()
|
|
||||||
assert isinstance(r_get, list), f"Expected a list but received: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert "user2" in r_get[0], f"'user2' key missing in response: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert "host1" in r_get[0], f"'host1' key missing in response: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert "port1" in r_get[0], f"'port1' key missing in response: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert "user1" in r_get[0], f"'user1' key missing in response: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert "enc1" in r_get[0], f"'enc1' key missing in response: {json.dumps(r_get, indent=2)}"
|
|
||||||
assert r_get[0]['user2'] == model.username, f"Wrong 'user2' received: {r_get[0]['user2']}, expected: {model.username}\n{json.dumps(r_get, indent=2)}"
|
|
||||||
assert r_get[0]['host1'] == model.host1, f"Wrong 'host1' received: {r_get[0]['host1']}, expected: {model.host1}\n{json.dumps(r_get, indent=2)}"
|
|
||||||
assert r_get[0]['port1'] == model.port1, f"Wrong 'port1' received: {r_get[0]['port1']}, expected: {model.port1}\n{json.dumps(r_get, indent=2)}"
|
|
||||||
assert r_get[0]['user1'] == model.user1, f"Wrong 'user1' received: {r_get[0]['user1']}, expected: {model.user1}\n{json.dumps(r_get, indent=2)}"
|
|
||||||
assert r_get[0]['enc1'] == model.enc1, f"Wrong 'enc1' received: {r_get[0]['enc1']}, expected: {model.enc1}\n{json.dumps(r_get, indent=2)}"
|
|
||||||
|
|
||||||
# 4. Syncjob edit tests
|
|
||||||
model.active = 1
|
|
||||||
r_edit = model.edit()
|
|
||||||
assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}"
|
|
||||||
assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}"
|
|
||||||
assert r_edit[0]['msg'][0] == "mailbox_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'mailbox_modified'\n{json.dumps(r_edit, indent=2)}"
|
|
||||||
|
|
||||||
# 5. Syncjob delete tests
|
|
||||||
r_delete = model.delete()
|
|
||||||
assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}"
|
|
||||||
assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}"
|
|
||||||
assert r_delete[0]['msg'][0] == "deleted_syncjob", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'deleted_syncjob'\n{json.dumps(r_delete, indent=2)}"
|
|
||||||
|
|
||||||
# delete testing Domain and Mailbox
|
|
||||||
mbox_model.delete()
|
|
||||||
domain_model.delete()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("Running SyncjobModel tests...")
|
|
||||||
test_model()
|
|
||||||
print("All tests passed!")
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
printf "READY\n";
|
|
||||||
|
|
||||||
while read line; do
|
|
||||||
echo "Processing Event: $line" >&2;
|
|
||||||
kill -3 $(cat "/var/run/supervisord.pid")
|
|
||||||
done < /dev/stdin
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
[supervisord]
|
|
||||||
nodaemon=true
|
|
||||||
user=root
|
|
||||||
pidfile=/var/run/supervisord.pid
|
|
||||||
|
|
||||||
[program:api]
|
|
||||||
command=python /app/api/main.py
|
|
||||||
autostart=true
|
|
||||||
autorestart=true
|
|
||||||
stdout_logfile=/dev/stdout
|
|
||||||
stderr_logfile=/dev/stderr
|
|
||||||
stdout_logfile_maxbytes=0
|
|
||||||
stderr_logfile_maxbytes=0
|
|
||||||
|
|
||||||
[eventlistener:processes]
|
|
||||||
command=/usr/local/sbin/stop-supervisor.sh
|
|
||||||
events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
@@ -6,29 +6,22 @@ ARG PIP_BREAK_SYSTEM_PACKAGES=1
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --update --no-cache python3 \
|
RUN apk add --update --no-cache python3 \
|
||||||
bash \
|
|
||||||
py3-pip \
|
py3-pip \
|
||||||
openssl \
|
openssl \
|
||||||
tzdata \
|
tzdata \
|
||||||
py3-psutil \
|
py3-psutil \
|
||||||
py3-redis \
|
py3-redis \
|
||||||
py3-async-timeout \
|
py3-async-timeout \
|
||||||
supervisor \
|
|
||||||
curl \
|
|
||||||
&& pip3 install --upgrade pip \
|
&& pip3 install --upgrade pip \
|
||||||
fastapi \
|
fastapi \
|
||||||
uvicorn \
|
uvicorn \
|
||||||
aiodocker \
|
aiodocker \
|
||||||
docker
|
docker
|
||||||
|
RUN mkdir /app/modules
|
||||||
COPY mailcow-adm/ /app/mailcow-adm/
|
|
||||||
RUN pip3 install -r /app/mailcow-adm/requirements.txt
|
|
||||||
|
|
||||||
COPY api/ /app/api/
|
|
||||||
|
|
||||||
COPY docker-entrypoint.sh /app/
|
COPY docker-entrypoint.sh /app/
|
||||||
COPY supervisord.conf /etc/supervisor/supervisord.conf
|
COPY main.py /app/main.py
|
||||||
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
COPY modules/ /app/modules/
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]
|
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]
|
||||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
CMD ["python", "main.py"]
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
`openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
|
||||||
|
-keyout /app/dockerapi_key.pem \
|
||||||
|
-out /app/dockerapi_cert.pem \
|
||||||
|
-subj /CN=dockerapi/O=mailcow \
|
||||||
|
-addext subjectAltName=DNS:dockerapi`
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
@@ -110,12 +110,12 @@ async def get_container(container_id : str):
|
|||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
@app.get("/containers/json")
|
@app.get("/containers/json")
|
||||||
async def get_containers():
|
async def get_containers(all: bool = False):
|
||||||
global dockerapi
|
global dockerapi
|
||||||
|
|
||||||
containers = {}
|
containers = {}
|
||||||
try:
|
try:
|
||||||
for container in (await dockerapi.async_docker_client.containers.list()):
|
for container in (await dockerapi.async_docker_client.containers.list(all=all)):
|
||||||
container_info = await container.show()
|
container_info = await container.show()
|
||||||
containers.update({container_info['Id']: container_info})
|
containers.update({container_info['Id']: container_info})
|
||||||
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
||||||
@@ -254,8 +254,8 @@ if __name__ == '__main__':
|
|||||||
app,
|
app,
|
||||||
host="0.0.0.0",
|
host="0.0.0.0",
|
||||||
port=443,
|
port=443,
|
||||||
ssl_certfile="/app/controller_cert.pem",
|
ssl_certfile="/app/dockerapi_cert.pem",
|
||||||
ssl_keyfile="/app/controller_key.pem",
|
ssl_keyfile="/app/dockerapi_key.pem",
|
||||||
log_level="info",
|
log_level="info",
|
||||||
loop="none"
|
loop="none"
|
||||||
)
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.22
|
||||||
|
|
||||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
|||||||
@@ -44,90 +44,109 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
|||||||
else
|
else
|
||||||
QUOTA_TABLE=quota2replica
|
QUOTA_TABLE=quota2replica
|
||||||
fi
|
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
|
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-quota.conf
|
||||||
# Autogenerated by mailcow
|
# Autogenerated by mailcow
|
||||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
dict_map priv/quota/storage {
|
||||||
map {
|
sql_table = ${QUOTA_TABLE}
|
||||||
pattern = priv/quota/storage
|
|
||||||
table = ${QUOTA_TABLE}
|
|
||||||
username_field = username
|
username_field = username
|
||||||
value_field = bytes
|
value_field bytes {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
map {
|
|
||||||
pattern = priv/quota/messages
|
dict_map priv/quota/messages {
|
||||||
table = ${QUOTA_TABLE}
|
sql_table = ${QUOTA_TABLE}
|
||||||
username_field = username
|
username_field = username
|
||||||
value_field = messages
|
value_field messages {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Create dict used for sieve pre and postfilters
|
# Create dict used for sieve pre and postfilters
|
||||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
|
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
|
||||||
# Autogenerated by mailcow
|
# Autogenerated by mailcow
|
||||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
|
||||||
map {
|
dict_map priv/sieve/name/\$script_name {
|
||||||
pattern = priv/sieve/name/\$script_name
|
sql_table = sieve_before
|
||||||
table = sieve_before
|
|
||||||
username_field = username
|
username_field = username
|
||||||
value_field = id
|
value_field id {
|
||||||
fields {
|
}
|
||||||
script_name = \$script_name
|
|
||||||
|
# The script name field in the table to query
|
||||||
|
key_field script_name {
|
||||||
|
value = \$script_name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
map {
|
|
||||||
pattern = priv/sieve/data/\$id
|
dict_map priv/sieve/data/\$id {
|
||||||
table = sieve_before
|
sql_table = sieve_before
|
||||||
username_field = username
|
username_field = username
|
||||||
value_field = script_data
|
value_field script_data {
|
||||||
fields {
|
}
|
||||||
id = \$id
|
key_field id {
|
||||||
|
value = \$id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
|
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
|
||||||
# Autogenerated by mailcow
|
# Autogenerated by mailcow
|
||||||
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
|
|
||||||
map {
|
dict_map priv/sieve/name/\$script_name {
|
||||||
pattern = priv/sieve/name/\$script_name
|
sql_table = sieve_after
|
||||||
table = sieve_after
|
|
||||||
username_field = username
|
username_field = username
|
||||||
value_field = id
|
value_field id {
|
||||||
fields {
|
}
|
||||||
script_name = \$script_name
|
key_field script_name {
|
||||||
|
value = \$script_name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
map {
|
|
||||||
pattern = priv/sieve/data/\$id
|
dict_map priv/sieve/data/\$id {
|
||||||
table = sieve_after
|
sql_table = sieve_after
|
||||||
username_field = username
|
username_field = username
|
||||||
value_field = script_data
|
value_field script_data {
|
||||||
fields {
|
}
|
||||||
id = \$id
|
key_field id {
|
||||||
|
value = \$id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
|
if [[ "${ACL_ANYONE}" == "allow" ]]; then
|
||||||
|
echo -n "yes" > /etc/dovecot/acl_anyone
|
||||||
|
else
|
||||||
|
echo -n "no" > /etc/dovecot/acl_anyone
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "${SKIP_FTS}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
if [[ "${SKIP_FTS}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
echo -e "\e[33mDetecting SKIP_FTS=y... not enabling Flatcurve (FTS) then...\e[0m"
|
echo -e "\e[33mDetecting SKIP_FTS=y... not enabling Flatcurve (FTS) then...\e[0m"
|
||||||
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication lazy_expunge' > /etc/dovecot/mail_plugins
|
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 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 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 sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
|
echo -n 'quota quota_clone sieve acl mail_crypt mail_crypt_acl mail_compress notify' > /etc/dovecot/mail_plugins_lmtp
|
||||||
else
|
else
|
||||||
echo -e "\e[32mDetecting SKIP_FTS=n... enabling Flatcurve (FTS)\e[0m"
|
echo -e "\e[32mDetecting SKIP_FTS=n... enabling Flatcurve (FTS)\e[0m"
|
||||||
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 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 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 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 sieve acl zlib mail_crypt mail_crypt_acl fts fts_flatcurve notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
|
echo -n 'quota quota_clone sieve acl mail_crypt mail_crypt_acl mail_compress fts fts_flatcurve notify' > /etc/dovecot/mail_plugins_lmtp
|
||||||
fi
|
fi
|
||||||
chmod 644 /etc/dovecot/mail_plugins /etc/dovecot/mail_plugins_imap /etc/dovecot/mail_plugins_lmtp /templates/quarantine.tpl
|
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
|
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
|
||||||
# Autogenerated by mailcow
|
# Autogenerated by mailcow
|
||||||
driver = mysql
|
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')
|
||||||
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';
|
iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -158,8 +177,8 @@ for cert_dir in /etc/ssl/mail/*/ ; do
|
|||||||
domains=($(cat ${cert_dir}domains))
|
domains=($(cat ${cert_dir}domains))
|
||||||
for domain in ${domains[@]}; do
|
for domain in ${domains[@]}; do
|
||||||
echo 'local_name '${domain}' {' >> /etc/dovecot/sni.conf;
|
echo 'local_name '${domain}' {' >> /etc/dovecot/sni.conf;
|
||||||
echo ' ssl_cert = <'${cert_dir}'cert.pem' >> /etc/dovecot/sni.conf;
|
echo ' ssl_server_cert_file = '${cert_dir}'cert.pem' >> /etc/dovecot/sni.conf;
|
||||||
echo ' ssl_key = <'${cert_dir}'key.pem' >> /etc/dovecot/sni.conf;
|
echo ' ssl_server_key_file = '${cert_dir}'key.pem' >> /etc/dovecot/sni.conf;
|
||||||
echo '}' >> /etc/dovecot/sni.conf;
|
echo '}' >> /etc/dovecot/sni.conf;
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
@@ -183,11 +202,13 @@ else
|
|||||||
fi
|
fi
|
||||||
cat <<EOF > /etc/dovecot/shared_namespace.conf
|
cat <<EOF > /etc/dovecot/shared_namespace.conf
|
||||||
# Autogenerated by mailcow
|
# Autogenerated by mailcow
|
||||||
namespace {
|
namespace shared {
|
||||||
type = shared
|
type = shared
|
||||||
separator = /
|
separator = /
|
||||||
prefix = Shared/%%u/
|
prefix = Shared/\$user/
|
||||||
location = maildir:%%h${MAILDIR_SUB_SHARED}:INDEX=~${MAILDIR_SUB_SHARED}/Shared/%%u
|
mail_driver = maildir
|
||||||
|
mail_path = %{owner_home}${MAILDIR_SUB_SHARED}
|
||||||
|
mail_index_private_path = ~${MAILDIR_SUB_SHARED}/Shared/%{owner_user}
|
||||||
subscriptions = no
|
subscriptions = no
|
||||||
list = children
|
list = children
|
||||||
}
|
}
|
||||||
@@ -197,7 +218,7 @@ EOF
|
|||||||
cat <<EOF > /etc/dovecot/sogo_trusted_ip.conf
|
cat <<EOF > /etc/dovecot/sogo_trusted_ip.conf
|
||||||
# Autogenerated by mailcow
|
# Autogenerated by mailcow
|
||||||
remote ${IPV4_NETWORK}.248 {
|
remote ${IPV4_NETWORK}.248 {
|
||||||
disable_plaintext_auth = no
|
auth_allow_cleartext = yes
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -206,9 +227,13 @@ 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
|
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
|
||||||
cat <<EOF > /etc/dovecot/sogo-sso.conf
|
cat <<EOF > /etc/dovecot/sogo-sso.conf
|
||||||
# Autogenerated by mailcow
|
# Autogenerated by mailcow
|
||||||
passdb {
|
passdb static {
|
||||||
driver = static
|
fields {
|
||||||
args = allow_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
|
allow_real_nets=${IPV4_NETWORK}.248/32
|
||||||
|
}
|
||||||
|
|
||||||
|
password={plain}${RAND_PASS}
|
||||||
|
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
@@ -236,9 +261,9 @@ fi
|
|||||||
if [[ "${SKIP_FTS}" =~ ^([nN][oO]|[nN])+$ ]]; then
|
if [[ "${SKIP_FTS}" =~ ^([nN][oO]|[nN])+$ ]]; then
|
||||||
echo -e "\e[94mConfiguring FTS Settings...\e[0m"
|
echo -e "\e[94mConfiguring FTS Settings...\e[0m"
|
||||||
echo -e "\e[94mSetting FTS Memory Limit (per process) to ${FTS_HEAP} MB\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/fts.conf
|
sed -i "s/vsz_limit\s*=\s*[0-9]*\s*MB*/vsz_limit=${FTS_HEAP} MB/" /etc/dovecot/conf.d/35-fts.conf
|
||||||
echo -e "\e[94mSetting FTS Process Limit to ${FTS_PROCS}\e[0m"
|
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/fts.conf
|
sed -i "s/process_limit\s*=\s*[0-9]*/process_limit=${FTS_PROCS}/" /etc/dovecot/conf.d/35-fts.conf
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 401 is user dovecot
|
# 401 is user dovecot
|
||||||
@@ -250,16 +275,16 @@ else
|
|||||||
chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem
|
chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
|
# # 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
|
# 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
|
# sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf
|
||||||
|
|
||||||
echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf
|
# echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf
|
||||||
echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf
|
# echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf
|
||||||
echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
|
# echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
|
||||||
echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
|
# echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
|
||||||
echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
|
# echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
|
||||||
fi
|
# fi
|
||||||
|
|
||||||
# Compile sieve scripts
|
# Compile sieve scripts
|
||||||
sievec /var/vmail/sieve/global_sieve_before.sieve
|
sievec /var/vmail/sieve/global_sieve_before.sieve
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ sed -i -e 's/\([^\\]\)\$\([^\/]\)/\1\\$\2/g' /etc/rspamd/custom/sa-rules
|
|||||||
|
|
||||||
if [[ "$(cat /etc/rspamd/custom/sa-rules | md5sum | cut -d' ' -f1)" != "${HASH_SA_RULES}" ]]; then
|
if [[ "$(cat /etc/rspamd/custom/sa-rules | md5sum | cut -d' ' -f1)" != "${HASH_SA_RULES}" ]]; then
|
||||||
CONTAINER_NAME=rspamd-mailcow
|
CONTAINER_NAME=rspamd-mailcow
|
||||||
CONTAINER_ID=$(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | \
|
CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | \
|
||||||
jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | \
|
jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | \
|
||||||
jq -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")
|
jq -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")
|
||||||
if [[ ! -z ${CONTAINER_ID} ]]; then
|
if [[ ! -z ${CONTAINER_ID} ]]; then
|
||||||
curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
|
curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
@@ -40,4 +40,4 @@ COPY ./docker-entrypoint.sh /app/
|
|||||||
|
|
||||||
RUN chmod +x /app/docker-entrypoint.sh
|
RUN chmod +x /app/docker-entrypoint.sh
|
||||||
|
|
||||||
CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"]
|
CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ ARG MEMCACHED_PECL_VERSION=3.4.0
|
|||||||
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||||
ARG REDIS_PECL_VERSION=6.3.0
|
ARG REDIS_PECL_VERSION=6.3.0
|
||||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||||
ARG COMPOSER_VERSION=2.8.6
|
ARG COMPOSER_VERSION=2.9.5
|
||||||
|
|
||||||
RUN apk add -U --no-cache autoconf \
|
RUN apk add -U --no-cache autoconf \
|
||||||
aspell-dev \
|
aspell-dev \
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ session.save_path = "tcp://'${REDIS_HOST}':'${REDIS_PORT}'?auth='${REDISPASS}'"
|
|||||||
# Check mysql_upgrade (master and slave)
|
# Check mysql_upgrade (master and slave)
|
||||||
CONTAINER_ID=
|
CONTAINER_ID=
|
||||||
until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do
|
until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do
|
||||||
CONTAINER_ID=$(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
|
CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
|
||||||
echo "Could not get mysql-mailcow container id... trying again"
|
echo "Could not get mysql-mailcow container id... trying again"
|
||||||
sleep 2
|
sleep 2
|
||||||
done
|
done
|
||||||
@@ -44,7 +44,7 @@ until [[ ${SQL_UPGRADE_STATUS} == 'success' ]]; do
|
|||||||
echo "Tried to upgrade MySQL and failed, giving up after ${SQL_LOOP_C} retries and starting container (oops, not good)"
|
echo "Tried to upgrade MySQL and failed, giving up after ${SQL_LOOP_C} retries and starting container (oops, not good)"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json')
|
SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json')
|
||||||
SQL_UPGRADE_STATUS=$(echo ${SQL_FULL_UPGRADE_RETURN} | jq -r .type)
|
SQL_UPGRADE_STATUS=$(echo ${SQL_FULL_UPGRADE_RETURN} | jq -r .type)
|
||||||
SQL_LOOP_C=$((SQL_LOOP_C+1))
|
SQL_LOOP_C=$((SQL_LOOP_C+1))
|
||||||
echo "SQL upgrade iteration #${SQL_LOOP_C}"
|
echo "SQL upgrade iteration #${SQL_LOOP_C}"
|
||||||
@@ -69,12 +69,12 @@ done
|
|||||||
|
|
||||||
# doing post-installation stuff, if SQL was upgraded (master and slave)
|
# doing post-installation stuff, if SQL was upgraded (master and slave)
|
||||||
if [ ${SQL_CHANGED} -eq 1 ]; then
|
if [ ${SQL_CHANGED} -eq 1 ]; then
|
||||||
POSTFIX=$(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
|
POSTFIX=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
|
||||||
if [[ -z "${POSTFIX}" ]] || ! [[ "${POSTFIX}" =~ ^[[:alnum:]]*$ ]]; then
|
if [[ -z "${POSTFIX}" ]] || ! [[ "${POSTFIX}" =~ ^[[:alnum:]]*$ ]]; then
|
||||||
echo "Could not determine Postfix container ID, skipping Postfix restart."
|
echo "Could not determine Postfix container ID, skipping Postfix restart."
|
||||||
else
|
else
|
||||||
echo "Restarting Postfix"
|
echo "Restarting Postfix"
|
||||||
curl -X POST --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/restart | jq -r '.msg'
|
curl -X POST --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/restart | jq -r '.msg'
|
||||||
echo "Sleeping 5 seconds..."
|
echo "Sleeping 5 seconds..."
|
||||||
sleep 5
|
sleep 5
|
||||||
fi
|
fi
|
||||||
@@ -83,7 +83,7 @@ fi
|
|||||||
# Check mysql tz import (master and slave)
|
# Check mysql tz import (master and slave)
|
||||||
TZ_CHECK=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null)
|
TZ_CHECK=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null)
|
||||||
if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then
|
if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then
|
||||||
SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json')
|
SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json')
|
||||||
echo "MySQL mysql_tzinfo_to_sql - debug output:"
|
echo "MySQL mysql_tzinfo_to_sql - debug output:"
|
||||||
echo ${SQL_FULL_TZINFO_IMPORT_RETURN}
|
echo ${SQL_FULL_TZINFO_IMPORT_RETURN}
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,47 +1,161 @@
|
|||||||
FROM debian:bookworm-slim
|
# SOGo built from source to enable security patch application
|
||||||
|
# Repository: https://github.com/Alinto/sogo
|
||||||
|
# Version: SOGo-5.12.4
|
||||||
|
#
|
||||||
|
# Applied security patches:
|
||||||
|
# - 16ab99e7cf8db2c30b211f0d5e338d7f9e3a9efb: XSS vulnerability in theme parameter
|
||||||
|
#
|
||||||
|
# To add new patches, modify SOGO_SECURITY_PATCHES ARG below with space-separated commit hashes
|
||||||
|
|
||||||
|
FROM debian:bookworm
|
||||||
|
|
||||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ARG DEBIAN_VERSION=bookworm
|
ARG SOGO_VERSION=SOGo-5.12.4
|
||||||
ARG SOGO_DEBIAN_REPOSITORY=https://packagingv2.sogo.nu/sogo-nightly-debian/
|
ARG SOPE_VERSION=SOPE-5.12.4
|
||||||
|
# Security patches to apply (space-separated commit hashes)
|
||||||
|
ARG SOGO_SECURITY_PATCHES="16ab99e7cf8db2c30b211f0d5e338d7f9e3a9efb"
|
||||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||||
ARG GOSU_VERSION=1.19
|
ARG GOSU_VERSION=1.19
|
||||||
ENV LC_ALL=C
|
ENV LC_ALL=C
|
||||||
|
|
||||||
# Prerequisites
|
# Install dependencies, build SOPE and SOGo, then clean up (all in one layer to minimize image size)
|
||||||
RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
# Build dependencies
|
||||||
apt-transport-https \
|
git \
|
||||||
ca-certificates \
|
build-essential \
|
||||||
gettext \
|
gobjc \
|
||||||
gnupg \
|
gnustep-make \
|
||||||
mariadb-client \
|
gnustep-base-runtime \
|
||||||
rsync \
|
libgnustep-base-dev \
|
||||||
supervisor \
|
libxml2-dev \
|
||||||
syslog-ng \
|
libldap2-dev \
|
||||||
syslog-ng-core \
|
libssl-dev \
|
||||||
syslog-ng-mod-redis \
|
zlib1g-dev \
|
||||||
dirmngr \
|
libpq-dev \
|
||||||
netcat-traditional \
|
libmariadb-dev-compat \
|
||||||
psmisc \
|
libmemcached-dev \
|
||||||
wget \
|
libsodium-dev \
|
||||||
patch \
|
libcurl4-openssl-dev \
|
||||||
|
libzip-dev \
|
||||||
|
libytnef0-dev \
|
||||||
|
curl \
|
||||||
|
ca-certificates \
|
||||||
|
# Runtime dependencies
|
||||||
|
apt-transport-https \
|
||||||
|
gettext \
|
||||||
|
gnupg \
|
||||||
|
mariadb-client \
|
||||||
|
rsync \
|
||||||
|
supervisor \
|
||||||
|
syslog-ng \
|
||||||
|
syslog-ng-core \
|
||||||
|
syslog-ng-mod-redis \
|
||||||
|
dirmngr \
|
||||||
|
netcat-traditional \
|
||||||
|
psmisc \
|
||||||
|
wget \
|
||||||
|
patch \
|
||||||
|
libobjc4 \
|
||||||
|
libxml2 \
|
||||||
|
libldap-2.5-0 \
|
||||||
|
libssl3 \
|
||||||
|
zlib1g \
|
||||||
|
libmariadb3 \
|
||||||
|
libmemcached11 \
|
||||||
|
libsodium23 \
|
||||||
|
libcurl4 \
|
||||||
|
libzip4 \
|
||||||
|
libytnef0 \
|
||||||
|
# Download gosu
|
||||||
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
|
&& 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" \
|
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
|
||||||
&& chmod +x /usr/local/bin/gosu \
|
&& chmod +x /usr/local/bin/gosu \
|
||||||
&& gosu nobody true \
|
&& gosu nobody true \
|
||||||
&& mkdir /usr/share/doc/sogo \
|
# Build SOPE
|
||||||
&& touch /usr/share/doc/sogo/empty.sh \
|
&& git clone --depth 1 --branch ${SOPE_VERSION} https://github.com/Alinto/sope.git /tmp/sope \
|
||||||
&& wget -O- https://keys.openpgp.org/vks/v1/by-fingerprint/74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 | gpg --dearmor | apt-key add - \
|
&& cd /tmp/sope \
|
||||||
&& echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} main" > /etc/apt/sources.list.d/sogo.list \
|
&& rm -rf .git \
|
||||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||||
sogo \
|
&& ./configure --prefix=/usr --disable-debug --disable-strip \
|
||||||
sogo-activesync \
|
&& make -j$(nproc) \
|
||||||
&& apt-get autoclean \
|
&& make install \
|
||||||
|
&& cd / \
|
||||||
|
&& rm -rf /tmp/sope \
|
||||||
|
# Build SOGo with security patches
|
||||||
|
&& git clone --depth 1 --branch ${SOGO_VERSION} https://github.com/Alinto/sogo.git /tmp/sogo \
|
||||||
|
&& cd /tmp/sogo \
|
||||||
|
&& git config user.email "builder@mailcow.local" \
|
||||||
|
&& git config user.name "SOGo Builder" \
|
||||||
|
&& for patch in ${SOGO_SECURITY_PATCHES}; do \
|
||||||
|
echo "Applying security patch: ${patch}"; \
|
||||||
|
git fetch origin ${patch} && git cherry-pick ${patch}; \
|
||||||
|
done \
|
||||||
|
&& rm -rf .git \
|
||||||
|
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
|
||||||
|
&& ./configure --disable-debug --disable-strip \
|
||||||
|
&& make -j$(nproc) \
|
||||||
|
&& make install \
|
||||||
|
&& cd / \
|
||||||
|
&& rm -rf /tmp/sogo \
|
||||||
|
# Strip binaries
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogod 2>/dev/null || true \
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogo-tool 2>/dev/null || true \
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogo-ealarms-notify 2>/dev/null || true \
|
||||||
|
&& strip --strip-unneeded /usr/local/sbin/sogo-slapd-sockd 2>/dev/null || true \
|
||||||
|
# Remove build dependencies and clean up
|
||||||
|
&& apt-get purge -y --auto-remove \
|
||||||
|
git \
|
||||||
|
build-essential \
|
||||||
|
gobjc \
|
||||||
|
gnustep-make \
|
||||||
|
libgnustep-base-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libldap2-dev \
|
||||||
|
libssl-dev \
|
||||||
|
zlib1g-dev \
|
||||||
|
libpq-dev \
|
||||||
|
libmariadb-dev-compat \
|
||||||
|
libmemcached-dev \
|
||||||
|
libsodium-dev \
|
||||||
|
libcurl4-openssl-dev \
|
||||||
|
libzip-dev \
|
||||||
|
libytnef0-dev \
|
||||||
|
curl \
|
||||||
|
&& apt-get autoremove -y \
|
||||||
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& rm -rf /usr/share/doc/* \
|
||||||
|
&& rm -rf /usr/share/man/* \
|
||||||
|
&& rm -rf /var/cache/debconf/* \
|
||||||
|
&& rm -rf /tmp/* \
|
||||||
|
&& rm -rf /root/.cache \
|
||||||
|
&& find /usr/local/lib -name '*.a' -delete \
|
||||||
|
&& find /usr/lib -name '*.a' -delete \
|
||||||
|
&& mkdir -p /usr/share/doc/sogo \
|
||||||
|
&& touch /usr/share/doc/sogo/empty.sh \
|
||||||
&& touch /etc/default/locale
|
&& touch /etc/default/locale
|
||||||
|
|
||||||
|
# Configure library paths
|
||||||
|
RUN echo "/usr/lib64" > /etc/ld.so.conf.d/sogo.conf \
|
||||||
|
&& echo "/usr/local/lib/sogo" >> /etc/ld.so.conf.d/sogo.conf \
|
||||||
|
&& echo "/usr/local/lib/GNUstep/Frameworks/SOGo.framework/Versions/5/sogo" >> /etc/ld.so.conf.d/sogo.conf \
|
||||||
|
&& ldconfig
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Create symlinks for SOGo binaries
|
||||||
|
RUN ln -s /usr/local/sbin/sogod /usr/sbin/sogod \
|
||||||
|
&& ln -s /usr/local/sbin/sogo-tool /usr/sbin/sogo-tool \
|
||||||
|
&& ln -s /usr/local/sbin/sogo-ealarms-notify /usr/sbin/sogo-ealarms-notify \
|
||||||
|
&& ln -s /usr/local/sbin/sogo-slapd-sockd /usr/sbin/sogo-slapd-sockd
|
||||||
|
|
||||||
|
# Copy configuration files and scripts
|
||||||
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
|
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
|
||||||
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||||
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
|
||||||
@@ -56,4 +170,4 @@ RUN chmod +x /bootstrap-sogo.sh \
|
|||||||
|
|
||||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
|
|
||||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM alpine:3.21
|
FROM alpine:3.23
|
||||||
|
|
||||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
|||||||
@@ -19,19 +19,19 @@ if [ -z "$HOST" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# run dig and measure the time it takes to run
|
# run dig and measure the time it takes to run
|
||||||
START_TIME=$(date +%s%3N)
|
START_TIME=$(perl -MTime::HiRes -e 'print Time::HiRes::time')
|
||||||
dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null)
|
dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null)
|
||||||
dig_rc=$?
|
dig_rc=$?
|
||||||
|
END_TIME=$(perl -MTime::HiRes -e 'print Time::HiRes::time')
|
||||||
dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -)
|
dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -)
|
||||||
END_TIME=$(date +%s%3N)
|
ELAPSED_TIME=$(perl -e "printf('%.3f', $END_TIME - $START_TIME)")
|
||||||
ELAPSED_TIME=$((END_TIME - START_TIME))
|
|
||||||
|
|
||||||
# validate and perform nagios like output and exit codes
|
# validate and perform nagios like output and exit codes
|
||||||
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
|
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
|
||||||
echo "Domain $HOST was not found by the server"
|
echo "Domain $HOST was not found by the server"
|
||||||
exit 2
|
exit 2
|
||||||
elif [ $dig_rc -eq 0 ]; then
|
elif [ $dig_rc -eq 0 ]; then
|
||||||
echo "DNS OK: $ELAPSED_TIME ms response time. $HOST returns $dig_output_ips"
|
echo "DNS OK: $ELAPSED_TIME seconds response time. $HOST returns $dig_output_ips"
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo "Unknown error"
|
echo "Unknown error"
|
||||||
|
|||||||
@@ -200,12 +200,12 @@ get_container_ip() {
|
|||||||
else
|
else
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
# get long container id for exact match
|
# get long container id for exact match
|
||||||
CONTAINER_ID=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id"))
|
CONTAINER_ID=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id"))
|
||||||
# returned id can have multiple elements (if scaled), shuffle for random test
|
# returned id can have multiple elements (if scaled), shuffle for random test
|
||||||
CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf))
|
CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf))
|
||||||
if [[ ! -z ${CONTAINER_ID} ]]; then
|
if [[ ! -z ${CONTAINER_ID} ]]; then
|
||||||
for matched_container in "${CONTAINER_ID[@]}"; do
|
for matched_container in "${CONTAINER_ID[@]}"; do
|
||||||
CONTAINER_IPS=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress'))
|
CONTAINER_IPS=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress'))
|
||||||
for ip_match in "${CONTAINER_IPS[@]}"; do
|
for ip_match in "${CONTAINER_IPS[@]}"; do
|
||||||
# grep will do nothing if one of these vars is empty
|
# grep will do nothing if one of these vars is empty
|
||||||
[[ -z ${ip_match} ]] && continue
|
[[ -z ${ip_match} ]] && continue
|
||||||
@@ -1075,15 +1075,15 @@ while true; do
|
|||||||
done
|
done
|
||||||
) &
|
) &
|
||||||
|
|
||||||
# Monitor controller
|
# Monitor dockerapi
|
||||||
(
|
(
|
||||||
while true; do
|
while true; do
|
||||||
while nc -z controller 443; do
|
while nc -z dockerapi 443; do
|
||||||
sleep 3
|
sleep 3
|
||||||
done
|
done
|
||||||
log_msg "Cannot find controller-mailcow, waiting to recover..."
|
log_msg "Cannot find dockerapi-mailcow, waiting to recover..."
|
||||||
kill -STOP ${BACKGROUND_TASKS[*]}
|
kill -STOP ${BACKGROUND_TASKS[*]}
|
||||||
until nc -z controller 443; do
|
until nc -z dockerapi 443; do
|
||||||
sleep 3
|
sleep 3
|
||||||
done
|
done
|
||||||
kill -CONT ${BACKGROUND_TASKS[*]}
|
kill -CONT ${BACKGROUND_TASKS[*]}
|
||||||
@@ -1143,12 +1143,12 @@ while true; do
|
|||||||
elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
|
elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
|
||||||
kill -STOP ${BACKGROUND_TASKS[*]}
|
kill -STOP ${BACKGROUND_TASKS[*]}
|
||||||
sleep 10
|
sleep 10
|
||||||
CONTAINER_ID=$(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")
|
CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")
|
||||||
if [[ ! -z ${CONTAINER_ID} ]]; then
|
if [[ ! -z ${CONTAINER_ID} ]]; then
|
||||||
if [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then
|
if [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then
|
||||||
HAS_INITDB=$(curl --silent --insecure -XPOST https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true)
|
HAS_INITDB=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true)
|
||||||
fi
|
fi
|
||||||
S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d)))
|
S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d)))
|
||||||
if [ ${S_RUNNING} -lt 360 ]; then
|
if [ ${S_RUNNING} -lt 360 ]; then
|
||||||
log_msg "Container is running for less than 360 seconds, skipping action..."
|
log_msg "Container is running for less than 360 seconds, skipping action..."
|
||||||
elif [[ ! -z ${HAS_INITDB} ]]; then
|
elif [[ ! -z ${HAS_INITDB} ]]; then
|
||||||
@@ -1156,7 +1156,7 @@ while true; do
|
|||||||
sleep 60
|
sleep 60
|
||||||
else
|
else
|
||||||
log_msg "Sending restart command to ${CONTAINER_ID}..."
|
log_msg "Sending restart command to ${CONTAINER_ID}..."
|
||||||
curl --silent --insecure -XPOST https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
|
curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
|
||||||
notify_error "${com_pipe_answer}"
|
notify_error "${com_pipe_answer}"
|
||||||
log_msg "Wait for restarted container to settle and continue watching..."
|
log_msg "Wait for restarted container to settle and continue watching..."
|
||||||
sleep 35
|
sleep 35
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
function auth_password_verify(request, password)
|
function auth_password_verify(request, password)
|
||||||
|
request.domain = request.auth_user:match("@(.+)") or nil
|
||||||
if request.domain == nil then
|
if request.domain == nil then
|
||||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
|
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
|
||||||
end
|
end
|
||||||
@@ -9,10 +10,10 @@ function auth_password_verify(request, password)
|
|||||||
https.TIMEOUT = 30
|
https.TIMEOUT = 30
|
||||||
|
|
||||||
local req = {
|
local req = {
|
||||||
username = request.user,
|
username = request.auth_user,
|
||||||
password = password,
|
password = password,
|
||||||
real_rip = request.real_rip,
|
real_rip = request.remote_ip,
|
||||||
service = request.service
|
service = request.protocol
|
||||||
}
|
}
|
||||||
local req_json = json.encode(req)
|
local req_json = json.encode(req)
|
||||||
local res = {}
|
local res = {}
|
||||||
@@ -33,7 +34,6 @@ function auth_password_verify(request, password)
|
|||||||
-- Returning PASSDB_RESULT_INTERNAL_FAILURE keeps the existing 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.
|
-- even if the TTL has expired. Useful to avoid cache eviction during backend issues.
|
||||||
if c ~= 200 and c ~= 401 then
|
if c ~= 200 and c ~= 401 then
|
||||||
dovecot.i_info("HTTP request failed with " .. c .. " for user " .. request.user)
|
|
||||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error"
|
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ function auth_password_verify(request, password)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if response_json.success == true then
|
if response_json.success == true then
|
||||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
return dovecot.auth.PASSDB_RESULT_OK, { msg = "" }
|
||||||
end
|
end
|
||||||
|
|
||||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
|
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
|
||||||
@@ -55,3 +55,7 @@ end
|
|||||||
function auth_passdb_lookup(req)
|
function auth_passdb_lookup(req)
|
||||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
|
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function auth_passdb_get_cache_key()
|
||||||
|
return "%{protocol}:%{user | username}\t:%{password}"
|
||||||
|
end
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# /etc/dovecot/conf.d/05-core.conf
|
||||||
|
# Core, single-line settings that don't fit elsewhere.
|
||||||
|
recipient_delimiter = +
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# /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
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# /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
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# /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
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# /etc/dovecot/conf.d/11-sql.conf
|
||||||
|
# Default SQL driver used by SQL-based dicts/userdb.
|
||||||
|
sql_driver = mysql
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Autogenerated by mailcow - DO NOT TOUCH!
|
||||||
|
mysql /var/run/mysqld/mysqld.sock {
|
||||||
|
dbname=mailcow
|
||||||
|
user=mailcow
|
||||||
|
password=D8O9BIivJc7Pb2VCfpAeLbAzUOZ0
|
||||||
|
|
||||||
|
ssl = no
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# /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
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# /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
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# /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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# /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
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
# /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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# /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
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# 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! ###
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# /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}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# /etc/dovecot/conf.d/40-attributes.conf
|
||||||
|
# User/mail attributes.
|
||||||
|
mail_attribute {
|
||||||
|
dict file {
|
||||||
|
path = /etc/dovecot/dovecot-attributes
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# /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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# /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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# /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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# /etc/dovecot/conf.d/80-compress.conf
|
||||||
|
# Compression settings.
|
||||||
|
mail_compress_write_method = lz4
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# /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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# /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
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# /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
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# 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! ###
|
|
||||||
+30
-307
@@ -1,311 +1,34 @@
|
|||||||
# --------------------------------------------------------------------------
|
# /etc/dovecot/dovecot.conf
|
||||||
# Please create a file "extra.conf" for persistent overrides to dovecot.conf
|
# Base file kept minimal. All real config lives under conf.d/.
|
||||||
# --------------------------------------------------------------------------
|
dovecot_config_version = 2.4.0
|
||||||
# LDAP example:
|
dovecot_storage_version = 2.4.0
|
||||||
#passdb {
|
|
||||||
# args = /etc/dovecot/ldap/passdb.conf
|
|
||||||
# driver = ldap
|
|
||||||
#}
|
|
||||||
|
|
||||||
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 = *,[::]
|
listen = *,[::]
|
||||||
ssl_cert = </etc/ssl/mail/cert.pem
|
protocols = imap sieve lmtp pop3
|
||||||
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
|
|
||||||
|
|
||||||
# -- Global keys
|
!include_try /etc/dovecot/conf.d/05-core.conf
|
||||||
mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem
|
!include_try /etc/dovecot/conf.d/10-logging.conf
|
||||||
mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem
|
!include_try /etc/dovecot/conf.d/10-mail.conf
|
||||||
mail_crypt_save_version = 2
|
!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
|
||||||
|
|
||||||
# Enable compression while saving, lz4 Dovecot v2.3.17+
|
# Last: local overrides
|
||||||
zlib_save = lz4
|
!include_try /etc/dovecot/extra.conf
|
||||||
|
|
||||||
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,10 +1,14 @@
|
|||||||
namespace inbox {
|
namespace inbox {
|
||||||
inbox = yes
|
inbox = yes
|
||||||
location =
|
|
||||||
separator = /
|
separator = /
|
||||||
|
mailbox storage/* {
|
||||||
|
quota_storage_extra = 100M
|
||||||
|
}
|
||||||
mailbox "Trash" {
|
mailbox "Trash" {
|
||||||
auto = subscribe
|
auto = subscribe
|
||||||
special_use = \Trash
|
special_use = \Trash
|
||||||
|
quota_storage_percentage = 100
|
||||||
|
fts_autoindex = no
|
||||||
}
|
}
|
||||||
mailbox "Deleted Messages" {
|
mailbox "Deleted Messages" {
|
||||||
special_use = \Trash
|
special_use = \Trash
|
||||||
@@ -195,6 +199,7 @@ namespace inbox {
|
|||||||
mailbox "Junk" {
|
mailbox "Junk" {
|
||||||
auto = subscribe
|
auto = subscribe
|
||||||
special_use = \Junk
|
special_use = \Junk
|
||||||
|
fts_autoindex = no
|
||||||
}
|
}
|
||||||
mailbox "Junk-E-Mail" {
|
mailbox "Junk-E-Mail" {
|
||||||
special_use = \Junk
|
special_use = \Junk
|
||||||
|
|||||||
@@ -185,7 +185,6 @@ location ^~ /Microsoft-Server-ActiveSync {
|
|||||||
auth_request_set $user $upstream_http_x_user;
|
auth_request_set $user $upstream_http_x_user;
|
||||||
auth_request_set $auth $upstream_http_x_auth;
|
auth_request_set $auth $upstream_http_x_auth;
|
||||||
auth_request_set $auth_type $upstream_http_x_auth_type;
|
auth_request_set $auth_type $upstream_http_x_auth_type;
|
||||||
auth_request_set $real_ip $remote_addr;
|
|
||||||
proxy_set_header x-webobjects-remote-user "$user";
|
proxy_set_header x-webobjects-remote-user "$user";
|
||||||
proxy_set_header Authorization "$auth";
|
proxy_set_header Authorization "$auth";
|
||||||
proxy_set_header x-webobjects-auth-type "$auth_type";
|
proxy_set_header x-webobjects-auth-type "$auth_type";
|
||||||
@@ -211,7 +210,6 @@ location ^~ /SOGo {
|
|||||||
auth_request_set $user $upstream_http_x_user;
|
auth_request_set $user $upstream_http_x_user;
|
||||||
auth_request_set $auth $upstream_http_x_auth;
|
auth_request_set $auth $upstream_http_x_auth;
|
||||||
auth_request_set $auth_type $upstream_http_x_auth_type;
|
auth_request_set $auth_type $upstream_http_x_auth_type;
|
||||||
auth_request_set $real_ip $remote_addr;
|
|
||||||
proxy_set_header x-webobjects-remote-user "$user";
|
proxy_set_header x-webobjects-remote-user "$user";
|
||||||
proxy_set_header Authorization "$auth";
|
proxy_set_header Authorization "$auth";
|
||||||
proxy_set_header x-webobjects-auth-type "$auth_type";
|
proxy_set_header x-webobjects-auth-type "$auth_type";
|
||||||
@@ -234,7 +232,6 @@ location ^~ /SOGo {
|
|||||||
auth_request_set $user $upstream_http_x_user;
|
auth_request_set $user $upstream_http_x_user;
|
||||||
auth_request_set $auth $upstream_http_x_auth;
|
auth_request_set $auth $upstream_http_x_auth;
|
||||||
auth_request_set $auth_type $upstream_http_x_auth_type;
|
auth_request_set $auth_type $upstream_http_x_auth_type;
|
||||||
auth_request_set $real_ip $remote_addr;
|
|
||||||
proxy_set_header x-webobjects-remote-user "$user";
|
proxy_set_header x-webobjects-remote-user "$user";
|
||||||
proxy_set_header Authorization "$auth";
|
proxy_set_header Authorization "$auth";
|
||||||
proxy_set_header x-webobjects-auth-type "$auth_type";
|
proxy_set_header x-webobjects-auth-type "$auth_type";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Whitelist generated by Postwhite v3.4 on Thu Jan 1 00:24:01 UTC 2026
|
# Whitelist generated by Postwhite v3.4 on Sun Mar 1 00:29:01 UTC 2026
|
||||||
# https://github.com/stevejenkins/postwhite/
|
# https://github.com/stevejenkins/postwhite/
|
||||||
# 2105 total rules
|
# 2174 total rules
|
||||||
2a00:1450:4000::/36 permit
|
2a00:1450:4000::/36 permit
|
||||||
2a01:111:f400::/48 permit
|
2a01:111:f400::/48 permit
|
||||||
2a01:111:f403:2800::/53 permit
|
2a01:111:f403:2800::/53 permit
|
||||||
@@ -52,10 +52,14 @@
|
|||||||
8.25.194.0/23 permit
|
8.25.194.0/23 permit
|
||||||
8.25.196.0/23 permit
|
8.25.196.0/23 permit
|
||||||
8.36.116.0/24 permit
|
8.36.116.0/24 permit
|
||||||
|
8.39.54.0/23 permit
|
||||||
|
8.39.54.250/31 permit
|
||||||
8.39.144.0/24 permit
|
8.39.144.0/24 permit
|
||||||
|
8.40.222.0/23 permit
|
||||||
|
8.40.222.250/31 permit
|
||||||
12.130.86.238 permit
|
12.130.86.238 permit
|
||||||
13.107.213.38 permit
|
13.107.213.51 permit
|
||||||
13.107.246.38 permit
|
13.107.246.51 permit
|
||||||
13.108.16.0/20 permit
|
13.108.16.0/20 permit
|
||||||
13.110.208.0/21 permit
|
13.110.208.0/21 permit
|
||||||
13.110.209.0/24 permit
|
13.110.209.0/24 permit
|
||||||
@@ -65,6 +69,7 @@
|
|||||||
13.111.191.0/24 permit
|
13.111.191.0/24 permit
|
||||||
13.216.7.111 permit
|
13.216.7.111 permit
|
||||||
13.216.54.180 permit
|
13.216.54.180 permit
|
||||||
|
13.247.164.219 permit
|
||||||
15.200.21.50 permit
|
15.200.21.50 permit
|
||||||
15.200.44.248 permit
|
15.200.44.248 permit
|
||||||
15.200.201.185 permit
|
15.200.201.185 permit
|
||||||
@@ -168,6 +173,7 @@
|
|||||||
34.215.104.144 permit
|
34.215.104.144 permit
|
||||||
34.218.115.239 permit
|
34.218.115.239 permit
|
||||||
34.225.212.172 permit
|
34.225.212.172 permit
|
||||||
|
34.241.242.183 permit
|
||||||
35.83.148.184 permit
|
35.83.148.184 permit
|
||||||
35.155.198.111 permit
|
35.155.198.111 permit
|
||||||
35.158.23.94 permit
|
35.158.23.94 permit
|
||||||
@@ -191,6 +197,7 @@
|
|||||||
40.233.64.216 permit
|
40.233.64.216 permit
|
||||||
40.233.83.78 permit
|
40.233.83.78 permit
|
||||||
40.233.88.28 permit
|
40.233.88.28 permit
|
||||||
|
43.239.212.33 permit
|
||||||
44.206.138.57 permit
|
44.206.138.57 permit
|
||||||
44.210.169.44 permit
|
44.210.169.44 permit
|
||||||
44.217.45.156 permit
|
44.217.45.156 permit
|
||||||
@@ -272,6 +279,7 @@
|
|||||||
50.112.246.219 permit
|
50.112.246.219 permit
|
||||||
52.1.14.157 permit
|
52.1.14.157 permit
|
||||||
52.5.230.59 permit
|
52.5.230.59 permit
|
||||||
|
52.6.74.205 permit
|
||||||
52.12.53.23 permit
|
52.12.53.23 permit
|
||||||
52.13.214.179 permit
|
52.13.214.179 permit
|
||||||
52.26.1.71 permit
|
52.26.1.71 permit
|
||||||
@@ -328,6 +336,7 @@
|
|||||||
54.244.54.130 permit
|
54.244.54.130 permit
|
||||||
54.244.242.0/24 permit
|
54.244.242.0/24 permit
|
||||||
54.255.61.23 permit
|
54.255.61.23 permit
|
||||||
|
56.124.6.228 permit
|
||||||
57.103.64.0/18 permit
|
57.103.64.0/18 permit
|
||||||
57.129.93.249 permit
|
57.129.93.249 permit
|
||||||
62.13.128.0/24 permit
|
62.13.128.0/24 permit
|
||||||
@@ -393,6 +402,7 @@
|
|||||||
65.110.161.77 permit
|
65.110.161.77 permit
|
||||||
65.123.29.213 permit
|
65.123.29.213 permit
|
||||||
65.123.29.220 permit
|
65.123.29.220 permit
|
||||||
|
65.154.166.0/24 permit
|
||||||
65.212.180.36 permit
|
65.212.180.36 permit
|
||||||
66.102.0.0/20 permit
|
66.102.0.0/20 permit
|
||||||
66.119.150.192/26 permit
|
66.119.150.192/26 permit
|
||||||
@@ -697,7 +707,9 @@
|
|||||||
87.248.117.205 permit
|
87.248.117.205 permit
|
||||||
87.253.232.0/21 permit
|
87.253.232.0/21 permit
|
||||||
89.22.108.0/24 permit
|
89.22.108.0/24 permit
|
||||||
91.198.2.0/24 permit
|
91.198.2.177 permit
|
||||||
|
91.198.2.217 permit
|
||||||
|
91.198.2.222 permit
|
||||||
91.211.240.0/22 permit
|
91.211.240.0/22 permit
|
||||||
94.236.119.0/26 permit
|
94.236.119.0/26 permit
|
||||||
95.131.104.0/21 permit
|
95.131.104.0/21 permit
|
||||||
@@ -1194,6 +1206,9 @@
|
|||||||
99.78.197.208/28 permit
|
99.78.197.208/28 permit
|
||||||
103.9.96.0/22 permit
|
103.9.96.0/22 permit
|
||||||
103.28.42.0/24 permit
|
103.28.42.0/24 permit
|
||||||
|
103.84.217.15 permit
|
||||||
|
103.84.217.238 permit
|
||||||
|
103.89.75.238 permit
|
||||||
103.151.192.0/23 permit
|
103.151.192.0/23 permit
|
||||||
103.168.172.128/27 permit
|
103.168.172.128/27 permit
|
||||||
103.237.104.0/22 permit
|
103.237.104.0/22 permit
|
||||||
@@ -1354,6 +1369,9 @@
|
|||||||
117.120.16.0/21 permit
|
117.120.16.0/21 permit
|
||||||
119.42.242.52/31 permit
|
119.42.242.52/31 permit
|
||||||
119.42.242.156 permit
|
119.42.242.156 permit
|
||||||
|
121.244.91.48 permit
|
||||||
|
121.244.91.52 permit
|
||||||
|
122.15.156.182 permit
|
||||||
123.126.78.64/29 permit
|
123.126.78.64/29 permit
|
||||||
124.108.96.24/31 permit
|
124.108.96.24/31 permit
|
||||||
124.108.96.28/31 permit
|
124.108.96.28/31 permit
|
||||||
@@ -1419,7 +1437,21 @@
|
|||||||
134.170.141.64/26 permit
|
134.170.141.64/26 permit
|
||||||
134.170.143.0/24 permit
|
134.170.143.0/24 permit
|
||||||
134.170.174.0/24 permit
|
134.170.174.0/24 permit
|
||||||
|
135.84.80.0/24 permit
|
||||||
|
135.84.81.0/24 permit
|
||||||
|
135.84.82.0/24 permit
|
||||||
|
135.84.83.0/24 permit
|
||||||
135.84.216.0/22 permit
|
135.84.216.0/22 permit
|
||||||
|
136.143.160.0/24 permit
|
||||||
|
136.143.161.0/24 permit
|
||||||
|
136.143.162.0/24 permit
|
||||||
|
136.143.176.0/24 permit
|
||||||
|
136.143.177.0/24 permit
|
||||||
|
136.143.178.49 permit
|
||||||
|
136.143.182.0/23 permit
|
||||||
|
136.143.184.0/24 permit
|
||||||
|
136.143.188.0/24 permit
|
||||||
|
136.143.190.0/23 permit
|
||||||
136.146.128.0/20 permit
|
136.146.128.0/20 permit
|
||||||
136.147.128.0/20 permit
|
136.147.128.0/20 permit
|
||||||
136.147.135.0/24 permit
|
136.147.135.0/24 permit
|
||||||
@@ -1435,6 +1467,7 @@
|
|||||||
139.138.46.219 permit
|
139.138.46.219 permit
|
||||||
139.138.57.55 permit
|
139.138.57.55 permit
|
||||||
139.138.58.119 permit
|
139.138.58.119 permit
|
||||||
|
139.167.79.86 permit
|
||||||
139.180.17.0/24 permit
|
139.180.17.0/24 permit
|
||||||
140.238.148.191 permit
|
140.238.148.191 permit
|
||||||
141.148.55.217 permit
|
141.148.55.217 permit
|
||||||
@@ -1523,8 +1556,10 @@
|
|||||||
159.135.224.0/20 permit
|
159.135.224.0/20 permit
|
||||||
159.135.228.10 permit
|
159.135.228.10 permit
|
||||||
159.183.0.0/16 permit
|
159.183.0.0/16 permit
|
||||||
|
159.183.14.233 permit
|
||||||
159.183.68.71 permit
|
159.183.68.71 permit
|
||||||
159.183.79.38 permit
|
159.183.79.38 permit
|
||||||
|
159.183.121.182 permit
|
||||||
159.183.129.172 permit
|
159.183.129.172 permit
|
||||||
160.1.62.192 permit
|
160.1.62.192 permit
|
||||||
161.38.192.0/20 permit
|
161.38.192.0/20 permit
|
||||||
@@ -1550,6 +1585,10 @@
|
|||||||
164.152.23.32 permit
|
164.152.23.32 permit
|
||||||
164.152.25.241 permit
|
164.152.25.241 permit
|
||||||
164.177.132.168/30 permit
|
164.177.132.168/30 permit
|
||||||
|
165.173.128.0/24 permit
|
||||||
|
165.173.180.1 permit
|
||||||
|
165.173.180.250/31 permit
|
||||||
|
165.173.182.250/31 permit
|
||||||
166.78.68.0/22 permit
|
166.78.68.0/22 permit
|
||||||
166.78.68.221 permit
|
166.78.68.221 permit
|
||||||
166.78.69.169 permit
|
166.78.69.169 permit
|
||||||
@@ -1579,6 +1618,18 @@
|
|||||||
168.245.12.252 permit
|
168.245.12.252 permit
|
||||||
168.245.46.9 permit
|
168.245.46.9 permit
|
||||||
168.245.127.231 permit
|
168.245.127.231 permit
|
||||||
|
169.148.129.0/24 permit
|
||||||
|
169.148.131.0/24 permit
|
||||||
|
169.148.138.0/24 permit
|
||||||
|
169.148.142.10 permit
|
||||||
|
169.148.142.33 permit
|
||||||
|
169.148.144.0/25 permit
|
||||||
|
169.148.144.10 permit
|
||||||
|
169.148.146.0/23 permit
|
||||||
|
169.148.175.3 permit
|
||||||
|
169.148.179.3 permit
|
||||||
|
169.148.188.0/24 permit
|
||||||
|
169.148.188.182 permit
|
||||||
170.9.232.254 permit
|
170.9.232.254 permit
|
||||||
170.10.128.0/24 permit
|
170.10.128.0/24 permit
|
||||||
170.10.129.0/24 permit
|
170.10.129.0/24 permit
|
||||||
@@ -1612,8 +1663,7 @@
|
|||||||
182.50.78.64/28 permit
|
182.50.78.64/28 permit
|
||||||
183.240.219.64/29 permit
|
183.240.219.64/29 permit
|
||||||
185.4.120.0/22 permit
|
185.4.120.0/22 permit
|
||||||
185.11.253.128/27 permit
|
185.11.255.144 permit
|
||||||
185.11.255.0/24 permit
|
|
||||||
185.12.80.0/22 permit
|
185.12.80.0/22 permit
|
||||||
185.28.196.0/22 permit
|
185.28.196.0/22 permit
|
||||||
185.58.84.93 permit
|
185.58.84.93 permit
|
||||||
@@ -1627,8 +1677,16 @@
|
|||||||
185.138.56.128/25 permit
|
185.138.56.128/25 permit
|
||||||
185.189.236.0/22 permit
|
185.189.236.0/22 permit
|
||||||
185.211.120.0/22 permit
|
185.211.120.0/22 permit
|
||||||
185.233.188.0/23 permit
|
185.233.188.68 permit
|
||||||
185.233.190.0/23 permit
|
185.233.188.75 permit
|
||||||
|
185.233.188.84 permit
|
||||||
|
185.233.188.160 permit
|
||||||
|
185.233.188.176 permit
|
||||||
|
185.233.188.247 permit
|
||||||
|
185.233.189.44 permit
|
||||||
|
185.233.189.98 permit
|
||||||
|
185.233.189.122 permit
|
||||||
|
185.233.189.228 permit
|
||||||
185.250.236.0/22 permit
|
185.250.236.0/22 permit
|
||||||
185.250.239.148 permit
|
185.250.239.148 permit
|
||||||
185.250.239.168 permit
|
185.250.239.168 permit
|
||||||
@@ -1704,7 +1762,9 @@
|
|||||||
193.109.254.0/23 permit
|
193.109.254.0/23 permit
|
||||||
193.122.128.100 permit
|
193.122.128.100 permit
|
||||||
193.123.56.63 permit
|
193.123.56.63 permit
|
||||||
193.142.157.0/24 permit
|
193.142.157.15 permit
|
||||||
|
193.142.157.125 permit
|
||||||
|
193.142.157.158 permit
|
||||||
193.142.157.191 permit
|
193.142.157.191 permit
|
||||||
193.142.157.198 permit
|
193.142.157.198 permit
|
||||||
194.19.134.0/25 permit
|
194.19.134.0/25 permit
|
||||||
@@ -1764,7 +1824,16 @@
|
|||||||
199.16.156.0/22 permit
|
199.16.156.0/22 permit
|
||||||
199.33.145.1 permit
|
199.33.145.1 permit
|
||||||
199.33.145.32 permit
|
199.33.145.32 permit
|
||||||
|
199.34.22.36 permit
|
||||||
199.59.148.0/22 permit
|
199.59.148.0/22 permit
|
||||||
|
199.67.80.2 permit
|
||||||
|
199.67.80.20 permit
|
||||||
|
199.67.82.2 permit
|
||||||
|
199.67.82.20 permit
|
||||||
|
199.67.84.0/24 permit
|
||||||
|
199.67.86.0/24 permit
|
||||||
|
199.67.88.0/24 permit
|
||||||
|
199.67.90.0/24 permit
|
||||||
199.101.161.130 permit
|
199.101.161.130 permit
|
||||||
199.101.162.0/25 permit
|
199.101.162.0/25 permit
|
||||||
199.122.120.0/21 permit
|
199.122.120.0/21 permit
|
||||||
@@ -1820,6 +1889,8 @@
|
|||||||
204.92.114.187 permit
|
204.92.114.187 permit
|
||||||
204.92.114.203 permit
|
204.92.114.203 permit
|
||||||
204.92.114.204/31 permit
|
204.92.114.204/31 permit
|
||||||
|
204.141.32.0/23 permit
|
||||||
|
204.141.42.0/23 permit
|
||||||
204.216.164.202 permit
|
204.216.164.202 permit
|
||||||
204.220.160.0/21 permit
|
204.220.160.0/21 permit
|
||||||
204.220.168.0/21 permit
|
204.220.168.0/21 permit
|
||||||
@@ -1997,8 +2068,6 @@
|
|||||||
212.227.126.225 permit
|
212.227.126.225 permit
|
||||||
212.227.126.226 permit
|
212.227.126.226 permit
|
||||||
212.227.126.227 permit
|
212.227.126.227 permit
|
||||||
213.95.19.64/27 permit
|
|
||||||
213.95.135.4 permit
|
|
||||||
213.199.128.139 permit
|
213.199.128.139 permit
|
||||||
213.199.128.145 permit
|
213.199.128.145 permit
|
||||||
213.199.138.181 permit
|
213.199.138.181 permit
|
||||||
@@ -2088,11 +2157,9 @@
|
|||||||
2001:748:400:3301::3 permit
|
2001:748:400:3301::3 permit
|
||||||
2001:748:400:3301::4 permit
|
2001:748:400:3301::4 permit
|
||||||
2404:6800:4000::/36 permit
|
2404:6800:4000::/36 permit
|
||||||
2603:1010:3:3::5b permit
|
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
|
||||||
2603:1020:201:10::10f permit
|
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
|
||||||
2603:1030:20e:3::23c permit
|
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
|
||||||
2603:1030:b:3::152 permit
|
|
||||||
2603:1030:c02:8::14 permit
|
|
||||||
2607:f8b0:4000::/36 permit
|
2607:f8b0:4000::/36 permit
|
||||||
2620:109:c003:104::/64 permit
|
2620:109:c003:104::/64 permit
|
||||||
2620:109:c003:104::215 permit
|
2620:109:c003:104::215 permit
|
||||||
@@ -2105,6 +2172,8 @@
|
|||||||
2620:10d:c09c:400::8:1 permit
|
2620:10d:c09c:400::8:1 permit
|
||||||
2620:119:50c0:207::/64 permit
|
2620:119:50c0:207::/64 permit
|
||||||
2620:119:50c0:207::215 permit
|
2620:119:50c0:207::215 permit
|
||||||
|
2620:1ec:46::51 permit
|
||||||
|
2620:1ec:bdf::51 permit
|
||||||
2800:3f0:4000::/36 permit
|
2800:3f0:4000::/36 permit
|
||||||
49.12.4.251 permit # checks.mailcow.email
|
49.12.4.251 permit # checks.mailcow.email
|
||||||
2a01:4f8:c17:7906::10 permit # checks.mailcow.email
|
2a01:4f8:c17:7906::10 permit # checks.mailcow.email
|
||||||
|
|||||||
@@ -392,6 +392,7 @@ rspamd_config:register_symbol({
|
|||||||
local rspamd_http = require "rspamd_http"
|
local rspamd_http = require "rspamd_http"
|
||||||
local rcpts = task:get_recipients('smtp')
|
local rcpts = task:get_recipients('smtp')
|
||||||
local lua_util = require "lua_util"
|
local lua_util = require "lua_util"
|
||||||
|
local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
|
||||||
|
|
||||||
local function remove_moo_tag()
|
local function remove_moo_tag()
|
||||||
local moo_tag_header = task:get_header('X-Moo-Tag', false)
|
local moo_tag_header = task:get_header('X-Moo-Tag', false)
|
||||||
@@ -416,12 +417,9 @@ rspamd_config:register_symbol({
|
|||||||
|
|
||||||
-- Check if recipient has a tag (contains '+')
|
-- Check if recipient has a tag (contains '+')
|
||||||
local tag = nil
|
local tag = nil
|
||||||
if rcpt_user:find('%+') then
|
if tagged_rcpt ~= nil then
|
||||||
local base_user, tag_part = rcpt_user:match('^(.-)%+(.+)$')
|
tag = tagged_rcpt
|
||||||
if base_user and tag_part then
|
rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag)
|
||||||
tag = tag_part
|
|
||||||
rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if not tag then
|
if not tag then
|
||||||
@@ -500,7 +498,8 @@ rspamd_config:register_symbol({
|
|||||||
else
|
else
|
||||||
rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
|
rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
|
||||||
local sbj = task:get_header('Subject') or ''
|
local sbj = task:get_header('Subject') or ''
|
||||||
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
|
local tag_value = tag[1] and tag[1].options and tag[1].options[1] or ''
|
||||||
|
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag_value .. '] ' .. sbj)) .. '?='
|
||||||
task:set_milter_reply({
|
task:set_milter_reply({
|
||||||
remove_headers = {
|
remove_headers = {
|
||||||
['Subject'] = 1,
|
['Subject'] = 1,
|
||||||
@@ -945,4 +944,4 @@ rspamd_config:register_symbol({
|
|||||||
return true
|
return true
|
||||||
end,
|
end,
|
||||||
priority = 1
|
priority = 1
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||||
header('Location: /admin/dashboard');
|
// Only redirect to dashboard if NO pending actions
|
||||||
exit();
|
if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) {
|
||||||
|
header('Location: /admin/dashboard');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||||
header('Location: /domainadmin/mailbox');
|
header('Location: /domainadmin/mailbox');
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
|
|||||||
@@ -2,19 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$js_minifier->add('/web/js/site/queue.js');
|
$js_minifier->add('/web/js/site/queue.js');
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
protect_route(['admin']);
|
||||||
header('Location: /domainadmin/mailbox');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
|
||||||
header('Location: /admin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
|
|||||||
@@ -2454,6 +2454,90 @@ paths:
|
|||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
summary: Delete mails in Quarantine
|
summary: Delete mails in Quarantine
|
||||||
|
/api/v1/edit/qitem:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
examples:
|
||||||
|
release:
|
||||||
|
value:
|
||||||
|
- log:
|
||||||
|
- quarantine
|
||||||
|
- edit
|
||||||
|
- id:
|
||||||
|
- "33"
|
||||||
|
action: release
|
||||||
|
msg:
|
||||||
|
- item_released
|
||||||
|
- "33"
|
||||||
|
type: success
|
||||||
|
learnham:
|
||||||
|
value:
|
||||||
|
- log:
|
||||||
|
- quarantine
|
||||||
|
- edit
|
||||||
|
- id:
|
||||||
|
- "34"
|
||||||
|
action: learnham
|
||||||
|
msg:
|
||||||
|
- item_learned
|
||||||
|
- "34"
|
||||||
|
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:
|
||||||
|
- Quarantine
|
||||||
|
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.
|
||||||
|
operationId: Edit mails in Quarantine
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
example:
|
||||||
|
items:
|
||||||
|
- "33"
|
||||||
|
- "34"
|
||||||
|
attr:
|
||||||
|
action: release
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
description: contains list of quarantine item IDs to release or learn as ham
|
||||||
|
type: object
|
||||||
|
attr:
|
||||||
|
description: attributes for the action
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
action:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- release
|
||||||
|
- learnham
|
||||||
|
description: "release - return email to inbox; learnham - learn as ham to improve filtering"
|
||||||
|
type: object
|
||||||
|
summary: Edit mails in Quarantine
|
||||||
/api/v1/delete/recipient_map:
|
/api/v1/delete/recipient_map:
|
||||||
post:
|
post:
|
||||||
responses:
|
responses:
|
||||||
@@ -5352,9 +5436,9 @@ paths:
|
|||||||
started_at: "2019-12-22T21:00:01.622856172Z"
|
started_at: "2019-12-22T21:00:01.622856172Z"
|
||||||
state: running
|
state: running
|
||||||
type: info
|
type: info
|
||||||
controller-mailcow:
|
dockerapi-mailcow:
|
||||||
container: controller-mailcow
|
container: dockerapi-mailcow
|
||||||
image: "mailcow/controller:1.36"
|
image: "mailcow/dockerapi:1.36"
|
||||||
started_at: "2019-12-22T20:59:59.984797808Z"
|
started_at: "2019-12-22T20:59:59.984797808Z"
|
||||||
state: running
|
state: running
|
||||||
type: info
|
type: info
|
||||||
|
|||||||
+148
-85
@@ -60,101 +60,31 @@ $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
|||||||
$iam_provider = identity_provider('init');
|
$iam_provider = identity_provider('init');
|
||||||
$iam_settings = identity_provider('get');
|
$iam_settings = identity_provider('get');
|
||||||
|
|
||||||
$login_user = strtolower(trim($_SERVER['PHP_AUTH_USER']));
|
// Passwordless autodiscover - no authentication required
|
||||||
$login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW']));
|
// Email will be extracted from the request body
|
||||||
|
$login_user = null;
|
||||||
|
$login_role = null;
|
||||||
|
|
||||||
if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) {
|
header("Content-Type: application/xml");
|
||||||
$json = json_encode(
|
echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
|
||||||
array(
|
|
||||||
"time" => time(),
|
|
||||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
|
||||||
"user" => "none",
|
|
||||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
|
||||||
"service" => "Error: must be authenticated"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
|
||||||
header('WWW-Authenticate: Basic realm="' . $_SERVER['HTTP_HOST'] . '"');
|
|
||||||
header('HTTP/1.0 401 Unauthorized');
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
$login_role = check_login($login_user, $login_pass, array('service' => 'EAS'));
|
|
||||||
|
|
||||||
if ($login_role === "user") {
|
|
||||||
header("Content-Type: application/xml");
|
|
||||||
echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
|
|
||||||
?>
|
?>
|
||||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
|
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
|
||||||
<?php
|
<?php
|
||||||
if(!$data) {
|
if(!$data) {
|
||||||
try {
|
|
||||||
$json = json_encode(
|
|
||||||
array(
|
|
||||||
"time" => time(),
|
|
||||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
|
||||||
"user" => $_SERVER['PHP_AUTH_USER'],
|
|
||||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
|
||||||
"service" => "Error: invalid or missing request data"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
|
||||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
|
||||||
}
|
|
||||||
catch (RedisException $e) {
|
|
||||||
$_SESSION['return'][] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'Redis: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
list($usec, $sec) = explode(' ', microtime());
|
|
||||||
?>
|
|
||||||
<Response>
|
|
||||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="2477272013">
|
|
||||||
<ErrorCode>600</ErrorCode>
|
|
||||||
<Message>Invalid Request</Message>
|
|
||||||
<DebugData />
|
|
||||||
</Error>
|
|
||||||
</Response>
|
|
||||||
</Autodiscover>
|
|
||||||
<?php
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$discover = new SimpleXMLElement($data);
|
|
||||||
$email = $discover->Request->EMailAddress;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$email = $_SERVER['PHP_AUTH_USER'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$username = trim($email);
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username");
|
|
||||||
$stmt->execute(array(':username' => $username));
|
|
||||||
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
catch(PDOException $e) {
|
|
||||||
die("Failed to determine name from SQL");
|
|
||||||
}
|
|
||||||
if (!empty($MailboxData['name'])) {
|
|
||||||
$displayname = $MailboxData['name'];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$displayname = $email;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
$json = json_encode(
|
$json = json_encode(
|
||||||
array(
|
array(
|
||||||
"time" => time(),
|
"time" => time(),
|
||||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
"user" => $_SERVER['PHP_AUTH_USER'],
|
"user" => "none",
|
||||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
"service" => $autodiscover_config['autodiscoverType']
|
"service" => "Error: invalid or missing request data"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
error_log("Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
|
||||||
}
|
}
|
||||||
catch (RedisException $e) {
|
catch (RedisException $e) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@@ -163,7 +93,143 @@ if ($login_role === "user") {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>600</ErrorCode>
|
||||||
|
<Message>Invalid Request</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$discover = new SimpleXMLElement($data);
|
||||||
|
$email = $discover->Request->EMailAddress;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// If parsing fails, return error
|
||||||
|
try {
|
||||||
|
$json = json_encode(
|
||||||
|
array(
|
||||||
|
"time" => time(),
|
||||||
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
|
"user" => "none",
|
||||||
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
|
"service" => "Error: could not parse email from request"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
$redis->publish("F2B_CHANNEL", "Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
error_log("Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
// Silently fail
|
||||||
|
}
|
||||||
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>600</ErrorCode>
|
||||||
|
<Message>Invalid Request</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = trim((string)$email);
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("SELECT `mailbox`.`name`, `mailbox`.`active` FROM `mailbox`
|
||||||
|
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
|
||||||
|
WHERE `mailbox`.`username` = :username
|
||||||
|
AND `mailbox`.`active` = '1'
|
||||||
|
AND `domain`.`active` = '1'");
|
||||||
|
$stmt->execute(array(':username' => $username));
|
||||||
|
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
catch(PDOException $e) {
|
||||||
|
// Database error - return error response with complete XML
|
||||||
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>500</ErrorCode>
|
||||||
|
<Message>Database Error</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mailbox not found or not active - return generic error to prevent user enumeration
|
||||||
|
if (empty($MailboxData)) {
|
||||||
|
try {
|
||||||
|
$json = json_encode(
|
||||||
|
array(
|
||||||
|
"time" => time(),
|
||||||
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
|
"user" => $email,
|
||||||
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
|
"service" => "Error: mailbox not found or inactive"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
error_log("Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
// Silently fail
|
||||||
|
}
|
||||||
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>600</ErrorCode>
|
||||||
|
<Message>Invalid Request</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($MailboxData['name'])) {
|
||||||
|
$displayname = $MailboxData['name'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$displayname = $email;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$json = json_encode(
|
||||||
|
array(
|
||||||
|
"time" => time(),
|
||||||
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
|
"user" => $email,
|
||||||
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
|
"service" => $autodiscover_config['autodiscoverType']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'msg' => 'Redis: '.$e
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
||||||
?>
|
?>
|
||||||
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
|
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
|
||||||
<User>
|
<User>
|
||||||
@@ -238,6 +304,3 @@ if ($login_role === "user") {
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</Autodiscover>
|
</Autodiscover>
|
||||||
<?php
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||||
header('Location: /domainadmin/mailbox');
|
// Only redirect to mailbox if NO pending actions
|
||||||
exit();
|
if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) {
|
||||||
|
header('Location: /domainadmin/mailbox');
|
||||||
|
exit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||||
header('Location: /admin/dashboard');
|
header('Location: /admin/dashboard');
|
||||||
|
|||||||
@@ -2,18 +2,7 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
protect_route(['domainadmin']);
|
||||||
header('Location: /admin/dashboard');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
|
||||||
header('Location: /domainadmin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
|
|||||||
@@ -2,41 +2,28 @@
|
|||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||||
|
|
||||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
/*
|
||||||
|
/ DOMAIN ADMIN
|
||||||
|
*/
|
||||||
|
|
||||||
/*
|
protect_route(['domainadmin']);
|
||||||
/ DOMAIN ADMIN
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||||
$tfa_data = get_tfa();
|
$tfa_data = get_tfa();
|
||||||
$fido2_data = fido2(array("action" => "get_friendly_names"));
|
$fido2_data = fido2(array("action" => "get_friendly_names"));
|
||||||
$username = $_SESSION['mailcow_cc_username'];
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
|
|
||||||
$template = 'domainadmin.twig';
|
$template = 'domainadmin.twig';
|
||||||
$template_data = [
|
$template_data = [
|
||||||
'acl' => $_SESSION['acl'],
|
'acl' => $_SESSION['acl'],
|
||||||
'acl_json' => json_encode($_SESSION['acl']),
|
'acl_json' => json_encode($_SESSION['acl']),
|
||||||
'user_spam_score' => mailbox('get', 'spam_score', $username),
|
'user_spam_score' => mailbox('get', 'spam_score', $username),
|
||||||
'tfa_data' => $tfa_data,
|
'tfa_data' => $tfa_data,
|
||||||
'fido2_data' => $fido2_data,
|
'fido2_data' => $fido2_data,
|
||||||
'lang_user' => json_encode($lang['user']),
|
'lang_user' => json_encode($lang['user']),
|
||||||
'lang_datatables' => json_encode($lang['datatables']),
|
'lang_datatables' => json_encode($lang['datatables']),
|
||||||
];
|
];
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
|
||||||
header('Location: /admin/dashboard');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
|
||||||
header('Location: /user');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
header('Location: /domainadmin');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$js_minifier->add('/web/js/site/user.js');
|
$js_minifier->add('/web/js/site/user.js');
|
||||||
$js_minifier->add('/web/js/site/pwgen.js');
|
$js_minifier->add('/web/js/site/pwgen.js');
|
||||||
|
|||||||
+3
-5
@@ -1,10 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||||
$AuthUsers = array("admin", "domainadmin", "user");
|
|
||||||
if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) {
|
protect_route();
|
||||||
header('Location: /');
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||||
|
|
||||||
$template = 'edit.twig';
|
$template = 'edit.twig';
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user