Page MenuHomeWMGMC Issues

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/data/Dockerfiles/watchdog/watchdog.sh b/data/Dockerfiles/watchdog/watchdog.sh
index 0cdc63c5..19c0d2ed 100755
--- a/data/Dockerfiles/watchdog/watchdog.sh
+++ b/data/Dockerfiles/watchdog/watchdog.sh
@@ -1,1126 +1,1126 @@
#!/bin/bash
trap "exit" INT TERM
trap "kill 0" EXIT
# Prepare
BACKGROUND_TASKS=()
echo "Waiting for containers to settle..."
sleep 30
if [[ "${USE_WATCHDOG}" =~ ^([nN][oO]|[nN])+$ ]]; then
echo -e "$(date) - USE_WATCHDOG=n, skipping watchdog..."
sleep 365d
exec $(readlink -f "$0")
fi
# Checks pipe their corresponding container name in this pipe
if [[ ! -p /tmp/com_pipe ]]; then
mkfifo /tmp/com_pipe
fi
# Wait for containers
while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
echo "Waiting for SQL..."
sleep 2
done
# Do not attempt to write to slave
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT}"
else
REDIS_CMDLINE="redis-cli -h redis -p 6379"
fi
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..."
sleep 2
done
${REDIS_CMDLINE} DEL F2B_RES > /dev/null
# Common functions
get_ipv6(){
local IPV6=
local IPV6_SRCS=
local TRY=
IPV6_SRCS[0]="ip6.mailcow.email"
IPV6_SRCS[1]="ip6.nevondo.com"
until [[ ! -z ${IPV6} ]] || [[ ${TRY} -ge 10 ]]; do
IPV6=$(curl --connect-timeout 3 -m 10 -L6s ${IPV6_SRCS[$RANDOM % ${#IPV6_SRCS[@]} ]} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$")
[[ ! -z ${TRY} ]] && sleep 1
TRY=$((TRY+1))
done
echo ${IPV6}
}
array_diff() {
# https://stackoverflow.com/questions/2312762, Alex Offshore
eval local ARR1=\(\"\${$2[@]}\"\)
eval local ARR2=\(\"\${$3[@]}\"\)
local IFS=$'\n'
mapfile -t $1 < <(comm -23 <(echo "${ARR1[*]}" | sort) <(echo "${ARR2[*]}" | sort))
}
progress() {
SERVICE=${1}
TOTAL=${2}
CURRENT=${3}
DIFF=${4}
[[ -z ${DIFF} ]] && DIFF=0
[[ -z ${TOTAL} || -z ${CURRENT} ]] && return
[[ ${CURRENT} -gt ${TOTAL} ]] && return
[[ ${CURRENT} -lt 0 ]] && CURRENT=0
PERCENT=$(( 200 * ${CURRENT} / ${TOTAL} % 2 + 100 * ${CURRENT} / ${TOTAL} ))
${REDIS_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"service\":\"${SERVICE}\",\"lvl\":\"${PERCENT}\",\"hpnow\":\"${CURRENT}\",\"hptotal\":\"${TOTAL}\",\"hpdiff\":\"${DIFF}\"}" > /dev/null
log_msg "${SERVICE} health level: ${PERCENT}% (${CURRENT}/${TOTAL}), health trend: ${DIFF}" no_redis
# Return 10 to indicate a dead service
[ ${CURRENT} -le 0 ] && return 10
}
log_msg() {
if [[ ${2} != "no_redis" ]]; then
${REDIS_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \
tr '\r\n%&;$"_[]{}-' ' ')\"}" > /dev/null
fi
echo $(date) $(printf '%s\n' "${1}")
}
function mail_error() {
THROTTLE=
[[ -z ${1} ]] && return 1
# If exists, body will be the content of "/tmp/${1}", even if ${2} is set
[[ -z ${2} ]] && BODY="Service was restarted on $(date), please check your mailcow installation." || BODY="$(date) - ${2}"
# If exists, mail will be throttled by argument in seconds
[[ ! -z ${3} ]] && THROTTLE=${3}
if [[ ! -z ${THROTTLE} ]]; then
TTL_LEFT="$(${REDIS_CMDLINE} TTL THROTTLE_${1} 2> /dev/null)"
if [[ "${TTL_LEFT}" == "-2" ]]; then
# Delay key not found, setting a delay key now
${REDIS_CMDLINE} SET THROTTLE_${1} 1 EX ${THROTTLE}
else
log_msg "Not sending notification email now, blocked for ${TTL_LEFT} seconds..."
return 1
fi
fi
WATCHDOG_NOTIFY_EMAIL=$(echo "${WATCHDOG_NOTIFY_EMAIL}" | sed 's/"//;s|"$||')
# Some exceptions for subject and body formats
if [[ ${1} == "fail2ban" ]]; then
SUBJECT="${BODY}"
BODY="Please see netfilter-mailcow for more details and triggered rules."
else
- SUBJECT="Watchdog ALERT: ${1}"
+ SUBJECT="${WATCHDOG_SUBJECT}: ${1}"
fi
IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}"
for rcpt in "${MAIL_RCPTS[@]}"; do
RCPT_DOMAIN=
#RCPT_MX=
RCPT_DOMAIN=$(echo ${rcpt} | awk -F @ {'print $NF'})
# Latest smtp-cli looks up mx via dns
#RCPT_MX=$(dig +short ${RCPT_DOMAIN} mx | sort -n | awk '{print $2; exit}')
#if [[ -z ${RCPT_MX} ]]; then
# log_msg "Cannot determine MX for ${rcpt}, skipping email notification..."
# return 1
#fi
[ -f "/tmp/${1}" ] && BODY="/tmp/${1}"
timeout 10s ./smtp-cli --missing-modules-ok \
--charset=UTF-8 \
--subject="${SUBJECT}" \
--body-plain="${BODY}" \
--add-header="X-Priority: 1" \
--to=${rcpt} \
--from="watchdog@${MAILCOW_HOSTNAME}" \
--hello-host=${MAILCOW_HOSTNAME} \
--ipv4
#--server="${RCPT_MX}"
log_msg "Sent notification email to ${rcpt}"
done
}
get_container_ip() {
# ${1} is container
CONTAINER_ID=()
CONTAINER_IPS=()
CONTAINER_IP=
LOOP_C=1
until [[ ${CONTAINER_IP} =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || [[ ${LOOP_C} -gt 5 ]]; do
if [ ${IP_BY_DOCKER_API} -eq 0 ]; then
CONTAINER_IP=$(dig a "${1}" +short)
else
sleep 0.5
# get long container id for exact match
CONTAINER_ID=($(curl --silent --insecure https://dockerapi/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
CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf))
if [[ ! -z ${CONTAINER_ID} ]]; then
for matched_container in "${CONTAINER_ID[@]}"; do
CONTAINER_IPS=($(curl --silent --insecure https://dockerapi/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress'))
for ip_match in "${CONTAINER_IPS[@]}"; do
# grep will do nothing if one of these vars is empty
[[ -z ${ip_match} ]] && continue
[[ -z ${IPV4_NETWORK} ]] && continue
# only return ips that are part of our network
if ! grep -q ${IPV4_NETWORK} <(echo ${ip_match}); then
continue
else
CONTAINER_IP=${ip_match}
break
fi
done
[[ ! -z ${CONTAINER_IP} ]] && break
done
fi
fi
LOOP_C=$((LOOP_C + 1))
done
[[ ${LOOP_C} -gt 5 ]] && echo 240.0.0.0 || echo ${CONTAINER_IP}
}
# One-time check
if grep -qi "$(echo ${IPV6_NETWORK} | cut -d: -f1-3)" <<< "$(ip a s)"; then
if [[ -z "$(get_ipv6)" ]]; then
mail_error "ipv6-config" "enable_ipv6 is true in docker-compose.yml, but an IPv6 link could not be established. Please verify your IPv6 connection."
fi
fi
external_checks() {
err_count=0
diff_c=0
THRESHOLD=${EXTERNAL_CHECKS_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
GUID=$(mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'GUID'" -BN)
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count}
CHECK_REPONSE="$(curl --connect-timeout 3 -m 10 -4 -s https://checks.mailcow.email -X POST -dguid=${GUID} 2> /dev/null)"
if [[ ! -z "${CHECK_REPONSE}" ]] && [[ "$(echo ${CHECK_REPONSE} | jq -r .response)" == "critical" ]]; then
echo ${CHECK_REPONSE} | jq -r .out > /tmp/external_checks
err_count=$(( ${err_count} + 1 ))
fi
CHECK_REPONSE6="$(curl --connect-timeout 3 -m 10 -6 -s https://checks.mailcow.email -X POST -dguid=${GUID} 2> /dev/null)"
if [[ ! -z "${CHECK_REPONSE6}" ]] && [[ "$(echo ${CHECK_REPONSE6} | jq -r .response)" == "critical" ]]; then
echo ${CHECK_REPONSE} | jq -r .out > /tmp/external_checks
err_count=$(( ${err_count} + 1 ))
fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "External checks" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 60
else
diff_c=0
sleep $(( ( RANDOM % 20 ) + 1800 ))
fi
done
return 1
}
nginx_checks() {
err_count=0
diff_c=0
THRESHOLD=${NGINX_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/nginx-mailcow; echo "$(tail -50 /tmp/nginx-mailcow)" > /tmp/nginx-mailcow
host_ip=$(get_container_ip nginx-mailcow)
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u / -p 8081 2>> /tmp/nginx-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Nginx" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
unbound_checks() {
err_count=0
diff_c=0
THRESHOLD=${UNBOUND_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/unbound-mailcow; echo "$(tail -50 /tmp/unbound-mailcow)" > /tmp/unbound-mailcow
host_ip=$(get_container_ip unbound-mailcow)
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_dns -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
DNSSEC=$(dig com +dnssec | egrep 'flags:.+ad')
if [[ -z ${DNSSEC} ]]; then
echo "DNSSEC failure" 2>> /tmp/unbound-mailcow 1>&2
err_count=$(( ${err_count} + 1))
else
echo "DNSSEC check succeeded" 2>> /tmp/unbound-mailcow 1>&2
fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Unbound" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
redis_checks() {
# A check for the local redis container
err_count=0
diff_c=0
THRESHOLD=${REDIS_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/redis-mailcow; echo "$(tail -50 /tmp/redis-mailcow)" > /tmp/redis-mailcow
host_ip=$(get_container_ip redis-mailcow)
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_tcp -4 -H redis-mailcow -p 6379 -E -s "PING\n" -q "QUIT" -e "PONG" 2>> /tmp/redis-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Redis" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
mysql_checks() {
err_count=0
diff_c=0
THRESHOLD=${MYSQL_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/mysql-mailcow; echo "$(tail -50 /tmp/mysql-mailcow)" > /tmp/mysql-mailcow
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_mysql -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
/usr/lib/nagios/plugins/check_mysql_query -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "MySQL/MariaDB" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
mysql_repl_checks() {
err_count=0
diff_c=0
THRESHOLD=${MYSQL_REPLICATION_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/mysql_repl_checks; echo "$(tail -50 /tmp/mysql_repl_checks)" > /tmp/mysql_repl_checks
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_mysql_slavestatus.sh -S /var/run/mysqld/mysqld.sock -u root -p ${DBROOT} 2>> /tmp/mysql_repl_checks 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "MySQL/MariaDB replication" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 60
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
sogo_checks() {
err_count=0
diff_c=0
THRESHOLD=${SOGO_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/sogo-mailcow; echo "$(tail -50 /tmp/sogo-mailcow)" > /tmp/sogo-mailcow
host_ip=$(get_container_ip sogo-mailcow)
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_http -4 -H ${host_ip} -u /SOGo.index/ -p 20000 -R "SOGo\.MainUI" 2>> /tmp/sogo-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "SOGo" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
postfix_checks() {
err_count=0
diff_c=0
THRESHOLD=${POSTFIX_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/postfix-mailcow; echo "$(tail -50 /tmp/postfix-mailcow)" > /tmp/postfix-mailcow
host_ip=$(get_container_ip postfix-mailcow)
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 589 -f "watchdog@invalid" -C "RCPT TO:watchdog@localhost" -C DATA -C . -R 250 2>> /tmp/postfix-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
/usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 589 -S 2>> /tmp/postfix-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Postfix" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
clamd_checks() {
err_count=0
diff_c=0
THRESHOLD=${CLAMD_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/clamd-mailcow; echo "$(tail -50 /tmp/clamd-mailcow)" > /tmp/clamd-mailcow
host_ip=$(get_container_ip clamd-mailcow)
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_clamd -4 -H ${host_ip} 2>> /tmp/clamd-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Clamd" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 120 ) + 20 ))
fi
done
return 1
}
dovecot_checks() {
err_count=0
diff_c=0
THRESHOLD=${DOVECOT_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/dovecot-mailcow; echo "$(tail -50 /tmp/dovecot-mailcow)" > /tmp/dovecot-mailcow
host_ip=$(get_container_ip dovecot-mailcow)
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_smtp -4 -H ${host_ip} -p 24 -f "watchdog@invalid" -C "RCPT TO:<watchdog@invalid>" -L -R "User doesn't exist" 2>> /tmp/dovecot-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
/usr/lib/nagios/plugins/check_imap -4 -H ${host_ip} -p 993 -S -e "OK " 2>> /tmp/dovecot-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
/usr/lib/nagios/plugins/check_imap -4 -H ${host_ip} -p 143 -e "OK " 2>> /tmp/dovecot-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
/usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 10001 -e "VERSION" 2>> /tmp/dovecot-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
/usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 4190 -e "Dovecot ready" 2>> /tmp/dovecot-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Dovecot" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
dovecot_repl_checks() {
err_count=0
diff_c=0
THRESHOLD=${DOVECOT_REPL_THRESHOLD}
D_REPL_STATUS=$(redis-cli -h redis -r GET DOVECOT_REPL_HEALTH)
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count}
D_REPL_STATUS=$(redis-cli --raw -h redis GET DOVECOT_REPL_HEALTH)
if [[ "${D_REPL_STATUS}" != "1" ]]; then
err_count=$(( ${err_count} + 1 ))
fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Dovecot replication" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 60
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
cert_checks() {
err_count=0
diff_c=0
THRESHOLD=7
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/certcheck; echo "$(tail -50 /tmp/certcheck)" > /tmp/certcheck
host_ip_postfix=$(get_container_ip postfix)
host_ip_dovecot=$(get_container_ip dovecot)
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_smtp -H ${host_ip_postfix} -p 589 -4 -S -D 7 2>> /tmp/certcheck 1>&2; err_count=$(( ${err_count} + $? ))
/usr/lib/nagios/plugins/check_imap -H ${host_ip_dovecot} -p 993 -4 -S -D 7 2>> /tmp/certcheck 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Primary certificate expiry check" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
# Always sleep 5 minutes, mail notifications are limited
sleep 300
done
return 1
}
phpfpm_checks() {
err_count=0
diff_c=0
THRESHOLD=${PHPFPM_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/php-fpm-mailcow; echo "$(tail -50 /tmp/php-fpm-mailcow)" > /tmp/php-fpm-mailcow
host_ip=$(get_container_ip php-fpm-mailcow)
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_tcp -H ${host_ip} -p 9001 2>> /tmp/php-fpm-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
/usr/lib/nagios/plugins/check_tcp -H ${host_ip} -p 9002 2>> /tmp/php-fpm-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "PHP-FPM" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
ratelimit_checks() {
err_count=0
diff_c=0
THRESHOLD=${RATELIMIT_THRESHOLD}
RL_LOG_STATUS=$(redis-cli -h redis LRANGE RL_LOG 0 0 | jq .qid)
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count}
RL_LOG_STATUS_PREV=${RL_LOG_STATUS}
RL_LOG_STATUS=$(redis-cli -h redis LRANGE RL_LOG 0 0 | jq .qid)
if [[ ${RL_LOG_STATUS_PREV} != ${RL_LOG_STATUS} ]]; then
err_count=$(( ${err_count} + 1 ))
echo 'Last 10 applied ratelimits (may overlap with previous reports).' > /tmp/ratelimit
echo 'Full ratelimit buckets can be emptied by deleting the ratelimit hash from within mailcow UI (see /debug -> Protocols -> Ratelimit):' >> /tmp/ratelimit
echo >> /tmp/ratelimit
redis-cli --raw -h redis LRANGE RL_LOG 0 10 | jq . >> /tmp/ratelimit
fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Ratelimit" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
mailq_checks() {
err_count=0
diff_c=0
THRESHOLD=${MAILQ_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/mail_queue_status; echo "$(tail -50 /tmp/mail_queue_status)" > /tmp/mail_queue_status
MAILQ_LOG_STATUS=$(find /var/spool/postfix/deferred -type f | wc -l)
echo "Mail queue contains ${MAILQ_LOG_STATUS} items (critical limit is ${MAILQ_CRIT}) at $(date)" >> /tmp/mail_queue_status
err_c_cur=${err_count}
if [ ${MAILQ_LOG_STATUS} -ge ${MAILQ_CRIT} ]; then
err_count=$(( ${err_count} + 1 ))
echo "Mail queue contains ${MAILQ_LOG_STATUS} items (critical limit is ${MAILQ_CRIT}) at $(date)" >> /tmp/mail_queue_status
fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Mail queue" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 60
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
fail2ban_checks() {
err_count=0
diff_c=0
THRESHOLD=${FAIL2BAN_THRESHOLD}
F2B_LOG_STATUS=($(${REDIS_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS))
F2B_RES=
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count}
F2B_LOG_STATUS_PREV=(${F2B_LOG_STATUS[@]})
F2B_LOG_STATUS=($(${REDIS_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS))
array_diff F2B_RES F2B_LOG_STATUS F2B_LOG_STATUS_PREV
if [[ ! -z "${F2B_RES}" ]]; then
err_count=$(( ${err_count} + 1 ))
echo -n "${F2B_RES[@]}" | tr -cd "[a-fA-F0-9.:/] " | timeout 3s ${REDIS_CMDLINE} -x SET F2B_RES > /dev/null
if [ $? -ne 0 ]; then
${REDIS_CMDLINE} -x DEL F2B_RES
fi
fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Fail2ban" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
acme_checks() {
err_count=0
diff_c=0
THRESHOLD=${ACME_THRESHOLD}
ACME_LOG_STATUS=$(redis-cli -h redis GET ACME_FAIL_TIME)
if [[ -z "${ACME_LOG_STATUS}" ]]; then
${REDIS_CMDLINE} SET ACME_FAIL_TIME 0
ACME_LOG_STATUS=0
fi
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count}
ACME_LOG_STATUS_PREV=${ACME_LOG_STATUS}
ACME_LC=0
until [[ ! -z ${ACME_LOG_STATUS} ]] || [ ${ACME_LC} -ge 3 ]; do
ACME_LOG_STATUS=$(redis-cli -h redis GET ACME_FAIL_TIME 2> /dev/null)
sleep 3
ACME_LC=$((ACME_LC+1))
done
if [[ ${ACME_LOG_STATUS_PREV} != ${ACME_LOG_STATUS} ]]; then
err_count=$(( ${err_count} + 1 ))
fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "ACME" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
ipv6nat_checks() {
err_count=0
diff_c=0
THRESHOLD=${IPV6NAT_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count}
CONTAINERS=$(curl --silent --insecure https://dockerapi/containers/json)
IPV6NAT_CONTAINER_ID=$(echo ${CONTAINERS} | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"ipv6nat-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")
if [[ ! -z ${IPV6NAT_CONTAINER_ID} ]]; then
LATEST_STARTED="$(echo ${CONTAINERS} | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], StartedAt: .State.StartedAt}" | jq -rc "select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | select( .name | tostring | contains(\"ipv6nat-mailcow\") | not)" | jq -rc .StartedAt | xargs -n1 date +%s -d | sort | tail -n1)"
LATEST_IPV6NAT="$(echo ${CONTAINERS} | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], StartedAt: .State.StartedAt}" | jq -rc "select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | select( .name | tostring | contains(\"ipv6nat-mailcow\"))" | jq -rc .StartedAt | xargs -n1 date +%s -d | sort | tail -n1)"
DIFFERENCE_START_TIME=$(expr ${LATEST_IPV6NAT} - ${LATEST_STARTED} 2>/dev/null)
if [[ "${DIFFERENCE_START_TIME}" -lt 30 ]]; then
err_count=$(( ${err_count} + 1 ))
fi
fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "IPv6 NAT" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 30
else
diff_c=0
sleep 300
fi
done
return 1
}
rspamd_checks() {
err_count=0
diff_c=0
THRESHOLD=${RSPAMD_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/rspamd-mailcow; echo "$(tail -50 /tmp/rspamd-mailcow)" > /tmp/rspamd-mailcow
host_ip=$(get_container_ip rspamd-mailcow)
err_c_cur=${err_count}
SCORE=$(echo 'To: null@localhost
From: watchdog@localhost
Empty
' | usr/bin/curl --max-time 10 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan | jq -rc .default.required_score)
if [[ ${SCORE} != "9999" ]]; then
echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2
err_count=$(( ${err_count} + 1))
else
echo "Rspamd settings check succeeded, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2
fi
# A dirty hack until a PING PONG event is implemented to worker proxy
# We expect an empty response, not a timeout
if [ "$(curl -s --max-time 10 ${host_ip}:9900 2> /dev/null ; echo $?)" == "28" ]; then
echo "Milter check failed" 2>> /tmp/rspamd-mailcow 1>&2; err_count=$(( ${err_count} + 1 ));
else
echo "Milter check succeeded" 2>> /tmp/rspamd-mailcow 1>&2
fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Rspamd" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
olefy_checks() {
err_count=0
diff_c=0
THRESHOLD=${OLEFY_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/olefy-mailcow; echo "$(tail -50 /tmp/olefy-mailcow)" > /tmp/olefy-mailcow
host_ip=$(get_container_ip olefy-mailcow)
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 10055 -s "PING\n" 2>> /tmp/olefy-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Olefy" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
# Notify about start
if [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]]; then
mail_error "watchdog-mailcow" "Watchdog started monitoring mailcow."
fi
# Create watchdog agents
(
while true; do
if ! nginx_checks; then
log_msg "Nginx hit error limit"
echo nginx-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned nginx_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
if [[ ${WATCHDOG_EXTERNAL_CHECKS} =~ ^([yY][eE][sS]|[yY])+$ ]]; then
(
while true; do
if ! external_checks; then
log_msg "External checks hit error limit"
echo external_checks > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned external_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
fi
if [[ ${WATCHDOG_MYSQL_REPLICATION_CHECKS} =~ ^([yY][eE][sS]|[yY])+$ ]]; then
(
while true; do
if ! mysql_repl_checks; then
log_msg "MySQL replication check hit error limit"
echo mysql_repl_checks > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned mysql_repl_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
fi
(
while true; do
if ! mysql_checks; then
log_msg "MySQL hit error limit"
echo mysql-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned mysql_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! redis_checks; then
log_msg "Local Redis hit error limit"
echo redis-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned redis_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! phpfpm_checks; then
log_msg "PHP-FPM hit error limit"
echo php-fpm-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned phpfpm_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
if [[ "${SKIP_SOGO}" =~ ^([nN][oO]|[nN])+$ ]]; then
(
while true; do
if ! sogo_checks; then
log_msg "SOGo hit error limit"
echo sogo-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned sogo_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
fi
if [ ${CHECK_UNBOUND} -eq 1 ]; then
(
while true; do
if ! unbound_checks; then
log_msg "Unbound hit error limit"
echo unbound-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned unbound_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
fi
if [[ "${SKIP_CLAMD}" =~ ^([nN][oO]|[nN])+$ ]]; then
(
while true; do
if ! clamd_checks; then
log_msg "Clamd hit error limit"
echo clamd-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned clamd_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
fi
(
while true; do
if ! postfix_checks; then
log_msg "Postfix hit error limit"
echo postfix-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned postfix_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! mailq_checks; then
log_msg "Mail queue hit error limit"
echo mail_queue_status > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned mailq_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! dovecot_checks; then
log_msg "Dovecot hit error limit"
echo dovecot-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned dovecot_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! dovecot_repl_checks; then
log_msg "Dovecot hit error limit"
echo dovecot_repl_checks > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned dovecot_repl_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! rspamd_checks; then
log_msg "Rspamd hit error limit"
echo rspamd-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned rspamd_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! ratelimit_checks; then
log_msg "Ratelimit hit error limit"
echo ratelimit > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned ratelimit_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! fail2ban_checks; then
log_msg "Fail2ban hit error limit"
echo fail2ban > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned fail2ban_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! cert_checks; then
log_msg "Cert check hit error limit"
echo certcheck > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned cert_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! olefy_checks; then
log_msg "Olefy hit error limit"
echo olefy-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned olefy_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! acme_checks; then
log_msg "ACME client hit error limit"
echo acme-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned acme_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! ipv6nat_checks; then
log_msg "IPv6 NAT warning: ipv6nat-mailcow container was not started at least 30s after siblings (not an error)"
echo ipv6nat-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned ipv6nat_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
# Monitor watchdog agents, stop script when agents fails and wait for respawn by Docker (restart:always:n)
(
while true; do
for bg_task in ${BACKGROUND_TASKS[*]}; do
if ! kill -0 ${bg_task} 1>&2; then
log_msg "Worker ${bg_task} died, stopping watchdog and waiting for respawn..."
kill -TERM 1
fi
sleep 10
done
done
) &
# Monitor dockerapi
(
while true; do
while nc -z dockerapi 443; do
sleep 3
done
log_msg "Cannot find dockerapi-mailcow, waiting to recover..."
kill -STOP ${BACKGROUND_TASKS[*]}
until nc -z dockerapi 443; do
sleep 3
done
kill -CONT ${BACKGROUND_TASKS[*]}
kill -USR1 ${BACKGROUND_TASKS[*]}
done
) &
# Actions when threshold limit is reached
while true; do
CONTAINER_ID=
HAS_INITDB=
read com_pipe_answer </tmp/com_pipe
if [ -s "/tmp/${com_pipe_answer}" ]; then
cat "/tmp/${com_pipe_answer}"
fi
if [[ ${com_pipe_answer} == "ratelimit" ]]; then
log_msg "At least one ratelimit was applied"
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}"
elif [[ ${com_pipe_answer} == "mail_queue_status" ]]; then
log_msg "Mail queue status is critical"
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}"
elif [[ ${com_pipe_answer} == "external_checks" ]]; then
log_msg "Your mailcow is an open relay!"
# Define $2 to override message text, else print service was restarted at ...
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please stop mailcow now and check your network configuration!"
elif [[ ${com_pipe_answer} == "mysql_repl_checks" ]]; then
log_msg "MySQL replication is not working properly"
# Define $2 to override message text, else print service was restarted at ...
# Once mail per 10 minutes
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check the SQL replication status" 600
elif [[ ${com_pipe_answer} == "dovecot_repl_checks" ]]; then
log_msg "Dovecot replication is not working properly"
# Define $2 to override message text, else print service was restarted at ...
# Once mail per 10 minutes
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check the Dovecot replicator status" 600
elif [[ ${com_pipe_answer} == "certcheck" ]]; then
log_msg "Certificates are about to expire"
# Define $2 to override message text, else print service was restarted at ...
# Only mail once a day
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please renew your certificate" 86400
elif [[ ${com_pipe_answer} == "acme-mailcow" ]]; then
log_msg "acme-mailcow did not complete successfully"
# Define $2 to override message text, else print service was restarted at ...
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check acme-mailcow for further information."
elif [[ ${com_pipe_answer} == "fail2ban" ]]; then
F2B_RES=($(timeout 4s ${REDIS_CMDLINE} --raw GET F2B_RES 2> /dev/null))
if [[ ! -z "${F2B_RES}" ]]; then
${REDIS_CMDLINE} DEL F2B_RES > /dev/null
host=
for host in "${F2B_RES[@]}"; do
log_msg "Banned ${host}"
rm /tmp/fail2ban 2> /dev/null
timeout 2s whois "${host}" > /tmp/fail2ban
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && [[ ${WATCHDOG_NOTIFY_BAN} =~ ^([yY][eE][sS]|[yY])+$ ]] && mail_error "${com_pipe_answer}" "IP ban: ${host}"
done
fi
elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
kill -STOP ${BACKGROUND_TASKS[*]}
sleep 10
CONTAINER_ID=$(curl --silent --insecure https://dockerapi/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 [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then
HAS_INITDB=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true)
fi
S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://dockerapi/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d)))
if [ ${S_RUNNING} -lt 360 ]; then
log_msg "Container is running for less than 360 seconds, skipping action..."
elif [[ ! -z ${HAS_INITDB} ]]; then
log_msg "Database is being initialized by php-fpm-mailcow, not restarting but delaying checks for a minute..."
sleep 60
else
log_msg "Sending restart command to ${CONTAINER_ID}..."
curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/restart
if [[ ${com_pipe_answer} != "ipv6nat-mailcow" ]]; then
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}"
fi
log_msg "Wait for restarted container to settle and continue watching..."
sleep 35
fi
fi
kill -CONT ${BACKGROUND_TASKS[*]}
sleep 1
kill -USR1 ${BACKGROUND_TASKS[*]}
fi
done
diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php
index db2b3dfd..0269c9fc 100644
--- a/data/web/inc/functions.inc.php
+++ b/data/web/inc/functions.inc.php
@@ -1,1967 +1,1973 @@
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
function is_valid_regex($exp) {
return @preg_match($exp, '') !== false;
}
function isset_has_content($var) {
if (isset($var) && $var != "") {
return true;
}
else {
return false;
}
}
// Validates ips and cidrs
function valid_network($network) {
if (filter_var($network, FILTER_VALIDATE_IP)) {
return true;
}
$parts = explode('/', $network);
if (count($parts) != 2) {
return false;
}
$ip = $parts[0];
$netmask = $parts[1];
if (!preg_match("/^\d+$/", $netmask)){
return false;
}
$netmask = intval($parts[1]);
if ($netmask < 0) {
return false;
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return $netmask <= 32;
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return $netmask <= 128;
}
return false;
}
function valid_hostname($hostname) {
return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
}
// Thanks to https://stackoverflow.com/a/49373789
// Validates exact ip matches and ip-in-cidr, ipv4 and ipv6
function ip_acl($ip, $networks) {
foreach($networks as $network) {
if (filter_var($network, FILTER_VALIDATE_IP)) {
if ($ip == $network) {
return true;
}
else {
continue;
}
}
$ipb = inet_pton($ip);
$iplen = strlen($ipb);
if (strlen($ipb) < 4) {
continue;
}
$ar = explode('/', $network);
$ip1 = $ar[0];
$ip1b = inet_pton($ip1);
$ip1len = strlen($ip1b);
if ($ip1len != $iplen) {
continue;
}
if (count($ar)>1) {
$bits=(int)($ar[1]);
}
else {
$bits = $iplen * 8;
}
for ($c=0; $bits>0; $c++) {
$bytemask = ($bits < 8) ? 0xff ^ ((1 << (8-$bits))-1) : 0xff;
if (((ord($ipb[$c]) ^ ord($ip1b[$c])) & $bytemask) != 0) {
continue 2;
}
$bits-=8;
}
return true;
}
return false;
}
function hash_password($password) {
// default_pass_scheme is determined in vars.inc.php (or corresponding local file)
// in case default pass scheme is not defined, falling back to BLF-CRYPT.
global $default_pass_scheme;
$pw_hash = NULL;
- switch (strtoupper($default_pass_scheme)) {
- case "SSHA":
- $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
- $pw_hash = "{SSHA}".base64_encode(hash('sha1', $password . $salt_str, true) . $salt_str);
- break;
- case "SSHA256":
- $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
- $pw_hash = "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str);
- break;
- case "SSHA512":
- $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
- $pw_hash = "{SSHA512}".base64_encode(hash('sha512', $password . $salt_str, true) . $salt_str);
- break;
- case "BLF-CRYPT":
- default:
- $pw_hash = "{BLF-CRYPT}" . password_hash($password, PASSWORD_BCRYPT);
- break;
+ // support pre-hashed passwords
+ if (preg_match('/^{(ARGON2I|ARGON2ID|BLF-CRYPT|CLEAR|CLEARTEXT|CRYPT|DES-CRYPT|LDAP-MD5|MD5|MD5-CRYPT|PBKDF2|PLAIN|PLAIN-MD4|PLAIN-MD5|PLAIN-TRUNC|PLAIN-TRUNC|SHA|SHA1|SHA256|SHA256-CRYPT|SHA512|SHA512-CRYPT|SMD5|SSHA|SSHA256|SSHA512)}/i', $password)) {
+ $pw_hash = $password;
+ }
+ else {
+ switch (strtoupper($default_pass_scheme)) {
+ case "SSHA":
+ $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
+ $pw_hash = "{SSHA}".base64_encode(hash('sha1', $password . $salt_str, true) . $salt_str);
+ break;
+ case "SSHA256":
+ $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
+ $pw_hash = "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str);
+ break;
+ case "SSHA512":
+ $salt_str = bin2hex(openssl_random_pseudo_bytes(8));
+ $pw_hash = "{SSHA512}".base64_encode(hash('sha512', $password . $salt_str, true) . $salt_str);
+ break;
+ case "BLF-CRYPT":
+ default:
+ $pw_hash = "{BLF-CRYPT}" . password_hash($password, PASSWORD_BCRYPT);
+ break;
+ }
}
return $pw_hash;
}
function last_login($user) {
global $pdo;
$stmt = $pdo->prepare('SELECT `remote`, `time` FROM `logs`
WHERE JSON_EXTRACT(`call`, "$[0]") = "check_login"
AND JSON_EXTRACT(`call`, "$[1]") = :user
AND `type` = "success" ORDER BY `time` DESC LIMIT 1');
$stmt->execute(array(':user' => $user));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!empty($row)) {
return $row;
}
else {
return false;
}
}
function flush_memcached() {
try {
$m = new Memcached();
$m->addServer('memcached', 11211);
$m->flush();
}
catch ( Exception $e ) {
// Dunno
}
}
function sys_mail($_data) {
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__),
'msg' => 'access_denied'
);
return false;
}
$excludes = $_data['mass_exclude'];
$includes = $_data['mass_include'];
$mailboxes = array();
$mass_from = $_data['mass_from'];
$mass_text = $_data['mass_text'];
$mass_html = $_data['mass_html'];
$mass_subject = $_data['mass_subject'];
if (!filter_var($mass_from, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__),
'msg' => 'from_invalid'
);
return false;
}
if (empty($mass_subject)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__),
'msg' => 'subject_empty'
);
return false;
}
if (empty($mass_text)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__),
'msg' => 'text_empty'
);
return false;
}
$domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
foreach ($domains as $domain) {
foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) {
$mailboxes[] = $mailbox;
}
}
if (!empty($includes)) {
$rcpts = array_intersect($mailboxes, $includes);
}
elseif (!empty($excludes)) {
$rcpts = array_diff($mailboxes, $excludes);
}
else {
$rcpts = $mailboxes;
}
if (!empty($rcpts)) {
ini_set('max_execution_time', 0);
ini_set('max_input_time', 0);
$mail = new PHPMailer;
$mail->Timeout = 10;
$mail->SMTPOptions = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true
)
);
$mail->isSMTP();
$mail->Host = 'dovecot-mailcow';
$mail->SMTPAuth = false;
$mail->Port = 24;
$mail->setFrom($mass_from);
$mail->Subject = $mass_subject;
$mail->CharSet ="UTF-8";
if (!empty($mass_html)) {
$mail->Body = $mass_html;
$mail->AltBody = $mass_text;
}
else {
$mail->Body = $mass_text;
}
$mail->XMailer = 'MooMassMail';
foreach ($rcpts as $rcpt) {
$mail->AddAddress($rcpt);
if (!$mail->send()) {
$_SESSION['return'][] = array(
'type' => 'warning',
'log' => array(__FUNCTION__),
'msg' => 'Mailer error (RCPT "' . htmlspecialchars($rcpt) . '"): ' . str_replace('https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting', '', $mail->ErrorInfo)
);
}
$mail->ClearAllRecipients();
}
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__),
'msg' => 'Mass mail job completed, sent ' . count($rcpts) . ' mails'
);
}
function logger($_data = false) {
/*
logger() will be called as last function
To manually log a message, logger needs to be called like below.
logger(array(
'return' => array(
array(
'type' => 'danger',
'log' => array(__FUNCTION__),
'msg' => $err
)
)
));
These messages will not be printed as alert box.
To do so, push them to $_SESSION['return'] and do not call logger as they will be included automatically:
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => $err
);
*/
global $pdo;
if (!$_data) {
$_data = $_SESSION;
}
if (!empty($_data['return'])) {
$task = substr(strtoupper(md5(uniqid(rand(), true))), 0, 6);
foreach ($_data['return'] as $return) {
$type = $return['type'];
$msg = json_encode($return['msg'], JSON_UNESCAPED_UNICODE);
$call = json_encode($return['log'], JSON_UNESCAPED_UNICODE);
if (!empty($_SESSION["dual-login"]["username"])) {
$user = $_SESSION["dual-login"]["username"] . ' => ' . $_SESSION['mailcow_cc_username'];
$role = $_SESSION["dual-login"]["role"] . ' => ' . $_SESSION['mailcow_cc_role'];
}
elseif (!empty($_SESSION['mailcow_cc_username'])) {
$user = $_SESSION['mailcow_cc_username'];
$role = $_SESSION['mailcow_cc_role'];
}
else {
$user = 'unauthenticated';
$role = 'unauthenticated';
}
// We cannot log when logs is missing...
try {
$stmt = $pdo->prepare("INSERT INTO `logs` (`type`, `task`, `msg`, `call`, `user`, `role`, `remote`, `time`) VALUES
(:type, :task, :msg, :call, :user, :role, :remote, UNIX_TIMESTAMP())");
$stmt->execute(array(
':type' => $type,
':task' => $task,
':call' => $call,
':msg' => $msg,
':user' => $user,
':role' => $role,
':remote' => get_remote_ip()
));
}
catch (Exception $e) {
// Do nothing
}
}
}
else {
return true;
}
}
function hasDomainAccess($username, $role, $domain) {
global $pdo;
if (!filter_var($username, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
return false;
}
if (empty($domain) || !is_valid_domain_name($domain)) {
return false;
}
if ($role != 'admin' && $role != 'domainadmin') {
return false;
}
if ($role == 'admin') {
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain` = :domain");
$stmt->execute(array(':domain' => $domain));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
WHERE `alias_domain` = :domain");
$stmt->execute(array(':domain' => $domain));
$num_results = $num_results + count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
return true;
}
}
elseif ($role == 'domainadmin') {
$stmt = $pdo->prepare("SELECT `domain` FROM `domain_admins`
WHERE (
`active`='1'
AND `username` = :username
AND (`domain` = :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2))
)");
$stmt->execute(array(':username' => $username, ':domain1' => $domain, ':domain2' => $domain));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if (!empty($num_results)) {
return true;
}
}
return false;
}
function hasMailboxObjectAccess($username, $role, $object) {
global $pdo;
if (empty($username) || empty($role) || empty($object)) {
return false;
}
if (!filter_var(html_entity_decode(rawurldecode($username)), FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
return false;
}
if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
return false;
}
if ($username == $object) {
return true;
}
$stmt = $pdo->prepare("SELECT `domain` FROM `mailbox` WHERE `username` = :object");
$stmt->execute(array(':object' => $object));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) {
return true;
}
return false;
}
function hasAliasObjectAccess($username, $role, $object) {
global $pdo;
if (!filter_var(html_entity_decode(rawurldecode($username)), FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) {
return false;
}
if ($role != 'admin' && $role != 'domainadmin' && $role != 'user') {
return false;
}
if ($username == $object) {
return true;
}
$stmt = $pdo->prepare("SELECT `domain` FROM `alias` WHERE `address` = :object");
$stmt->execute(array(':object' => $object));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (isset($row['domain']) && hasDomainAccess($username, $role, $row['domain'])) {
return true;
}
return false;
}
function pem_to_der($pem_key) {
// Need to remove BEGIN/END PUBLIC KEY
$lines = explode("\n", trim($pem_key));
unset($lines[count($lines)-1]);
unset($lines[0]);
return base64_decode(implode('', $lines));
}
function expand_ipv6($ip) {
$hex = unpack("H*hex", inet_pton($ip));
$ip = substr(preg_replace("/([A-f0-9]{4})/", "$1:", $hex['hex']), 0, -1);
return $ip;
}
function generate_tlsa_digest($hostname, $port, $starttls = null) {
if (!is_valid_domain_name($hostname)) {
return "Not a valid hostname";
}
if (empty($starttls)) {
$context = stream_context_create(array("ssl" => array("capture_peer_cert" => true, 'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true)));
$stream = stream_socket_client('ssl://' . $hostname . ':' . $port, $error_nr, $error_msg, 5, STREAM_CLIENT_CONNECT, $context);
if (!$stream) {
$error_msg = isset($error_msg) ? $error_msg : '-';
return $error_nr . ': ' . $error_msg;
}
}
else {
$stream = stream_socket_client('tcp://' . $hostname . ':' . $port, $error_nr, $error_msg, 5);
if (!$stream) {
return $error_nr . ': ' . $error_msg;
}
$banner = fread($stream, 512 );
if (preg_match("/^220/i", $banner)) { // SMTP
fwrite($stream,"HELO tlsa.generator.local\r\n");
fread($stream, 512);
fwrite($stream,"STARTTLS\r\n");
fread($stream, 512);
}
elseif (preg_match("/imap.+starttls/i", $banner)) { // IMAP
fwrite($stream,"A1 STARTTLS\r\n");
fread($stream, 512);
}
elseif (preg_match("/^\+OK/", $banner)) { // POP3
fwrite($stream,"STLS\r\n");
fread($stream, 512);
}
elseif (preg_match("/^OK/m", $banner)) { // Sieve
fwrite($stream,"STARTTLS\r\n");
fread($stream, 512);
}
else {
return 'Unknown banner: "' . htmlspecialchars(trim($banner)) . '"';
}
// Upgrade connection
stream_set_blocking($stream, true);
stream_context_set_option($stream, 'ssl', 'capture_peer_cert', true);
stream_context_set_option($stream, 'ssl', 'verify_peer', false);
stream_context_set_option($stream, 'ssl', 'verify_peer_name', false);
stream_context_set_option($stream, 'ssl', 'allow_self_signed', true);
stream_socket_enable_crypto($stream, true, STREAM_CRYPTO_METHOD_ANY_CLIENT);
stream_set_blocking($stream, false);
}
$params = stream_context_get_params($stream);
if (!empty($params['options']['ssl']['peer_certificate'])) {
$key_resource = openssl_pkey_get_public($params['options']['ssl']['peer_certificate']);
// We cannot get ['rsa']['n'], the binary data would contain BEGIN/END PUBLIC KEY
$key_data = openssl_pkey_get_details($key_resource)['key'];
return '3 1 1 ' . openssl_digest(pem_to_der($key_data), 'sha256');
}
else {
return 'Error: Cannot read peer certificate';
}
}
function alertbox_log_parser($_data) {
global $lang;
if (isset($_data['return'])) {
foreach ($_data['return'] as $return) {
// Get type
$type = $return['type'];
// If a lang[type][msg] string exists, use it as message
if (is_string($lang[$return['type']][$return['msg']])) {
$msg = $lang[$return['type']][$return['msg']];
}
// If msg is an array, use first element as language string and run printf on it with remaining array elements
elseif (is_array($return['msg'])) {
$msg = array_shift($return['msg']);
$msg = vsprintf(
$lang[$return['type']][$msg],
$return['msg']
);
}
// If none applies, use msg as returned message
else {
$msg = $return['msg'];
}
$log_array[] = array('msg' => $msg, 'type' => json_encode($type));
}
if (!empty($log_array)) {
return $log_array;
}
}
return false;
}
function verify_salted_hash($hash, $password, $algo, $salt_length) {
// Decode hash
$dhash = base64_decode($hash);
// Get first n bytes of binary which equals a SSHA hash
$ohash = substr($dhash, 0, $salt_length);
// Remove SSHA hash from decoded hash to get original salt string
$osalt = str_replace($ohash, '', $dhash);
// Check single salted SSHA hash against extracted hash
if (hash_equals(hash($algo, $password . $osalt, true), $ohash)) {
return true;
}
return false;
}
function verify_hash($hash, $password) {
if (preg_match('/^{(.+)}(.+)/i', $hash, $hash_array)) {
$scheme = strtoupper($hash_array[1]);
$hash = $hash_array[2];
switch ($scheme) {
case "ARGON2I":
case "ARGON2ID":
case "BLF-CRYPT":
case "CRYPT":
case "DES-CRYPT":
case "MD5-CRYPT":
case "MD5":
case "SHA256-CRYPT":
case "SHA512-CRYPT":
return password_verify($password, $hash);
case "CLEAR":
case "CLEARTEXT":
case "PLAIN":
return $password == $hash;
case "LDAP-MD5":
$hash = base64_decode($hash);
return hash_equals(hash('md5', $password, true), $hash);
case "PBKDF2":
$components = explode('$', $hash);
$salt = $components[2];
$rounds = $components[3];
$hash = $components[4];
return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash);
case "PLAIN-MD4":
return hash_equals(hash('md4', $password), $hash);
case "PLAIN-MD5":
return md5($password) == $hash;
case "PLAIN-TRUNC":
$components = explode('-', $hash);
if (count($components) > 1) {
$trunc_len = $components[0];
$trunc_password = $components[1];
return substr($password, 0, $trunc_len) == $trunc_password;
} else {
return $password == $hash;
}
case "SHA":
case "SHA1":
case "SHA256":
case "SHA512":
// SHA is an alias for SHA1
$scheme = $scheme == "SHA" ? "sha1" : strtolower($scheme);
$hash = base64_decode($hash);
return hash_equals(hash($scheme, $password, true), $hash);
case "SMD5":
return verify_salted_hash($hash, $password, 'md5', 16);
case "SSHA":
return verify_salted_hash($hash, $password, 'sha1', 20);
case "SSHA256":
return verify_salted_hash($hash, $password, 'sha256', 32);
case "SSHA512":
return verify_salted_hash($hash, $password, 'sha512', 64);
default:
return false;
}
}
return false;
}
function check_login($user, $pass) {
global $pdo;
global $redis;
global $imap_server;
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => 'malformed_username'
);
return false;
}
$user = strtolower(trim($user));
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
WHERE `superadmin` = '1'
AND `active` = '1'
AND `username` = :user");
$stmt->execute(array(':user' => $user));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
if (verify_hash($row['password'], $pass)) {
if (get_tfa($user)['name'] != "none") {
$_SESSION['pending_mailcow_cc_username'] = $user;
$_SESSION['pending_mailcow_cc_role'] = "admin";
$_SESSION['pending_tfa_method'] = get_tfa($user)['name'];
unset($_SESSION['ldelay']);
$_SESSION['return'][] = array(
'type' => 'info',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => 'awaiting_tfa_confirmation'
);
return "pending";
}
else {
unset($_SESSION['ldelay']);
// Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
return "admin";
}
}
}
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
WHERE `superadmin` = '0'
AND `active`='1'
AND `username` = :user");
$stmt->execute(array(':user' => $user));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
if (verify_hash($row['password'], $pass) !== false) {
if (get_tfa($user)['name'] != "none") {
$_SESSION['pending_mailcow_cc_username'] = $user;
$_SESSION['pending_mailcow_cc_role'] = "domainadmin";
$_SESSION['pending_tfa_method'] = get_tfa($user)['name'];
unset($_SESSION['ldelay']);
$_SESSION['return'][] = array(
'type' => 'info',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => 'awaiting_tfa_confirmation'
);
return "pending";
}
else {
unset($_SESSION['ldelay']);
// Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
return "domainadmin";
}
}
}
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
INNER JOIN domain on mailbox.domain = domain.domain
WHERE `kind` NOT REGEXP 'location|thing|group'
AND `mailbox`.`active`='1'
AND `domain`.`active`='1'
AND `username` = :user");
$stmt->execute(array(':user' => $user));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
if (verify_hash($row['password'], $pass) !== false) {
unset($_SESSION['ldelay']);
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
return "user";
}
}
if (!isset($_SESSION['ldelay'])) {
$_SESSION['ldelay'] = "0";
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
}
elseif (!isset($_SESSION['mailcow_cc_username'])) {
$_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
}
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => 'login_failed'
);
sleep($_SESSION['ldelay']);
return false;
}
function formatBytes($size, $precision = 2) {
if(!is_numeric($size)) {
return "0";
}
$base = log($size, 1024);
$suffixes = array(' Byte', ' KiB', ' MiB', ' GiB', ' TiB');
if ($size == "0") {
return "0";
}
return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
}
function update_sogo_static_view() {
if (getenv('SKIP_SOGO') == "y") {
return true;
}
global $pdo;
global $lang;
$stmt = $pdo->query("SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'sogo_view'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`)
SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view");
$stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
}
flush_memcached();
}
function edit_user_account($_data) {
global $lang;
global $pdo;
$_data_log = $_data;
!isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
!isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
!isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
$username = $_SESSION['mailcow_cc_username'];
$role = $_SESSION['mailcow_cc_role'];
$password_old = $_data['user_old_pass'];
if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied'
);
return false;
}
if (isset($_data['user_new_pass']) && isset($_data['user_new_pass2'])) {
$password_new = $_data['user_new_pass'];
$password_new2 = $_data['user_new_pass2'];
}
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
WHERE `kind` NOT REGEXP 'location|thing|group'
AND `username` = :user");
$stmt->execute(array(':user' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!verify_hash($row['password'], $password_old)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied'
);
return false;
}
if (isset($password_new) && isset($password_new2)) {
if (!empty($password_new2) && !empty($password_new)) {
if ($password_new2 != $password_new) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'password_mismatch'
);
return false;
}
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password_new)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'password_complexity'
);
return false;
}
$password_hashed = hash_password($password_new);
try {
$stmt = $pdo->prepare("UPDATE `mailbox` SET `password` = :password_hashed, `attributes` = JSON_SET(`attributes`, '$.force_pw_update', '0') WHERE `username` = :username");
$stmt->execute(array(
':password_hashed' => $password_hashed,
':username' => $username
));
}
catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => array('mysql_error', $e)
);
return false;
}
}
}
update_sogo_static_view();
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_data_log),
'msg' => array('mailbox_modified', htmlspecialchars($username))
);
}
function user_get_alias_details($username) {
global $pdo;
$data['direct_aliases'] = false;
$data['shared_aliases'] = false;
if ($_SESSION['mailcow_cc_role'] == "user") {
$username = $_SESSION['mailcow_cc_username'];
}
if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
return false;
}
$data['address'] = $username;
$stmt = $pdo->prepare("SELECT `address` AS `shared_aliases`, `public_comment` FROM `alias`
WHERE `goto` REGEXP :username_goto
AND `address` NOT LIKE '@%'
AND `goto` != :username_goto2
AND `address` != :username_address");
$stmt->execute(array(
':username_goto' => '(^|,)'.$username.'($|,)',
':username_goto2' => $username,
':username_address' => $username
));
$run = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($run)) {
$data['shared_aliases'][$row['shared_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
//$data['shared_aliases'][] = $row['shared_aliases'];
}
$stmt = $pdo->prepare("SELECT `address` AS `direct_aliases`, `public_comment` FROM `alias`
WHERE `goto` = :username_goto
AND `address` NOT LIKE '@%'
AND `address` != :username_address");
$stmt->execute(
array(
':username_goto' => $username,
':username_address' => $username
));
$run = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($run)) {
$data['direct_aliases'][$row['direct_aliases']]['public_comment'] = htmlspecialchars($row['public_comment']);
}
$stmt = $pdo->prepare("SELECT CONCAT(local_part, '@', alias_domain) AS `ad_alias`, `alias_domain` FROM `mailbox`
LEFT OUTER JOIN `alias_domain` on `target_domain` = `domain`
WHERE `username` = :username ;");
$stmt->execute(array(':username' => $username));
$run = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($run)) {
if (empty($row['ad_alias'])) {
continue;
}
$data['direct_aliases'][$row['ad_alias']]['public_comment'] = '↪ ' . $row['alias_domain'];
}
$stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`send_as` SEPARATOR ', '), '&#10008;') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `send_as` NOT LIKE '@%';");
$stmt->execute(array(':username' => $username));
$run = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($run)) {
$data['aliases_also_send_as'] = $row['send_as'];
}
$stmt = $pdo->prepare("SELECT CONCAT_WS(', ', IFNULL(GROUP_CONCAT(DISTINCT `send_as` SEPARATOR ', '), '&#10008;'), GROUP_CONCAT(DISTINCT CONCAT('@',`alias_domain`) SEPARATOR ', ')) AS `send_as` FROM `sender_acl` LEFT JOIN `alias_domain` ON `alias_domain`.`target_domain` = TRIM(LEADING '@' FROM `send_as`) WHERE `logged_in_as` = :username AND `send_as` LIKE '@%';");
$stmt->execute(array(':username' => $username));
$run = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($run)) {
$data['aliases_send_as_all'] = $row['send_as'];
}
$stmt = $pdo->prepare("SELECT IFNULL(GROUP_CONCAT(`address` SEPARATOR ', '), '&#10008;') as `address` FROM `alias` WHERE `goto` REGEXP :username AND `address` LIKE '@%';");
$stmt->execute(array(':username' => '(^|,)'.$username.'($|,)'));
$run = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($run)) {
$data['is_catch_all'] = $row['address'];
}
return $data;
}
function is_valid_domain_name($domain_name) {
if (empty($domain_name)) {
return false;
}
$domain_name = idn_to_ascii($domain_name, 0, INTL_IDNA_VARIANT_UTS46);
return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name)
&& preg_match("/^.{1,253}$/", $domain_name)
&& preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name));
}
function set_tfa($_data) {
global $pdo;
global $yubi;
global $u2f;
global $tfa;
$_data_log = $_data;
!isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
$username = $_SESSION['mailcow_cc_username'];
if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
$_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
WHERE `username` = :user");
$stmt->execute(array(':user' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!verify_hash($row['password'], $_data["confirm_password"])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied'
);
return false;
}
switch ($_data["tfa_method"]) {
case "yubi_otp":
$key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
$yubico_id = $_data['yubico_id'];
$yubico_key = $_data['yubico_key'];
$yubi = new Auth_Yubico($yubico_id, $yubico_key);
if (!$yubi) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied'
);
return false;
}
if (!ctype_alnum($_data["otp_token"]) || strlen($_data["otp_token"]) != 44) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'tfa_token_invalid'
);
return false;
}
$yauth = $yubi->verify($_data["otp_token"]);
if (PEAR::isError($yauth)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => array('yotp_verification_failed', $yauth->getMessage())
);
return false;
}
try {
// We could also do a modhex translation here
$yubico_modhex_id = substr($_data["otp_token"], 0, 12);
$stmt = $pdo->prepare("DELETE FROM `tfa`
WHERE `username` = :username
AND (`authmech` != 'yubi_otp')
OR (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
$stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
$stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES
(:key_id, :username, 'yubi_otp', '1', :secret)");
$stmt->execute(array(':key_id' => $key_id, ':username' => $username, ':secret' => $yubico_id . ':' . $yubico_key . ':' . $yubico_modhex_id));
}
catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => array('mysql_error', $e)
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_data_log),
'msg' => array('object_modified', htmlspecialchars($username))
);
break;
case "u2f":
$key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
try {
$reg = $u2f->doRegister(json_decode($_SESSION['regReq']), json_decode($_data['token']));
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `authmech` != 'u2f'");
$stmt->execute(array(':username' => $username));
$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`) VALUES (?, ?, 'u2f', ?, ?, ?, ?, '1')");
$stmt->execute(array($username, $key_id, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_data_log),
'msg' => array('object_modified', $username)
);
$_SESSION['regReq'] = null;
}
catch (Exception $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => array('u2f_verification_failed', $e->getMessage())
);
$_SESSION['regReq'] = null;
return false;
}
break;
case "totp":
$key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) {
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
$stmt->execute(array($username, $key_id, $_POST['totp_secret']));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_data_log),
'msg' => array('object_modified', $username)
);
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'totp_verification_failed'
);
}
break;
case "none":
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_data_log),
'msg' => array('object_modified', htmlspecialchars($username))
);
break;
}
}
function fido2($_data) {
global $pdo;
$_data_log = $_data;
// Not logging registration data, only actions
// Silent errors for "get" requests
switch ($_data["action"]) {
case "register":
$username = $_SESSION['mailcow_cc_username'];
if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
$_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data["action"]),
'msg' => 'access_denied'
);
return false;
}
$stmt = $pdo->prepare("INSERT INTO `fido2` (`username`, `rpId`, `credentialPublicKey`, `certificateChain`, `certificate`, `certificateIssuer`, `certificateSubject`, `signatureCounter`, `AAGUID`, `credentialId`)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute(array(
$username,
$_data['registration']->rpId,
$_data['registration']->credentialPublicKey,
$_data['registration']->certificateChain,
$_data['registration']->certificate,
$_data['registration']->certificateIssuer,
$_data['registration']->certificateSubject,
$_data['registration']->signatureCounter,
$_data['registration']->AAGUID,
$_data['registration']->credentialId)
);
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_data["action"]),
'msg' => array('object_modified', $username)
);
break;
case "get_user_cids":
// Used to exclude existing CredentialIds while registering
$username = $_SESSION['mailcow_cc_username'];
if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
$_SESSION['mailcow_cc_role'] != "admin") {
return false;
}
$stmt = $pdo->prepare("SELECT `credentialId` FROM `fido2` WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$cids[] = $row['credentialId'];
}
return $cids;
break;
case "get_all_cids":
// Only needed when using fido2 with username
$stmt = $pdo->query("SELECT `credentialId` FROM `fido2`");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$cids[] = $row['credentialId'];
}
return $cids;
break;
case "get_by_b64cid":
if (!isset($_data['cid']) || empty($_data['cid'])) {
return false;
}
$stmt = $pdo->prepare("SELECT `certificateSubject`, `username`, `credentialPublicKey`, SHA2(`credentialId`, 256) AS `cid` FROM `fido2` WHERE TO_BASE64(`credentialId`) = :cid");
$stmt->execute(array(':cid' => $_data['cid']));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($row) || empty($row['credentialPublicKey']) || empty($row['username'])) {
return false;
}
$data['pub_key'] = $row['credentialPublicKey'];
$data['username'] = $row['username'];
$data['subject'] = $row['certificateSubject'];
$data['cid'] = $row['cid'];
return $data;
break;
case "get_friendly_names":
$username = $_SESSION['mailcow_cc_username'];
if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
$_SESSION['mailcow_cc_role'] != "admin") {
return false;
}
$stmt = $pdo->prepare("SELECT SHA2(`credentialId`, 256) AS `cid`, `created`, `certificateSubject`, `friendlyName` FROM `fido2` WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$fns[] = array(
"subject" => (empty($row['certificateSubject']) ? 'Unknown (' . $row['created'] . ')' : $row['certificateSubject']),
"fn" => $row['friendlyName'],
"cid" => $row['cid']
);
}
return $fns;
break;
case "unset_fido2_key":
$username = $_SESSION['mailcow_cc_username'];
if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
$_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data["action"]),
'msg' => 'access_denied'
);
return false;
}
$stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username AND SHA2(`credentialId`, 256) = :cid");
$stmt->execute(array(
':username' => $username,
':cid' => $_data['post_data']['unset_fido2_key']
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_data_log),
'msg' => array('object_modified', htmlspecialchars($username))
);
break;
case "edit_fn":
$username = $_SESSION['mailcow_cc_username'];
if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
$_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data["action"]),
'msg' => 'access_denied'
);
return false;
}
$stmt = $pdo->prepare("UPDATE `fido2` SET `friendlyName` = :friendlyName WHERE SHA2(`credentialId`, 256) = :cid AND `username` = :username");
$stmt->execute(array(
':username' => $username,
':friendlyName' => $_data['fido2_attrs']['fido2_fn'],
':cid' => $_data['fido2_attrs']['fido2_cid']
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_data_log),
'msg' => array('object_modified', htmlspecialchars($username))
);
break;
}
}
function unset_tfa_key($_data) {
// Can only unset own keys
// Needs at least one key left
global $pdo;
global $lang;
$_data_log = $_data;
$id = intval($_data['unset_tfa_key']);
$username = $_SESSION['mailcow_cc_username'];
if ($_SESSION['mailcow_cc_role'] != "domainadmin" &&
$_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied'
);
return false;
}
try {
if (!is_numeric($id)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
WHERE `username` = :username AND `active` = '1'");
$stmt->execute(array(':username' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row['keys'] == "1") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'last_key'
);
return false;
}
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id");
$stmt->execute(array(':username' => $username, ':id' => $id));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_data_log),
'msg' => array('object_modified', $username)
);
}
catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => array('mysql_error', $e)
);
return false;
}
}
function get_tfa($username = null) {
global $pdo;
if (isset($_SESSION['mailcow_cc_username'])) {
$username = $_SESSION['mailcow_cc_username'];
}
elseif (empty($username)) {
return false;
}
$stmt = $pdo->prepare("SELECT * FROM `tfa`
WHERE `username` = :username AND `active` = '1'");
$stmt->execute(array(':username' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
switch ($row["authmech"]) {
case "yubi_otp":
$data['name'] = "yubi_otp";
$data['pretty'] = "Yubico OTP";
$stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$data['additional'][] = $row;
}
return $data;
break;
case "u2f":
$data['name'] = "u2f";
$data['pretty'] = "Fido U2F";
$stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$data['additional'][] = $row;
}
return $data;
break;
case "hotp":
$data['name'] = "hotp";
$data['pretty'] = "HMAC-based OTP";
return $data;
break;
case "totp":
$data['name'] = "totp";
$data['pretty'] = "Time-based OTP";
$stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$data['additional'][] = $row;
}
return $data;
break;
default:
$data['name'] = 'none';
$data['pretty'] = "-";
return $data;
break;
}
}
function verify_tfa_login($username, $token) {
global $pdo;
global $yubi;
global $u2f;
global $tfa;
$stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`
WHERE `username` = :username AND `active` = '1'");
$stmt->execute(array(':username' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
switch ($row["authmech"]) {
case "yubi_otp":
if (!ctype_alnum($token) || strlen($token) != 44) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('yotp_verification_failed', 'token length error')
);
return false;
}
$yubico_modhex_id = substr($token, 0, 12);
$stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
WHERE `username` = :username
AND `authmech` = 'yubi_otp'
AND `active`='1'
AND `secret` LIKE :modhex");
$stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$yubico_auth = explode(':', $row['secret']);
$yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);
$yauth = $yubi->verify($token);
if (PEAR::isError($yauth)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('yotp_verification_failed', $yauth->getMessage())
);
return false;
}
else {
$_SESSION['tfa_id'] = $row['id'];
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => 'verified_yotp_login'
);
return true;
}
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('yotp_verification_failed', 'unknown')
);
return false;
break;
case "u2f":
try {
$reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), get_u2f_registrations($username), json_decode($token));
$stmt = $pdo->prepare("SELECT `id` FROM `tfa` WHERE `keyHandle` = ?");
$stmt->execute(array($reg->keyHandle));
$row_key_id = $stmt->fetch(PDO::FETCH_ASSOC);
$_SESSION['tfa_id'] = $row_key_id['id'];
$_SESSION['authReq'] = null;
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => 'verified_u2f_login'
);
return true;
}
catch (Exception $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('u2f_verification_failed', $e->getMessage())
);
$_SESSION['regReq'] = null;
return false;
}
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('u2f_verification_failed', 'unknown')
);
return false;
break;
case "hotp":
return false;
break;
case "totp":
try {
$stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
WHERE `username` = :username
AND `authmech` = 'totp'
AND `active`='1'");
$stmt->execute(array(':username' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($tfa->verifyCode($row['secret'], $_POST['token']) === true) {
$_SESSION['tfa_id'] = $row['id'];
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => 'verified_totp_login'
);
return true;
}
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => 'totp_verification_failed'
);
return false;
}
catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('mysql_error', $e)
);
return false;
}
break;
default:
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => 'unknown_tfa_method'
);
return false;
break;
}
return false;
}
function admin_api($access, $action, $data = null) {
global $pdo;
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__),
'msg' => 'access_denied'
);
return false;
}
if ($access !== "ro" && $access !== "rw") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__),
'msg' => 'invalid access type'
);
return false;
}
if ($action == "edit") {
$active = (!empty($data['active'])) ? 1 : 0;
$skip_ip_check = (isset($data['skip_ip_check'])) ? 1 : 0;
$allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $data['allow_from']));
foreach ($allow_from as $key => $val) {
if (empty($val)) {
unset($allow_from[$key]);
continue;
}
if (valid_network($val) !== true) {
$_SESSION['return'][] = array(
'type' => 'warning',
'log' => array(__FUNCTION__, $data),
'msg' => array('ip_invalid', htmlspecialchars($allow_from[$key]))
);
unset($allow_from[$key]);
continue;
}
}
$allow_from = implode(',', array_unique(array_filter($allow_from)));
if (empty($allow_from) && $skip_ip_check == 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $data),
'msg' => 'ip_list_empty'
);
return false;
}
$api_key = implode('-', array(
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3)))
));
$stmt = $pdo->query("SELECT `api_key` FROM `api` WHERE `access` = '" . $access . "'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if (empty($num_results)) {
$stmt = $pdo->prepare("INSERT INTO `api` (`api_key`, `skip_ip_check`, `active`, `allow_from`, `access`)
VALUES (:api_key, :skip_ip_check, :active, :allow_from, :access);");
$stmt->execute(array(
':api_key' => $api_key,
':skip_ip_check' => $skip_ip_check,
':active' => $active,
':allow_from' => $allow_from,
':access' => $access
));
}
else {
if ($skip_ip_check == 0) {
$stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
`active` = :active,
`allow_from` = :allow_from
WHERE `access` = :access;");
$stmt->execute(array(
':active' => $active,
':skip_ip_check' => $skip_ip_check,
':allow_from' => $allow_from,
':access' => $access
));
}
else {
$stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check,
`active` = :active
WHERE `access` = :access;");
$stmt->execute(array(
':active' => $active,
':skip_ip_check' => $skip_ip_check,
':access' => $access
));
}
}
}
elseif ($action == "regen_key") {
$api_key = implode('-', array(
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3)))
));
$stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key WHERE `access` = :access");
$stmt->execute(array(
':api_key' => $api_key,
':access' => $access
));
}
elseif ($action == "get") {
$stmt = $pdo->query("SELECT * FROM `api` WHERE `access` = '" . $access . "'");
$apidata = $stmt->fetch(PDO::FETCH_ASSOC);
$apidata['allow_from'] = str_replace(',', PHP_EOL, $apidata['allow_from']);
return $apidata;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $data),
'msg' => 'admin_api_modified'
);
}
function license($action, $data = null) {
global $pdo;
global $redis;
global $lang;
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__),
'msg' => 'access_denied'
);
return false;
}
switch ($action) {
case "verify":
// Keep result until revalidate button is pressed or session expired
$stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
$versions = $stmt->fetch(PDO::FETCH_ASSOC);
$post = array('guid' => $versions['version']);
$curl = curl_init('https://verify.mailcow.email');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
$response = curl_exec($curl);
curl_close($curl);
$json_return = json_decode($response, true);
if ($response && $json_return) {
if ($json_return['response'] === "ok") {
$_SESSION['gal']['valid'] = "true";
$_SESSION['gal']['c'] = $json_return['c'];
$_SESSION['gal']['s'] = $json_return['s'];
if ($json_return['m'] == 'NoMoore') {
$_SESSION['gal']['m'] = '🐄';
}
else {
$_SESSION['gal']['m'] = str_repeat('🐄', substr_count($json_return['m'], 'o'));
}
}
elseif ($json_return['response'] === "invalid") {
$_SESSION['gal']['valid'] = "false";
$_SESSION['gal']['c'] = $lang['mailbox']['no'];
$_SESSION['gal']['s'] = $lang['mailbox']['no'];
$_SESSION['gal']['m'] = $lang['mailbox']['no'];
}
}
else {
$_SESSION['gal']['valid'] = "false";
$_SESSION['gal']['c'] = $lang['danger']['temp_error'];
$_SESSION['gal']['s'] = $lang['danger']['temp_error'];
$_SESSION['gal']['m'] = $lang['danger']['temp_error'];
}
try {
// json_encode needs "true"/"false" instead of true/false, to not encode it to 0 or 1
$redis->Set('LICENSE_STATUS_CACHE', json_encode($_SESSION['gal']));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e)
);
return false;
}
return $_SESSION['gal']['valid'];
break;
case "guid":
$stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'GUID'");
$versions = $stmt->fetch(PDO::FETCH_ASSOC);
return $versions['version'];
break;
}
}
function rspamd_ui($action, $data = null) {
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__),
'msg' => 'access_denied'
);
return false;
}
switch ($action) {
case "edit":
$rspamd_ui_pass = $data['rspamd_ui_pass'];
$rspamd_ui_pass2 = $data['rspamd_ui_pass2'];
if (empty($rspamd_ui_pass) || empty($rspamd_ui_pass2)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, '*', '*'),
'msg' => 'password_empty'
);
return false;
}
if ($rspamd_ui_pass != $rspamd_ui_pass2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, '*', '*'),
'msg' => 'password_mismatch'
);
return false;
}
if (strlen($rspamd_ui_pass) < 6) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, '*', '*'),
'msg' => 'rspamd_ui_pw_length'
);
return false;
}
$docker_return = docker('post', 'rspamd-mailcow', 'exec', array('cmd' => 'rspamd', 'task' => 'worker_password', 'raw' => $rspamd_ui_pass), array('Content-Type: application/json'));
if ($docker_return_array = json_decode($docker_return, true)) {
if ($docker_return_array['type'] == 'success') {
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, '*', '*'),
'msg' => 'rspamd_ui_pw_set'
);
return true;
}
else {
$_SESSION['return'][] = array(
'type' => $docker_return_array['type'],
'log' => array(__FUNCTION__, '*', '*'),
'msg' => $docker_return_array['msg']
);
return false;
}
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, '*', '*'),
'msg' => 'unknown'
);
return false;
}
break;
}
}
function get_u2f_registrations($username) {
global $pdo;
$sel = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = ? AND `active` = '1'");
$sel->execute(array($username));
return $sel->fetchAll(PDO::FETCH_OBJ);
}
function get_logs($application, $lines = false) {
if ($lines === false) {
$lines = $GLOBALS['LOG_LINES'] - 1;
}
elseif(is_numeric($lines) && $lines >= 1) {
$lines = abs(intval($lines) - 1);
}
else {
list ($from, $to) = explode('-', $lines);
$from = intval($from);
$to = intval($to);
if ($from < 1 || $to < $from) { return false; }
}
global $redis;
global $pdo;
if ($_SESSION['mailcow_cc_role'] != "admin") {
return false;
}
// SQL
if ($application == "mailcow-ui") {
if (isset($from) && isset($to)) {
$stmt = $pdo->prepare("SELECT * FROM `logs` ORDER BY `id` DESC LIMIT :from, :to");
$stmt->execute(array(
':from' => $from - 1,
':to' => $to
));
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
else {
$stmt = $pdo->prepare("SELECT * FROM `logs` ORDER BY `id` DESC LIMIT :lines");
$stmt->execute(array(
':lines' => $lines + 1,
));
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
if (is_array($data)) {
return $data;
}
}
// Redis
if ($application == "dovecot-mailcow") {
if (isset($from) && isset($to)) {
$data = $redis->lRange('DOVECOT_MAILLOG', $from - 1, $to - 1);
}
else {
$data = $redis->lRange('DOVECOT_MAILLOG', 0, $lines);
}
if ($data) {
foreach ($data as $json_line) {
$data_array[] = json_decode($json_line, true);
}
return $data_array;
}
}
if ($application == "postfix-mailcow") {
if (isset($from) && isset($to)) {
$data = $redis->lRange('POSTFIX_MAILLOG', $from - 1, $to - 1);
}
else {
$data = $redis->lRange('POSTFIX_MAILLOG', 0, $lines);
}
if ($data) {
foreach ($data as $json_line) {
$data_array[] = json_decode($json_line, true);
}
return $data_array;
}
}
if ($application == "sogo-mailcow") {
if (isset($from) && isset($to)) {
$data = $redis->lRange('SOGO_LOG', $from - 1, $to - 1);
}
else {
$data = $redis->lRange('SOGO_LOG', 0, $lines);
}
if ($data) {
foreach ($data as $json_line) {
$data_array[] = json_decode($json_line, true);
}
return $data_array;
}
}
if ($application == "watchdog-mailcow") {
if (isset($from) && isset($to)) {
$data = $redis->lRange('WATCHDOG_LOG', $from - 1, $to - 1);
}
else {
$data = $redis->lRange('WATCHDOG_LOG', 0, $lines);
}
if ($data) {
foreach ($data as $json_line) {
$data_array[] = json_decode($json_line, true);
}
return $data_array;
}
}
if ($application == "acme-mailcow") {
if (isset($from) && isset($to)) {
$data = $redis->lRange('ACME_LOG', $from - 1, $to - 1);
}
else {
$data = $redis->lRange('ACME_LOG', 0, $lines);
}
if ($data) {
foreach ($data as $json_line) {
$data_array[] = json_decode($json_line, true);
}
return $data_array;
}
}
if ($application == "ratelimited") {
if (isset($from) && isset($to)) {
$data = $redis->lRange('RL_LOG', $from - 1, $to - 1);
}
else {
$data = $redis->lRange('RL_LOG', 0, $lines);
}
if ($data) {
foreach ($data as $json_line) {
$data_array[] = json_decode($json_line, true);
}
return $data_array;
}
}
if ($application == "api-mailcow") {
if (isset($from) && isset($to)) {
$data = $redis->lRange('API_LOG', $from - 1, $to - 1);
}
else {
$data = $redis->lRange('API_LOG', 0, $lines);
}
if ($data) {
foreach ($data as $json_line) {
$data_array[] = json_decode($json_line, true);
}
return $data_array;
}
}
if ($application == "netfilter-mailcow") {
if (isset($from) && isset($to)) {
$data = $redis->lRange('NETFILTER_LOG', $from - 1, $to - 1);
}
else {
$data = $redis->lRange('NETFILTER_LOG', 0, $lines);
}
if ($data) {
foreach ($data as $json_line) {
$data_array[] = json_decode($json_line, true);
}
return $data_array;
}
}
if ($application == "autodiscover-mailcow") {
if (isset($from) && isset($to)) {
$data = $redis->lRange('AUTODISCOVER_LOG', $from - 1, $to - 1);
}
else {
$data = $redis->lRange('AUTODISCOVER_LOG', 0, $lines);
}
if ($data) {
foreach ($data as $json_line) {
$data_array[] = json_decode($json_line, true);
}
return $data_array;
}
}
if ($application == "rspamd-history") {
$curl = curl_init();
curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
if (!is_numeric($lines)) {
list ($from, $to) = explode('-', $lines);
curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?from=" . intval($from) . "&to=" . intval($to));
}
else {
curl_setopt($curl, CURLOPT_URL,"http://rspamd/history?to=" . intval($lines));
}
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$history = curl_exec($curl);
if (!curl_errno($curl)) {
$data_array = json_decode($history, true);
curl_close($curl);
return $data_array['rows'];
}
curl_close($curl);
return false;
}
if ($application == "rspamd-stats") {
$curl = curl_init();
curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
curl_setopt($curl, CURLOPT_URL,"http://rspamd/stat");
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$stats = curl_exec($curl);
if (!curl_errno($curl)) {
$data_array = json_decode($stats, true);
curl_close($curl);
return $data_array;
}
curl_close($curl);
return false;
}
return false;
}
function getGUID() {
if (function_exists('com_create_guid')) {
return com_create_guid();
}
mt_srand((double)microtime()*10000);//optional for php 4.2.0 and up.
$charid = strtoupper(md5(uniqid(rand(), true)));
$hyphen = chr(45);// "-"
return substr($charid, 0, 8).$hyphen
.substr($charid, 8, 4).$hyphen
.substr($charid,12, 4).$hyphen
.substr($charid,16, 4).$hyphen
.substr($charid,20,12);
}
function solr_status() {
$curl = curl_init();
$endpoint = 'http://solr:8983/solr/admin/cores';
$params = array(
'action' => 'STATUS',
'core' => 'dovecot-fts',
'indexInfo' => 'true'
);
$url = $endpoint . '?' . http_build_query($params);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 0);
curl_setopt($curl, CURLOPT_TIMEOUT, 10);
$response_core = curl_exec($curl);
if ($response_core === false) {
$err = curl_error($curl);
curl_close($curl);
return false;
}
else {
curl_close($curl);
$curl = curl_init();
$status_core = json_decode($response_core, true);
$url = 'http://solr:8983/solr/admin/info/system';
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 0);
curl_setopt($curl, CURLOPT_TIMEOUT, 10);
$response_sysinfo = curl_exec($curl);
if ($response_sysinfo === false) {
$err = curl_error($curl);
curl_close($curl);
return false;
}
else {
curl_close($curl);
$status_sysinfo = json_decode($response_sysinfo, true);
$status = array_merge($status_core, $status_sysinfo);
return (!empty($status['status']['dovecot-fts']) && !empty($status['jvm']['memory'])) ? $status : false;
}
return (!empty($status['status']['dovecot-fts'])) ? $status['status']['dovecot-fts'] : false;
}
return false;
}
function cleanupJS($ignore = '', $folder = '/tmp/*.js') {
$now = time();
foreach (glob($folder) as $filename) {
if(strpos($filename, $ignore) !== false) {
continue;
}
if (is_file($filename)) {
if ($now - filemtime($filename) >= 60 * 60) {
unlink($filename);
}
}
}
}
function cleanupCSS($ignore = '', $folder = '/tmp/*.css') {
$now = time();
foreach (glob($folder) as $filename) {
if(strpos($filename, $ignore) !== false) {
continue;
}
if (is_file($filename)) {
if ($now - filemtime($filename) >= 60 * 60) {
unlink($filename);
}
}
}
}
?>
diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php
index 8e0c6e7c..e22abdd2 100644
--- a/data/web/inc/functions.mailbox.inc.php
+++ b/data/web/inc/functions.mailbox.inc.php
@@ -1,4311 +1,4305 @@
<?php
function mailbox($_action, $_type, $_data = null, $_extra = null) {
global $pdo;
global $redis;
global $lang;
global $MAILBOX_DEFAULT_ATTRIBUTES;
$_data_log = $_data;
!isset($_data_log['password']) ?: $_data_log['password'] = '*';
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
switch ($_action) {
case 'add':
switch ($_type) {
case 'time_limited_alias':
if (!isset($_SESSION['acl']['spam_alias']) || $_SESSION['acl']['spam_alias'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
if (isset($_data['username']) && filter_var($_data['username'], FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data['username'])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
else {
$username = $_data['username'];
}
}
else {
$username = $_SESSION['mailcow_cc_username'];
}
if (!is_numeric($_data["validity"]) || $_data["validity"] > 672) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'validity_missing'
);
return false;
}
$domain = mailbox('get', 'mailbox_details', $username)['domain'];
if (!is_valid_domain_name($domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_invalid'
);
return false;
}
$validity = strtotime("+".$_data["validity"]." hour");
$letters = 'abcefghijklmnopqrstuvwxyz1234567890';
$random_name = substr(str_shuffle($letters), 0, 24);
$stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `goto`, `validity`) VALUES
(:address, :goto, :validity)");
$stmt->execute(array(
':address' => $random_name . '@' . $domain,
':goto' => $username,
':validity' => $validity
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
break;
case 'global_filter':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
$sieve = new Sieve\SieveParser();
$script_data = $_data['script_data'];
$script_data = str_replace("\r\n", "\n", $script_data); // windows -> unix
$script_data = str_replace("\r", "\n", $script_data); // remaining -> unix
$filter_type = $_data['filter_type'];
try {
$sieve->parse($script_data);
}
catch (Exception $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('sieve_error', $e->getMessage())
);
return false;
}
if ($filter_type == 'prefilter') {
try {
if (file_exists('/global_sieve/before')) {
$filter_handle = fopen('/global_sieve/before', 'w');
if (!$filter_handle) {
throw new Exception($lang['danger']['file_open_error']);
}
fwrite($filter_handle, $script_data);
fclose($filter_handle);
}
$restart_response = json_decode(docker('post', 'dovecot-mailcow', 'restart'), true);
if ($restart_response['type'] == "success") {
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'dovecot_restart_success'
);
}
else {
$_SESSION['return'][] = array(
'type' => 'warning',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'dovecot_restart_failed'
);
}
}
catch (Exception $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('global_filter_write_error', htmlspecialchars($e->getMessage()))
);
return false;
}
}
elseif ($filter_type == 'postfilter') {
try {
if (file_exists('/global_sieve/after')) {
$filter_handle = fopen('/global_sieve/after', 'w');
if (!$filter_handle) {
throw new Exception($lang['danger']['file_open_error']);
}
fwrite($filter_handle, $script_data);
fclose($filter_handle);
}
$restart_response = json_decode(docker('post', 'dovecot-mailcow', 'restart'), true);
if ($restart_response['type'] == "success") {
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'dovecot_restart_success'
);
}
else {
$_SESSION['return'][] = array(
'type' => 'warning',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'dovecot_restart_failed'
);
}
}
catch (Exception $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('global_filter_write_error', htmlspecialchars($e->getMessage()))
);
return false;
}
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'invalid_filter_type'
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'global_filter_written'
);
return true;
case 'filter':
$sieve = new Sieve\SieveParser();
if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
if (isset($_data['username']) && filter_var($_data['username'], FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data['username'])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
else {
$username = $_data['username'];
}
}
elseif ($_SESSION['mailcow_cc_role'] == "user") {
$username = $_SESSION['mailcow_cc_username'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'no_user_defined'
);
return false;
}
$active = intval($_data['active']);
$script_data = $_data['script_data'];
$script_desc = $_data['script_desc'];
$filter_type = $_data['filter_type'];
if (empty($script_data)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'script_empty'
);
return false;
}
try {
$sieve->parse($script_data);
}
catch (Exception $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('sieve_error', $e->getMessage())
);
return false;
}
if (empty($script_data) || empty($script_desc)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'value_missing'
);
return false;
}
if ($filter_type != 'postfilter' && $filter_type != 'prefilter') {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'filter_type'
);
return false;
}
if (!empty($active)) {
$script_name = 'active';
$stmt = $pdo->prepare("UPDATE `sieve_filters` SET `script_name` = 'inactive' WHERE `username` = :username AND `filter_type` = :filter_type");
$stmt->execute(array(
':username' => $username,
':filter_type' => $filter_type
));
}
else {
$script_name = 'inactive';
}
$stmt = $pdo->prepare("INSERT INTO `sieve_filters` (`username`, `script_data`, `script_desc`, `script_name`, `filter_type`)
VALUES (:username, :script_data, :script_desc, :script_name, :filter_type)");
$stmt->execute(array(
':username' => $username,
':script_data' => $script_data,
':script_desc' => $script_desc,
':script_name' => $script_name,
':filter_type' => $filter_type
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
break;
case 'syncjob':
if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
if (isset($_data['username']) && filter_var($_data['username'], FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data['username'])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
else {
$username = $_data['username'];
}
}
elseif ($_SESSION['mailcow_cc_role'] == "user") {
$username = $_SESSION['mailcow_cc_username'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'no_user_defined'
);
return false;
}
$active = intval($_data['active']);
$subscribeall = intval($_data['subscribeall']);
$delete2duplicates = intval($_data['delete2duplicates']);
$delete1 = intval($_data['delete1']);
$delete2 = intval($_data['delete2']);
$timeout1 = intval($_data['timeout1']);
$timeout2 = intval($_data['timeout2']);
$skipcrossduplicates = intval($_data['skipcrossduplicates']);
$automap = intval($_data['automap']);
$port1 = $_data['port1'];
$host1 = strtolower($_data['host1']);
$password1 = $_data['password1'];
$exclude = $_data['exclude'];
$maxage = $_data['maxage'];
$maxbytespersecond = $_data['maxbytespersecond'];
$subfolder2 = $_data['subfolder2'];
$user1 = $_data['user1'];
$mins_interval = $_data['mins_interval'];
$enc1 = $_data['enc1'];
$custom_params = (empty(trim($_data['custom_params']))) ? '' : trim($_data['custom_params']);
// Workaround, fixme
if (strpos($custom_params, 'pipemess')) {
$custom_params = '';
}
if (empty($subfolder2)) {
$subfolder2 = "";
}
if (!isset($maxage) || !filter_var($maxage, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
$maxage = "0";
}
if (!isset($timeout1) || !filter_var($timeout1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
$timeout1 = "600";
}
if (!isset($timeout2) || !filter_var($timeout2, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
$timeout2 = "600";
}
if (!isset($maxbytespersecond) || !filter_var($maxbytespersecond, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 125000000)))) {
$maxbytespersecond = "0";
}
if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
if (!filter_var($mins_interval, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 43800)))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
// if (!is_valid_domain_name($host1)) {
// $_SESSION['return'][] = array(
// 'type' => 'danger',
// 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
// 'msg' => 'access_denied'
// );
// return false;
// }
if ($enc1 != "TLS" && $enc1 != "SSL" && $enc1 != "PLAIN") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
if (@preg_match("/" . $exclude . "/", null) === false) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
$stmt = $pdo->prepare("SELECT '1' FROM `imapsync`
WHERE `user2` = :user2 AND `user1` = :user1 AND `host1` = :host1");
$stmt->execute(array(':user1' => $user1, ':user2' => $username, ':host1' => $host1));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('object_exists', htmlspecialchars($host1 . ' / ' . $user1))
);
return false;
}
$stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `timeout1`, `timeout2`, `automap`, `skipcrossduplicates`, `maxbytespersecond`, `subscribeall`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `custom_params`, `active`)
VALUES (:user2, :exclude, :delete1, :delete2, :timeout1, :timeout2, :automap, :skipcrossduplicates, :maxbytespersecond, :subscribeall, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :custom_params, :active)");
$stmt->execute(array(
':user2' => $username,
':custom_params' => $custom_params,
':exclude' => $exclude,
':maxage' => $maxage,
':delete1' => $delete1,
':delete2' => $delete2,
':timeout1' => $timeout1,
':timeout2' => $timeout2,
':automap' => $automap,
':skipcrossduplicates' => $skipcrossduplicates,
':maxbytespersecond' => $maxbytespersecond,
':subscribeall' => $subscribeall,
':subfolder2' => $subfolder2,
':host1' => $host1,
':authmech1' => 'PLAIN',
':user1' => $user1,
':password1' => $password1,
':mins_interval' => $mins_interval,
':port1' => $port1,
':enc1' => $enc1,
':delete2duplicates' => $delete2duplicates,
':active' => $active,
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
break;
case 'domain':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
$description = $_data['description'];
$xmpp_prefix = preg_replace('/[^\da-z-]/i', '', $_data['xmpp_prefix']);
if (empty($description)) {
$description = $domain;
}
$aliases = (int)$_data['aliases'];
$mailboxes = (int)$_data['mailboxes'];
$defquota = (int)$_data['defquota'];
$maxquota = (int)$_data['maxquota'];
$restart_sogo = (int)$_data['restart_sogo'];
$quota = (int)$_data['quota'];
if ($defquota > $maxquota) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'mailbox_defquota_exceeds_mailbox_maxquota'
);
return false;
}
if ($maxquota > $quota) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'mailbox_quota_exceeds_domain_quota'
);
return false;
}
if ($defquota == "0" || empty($defquota)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'defquota_empty'
);
return false;
}
if ($maxquota == "0" || empty($maxquota)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'maxquota_empty'
);
return false;
}
$active = intval($_data['active']);
$relay_all_recipients = intval($_data['relay_all_recipients']);
$relay_unknown_only = intval($_data['relay_unknown_only']);
$backupmx = intval($_data['backupmx']);
$gal = intval($_data['gal']);
$xmpp = intval($_data['xmpp']);
if ($relay_all_recipients == 1) {
$backupmx = '1';
}
if ($relay_unknown_only == 1) {
$backupmx = 1;
$relay_all_recipients = 1;
}
if (!is_valid_domain_name($domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_invalid'
);
return false;
}
foreach (array($quota, $maxquota, $mailboxes, $aliases) as $data) {
if (!is_numeric($data)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('object_is_not_numeric', htmlspecialchars($data))
);
return false;
}
}
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain` = :domain");
$stmt->execute(array(':domain' => $domain));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain`
WHERE `alias_domain` = :domain");
$stmt->execute(array(':domain' => $domain));
$num_results = $num_results + count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('domain_exists', htmlspecialchars($domain))
);
return false;
}
if ($domain == getenv('MAILCOW_HOSTNAME')) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_cannot_match_hostname'
);
return false;
}
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `send_as` LIKE :domain");
$stmt->execute(array(
':domain' => '%@' . $domain
));
$stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `defquota`, `maxquota`, `quota`, `backupmx`, `gal`, `xmpp`, `xmpp_prefix`, `active`, `relay_unknown_only`, `relay_all_recipients`)
VALUES (:domain, :description, :aliases, :mailboxes, :defquota, :maxquota, :quota, :backupmx, :gal, :xmpp, :xmpp_prefix, :active, :relay_unknown_only, :relay_all_recipients)");
$stmt->execute(array(
':domain' => $domain,
':description' => $description,
':aliases' => $aliases,
':mailboxes' => $mailboxes,
':defquota' => $defquota,
':maxquota' => $maxquota,
':quota' => $quota,
':backupmx' => $backupmx,
':gal' => $gal,
':xmpp' => $xmpp,
':xmpp_prefix' => $xmpp_prefix,
':active' => $active,
':relay_unknown_only' => $relay_unknown_only,
':relay_all_recipients' => $relay_all_recipients
));
try {
$redis->hSet('DOMAIN_MAP', $domain, 1);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e)
);
return false;
}
if (!empty(intval($_data['rl_value']))) {
ratelimit('edit', 'domain', array('rl_value' => $_data['rl_value'], 'rl_frame' => $_data['rl_frame'], 'object' => $domain));
}
if (!empty($restart_sogo)) {
$restart_response = json_decode(docker('post', 'sogo-mailcow', 'restart'), true);
if ($restart_response['type'] == "success") {
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('domain_added', htmlspecialchars($domain))
);
return true;
}
else {
$_SESSION['return'][] = array(
'type' => 'warning',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_added_sogo_failed'
);
return false;
}
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('domain_added', htmlspecialchars($domain))
);
return true;
break;
case 'alias':
$addresses = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['address']));
$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto']));
$active = intval($_data['active']);
$sogo_visible = intval($_data['sogo_visible']);
$goto_null = intval($_data['goto_null']);
$goto_spam = intval($_data['goto_spam']);
$goto_ham = intval($_data['goto_ham']);
$private_comment = $_data['private_comment'];
$public_comment = $_data['public_comment'];
if (strlen($private_comment) > 160 | strlen($public_comment) > 160){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'comment_too_long'
);
return false;
}
if (empty($addresses[0])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'alias_empty'
);
return false;
}
if (empty($gotos[0]) && ($goto_null + $goto_spam + $goto_ham == 0)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'goto_empty'
);
return false;
}
if ($goto_null == "1") {
$goto = "null@localhost";
}
elseif ($goto_spam == "1") {
$goto = "spam@localhost";
}
elseif ($goto_ham == "1") {
$goto = "ham@localhost";
}
else {
foreach ($gotos as $i => &$goto) {
if (empty($goto)) {
continue;
}
$goto_domain = idn_to_ascii(substr(strstr($goto, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
$goto_local_part = strstr($goto, '@', true);
$goto = $goto_local_part.'@'.$goto_domain;
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
WHERE `kind` REGEXP 'location|thing|group'
AND `username`= :goto");
$stmt->execute(array(':goto' => $goto));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('goto_invalid', htmlspecialchars($goto))
);
unset($gotos[$i]);
continue;
}
if (!filter_var($goto, FILTER_VALIDATE_EMAIL) === true) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('goto_invalid', htmlspecialchars($goto))
);
unset($gotos[$i]);
continue;
}
}
$gotos = array_filter($gotos);
if (empty($gotos)) { return false; }
$goto = implode(",", $gotos);
}
foreach ($addresses as $address) {
if (empty($address)) {
continue;
}
if (in_array($address, $gotos)) {
continue;
}
$domain = idn_to_ascii(substr(strstr($address, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
$local_part = strstr($address, '@', true);
$address = $local_part.'@'.$domain;
$domaindata = mailbox('get', 'domain_details', $domain);
if (is_array($domaindata) && $domaindata['aliases_left'] == "0") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'max_alias_exceeded'
);
return false;
}
$stmt = $pdo->prepare("SELECT `address` FROM `alias`
WHERE `address`= :address OR `address` IN (
SELECT `username` FROM `mailbox`, `alias_domain`
WHERE (
`alias_domain`.`alias_domain` = :address_d
AND `mailbox`.`username` = CONCAT(:address_l, '@', alias_domain.target_domain)))");
$stmt->execute(array(
':address' => $address,
':address_l' => $local_part,
':address_d' => $domain
));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('is_alias_or_mailbox', htmlspecialchars($address))
);
continue;
}
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)");
$stmt->execute(array(':domain1' => $domain, ':domain2' => $domain));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results == 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('domain_not_found', htmlspecialchars($domain))
);
continue;
}
$stmt = $pdo->prepare("SELECT `address` FROM `spamalias`
WHERE `address`= :address");
$stmt->execute(array(':address' => $address));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('is_spam_alias', htmlspecialchars($address))
);
continue;
}
if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('alias_invalid', $address)
);
continue;
}
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `active`)
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :active)");
if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
$stmt->execute(array(
':address' => '@'.$domain,
':public_comment' => $public_comment,
':private_comment' => $private_comment,
':address' => '@'.$domain,
':goto' => $goto,
':domain' => $domain,
':sogo_visible' => $sogo_visible,
':active' => $active
));
}
else {
$stmt->execute(array(
':address' => $address,
':public_comment' => $public_comment,
':private_comment' => $private_comment,
':goto' => $goto,
':domain' => $domain,
':sogo_visible' => $sogo_visible,
':active' => $active
));
}
$id = $pdo->lastInsertId();
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('alias_added', $address, $id)
);
}
break;
case 'alias_domain':
$active = intval($_data['active']);
$alias_domains = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['alias_domain']));
$alias_domains = array_filter($alias_domains);
$target_domain = idn_to_ascii(strtolower(trim($_data['target_domain'])), 0, INTL_IDNA_VARIANT_UTS46);
if (!isset($_SESSION['acl']['alias_domains']) || $_SESSION['acl']['alias_domains'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
if (!is_valid_domain_name($target_domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'target_domain_invalid'
);
return false;
}
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $target_domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($alias_domains as $i => $alias_domain) {
$alias_domain = idn_to_ascii(strtolower(trim($alias_domain)), 0, INTL_IDNA_VARIANT_UTS46);
if (!is_valid_domain_name($alias_domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('alias_domain_invalid', htmlspecialchars(alias_domain))
);
continue;
}
if ($alias_domain == $target_domain) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('aliasd_targetd_identical', htmlspecialchars($target_domain))
);
continue;
}
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain`= :target_domain");
$stmt->execute(array(':target_domain' => $target_domain));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results == 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('targetd_not_found', htmlspecialchars($target_domain))
);
continue;
}
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain`= :target_domain AND `backupmx` = '1'");
$stmt->execute(array(':target_domain' => $target_domain));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results == 1) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('targetd_relay_domain', htmlspecialchars($target_domain))
);
continue;
}
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain`= :alias_domain
UNION
SELECT `domain` FROM `domain` WHERE `domain`= :alias_domain_in_domain");
$stmt->execute(array(':alias_domain' => $alias_domain, ':alias_domain_in_domain' => $alias_domain));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('alias_domain_invalid', $alias_domain)
);
continue;
}
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `send_as` LIKE :domain");
$stmt->execute(array(
':domain' => '%@' . $domain
));
$stmt = $pdo->prepare("INSERT INTO `alias_domain` (`alias_domain`, `target_domain`, `active`)
VALUES (:alias_domain, :target_domain, :active)");
$stmt->execute(array(
':alias_domain' => $alias_domain,
':target_domain' => $target_domain,
':active' => $active
));
try {
$redis->hSet('DOMAIN_MAP', $alias_domain, 1);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e)
);
return false;
}
if (!empty(intval($_data['rl_value']))) {
ratelimit('edit', 'domain', array('rl_value' => $_data['rl_value'], 'rl_frame' => $_data['rl_frame'], 'object' => $alias_domain));
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('aliasd_added', htmlspecialchars($alias_domain))
);
}
break;
case 'mailbox':
$local_part = strtolower(trim($_data['local_part']));
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
$username = $local_part . '@' . $domain;
if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'mailbox_invalid'
);
return false;
}
if (empty($_data['local_part'])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'mailbox_invalid'
);
return false;
}
$password = $_data['password'];
$password2 = $_data['password2'];
$name = ltrim(rtrim($_data['name'], '>'), '<');
$quota_m = intval($_data['quota']);
if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'unlimited_quota_acl'
);
return false;
}
if (empty($name)) {
$name = $local_part;
}
$active = intval($_data['active']);
$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
$tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
$tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
$sogo_access = (isset($_data['sogo_access'])) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
$imap_access = (isset($_data['imap_access'])) ? intval($_data['imap_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
$pop3_access = (isset($_data['pop3_access'])) ? intval($_data['pop3_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
$smtp_access = (isset($_data['smtp_access'])) ? intval($_data['smtp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
$xmpp_access = (isset($_data['xmpp_access'])) ? intval($_data['xmpp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['xmpp_access']);
$xmpp_admin = (isset($_data['xmpp_admin'])) ? intval($_data['xmpp_admin']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['xmpp_admin']);
$quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
$quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
$quota_b = ($quota_m * 1048576);
$mailbox_attrs = json_encode(
array(
'force_pw_update' => strval($force_pw_update),
'tls_enforce_in' => strval($tls_enforce_in),
'tls_enforce_out' => strval($tls_enforce_out),
'sogo_access' => strval($sogo_access),
'imap_access' => strval($imap_access),
'pop3_access' => strval($pop3_access),
'smtp_access' => strval($smtp_access),
'xmpp_access' => strval($xmpp_access),
'xmpp_admin' => strval($xmpp_admin),
'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']),
'quarantine_notification' => strval($quarantine_notification),
'quarantine_category' => strval($quarantine_category)
)
);
if (!is_valid_domain_name($domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_invalid'
);
return false;
}
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
$stmt = $pdo->prepare("SELECT `mailboxes`, `maxquota`, `quota` FROM `domain`
WHERE `domain` = :domain");
$stmt->execute(array(':domain' => $domain));
$DomainData = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT
COUNT(*) as count,
COALESCE(ROUND(SUM(`quota`)/1048576), 0) as `quota`
FROM `mailbox`
WHERE `kind` NOT REGEXP 'location|thing|group'
AND `domain` = :domain");
$stmt->execute(array(':domain' => $domain));
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT `local_part` FROM `mailbox` WHERE `local_part` = :local_part and `domain`= :domain");
$stmt->execute(array(':local_part' => $local_part, ':domain' => $domain));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('object_exists', htmlspecialchars($username))
);
return false;
}
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE address= :username");
$stmt->execute(array(':username' => $username));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('is_alias', htmlspecialchars($username))
);
return false;
}
$stmt = $pdo->prepare("SELECT `address` FROM `spamalias` WHERE `address`= :username");
$stmt->execute(array(':username' => $username));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('is_spam_alias', htmlspecialchars($username))
);
return false;
}
$stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain`= :domain");
$stmt->execute(array(':domain' => $domain));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results == 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('domain_not_found', htmlspecialchars($domain))
);
return false;
}
if (!empty($password) && !empty($password2)) {
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_complexity'
);
return false;
}
if ($password != $password2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_mismatch'
);
return false;
}
- // support pre hashed passwords
- if (preg_match('/^{(ARGON2I|ARGON2ID|BLF-CRYPT|CLEAR|CLEARTEXT|CRYPT|DES-CRYPT|LDAP-MD5|MD5|MD5-CRYPT|PBKDF2|PLAIN|PLAIN-MD4|PLAIN-MD5|PLAIN-TRUNC|PLAIN-TRUNC|SHA|SHA1|SHA256|SHA256-CRYPT|SHA512|SHA512-CRYPT|SMD5|SSHA|SSHA256|SSHA512)}/i', $password)) {
- $password_hashed = $password;
- }
- else {
- $password_hashed = hash_password($password);
- }
+ $password_hashed = hash_password($password);
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_empty'
);
return false;
}
if ($MailboxData['count'] >= $DomainData['mailboxes']) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('max_mailbox_exceeded', $MailboxData['count'], $DomainData['mailboxes'])
);
return false;
}
if ($quota_m > $DomainData['maxquota']) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_quota_exceeded', $DomainData['maxquota'])
);
return false;
}
if (($MailboxData['quota'] + $quota_m) > $DomainData['quota']) {
$quota_left_m = ($DomainData['quota'] - $MailboxData['quota']);
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_quota_left_exceeded', $quota_left_m)
);
return false;
}
$stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `quota`, `local_part`, `domain`, `attributes`, `active`)
VALUES (:username, :password_hashed, :name, :quota_b, :local_part, :domain, :mailbox_attrs, :active)");
$stmt->execute(array(
':username' => $username,
':password_hashed' => $password_hashed,
':name' => $name,
':quota_b' => $quota_b,
':local_part' => $local_part,
':domain' => $domain,
':mailbox_attrs' => $mailbox_attrs,
':active' => $active
));
$stmt = $pdo->prepare("INSERT INTO `quota2` (`username`, `bytes`, `messages`)
VALUES (:username, '0', '0') ON DUPLICATE KEY UPDATE `bytes` = '0', `messages` = '0';");
$stmt->execute(array(':username' => $username));
$stmt = $pdo->prepare("INSERT INTO `quota2replica` (`username`, `bytes`, `messages`)
VALUES (:username, '0', '0') ON DUPLICATE KEY UPDATE `bytes` = '0', `messages` = '0';");
$stmt->execute(array(':username' => $username));
$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `active`)
VALUES (:username1, :username2, :domain, :active)");
$stmt->execute(array(
':username1' => $username,
':username2' => $username,
':domain' => $domain,
':active' => $active
));
$stmt = $pdo->prepare("INSERT INTO `user_acl` (`username`) VALUES (:username)");
$stmt->execute(array(
':username' => $username
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_added', htmlspecialchars($username))
);
break;
case 'resource':
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
$description = $_data['description'];
$local_part = preg_replace('/[^\da-z]/i', '', preg_quote($description, '/'));
$name = $local_part . '@' . $domain;
$kind = $_data['kind'];
$multiple_bookings = intval($_data['multiple_bookings']);
$active = intval($_data['active']);
if (!filter_var($name, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'resource_invalid'
);
return false;
}
if (empty($description)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'description_invalid'
);
return false;
}
if (!isset($multiple_bookings) || $multiple_bookings < -1) {
$multiple_bookings = -1;
}
if ($kind != 'location' && $kind != 'group' && $kind != 'thing') {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'resource_invalid'
);
return false;
}
if (!is_valid_domain_name($domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_invalid'
);
return false;
}
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :name");
$stmt->execute(array(':name' => $name));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('object_exists', htmlspecialchars($name))
);
return false;
}
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE address= :name");
$stmt->execute(array(':name' => $name));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('is_alias', htmlspecialchars($name))
);
return false;
}
$stmt = $pdo->prepare("SELECT `address` FROM `spamalias` WHERE `address`= :name");
$stmt->execute(array(':name' => $name));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('is_spam_alias', htmlspecialchars($name))
);
return false;
}
$stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain`= :domain");
$stmt->execute(array(':domain' => $domain));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results == 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('domain_not_found', htmlspecialchars($domain))
);
return false;
}
$stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `quota`, `local_part`, `domain`, `active`, `multiple_bookings`, `kind`)
VALUES (:name, 'RESOURCE', :description, 0, :local_part, :domain, :active, :multiple_bookings, :kind)");
$stmt->execute(array(
':name' => $name,
':description' => $description,
':local_part' => $local_part,
':domain' => $domain,
':active' => $active,
':kind' => $kind,
':multiple_bookings' => $multiple_bookings
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('resource_added', htmlspecialchars($name))
);
break;
}
break;
case 'edit':
switch ($_type) {
case 'alias_domain':
$alias_domains = (array)$_data['alias_domain'];
foreach ($alias_domains as $alias_domain) {
$alias_domain = idn_to_ascii(strtolower(trim($alias_domain)), 0, INTL_IDNA_VARIANT_UTS46);
$is_now = mailbox('get', 'alias_domain_details', $alias_domain);
if (!empty($is_now)) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$target_domain = (!empty($_data['target_domain'])) ? idn_to_ascii(strtolower(trim($_data['target_domain'])), 0, INTL_IDNA_VARIANT_UTS46) : $is_now['target_domain'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('alias_domain_invalid', htmlspecialchars($alias_domain))
);
continue;
}
if (!is_valid_domain_name($target_domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('target_domain_invalid', htmlspecialchars($target_domain))
);
continue;
}
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $target_domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (empty(mailbox('get', 'domain_details', $target_domain)) || !empty(mailbox('get', 'alias_domain_details', $target_domain))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('target_domain_invalid', htmlspecialchars($target_domain))
);
continue;
}
$stmt = $pdo->prepare("UPDATE `alias_domain` SET
`target_domain` = :target_domain,
`active` = :active
WHERE `alias_domain` = :alias_domain");
$stmt->execute(array(
':alias_domain' => $alias_domain,
':target_domain' => $target_domain,
':active' => $active
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('aliasd_modified', htmlspecialchars($alias_domain))
);
}
break;
case 'tls_policy':
if (!is_array($_data['username'])) {
$usernames = array();
$usernames[] = $_data['username'];
}
else {
$usernames = $_data['username'];
}
if (!isset($_SESSION['acl']['tls_policy']) || $_SESSION['acl']['tls_policy'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($usernames as $username) {
if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$is_now = mailbox('get', 'tls_policy', $username);
if (!empty($is_now)) {
$tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in'];
$tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : $is_now['tls_enforce_out'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("UPDATE `mailbox`
SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', :tls_out),
`attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', :tls_in)
WHERE `username` = :username");
$stmt->execute(array(
':tls_out' => intval($tls_enforce_out),
':tls_in' => intval($tls_enforce_in),
':username' => $username
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
}
break;
case 'quarantine_notification':
if (!is_array($_data['username'])) {
$usernames = array();
$usernames[] = $_data['username'];
}
else {
$usernames = $_data['username'];
}
if (!isset($_SESSION['acl']['quarantine_notification']) || $_SESSION['acl']['quarantine_notification'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($usernames as $username) {
if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$is_now = mailbox('get', 'quarantine_notification', $username);
if (!empty($is_now)) {
$quarantine_notification = (isset($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (!in_array($quarantine_notification, array('never', 'hourly', 'daily', 'weekly'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("UPDATE `mailbox`
SET `attributes` = JSON_SET(`attributes`, '$.quarantine_notification', :quarantine_notification)
WHERE `username` = :username");
$stmt->execute(array(
':quarantine_notification' => $quarantine_notification,
':username' => $username
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
}
break;
case 'quarantine_category':
if (!is_array($_data['username'])) {
$usernames = array();
$usernames[] = $_data['username'];
}
else {
$usernames = $_data['username'];
}
if (!isset($_SESSION['acl']['quarantine_category']) || $_SESSION['acl']['quarantine_category'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($usernames as $username) {
if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$is_now = mailbox('get', 'quarantine_category', $username);
if (!empty($is_now)) {
$quarantine_category = (isset($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (!in_array($quarantine_category, array('add_header', 'reject', 'all'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("UPDATE `mailbox`
SET `attributes` = JSON_SET(`attributes`, '$.quarantine_category', :quarantine_category)
WHERE `username` = :username");
$stmt->execute(array(
':quarantine_category' => $quarantine_category,
':username' => $username
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
}
break;
case 'spam_score':
if (!is_array($_data['username'])) {
$usernames = array();
$usernames[] = $_data['username'];
}
else {
$usernames = $_data['username'];
}
if (!isset($_SESSION['acl']['spam_score']) || $_SESSION['acl']['spam_score'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($usernames as $username) {
if ($_data['spam_score'] == "default") {
$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username
AND (`option` = 'lowspamlevel' OR `option` = 'highspamlevel')");
$stmt->execute(array(
':username' => $username
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
continue;
}
$lowspamlevel = explode(',', $_data['spam_score'])[0];
$highspamlevel = explode(',', $_data['spam_score'])[1];
if (!is_numeric($lowspamlevel) || !is_numeric($highspamlevel)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'Invalid spam score, format must be "1,2" where first is low and second is high spam value.'
);
continue;
}
$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username
AND (`option` = 'lowspamlevel' OR `option` = 'highspamlevel')");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("INSERT INTO `filterconf` (`object`, `option`, `value`)
VALUES (:username, 'highspamlevel', :highspamlevel)");
$stmt->execute(array(
':username' => $username,
':highspamlevel' => $highspamlevel
));
$stmt = $pdo->prepare("INSERT INTO `filterconf` (`object`, `option`, `value`)
VALUES (:username, 'lowspamlevel', :lowspamlevel)");
$stmt->execute(array(
':username' => $username,
':lowspamlevel' => $lowspamlevel
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
}
break;
case 'time_limited_alias':
if (!isset($_SESSION['acl']['spam_alias']) || $_SESSION['acl']['spam_alias'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
if (!is_array($_data['address'])) {
$addresses = array();
$addresses[] = $_data['address'];
}
else {
$addresses = $_data['address'];
}
foreach ($addresses as $address) {
$stmt = $pdo->prepare("SELECT `goto` FROM `spamalias` WHERE `address` = :address");
$stmt->execute(array(':address' => $address));
$goto = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $goto)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (empty($_data['validity'])) {
continue;
}
$validity = round((int)time() + ($_data['validity'] * 3600));
$stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity WHERE
`address` = :address");
$stmt->execute(array(
':address' => $address,
':validity' => $validity
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', htmlspecialchars(implode(', ', $usernames)))
);
}
break;
case 'delimiter_action':
if (!is_array($_data['username'])) {
$usernames = array();
$usernames[] = $_data['username'];
}
else {
$usernames = $_data['username'];
}
if (!isset($_SESSION['acl']['delimiter_action']) || $_SESSION['acl']['delimiter_action'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($usernames as $username) {
if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (isset($_data['tagged_mail_handler']) && $_data['tagged_mail_handler'] == "subject") {
try {
$redis->hSet('RCPT_WANTS_SUBJECT_TAG', $username, 1);
$redis->hDel('RCPT_WANTS_SUBFOLDER_TAG', $username);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e)
);
continue;
}
}
else if (isset($_data['tagged_mail_handler']) && $_data['tagged_mail_handler'] == "subfolder") {
try {
$redis->hSet('RCPT_WANTS_SUBFOLDER_TAG', $username, 1);
$redis->hDel('RCPT_WANTS_SUBJECT_TAG', $username);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e)
);
continue;
}
}
else {
try {
$redis->hDel('RCPT_WANTS_SUBJECT_TAG', $username);
$redis->hDel('RCPT_WANTS_SUBFOLDER_TAG', $username);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e)
);
continue;
}
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
}
break;
case 'syncjob':
if (!is_array($_data['id'])) {
$ids = array();
$ids[] = $_data['id'];
}
else {
$ids = $_data['id'];
}
if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($ids as $id) {
$is_now = mailbox('get', 'syncjob_details', $id, array('with_password'));
if (!empty($is_now)) {
$username = $is_now['user2'];
$user1 = (!empty($_data['user1'])) ? $_data['user1'] : $is_now['user1'];
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$last_run = (isset($_data['last_run'])) ? NULL : $is_now['last_run'];
$delete2duplicates = (isset($_data['delete2duplicates'])) ? intval($_data['delete2duplicates']) : $is_now['delete2duplicates'];
$subscribeall = (isset($_data['subscribeall'])) ? intval($_data['subscribeall']) : $is_now['subscribeall'];
$delete1 = (isset($_data['delete1'])) ? intval($_data['delete1']) : $is_now['delete1'];
$delete2 = (isset($_data['delete2'])) ? intval($_data['delete2']) : $is_now['delete2'];
$automap = (isset($_data['automap'])) ? intval($_data['automap']) : $is_now['automap'];
$skipcrossduplicates = (isset($_data['skipcrossduplicates'])) ? intval($_data['skipcrossduplicates']) : $is_now['skipcrossduplicates'];
$port1 = (!empty($_data['port1'])) ? $_data['port1'] : $is_now['port1'];
$password1 = (!empty($_data['password1'])) ? $_data['password1'] : $is_now['password1'];
$host1 = (!empty($_data['host1'])) ? $_data['host1'] : $is_now['host1'];
$subfolder2 = (isset($_data['subfolder2'])) ? $_data['subfolder2'] : $is_now['subfolder2'];
$enc1 = (!empty($_data['enc1'])) ? $_data['enc1'] : $is_now['enc1'];
$mins_interval = (!empty($_data['mins_interval'])) ? $_data['mins_interval'] : $is_now['mins_interval'];
$exclude = (isset($_data['exclude'])) ? $_data['exclude'] : $is_now['exclude'];
$custom_params = (isset($_data['custom_params'])) ? $_data['custom_params'] : $is_now['custom_params'];
$maxage = (isset($_data['maxage']) && $_data['maxage'] != "") ? intval($_data['maxage']) : $is_now['maxage'];
$maxbytespersecond = (isset($_data['maxbytespersecond']) && $_data['maxbytespersecond'] != "") ? intval($_data['maxbytespersecond']) : $is_now['maxbytespersecond'];
$timeout1 = (isset($_data['timeout1']) && $_data['timeout1'] != "") ? intval($_data['timeout1']) : $is_now['timeout1'];
$timeout2 = (isset($_data['timeout2']) && $_data['timeout2'] != "") ? intval($_data['timeout2']) : $is_now['timeout2'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (strpos($custom_params, 'pipemess')) {
$custom_params = '';
}
if (empty($subfolder2)) {
$subfolder2 = "";
}
if (!isset($maxage) || !filter_var($maxage, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
$maxage = "0";
}
if (!isset($timeout1) || !filter_var($timeout1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
$timeout1 = "600";
}
if (!isset($timeout2) || !filter_var($timeout2, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
$timeout2 = "600";
}
if (!isset($maxbytespersecond) || !filter_var($maxbytespersecond, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 125000000)))) {
$maxbytespersecond = "0";
}
if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (!filter_var($mins_interval, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 43800)))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (!is_valid_domain_name($host1)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if ($enc1 != "TLS" && $enc1 != "SSL" && $enc1 != "PLAIN") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (@preg_match("/" . $exclude . "/", null) === false) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("UPDATE `imapsync` SET `delete1` = :delete1,
`delete2` = :delete2,
`automap` = :automap,
`skipcrossduplicates` = :skipcrossduplicates,
`maxage` = :maxage,
`maxbytespersecond` = :maxbytespersecond,
`subfolder2` = :subfolder2,
`exclude` = :exclude,
`host1` = :host1,
`last_run` = :last_run,
`user1` = :user1,
`password1` = :password1,
`mins_interval` = :mins_interval,
`port1` = :port1,
`enc1` = :enc1,
`delete2duplicates` = :delete2duplicates,
`custom_params` = :custom_params,
`timeout1` = :timeout1,
`timeout2` = :timeout2,
`subscribeall` = :subscribeall,
`active` = :active
WHERE `id` = :id");
$stmt->execute(array(
':delete1' => $delete1,
':delete2' => $delete2,
':automap' => $automap,
':skipcrossduplicates' => $skipcrossduplicates,
':id' => $id,
':exclude' => $exclude,
':maxage' => $maxage,
':maxbytespersecond' => $maxbytespersecond,
':subfolder2' => $subfolder2,
':host1' => $host1,
':user1' => $user1,
':password1' => $password1,
':last_run' => $last_run,
':mins_interval' => $mins_interval,
':port1' => $port1,
':enc1' => $enc1,
':delete2duplicates' => $delete2duplicates,
':custom_params' => $custom_params,
':timeout1' => $timeout1,
':timeout2' => $timeout2,
':subscribeall' => $subscribeall,
':active' => $active,
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
}
break;
case 'filter':
if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
$sieve = new Sieve\SieveParser();
if (!is_array($_data['id'])) {
$ids = array();
$ids[] = $_data['id'];
}
else {
$ids = $_data['id'];
}
foreach ($ids as $id) {
$is_now = mailbox('get', 'filter_details', $id);
if (!empty($is_now)) {
$username = $is_now['username'];
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$script_desc = (!empty($_data['script_desc'])) ? $_data['script_desc'] : $is_now['script_desc'];
$script_data = (!empty($_data['script_data'])) ? $_data['script_data'] : $is_now['script_data'];
$filter_type = (!empty($_data['filter_type'])) ? $_data['filter_type'] : $is_now['filter_type'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
try {
$sieve->parse($script_data);
}
catch (Exception $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('sieve_error', $e->getMessage())
);
continue;
}
if ($filter_type != 'postfilter' && $filter_type != 'prefilter') {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'filter_type'
);
continue;
}
if ($active == '1') {
$script_name = 'active';
$stmt = $pdo->prepare("UPDATE `sieve_filters`
SET `script_name` = 'inactive'
WHERE `username` = :username
AND `filter_type` = :filter_type");
$stmt->execute(array(
':username' => $username,
':filter_type' => $filter_type
));
}
else {
$script_name = 'inactive';
}
$stmt = $pdo->prepare("UPDATE `sieve_filters` SET `script_desc` = :script_desc, `script_data` = :script_data, `script_name` = :script_name, `filter_type` = :filter_type
WHERE `id` = :id");
$stmt->execute(array(
':script_desc' => $script_desc,
':script_data' => $script_data,
':script_name' => $script_name,
':filter_type' => $filter_type,
':id' => $id
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
}
break;
case 'alias':
if (!is_array($_data['id'])) {
$ids = array();
$ids[] = $_data['id'];
}
else {
$ids = $_data['id'];
}
foreach ($ids as $id) {
$is_now = mailbox('get', 'alias_details', $id);
if (!empty($is_now)) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$sogo_visible = (isset($_data['sogo_visible'])) ? intval($_data['sogo_visible']) : $is_now['sogo_visible'];
$goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0;
$goto_spam = (isset($_data['goto_spam'])) ? intval($_data['goto_spam']) : 0;
$goto_ham = (isset($_data['goto_ham'])) ? intval($_data['goto_ham']) : 0;
$public_comment = (isset($_data['public_comment'])) ? $_data['public_comment'] : $is_now['public_comment'];
$private_comment = (isset($_data['private_comment'])) ? $_data['private_comment'] : $is_now['private_comment'];
$goto = (!empty($_data['goto'])) ? $_data['goto'] : $is_now['goto'];
$address = (!empty($_data['address'])) ? $_data['address'] : $is_now['address'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('alias_invalid', $address)
);
continue;
}
$domain = idn_to_ascii(substr(strstr($address, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
if ($is_now['address'] != $address) {
$local_part = strstr($address, '@', true);
$address = $local_part.'@'.$domain;
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('alias_invalid', $address)
);
continue;
}
if (strtolower($is_now['address']) != strtolower($address)) {
$stmt = $pdo->prepare("SELECT `address` FROM `alias`
WHERE `address`= :address OR `address` IN (
SELECT `username` FROM `mailbox`, `alias_domain`
WHERE (
`alias_domain`.`alias_domain` = :address_d
AND `mailbox`.`username` = CONCAT(:address_l, '@', alias_domain.target_domain)))");
$stmt->execute(array(
':address' => $address,
':address_l' => $local_part,
':address_d' => $domain
));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('is_alias_or_mailbox', htmlspecialchars($address))
);
continue;
}
}
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)");
$stmt->execute(array(':domain1' => $domain, ':domain2' => $domain));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results == 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('domain_not_found', htmlspecialchars($domain))
);
continue;
}
$stmt = $pdo->prepare("SELECT `address` FROM `spamalias`
WHERE `address`= :address");
$stmt->execute(array(':address' => $address));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('is_spam_alias', htmlspecialchars($address))
);
continue;
}
}
if ($goto_null == "1") {
$goto = "null@localhost";
}
elseif ($goto_spam == "1") {
$goto = "spam@localhost";
}
elseif ($goto_ham == "1") {
$goto = "ham@localhost";
}
else {
$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $goto));
foreach ($gotos as $i => &$goto) {
if (empty($goto)) {
continue;
}
if (!filter_var($goto, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('goto_invalid', $goto)
);
unset($gotos[$i]);
continue;
}
if ($goto == $address) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'alias_goto_identical'
);
unset($gotos[$i]);
continue;
}
// Delete from sender_acl to prevent duplicates
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE
`logged_in_as` = :goto AND
`send_as` = :address");
$stmt->execute(array(
':goto' => $goto,
':address' => $address
));
}
$gotos = array_filter($gotos);
$goto = implode(",", $gotos);
}
if (!empty($goto)) {
$stmt = $pdo->prepare("UPDATE `alias` SET
`address` = :address,
`public_comment` = :public_comment,
`private_comment` = :private_comment,
`domain` = :domain,
`goto` = :goto,
`sogo_visible`= :sogo_visible,
`active`= :active
WHERE `id` = :id");
$stmt->execute(array(
':address' => $address,
':public_comment' => $public_comment,
':private_comment' => $private_comment,
':domain' => $domain,
':goto' => $goto,
':sogo_visible' => $sogo_visible,
':active' => $active,
':id' => $is_now['id']
));
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('alias_modified', htmlspecialchars($address))
);
}
break;
case 'domain':
if (!is_array($_data['domain'])) {
$domains = array();
$domains[] = $_data['domain'];
}
else {
$domains = $_data['domain'];
}
foreach ($domains as $domain) {
$domain = idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46);
if (!is_valid_domain_name($domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_invalid'
);
continue;
}
if ($_SESSION['mailcow_cc_role'] == "domainadmin" &&
hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$is_now = mailbox('get', 'domain_details', $domain);
if (!empty($is_now)) {
$gal = (isset($_data['gal'])) ? intval($_data['gal']) : $is_now['gal'];
$xmpp = (isset($_data['xmpp']) && !empty($_SESSION['acl']['xmpp_domain_access']) && $_SESSION['acl']['xmpp_domain_access'] == "1") ? intval($_data['xmpp']) : $is_now['xmpp'];
$xmpp_prefix = (!empty($_data['xmpp_prefix']) && !empty($_SESSION['acl']['xmpp_prefix']) && $_SESSION['acl']['xmpp_prefix'] == "1") ? $_data['xmpp_prefix'] : $is_now['xmpp_prefix'];
$description = (!empty($_data['description']) && isset($_SESSION['acl']['domain_desc']) && $_SESSION['acl']['domain_desc'] == "1") ? $_data['description'] : $is_now['description'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_invalid'
);
continue;
}
$xmpp_prefix = preg_replace('/[^\da-z-]/i', '', $xmpp_prefix);
$stmt = $pdo->prepare("UPDATE `domain` SET
`description` = :description,
`gal` = :gal,
`xmpp` = :xmpp,
`xmpp_prefix` = :xmpp_prefix
WHERE `domain` = :domain");
$stmt->execute(array(
':description' => $description,
':gal' => $gal,
':xmpp' => $xmpp,
':xmpp_prefix' => $xmpp_prefix,
':domain' => $domain
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('domain_modified', htmlspecialchars($domain))
);
}
elseif ($_SESSION['mailcow_cc_role'] == "admin") {
$is_now = mailbox('get', 'domain_details', $domain);
if (!empty($is_now)) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$backupmx = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : $is_now['backupmx'];
$gal = (isset($_data['gal'])) ? intval($_data['gal']) : $is_now['gal'];
$xmpp = (isset($_data['xmpp'])) ? intval($_data['xmpp']) : $is_now['xmpp'];
$relay_all_recipients = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : $is_now['relay_all_recipients'];
$relay_unknown_only = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : $is_now['relay_unknown_only'];
$relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : $is_now['relayhost'];
$aliases = (!empty($_data['aliases'])) ? $_data['aliases'] : $is_now['max_num_aliases_for_domain'];
$mailboxes = (isset($_data['mailboxes']) && $_data['mailboxes'] != '') ? intval($_data['mailboxes']) : $is_now['max_num_mboxes_for_domain'];
$defquota = (isset($_data['defquota']) && $_data['defquota'] != '') ? intval($_data['defquota']) : ($is_now['def_quota_for_mbox'] / 1048576);
$maxquota = (!empty($_data['maxquota'])) ? $_data['maxquota'] : ($is_now['max_quota_for_mbox'] / 1048576);
$quota = (!empty($_data['quota'])) ? $_data['quota'] : ($is_now['max_quota_for_domain'] / 1048576);
$description = (!empty($_data['description'])) ? $_data['description'] : $is_now['description'];
$xmpp_prefix = (!empty($_data['xmpp_prefix'])) ? $_data['xmpp_prefix'] : $is_now['xmpp_prefix'];
if ($relay_all_recipients == '1') {
$backupmx = '1';
}
if ($relay_unknown_only == '1') {
$backupmx = '1';
$relay_all_recipients = '1';
}
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_invalid'
);
continue;
}
$xmpp_prefix = preg_replace('/[^\da-z-]/i', '', $xmpp_prefix);
// todo: should be using api here
$stmt = $pdo->prepare("SELECT
COUNT(*) AS count,
MAX(COALESCE(ROUND(`quota`/1048576), 0)) AS `biggest_mailbox`,
COALESCE(ROUND(SUM(`quota`)/1048576), 0) AS `quota_all`
FROM `mailbox`
WHERE `kind` NOT REGEXP 'location|thing|group'
AND domain = :domain");
$stmt->execute(array(':domain' => $domain));
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
// todo: should be using api here
$stmt = $pdo->prepare("SELECT COUNT(*) AS `count` FROM `alias`
WHERE domain = :domain
AND address NOT IN (
SELECT `username` FROM `mailbox`
)");
$stmt->execute(array(':domain' => $domain));
$AliasData = $stmt->fetch(PDO::FETCH_ASSOC);
if ($defquota > $maxquota) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'mailbox_defquota_exceeds_mailbox_maxquota'
);
continue;
}
if ($defquota == "0" || empty($defquota)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'defquota_empty'
);
continue;
}
if ($maxquota > $quota) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'mailbox_quota_exceeds_domain_quota'
);
continue;
}
if ($maxquota == "0" || empty($maxquota)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'maxquota_empty'
);
continue;
}
if ($MailboxData['biggest_mailbox'] > $maxquota) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('max_quota_in_use', $MailboxData['biggest_mailbox'])
);
continue;
}
if ($MailboxData['quota_all'] > $quota) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('domain_quota_m_in_use', $MailboxData['quota_all'])
);
continue;
}
if ($MailboxData['count'] > $mailboxes) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailboxes_in_use', $MailboxData['count'])
);
continue;
}
if ($AliasData['count'] > $aliases) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('aliases_in_use', $AliasData['count'])
);
continue;
}
$stmt = $pdo->prepare("UPDATE `domain` SET
`relay_all_recipients` = :relay_all_recipients,
`relay_unknown_only` = :relay_unknown_only,
`backupmx` = :backupmx,
`gal` = :gal,
`xmpp` = :xmpp,
`xmpp_prefix` = :xmpp_prefix,
`active` = :active,
`quota` = :quota,
`defquota` = :defquota,
`maxquota` = :maxquota,
`relayhost` = :relayhost,
`mailboxes` = :mailboxes,
`aliases` = :aliases,
`description` = :description
WHERE `domain` = :domain");
$stmt->execute(array(
':relay_all_recipients' => $relay_all_recipients,
':relay_unknown_only' => $relay_unknown_only,
':backupmx' => $backupmx,
':gal' => $gal,
':xmpp' => $xmpp,
':xmpp_prefix' => $xmpp_prefix,
':active' => $active,
':quota' => $quota,
':defquota' => $defquota,
':maxquota' => $maxquota,
':relayhost' => $relayhost,
':mailboxes' => $mailboxes,
':aliases' => $aliases,
':description' => $description,
':domain' => $domain
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('domain_modified', htmlspecialchars($domain))
);
}
}
break;
case 'mailbox':
if (!is_array($_data['username'])) {
$usernames = array();
$usernames[] = $_data['username'];
}
else {
$usernames = $_data['username'];
}
foreach ($usernames as $username) {
if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('username_invalid', $username)
);
continue;
}
$is_now = mailbox('get', 'mailbox_details', $username);
if (isset($_data['protocol_access'])) {
$_data['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
$_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
}
if (!empty($is_now)) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
(int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
(int)$sogo_access = (isset($_data['sogo_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
(int)$imap_access = (isset($_data['imap_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
(int)$pop3_access = (isset($_data['pop3_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
(int)$smtp_access = (isset($_data['smtp_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
(int)$xmpp_admin = (isset($_data['xmpp_admin']) && isset($_SESSION['acl']['xmpp_admin']) && $_SESSION['acl']['xmpp_admin'] == "1") ? intval($_data['xmpp_admin']) : intval($is_now['attributes']['xmpp_admin']);
(int)$xmpp_access = (isset($_data['xmpp_access']) && isset($_SESSION['acl']['xmpp_mailbox_access']) && $_SESSION['acl']['xmpp_mailbox_access'] == "1") ? intval($_data['xmpp_access']) : intval($is_now['attributes']['xmpp_access']);
(int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
$name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
$domain = $is_now['domain'];
$quota_b = $quota_m * 1048576;
$password = (!empty($_data['password'])) ? $_data['password'] : null;
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
// if already 0 == ok
if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && ($quota_m == 0 && $is_now['quota'] != 0)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'unlimited_quota_acl'
);
return false;
}
$stmt = $pdo->prepare("SELECT `quota`, `maxquota`
FROM `domain`
WHERE `domain` = :domain");
$stmt->execute(array(':domain' => $domain));
$DomainData = $stmt->fetch(PDO::FETCH_ASSOC);
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if ((($is_now['quota_used'] / 1048576) + $quota_m) > $DomainData['quota']) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_quota_left_exceeded', ($is_now['max_new_quota'] / 1048576))
);
continue;
}
if ($quota_m > $DomainData['maxquota']) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_quota_exceeded', $DomainData['maxquota'])
);
continue;
}
$extra_acls = array();
if (isset($_data['extended_sender_acl'])) {
if (!isset($_SESSION['acl']['extend_sender_acl']) || $_SESSION['acl']['extend_sender_acl'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
$extra_acls = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['extended_sender_acl']));
foreach ($extra_acls as $i => &$extra_acl) {
if (empty($extra_acl)) {
continue;
}
if (substr($extra_acl, 0, 1) === "@") {
$extra_acl = ltrim($extra_acl, '@');
}
if (!filter_var($extra_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name($extra_acl)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('extra_acl_invalid', htmlspecialchars($extra_acl))
);
unset($extra_acls[$i]);
continue;
}
$domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
if (filter_var($extra_acl, FILTER_VALIDATE_EMAIL)) {
$extra_acl_domain = idn_to_ascii(substr(strstr($extra_acl, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
if (in_array($extra_acl_domain, $domains)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
);
unset($extra_acls[$i]);
continue;
}
}
else {
if (in_array($extra_acl, $domains)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
);
unset($extra_acls[$i]);
continue;
}
$extra_acl = '@' . $extra_acl;
}
}
$extra_acls = array_filter($extra_acls);
$extra_acls = array_values($extra_acls);
$extra_acls = array_unique($extra_acls);
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `logged_in_as` = :username");
$stmt->execute(array(
':username' => $username
));
foreach ($extra_acls as $sender_acl_external) {
$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`, `external`)
VALUES (:sender_acl, :username, 1)");
$stmt->execute(array(
':sender_acl' => $sender_acl_external,
':username' => $username
));
}
}
if (isset($_data['sender_acl'])) {
// Get sender_acl items set by admin
$sender_acl_admin = array_merge(
mailbox('get', 'sender_acl_handles', $username)['sender_acl_domains']['ro'],
mailbox('get', 'sender_acl_handles', $username)['sender_acl_addresses']['ro']
);
// Get sender_acl items from POST array
// Set sender_acl_domain_admin to empty array if sender_acl contains "default" to trigger a reset
// Delete records from sender_acl if sender_acl contains "*" and set to array("*")
$_data['sender_acl'] = (array)$_data['sender_acl'];
if (in_array("*", $_data['sender_acl'])) {
$sender_acl_domain_admin = array('*');
}
elseif (array("default") === $_data['sender_acl']) {
$sender_acl_domain_admin = array();
}
else {
if (array_search('default', $_data['sender_acl']) !== false){
unset($_data['sender_acl'][array_search('default', $_data['sender_acl'])]);
}
$sender_acl_domain_admin = $_data['sender_acl'];
}
if (!empty($sender_acl_domain_admin) || !empty($sender_acl_admin)) {
// Check items in POST array and skip invalid
foreach ($sender_acl_domain_admin as $key => $val) {
// Check for invalid domain or email format or not *
if (!filter_var($val, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name(ltrim($val, '@')) && $val != '*') {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('sender_acl_invalid', $sender_acl_domain_admin[$key])
);
unset($sender_acl_domain_admin[$key]);
continue;
}
// Check if user has domain access (if object is domain)
$domain = ltrim($sender_acl_domain_admin[$key], '@');
if (is_valid_domain_name($domain)) {
// Check for- and skip non-mailcow domains
$domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
if (!empty($domains)) {
if (!in_array($domain, $domains)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('sender_acl_invalid', $sender_acl_domain_admin[$key])
);
unset($sender_acl_domain_admin[$key]);
continue;
}
}
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('sender_acl_invalid', $sender_acl_domain_admin[$key])
);
unset($sender_acl_domain_admin[$key]);
continue;
}
}
// Wildcard can only be used if role == admin
if ($val == '*' && $_SESSION['mailcow_cc_role'] != 'admin') {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('sender_acl_invalid', $sender_acl_domain_admin[$key])
);
unset($sender_acl_domain_admin[$key]);
continue;
}
// Check if user has alias access (if object is email)
if (filter_var($val, FILTER_VALIDATE_EMAIL)) {
if (!hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $val)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('sender_acl_invalid', $sender_acl_domain_admin[$key])
);
unset($sender_acl_domain_admin[$key]);
continue;
}
}
}
// Merge both arrays
$sender_acl_merged = array_merge($sender_acl_domain_admin, $sender_acl_admin);
// If merged array still contains "*", set it as only value
!in_array('*', $sender_acl_merged) ?: $sender_acl_merged = array('*');
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 0 AND `logged_in_as` = :username");
$stmt->execute(array(
':username' => $username
));
$fixed_sender_aliases = mailbox('get', 'sender_acl_handles', $username)['fixed_sender_aliases'];
foreach ($sender_acl_merged as $sender_acl) {
$domain = ltrim($sender_acl, '@');
if (is_valid_domain_name($domain)) {
$sender_acl = '@' . $domain;
}
// Don't add if allowed by alias
if (in_array($sender_acl, $fixed_sender_aliases)) {
continue;
}
$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`)
VALUES (:sender_acl, :username)");
$stmt->execute(array(
':sender_acl' => $sender_acl,
':username' => $username
));
}
}
else {
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 0 AND `logged_in_as` = :username");
$stmt->execute(array(
':username' => $username
));
}
}
if (!empty($password) && !empty($password2)) {
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_complexity'
);
continue;
}
if ($password != $password2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_mismatch'
);
continue;
}
// support pre hashed passwords
if (preg_match('/^{(ARGON2I|ARGON2ID|BLF-CRYPT|CLEAR|CLEARTEXT|CRYPT|DES-CRYPT|LDAP-MD5|MD5|MD5-CRYPT|PBKDF2|PLAIN|PLAIN-MD4|PLAIN-MD5|PLAIN-TRUNC|PLAIN-TRUNC|SHA|SHA1|SHA256|SHA256-CRYPT|SHA512|SHA512-CRYPT|SMD5|SSHA|SSHA256|SSHA512)}/i', $password)) {
$password_hashed = $password;
}
else {
$password_hashed = hash_password($password);
}
$stmt = $pdo->prepare("UPDATE `mailbox` SET
`password` = :password_hashed
WHERE `username` = :username");
$stmt->execute(array(
':password_hashed' => $password_hashed,
':username' => $username
));
}
// We could either set alias = 1 if alias = 2 or tune the Postfix alias table (that's what we did, TODO: to it the other way)
$stmt = $pdo->prepare("UPDATE `alias` SET
`active` = :active
WHERE `address` = :address");
$stmt->execute(array(
':address' => $username,
':active' => $active
));
$stmt = $pdo->prepare("UPDATE `mailbox` SET
`active` = :active,
`name`= :name,
`quota` = :quota_b,
`attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update),
`attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
`attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
`attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
`attributes` = JSON_SET(`attributes`, '$.xmpp_admin', :xmpp_admin),
`attributes` = JSON_SET(`attributes`, '$.xmpp_access', :xmpp_access),
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access)
WHERE `username` = :username");
$stmt->execute(array(
':active' => $active,
':name' => $name,
':quota_b' => $quota_b,
':force_pw_update' => $force_pw_update,
':sogo_access' => $sogo_access,
':imap_access' => $imap_access,
':pop3_access' => $pop3_access,
':smtp_access' => $smtp_access,
':xmpp_admin' => $xmpp_admin,
':xmpp_access' => $xmpp_access,
':username' => $username
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
}
break;
case 'resource':
if (!is_array($_data['name'])) {
$names = array();
$names[] = $_data['name'];
}
else {
$names = $_data['name'];
}
foreach ($names as $name) {
$is_now = mailbox('get', 'resource_details', $name);
if (!empty($is_now)) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$multiple_bookings = (isset($_data['multiple_bookings'])) ? intval($_data['multiple_bookings']) : $is_now['multiple_bookings'];
$description = (!empty($_data['description'])) ? $_data['description'] : $is_now['description'];
$kind = (!empty($_data['kind'])) ? $_data['kind'] : $is_now['kind'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('resource_invalid', htmlspecialchars($name))
);
continue;
}
if (!filter_var($name, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('resource_invalid', htmlspecialchars($name))
);
continue;
}
if (!isset($multiple_bookings) || $multiple_bookings < -1) {
$multiple_bookings = -1;
}
if (empty($description)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('description_invalid', htmlspecialchars($name))
);
continue;
}
if ($kind != 'location' && $kind != 'group' && $kind != 'thing') {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('resource_invalid', htmlspecialchars($name))
);
continue;
}
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $name)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("UPDATE `mailbox` SET
`active` = :active,
`name`= :description,
`kind`= :kind,
`multiple_bookings`= :multiple_bookings
WHERE `username` = :name");
$stmt->execute(array(
':active' => $active,
':description' => $description,
':multiple_bookings' => $multiple_bookings,
':kind' => $kind,
':name' => $name
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('resource_modified', htmlspecialchars($name))
);
}
break;
}
break;
case 'get':
switch ($_type) {
case 'sender_acl_handles':
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
return false;
}
$data['sender_acl_domains']['ro'] = array();
$data['sender_acl_domains']['rw'] = array();
$data['sender_acl_domains']['selectable'] = array();
$data['sender_acl_addresses']['ro'] = array();
$data['sender_acl_addresses']['rw'] = array();
$data['sender_acl_addresses']['selectable'] = array();
$data['fixed_sender_aliases'] = array();
$data['external_sender_aliases'] = array();
// Fixed addresses
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
$stmt->execute(array(':goto' => '(^|,)'.$_data.'($|,)'));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) {
$data['fixed_sender_aliases'][] = $row['address'];
}
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias_domain_alias` FROM `mailbox`, `alias_domain`
WHERE `alias_domain`.`target_domain` = `mailbox`.`domain`
AND `mailbox`.`username` = :username");
$stmt->execute(array(':username' => $_data));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) {
if (!empty($row['alias_domain_alias'])) {
$data['fixed_sender_aliases'][] = $row['alias_domain_alias'];
}
}
// External addresses
$stmt = $pdo->prepare("SELECT `send_as` as `send_as_external` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `external` = '1'");
$stmt->execute(array(':logged_in_as' => $_data));
$exernal_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($exernal_rows)) {
if (!empty($row['send_as_external'])) {
$data['external_sender_aliases'][] = $row['send_as_external'];
}
}
// Return array $data['sender_acl_domains/addresses']['ro'] with read-only objects
// Return array $data['sender_acl_domains/addresses']['rw'] with read-write objects (can be deleted)
$stmt = $pdo->prepare("SELECT REPLACE(`send_as`, '@', '') AS `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `external` = '0' AND (`send_as` LIKE '@%' OR `send_as` = '*')");
$stmt->execute(array(':logged_in_as' => $_data));
$domain_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($domain_row = array_shift($domain_rows)) {
if (is_valid_domain_name($domain_row['send_as']) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain_row['send_as'])) {
$data['sender_acl_domains']['ro'][] = $domain_row['send_as'];
continue;
}
if (is_valid_domain_name($domain_row['send_as']) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain_row['send_as'])) {
$data['sender_acl_domains']['rw'][] = $domain_row['send_as'];
continue;
}
if ($domain_row['send_as'] == '*' && $_SESSION['mailcow_cc_role'] != 'admin') {
$data['sender_acl_domains']['ro'][] = $domain_row['send_as'];
}
if ($domain_row['send_as'] == '*' && $_SESSION['mailcow_cc_role'] == 'admin') {
$data['sender_acl_domains']['rw'][] = $domain_row['send_as'];
}
}
$stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as AND `external` = '0' AND (`send_as` NOT LIKE '@%' AND `send_as` != '*')");
$stmt->execute(array(':logged_in_as' => $_data));
$address_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($address_row = array_shift($address_rows)) {
if (filter_var($address_row['send_as'], FILTER_VALIDATE_EMAIL) && !hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $address_row['send_as'])) {
$data['sender_acl_addresses']['ro'][] = $address_row['send_as'];
continue;
}
if (filter_var($address_row['send_as'], FILTER_VALIDATE_EMAIL) && hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $address_row['send_as'])) {
$data['sender_acl_addresses']['rw'][] = $address_row['send_as'];
continue;
}
}
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain` NOT IN (
SELECT REPLACE(`send_as`, '@', '') FROM `sender_acl`
WHERE `logged_in_as` = :logged_in_as1
AND `external` = '0'
AND `send_as` LIKE '@%')
UNION
SELECT '*' FROM `domain`
WHERE '*' NOT IN (
SELECT `send_as` FROM `sender_acl`
WHERE `logged_in_as` = :logged_in_as2
AND `external` = '0'
)");
$stmt->execute(array(
':logged_in_as1' => $_data,
':logged_in_as2' => $_data
));
$rows_domain = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row_domain = array_shift($rows_domain)) {
if (is_valid_domain_name($row_domain['domain']) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row_domain['domain'])) {
$data['sender_acl_domains']['selectable'][] = $row_domain['domain'];
continue;
}
if ($row_domain['domain'] == '*' && $_SESSION['mailcow_cc_role'] == 'admin') {
$data['sender_acl_domains']['selectable'][] = $row_domain['domain'];
continue;
}
}
$stmt = $pdo->prepare("SELECT `address` FROM `alias`
WHERE `goto` != :goto
AND `address` NOT IN (
SELECT `send_as` FROM `sender_acl`
WHERE `logged_in_as` = :logged_in_as
AND `external` = '0'
AND `send_as` NOT LIKE '@%')");
$stmt->execute(array(
':logged_in_as' => $_data,
':goto' => $_data
));
$rows_mbox = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows_mbox)) {
// Aliases are not selectable
if (in_array($row['address'], $data['fixed_sender_aliases'])) {
continue;
}
if (filter_var($row['address'], FILTER_VALIDATE_EMAIL) && hasAliasObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['address'])) {
$data['sender_acl_addresses']['selectable'][] = $row['address'];
}
}
return $data;
break;
case 'mailboxes':
$mailboxes = array();
if (isset($_data) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
elseif (isset($_data) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `domain` = :domain");
$stmt->execute(array(
':domain' => $_data,
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$mailboxes[] = $row['username'];
}
}
else {
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND (`domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :username) OR 'admin' = :role)");
$stmt->execute(array(
':username' => $_SESSION['mailcow_cc_username'],
':role' => $_SESSION['mailcow_cc_role'],
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$mailboxes[] = $row['username'];
}
}
return $mailboxes;
break;
case 'tls_policy':
$attrs = array();
if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
}
else {
$_data = $_SESSION['mailcow_cc_username'];
}
$stmt = $pdo->prepare("SELECT `attributes` FROM `mailbox` WHERE `username` = :username");
$stmt->execute(array(':username' => $_data));
$attrs = $stmt->fetch(PDO::FETCH_ASSOC);
$attrs = json_decode($attrs['attributes'], true);
return array(
'tls_enforce_in' => $attrs['tls_enforce_in'],
'tls_enforce_out' => $attrs['tls_enforce_out']
);
break;
case 'quarantine_notification':
$attrs = array();
if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
}
else {
$_data = $_SESSION['mailcow_cc_username'];
}
$stmt = $pdo->prepare("SELECT `attributes` FROM `mailbox` WHERE `username` = :username");
$stmt->execute(array(':username' => $_data));
$attrs = $stmt->fetch(PDO::FETCH_ASSOC);
$attrs = json_decode($attrs['attributes'], true);
return $attrs['quarantine_notification'];
break;
case 'quarantine_category':
$attrs = array();
if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
}
else {
$_data = $_SESSION['mailcow_cc_username'];
}
$stmt = $pdo->prepare("SELECT `attributes` FROM `mailbox` WHERE `username` = :username");
$stmt->execute(array(':username' => $_data));
$attrs = $stmt->fetch(PDO::FETCH_ASSOC);
$attrs = json_decode($attrs['attributes'], true);
return $attrs['quarantine_category'];
break;
case 'filters':
$filters = array();
if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
}
else {
$_data = $_SESSION['mailcow_cc_username'];
}
$stmt = $pdo->prepare("SELECT `id` FROM `sieve_filters` WHERE `username` = :username");
$stmt->execute(array(':username' => $_data));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$filters[] = $row['id'];
}
return $filters;
break;
case 'global_filter_details':
$global_filters = array();
if ($_SESSION['mailcow_cc_role'] != "admin") {
return false;
}
$global_filters['prefilter'] = file_get_contents('/global_sieve/before');
$global_filters['postfilter'] = file_get_contents('/global_sieve/after');
return $global_filters;
break;
case 'filter_details':
$filter_details = array();
if (!is_numeric($_data)) {
return false;
}
$stmt = $pdo->prepare("SELECT CASE `script_name` WHEN 'active' THEN 1 ELSE 0 END AS `active`,
id,
username,
filter_type,
script_data,
script_desc
FROM `sieve_filters`
WHERE `id` = :id");
$stmt->execute(array(':id' => $_data));
$filter_details = $stmt->fetch(PDO::FETCH_ASSOC);
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $filter_details['username'])) {
return false;
}
return $filter_details;
break;
case 'active_user_sieve':
$filter_details = array();
if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
}
else {
$_data = $_SESSION['mailcow_cc_username'];
}
$exec_fields = array(
'cmd' => 'sieve',
'task' => 'list',
'username' => $_data
);
$filters = docker('post', 'dovecot-mailcow', 'exec', $exec_fields);
$filters = array_filter(preg_split("/(\r\n|\n|\r)/",$filters));
foreach ($filters as $filter) {
if (preg_match('/.+ ACTIVE/i', $filter)) {
$exec_fields = array(
'cmd' => 'sieve',
'task' => 'print',
'script_name' => substr($filter, 0, -7),
'username' => $_data
);
$script = docker('post', 'dovecot-mailcow', 'exec', $exec_fields);
// Remove first line
return preg_replace('/^.+\n/', '', $script);
}
}
return false;
break;
case 'syncjob_details':
$syncjobdetails = array();
if (!is_numeric($_data)) {
return false;
}
if (isset($_extra) && in_array('no_log', $_extra)) {
$field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("returned_text", "password1")');
$fields = $field_query->fetchAll(PDO::FETCH_ASSOC);
while($field = array_shift($fields)) {
$shown_fields[] = $field['Field'];
}
$stmt = $pdo->prepare("SELECT " . implode(',', $shown_fields) . ",
`active`
FROM `imapsync` WHERE id = :id");
}
elseif (isset($_extra) && in_array('with_password', $_extra)) {
$stmt = $pdo->prepare("SELECT *,
`active`
FROM `imapsync` WHERE id = :id");
}
else {
$field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("password1")');
$fields = $field_query->fetchAll(PDO::FETCH_ASSOC);
while($field = array_shift($fields)) {
$shown_fields[] = $field['Field'];
}
$stmt = $pdo->prepare("SELECT " . implode(',', $shown_fields) . ",
`active`
FROM `imapsync` WHERE id = :id");
}
$stmt->execute(array(':id' => $_data));
$syncjobdetails = $stmt->fetch(PDO::FETCH_ASSOC);
if (!empty($syncjobdetails['returned_text'])) {
$syncjobdetails['log'] = $syncjobdetails['returned_text'];
}
else {
$syncjobdetails['log'] = '';
}
unset($syncjobdetails['returned_text']);
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $syncjobdetails['user2'])) {
return false;
}
return $syncjobdetails;
break;
case 'syncjobs':
$syncjobdata = array();
if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
}
else {
$_data = $_SESSION['mailcow_cc_username'];
}
$stmt = $pdo->prepare("SELECT `id` FROM `imapsync` WHERE `user2` = :username");
$stmt->execute(array(':username' => $_data));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$syncjobdata[] = $row['id'];
}
return $syncjobdata;
break;
case 'spam_score':
$curl = curl_init();
curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
curl_setopt($curl, CURLOPT_URL,"http://rspamd/actions");
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$default_actions = curl_exec($curl);
if (!curl_errno($curl)) {
$data_array = json_decode($default_actions, true);
curl_close($curl);
foreach ($data_array as $data) {
if ($data['action'] == 'reject') {
$reject = $data['value'];
continue;
}
elseif ($data['action'] == 'add header') {
$add_header = $data['value'];
continue;
}
}
if (empty($add_header) || empty($reject)) {
// Assume default, set warning
$default = "5, 15";
$_SESSION['return'][] = array(
'type' => 'warning',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'Could not determine servers default spam score, assuming default'
);
}
else {
$default = $add_header . ', ' . $reject;
}
}
else {
// Assume default, set warning
$default = "5, 15";
$_SESSION['return'][] = array(
'type' => 'warning',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'Could not determine servers default spam score, assuming default'
);
}
curl_close($curl);
$policydata = array();
if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
}
else {
$_data = $_SESSION['mailcow_cc_username'];
}
$stmt = $pdo->prepare("SELECT `value` FROM `filterconf` WHERE `object` = :username AND
(`option` = 'lowspamlevel' OR `option` = 'highspamlevel')");
$stmt->execute(array(':username' => $_data));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if (empty($num_results)) {
return $default;
}
else {
$stmt = $pdo->prepare("SELECT `value` FROM `filterconf` WHERE `option` = 'highspamlevel' AND `object` = :username");
$stmt->execute(array(':username' => $_data));
$highspamlevel = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT `value` FROM `filterconf` WHERE `option` = 'lowspamlevel' AND `object` = :username");
$stmt->execute(array(':username' => $_data));
$lowspamlevel = $stmt->fetch(PDO::FETCH_ASSOC);
return $lowspamlevel['value'].', '.$highspamlevel['value'];
}
break;
case 'time_limited_aliases':
$tladata = array();
if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
}
else {
$_data = $_SESSION['mailcow_cc_username'];
}
$stmt = $pdo->prepare("SELECT `address`,
`goto`,
`validity`
FROM `spamalias`
WHERE `goto` = :username
AND `validity` >= :unixnow");
$stmt->execute(array(':username' => $_data, ':unixnow' => time()));
$tladata = $stmt->fetchAll(PDO::FETCH_ASSOC);
return $tladata;
break;
case 'delimiter_action':
$policydata = array();
if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
}
else {
$_data = $_SESSION['mailcow_cc_username'];
}
try {
if ($redis->hGet('RCPT_WANTS_SUBJECT_TAG', $_data)) {
return "subject";
}
elseif ($redis->hGet('RCPT_WANTS_SUBFOLDER_TAG', $_data)) {
return "subfolder";
}
else {
return "none";
}
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e)
);
return false;
}
break;
case 'resources':
$resources = array();
if (isset($_data) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
elseif (isset($_data) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` REGEXP 'location|thing|group' AND `domain` = :domain");
$stmt->execute(array(
':domain' => $_data,
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$resources[] = $row['username'];
}
}
else {
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `kind` REGEXP 'location|thing|group' AND `domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :username) OR 'admin' = :role");
$stmt->execute(array(
':username' => $_SESSION['mailcow_cc_username'],
':role' => $_SESSION['mailcow_cc_role'],
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$resources[] = $row['username'];
}
}
return $resources;
break;
case 'alias_domains':
$aliasdomains = array();
if (isset($_data) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
elseif (isset($_data) && hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :domain");
$stmt->execute(array(
':domain' => $_data,
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$aliasdomains[] = $row['alias_domain'];
}
}
else {
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` IN (SELECT `domain` FROM `domain_admins` WHERE `active` = '1' AND `username` = :username) OR 'admin' = :role");
$stmt->execute(array(
':username' => $_SESSION['mailcow_cc_username'],
':role' => $_SESSION['mailcow_cc_role'],
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$aliasdomains[] = $row['alias_domain'];
}
}
return $aliasdomains;
break;
case 'aliases':
$aliases = array();
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
$stmt = $pdo->prepare("SELECT `id` FROM `alias` WHERE `address` != `goto` AND `domain` = :domain");
$stmt->execute(array(
':domain' => $_data,
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$aliases[] = $row['id'];
}
return $aliases;
break;
case 'alias_details':
$aliasdata = array();
$stmt = $pdo->prepare("SELECT
`id`,
`domain`,
`goto`,
`address`,
`public_comment`,
`private_comment`,
`active`,
`sogo_visible`,
`created`,
`modified`
FROM `alias`
WHERE (`id` = :id OR `address` = :address) AND `address` != `goto`");
$stmt->execute(array(
':id' => $_data,
':address' => $_data,
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain");
$stmt->execute(array(
':domain' => $row['domain'],
));
$row_alias_domain = $stmt->fetch(PDO::FETCH_ASSOC);
if (isset($row_alias_domain['target_domain']) && !empty($row_alias_domain['target_domain'])) {
$aliasdata['in_primary_domain'] = $row_alias_domain['target_domain'];
}
else {
$aliasdata['in_primary_domain'] = "";
}
$aliasdata['id'] = $row['id'];
$aliasdata['domain'] = $row['domain'];
$aliasdata['public_comment'] = $row['public_comment'];
$aliasdata['private_comment'] = $row['private_comment'];
$aliasdata['domain'] = $row['domain'];
$aliasdata['goto'] = $row['goto'];
$aliasdata['address'] = $row['address'];
(!filter_var($aliasdata['address'], FILTER_VALIDATE_EMAIL)) ? $aliasdata['is_catch_all'] = 1 : $aliasdata['is_catch_all'] = 0;
$aliasdata['active'] = $row['active'];
$aliasdata['active_int'] = $row['active'];
$aliasdata['sogo_visible'] = $row['sogo_visible'];
$aliasdata['sogo_visible_int'] = $row['sogo_visible'];
$aliasdata['created'] = $row['created'];
$aliasdata['modified'] = $row['modified'];
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) {
return false;
}
return $aliasdata;
break;
case 'alias_domain_details':
$aliasdomaindata = array();
$rl = ratelimit('get', 'domain', $_data);
$stmt = $pdo->prepare("SELECT
`alias_domain`,
`target_domain`,
`active`,
`created`,
`modified`
FROM `alias_domain`
WHERE `alias_domain` = :aliasdomain");
$stmt->execute(array(
':aliasdomain' => $_data,
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT `backupmx` FROM `domain` WHERE `domain` = :target_domain");
$stmt->execute(array(
':target_domain' => $row['target_domain']
));
$row_parent = $stmt->fetch(PDO::FETCH_ASSOC);
$aliasdomaindata['alias_domain'] = $row['alias_domain'];
$aliasdomaindata['parent_is_backupmx'] = $row_parent['backupmx'];
$aliasdomaindata['target_domain'] = $row['target_domain'];
$aliasdomaindata['active'] = $row['active'];
$aliasdomaindata['active_int'] = $row['active'];
$aliasdomaindata['rl'] = $rl;
$aliasdomaindata['created'] = $row['created'];
$aliasdomaindata['modified'] = $row['modified'];
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdomaindata['target_domain'])) {
return false;
}
return $aliasdomaindata;
break;
case 'domains':
$domains = array();
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
return false;
}
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE (`domain` IN (
SELECT `domain` from `domain_admins`
WHERE (`active`='1' AND `username` = :username))
)
OR 'admin'= :role");
$stmt->execute(array(
':username' => $_SESSION['mailcow_cc_username'],
':role' => $_SESSION['mailcow_cc_role'],
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$domains[] = $row['domain'];
}
return $domains;
break;
case 'domain_details':
$domaindata = array();
$_data = idn_to_ascii(strtolower(trim($_data)), 0, INTL_IDNA_VARIANT_UTS46);
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain");
$stmt->execute(array(
':domain' => $_data
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!empty($row)) {
$_data = $row['target_domain'];
}
$stmt = $pdo->prepare("SELECT
`domain`,
`description`,
`aliases`,
`mailboxes`,
`defquota`,
`maxquota`,
`quota`,
`relayhost`,
`relay_all_recipients`,
`relay_unknown_only`,
`backupmx`,
`gal`,
`xmpp`,
`xmpp_prefix`,
`active`
FROM `domain` WHERE `domain`= :domain");
$stmt->execute(array(
':domain' => $_data
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($row)) {
return false;
}
$stmt = $pdo->prepare("SELECT COUNT(*) AS `count`,
COALESCE(SUM(`quota`), 0) AS `in_use`
FROM `mailbox`
WHERE `kind` NOT REGEXP 'location|thing|group'
AND `domain` = :domain");
$stmt->execute(array(':domain' => $row['domain']));
$MailboxDataDomain = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT SUM(bytes) AS `bytes_total`, SUM(messages) AS `msgs_total` FROM `quota2`
WHERE `username` IN (
SELECT `username` FROM `mailbox`
WHERE `domain` = :domain
);");
$stmt->execute(array(':domain' => $row['domain']));
$SumQuotaInUse = $stmt->fetch(PDO::FETCH_ASSOC);
$rl = ratelimit('get', 'domain', $_data);
$domaindata['max_new_mailbox_quota'] = ($row['quota'] * 1048576) - $MailboxDataDomain['in_use'];
if ($domaindata['max_new_mailbox_quota'] > ($row['maxquota'] * 1048576)) {
$domaindata['max_new_mailbox_quota'] = ($row['maxquota'] * 1048576);
}
$domaindata['def_new_mailbox_quota'] = $domaindata['max_new_mailbox_quota'];
if ($domaindata['def_new_mailbox_quota'] > ($row['defquota'] * 1048576)) {
$domaindata['def_new_mailbox_quota'] = ($row['defquota'] * 1048576);
}
$domaindata['quota_used_in_domain'] = $MailboxDataDomain['in_use'];
if (!empty($SumQuotaInUse['bytes_total'])) {
$domaindata['bytes_total'] = $SumQuotaInUse['bytes_total'];
}
else {
$domaindata['bytes_total'] = 0;
}
if (!empty($SumQuotaInUse['msgs_total'])) {
$domaindata['msgs_total'] = $SumQuotaInUse['msgs_total'];
}
else {
$domaindata['msgs_total'] = 0;
}
$domaindata['mboxes_in_domain'] = $MailboxDataDomain['count'];
$domaindata['mboxes_left'] = $row['mailboxes'] - $MailboxDataDomain['count'];
$domaindata['domain_name'] = $row['domain'];
$domaindata['description'] = $row['description'];
$domaindata['max_num_aliases_for_domain'] = $row['aliases'];
$domaindata['max_num_mboxes_for_domain'] = $row['mailboxes'];
$domaindata['def_quota_for_mbox'] = $row['defquota'] * 1048576;
$domaindata['max_quota_for_mbox'] = $row['maxquota'] * 1048576;
$domaindata['max_quota_for_domain'] = $row['quota'] * 1048576;
$domaindata['relayhost'] = $row['relayhost'];
$domaindata['backupmx'] = $row['backupmx'];
$domaindata['backupmx_int'] = $row['backupmx'];
$domaindata['gal'] = $row['gal'];
$domaindata['xmpp'] = $row['xmpp'];
$domaindata['xmpp_prefix'] = $row['xmpp_prefix'];
$domaindata['gal_int'] = $row['gal'];
$domaindata['rl'] = $rl;
$domaindata['active'] = $row['active'];
$domaindata['active_int'] = $row['active'];
$domaindata['relay_all_recipients'] = $row['relay_all_recipients'];
$domaindata['relay_all_recipients_int'] = $row['relay_all_recipients'];
$domaindata['relay_unknown_only'] = $row['relay_unknown_only'];
$domaindata['relay_unknown_only_int'] = $row['relay_unknown_only'];
$stmt = $pdo->prepare("SELECT COUNT(*) AS `alias_count` FROM `alias`
WHERE (`domain`= :domain OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :domain2))
AND `address` NOT IN (
SELECT `username` FROM `mailbox`
)");
$stmt->execute(array(
':domain' => $_data,
':domain2' => $_data
));
$AliasDataDomain = $stmt->fetch(PDO::FETCH_ASSOC);
(isset($AliasDataDomain['alias_count'])) ? $domaindata['aliases_in_domain'] = $AliasDataDomain['alias_count'] : $domaindata['aliases_in_domain'] = "0";
$domaindata['aliases_left'] = $row['aliases'] - $AliasDataDomain['alias_count'];
if ($_SESSION['mailcow_cc_role'] == "admin")
{
$stmt = $pdo->prepare("SELECT GROUP_CONCAT(`username` SEPARATOR ', ') AS domain_admins FROM `domain_admins` WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $_data
));
$domain_admins = $stmt->fetch(PDO::FETCH_ASSOC);
(isset($domain_admins['domain_admins'])) ? $domaindata['domain_admins'] = $domain_admins['domain_admins'] : $domaindata['domain_admins'] = "-";
}
return $domaindata;
break;
case 'mailbox_details':
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
$mailboxdata = array();
$rl = ratelimit('get', 'mailbox', $_data);
$last_imap_login = $redis->Get('last-login/imap/' . $_data);
$last_smtp_login = $redis->Get('last-login/smtp/' . $_data);
$last_pop3_login = $redis->Get('last-login/pop3/' . $_data);
if ($last_imap_login === false || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
$last_imap_login = '0';
}
if ($last_smtp_login === false || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
$last_smtp_login = '0';
}
if ($last_pop3_login === false || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
$last_pop3_login = '0';
}
if (preg_match('/y|yes/i', getenv('MASTER'))) {
$stmt = $pdo->prepare("SELECT
`domain`.`backupmx`,
`mailbox`.`username`,
`mailbox`.`name`,
`mailbox`.`active`,
`mailbox`.`domain`,
`mailbox`.`local_part`,
`mailbox`.`quota`,
`domain`.`xmpp` AS `domain_xmpp`,
`domain`.`xmpp_prefix` AS `domain_xmpp_prefix`,
`quota2`.`bytes`,
`attributes`,
`quota2`.`messages`
FROM `mailbox`, `quota2`, `domain`
WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group' AND `mailbox`.`username` = `quota2`.`username` AND `domain`.`domain` = `mailbox`.`domain` AND `mailbox`.`username` = :mailbox");
}
else {
$stmt = $pdo->prepare("SELECT
`domain`.`backupmx`,
`mailbox`.`username`,
`mailbox`.`name`,
`mailbox`.`active`,
`mailbox`.`domain`,
`mailbox`.`local_part`,
`mailbox`.`quota`,
`domain`.`xmpp` AS `domain_xmpp`,
`domain`.`xmpp_prefix` AS `domain_xmpp_prefix`,
`quota2replica`.`bytes`,
`attributes`,
`quota2replica`.`messages`
FROM `mailbox`, `quota2replica`, `domain`
WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group' AND `mailbox`.`username` = `quota2replica`.`username` AND `domain`.`domain` = `mailbox`.`domain` AND `mailbox`.`username` = :mailbox");
}
$stmt->execute(array(
':mailbox' => $_data,
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT `maxquota`, `quota` FROM `domain` WHERE `domain` = :domain");
$stmt->execute(array(':domain' => $row['domain']));
$DomainQuota = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT IFNULL(COUNT(`active`), 0) AS `pushover_active` FROM `pushover` WHERE `username` = :username AND `active` = 1");
$stmt->execute(array(':username' => $_data));
$PushoverActive = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `domain` = :domain AND `username` != :username");
$stmt->execute(array(':domain' => $row['domain'], ':username' => $_data));
$MailboxUsage = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND `validity` >= :unixnow");
$stmt->execute(array(':address' => $_data, ':unixnow' => time()));
$SpamaliasUsage = $stmt->fetch(PDO::FETCH_ASSOC);
$mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use'];
if ($mailboxdata['max_new_quota'] > ($DomainQuota['maxquota'] * 1048576)) {
$mailboxdata['max_new_quota'] = ($DomainQuota['maxquota'] * 1048576);
}
$mailboxdata['username'] = $row['username'];
if (!empty($rl)) {
$mailboxdata['rl'] = $rl;
$mailboxdata['rl_scope'] = 'mailbox';
}
else {
$mailboxdata['rl'] = ratelimit('get', 'domain', $row['domain']);
$mailboxdata['rl_scope'] = 'domain';
}
$mailboxdata['is_relayed'] = $row['backupmx'];
$mailboxdata['name'] = $row['name'];
$mailboxdata['last_imap_login'] = $last_imap_login;
$mailboxdata['last_smtp_login'] = $last_smtp_login;
$mailboxdata['last_pop3_login'] = $last_pop3_login;
$mailboxdata['active'] = $row['active'];
$mailboxdata['active_int'] = $row['active'];
$mailboxdata['domain'] = $row['domain'];
$mailboxdata['domain_xmpp'] = $row['domain_xmpp'];
$mailboxdata['domain_xmpp_prefix'] = $row['domain_xmpp_prefix'];
$mailboxdata['local_part'] = $row['local_part'];
$mailboxdata['quota'] = $row['quota'];
$mailboxdata['attributes'] = json_decode($row['attributes'], true);
$mailboxdata['quota_used'] = intval($row['bytes']);
$mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
$mailboxdata['messages'] = $row['messages'];
$mailboxdata['spam_aliases'] = $SpamaliasUsage['sa_count'];
$mailboxdata['pushover_active'] = ($PushoverActive['pushover_active'] == 1) ? 1 : 0;
if ($mailboxdata['percent_in_use'] === '- ') {
$mailboxdata['percent_class'] = "info";
}
elseif ($mailboxdata['percent_in_use'] >= 90) {
$mailboxdata['percent_class'] = "danger";
}
elseif ($mailboxdata['percent_in_use'] >= 75) {
$mailboxdata['percent_class'] = "warning";
}
else {
$mailboxdata['percent_class'] = "success";
}
return $mailboxdata;
break;
case 'resource_details':
$resourcedata = array();
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
$stmt = $pdo->prepare("SELECT
`username`,
`name`,
`kind`,
`multiple_bookings`,
`local_part`,
`active`,
`domain`
FROM `mailbox` WHERE `kind` REGEXP 'location|thing|group' AND `username` = :resource");
$stmt->execute(array(
':resource' => $_data,
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$resourcedata['name'] = $row['username'];
$resourcedata['kind'] = $row['kind'];
$resourcedata['multiple_bookings'] = $row['multiple_bookings'];
$resourcedata['description'] = $row['name'];
$resourcedata['active'] = $row['active'];
$resourcedata['active_int'] = $row['active'];
$resourcedata['domain'] = $row['domain'];
$resourcedata['local_part'] = $row['local_part'];
if (!isset($resourcedata['domain']) ||
(isset($resourcedata['domain']) && !hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $resourcedata['domain']))) {
return false;
}
return $resourcedata;
break;
}
break;
case 'delete':
switch ($_type) {
case 'syncjob':
if (!is_array($_data['id'])) {
$ids = array();
$ids[] = $_data['id'];
}
else {
$ids = $_data['id'];
}
if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($ids as $id) {
if (!is_numeric($id)) {
return false;
}
$stmt = $pdo->prepare("SELECT `user2` FROM `imapsync` WHERE id = :id");
$stmt->execute(array(':id' => $id));
$user2 = $stmt->fetch(PDO::FETCH_ASSOC)['user2'];
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $user2)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("DELETE FROM `imapsync` WHERE `id`= :id");
$stmt->execute(array(':id' => $id));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('deleted_syncjob', $id)
);
}
break;
case 'filter':
if (!is_array($_data['id'])) {
$ids = array();
$ids[] = $_data['id'];
}
else {
$ids = $_data['id'];
}
if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($ids as $id) {
if (!is_numeric($id)) {
continue;
}
$stmt = $pdo->prepare("SELECT `username` FROM `sieve_filters` WHERE id = :id");
$stmt->execute(array(':id' => $id));
$usr = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $usr)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("DELETE FROM `sieve_filters` WHERE `id`= :id");
$stmt->execute(array(':id' => $id));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('delete_filter', $id)
);
}
break;
case 'time_limited_alias':
if (!is_array($_data['address'])) {
$addresses = array();
$addresses[] = $_data['address'];
}
else {
$addresses = $_data['address'];
}
if (!isset($_SESSION['acl']['spam_alias']) || $_SESSION['acl']['spam_alias'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($addresses as $address) {
$stmt = $pdo->prepare("SELECT `goto` FROM `spamalias` WHERE `address` = :address");
$stmt->execute(array(':address' => $address));
$goto = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $goto)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("DELETE FROM `spamalias` WHERE `goto` = :username AND `address` = :item");
$stmt->execute(array(
':username' => $goto,
':item' => $address
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', htmlspecialchars($goto))
);
}
break;
case 'eas_cache':
if (!is_array($_data['username'])) {
$usernames = array();
$usernames[] = $_data['username'];
}
else {
$usernames = $_data['username'];
}
if (!isset($_SESSION['acl']['eas_reset']) || $_SESSION['acl']['eas_reset'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($usernames as $username) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("DELETE FROM `sogo_cache_folder` WHERE `c_uid` = :username");
$stmt->execute(array(
':username' => $username
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('eas_reset', htmlspecialchars($username))
);
}
break;
case 'sogo_profile':
if (!is_array($_data['username'])) {
$usernames = array();
$usernames[] = $_data['username'];
}
else {
$usernames = $_data['username'];
}
if (!isset($_SESSION['acl']['sogo_profile_reset']) || $_SESSION['acl']['sogo_profile_reset'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($usernames as $username) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("DELETE FROM `sogo_user_profile` WHERE `c_uid` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_cache_folder` WHERE `c_uid` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $username . "/%' OR `c_uid` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_quick_contact` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_quick_appointment` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_folder_info` WHERE `c_path2` = :username");
$stmt->execute(array(
':username' => $username
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('sogo_profile_reset', htmlspecialchars($username))
);
}
break;
case 'domain':
if (!is_array($_data['domain'])) {
$domains = array();
$domains[] = $_data['domain'];
}
else {
$domains = $_data['domain'];
}
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($domains as $domain) {
if (!is_valid_domain_name($domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_invalid'
);
continue;
}
$domain = idn_to_ascii(strtolower(trim($domain)), 0, INTL_IDNA_VARIANT_UTS46);
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
WHERE `domain` = :domain");
$stmt->execute(array(':domain' => $domain));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0 || !empty($num_results)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('domain_not_empty', $domain)
);
continue;
}
$exec_fields = array('cmd' => 'maildir', 'task' => 'cleanup', 'maildir' => $domain);
$maildir_gc = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true);
if ($maildir_gc['type'] != 'success') {
$_SESSION['return'][] = array(
'type' => 'warning',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'Could not move mail storage to garbage collector: ' . $maildir_gc['msg']
);
}
$stmt = $pdo->prepare("DELETE FROM `domain` WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain,
));
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain,
));
$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain,
));
$stmt = $pdo->prepare("DELETE FROM `alias_domain` WHERE `target_domain` = :domain");
$stmt->execute(array(
':domain' => $domain,
));
$stmt = $pdo->prepare("DELETE FROM `mailbox` WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain,
));
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` LIKE :domain");
$stmt->execute(array(
':domain' => '%@'.$domain,
));
$stmt = $pdo->prepare("DELETE FROM `quota2` WHERE `username` LIKE :domain");
$stmt->execute(array(
':domain' => '%@'.$domain,
));
$stmt = $pdo->prepare("DELETE FROM `pushover` WHERE `username` LIKE :domain");
$stmt->execute(array(
':domain' => '%@'.$domain,
));
$stmt = $pdo->prepare("DELETE FROM `quota2replica` WHERE `username` LIKE :domain");
$stmt->execute(array(
':domain' => '%@'.$domain,
));
$stmt = $pdo->prepare("DELETE FROM `spamalias` WHERE `address` LIKE :domain");
$stmt->execute(array(
':domain' => '%@'.$domain,
));
$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :domain");
$stmt->execute(array(
':domain' => $domain,
));
$stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `local_dest` = :domain");
$stmt->execute(array(
':domain' => $domain,
));
$stmt = $pdo->query("DELETE FROM `admin` WHERE `superadmin` = 0 AND `username` NOT IN (SELECT `username`FROM `domain_admins`);");
$stmt = $pdo->query("DELETE FROM `da_acl` WHERE `username` NOT IN (SELECT `username`FROM `domain_admins`);");
try {
$redis->hDel('DOMAIN_MAP', $domain);
$redis->hDel('RL_VALUE', $domain);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e)
);
continue;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('domain_removed', htmlspecialchars($domain))
);
}
break;
case 'alias':
if (!is_array($_data['id'])) {
$ids = array();
$ids[] = $_data['id'];
}
else {
$ids = $_data['id'];
}
foreach ($ids as $id) {
$alias_data = mailbox('get', 'alias_details', $id);
if (empty($alias_data)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `id` = :id");
$stmt->execute(array(
':id' => $alias_data['id']
));
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `send_as` = :alias_address");
$stmt->execute(array(
':alias_address' => $alias_data['address']
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('alias_removed', htmlspecialchars($alias_data['address']))
);
}
break;
case 'alias_domain':
if (!is_array($_data['alias_domain'])) {
$alias_domains = array();
$alias_domains[] = $_data['alias_domain'];
}
else {
$alias_domains = $_data['alias_domain'];
}
foreach ($alias_domains as $alias_domain) {
if (!is_valid_domain_name($alias_domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_invalid'
);
continue;
}
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain`
WHERE `alias_domain`= :alias_domain");
$stmt->execute(array(':alias_domain' => $alias_domain));
$DomainData = $stmt->fetch(PDO::FETCH_ASSOC);
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $DomainData['target_domain'])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("DELETE FROM `alias_domain` WHERE `alias_domain` = :alias_domain");
$stmt->execute(array(
':alias_domain' => $alias_domain,
));
$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `domain` = :alias_domain");
$stmt->execute(array(
':alias_domain' => $alias_domain,
));
$stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `local_dest` = :alias_domain");
$stmt->execute(array(
':alias_domain' => $alias_domain,
));
try {
$redis->hDel('DOMAIN_MAP', $alias_domain);
$redis->hDel('RL_VALUE', $domain);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e)
);
continue;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('alias_domain_removed', htmlspecialchars($alias_domain))
);
}
break;
case 'mailbox':
if (!is_array($_data['username'])) {
$usernames = array();
$usernames[] = $_data['username'];
}
else {
$usernames = $_data['username'];
}
foreach ($usernames as $username) {
if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$mailbox_details = mailbox('get', 'mailbox_details', $username);
if (!empty($mailbox_details['domain']) && !empty($mailbox_details['local_part'])) {
$maildir = $mailbox_details['domain'] . '/' . $mailbox_details['local_part'];
$exec_fields = array('cmd' => 'maildir', 'task' => 'cleanup', 'maildir' => $maildir);
$maildir_gc = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true);
if ($maildir_gc['type'] != 'success') {
$_SESSION['return'][] = array(
'type' => 'warning',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'Could not move maildir to garbage collector: ' . $maildir_gc['msg']
);
}
}
else {
$_SESSION['return'][] = array(
'type' => 'warning',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'Could not move maildir to garbage collector: variables local_part and/or domain empty'
);
}
if (strtolower(getenv('SKIP_SOLR')) == 'n') {
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, 'http://solr:8983/solr/dovecot-fts/update?commit=true');
curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: text/xml'));
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, '<delete><query>user:' . $username . '</query></delete>');
curl_setopt($curl, CURLOPT_TIMEOUT, 30);
$response = curl_exec($curl);
if ($response === false) {
$err = curl_error($curl);
$_SESSION['return'][] = array(
'type' => 'warning',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'Could not remove Solr index: ' . print_r($err, true)
);
}
curl_close($curl);
}
$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `goto` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `pushover` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `quarantine` WHERE `rcpt` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `quota2` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `quota2replica` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `mailbox` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username");
$stmt->execute(array(
':username' => $username
));
// fk, better safe than sorry
$stmt = $pdo->prepare("DELETE FROM `user_acl` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `spamalias` WHERE `goto` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `imapsync` WHERE `user2` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `filterconf` WHERE `object` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_user_profile` WHERE `c_uid` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_cache_folder` WHERE `c_uid` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . str_replace('%', '\%', $username) . "/%' OR `c_uid` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_quick_contact` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_quick_appointment` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_folder_info` WHERE `c_path2` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `bcc_maps` WHERE `local_dest` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `oauth_access_tokens` WHERE `user_id` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `oauth_refresh_tokens` WHERE `user_id` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `oauth_authorization_codes` WHERE `user_id` = :username");
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("SELECT `address`, `goto` FROM `alias`
WHERE `goto` REGEXP :username");
$stmt->execute(array(':username' => '(^|,)'.$username.'($|,)'));
$GotoData = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($GotoData as $gotos) {
$goto_exploded = explode(',', $gotos['goto']);
if (($key = array_search($username, $goto_exploded)) !== false) {
unset($goto_exploded[$key]);
}
$gotos_rebuild = implode(',', $goto_exploded);
$stmt = $pdo->prepare("UPDATE `alias` SET
`goto` = :goto
WHERE `address` = :address");
$stmt->execute(array(
':goto' => $gotos_rebuild,
':address' => $gotos['address']
));
}
try {
$redis->hDel('RL_VALUE', $username);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e)
);
continue;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_removed', htmlspecialchars($username))
);
}
break;
case 'resource':
if (!is_array($_data['name'])) {
$names = array();
$names[] = $_data['name'];
}
else {
$names = $_data['name'];
}
foreach ($names as $name) {
if (!filter_var($name, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $name)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("DELETE FROM `mailbox` WHERE `username` = :username");
$stmt->execute(array(
':username' => $name
));
$stmt = $pdo->prepare("DELETE FROM `sogo_user_profile` WHERE `c_uid` = :username");
$stmt->execute(array(
':username' => $name
));
$stmt = $pdo->prepare("DELETE FROM `sogo_cache_folder` WHERE `c_uid` = :username");
$stmt->execute(array(
':username' => $name
));
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $name . "/%' OR `c_uid` = :username");
$stmt->execute(array(
':username' => $name
));
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
$stmt->execute(array(
':username' => $name
));
$stmt = $pdo->prepare("DELETE FROM `sogo_quick_contact` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
$stmt->execute(array(
':username' => $name
));
$stmt = $pdo->prepare("DELETE FROM `sogo_quick_appointment` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
$stmt->execute(array(
':username' => $name
));
$stmt = $pdo->prepare("DELETE FROM `sogo_folder_info` WHERE `c_path2` = :username");
$stmt->execute(array(
':username' => $name
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('resource_removed', htmlspecialchars($name))
);
}
break;
}
break;
}
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource'))) {
update_sogo_static_view();
xmpp_rebuild_configs();
}
}
diff --git a/docker-compose.yml b/docker-compose.yml
index 1e49147f..2c793416 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,619 +1,620 @@
version: '2.1'
services:
unbound-mailcow:
image: mailcow/unbound:1.13
environment:
- TZ=${TZ}
volumes:
- ./data/hooks/unbound:/hooks:Z
- ./data/conf/unbound/unbound.conf:/etc/unbound/unbound.conf:ro,Z
restart: always
tty: true
networks:
mailcow-network:
ipv4_address: ${IPV4_NETWORK:-172.22.1}.254
aliases:
- unbound
mysql-mailcow:
image: mariadb:10.4
depends_on:
- unbound-mailcow
stop_grace_period: 45s
volumes:
- mysql-vol-1:/var/lib/mysql/:Z
- mysql-socket-vol-1:/var/run/mysqld/:z
- ./data/conf/mysql/:/etc/mysql/conf.d/:ro,Z
environment:
- TZ=${TZ}
- MYSQL_ROOT_PASSWORD=${DBROOT}
- MYSQL_DATABASE=${DBNAME}
- MYSQL_USER=${DBUSER}
- MYSQL_PASSWORD=${DBPASS}
- MYSQL_INITDB_SKIP_TZINFO=1
restart: always
ports:
- "${SQL_PORT:-127.0.0.1:13306}:3306"
networks:
mailcow-network:
aliases:
- mysql
redis-mailcow:
image: redis:5-alpine
volumes:
- redis-vol-1:/data/:Z
restart: always
ports:
- "${REDIS_PORT:-127.0.0.1:7654}:6379"
environment:
- TZ=${TZ}
networks:
mailcow-network:
ipv4_address: ${IPV4_NETWORK:-172.22.1}.249
aliases:
- redis
clamd-mailcow:
image: mailcow/clamd:1.38
restart: always
dns:
- ${IPV4_NETWORK:-172.22.1}.254
environment:
- TZ=${TZ}
- SKIP_CLAMD=${SKIP_CLAMD:-n}
volumes:
- ./data/conf/clamav/:/etc/clamav/:Z
networks:
mailcow-network:
aliases:
- clamd
rspamd-mailcow:
image: mailcow/rspamd:1.76
stop_grace_period: 30s
depends_on:
- dovecot-mailcow
environment:
- TZ=${TZ}
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
- IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
volumes:
- ./data/hooks/rspamd:/hooks:Z
- ./data/conf/rspamd/custom/:/etc/rspamd/custom:z
- ./data/conf/rspamd/override.d/:/etc/rspamd/override.d:Z
- ./data/conf/rspamd/local.d/:/etc/rspamd/local.d:Z
- ./data/conf/rspamd/plugins.d/:/etc/rspamd/plugins.d:Z
- ./data/conf/rspamd/lua/:/etc/rspamd/lua/:ro,Z
- ./data/conf/rspamd/rspamd.conf.local:/etc/rspamd/rspamd.conf.local:Z
- ./data/conf/rspamd/rspamd.conf.override:/etc/rspamd/rspamd.conf.override:Z
- rspamd-vol-1:/var/lib/rspamd:z
restart: always
hostname: rspamd
dns:
- ${IPV4_NETWORK:-172.22.1}.254
networks:
mailcow-network:
aliases:
- rspamd
php-fpm-mailcow:
image: mailcow/phpfpm:1.73
command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
depends_on:
- redis-mailcow
volumes:
- ./data/hooks/phpfpm:/hooks:Z
- ./data/web:/web:z
- ./data/conf/rspamd/dynmaps:/dynmaps:ro,z
- ./data/conf/rspamd/custom/:/rspamd_custom_maps:z
- rspamd-vol-1:/var/lib/rspamd:z
- mysql-socket-vol-1:/var/run/mysqld/:z
- ./data/conf/sogo/:/etc/sogo/:z
- ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z
- ./data/conf/phpfpm/sogo-sso/:/etc/sogo-sso/:z
- ./data/conf/phpfpm/php-fpm.d/pools.conf:/usr/local/etc/php-fpm.d/z-pools.conf:Z
- ./data/conf/phpfpm/php-conf.d/opcache-recommended.ini:/usr/local/etc/php/conf.d/opcache-recommended.ini:Z
- ./data/conf/phpfpm/php-conf.d/upload.ini:/usr/local/etc/php/conf.d/upload.ini:Z
- ./data/conf/phpfpm/php-conf.d/other.ini:/usr/local/etc/php/conf.d/zzz-other.ini:Z
- ./data/conf/dovecot/global_sieve_before:/global_sieve/before:z
- ./data/conf/dovecot/global_sieve_after:/global_sieve/after:z
- ./data/assets/templates:/tpls:z
- ./data/conf/ejabberd/autogen:/ejabberd/:z
- ./data/conf/nginx/:/etc/nginx/conf.d/:z
dns:
- ${IPV4_NETWORK:-172.22.1}.254
environment:
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
- LOG_LINES=${LOG_LINES:-9999}
- TZ=${TZ}
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
- DBPASS=${DBPASS}
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
- MAILCOW_PASS_SCHEME=${MAILCOW_PASS_SCHEME:-BLF-CRYPT}
- IMAP_PORT=${IMAP_PORT:-143}
- IMAPS_PORT=${IMAPS_PORT:-993}
- POP_PORT=${POP_PORT:-110}
- POPS_PORT=${POPS_PORT:-995}
- SIEVE_PORT=${SIEVE_PORT:-4190}
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
- IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
- SUBMISSION_PORT=${SUBMISSION_PORT:-587}
- SMTPS_PORT=${SMTPS_PORT:-465}
- SMTP_PORT=${SMTP_PORT:-25}
- XMPP_C2S_PORT=${XMPP_C2S_PORT:-5222}
- XMPP_S2S_PORT=${XMPP_S2S_PORT:-5269}
- API_KEY=${API_KEY:-invalid}
- API_KEY_READ_ONLY=${API_KEY_READ_ONLY:-invalid}
- API_ALLOW_FROM=${API_ALLOW_FROM:-invalid}
- COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized}
- SKIP_SOLR=${SKIP_SOLR:-y}
- SKIP_CLAMD=${SKIP_CLAMD:-n}
- SKIP_SOGO=${SKIP_SOGO:-n}
- ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n}
- MASTER=${MASTER:-y}
restart: always
networks:
mailcow-network:
aliases:
- phpfpm
sogo-mailcow:
image: mailcow/sogo:1.96
environment:
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
- DBPASS=${DBPASS}
- TZ=${TZ}
- LOG_LINES=${LOG_LINES:-9999}
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
- MAILCOW_PASS_SCHEME=${MAILCOW_PASS_SCHEME:-BLF-CRYPT}
- ACL_ANYONE=${ACL_ANYONE:-disallow}
- ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n}
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
- SOGO_EXPIRE_SESSION=${SOGO_EXPIRE_SESSION:-480}
- SKIP_SOGO=${SKIP_SOGO:-n}
- MASTER=${MASTER:-y}
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
dns:
- ${IPV4_NETWORK:-172.22.1}.254
volumes:
- ./data/conf/sogo/:/etc/sogo/:z
- ./data/web/inc/init_db.inc.php:/init_db.inc.php:Z
- ./data/conf/sogo/custom-favicon.ico:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo.ico:z
- ./data/conf/sogo/custom-theme.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/theme.js:z
- ./data/conf/sogo/custom-sogo.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/custom-sogo.js:z
- mysql-socket-vol-1:/var/run/mysqld/:z
- sogo-web-vol-1:/sogo_web:z
- sogo-userdata-backup-vol-1:/sogo_backup:Z
restart: always
networks:
mailcow-network:
ipv4_address: ${IPV4_NETWORK:-172.22.1}.248
aliases:
- sogo
dovecot-mailcow:
image: mailcow/dovecot:1.142
depends_on:
- mysql-mailcow
dns:
- ${IPV4_NETWORK:-172.22.1}.254
cap_add:
- NET_BIND_SERVICE
volumes:
- ./data/hooks/dovecot:/hooks:Z
- ./data/conf/dovecot:/etc/dovecot:z
- ./data/assets/ssl:/etc/ssl/mail/:ro,z
- ./data/conf/sogo/:/etc/sogo/:z
- ./data/conf/phpfpm/sogo-sso/:/etc/phpfpm/:z
- vmail-vol-1:/var/vmail:Z
- vmail-index-vol-1:/var/vmail_index:Z
- crypt-vol-1:/mail_crypt/:z
- ./data/conf/rspamd/custom/:/etc/rspamd/custom:z
- ./data/assets/templates:/templates:z
- rspamd-vol-1:/var/lib/rspamd:z
- mysql-socket-vol-1:/var/run/mysqld/:z
environment:
- DOVECOT_MASTER_USER=${DOVECOT_MASTER_USER:-}
- DOVECOT_MASTER_PASS=${DOVECOT_MASTER_PASS:-}
- LOG_LINES=${LOG_LINES:-9999}
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
- DBPASS=${DBPASS}
- TZ=${TZ}
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
- MAILCOW_PASS_SCHEME=${MAILCOW_PASS_SCHEME:-BLF-CRYPT}
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
- ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n}
- MAILDIR_GC_TIME=${MAILDIR_GC_TIME:-1440}
- ACL_ANYONE=${ACL_ANYONE:-disallow}
- SKIP_SOLR=${SKIP_SOLR:-y}
- MAILDIR_SUB=${MAILDIR_SUB:-}
- MASTER=${MASTER:-y}
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
- COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized}
ports:
- "${DOVEADM_PORT:-127.0.0.1:19991}:12345"
- "${IMAP_PORT:-143}:143"
- "${IMAPS_PORT:-993}:993"
- "${POP_PORT:-110}:110"
- "${POPS_PORT:-995}:995"
- "${SIEVE_PORT:-4190}:4190"
restart: always
tty: true
ulimits:
nproc: 65535
nofile:
soft: 20000
hard: 40000
networks:
mailcow-network:
ipv4_address: ${IPV4_NETWORK:-172.22.1}.250
aliases:
- dovecot
postfix-mailcow:
image: mailcow/postfix:1.59
depends_on:
- mysql-mailcow
volumes:
- ./data/hooks/postfix:/hooks:Z
- ./data/conf/postfix:/opt/postfix/conf:z
- ./data/assets/ssl:/etc/ssl/mail/:ro,z
- postfix-vol-1:/var/spool/postfix:z
- crypt-vol-1:/var/lib/zeyple:z
- rspamd-vol-1:/var/lib/rspamd:z
- mysql-socket-vol-1:/var/run/mysqld/:z
environment:
- LOG_LINES=${LOG_LINES:-9999}
- TZ=${TZ}
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
- DBPASS=${DBPASS}
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
cap_add:
- NET_BIND_SERVICE
ports:
- "${SMTP_PORT:-25}:25"
- "${SMTPS_PORT:-465}:465"
- "${SUBMISSION_PORT:-587}:587"
restart: always
dns:
- ${IPV4_NETWORK:-172.22.1}.254
networks:
mailcow-network:
aliases:
- postfix
memcached-mailcow:
image: memcached:alpine
restart: always
environment:
- TZ=${TZ}
networks:
mailcow-network:
aliases:
- memcached
nginx-mailcow:
depends_on:
- sogo-mailcow
- php-fpm-mailcow
- redis-mailcow
image: nginx:mainline-alpine
dns:
- ${IPV4_NETWORK:-172.22.1}.254
command: /bin/sh -c "envsubst < /etc/nginx/conf.d/templates/listen_plain.template > /etc/nginx/conf.d/listen_plain.active &&
envsubst < /etc/nginx/conf.d/templates/listen_ssl.template > /etc/nginx/conf.d/listen_ssl.active &&
envsubst < /etc/nginx/conf.d/templates/sogo.template > /etc/nginx/conf.d/sogo.active &&
. /etc/nginx/conf.d/templates/sogo.auth_request.template.sh > /etc/nginx/conf.d/sogo_proxy_auth.active &&
. /etc/nginx/conf.d/templates/server_name.template.sh > /etc/nginx/conf.d/server_name.active &&
. /etc/nginx/conf.d/templates/sites.template.sh > /etc/nginx/conf.d/sites.active &&
. /etc/nginx/conf.d/templates/sogo_eas.template.sh > /etc/nginx/conf.d/sogo_eas.active &&
nginx -qt &&
until ping phpfpm -c1 > /dev/null; do sleep 1; done &&
until ping sogo -c1 > /dev/null; do sleep 1; done &&
until ping redis -c1 > /dev/null; do sleep 1; done &&
until ping rspamd -c1 > /dev/null; do sleep 1; done &&
until ping ejabberd -c1 > /dev/null; do sleep 1; done &&
exec nginx -g 'daemon off;'"
environment:
- HTTPS_PORT=${HTTPS_PORT:-443}
- HTTP_PORT=${HTTP_PORT:-80}
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
- TZ=${TZ}
- SKIP_SOGO=${SKIP_SOGO:-n}
- ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n}
- ADDITIONAL_SERVER_NAMES=${ADDITIONAL_SERVER_NAMES:-}
volumes:
- ./data/web:/web:ro,z
- ./data/conf/rspamd/dynmaps:/dynmaps:ro,z
- ./data/assets/ssl/:/etc/ssl/mail/:ro,z
- ./data/conf/nginx/:/etc/nginx/conf.d/:z
- ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z
- sogo-web-vol-1:/usr/lib/GNUstep/SOGo/:z
ports:
- "${HTTPS_BIND:-:}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}"
- "${HTTP_BIND:-:}:${HTTP_PORT:-80}:${HTTP_PORT:-80}"
restart: always
networks:
mailcow-network:
aliases:
- nginx
acme-mailcow:
depends_on:
- nginx-mailcow
image: mailcow/acme:1.78
dns:
- ${IPV4_NETWORK:-172.22.1}.254
environment:
- LOG_LINES=${LOG_LINES:-9999}
- ADDITIONAL_SAN=${ADDITIONAL_SAN}
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
- DBPASS=${DBPASS}
- SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n}
- COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized}
- DIRECTORY_URL=${DIRECTORY_URL:-}
- ENABLE_SSL_SNI=${ENABLE_SSL_SNI:-n}
- SKIP_IP_CHECK=${SKIP_IP_CHECK:-n}
- SKIP_HTTP_VERIFICATION=${SKIP_HTTP_VERIFICATION:-n}
- ONLY_MAILCOW_HOSTNAME=${ONLY_MAILCOW_HOSTNAME:-n}
- LE_STAGING=${LE_STAGING:-n}
- TZ=${TZ}
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
- SNAT_TO_SOURCE=${SNAT_TO_SOURCE:-n}
- SNAT6_TO_SOURCE=${SNAT6_TO_SOURCE:-n}
volumes:
- ./data/web/.well-known/acme-challenge:/var/www/acme:z
- ./data/assets/ssl:/var/lib/acme/:z
- ./data/assets/ssl-example:/var/lib/ssl-example/:ro,Z
- mysql-socket-vol-1:/var/run/mysqld/:z
restart: always
networks:
mailcow-network:
aliases:
- acme
netfilter-mailcow:
image: mailcow/netfilter:1.39
stop_grace_period: 30s
depends_on:
- dovecot-mailcow
- postfix-mailcow
- sogo-mailcow
- php-fpm-mailcow
- redis-mailcow
restart: always
privileged: true
environment:
- TZ=${TZ}
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
- IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
- SNAT_TO_SOURCE=${SNAT_TO_SOURCE:-n}
- SNAT6_TO_SOURCE=${SNAT6_TO_SOURCE:-n}
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
network_mode: "host"
volumes:
- /lib/modules:/lib/modules:ro
watchdog-mailcow:
image: mailcow/watchdog:1.91
# Debug
#command: /watchdog.sh
dns:
- ${IPV4_NETWORK:-172.22.1}.254
tmpfs:
- /tmp
volumes:
- rspamd-vol-1:/var/lib/rspamd:z
- mysql-socket-vol-1:/var/run/mysqld/:z
- postfix-vol-1:/var/spool/postfix:z
- ./data/assets/ssl:/etc/ssl/mail/:ro,z
restart: always
environment:
- IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
- LOG_LINES=${LOG_LINES:-9999}
- TZ=${TZ}
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
- DBPASS=${DBPASS}
- DBROOT=${DBROOT}
- USE_WATCHDOG=${USE_WATCHDOG:-n}
- WATCHDOG_NOTIFY_EMAIL=${WATCHDOG_NOTIFY_EMAIL}
- WATCHDOG_NOTIFY_BAN=${WATCHDOG_NOTIFY_BAN:-y}
+ - WATCHDOG_SUBJECT=${WATCHDOG_SUBJECT:-Watchdog ALERT}
- WATCHDOG_EXTERNAL_CHECKS=${WATCHDOG_EXTERNAL_CHECKS:-n}
- WATCHDOG_MYSQL_REPLICATION_CHECKS=${WATCHDOG_MYSQL_REPLICATION_CHECKS:-n}
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
- COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized}
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
- IP_BY_DOCKER_API=${IP_BY_DOCKER_API:-0}
- CHECK_UNBOUND=${CHECK_UNBOUND:-1}
- SKIP_CLAMD=${SKIP_CLAMD:-n}
- SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n}
- SKIP_SOGO=${SKIP_SOGO:-n}
- HTTPS_PORT=${HTTPS_PORT:-443}
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
- EXTERNAL_CHECKS_THRESHOLD=${EXTERNAL_CHECKS_THRESHOLD:-1}
- NGINX_THRESHOLD=${NGINX_THRESHOLD:-5}
- UNBOUND_THRESHOLD=${UNBOUND_THRESHOLD:-5}
- REDIS_THRESHOLD=${REDIS_THRESHOLD:-5}
- MYSQL_THRESHOLD=${MYSQL_THRESHOLD:-5}
- MYSQL_REPLICATION_THRESHOLD=${MYSQL_REPLICATION_THRESHOLD:-1}
- SOGO_THRESHOLD=${SOGO_THRESHOLD:-3}
- POSTFIX_THRESHOLD=${POSTFIX_THRESHOLD:-8}
- CLAMD_THRESHOLD=${CLAMD_THRESHOLD:-15}
- DOVECOT_THRESHOLD=${DOVECOT_THRESHOLD:-12}
- DOVECOT_REPL_THRESHOLD=${DOVECOT_REPL_THRESHOLD:-20}
- PHPFPM_THRESHOLD=${PHPFPM_THRESHOLD:-5}
- RATELIMIT_THRESHOLD=${RATELIMIT_THRESHOLD:-1}
- FAIL2BAN_THRESHOLD=${FAIL2BAN_THRESHOLD:-1}
- ACME_THRESHOLD=${ACME_THRESHOLD:-1}
- IPV6NAT_THRESHOLD=${IPV6NAT_THRESHOLD:-1}
- RSPAMD_THRESHOLD=${RSPAMD_THRESHOLD:-5}
- OLEFY_THRESHOLD=${OLEFY_THRESHOLD:-5}
- MAILQ_THRESHOLD=${MAILQ_THRESHOLD:-20}
- MAILQ_CRIT=${MAILQ_CRIT:-30}
networks:
mailcow-network:
aliases:
- watchdog
dockerapi-mailcow:
image: mailcow/dockerapi:1.38
security_opt:
- label=disable
restart: always
oom_kill_disable: true
dns:
- ${IPV4_NETWORK:-172.22.1}.254
environment:
- DBROOT=${DBROOT}
- TZ=${TZ}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
mailcow-network:
aliases:
- dockerapi
solr-mailcow:
image: mailcow/solr:1.7
restart: always
volumes:
- solr-vol-1:/opt/solr/server/solr/dovecot-fts/data:Z
ports:
- "${SOLR_PORT:-127.0.0.1:18983}:8983"
environment:
- TZ=${TZ}
- SOLR_HEAP=${SOLR_HEAP:-1024}
- SKIP_SOLR=${SKIP_SOLR:-y}
networks:
mailcow-network:
aliases:
- solr
olefy-mailcow:
image: mailcow/olefy:1.6
restart: always
environment:
- TZ=${TZ}
- OLEFY_BINDADDRESS=0.0.0.0
- OLEFY_BINDPORT=10055
- OLEFY_TMPDIR=/tmp
- OLEFY_PYTHON_PATH=/usr/bin/python3
- OLEFY_OLEVBA_PATH=/usr/bin/olevba3
- OLEFY_LOGLVL=20
- OLEFY_MINLENGTH=500
- OLEFY_DEL_TMP=1
networks:
mailcow-network:
aliases:
- olefy
ejabberd-mailcow:
image: mailcow/ejabberd:1.4
volumes:
- ./data/conf/ejabberd/ejabberd.yml:/home/ejabberd/conf/ejabberd.yml:z
- xmpp-vol-1:/home/ejabberd/database:z
- xmpp-upload-vol-1:/var/www/upload:z
- ./data/assets/ejabberd/sqlite:/sqlite:z
- ./data/conf/ejabberd/autogen:/ejabberd/:z
- mysql-socket-vol-1:/var/run/mysqld/:z
- ./data/assets/ssl:/ssl:ro,z
restart: always
dns:
- ${IPV4_NETWORK:-172.22.1}.254
hostname: ejabberd.mailcow.local
extra_hosts:
- "${MAILCOW_HOSTNAME}:127.0.0.1"
environment:
- TZ=${TZ}
- MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
- MASTER=${MASTER:-y}
- IPV4_NETWORK=${IPV4_NETWORK:-172.22.1}
- XMPP_HTTPS_PORT=${XMPP_HTTPS_PORT:-5443}
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
- DBPASS=${DBPASS}
ports:
- "${XMPP_C2S_PORT:-5222}:5222"
- "${XMPP_S2S_PORT:-5269}:5269"
- "${XMPP_HTTPS_PORT:-5443}:5443"
networks:
mailcow-network:
aliases:
- ejabberd
ipv6nat-mailcow:
depends_on:
- unbound-mailcow
- mysql-mailcow
- redis-mailcow
- clamd-mailcow
- rspamd-mailcow
- php-fpm-mailcow
- sogo-mailcow
- dovecot-mailcow
- postfix-mailcow
- memcached-mailcow
- nginx-mailcow
- acme-mailcow
- netfilter-mailcow
- watchdog-mailcow
- dockerapi-mailcow
- solr-mailcow
environment:
- TZ=${TZ}
image: robbertkl/ipv6nat
security_opt:
- label=disable
restart: always
privileged: true
network_mode: "host"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /lib/modules:/lib/modules:ro
networks:
mailcow-network:
driver: bridge
driver_opts:
com.docker.network.bridge.name: br-mailcow
enable_ipv6: true
ipam:
driver: default
config:
- subnet: ${IPV4_NETWORK:-172.22.1}.0/24
- subnet: ${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
volumes:
vmail-vol-1:
vmail-index-vol-1:
mysql-vol-1:
mysql-socket-vol-1:
redis-vol-1:
rspamd-vol-1:
solr-vol-1:
postfix-vol-1:
crypt-vol-1:
sogo-web-vol-1:
sogo-userdata-backup-vol-1:
xmpp-vol-1:
xmpp-upload-vol-1:
diff --git a/generate_config.sh b/generate_config.sh
index 978a32fa..61a90aa0 100755
--- a/generate_config.sh
+++ b/generate_config.sh
@@ -1,347 +1,350 @@
#!/usr/bin/env bash
set -o pipefail
if [[ "$(uname -r)" =~ ^4\.15\.0-60 ]]; then
echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!";
echo "Please update to 5.x or use another distribution."
exit 1
fi
if [[ "$(uname -r)" =~ ^4\.4\. ]]; then
if grep -q Ubuntu <<< $(uname -a); then
echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!";
echo "Please update to linux-generic-hwe-16.04 by running \"apt-get install --install-recommends linux-generic-hwe-16.04\""
fi
exit 1
fi
if grep --help 2>&1 | grep -q -i "busybox"; then
echo "BusyBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\""
exit 1
fi
if cp --help 2>&1 | grep -q -i "busybox"; then
echo "BusyBox cp detected, please install coreutils, \"apk add --no-cache --upgrade coreutils\""
exit 1
fi
for bin in openssl curl docker-compose docker git awk sha1sum; do
if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
done
if [ -f mailcow.conf ]; then
read -r -p "A config file exists and will be overwritten, are you sure you want to continue? [y/N] " response
case $response in
[yY][eE][sS]|[yY])
mv mailcow.conf mailcow.conf_backup
chmod 600 mailcow.conf_backup
;;
*)
exit 1
;;
esac
fi
echo "Press enter to confirm the detected value '[value]' where applicable or enter a custom value."
while [ -z "${MAILCOW_HOSTNAME}" ]; do
read -p "Mail server hostname (FQDN) - this is not your mail domain, but your mail servers hostname: " -e MAILCOW_HOSTNAME
DOTS=${MAILCOW_HOSTNAME//[^.]};
if [ ${#DOTS} -lt 2 ] && [ ! -z ${MAILCOW_HOSTNAME} ]; then
echo "${MAILCOW_HOSTNAME} is not a FQDN"
MAILCOW_HOSTNAME=
fi
done
if [ -a /etc/timezone ]; then
DETECTED_TZ=$(cat /etc/timezone)
elif [ -a /etc/localtime ]; then
DETECTED_TZ=$(readlink /etc/localtime|sed -n 's|^.*zoneinfo/||p')
fi
while [ -z "${MAILCOW_TZ}" ]; do
if [ -z "${DETECTED_TZ}" ]; then
read -p "Timezone: " -e MAILCOW_TZ
else
read -p "Timezone [${DETECTED_TZ}]: " -e MAILCOW_TZ
[ -z "${MAILCOW_TZ}" ] && MAILCOW_TZ=${DETECTED_TZ}
fi
done
MEM_TOTAL=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
if [ ${MEM_TOTAL} -le "2621440" ]; then
echo "Installed memory is <= 2.5 GiB. It is recommended to disable ClamAV to prevent out-of-memory situations."
echo "ClamAV can be re-enabled by setting SKIP_CLAMD=n in mailcow.conf."
read -r -p "Do you want to disable ClamAV now? [Y/n] " response
case $response in
[nN][oO]|[nN])
SKIP_CLAMD=n
;;
*)
SKIP_CLAMD=y
;;
esac
else
SKIP_CLAMD=n
fi
if [ ${MEM_TOTAL} -le "2097152" ]; then
echo "Disabling Solr on low-memory system."
SKIP_SOLR=y
elif [ ${MEM_TOTAL} -le "3670016" ]; then
echo "Installed memory is <= 3.5 GiB. It is recommended to disable Solr to prevent out-of-memory situations."
echo "Solr is a prone to run OOM and should be monitored. The default Solr heap size is 1024 MiB and should be set in mailcow.conf according to your expected load."
echo "Solr can be re-enabled by setting SKIP_SOLR=n in mailcow.conf but will refuse to start with less than 2 GB total memory."
read -r -p "Do you want to disable Solr now? [Y/n] " response
case $response in
[nN][oO]|[nN])
SKIP_SOLR=n
;;
*)
SKIP_SOLR=y
;;
esac
else
SKIP_SOLR=n
fi
[ ! -f ./data/conf/rspamd/override.d/worker-controller-password.inc ] && echo '# Placeholder' > ./data/conf/rspamd/override.d/worker-controller-password.inc
cat << EOF > mailcow.conf
# ------------------------------
# mailcow web ui configuration
# ------------------------------
# example.org is _not_ a valid hostname, use a fqdn here.
# Default admin user is "admin"
# Default password is "moohoo"
MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME}
# Password hash algorithm
# Only certain password hash algorithm are supported. For a fully list of supported schemes,
# see https://mailcow.github.io/mailcow-dockerized-docs/model-passwd/
MAILCOW_PASS_SCHEME=BLF-CRYPT
# ------------------------------
# SQL database configuration
# ------------------------------
DBNAME=mailcow
DBUSER=mailcow
# Please use long, random alphanumeric strings (A-Za-z0-9)
DBPASS=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 | head -c 28)
DBROOT=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 | head -c 28)
# ------------------------------
# HTTP/S Bindings
# ------------------------------
# You should use HTTPS, but in case of SSL offloaded reverse proxies:
# Might be important: This will also change the binding within the container.
# If you use a proxy within Docker, point it to the ports you set below.
# Do _not_ use IP:PORT in HTTP(S)_BIND or HTTP(S)_PORT
# IMPORTANT: Do not use port 8081, 9081 or 65510!
# Example: HTTP_BIND=1.2.3.4
# For IPv6 see https://mailcow.github.io/mailcow-dockerized-docs/firststeps-ip_bindings/
HTTP_PORT=80
HTTP_BIND=
HTTPS_PORT=443
HTTPS_BIND=
# ------------------------------
# Other bindings
# ------------------------------
# You should leave that alone
# Format: 11.22.33.44:25 or 12.34.56.78:465 etc.
SMTP_PORT=25
SMTPS_PORT=465
SUBMISSION_PORT=587
IMAP_PORT=143
IMAPS_PORT=993
POP_PORT=110
POPS_PORT=995
SIEVE_PORT=4190
DOVEADM_PORT=127.0.0.1:19991
SQL_PORT=127.0.0.1:13306
SOLR_PORT=127.0.0.1:18983
REDIS_PORT=127.0.0.1:7654
XMPP_C2S_PORT=5222
XMPP_S2S_PORT=5269
XMPP_HTTPS_PORT=5443
# Your timezone
# See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a list of timezones
# Use the row named 'TZ database name' + pay attention for 'Notes' row
TZ=${MAILCOW_TZ}
# Fixed project name
# Please use lowercase letters only
COMPOSE_PROJECT_NAME=mailcowdockerized
# Set this to "allow" to enable the anyone pseudo user. Disabled by default.
# When enabled, ACL can be created, that apply to "All authenticated users"
# This should probably only be activated on mail hosts, that are used exclusivly by one organisation.
# Otherwise a user might share data with too many other users.
ACL_ANYONE=disallow
# Garbage collector cleanup
# Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring
# How long should objects remain in the garbage until they are being deleted? (value in minutes)
# Check interval is hourly
MAILDIR_GC_TIME=7200
# Additional SAN for the certificate
#
# You can use wildcard records to create specific names for every domain you add to mailcow.
# Example: Add domains "example.com" and "example.net" to mailcow, change ADDITIONAL_SAN to a value like:
#ADDITIONAL_SAN=imap.*,smtp.*
# This will expand the certificate to "imap.example.com", "smtp.example.com", "imap.example.net", "imap.example.net"
# plus every domain you add in the future.
#
# You can also just add static names...
#ADDITIONAL_SAN=srv1.example.net
# ...or combine wildcard and static names:
#ADDITIONAL_SAN=imap.*,srv1.example.com
#
ADDITIONAL_SAN=
# Additional server names for mailcow UI
#
# Specify alternative addresses for the mailcow UI to respond to
# This is useful when you set mail.* as ADDITIONAL_SAN and want to make sure mail.maildomain.com will always point to the mailcow UI.
# If the server name does not match a known site, Nginx decides by best-guess and may redirect users to the wrong web root.
# You can understand this as server_name directive in Nginx.
# Comma separated list without spaces! Example: ADDITIONAL_SERVER_NAMES=a.b.c,d.e.f
ADDITIONAL_SERVER_NAMES=
# Skip running ACME (acme-mailcow, Let's Encrypt certs) - y/n
SKIP_LETS_ENCRYPT=n
# Create seperate certificates for all domains - y/n
# this will allow adding more than 100 domains, but some email clients will not be able to connect with alternative hostnames
# see https://wiki.dovecot.org/SSL/SNIClientSupport
ENABLE_SSL_SNI=n
# Skip IPv4 check in ACME container - y/n
SKIP_IP_CHECK=n
# Skip HTTP verification in ACME container - y/n
SKIP_HTTP_VERIFICATION=n
# Skip ClamAV (clamd-mailcow) anti-virus (Rspamd will auto-detect a missing ClamAV container) - y/n
SKIP_CLAMD=${SKIP_CLAMD}
# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n
SKIP_SOGO=n
# Skip Solr on low-memory systems or if you do not want to store a readable index of your mails in solr-vol-1.
SKIP_SOLR=${SKIP_SOLR}
# Solr heap size in MB, there is no recommendation, please see Solr docs.
# Solr is a prone to run OOM and should be monitored. Unmonitored Solr setups are not recommended.
SOLR_HEAP=1024
# Allow admins to log into SOGo as email user (without any password)
ALLOW_ADMIN_EMAIL_LOGIN=n
# Enable watchdog (watchdog-mailcow) to restart unhealthy containers
USE_WATCHDOG=y
# Send watchdog notifications by mail (sent from watchdog@MAILCOW_HOSTNAME)
# CAUTION:
# 1. You should use external recipients
# 2. Mails are sent unsigned (no DKIM)
# 3. If you use DMARC, create a separate DMARC policy ("v=DMARC1; p=none;" in _dmarc.MAILCOW_HOSTNAME)
# Multiple rcpts allowed, NO quotation marks, NO spaces
#WATCHDOG_NOTIFY_EMAIL=a@example.com,b@example.com,c@example.com
#WATCHDOG_NOTIFY_EMAIL=
# Notify about banned IP (includes whois lookup)
WATCHDOG_NOTIFY_BAN=n
+# Subject for watchdog mails. Defaults to "Watchdog ALERT" followed by the error message.
+#WATCHDOG_SUBJECT=
+
# Checks if mailcow is an open relay. Requires a SAL. More checks will follow.
# https://www.servercow.de/mailcow?lang=en
# https://www.servercow.de/mailcow?lang=de
# No data is collected. Opt-in and anonymous.
# Will only work with unmodified mailcow setups.
WATCHDOG_EXTERNAL_CHECKS=n
# Max log lines per service to keep in Redis logs
LOG_LINES=9999
# Internal IPv4 /24 subnet, format n.n.n (expands to n.n.n.0/24)
# Use private IPv4 addresses only, see https://en.wikipedia.org/wiki/Private_network#Private_IPv4_addresses
IPV4_NETWORK=172.22.1
# Internal IPv6 subnet in fc00::/7
# Use private IPv6 addresses only, see https://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses
IPV6_NETWORK=fd4d:6169:6c63:6f77::/64
# Use this IPv4 for outgoing connections (SNAT)
#SNAT_TO_SOURCE=
# Use this IPv6 for outgoing connections (SNAT)
#SNAT6_TO_SOURCE=
# Create or override an API key for the web UI
# You _must_ define API_ALLOW_FROM, which is a comma separated list of IPs
# An API key defined as API_KEY has read-write access
# An API key defined as API_KEY_READ_ONLY has read-only access
# Allowed chars for API_KEY and API_KEY_READ_ONLY: a-z, A-Z, 0-9, -
# You can define API_KEY and/or API_KEY_READ_ONLY
#API_KEY=
#API_KEY_READ_ONLY=
#API_ALLOW_FROM=172.22.1.1,127.0.0.1
# mail_home is ~/Maildir
MAILDIR_SUB=Maildir
# SOGo session timeout in minutes
SOGO_EXPIRE_SESSION=480
# DOVECOT_MASTER_USER and DOVECOT_MASTER_PASS must both be provided. No special chars.
# Empty by default to auto-generate master user and password on start.
# User expands to DOVECOT_MASTER_USER@mailcow.local
# LEAVE EMPTY IF UNSURE
DOVECOT_MASTER_USER=
# LEAVE EMPTY IF UNSURE
DOVECOT_MASTER_PASS=
EOF
mkdir -p data/assets/ssl
chmod 600 mailcow.conf
# copy but don't overwrite existing certificate
echo "Generating snake-oil certificate..."
# Making Willich more popular
openssl req -x509 -newkey rsa:4096 -keyout data/assets/ssl-example/key.pem -out data/assets/ssl-example/cert.pem -days 365 -subj "/C=DE/ST=NRW/L=Willich/O=mailcow/OU=mailcow/CN=${MAILCOW_HOSTNAME}" -sha256 -nodes
echo "Copying snake-oil certificate..."
cp -n -d data/assets/ssl-example/*.pem data/assets/ssl/
diff --git a/update.sh b/update.sh
index 138e01f3..d5114c9f 100755
--- a/update.sh
+++ b/update.sh
@@ -1,635 +1,642 @@
#!/usr/bin/env bash
# Check permissions
if [ "$(id -u)" -ne "0" ]; then
echo "You need to be root"
exit 1
fi
if [[ "$(uname -r)" =~ ^4\.15\.0-60 ]]; then
echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!";
echo "Please update to 5.x or use another distribution."
exit 1
fi
if [[ "$(uname -r)" =~ ^4\.4\. ]]; then
if grep -q Ubuntu <<< $(uname -a); then
echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"
echo "Please update to linux-generic-hwe-16.04 by running \"apt-get install --install-recommends linux-generic-hwe-16.04\""
exit 1
fi
echo "mailcow on a 4.4.x kernel is not supported. It may or may not work, please upgrade your kernel or continue at your own risk."
read -p "Press any key to continue..." < /dev/tty
fi
# Exit on error and pipefail
set -o pipefail
# Setting high dc timeout
export COMPOSE_HTTP_TIMEOUT=600
# Add /opt/bin to PATH
PATH=$PATH:/opt/bin
umask 0022
for bin in curl docker-compose docker git awk sha1sum; do
if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
done
export LC_ALL=C
DATE=$(date +%Y-%m-%d_%H_%M_%S)
BRANCH=$(git rev-parse --abbrev-ref HEAD)
check_online_status() {
CHECK_ONLINE_IPS=(1.1.1.1 9.9.9.9 8.8.8.8)
for ip in "${CHECK_ONLINE_IPS[@]}"; do
if timeout 3 ping -c 1 ${ip} > /dev/null; then
return 0
fi
done
return 1
}
prefetch_images() {
[[ -z ${BRANCH} ]] && { echo -e "\e[33m\nUnknown branch...\e[0m"; exit 1; }
git fetch origin #${BRANCH}
while read image; do
RET_C=0
until docker pull ${image}; do
RET_C=$((RET_C + 1))
echo -e "\e[33m\nError pulling $image, retrying...\e[0m"
[ ${RET_C} -gt 3 ] && { echo -e "\e[31m\nToo many failed retries, exiting\e[0m"; exit 1; }
sleep 1
done
done < <(git show origin/${BRANCH}:docker-compose.yml | grep "image:" | awk '{ gsub("image:","", $3); print $2 }')
}
docker_garbage() {
IMGS_TO_DELETE=()
for container in $(grep -oP "image: \Kmailcow.+" docker-compose.yml); do
REPOSITORY=${container/:*}
TAG=${container/*:}
V_MAIN=${container/*.}
V_SUB=${container/*.}
EXISTING_TAGS=$(docker images | grep ${REPOSITORY} | awk '{ print $2 }')
for existing_tag in ${EXISTING_TAGS[@]}; do
V_MAIN_EXISTING=${existing_tag/*.}
V_SUB_EXISTING=${existing_tag/*.}
# Not an integer
[[ ! $V_MAIN_EXISTING =~ ^[0-9]+$ ]] && continue
[[ ! $V_SUB_EXISTING =~ ^[0-9]+$ ]] && continue
if [[ $V_MAIN_EXISTING == "latest" ]]; then
echo "Found deprecated label \"latest\" for repository $REPOSITORY, it should be deleted."
IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)
elif [[ $V_MAIN_EXISTING -lt $V_MAIN ]]; then
echo "Found tag $existing_tag for $REPOSITORY, which is older than the current tag $TAG and should be deleted."
IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)
elif [[ $V_SUB_EXISTING -lt $V_SUB ]]; then
echo "Found tag $existing_tag for $REPOSITORY, which is older than the current tag $TAG and should be deleted."
IMGS_TO_DELETE+=($REPOSITORY:$existing_tag)
fi
done
done
if [[ ! -z ${IMGS_TO_DELETE[*]} ]]; then
echo "Run the following command to delete unused image tags:"
echo
echo " docker rmi ${IMGS_TO_DELETE[*]}"
echo
if [ ! $FORCE ]; then
read -r -p "Do you want to delete old image tags right now? [y/N] " response
if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
docker rmi ${IMGS_TO_DELETE[*]}
else
echo "OK, skipped."
fi
else
echo "Skipped image removal because of force mode."
fi
fi
echo -e "\e[32mFurther cleanup...\e[0m"
echo "If you want to cleanup further garbage collected by Docker, please make sure all containers are up and running before cleaning your system by executing \"docker system prune\""
}
while (($#)); do
case "${1}" in
--check|-c)
echo "Checking remote code for updates..."
LATEST_REV=$(git ls-remote --exit-code --refs --quiet https://github.com/mailcow/mailcow-dockerized ${BRANCH} | cut -f1)
if [ $? -ne 0 ]; then
echo "A problem occurred while trying to fetch the latest revision from github."
exit 99
fi
if [[ -z $(git log HEAD --pretty=format:"%H" | grep "${LATEST_REV}") ]]; then
echo "Updated code is available."
git log --date=short --pretty=format:"%ad - %s" $(git rev-parse --short HEAD)..origin/master
exit 0
else
echo "No updates available."
exit 3
fi
;;
--ours)
MERGE_STRATEGY=ours
;;
--skip-start)
SKIP_START=y
;;
--gc)
echo -e "\e[32mCollecting garbage...\e[0m"
docker_garbage
exit 0
;;
--prefetch)
echo -e "\e[32mPrefetching images...\e[0m"
prefetch_images
exit 0
;;
-f|--force)
echo -e "\e[32mForcing Update...\e[0m"
FORCE=y
;;
--no-update-compose)
NO_UPDATE_COMPOSE=y
;;
--help|-h)
echo './update.sh [-c|--check, --ours, --gc, --no-update-compose, --prefetch, --skip-start, -f|--force, -h|--help]
-c|--check - Check for updates and exit (exit codes => 0: update available, 3: no updates)
--ours - Use merge strategy option "ours" to solve conflicts in favor of non-mailcow code (local changes over remote changes), not recommended!
--gc - Run garbage collector to delete old image tags
--no-update-compose - Do not update docker-compose
--prefetch - Only prefetch new images and exit (useful to prepare updates)
--skip-start - Do not start mailcow after update
-f|--force - Force update, do not ask questions
'
exit 1
esac
shift
done
[[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing"; exit 1;}
chmod 600 mailcow.conf
source mailcow.conf
DOTS=${MAILCOW_HOSTNAME//[^.]};
if [ ${#DOTS} -lt 2 ]; then
echo "MAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) is not a FQDN!"
echo "Please change it to a FQDN and run docker-compose down followed by docker-compose up -d"
exit 1
fi
if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\""; exit 1; fi
if cp --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox cp detected, please install coreutils, \"apk add --no-cache --upgrade coreutils\""; exit 1; fi
if sed --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox sed detected, please install gnu sed, \"apk add --no-cache --upgrade sed\""; exit 1; fi
CONFIG_ARRAY=(
"SKIP_LETS_ENCRYPT"
"SKIP_SOGO"
"USE_WATCHDOG"
"WATCHDOG_NOTIFY_EMAIL"
"WATCHDOG_NOTIFY_BAN"
"WATCHDOG_EXTERNAL_CHECKS"
+ "WATCHDOG_SUBJECT"
"SKIP_CLAMD"
"SKIP_IP_CHECK"
"ADDITIONAL_SAN"
"DOVEADM_PORT"
"IPV4_NETWORK"
"IPV6_NETWORK"
"LOG_LINES"
"SNAT_TO_SOURCE"
"SNAT6_TO_SOURCE"
"COMPOSE_PROJECT_NAME"
"SQL_PORT"
"API_KEY"
"API_KEY_READ_ONLY"
"API_ALLOW_FROM"
"MAILDIR_GC_TIME"
"MAILDIR_SUB"
"ACL_ANYONE"
"SOLR_HEAP"
"SKIP_SOLR"
"ENABLE_SSL_SNI"
"ALLOW_ADMIN_EMAIL_LOGIN"
"SKIP_HTTP_VERIFICATION"
"SOGO_EXPIRE_SESSION"
"REDIS_PORT"
"DOVECOT_MASTER_USER"
"DOVECOT_MASTER_PASS"
"MAILCOW_PASS_SCHEME"
"XMPP_C2S_PORT"
"XMPP_S2S_PORT"
"XMPP_HTTPS_PORT"
"ADDITIONAL_SERVER_NAMES"
)
sed -i --follow-symlinks '$a\' mailcow.conf
for option in ${CONFIG_ARRAY[@]}; do
if [[ ${option} == "ADDITIONAL_SAN" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo "${option}=" >> mailcow.conf
fi
elif [[ ${option} == "COMPOSE_PROJECT_NAME" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo "COMPOSE_PROJECT_NAME=mailcowdockerized" >> mailcow.conf
fi
elif [[ ${option} == "DOVEADM_PORT" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo "DOVEADM_PORT=127.0.0.1:19991" >> mailcow.conf
fi
elif [[ ${option} == "WATCHDOG_NOTIFY_EMAIL" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo "WATCHDOG_NOTIFY_EMAIL=" >> mailcow.conf
fi
elif [[ ${option} == "LOG_LINES" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Max log lines per service to keep in Redis logs' >> mailcow.conf
echo "LOG_LINES=9999" >> mailcow.conf
fi
elif [[ ${option} == "IPV4_NETWORK" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Internal IPv4 /24 subnet, format n.n.n. (expands to n.n.n.0/24)' >> mailcow.conf
echo "IPV4_NETWORK=172.22.1" >> mailcow.conf
fi
elif [[ ${option} == "IPV6_NETWORK" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Internal IPv6 subnet in fc00::/7' >> mailcow.conf
echo "IPV6_NETWORK=fd4d:6169:6c63:6f77::/64" >> mailcow.conf
fi
elif [[ ${option} == "SQL_PORT" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Bind SQL to 127.0.0.1 on port 13306' >> mailcow.conf
echo "SQL_PORT=127.0.0.1:13306" >> mailcow.conf
fi
elif [[ ${option} == "API_KEY" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Create or override API key for web UI' >> mailcow.conf
echo "#API_KEY=" >> mailcow.conf
fi
elif [[ ${option} == "API_KEY_READ_ONLY" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Create or override read-only API key for web UI' >> mailcow.conf
echo "#API_KEY_READ_ONLY=" >> mailcow.conf
fi
elif [[ ${option} == "API_ALLOW_FROM" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Must be set for API_KEY to be active' >> mailcow.conf
echo '# IPs only, no networks (networks can be set via UI)' >> mailcow.conf
echo "#API_ALLOW_FROM=" >> mailcow.conf
fi
elif [[ ${option} == "SNAT_TO_SOURCE" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Use this IPv4 for outgoing connections (SNAT)' >> mailcow.conf
echo "#SNAT_TO_SOURCE=" >> mailcow.conf
fi
elif [[ ${option} == "SNAT6_TO_SOURCE" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Use this IPv6 for outgoing connections (SNAT)' >> mailcow.conf
echo "#SNAT6_TO_SOURCE=" >> mailcow.conf
fi
elif [[ ${option} == "MAILDIR_GC_TIME" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Garbage collector cleanup' >> mailcow.conf
echo '# Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring' >> mailcow.conf
echo '# How long should objects remain in the garbage until they are being deleted? (value in minutes)' >> mailcow.conf
echo '# Check interval is hourly' >> mailcow.conf
echo 'MAILDIR_GC_TIME=1440' >> mailcow.conf
fi
elif [[ ${option} == "ACL_ANYONE" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Set this to "allow" to enable the anyone pseudo user. Disabled by default.' >> mailcow.conf
echo '# When enabled, ACL can be created, that apply to "All authenticated users"' >> mailcow.conf
echo '# This should probably only be activated on mail hosts, that are used exclusivly by one organisation.' >> mailcow.conf
echo '# Otherwise a user might share data with too many other users.' >> mailcow.conf
echo 'ACL_ANYONE=disallow' >> mailcow.conf
fi
elif [[ ${option} == "SOLR_HEAP" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Solr heap size, there is no recommendation, please see Solr docs.' >> mailcow.conf
echo '# Solr is a prone to run OOM on large systems and should be monitored. Unmonitored Solr setups are not recommended.' >> mailcow.conf
echo '# Solr will refuse to start with total system memory below or equal to 2 GB.' >> mailcow.conf
echo "SOLR_HEAP=1024" >> mailcow.conf
fi
elif [[ ${option} == "SKIP_SOLR" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Solr is disabled by default after upgrading from non-Solr to Solr-enabled mailcows.' >> mailcow.conf
echo '# Disable Solr or if you do not want to store a readable index of your mails in solr-vol-1.' >> mailcow.conf
echo "SKIP_SOLR=y" >> mailcow.conf
fi
elif [[ ${option} == "ENABLE_SSL_SNI" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Create seperate certificates for all domains - y/n' >> mailcow.conf
echo '# this will allow adding more than 100 domains, but some email clients will not be able to connect with alternative hostnames' >> mailcow.conf
echo '# see https://wiki.dovecot.org/SSL/SNIClientSupport' >> mailcow.conf
echo "ENABLE_SSL_SNI=n" >> mailcow.conf
fi
elif [[ ${option} == "SKIP_SOGO" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n' >> mailcow.conf
echo "SKIP_SOGO=n" >> mailcow.conf
fi
elif [[ ${option} == "MAILDIR_SUB" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# MAILDIR_SUB defines a path in a users virtual home to keep the maildir in. Leave empty for updated setups.' >> mailcow.conf
echo "#MAILDIR_SUB=Maildir" >> mailcow.conf
echo "MAILDIR_SUB=" >> mailcow.conf
fi
elif [[ ${option} == "WATCHDOG_NOTIFY_BAN" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Notify about banned IP. Includes whois lookup.' >> mailcow.conf
echo "WATCHDOG_NOTIFY_BAN=y" >> mailcow.conf
fi
+ elif [[ ${option} == "WATCHDOG_SUBJECT" ]]; then
+ if ! grep -q ${option} mailcow.conf; then
+ echo "Adding new option \"${option}\" to mailcow.conf"
+ echo '# Subject for watchdog mails. Defaults to "Watchdog ALERT" followed by the error message.' >> mailcow.conf
+ echo "#WATCHDOG_SUBJECT=" >> mailcow.conf
+ fi
elif [[ ${option} == "WATCHDOG_EXTERNAL_CHECKS" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Checks if mailcow is an open relay. Requires a SAL. More checks will follow.' >> mailcow.conf
echo '# No data is collected. Opt-in and anonymous.' >> mailcow.conf
echo '# Will only work with unmodified mailcow setups.' >> mailcow.conf
echo "WATCHDOG_EXTERNAL_CHECKS=n" >> mailcow.conf
fi
elif [[ ${option} == "SOGO_EXPIRE_SESSION" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# SOGo session timeout in minutes' >> mailcow.conf
echo "SOGO_EXPIRE_SESSION=480" >> mailcow.conf
fi
elif [[ ${option} == "REDIS_PORT" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo "REDIS_PORT=127.0.0.1:7654" >> mailcow.conf
fi
elif [[ ${option} == "DOVECOT_MASTER_USER" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# DOVECOT_MASTER_USER and _PASS must _both_ be provided. No special chars.' >> mailcow.conf
echo '# Empty by default to auto-generate master user and password on start.' >> mailcow.conf
echo '# User expands to DOVECOT_MASTER_USER@mailcow.local' >> mailcow.conf
echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf
echo "DOVECOT_MASTER_USER=" >> mailcow.conf
fi
elif [[ ${option} == "DOVECOT_MASTER_PASS" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf
echo "DOVECOT_MASTER_PASS=" >> mailcow.conf
fi
elif [[ ${option} == "MAILCOW_PASS_SCHEME" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Password hash algorithm' >> mailcow.conf
echo '# Only certain password hash algorithm are supported. For a fully list of supported schemes,' >> mailcow.conf
echo '# see https://mailcow.github.io/mailcow-dockerized-docs/model-passwd/' >> mailcow.conf
echo "MAILCOW_PASS_SCHEME=BLF-CRYPT" >> mailcow.conf
fi
elif [[ ${option} == "XMPP_C2S_PORT" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "XMPP_C2S_PORT=5222" >> mailcow.conf
fi
elif [[ ${option} == "XMPP_S2S_PORT" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "XMPP_S2S_PORT=5269" >> mailcow.conf
fi
elif [[ ${option} == "ADDITIONAL_SERVER_NAMES" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo '# Additional server names for mailcow UI' >> mailcow.conf
echo '#' >> mailcow.conf
echo '# Specify alternative addresses for the mailcow UI to respond to' >> mailcow.conf
echo '# This is useful when you set mail.* as ADDITIONAL_SAN and want to make sure mail.maildomain.com will always point to the mailcow UI.' >> mailcow.conf
echo '# If the server name does not match a known site, Nginx decides by best-guess and may redirect users to the wrong web root.' >> mailcow.conf
echo '# You can understand this as server_name directive in Nginx.' >> mailcow.conf
echo '# Comma separated list without spaces! Example: ADDITIONAL_SERVER_NAMES=a.b.c,d.e.f' >> mailcow.conf
echo 'ADDITIONAL_SERVER_NAMES=' >> mailcow.conf
fi
elif [[ ${option} == "XMPP_HTTPS_PORT" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "XMPP_HTTPS_PORT=5443" >> mailcow.conf
fi
elif ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo "${option}=n" >> mailcow.conf
fi
done
echo -en "Checking internet connection... "
if ! check_online_status; then
echo -e "\e[31mfailed\e[0m"
exit 1
else
echo -e "\e[32mOK\e[0m"
fi
echo -e "\e[32mChecking for newer update script...\e[0m"
SHA1_1=$(sha1sum update.sh)
git fetch origin #${BRANCH}
git checkout origin/${BRANCH} update.sh
SHA1_2=$(sha1sum update.sh)
if [[ ${SHA1_1} != ${SHA1_2} ]]; then
echo "update.sh changed, please run this script again, exiting."
chmod +x update.sh
exit 2
fi
if [[ -f mailcow.conf ]]; then
source mailcow.conf
else
echo -e "\e[31mNo mailcow.conf - is mailcow installed?\e[0m"
exit 1
fi
if [ ! $FORCE ]; then
read -r -p "Are you sure you want to update mailcow: dockerized? All containers will be stopped. [y/N] " response
if [[ ! "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "OK, exiting."
exit 0
fi
fi
echo -e "\e[32mValidating docker-compose stack configuration...\e[0m"
if ! docker-compose config -q; then
echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m"
exit 1
fi
echo -e "\e[32mChecking for conflicting bridges...\e[0m"
MAILCOW_BRIDGE=$(docker-compose config | grep -i com.docker.network.bridge.name | cut -d':' -f2)
while read NAT_ID; do
iptables -t nat -D POSTROUTING $NAT_ID
done < <(iptables -L -vn -t nat --line-numbers | grep $IPV4_NETWORK | grep -E 'MASQUERADE.*all' | grep -v ${MAILCOW_BRIDGE} | cut -d' ' -f1)
DIFF_DIRECTORY=update_diffs
DIFF_FILE=${DIFF_DIRECTORY}/diff_before_update_$(date +"%Y-%m-%d-%H-%M-%S")
mv diff_before_update* ${DIFF_DIRECTORY}/ 2> /dev/null
if ! git diff-index --quiet HEAD; then
echo -e "\e[32mSaving diff to ${DIFF_FILE}...\e[0m"
mkdir -p ${DIFF_DIRECTORY}
git diff --stat > ${DIFF_FILE}
git diff >> ${DIFF_FILE}
fi
echo -e "\e[32mPrefetching images...\e[0m"
prefetch_images
echo -e "\e[32mStopping mailcow...\e[0m"
sleep 2
MAILCOW_CONTAINERS=($(docker-compose ps -q))
docker-compose down
echo -e "\e[32mChecking for remaining containers...\e[0m"
sleep 2
for container in "${MAILCOW_CONTAINERS[@]}"; do
docker rm -f "$container" 2> /dev/null
done
[[ -f data/conf/nginx/ejabberd.conf ]] && mv data/conf/nginx/ejabberd.conf data/conf/nginx/ZZZ-ejabberd.conf
# Silently fixing remote url from andryyy to mailcow
git remote set-url origin https://github.com/mailcow/mailcow-dockerized
echo -e "\e[32mCommitting current status...\e[0m"
[[ -z "$(git config user.name)" ]] && git config user.name moo
[[ -z "$(git config user.email)" ]] && git config user.email moo@cow.moo
[[ ! -z $(git ls-files data/conf/rspamd/override.d/worker-controller-password.inc) ]] && git rm data/conf/rspamd/override.d/worker-controller-password.inc
git add -u
git commit -am "Before update on ${DATE}" > /dev/null
echo -e "\e[32mFetching updated code from remote...\e[0m"
git fetch origin #${BRANCH}
echo -e "\e[32mMerging local with remote code (recursive, strategy: \"${MERGE_STRATEGY:-theirs}\", options: \"patience\"...\e[0m"
git config merge.defaultToUpstream true
git merge -X${MERGE_STRATEGY:-theirs} -Xpatience -m "After update on ${DATE}"
# Need to use a variable to not pass return codes of if checks
MERGE_RETURN=$?
if [[ ${MERGE_RETURN} == 128 ]]; then
echo -e "\e[31m\nOh no, what happened?\n=> You most likely added files to your local mailcow instance that were now added to the official mailcow repository. Please move them to another location before updating mailcow.\e[0m"
exit 1
elif [[ ${MERGE_RETURN} == 1 ]]; then
echo -e "\e[93mPotenial conflict, trying to fix...\e[0m"
git status --porcelain | grep -E "UD|DU" | awk '{print $2}' | xargs rm -v
git add -A
git commit -m "After update on ${DATE}" > /dev/null
git checkout .
echo -e "\e[32mRemoved and recreated files if necessary.\e[0m"
elif [[ ${MERGE_RETURN} != 0 ]]; then
echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m"
echo
echo "Run docker-compose up -d to restart your stack without updates or try again after fixing the mentioned errors."
exit 1
fi
if [[ ${NO_UPDATE_COMPOSE} == "y" ]]; then
echo -e "\e[33mNot fetching latest docker-compose, please check for updates manually!\e[0m"
elif [[ -e /etc/alpine-release ]]; then
echo -e "\e[33mNot fetching latest docker-compose, because you are using Alpine Linux without glibc support. Please update docker-compose via apk!\e[0m"
else
echo -e "\e[32mFetching new docker-compose version...\e[0m"
echo -e "\e[32mTrying to determine GLIBC version...\e[0m"
if ldd --version > /dev/null; then
GLIBC_V=$(ldd --version | grep -E '(GLIBC|GNU libc)' | rev | cut -d ' ' -f1 | rev | cut -d '.' -f2)
if [ ! -z "${GLIBC_V}" ] && [ ${GLIBC_V} -gt 27 ]; then
DC_DL_SUFFIX=
else
DC_DL_SUFFIX=legacy
fi
else
DC_DL_SUFFIX=legacy
fi
sleep 1
if [[ ! -z $(which pip) && $(pip list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 ]]; then
true
#prevent breaking a working docker-compose installed with pip
elif [[ $(curl -sL -w "%{http_code}" https://www.servercow.de/docker-compose/latest.php?vers=${DC_DL_SUFFIX} -o /dev/null) == "200" ]]; then
LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php)
COMPOSE_VERSION=$(docker-compose version --short)
if [[ "$LATEST_COMPOSE" != "$COMPOSE_VERSION" ]]; then
COMPOSE_PATH=$(which docker-compose)
if [[ -w ${COMPOSE_PATH} ]]; then
curl -#L https://github.com/docker/compose/releases/download/${LATEST_COMPOSE}/docker-compose-$(uname -s)-$(uname -m) > $COMPOSE_PATH
chmod +x $COMPOSE_PATH
else
echo -e "\e[33mWARNING: $COMPOSE_PATH is not writable, but new version $LATEST_COMPOSE is available (installed: $COMPOSE_VERSION)\e[0m"
fi
fi
else
echo -e "\e[33mCannot determine latest docker-compose version, skipping...\e[0m"
fi
fi
echo -e "\e[32mFetching new images, if any...\e[0m"
sleep 2
docker-compose pull
# Fix missing SSL, does not overwrite existing files
[[ ! -d data/assets/ssl ]] && mkdir -p data/assets/ssl
cp -n -d data/assets/ssl-example/*.pem data/assets/ssl/
echo -e "Checking IPv6 settings... "
if grep -q 'SYSCTL_IPV6_DISABLED=1' mailcow.conf; then
echo
echo '!! IMPORTANT !!'
echo
echo 'SYSCTL_IPV6_DISABLED was removed due to complications. IPv6 can be disabled by editing "docker-compose.yml" and setting "enable_ipv6: true" to "enable_ipv6: false".'
echo 'This setting will only be active after a complete shutdown of mailcow by running "docker-compose down" followed by "docker-compose up -d".'
echo
echo '!! IMPORTANT !!'
echo
read -p "Press any key to continue..." < /dev/tty
fi
# Checking for old project name bug
sed -i --follow-symlinks 's#COMPOSEPROJECT_NAME#COMPOSE_PROJECT_NAME#g' mailcow.conf
# Checking old, wrong bindings
sed -i --follow-symlinks 's/HTTP_BIND=0.0.0.0/HTTP_BIND=/g' mailcow.conf
sed -i --follow-symlinks 's/HTTPS_BIND=0.0.0.0/HTTPS_BIND=/g' mailcow.conf
# Fix Rspamd maps
if [ -f data/conf/rspamd/custom/global_from_blacklist.map ]; then
mv data/conf/rspamd/custom/global_from_blacklist.map data/conf/rspamd/custom/global_smtp_from_blacklist.map
fi
if [ -f data/conf/rspamd/custom/global_from_whitelist.map ]; then
mv data/conf/rspamd/custom/global_from_whitelist.map data/conf/rspamd/custom/global_smtp_from_whitelist.map
fi
# Fix deprecated metrics.conf
if [ -f "data/conf/rspamd/local.d/metrics.conf" ]; then
if [ ! -z "$(git diff --name-only origin/master data/conf/rspamd/local.d/metrics.conf)" ]; then
echo -e "\e[33mWARNING\e[0m - Please migrate your customizations of data/conf/rspamd/local.d/metrics.conf to actions.conf and groups.conf after this update."
echo "The deprecated configuration file metrics.conf will be moved to metrics.conf_deprecated after updating mailcow."
fi
mv data/conf/rspamd/local.d/metrics.conf data/conf/rspamd/local.d/metrics.conf_deprecated
fi
if [[ ${SKIP_START} == "y" ]]; then
echo -e "\e[33mNot starting mailcow, please run \"docker-compose up -d --remove-orphans\" to start mailcow.\e[0m"
else
echo -e "\e[32mStarting mailcow...\e[0m"
sleep 2
docker-compose up -d --remove-orphans
fi
echo -e "\e[32mCollecting garbage...\e[0m"
docker_garbage
#echo "In case you encounter any problem, hard-reset to a state before updating mailcow:"
#echo
#git reflog --color=always | grep "Before update on "
#echo
#echo "Use \"git reset --hard hash-on-the-left\" and run docker-compose up -d afterwards."

File Metadata

Mime Type
text/x-diff
Expires
9月 11 Thu, 1:45 PM (13 h, 24 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5576
默认替代文本
(379 KB)

Event Timeline