mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-01-08 14:39:16 +00:00
Merge branch 'staging' into feat/valkey
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
RUN apk upgrade --no-cache \
|
||||
&& apk add --update --no-cache \
|
||||
bash \
|
||||
@@ -15,7 +14,7 @@ RUN apk upgrade --no-cache \
|
||||
tini \
|
||||
tzdata \
|
||||
python3 \
|
||||
acme-tiny --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/
|
||||
acme-tiny
|
||||
|
||||
COPY acme.sh /srv/acme.sh
|
||||
COPY functions.sh /srv/functions.sh
|
||||
|
||||
@@ -138,7 +138,7 @@ log_f "Resolver OK"
|
||||
log_f "Waiting for domain table..."
|
||||
while [[ -z ${DOMAIN_TABLE} ]]; do
|
||||
curl --silent http://nginx.${COMPOSE_PROJECT_NAME}_mailcow-network/ >/dev/null 2>&1
|
||||
DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs)
|
||||
DOMAIN_TABLE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs)
|
||||
[[ -z ${DOMAIN_TABLE} ]] && sleep 10
|
||||
done
|
||||
log_f "OK" no_date
|
||||
@@ -159,18 +159,6 @@ while true; do
|
||||
fi
|
||||
if [[ ! -f ${ACME_BASE}/acme/account.pem ]]; then
|
||||
log_f "Generating missing Lets Encrypt account key..."
|
||||
if [[ ! -z ${ACME_CONTACT} ]]; then
|
||||
if ! verify_email "${ACME_CONTACT}"; then
|
||||
log_f "Invalid email address, will not start registration!"
|
||||
sleep 365d
|
||||
exec $(readlink -f "$0")
|
||||
else
|
||||
ACME_CONTACT_PARAMETER="--contact mailto:${ACME_CONTACT}"
|
||||
log_f "Valid email address, using ${ACME_CONTACT} for registration"
|
||||
fi
|
||||
else
|
||||
ACME_CONTACT_PARAMETER=""
|
||||
fi
|
||||
openssl genrsa 4096 > ${ACME_BASE}/acme/account.pem
|
||||
else
|
||||
log_f "Using existing Lets Encrypt account key ${ACME_BASE}/acme/account.pem"
|
||||
@@ -218,7 +206,7 @@ while true; do
|
||||
|
||||
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
||||
# Fetch certs for autoconfig and autodiscover subdomains
|
||||
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
|
||||
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig' 'mta-sts')
|
||||
fi
|
||||
|
||||
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
|
||||
@@ -231,7 +219,7 @@ while true; do
|
||||
|
||||
#########################################
|
||||
# IP and webroot challenge verification #
|
||||
SQL_DOMAINS=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0 and active=1" -Bs)
|
||||
SQL_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0 and active=1" -Bs)
|
||||
if [[ ! $? -eq 0 ]]; then
|
||||
log_f "Failed to read SQL domains, retrying in 1 minute..."
|
||||
sleep 1m
|
||||
@@ -299,7 +287,7 @@ while true; do
|
||||
VALIDATED_CERTIFICATES+=("${CERT_NAME}")
|
||||
|
||||
# obtain server certificate if required
|
||||
ACME_CONTACT_PARAMETER=${ACME_CONTACT_PARAMETER} DOMAINS=${SERVER_SAN_VALIDATED[@]} /srv/obtain-certificate.sh rsa
|
||||
DOMAINS=${SERVER_SAN_VALIDATED[@]} /srv/obtain-certificate.sh rsa
|
||||
RETURN="$?"
|
||||
if [[ "$RETURN" == "0" ]]; then # 0 = cert created successfully
|
||||
CERT_AMOUNT_CHANGED=1
|
||||
|
||||
@@ -93,8 +93,8 @@ until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
log_f "Resolver OK"
|
||||
log_f "Using command acme-tiny ${DIRECTORY_URL} ${ACME_CONTACT_PARAMETER} --account-key ${ACME_BASE}/acme/account.pem --disable-check --csr ${CSR} --acme-dir /var/www/acme/"
|
||||
ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} ${ACME_CONTACT_PARAMETER} \
|
||||
log_f "Using command acme-tiny ${DIRECTORY_URL} --account-key ${ACME_BASE}/acme/account.pem --disable-check --csr ${CSR} --acme-dir /var/www/acme/"
|
||||
ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} \
|
||||
--account-key ${ACME_BASE}/acme/account.pem \
|
||||
--disable-check \
|
||||
--csr ${CSR} \
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt update && apt install pigz
|
||||
RUN apt update && apt install pigz -y --no-install-recommends
|
||||
@@ -8,7 +8,7 @@ fi
|
||||
|
||||
# Cleaning up garbage
|
||||
echo "Cleaning up tmp files..."
|
||||
rm -rf /var/lib/clamav/clamav-*.tmp
|
||||
rm -rf /var/lib/clamav/tmp.*
|
||||
|
||||
# Prepare whitelist
|
||||
|
||||
|
||||
@@ -11,4 +11,4 @@ if [ "${CLAMAV_NO_CLAMD:-}" != "false" ]; then
|
||||
echo "Clamd is up"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
exit 0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -241,9 +241,9 @@ async def handle_pubsub_messages(channel: aioredis.client.PubSub):
|
||||
else:
|
||||
dockerapi.logger.error("api call: missing container_name, post_action or request")
|
||||
else:
|
||||
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
|
||||
dockerapi.logger.error("Unknown PubSub received - %s" % json.dumps(data_json))
|
||||
else:
|
||||
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
|
||||
dockerapi.logger.error("Unknown PubSub received - %s" % json.dumps(data_json))
|
||||
|
||||
await asyncio.sleep(0.0)
|
||||
except asyncio.TimeoutError:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||
ARG GOSU_VERSION=1.16
|
||||
ARG GOSU_VERSION=1.17
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
@@ -34,9 +34,13 @@ RUN addgroup -g 5000 vmail \
|
||||
lua5.3-sql-mysql \
|
||||
icu-data-full \
|
||||
mariadb-connector-c \
|
||||
lua-sec \
|
||||
mariadb-dev \
|
||||
glib-dev \
|
||||
gcompat \
|
||||
mariadb-client \
|
||||
perl \
|
||||
perl-dev \
|
||||
perl-ntlm \
|
||||
perl-cgi \
|
||||
perl-crypt-openssl-rsa \
|
||||
@@ -65,7 +69,7 @@ RUN addgroup -g 5000 vmail \
|
||||
perl-par-packer \
|
||||
perl-parse-recdescent \
|
||||
perl-lockfile-simple \
|
||||
libproc \
|
||||
libproc2 \
|
||||
perl-readonly \
|
||||
perl-regexp-common \
|
||||
perl-sys-meminfo \
|
||||
|
||||
@@ -15,6 +15,6 @@ if ! [[ ${MAX_AGE} =~ ${NUM_REGEXP} ]] ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TO_DELETE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT COUNT(id) FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY" -BN)
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY"
|
||||
TO_DELETE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT COUNT(id) FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY" -BN)
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY"
|
||||
echo "Deleted ${TO_DELETE} items from quarantine table (max age is ${MAX_AGE//[!0-9]/} days)"
|
||||
|
||||
@@ -28,7 +28,7 @@ ${VALKEY_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
|
||||
|
||||
# Create missing directories
|
||||
[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
|
||||
[[ ! -d /etc/dovecot/lua/ ]] && mkdir -p /etc/dovecot/lua/
|
||||
[[ ! -d /etc/dovecot/auth/ ]] && mkdir -p /etc/dovecot/auth/
|
||||
[[ ! -d /etc/dovecot/conf.d/ ]] && mkdir -p /etc/dovecot/conf.d/
|
||||
[[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage
|
||||
[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
|
||||
@@ -131,123 +131,6 @@ user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format
|
||||
iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
|
||||
EOF
|
||||
|
||||
cat <<EOF > /etc/dovecot/lua/passwd-verify.lua
|
||||
function auth_password_verify(req, pass)
|
||||
|
||||
if req.domain == nil then
|
||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
|
||||
end
|
||||
|
||||
if cur == nil then
|
||||
script_init()
|
||||
end
|
||||
|
||||
if req.user == nil then
|
||||
req.user = ''
|
||||
end
|
||||
|
||||
respbody = {}
|
||||
|
||||
-- check against mailbox passwds
|
||||
local cur,errorString = con:execute(string.format([[SELECT password FROM mailbox
|
||||
WHERE username = '%s'
|
||||
AND active = '1'
|
||||
AND domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')
|
||||
AND IFNULL(JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.force_pw_update')), 0) != '1'
|
||||
AND IFNULL(JSON_UNQUOTE(JSON_VALUE(attributes, '$.%s_access')), 1) = '1']], con:escape(req.user), con:escape(req.domain), con:escape(req.service)))
|
||||
local row = cur:fetch ({}, "a")
|
||||
while row do
|
||||
if req.password_verify(req, row.password, pass) == 1 then
|
||||
con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
|
||||
VALUES ("%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip)))
|
||||
cur:close()
|
||||
con:close()
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
end
|
||||
row = cur:fetch (row, "a")
|
||||
end
|
||||
|
||||
-- check against app passwds for imap and smtp
|
||||
-- app passwords are only available for imap, smtp, sieve and pop3 when using sasl
|
||||
if req.service == "smtp" or req.service == "imap" or req.service == "sieve" or req.service == "pop3" then
|
||||
local cur,errorString = con:execute(string.format([[SELECT app_passwd.id, %s_access AS has_prot_access, app_passwd.password FROM app_passwd
|
||||
INNER JOIN mailbox ON mailbox.username = app_passwd.mailbox
|
||||
WHERE mailbox = '%s'
|
||||
AND app_passwd.active = '1'
|
||||
AND mailbox.active = '1'
|
||||
AND app_passwd.domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.service), con:escape(req.user), con:escape(req.domain)))
|
||||
local row = cur:fetch ({}, "a")
|
||||
while row do
|
||||
if req.password_verify(req, row.password, pass) == 1 then
|
||||
-- if password is valid and protocol access is 1 OR real_rip matches SOGo, proceed
|
||||
if tostring(req.real_rip) == "__IPV4_SOGO__" then
|
||||
cur:close()
|
||||
con:close()
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
elseif row.has_prot_access == "1" then
|
||||
con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
|
||||
VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
|
||||
cur:close()
|
||||
con:close()
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
end
|
||||
end
|
||||
row = cur:fetch (row, "a")
|
||||
end
|
||||
end
|
||||
|
||||
cur:close()
|
||||
con:close()
|
||||
|
||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
|
||||
|
||||
-- PoC
|
||||
-- local reqbody = string.format([[{
|
||||
-- "success":0,
|
||||
-- "service":"%s",
|
||||
-- "app_password":false,
|
||||
-- "username":"%s",
|
||||
-- "real_rip":"%s"
|
||||
-- }]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip))
|
||||
-- http.request {
|
||||
-- method = "POST",
|
||||
-- url = "http://nginx:8081/sasl_log.php",
|
||||
-- source = ltn12.source.string(reqbody),
|
||||
-- headers = {
|
||||
-- ["content-type"] = "application/json",
|
||||
-- ["content-length"] = tostring(#reqbody)
|
||||
-- },
|
||||
-- sink = ltn12.sink.table(respbody)
|
||||
-- }
|
||||
|
||||
end
|
||||
|
||||
function auth_passdb_lookup(req)
|
||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
|
||||
end
|
||||
|
||||
function script_init()
|
||||
mysql = require "luasql.mysql"
|
||||
http = require "socket.http"
|
||||
http.TIMEOUT = 5
|
||||
ltn12 = require "ltn12"
|
||||
env = mysql.mysql()
|
||||
con = env:connect("__DBNAME__","__DBUSER__","__DBPASS__","localhost")
|
||||
return 0
|
||||
end
|
||||
|
||||
function script_deinit()
|
||||
con:close()
|
||||
env:close()
|
||||
end
|
||||
EOF
|
||||
|
||||
# Replace patterns in app-passdb.lua
|
||||
sed -i "s/__DBUSER__/${DBUSER}/g" /etc/dovecot/lua/passwd-verify.lua
|
||||
sed -i "s/__DBPASS__/${DBPASS}/g" /etc/dovecot/lua/passwd-verify.lua
|
||||
sed -i "s/__DBNAME__/${DBNAME}/g" /etc/dovecot/lua/passwd-verify.lua
|
||||
sed -i "s/__IPV4_SOGO__/${IPV4_NETWORK}.248/g" /etc/dovecot/lua/passwd-verify.lua
|
||||
|
||||
|
||||
# Migrate old sieve_after file
|
||||
[[ -f /etc/dovecot/sieve_after ]] && mv /etc/dovecot/sieve_after /etc/dovecot/global_sieve_after
|
||||
@@ -385,8 +268,8 @@ sievec /usr/lib/dovecot/sieve/report-ham.sieve
|
||||
|
||||
# Fix permissions
|
||||
chown root:root /etc/dovecot/sql/*.conf
|
||||
chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua
|
||||
chmod 640 /etc/dovecot/sql/*.conf /etc/dovecot/lua/passwd-verify.lua
|
||||
chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/auth/passwd-verify.lua
|
||||
chmod 640 /etc/dovecot/sql/*.conf /etc/dovecot/auth/passwd-verify.lua
|
||||
chown -R vmail:vmail /var/vmail/sieve
|
||||
chown -R vmail:vmail /var/volatile
|
||||
chown -R vmail:vmail /var/vmail_index
|
||||
@@ -414,15 +297,15 @@ printenv | sed 's/^\(.*\)$/export \1/g' > /source_env.sh
|
||||
|
||||
# Clean stopped imapsync jobs
|
||||
rm -f /tmp/imapsync_busy.lock
|
||||
IMAPSYNC_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs)
|
||||
[[ ! -z ${IMAPSYNC_TABLE} ]] && mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'"
|
||||
IMAPSYNC_TABLE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs)
|
||||
[[ ! -z ${IMAPSYNC_TABLE} ]] && mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'"
|
||||
|
||||
# Envsubst maildir_gc
|
||||
echo "$(envsubst < /usr/local/bin/maildir_gc.sh)" > /usr/local/bin/maildir_gc.sh
|
||||
|
||||
# GUID generation
|
||||
while [[ ${VERSIONS_OK} != 'OK' ]]; do
|
||||
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = \"${DBNAME}\" AND TABLE_NAME = 'versions'") ]]; then
|
||||
if [[ ! -z $(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = \"${DBNAME}\" AND TABLE_NAME = 'versions'") ]]; then
|
||||
VERSIONS_OK=OK
|
||||
else
|
||||
echo "Waiting for versions table to be created..."
|
||||
@@ -433,11 +316,11 @@ PUBKEY_MCRYPT=$(doveconf -P 2> /dev/null | grep -i mail_crypt_global_public_key
|
||||
if [ -f ${PUBKEY_MCRYPT} ]; then
|
||||
GUID=$(cat <(echo ${MAILCOW_HOSTNAME}) /mail_crypt/ecpubkey.pem | sha256sum | cut -d ' ' -f1 | tr -cd "[a-fA-F0-9.:/] ")
|
||||
if [ ${#GUID} -eq 64 ]; then
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
REPLACE INTO versions (application, version) VALUES ("GUID", "${GUID}");
|
||||
EOF
|
||||
else
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
REPLACE INTO versions (application, version) VALUES ("GUID", "INVALID");
|
||||
EOF
|
||||
fi
|
||||
@@ -456,7 +339,7 @@ done
|
||||
|
||||
# For some strange, unknown and stupid reason, Dovecot may run into a race condition, when this file is not touched before it is read by dovecot/auth
|
||||
# May be related to something inside Docker, I seriously don't know
|
||||
touch /etc/dovecot/lua/passwd-verify.lua
|
||||
touch /etc/dovecot/auth/passwd-verify.lua
|
||||
|
||||
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
|
||||
cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf
|
||||
|
||||
@@ -132,8 +132,8 @@ while ($row = $sth->fetchrow_arrayref()) {
|
||||
"--tmpdir", "/tmp",
|
||||
"--nofoldersizes",
|
||||
"--addheader",
|
||||
($timeout1 gt "0" ? () : ('--timeout1', $timeout1)),
|
||||
($timeout2 gt "0" ? () : ('--timeout2', $timeout2)),
|
||||
($timeout1 le "0" ? () : ('--timeout1', $timeout1)),
|
||||
($timeout2 le "0" ? () : ('--timeout2', $timeout2)),
|
||||
($exclude eq "" ? () : ("--exclude", $exclude)),
|
||||
($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
|
||||
($maxage eq "0" ? () : ('--maxage', $maxage)),
|
||||
|
||||
@@ -8,7 +8,8 @@ from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import COMMASPACE, formatdate
|
||||
import jinja2
|
||||
from jinja2 import Template
|
||||
from jinja2 import TemplateError
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
import json
|
||||
import redis
|
||||
import time
|
||||
@@ -75,22 +76,27 @@ try:
|
||||
|
||||
def notify_rcpt(rcpt, msg_count, quarantine_acl, category):
|
||||
if category == "add_header": category = "add header"
|
||||
meta_query = query_mysql('SELECT SHA2(CONCAT(id, qid), 256) AS qhash, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category))
|
||||
meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category))
|
||||
print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count))
|
||||
if len(meta_query) == 0:
|
||||
return
|
||||
msg_count = len(meta_query)
|
||||
env = SandboxedEnvironment()
|
||||
if r.get('Q_HTML'):
|
||||
try:
|
||||
template = Template(r.get('Q_HTML'))
|
||||
except:
|
||||
print("Error: Cannot parse quarantine template, falling back to default template.")
|
||||
with open('/templates/quarantine.tpl') as file_:
|
||||
template = Template(file_.read())
|
||||
try:
|
||||
template = env.from_string(r.get('Q_HTML'))
|
||||
except Exception:
|
||||
print("Error: Cannot parse quarantine template, falling back to default template.")
|
||||
with open('/templates/quarantine.tpl') as file_:
|
||||
template = env.from_string(file_.read())
|
||||
else:
|
||||
with open('/templates/quarantine.tpl') as file_:
|
||||
template = Template(file_.read())
|
||||
html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl)
|
||||
with open('/templates/quarantine.tpl') as file_:
|
||||
template = env.from_string(file_.read())
|
||||
try:
|
||||
html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl)
|
||||
except (jinja2.exceptions.SecurityError, TemplateError) as ex:
|
||||
print(f"SecurityError or TemplateError in template rendering: {ex}")
|
||||
return
|
||||
text = html2text.html2text(html)
|
||||
count = 0
|
||||
while count < 15:
|
||||
|
||||
@@ -6,7 +6,7 @@ from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import COMMASPACE, formatdate
|
||||
import jinja2
|
||||
from jinja2 import Template
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
import redis
|
||||
import time
|
||||
import json
|
||||
@@ -33,16 +33,24 @@ while True:
|
||||
|
||||
if r.get('QW_HTML'):
|
||||
try:
|
||||
template = Template(r.get('QW_HTML'))
|
||||
except:
|
||||
print("Error: Cannot parse quarantine template, falling back to default template.")
|
||||
env = SandboxedEnvironment()
|
||||
template = env.from_string(r.get('QW_HTML'))
|
||||
except Exception:
|
||||
print("Error: Cannot parse quota template, falling back to default template.")
|
||||
with open('/templates/quota.tpl') as file_:
|
||||
template = Template(file_.read())
|
||||
env = SandboxedEnvironment()
|
||||
template = env.from_string(file_.read())
|
||||
else:
|
||||
with open('/templates/quota.tpl') as file_:
|
||||
template = Template(file_.read())
|
||||
env = SandboxedEnvironment()
|
||||
template = env.from_string(file_.read())
|
||||
|
||||
try:
|
||||
html = template.render(username=username, percent=percent)
|
||||
except (jinja2.exceptions.SecurityError, jinja2.TemplateError) as ex:
|
||||
print(f"SecurityError or TemplateError in template rendering: {ex}")
|
||||
sys.exit(1)
|
||||
|
||||
html = template.render(username=username, percent=percent)
|
||||
text = html2text.html2text(html)
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
backend=iptables
|
||||
backend=nftables
|
||||
|
||||
nft list table ip filter &>/dev/null
|
||||
nftables_found=$?
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
DEBUG = False
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
@@ -20,10 +22,13 @@ from modules.Logger import Logger
|
||||
from modules.IPTables import IPTables
|
||||
from modules.NFTables import NFTables
|
||||
|
||||
def logdebug(msg):
|
||||
if DEBUG:
|
||||
logger.logInfo("DEBUG: %s" % msg)
|
||||
|
||||
# globals
|
||||
# Globals
|
||||
WHITELIST = []
|
||||
BLACKLIST= []
|
||||
BLACKLIST = []
|
||||
bans = {}
|
||||
quit_now = False
|
||||
exit_code = 0
|
||||
@@ -33,12 +38,10 @@ r = None
|
||||
pubsub = None
|
||||
clear_before_quit = False
|
||||
|
||||
|
||||
def refreshF2boptions():
|
||||
global f2boptions
|
||||
global quit_now
|
||||
global exit_code
|
||||
|
||||
f2boptions = {}
|
||||
|
||||
if not valkey.get('F2B_OPTIONS'):
|
||||
@@ -61,15 +64,15 @@ def refreshF2boptions():
|
||||
valkey.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
||||
|
||||
def verifyF2boptions(f2boptions):
|
||||
verifyF2boption(f2boptions,'ban_time', 1800)
|
||||
verifyF2boption(f2boptions,'max_ban_time', 10000)
|
||||
verifyF2boption(f2boptions,'ban_time_increment', True)
|
||||
verifyF2boption(f2boptions,'max_attempts', 10)
|
||||
verifyF2boption(f2boptions,'retry_window', 600)
|
||||
verifyF2boption(f2boptions,'netban_ipv4', 32)
|
||||
verifyF2boption(f2boptions,'netban_ipv6', 128)
|
||||
verifyF2boption(f2boptions,'banlist_id', str(uuid.uuid4()))
|
||||
verifyF2boption(f2boptions,'manage_external', 0)
|
||||
verifyF2boption(f2boptions, 'ban_time', 1800)
|
||||
verifyF2boption(f2boptions, 'max_ban_time', 10000)
|
||||
verifyF2boption(f2boptions, 'ban_time_increment', True)
|
||||
verifyF2boption(f2boptions, 'max_attempts', 10)
|
||||
verifyF2boption(f2boptions, 'retry_window', 600)
|
||||
verifyF2boption(f2boptions, 'netban_ipv4', 32)
|
||||
verifyF2boption(f2boptions, 'netban_ipv6', 128)
|
||||
verifyF2boption(f2boptions, 'banlist_id', str(uuid.uuid4()))
|
||||
verifyF2boption(f2boptions, 'manage_external', 0)
|
||||
|
||||
def verifyF2boption(f2boptions, f2boption, f2bdefault):
|
||||
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
|
||||
@@ -111,7 +114,7 @@ def get_ip(address):
|
||||
def ban(address):
|
||||
global f2boptions
|
||||
global lock
|
||||
|
||||
logdebug("ban() called with address=%s" % address)
|
||||
refreshF2boptions()
|
||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||
RETRY_WINDOW = int(f2boptions['retry_window'])
|
||||
@@ -119,31 +122,43 @@ def ban(address):
|
||||
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
|
||||
|
||||
ip = get_ip(address)
|
||||
if not ip: return
|
||||
if not ip:
|
||||
logdebug("No valid IP -- skipping ban()")
|
||||
return
|
||||
address = str(ip)
|
||||
self_network = ipaddress.ip_network(address)
|
||||
|
||||
with lock:
|
||||
temp_whitelist = set(WHITELIST)
|
||||
if temp_whitelist:
|
||||
for wl_key in temp_whitelist:
|
||||
wl_net = ipaddress.ip_network(wl_key, False)
|
||||
if wl_net.overlaps(self_network):
|
||||
logger.logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
|
||||
return
|
||||
logdebug("Checking if %s overlaps with any WHITELIST entries" % self_network)
|
||||
if temp_whitelist:
|
||||
for wl_key in temp_whitelist:
|
||||
wl_net = ipaddress.ip_network(wl_key, False)
|
||||
logdebug("Checking overlap between %s and %s" % (self_network, wl_net))
|
||||
if wl_net.overlaps(self_network):
|
||||
logger.logInfo(
|
||||
'Address %s is allowlisted by rule %s' % (self_network, wl_net))
|
||||
return
|
||||
|
||||
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
|
||||
net = ipaddress.ip_network(
|
||||
(address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
|
||||
net = str(net)
|
||||
logdebug("Ban net: %s" % net)
|
||||
|
||||
if not net in bans:
|
||||
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
|
||||
logdebug("Initing new ban counter for %s" % net)
|
||||
|
||||
current_attempt = time.time()
|
||||
logdebug("Current attempt ts=%s, previous: %s, retry_window: %s" %
|
||||
(current_attempt, bans[net]['last_attempt'], RETRY_WINDOW))
|
||||
if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW:
|
||||
bans[net]['attempts'] = 0
|
||||
logdebug("Ban counter for %s reset as window expired" % net)
|
||||
|
||||
bans[net]['attempts'] += 1
|
||||
bans[net]['last_attempt'] = current_attempt
|
||||
logdebug("%s attempts now %d" % (net, bans[net]['attempts']))
|
||||
|
||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||
cur_time = int(round(time.time()))
|
||||
@@ -151,34 +166,41 @@ def ban(address):
|
||||
logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
|
||||
if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1:
|
||||
with lock:
|
||||
logdebug("Calling tables.banIPv4(%s)" % net)
|
||||
tables.banIPv4(net)
|
||||
elif int(f2boptions['manage_external']) != 1:
|
||||
with lock:
|
||||
logdebug("Calling tables.banIPv6(%s)" % net)
|
||||
tables.banIPv6(net)
|
||||
|
||||
logdebug("Updating F2B_ACTIVE_BANS[%s]=%d" %
|
||||
(net, cur_time + NET_BAN_TIME))
|
||||
valkey.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
|
||||
else:
|
||||
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
||||
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (
|
||||
MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
||||
|
||||
def unban(net):
|
||||
global lock
|
||||
|
||||
logdebug("Calling unban() with net=%s" % net)
|
||||
if not net in bans:
|
||||
logger.logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
|
||||
valkey.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||
return
|
||||
|
||||
logger.logInfo(
|
||||
'%s is not banned, skipping unban and deleting from queue (if any)' % net)
|
||||
valkey.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||
return
|
||||
logger.logInfo('Unbanning %s' % net)
|
||||
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
|
||||
with lock:
|
||||
logdebug("Calling tables.unbanIPv4(%s)" % net)
|
||||
tables.unbanIPv4(net)
|
||||
else:
|
||||
with lock:
|
||||
logdebug("Calling tables.unbanIPv6(%s)" % net)
|
||||
tables.unbanIPv6(net)
|
||||
|
||||
valkey.hdel('F2B_ACTIVE_BANS', '%s' % net)
|
||||
valkey.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||
if net in bans:
|
||||
logdebug("Unban for %s, setting attempts=0, ban_counter+=1" % net)
|
||||
bans[net]['attempts'] = 0
|
||||
bans[net]['ban_counter'] += 1
|
||||
|
||||
@@ -204,17 +226,19 @@ def permBan(net, unban=False):
|
||||
|
||||
if is_unbanned:
|
||||
valkey.hdel('F2B_PERM_BANS', '%s' % net)
|
||||
logger.logCrit('Removed host/network %s from blacklist' % net)
|
||||
logger.logCrit('Removed host/network %s from denylist' % net)
|
||||
elif is_banned:
|
||||
valkey.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
||||
logger.logCrit('Added host/network %s to blacklist' % net)
|
||||
logger.logCrit('Added host/network %s to denylist' % net)
|
||||
|
||||
def clear():
|
||||
global lock
|
||||
logger.logInfo('Clearing all bans')
|
||||
for net in bans.copy():
|
||||
logdebug("Unbanning net: %s" % net)
|
||||
unban(net)
|
||||
with lock:
|
||||
logdebug("Clearing IPv4/IPv6 table")
|
||||
tables.clearIPv4Table()
|
||||
tables.clearIPv6Table()
|
||||
try:
|
||||
@@ -275,21 +299,35 @@ def snat6(snat_target):
|
||||
|
||||
def autopurge():
|
||||
global f2boptions
|
||||
|
||||
logdebug("autopurge thread started")
|
||||
while not quit_now:
|
||||
logdebug("autopurge tick")
|
||||
time.sleep(10)
|
||||
refreshF2boptions()
|
||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||
QUEUE_UNBAN = valkey.hgetall('F2B_QUEUE_UNBAN')
|
||||
logdebug("QUEUE_UNBAN: %s" % QUEUE_UNBAN)
|
||||
if QUEUE_UNBAN:
|
||||
for net in QUEUE_UNBAN:
|
||||
logdebug("Autopurge: unbanning queued net: %s" % net)
|
||||
unban(str(net))
|
||||
for net in bans.copy():
|
||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||
NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter'])
|
||||
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
|
||||
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME:
|
||||
unban(net)
|
||||
# Only check expiry for actively banned IPs:
|
||||
active_bans = r.hgetall('F2B_ACTIVE_BANS')
|
||||
now = time.time()
|
||||
for net_str, expire_str in active_bans.items():
|
||||
logdebug("Checking ban expiry for (actively banned): %s" % net_str)
|
||||
# Defensive: always process if timer missing or expired
|
||||
try:
|
||||
expire = float(expire_str)
|
||||
except Exception:
|
||||
logdebug("Invalid expire time for %s; unbanning" % net_str)
|
||||
unban(net_str)
|
||||
continue
|
||||
time_left = expire - now
|
||||
logdebug("Time left for %s: %.1f seconds" % (net_str, time_left))
|
||||
if time_left <= 0:
|
||||
logdebug("Ban expired for %s" % net_str)
|
||||
unban(net_str)
|
||||
|
||||
def mailcowChainOrder():
|
||||
global lock
|
||||
@@ -359,7 +397,7 @@ def whitelistUpdate():
|
||||
with lock:
|
||||
if Counter(new_whitelist) != Counter(WHITELIST):
|
||||
WHITELIST = new_whitelist
|
||||
logger.logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
|
||||
logger.logInfo('Allowlist was changed, it has %s entries' % len(WHITELIST))
|
||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||
|
||||
def blacklistUpdate():
|
||||
@@ -375,7 +413,7 @@ def blacklistUpdate():
|
||||
addban = set(new_blacklist).difference(BLACKLIST)
|
||||
delban = set(BLACKLIST).difference(new_blacklist)
|
||||
BLACKLIST = new_blacklist
|
||||
logger.logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
|
||||
logger.logInfo('Denylist was changed, it has %s entries' % len(BLACKLIST))
|
||||
if addban:
|
||||
for net in addban:
|
||||
permBan(net=net)
|
||||
@@ -386,42 +424,43 @@ def blacklistUpdate():
|
||||
|
||||
def sigterm_quit(signum, frame):
|
||||
global clear_before_quit
|
||||
logdebug("SIGTERM received, setting clear_before_quit to True and exiting")
|
||||
clear_before_quit = True
|
||||
sys.exit(exit_code)
|
||||
|
||||
def berfore_quit():
|
||||
def before_quit():
|
||||
logdebug("before_quit called, clear_before_quit=%s" % clear_before_quit)
|
||||
if clear_before_quit:
|
||||
clear()
|
||||
if pubsub is not None:
|
||||
pubsub.unsubscribe()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
atexit.register(berfore_quit)
|
||||
logger = Logger()
|
||||
logdebug("Sys.argv: %s" % sys.argv)
|
||||
atexit.register(before_quit)
|
||||
signal.signal(signal.SIGTERM, sigterm_quit)
|
||||
|
||||
# init Logger
|
||||
logger = Logger()
|
||||
|
||||
# init backend
|
||||
backend = sys.argv[1]
|
||||
logdebug("Backend: %s" % backend)
|
||||
if backend == "nftables":
|
||||
logger.logInfo('Using NFTables backend')
|
||||
tables = NFTables(chain_name, logger)
|
||||
else:
|
||||
logger.logInfo('Using IPTables backend')
|
||||
logger.logWarn(
|
||||
"DEPRECATION: iptables-legacy is deprecated and will be removed in future releases. "
|
||||
"Please switch to nftables on your host to ensure complete compatibility."
|
||||
)
|
||||
time.sleep(5)
|
||||
tables = IPTables(chain_name, logger)
|
||||
|
||||
# In case a previous session was killed without cleanup
|
||||
clear()
|
||||
|
||||
# Reinit MAILCOW chain
|
||||
# Is called before threads start, no locking
|
||||
logger.logInfo("Initializing mailcow netfilter chain")
|
||||
tables.initChainIPv4()
|
||||
tables.initChainIPv6()
|
||||
|
||||
if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE").lower() in ("y", "yes"):
|
||||
if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE", "").lower() in ("y", "yes"):
|
||||
logger.logInfo(f"Skipping {chain_name} isolation")
|
||||
else:
|
||||
logger.logInfo(f"Setting {chain_name} isolation")
|
||||
@@ -432,23 +471,28 @@ if __name__ == '__main__':
|
||||
try:
|
||||
valkey_slaveof_ip = os.getenv('VALKEY_SLAVEOF_IP', '')
|
||||
valkey_slaveof_port = os.getenv('VALKEY_SLAVEOF_PORT', '')
|
||||
logdebug(
|
||||
"Connecting valkey (SLAVEOF_IP:%s, PORT:%s)" % (valkey_slaveof_ip, valkey_slaveof_port))
|
||||
if "".__eq__(valkey_slaveof_ip):
|
||||
valkey = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['VALKEYPASS'])
|
||||
valkey = redis.StrictRedis(
|
||||
host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['VALKEYPASS'])
|
||||
else:
|
||||
valkey = redis.StrictRedis(host=valkey_slaveof_ip, decode_responses=True, port=valkey_slaveof_port, db=0, password=os.environ['VALKEYPASS'])
|
||||
valkey = redis.StrictRedis(
|
||||
host=valkey_slaveof_ip, decode_responses=True, port=valkey_slaveof_port, db=0, password=os.environ['VALKEYPASS'])
|
||||
valkey.ping()
|
||||
pubsub = valkey.pubsub()
|
||||
except Exception as ex:
|
||||
print('%s - trying again in 3 seconds' % (ex))
|
||||
logdebug(
|
||||
'Redis connection failed: %s - trying again in 3 seconds' % (ex))
|
||||
time.sleep(3)
|
||||
else:
|
||||
break
|
||||
logger.set_valkey(r)
|
||||
logger.set_valkey(valkey)
|
||||
logdebug("Valkey connection established, setting up F2B keys")
|
||||
|
||||
# rename fail2ban to netfilter
|
||||
if valkey.exists('F2B_LOG'):
|
||||
logdebug("Renaming F2B_LOG to NETFILTER_LOG")
|
||||
valkey.rename('F2B_LOG', 'NETFILTER_LOG')
|
||||
# clear bans in valkey
|
||||
valkey.delete('F2B_ACTIVE_BANS')
|
||||
valkey.delete('F2B_PERM_BANS')
|
||||
|
||||
@@ -463,7 +507,7 @@ if __name__ == '__main__':
|
||||
snat_ip = os.getenv('SNAT_TO_SOURCE')
|
||||
snat_ipo = ipaddress.ip_address(snat_ip)
|
||||
if type(snat_ipo) is ipaddress.IPv4Address:
|
||||
snat4_thread = Thread(target=snat4,args=(snat_ip,))
|
||||
snat4_thread = Thread(target=snat4, args=(snat_ip,))
|
||||
snat4_thread.daemon = True
|
||||
snat4_thread.start()
|
||||
except ValueError:
|
||||
@@ -499,4 +543,5 @@ if __name__ == '__main__':
|
||||
while not quit_now:
|
||||
time.sleep(0.5)
|
||||
|
||||
sys.exit(exit_code)
|
||||
logdebug("Exiting with code %s" % exit_code)
|
||||
sys.exit(exit_code)
|
||||
@@ -1,5 +1,6 @@
|
||||
import time
|
||||
import json
|
||||
import datetime
|
||||
|
||||
class Logger:
|
||||
def __init__(self):
|
||||
@@ -8,17 +9,28 @@ class Logger:
|
||||
def set_valkey(self, valkey):
|
||||
self.valkey = valkey
|
||||
|
||||
def _format_timestamp(self):
|
||||
# Local time with milliseconds
|
||||
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def log(self, priority, message):
|
||||
tolog = {}
|
||||
tolog['time'] = int(round(time.time()))
|
||||
tolog['priority'] = priority
|
||||
tolog['message'] = message
|
||||
print(message)
|
||||
# build valkey-friendly dict
|
||||
tolog = {
|
||||
'time': int(round(time.time())), # keep raw timestamp for Valkey
|
||||
'priority': priority,
|
||||
'message': message
|
||||
}
|
||||
|
||||
# print human-readable message with timestamp
|
||||
ts = self._format_timestamp()
|
||||
print(f"{ts} {priority.upper()}: {message}", flush=True)
|
||||
|
||||
# also push JSON to Redis if connected
|
||||
if self.valkey is not None:
|
||||
try:
|
||||
self.valkey.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
|
||||
except Exception as ex:
|
||||
print('Failed logging to valkey: %s' % (ex))
|
||||
print(f'{ts} WARN: Failed logging to valkey: {ex}', flush=True)
|
||||
|
||||
def logWarn(self, message):
|
||||
self.log('warn', message)
|
||||
@@ -27,4 +39,4 @@ class Logger:
|
||||
self.log('crit', message)
|
||||
|
||||
def logInfo(self, message):
|
||||
self.log('info', message)
|
||||
self.log('info', message)
|
||||
@@ -10,7 +10,7 @@ def includes_conf(env, template_vars):
|
||||
server_name_config = f"server_name {template_vars['MAILCOW_HOSTNAME']} autodiscover.* autoconfig.* {' '.join(template_vars['ADDITIONAL_SERVER_NAMES'])};"
|
||||
listen_plain_config = f"listen {template_vars['HTTP_PORT']};"
|
||||
listen_ssl_config = f"listen {template_vars['HTTPS_PORT']};"
|
||||
if not template_vars['DISABLE_IPv6']:
|
||||
if template_vars['ENABLE_IPV6']:
|
||||
listen_plain_config += f"\nlisten [::]:{template_vars['HTTP_PORT']};"
|
||||
listen_ssl_config += f"\nlisten [::]:{template_vars['HTTPS_PORT']} ssl;"
|
||||
listen_ssl_config += "\nhttp2 on;"
|
||||
@@ -58,7 +58,7 @@ def prepare_template_vars():
|
||||
'SOGOHOST': os.getenv("SOGOHOST", ipv4_network + ".248"),
|
||||
'RSPAMDHOST': os.getenv("RSPAMDHOST", "rspamd-mailcow"),
|
||||
'PHPFPMHOST': os.getenv("PHPFPMHOST", "php-fpm-mailcow"),
|
||||
'DISABLE_IPv6': os.getenv("DISABLE_IPv6", "n").lower() in ("y", "yes"),
|
||||
'ENABLE_IPV6': os.getenv("ENABLE_IPV6", "true").lower() != "false",
|
||||
'HTTP_REDIRECT': os.getenv("HTTP_REDIRECT", "n").lower() in ("y", "yes"),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -32,6 +32,13 @@ import time
|
||||
import magic
|
||||
import re
|
||||
|
||||
skip_olefy = os.getenv('SKIP_OLEFY', '')
|
||||
|
||||
if skip_olefy.lower() in ['yes', 'y']:
|
||||
print("SKIP_OLEFY=y, skipping Olefy...")
|
||||
time.sleep(365 * 24 * 60 * 60)
|
||||
sys.exit(0)
|
||||
|
||||
# merge variables from /etc/olefy.conf and the defaults
|
||||
olefy_listen_addr_string = os.getenv('OLEFY_BINDADDRESS', '127.0.0.1,::1')
|
||||
olefy_listen_port = int(os.getenv('OLEFY_BINDPORT', '10050'))
|
||||
@@ -113,7 +120,7 @@ def oletools( stream, tmp_file_name, lid ):
|
||||
out = bytes(out.decode('utf-8', 'ignore').replace(' ', ' ').replace('\t', '').replace('\n', '').replace('XLMMacroDeobfuscator: pywin32 is not installed (only is required if you want to use MS Excel)', ''), encoding="utf-8")
|
||||
failed = False
|
||||
if out.__len__() < 30:
|
||||
logger.error('{} olevba returned <30 chars - rc: {!r}, response: {!r}, error: {!r}'.format(lid,cmd_tmp.returncode,
|
||||
logger.error('{} olevba returned <30 chars - rc: {!r}, response: {!r}, error: {!r}'.format(lid,cmd_tmp.returncode,
|
||||
out.decode('utf-8', 'ignore'), err.decode('utf-8', 'ignore')))
|
||||
out = b'[ { "error": "Unhandled error - too short olevba response" } ]'
|
||||
failed = True
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
FROM php:8.2-fpm-alpine3.20
|
||||
FROM php:8.2-fpm-alpine3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG APCU_PECL_VERSION=5.1.24
|
||||
ARG APCU_PECL_VERSION=5.1.26
|
||||
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG IMAGICK_PECL_VERSION=3.7.0
|
||||
ARG IMAGICK_PECL_VERSION=3.8.0
|
||||
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG MAILPARSE_PECL_VERSION=3.1.8
|
||||
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG MEMCACHED_PECL_VERSION=3.2.0
|
||||
ARG MEMCACHED_PECL_VERSION=3.3.0
|
||||
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG REDIS_PECL_VERSION=6.1.0
|
||||
ARG REDIS_PECL_VERSION=6.2.0
|
||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG COMPOSER_VERSION=2.6.6
|
||||
ARG COMPOSER_VERSION=2.8.6
|
||||
|
||||
RUN apk add -U --no-cache autoconf \
|
||||
aspell-dev \
|
||||
|
||||
@@ -81,7 +81,7 @@ if [ ${SQL_CHANGED} -eq 1 ]; then
|
||||
fi
|
||||
|
||||
# Check mysql tz import (master and slave)
|
||||
TZ_CHECK=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null)
|
||||
TZ_CHECK=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null)
|
||||
if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then
|
||||
SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json')
|
||||
echo "MySQL mysql_tzinfo_to_sql - debug output:"
|
||||
@@ -120,11 +120,11 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
while read line
|
||||
do
|
||||
DOMAIN_ARR+=("$line")
|
||||
done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs)
|
||||
done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs)
|
||||
while read line
|
||||
do
|
||||
DOMAIN_ARR+=("$line")
|
||||
done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs)
|
||||
done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs)
|
||||
|
||||
if [[ ! -z ${DOMAIN_ARR} ]]; then
|
||||
for domain in "${DOMAIN_ARR[@]}"; do
|
||||
@@ -146,13 +146,13 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
VALIDATED_IPS=$(array_by_comma ${VALIDATED_API_ALLOW_FROM_ARR[*]})
|
||||
if [[ ! -z ${VALIDATED_IPS} ]]; then
|
||||
if [[ ${API_KEY} != "invalid" ]] && [[ ! -z ${API_KEY} ]]; then
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
DELETE FROM api WHERE access = 'rw';
|
||||
INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY}", "1", "${VALIDATED_IPS}", "rw");
|
||||
EOF
|
||||
fi
|
||||
if [[ ${API_KEY_READ_ONLY} != "invalid" ]] && [[ ! -z ${API_KEY_READ_ONLY} ]]; then
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
DELETE FROM api WHERE access = 'ro';
|
||||
INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY_READ_ONLY}", "1", "${VALIDATED_IPS}", "ro");
|
||||
EOF
|
||||
@@ -161,7 +161,7 @@ EOF
|
||||
fi
|
||||
|
||||
# Create events (master only, STATUS for event on slave will be SLAVESIDE_DISABLED)
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
DROP EVENT IF EXISTS clean_spamalias;
|
||||
DELIMITER //
|
||||
CREATE EVENT clean_spamalias
|
||||
|
||||
50
data/Dockerfiles/postfix-tlspol/Dockerfile
Normal file
50
data/Dockerfiles/postfix-tlspol/Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
WORKDIR /src
|
||||
|
||||
ENV CGO_ENABLED=0 \
|
||||
GO111MODULE=on \
|
||||
NOOPT=1 \
|
||||
VERSION=1.8.14
|
||||
|
||||
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
|
||||
cd /src/postfix-tlspol && \
|
||||
scripts/build.sh build-only
|
||||
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ENV LC_ALL=C
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
dirmngr \
|
||||
dnsutils \
|
||||
iputils-ping \
|
||||
sudo \
|
||||
supervisor \
|
||||
redis-tools \
|
||||
syslog-ng \
|
||||
syslog-ng-core \
|
||||
syslog-ng-mod-redis \
|
||||
tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& touch /etc/default/locale
|
||||
|
||||
COPY supervisord.conf /etc/supervisor/supervisord.conf
|
||||
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||
COPY syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf
|
||||
COPY postfix-tlspol.sh /opt/postfix-tlspol.sh
|
||||
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
COPY --from=builder /src/postfix-tlspol/build/postfix-tlspol /usr/local/bin/postfix-tlspol
|
||||
|
||||
RUN chmod +x /opt/postfix-tlspol.sh \
|
||||
/usr/local/sbin/stop-supervisor.sh \
|
||||
/docker-entrypoint.sh
|
||||
RUN rm -rf /tmp/* /var/tmp/*
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
7
data/Dockerfiles/postfix-tlspol/docker-entrypoint.sh
Executable file
7
data/Dockerfiles/postfix-tlspol/docker-entrypoint.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
|
||||
cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
52
data/Dockerfiles/postfix-tlspol/postfix-tlspol.sh
Executable file
52
data/Dockerfiles/postfix-tlspol/postfix-tlspol.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
LOGLVL=info
|
||||
|
||||
if [ ${DEV_MODE} != "n" ]; then
|
||||
echo -e "\e[31mEnabling debug mode\e[0m"
|
||||
set -x
|
||||
LOGLVL=debug
|
||||
fi
|
||||
|
||||
[[ ! -d /etc/postfix-tlspol ]] && mkdir -p /etc/postfix-tlspol
|
||||
[[ ! -d /var/lib/postfix-tlspol ]] && mkdir -p /var/lib/postfix-tlspol
|
||||
|
||||
until dig +short mailcow.email > /dev/null; do
|
||||
echo "Waiting for DNS..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Do not attempt to write to slave
|
||||
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
|
||||
export VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
|
||||
else
|
||||
export VALKEY_CMDLINE="redis-cli -h valkey -p 6379 -a ${VALKEYPASS} --no-auth-warning"
|
||||
fi
|
||||
|
||||
until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
|
||||
echo "Waiting for Valkey..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Waiting for Postfix..."
|
||||
until ping postfix -c1 > /dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
echo "Postfix OK"
|
||||
|
||||
cat <<EOF > /etc/postfix-tlspol/config.yaml
|
||||
server:
|
||||
address: 0.0.0.0:8642
|
||||
|
||||
log-level: ${LOGLVL}
|
||||
|
||||
prefetch: true
|
||||
|
||||
cache-file: /var/lib/postfix-tlspol/cache.db
|
||||
|
||||
dns:
|
||||
# must support DNSSEC
|
||||
address: 127.0.0.11:53
|
||||
EOF
|
||||
|
||||
/usr/local/bin/postfix-tlspol -config /etc/postfix-tlspol/config.yaml
|
||||
8
data/Dockerfiles/postfix-tlspol/stop-supervisor.sh
Executable file
8
data/Dockerfiles/postfix-tlspol/stop-supervisor.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
printf "READY\n";
|
||||
|
||||
while read line; do
|
||||
echo "Processing Event: $line" >&2;
|
||||
kill -3 $(cat "/var/run/supervisord.pid")
|
||||
done < /dev/stdin
|
||||
25
data/Dockerfiles/postfix-tlspol/supervisord.conf
Normal file
25
data/Dockerfiles/postfix-tlspol/supervisord.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
[supervisord]
|
||||
pidfile=/var/run/supervisord.pid
|
||||
nodaemon=true
|
||||
user=root
|
||||
|
||||
[program:syslog-ng]
|
||||
command=/usr/sbin/syslog-ng --foreground --no-caps
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
autostart=true
|
||||
|
||||
[program:postfix-tlspol]
|
||||
startsecs=10
|
||||
autorestart=true
|
||||
command=/opt/postfix-tlspol.sh
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[eventlistener:processes]
|
||||
command=/usr/local/sbin/stop-supervisor.sh
|
||||
events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL
|
||||
45
data/Dockerfiles/postfix-tlspol/syslog-ng-valkey_slave.conf
Normal file
45
data/Dockerfiles/postfix-tlspol/syslog-ng-valkey_slave.conf
Normal file
@@ -0,0 +1,45 @@
|
||||
@version: 3.38
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
flush_lines(0);
|
||||
use_dns(no);
|
||||
dns_cache(no);
|
||||
use_fqdn(no);
|
||||
owner("root"); group("adm"); perm(0640);
|
||||
stats_freq(0);
|
||||
bad_hostname("^gconfd$");
|
||||
};
|
||||
source s_src {
|
||||
unix-stream("/dev/log");
|
||||
internal();
|
||||
};
|
||||
destination d_stdout { pipe("/dev/stdout"); };
|
||||
destination d_valkey_ui_log {
|
||||
redis(
|
||||
host("`VALKEY_SLAVEOF_IP`")
|
||||
persist-name("valkey1")
|
||||
port(`VALKEY_SLAVEOF_PORT`)
|
||||
auth("`VALKEYPASS`")
|
||||
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
|
||||
);
|
||||
};
|
||||
filter f_mail { facility(mail); };
|
||||
# start
|
||||
# overriding warnings are still displayed when the entrypoint runs its initial check
|
||||
# warnings logged by postfix-mailcow to syslog are hidden to reduce repeating msgs
|
||||
# Some other warnings are ignored
|
||||
filter f_ignore {
|
||||
not match("overriding earlier entry" value("MESSAGE"));
|
||||
not match("TLS SNI from checks.mailcow.email" value("MESSAGE"));
|
||||
not match("no SASL support" value("MESSAGE"));
|
||||
not facility (local0, local1, local2, local3, local4, local5, local6, local7);
|
||||
};
|
||||
# end
|
||||
log {
|
||||
source(s_src);
|
||||
filter(f_ignore);
|
||||
destination(d_stdout);
|
||||
filter(f_mail);
|
||||
destination(d_valkey_ui_log);
|
||||
};
|
||||
45
data/Dockerfiles/postfix-tlspol/syslog-ng.conf
Normal file
45
data/Dockerfiles/postfix-tlspol/syslog-ng.conf
Normal file
@@ -0,0 +1,45 @@
|
||||
@version: 3.38
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
flush_lines(0);
|
||||
use_dns(no);
|
||||
dns_cache(no);
|
||||
use_fqdn(no);
|
||||
owner("root"); group("adm"); perm(0640);
|
||||
stats_freq(0);
|
||||
bad_hostname("^gconfd$");
|
||||
};
|
||||
source s_src {
|
||||
unix-stream("/dev/log");
|
||||
internal();
|
||||
};
|
||||
destination d_stdout { pipe("/dev/stdout"); };
|
||||
destination d_valkey_ui_log {
|
||||
redis(
|
||||
host("valkey-mailcow")
|
||||
persist-name("valkey1")
|
||||
port(6379)
|
||||
auth("`VALKEYPASS`")
|
||||
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
|
||||
);
|
||||
};
|
||||
filter f_mail { facility(mail); };
|
||||
# start
|
||||
# overriding warnings are still displayed when the entrypoint runs its initial check
|
||||
# warnings logged by postfix-mailcow to syslog are hidden to reduce repeating msgs
|
||||
# Some other warnings are ignored
|
||||
filter f_ignore {
|
||||
not match("overriding earlier entry" value("MESSAGE"));
|
||||
not match("TLS SNI from checks.mailcow.email" value("MESSAGE"));
|
||||
not match("no SASL support" value("MESSAGE"));
|
||||
not facility (local0, local1, local2, local3, local4, local5, local6, local7);
|
||||
};
|
||||
# end
|
||||
log {
|
||||
source(s_src);
|
||||
filter(f_ignore);
|
||||
destination(d_stdout);
|
||||
filter(f_mail);
|
||||
destination(d_valkey_ui_log);
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ENV LC_ALL C
|
||||
ENV LC_ALL=C
|
||||
|
||||
RUN dpkg-divert --local --rename --add /sbin/initctl \
|
||||
&& ln -sf /bin/true /sbin/initctl \
|
||||
|
||||
@@ -524,4 +524,4 @@ if [[ $? != 0 ]]; then
|
||||
else
|
||||
postfix -c /opt/postfix/conf start
|
||||
sleep 126144000
|
||||
fi
|
||||
fi
|
||||
@@ -2,11 +2,11 @@ FROM debian:bookworm-slim
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG RSPAMD_VER=rspamd_3.11.0-2~90a175b45
|
||||
ARG RSPAMD_VER=rspamd_3.12.1-1~6dbfca2fa
|
||||
ARG CODENAME=bookworm
|
||||
ENV LC_ALL=C
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tzdata \
|
||||
ca-certificates \
|
||||
gnupg2 \
|
||||
|
||||
@@ -81,6 +81,29 @@ EOF
|
||||
redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning SLAVEOF NO ONE
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
if [[ -f /etc/rspamd/local.d/external_services.conf ]]; then
|
||||
rm /etc/rspamd/local.d/external_services.conf
|
||||
fi
|
||||
else
|
||||
if [[ ! -f /etc/rspamd/local.d/external_services.conf ]]; then
|
||||
cat <<EOF > /etc/rspamd/local.d/external_services.conf
|
||||
oletools {
|
||||
# default olefy settings
|
||||
servers = "olefy:10055";
|
||||
# needs to be set explicitly for Rspamd < 1.9.5
|
||||
scan_mime_parts = true;
|
||||
# mime-part regex matching in content-type or filename
|
||||
# block all macros
|
||||
extended = true;
|
||||
max_size = 3145728;
|
||||
timeout = 20.0;
|
||||
retransmits = 1;
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
# Provide additional lua modules
|
||||
ln -s /usr/lib/$(uname -m)-linux-gnu/liblua5.1-cjson.so.0.0.0 /usr/lib/rspamd/cjson.so
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||
COPY syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf
|
||||
COPY supervisord.conf /etc/supervisor/supervisord.conf
|
||||
COPY acl.diff /acl.diff
|
||||
COPY navMailcowBtns.diff /navMailcowBtns.diff
|
||||
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||
COPY docker-entrypoint.sh /
|
||||
|
||||
|
||||
@@ -14,118 +14,18 @@ do
|
||||
done
|
||||
|
||||
# Wait for updated schema
|
||||
DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
|
||||
DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
|
||||
DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
|
||||
while [[ "${DBV_NOW}" != "${DBV_NEW}" ]]; do
|
||||
echo "Waiting for schema update..."
|
||||
DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
|
||||
DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
|
||||
DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
|
||||
sleep 5
|
||||
done
|
||||
echo "DB schema is ${DBV_NOW}"
|
||||
|
||||
# Recreate view
|
||||
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo "We are master, preparing sogo_view..."
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS sogo_view"
|
||||
while [[ ${VIEW_OK} != 'OK' ]]; do
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings) AS
|
||||
SELECT
|
||||
mailbox.username,
|
||||
mailbox.domain,
|
||||
mailbox.username,
|
||||
IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.force_pw_update')) = '0', IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.sogo_access')) = 1, password, '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'), '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'),
|
||||
mailbox.name,
|
||||
mailbox.username,
|
||||
IFNULL(GROUP_CONCAT(ga.aliases ORDER BY ga.aliases SEPARATOR ' '), ''),
|
||||
IFNULL(gda.ad_alias, ''),
|
||||
IFNULL(external_acl.send_as_acl, ''),
|
||||
mailbox.kind,
|
||||
mailbox.multiple_bookings
|
||||
FROM
|
||||
mailbox
|
||||
LEFT OUTER JOIN
|
||||
grouped_mail_aliases ga
|
||||
ON ga.username REGEXP CONCAT('(^|,)', mailbox.username, '($|,)')
|
||||
LEFT OUTER JOIN
|
||||
grouped_domain_alias_address gda
|
||||
ON gda.username = mailbox.username
|
||||
LEFT OUTER JOIN
|
||||
grouped_sender_acl_external external_acl
|
||||
ON external_acl.username = mailbox.username
|
||||
WHERE
|
||||
mailbox.active = '1'
|
||||
GROUP BY
|
||||
mailbox.username;
|
||||
EOF
|
||||
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view'") ]]; then
|
||||
VIEW_OK=OK
|
||||
else
|
||||
echo "Will retry to setup SOGo view in 3s..."
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
else
|
||||
while [[ ${VIEW_OK} != 'OK' ]]; do
|
||||
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view'") ]]; then
|
||||
VIEW_OK=OK
|
||||
else
|
||||
echo "Waiting for SOGo view to be created by master..."
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Wait for static view table if missing after update and update content
|
||||
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo "We are master, preparing _sogo_static_view..."
|
||||
while [[ ${STATIC_VIEW_OK} != 'OK' ]]; do
|
||||
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then
|
||||
STATIC_VIEW_OK=OK
|
||||
echo "Updating _sogo_static_view content..."
|
||||
# If changed, also update init_db.inc.php
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "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;"
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "DELETE FROM _sogo_static_view WHERE c_uid NOT IN (SELECT username FROM mailbox WHERE active = '1')"
|
||||
else
|
||||
echo "Waiting for database initialization..."
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
else
|
||||
while [[ ${STATIC_VIEW_OK} != 'OK' ]]; do
|
||||
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then
|
||||
STATIC_VIEW_OK=OK
|
||||
else
|
||||
echo "Waiting for database initialization by master..."
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Recreate password update trigger
|
||||
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo "We are master, preparing update trigger..."
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TRIGGER IF EXISTS sogo_update_password"
|
||||
while [[ ${TRIGGER_OK} != 'OK' ]]; do
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
DELIMITER -
|
||||
CREATE TRIGGER sogo_update_password AFTER UPDATE ON _sogo_static_view
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE mailbox SET password = NEW.c_password WHERE NEW.c_uid = username;
|
||||
END;
|
||||
-
|
||||
DELIMITER ;
|
||||
EOF
|
||||
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_NAME = 'sogo_update_password'") ]]; then
|
||||
TRIGGER_OK=OK
|
||||
else
|
||||
echo "Will retry to setup SOGo password update trigger in 3s"
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TRIGGER IF EXISTS sogo_update_password"
|
||||
fi
|
||||
|
||||
# cat /dev/urandom seems to hang here occasionally and is not recommended anyway, better use openssl
|
||||
@@ -150,6 +50,10 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
<string>YES</string>
|
||||
<key>SOGoEncryptionKey</key>
|
||||
<string>${RAND_PASS}</string>
|
||||
<key>SOGoURLEncryptionEnabled</key>
|
||||
<string>YES</string>
|
||||
<key>SOGoURLEncryptionPassphrase</key>
|
||||
<string>${SOGO_URL_ENCRYPTION_KEY}</string>
|
||||
<key>OCSAdminURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
|
||||
<key>OCSCacheFolderURL</key>
|
||||
@@ -213,10 +117,10 @@ while read -r line gal
|
||||
</dict>" >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
# Generate alternative LDAP authentication dict, when SQL authentication fails
|
||||
# This will nevertheless read attributes from LDAP
|
||||
line=${line} envsubst < /etc/sogo/plist_ldap >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
/etc/sogo/plist_ldap.sh ${line} ${gal} >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
echo " </array>
|
||||
</dict>" >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain, CASE gal WHEN '1' THEN 'YES' ELSE 'NO' END AS gal FROM domain;" -B -N)
|
||||
done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain, CASE gal WHEN '1' THEN 'YES' ELSE 'NO' END AS gal FROM domain;" -B -N)
|
||||
|
||||
# Generate footer
|
||||
echo ' </dict>
|
||||
@@ -240,6 +144,10 @@ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
# fi
|
||||
#fi
|
||||
|
||||
if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then
|
||||
patch -R /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
|
||||
fi
|
||||
|
||||
# Rename custom logo, if any
|
||||
[[ -f /etc/sogo/sogo-full.svg ]] && mv /etc/sogo/sogo-full.svg /etc/sogo/custom-fulllogo.svg
|
||||
|
||||
|
||||
15
data/Dockerfiles/sogo/navMailcowBtns.diff
Normal file
15
data/Dockerfiles/sogo/navMailcowBtns.diff
Normal file
@@ -0,0 +1,15 @@
|
||||
60,65d58
|
||||
< var:ng-click="navButtonClick"
|
||||
< ng-href="/user">
|
||||
< <md-icon>build</md-icon>
|
||||
< <md-tooltip>mailcow <var:string label:value="Preferences"/></md-tooltip>
|
||||
< </md-button>
|
||||
< <md-button class="md-icon-button"
|
||||
83c76
|
||||
< onclick="mc_logout();"
|
||||
---
|
||||
> ng-show="::activeUser.path.logoff.length"
|
||||
85c78
|
||||
< ng-href="#">
|
||||
---
|
||||
> ng-href="{{::activeUser.path.logoff}}">
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
@@ -16,7 +16,6 @@ RUN apk add --update \
|
||||
fcgi \
|
||||
openssl \
|
||||
nagios-plugins-mysql \
|
||||
nagios-plugins-dns \
|
||||
nagios-plugins-disk \
|
||||
bind-tools \
|
||||
redis \
|
||||
@@ -32,9 +31,11 @@ RUN apk add --update \
|
||||
tzdata \
|
||||
whois \
|
||||
&& curl https://raw.githubusercontent.com/mludvig/smtp-cli/v3.10/smtp-cli -o /smtp-cli \
|
||||
&& chmod +x smtp-cli
|
||||
&& chmod +x smtp-cli \
|
||||
&& mkdir /usr/lib/mailcow
|
||||
|
||||
COPY watchdog.sh /watchdog.sh
|
||||
COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
|
||||
COPY check_dns.sh /usr/lib/mailcow/check_dns.sh
|
||||
|
||||
CMD ["/watchdog.sh"]
|
||||
|
||||
39
data/Dockerfiles/watchdog/check_dns.sh
Executable file
39
data/Dockerfiles/watchdog/check_dns.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
|
||||
while getopts "H:s:" opt; do
|
||||
case "$opt" in
|
||||
H) HOST="$OPTARG" ;;
|
||||
s) SERVER="$OPTARG" ;;
|
||||
*) echo "Usage: $0 -H host -s server"; exit 3 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$SERVER" ]; then
|
||||
echo "No DNS Server provided"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if [ -z "$HOST" ]; then
|
||||
echo "No host to test provided"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# run dig and measure the time it takes to run
|
||||
START_TIME=$(date +%s%3N)
|
||||
dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null)
|
||||
dig_rc=$?
|
||||
dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -)
|
||||
END_TIME=$(date +%s%3N)
|
||||
ELAPSED_TIME=$((END_TIME - START_TIME))
|
||||
|
||||
# validate and perform nagios like output and exit codes
|
||||
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
|
||||
echo "Domain $HOST was not found by the server"
|
||||
exit 2
|
||||
elif [ $dig_rc -eq 0 ]; then
|
||||
echo "DNS OK: $ELAPSED_TIME ms response time. $HOST returns $dig_output_ips"
|
||||
exit 0
|
||||
else
|
||||
echo "Unknown error"
|
||||
exit 3
|
||||
fi
|
||||
@@ -49,7 +49,7 @@
|
||||
# 2013101601 Optical clean up #
|
||||
# 2013101602 Rewrite help output #
|
||||
# 2013101700 Handle Slave IO in 'Connecting' state #
|
||||
# 2013101701 Minor changes in output, handling UNKWNON situations now #
|
||||
# 2013101701 Minor changes in output, handling UNKNOWN situations now #
|
||||
# 2013101702 Exit CRITICAL when Slave IO in Connecting state #
|
||||
# 2013123000 Slave_SQL_Running also matched Slave_SQL_Running_State #
|
||||
# 2015011600 Added 'moving' check to catch possible connection issues #
|
||||
@@ -131,10 +131,10 @@ elif [[ -n "${socket}" && (-z "${user}" || -z "${password}") ]]; then
|
||||
fi
|
||||
|
||||
# Connect to the DB server and store output in vars
|
||||
if [[ -n $socket ]]; then
|
||||
ConnectionResult=$(mysql ${optfile} ${socket} ${user} -e "show slave ${connection} status\G" 2>&1)
|
||||
if [[ -n $socket ]]; then
|
||||
ConnectionResult=$(mariadb --skip-ssl ${optfile} ${socket} ${user} -e "show slave ${connection} status\G" 2>&1)
|
||||
else
|
||||
ConnectionResult=$(mysql ${optfile} ${host} ${port} ${user} -e "show slave ${connection} status\G" 2>&1)
|
||||
ConnectionResult=$(mariadb --skip-ssl ${optfile} ${host} ${port} ${user} -e "show slave ${connection} status\G" 2>&1)
|
||||
fi
|
||||
|
||||
if [ -z "`echo "${ConnectionResult}" |grep Slave_IO_State`" ]; then
|
||||
@@ -178,33 +178,33 @@ if [ ${check} = ${ok} ] && [ ${checkio} = ${ok} ]; then
|
||||
then echo "CRITICAL: Slave is ${delayinfo} seconds behind Master | delay=${delayinfo}s"; exit ${STATE_CRITICAL}
|
||||
elif [[ ${delayinfo} -ge ${warn_delay} ]]
|
||||
then echo "WARNING: Slave is ${delayinfo} seconds behind Master | delay=${delayinfo}s"; exit ${STATE_WARNING}
|
||||
else
|
||||
else
|
||||
# Everything looks OK here but now let us check if the replication is moving
|
||||
if [[ -n ${moving} ]] && [[ -n ${tmpfile} ]] && [[ $readpos -eq $execpos ]]
|
||||
then
|
||||
#echo "Debug: Read pos is $readpos - Exec pos is $execpos"
|
||||
then
|
||||
#echo "Debug: Read pos is $readpos - Exec pos is $execpos"
|
||||
# Check if tmp file exists
|
||||
curtime=`date +%s`
|
||||
if [[ -w $tmpfile ]]
|
||||
then
|
||||
if [[ -w $tmpfile ]]
|
||||
then
|
||||
tmpfiletime=`date +%s -r $tmpfile`
|
||||
if [[ `expr $curtime - $tmpfiletime` -gt ${moving} ]]
|
||||
then
|
||||
exectmp=`cat $tmpfile`
|
||||
#echo "Debug: Exec pos in tmpfile is $exectmp"
|
||||
if [[ $exectmp -eq $execpos ]]
|
||||
then
|
||||
then
|
||||
# The value read from the tmp file and from db are the same. Replication hasnt moved!
|
||||
echo "WARNING: Slave replication has not moved in ${moving} seconds. Manual check required."; exit ${STATE_WARNING}
|
||||
else
|
||||
else
|
||||
# Replication has moved since the tmp file was written. Delete tmp file and output OK.
|
||||
rm $tmpfile
|
||||
echo "OK: Slave SQL running: ${check} Slave IO running: ${checkio} / master: ${masterinfo} / slave is ${delayinfo} seconds behind master | delay=${delayinfo}s"; exit ${STATE_OK};
|
||||
fi
|
||||
else
|
||||
else
|
||||
echo "OK: Slave SQL running: ${check} Slave IO running: ${checkio} / master: ${masterinfo} / slave is ${delayinfo} seconds behind master | delay=${delayinfo}s"; exit ${STATE_OK};
|
||||
fi
|
||||
else
|
||||
else
|
||||
echo "$execpos" > $tmpfile
|
||||
echo "OK: Slave SQL running: ${check} Slave IO running: ${checkio} / master: ${masterinfo} / slave is ${delayinfo} seconds behind master | delay=${delayinfo}s"; exit ${STATE_OK};
|
||||
fi
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "${DEV_MODE}" != "n" ]; then
|
||||
echo -e "\e[31mEnabled Debug Mode\e[0m"
|
||||
set -x
|
||||
fi
|
||||
|
||||
trap "exit" INT TERM
|
||||
trap "kill 0" EXIT
|
||||
|
||||
@@ -234,7 +239,7 @@ external_checks() {
|
||||
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)
|
||||
GUID=$(mariadb --skip-ssl -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}
|
||||
@@ -297,7 +302,7 @@ unbound_checks() {
|
||||
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} + $? ))
|
||||
/usr/lib/mailcow/check_dns.sh -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
|
||||
@@ -445,6 +450,31 @@ postfix_checks() {
|
||||
return 1
|
||||
}
|
||||
|
||||
postfix-tlspol_checks() {
|
||||
err_count=0
|
||||
diff_c=0
|
||||
THRESHOLD=${POSTFIX_TLSPOL_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-tlspol-mailcow; echo "$(tail -50 /tmp/postfix-tlspol-mailcow)" > /tmp/postfix-tlspol-mailcow
|
||||
host_ip=$(get_container_ip postfix-tlspol-mailcow)
|
||||
err_c_cur=${err_count}
|
||||
/usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 8642 2>> /tmp/postfix-tlspol-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 TLS Policy companion" ${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
|
||||
@@ -922,6 +952,18 @@ PID=$!
|
||||
echo "Spawned mailq_checks with PID ${PID}"
|
||||
BACKGROUND_TASKS+=(${PID})
|
||||
|
||||
(
|
||||
while true; do
|
||||
if ! postfix-tlspol_checks; then
|
||||
log_msg "Postfix TLS Policy hit error limit"
|
||||
echo postfix-tlspol-mailcow > /tmp/com_pipe
|
||||
fi
|
||||
done
|
||||
) &
|
||||
PID=$!
|
||||
echo "Spawned postfix-tlspol_checks with PID ${PID}"
|
||||
BACKGROUND_TASKS+=(${PID})
|
||||
|
||||
(
|
||||
while true; do
|
||||
if ! dovecot_checks; then
|
||||
@@ -994,6 +1036,7 @@ PID=$!
|
||||
echo "Spawned cert_checks with PID ${PID}"
|
||||
BACKGROUND_TASKS+=(${PID})
|
||||
|
||||
if [[ "${SKIP_OLEFY}" =~ ^([nN][oO]|[nN])+$ ]]; then
|
||||
(
|
||||
while true; do
|
||||
if ! olefy_checks; then
|
||||
@@ -1005,6 +1048,7 @@ done
|
||||
PID=$!
|
||||
echo "Spawned olefy_checks with PID ${PID}"
|
||||
BACKGROUND_TASKS+=(${PID})
|
||||
fi
|
||||
|
||||
(
|
||||
while true; do
|
||||
|
||||
115
data/conf/dovecot/auth/mailcowauth.php
Normal file
115
data/conf/dovecot/auth/mailcowauth.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
ini_set('error_reporting', 0);
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$post = trim(file_get_contents('php://input'));
|
||||
if ($post) {
|
||||
$post = json_decode($post, true);
|
||||
}
|
||||
|
||||
|
||||
$return = array("success" => false);
|
||||
if(!isset($post['username']) || !isset($post['password']) || !isset($post['real_rip'])){
|
||||
error_log("MAILCOWAUTH: Bad Request");
|
||||
http_response_code(400); // Bad Request
|
||||
echo json_encode($return);
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once('../../../web/inc/vars.inc.php');
|
||||
if (file_exists('../../../web/inc/vars.local.inc.php')) {
|
||||
include_once('../../../web/inc/vars.local.inc.php');
|
||||
}
|
||||
require_once '../../../web/inc/lib/vendor/autoload.php';
|
||||
|
||||
|
||||
// Init Valkey
|
||||
$valkey = new Redis();
|
||||
try {
|
||||
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
|
||||
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
|
||||
}
|
||||
else {
|
||||
$valkey->connect('valkey-mailcow', 6379);
|
||||
}
|
||||
$valkey->auth(getenv("VALKEYPASS"));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
error_log("MAILCOWAUTH: " . $e . PHP_EOL);
|
||||
http_response_code(500); // Internal Server Error
|
||||
echo json_encode($return);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Init database
|
||||
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
try {
|
||||
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
error_log("MAILCOWAUTH: " . $e . PHP_EOL);
|
||||
http_response_code(500); // Internal Server Error
|
||||
echo json_encode($return);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Load core functions first
|
||||
require_once 'functions.inc.php';
|
||||
require_once 'functions.auth.inc.php';
|
||||
require_once 'sessions.inc.php';
|
||||
require_once 'functions.mailbox.inc.php';
|
||||
require_once 'functions.ratelimit.inc.php';
|
||||
require_once 'functions.acl.inc.php';
|
||||
|
||||
|
||||
$isSOGoRequest = $post['real_rip'] == getenv('IPV4_NETWORK') . '.248';
|
||||
$result = false;
|
||||
if ($isSOGoRequest) {
|
||||
// This is a SOGo Auth request. First check for SSO password.
|
||||
$sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
|
||||
if ($sogo_sso_pass === $post['password']){
|
||||
error_log('MAILCOWAUTH: SOGo SSO auth for user ' . $post['username']);
|
||||
set_sasl_log($post['username'], $post['real_rip'], "SOGO");
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
if ($result === false){
|
||||
// If it's a SOGo Request, don't check for protocol access
|
||||
$service = ($isSOGoRequest) ? false : array($post['service'] => true);
|
||||
$result = apppass_login($post['username'], $post['password'], $service, array(
|
||||
'is_internal' => true,
|
||||
'remote_addr' => $post['real_rip']
|
||||
));
|
||||
if ($result) {
|
||||
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
|
||||
set_sasl_log($post['username'], $post['real_rip'], $post['service']);
|
||||
}
|
||||
}
|
||||
if ($result === false){
|
||||
// Init Identity Provider
|
||||
$iam_provider = identity_provider('init');
|
||||
$iam_settings = identity_provider('get');
|
||||
$result = user_login($post['username'], $post['password'], array('is_internal' => true, 'service' => $post['service']));
|
||||
if ($result) {
|
||||
error_log('MAILCOWAUTH: User auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
|
||||
set_sasl_log($post['username'], $post['real_rip'], $post['service']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
http_response_code(200); // OK
|
||||
$return['success'] = true;
|
||||
} else {
|
||||
error_log("MAILCOWAUTH: Login failed for user " . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
|
||||
http_response_code(401); // Unauthorized
|
||||
}
|
||||
|
||||
|
||||
echo json_encode($return);
|
||||
session_destroy();
|
||||
exit;
|
||||
57
data/conf/dovecot/auth/passwd-verify.lua
Normal file
57
data/conf/dovecot/auth/passwd-verify.lua
Normal file
@@ -0,0 +1,57 @@
|
||||
function auth_password_verify(request, password)
|
||||
if request.domain == nil then
|
||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
|
||||
end
|
||||
|
||||
local json = require "cjson"
|
||||
local ltn12 = require "ltn12"
|
||||
local https = require "ssl.https"
|
||||
https.TIMEOUT = 30
|
||||
|
||||
local req = {
|
||||
username = request.user,
|
||||
password = password,
|
||||
real_rip = request.real_rip,
|
||||
service = request.service
|
||||
}
|
||||
local req_json = json.encode(req)
|
||||
local res = {}
|
||||
|
||||
local b, c = https.request {
|
||||
method = "POST",
|
||||
url = "https://nginx:9082",
|
||||
source = ltn12.source.string(req_json),
|
||||
headers = {
|
||||
["content-type"] = "application/json",
|
||||
["content-length"] = tostring(#req_json)
|
||||
},
|
||||
sink = ltn12.sink.table(res),
|
||||
insecure = true
|
||||
}
|
||||
|
||||
-- Returning PASSDB_RESULT_PASSWORD_MISMATCH will reset the user's auth cache entry.
|
||||
-- Returning PASSDB_RESULT_INTERNAL_FAILURE keeps the existing cache entry,
|
||||
-- even if the TTL has expired. Useful to avoid cache eviction during backend issues.
|
||||
if c ~= 200 and c ~= 401 then
|
||||
dovecot.i_info("HTTP request failed with " .. c .. " for user " .. request.user)
|
||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error"
|
||||
end
|
||||
|
||||
local response_str = table.concat(res)
|
||||
local is_response_valid, response_json = pcall(json.decode, response_str)
|
||||
|
||||
if not is_response_valid then
|
||||
dovecot.i_info("Invalid JSON received: " .. response_str)
|
||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Invalid response format"
|
||||
end
|
||||
|
||||
if response_json.success == true then
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
end
|
||||
|
||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
|
||||
end
|
||||
|
||||
function auth_passdb_lookup(req)
|
||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
|
||||
end
|
||||
@@ -53,7 +53,7 @@ mail_shared_explicit_inbox = yes
|
||||
mail_prefetch_count = 30
|
||||
passdb {
|
||||
driver = lua
|
||||
args = file=/etc/dovecot/lua/passwd-verify.lua blocking=yes
|
||||
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes cache_key=%s:%u:%w
|
||||
result_success = return-ok
|
||||
result_failure = continue
|
||||
result_internalfail = continue
|
||||
@@ -69,7 +69,7 @@ passdb {
|
||||
# a return of the following passdb is mandatory
|
||||
passdb {
|
||||
driver = lua
|
||||
args = file=/etc/dovecot/lua/passwd-verify.lua blocking=yes
|
||||
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes
|
||||
}
|
||||
# Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
|
||||
service doveadm {
|
||||
@@ -125,6 +125,7 @@ service managesieve-login {
|
||||
}
|
||||
service imap-login {
|
||||
service_count = 1
|
||||
process_min_avail = 2
|
||||
process_limit = 10000
|
||||
vsz_limit = 1G
|
||||
user = dovenull
|
||||
@@ -140,6 +141,7 @@ service imap-login {
|
||||
}
|
||||
service pop3-login {
|
||||
service_count = 1
|
||||
process_min_avail = 1
|
||||
vsz_limit = 1G
|
||||
inet_listener pop3_haproxy {
|
||||
port = 10110
|
||||
@@ -239,7 +241,7 @@ plugin {
|
||||
mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem
|
||||
mail_crypt_save_version = 2
|
||||
|
||||
# Enable compression while saving, lz4 Dovecot v2.2.11+
|
||||
# Enable compression while saving, lz4 Dovecot v2.3.17+
|
||||
zlib_save = lz4
|
||||
|
||||
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
|
||||
@@ -274,10 +276,10 @@ service stats {
|
||||
}
|
||||
}
|
||||
imap_max_line_length = 2 M
|
||||
#auth_cache_verify_password_with_worker = yes
|
||||
#auth_cache_negative_ttl = 0
|
||||
#auth_cache_ttl = 30 s
|
||||
#auth_cache_size = 2 M
|
||||
auth_cache_verify_password_with_worker = yes
|
||||
auth_cache_negative_ttl = 60s
|
||||
auth_cache_ttl = 300s
|
||||
auth_cache_size = 10M
|
||||
auth_verbose_passwords = sha1:6
|
||||
service replicator {
|
||||
process_min_avail = 1
|
||||
@@ -302,7 +304,6 @@ replication_dsync_parameters = -d -l 30 -U -n INBOX
|
||||
!include_try /etc/dovecot/sni.conf
|
||||
!include_try /etc/dovecot/sogo_trusted_ip.conf
|
||||
!include_try /etc/dovecot/extra.conf
|
||||
!include_try /etc/dovecot/sogo-sso.conf
|
||||
!include_try /etc/dovecot/shared_namespace.conf
|
||||
!include_try /etc/dovecot/conf.d/fts.conf
|
||||
# </Includes>
|
||||
|
||||
@@ -48,13 +48,21 @@ http {
|
||||
listen {{ HTTP_PORT }} default_server;
|
||||
listen [::]:{{ HTTP_PORT }} default_server;
|
||||
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
|
||||
|
||||
if ( $request_uri ~* "%0A|%0D" ) { return 403; }
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
allow all;
|
||||
default_type "text/plain";
|
||||
}
|
||||
location ^~ /.well-known/mta-sts.txt {
|
||||
allow all;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass {{ PHPFPMHOST }}:9002;
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/mta-sts.php;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
location / {
|
||||
return 301 https://$host$uri$is_args$args;
|
||||
}
|
||||
@@ -70,7 +78,7 @@ http {
|
||||
{%endif%}
|
||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||
|
||||
{% if not DISABLE_IPv6 %}
|
||||
{% if ENABLE_IPV6 %}
|
||||
{% if not HTTP_REDIRECT %}
|
||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||
{%endif%}
|
||||
@@ -82,7 +90,7 @@ http {
|
||||
ssl_certificate /etc/ssl/mail/cert.pem;
|
||||
ssl_certificate_key /etc/ssl/mail/key.pem;
|
||||
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.*;
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.*;
|
||||
|
||||
include /etc/nginx/includes/sites-default.conf;
|
||||
}
|
||||
@@ -97,7 +105,7 @@ http {
|
||||
{%endif%}
|
||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||
|
||||
{% if not DISABLE_IPv6 %}
|
||||
{% if ENABLE_IPV6 %}
|
||||
{% if not HTTP_REDIRECT %}
|
||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||
{%endif%}
|
||||
@@ -118,7 +126,7 @@ http {
|
||||
# rspamd dynmaps:
|
||||
server {
|
||||
listen 8081;
|
||||
{% if not DISABLE_IPv6 %}
|
||||
{% if ENABLE_IPV6 %}
|
||||
listen [::]:8081;
|
||||
{%endif%}
|
||||
index index.php index.html;
|
||||
@@ -159,6 +167,31 @@ http {
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 9082 ssl http2;
|
||||
|
||||
ssl_certificate /etc/ssl/mail/cert.pem;
|
||||
ssl_certificate_key /etc/ssl/mail/key.pem;
|
||||
|
||||
index mailcowauth.php;
|
||||
server_name _;
|
||||
error_log /var/log/nginx/error.log;
|
||||
access_log /var/log/nginx/access.log;
|
||||
root /mailcowauth;
|
||||
client_max_body_size 10M;
|
||||
location ~ \.php$ {
|
||||
client_max_body_size 10M;
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass phpfpm:9001;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
|
||||
{% for cert in valid_cert_dirs %}
|
||||
server {
|
||||
{% if not HTTP_REDIRECT %}
|
||||
@@ -166,7 +199,7 @@ http {
|
||||
{%endif%}
|
||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||
|
||||
{% if not DISABLE_IPv6 %}
|
||||
{% if ENABLE_IPV6 %}
|
||||
{% if not HTTP_REDIRECT %}
|
||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||
{%endif%}
|
||||
@@ -183,6 +216,4 @@ http {
|
||||
include /etc/nginx/includes/sites-default.conf;
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
|
||||
@@ -76,6 +76,14 @@ location ^~ /.well-known/acme-challenge/ {
|
||||
allow all;
|
||||
default_type "text/plain";
|
||||
}
|
||||
location ^~ /.well-known/mta-sts.txt {
|
||||
allow all;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass {{ PHPFPMHOST }}:9002;
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/mta-sts.php;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
|
||||
rewrite ^/.well-known/caldav$ /SOGo/dav/ permanent;
|
||||
rewrite ^/.well-known/carddav$ /SOGo/dav/ permanent;
|
||||
|
||||
231
data/conf/phpfpm/crons/keycloak-sync.php
Normal file
231
data/conf/phpfpm/crons/keycloak-sync.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
require_once(__DIR__ . '/../web/inc/vars.inc.php');
|
||||
if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
|
||||
include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
|
||||
}
|
||||
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
|
||||
|
||||
// Init database
|
||||
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
|
||||
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
try {
|
||||
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
logMsg("err", $e->getMessage());
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Init Valkey
|
||||
$valkey = new Redis();
|
||||
try {
|
||||
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
|
||||
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
|
||||
}
|
||||
else {
|
||||
$valkey->connect('valkey-mailcow', 6379);
|
||||
}
|
||||
$valkey->auth(getenv("VALKEYPASS"));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
echo "Exiting: " . $e->getMessage();
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
function logMsg($priority, $message, $task = "Keycloak Sync") {
|
||||
global $valkey;
|
||||
|
||||
$finalMsg = array(
|
||||
"time" => time(),
|
||||
"priority" => $priority,
|
||||
"task" => $task,
|
||||
"message" => $message
|
||||
);
|
||||
$valkey->lPush('CRON_LOG', json_encode($finalMsg));
|
||||
}
|
||||
|
||||
// Load core functions first
|
||||
require_once __DIR__ . '/../web/inc/functions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/sessions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.ratelimit.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.acl.inc.php';
|
||||
|
||||
$_SESSION['mailcow_cc_username'] = "admin";
|
||||
$_SESSION['mailcow_cc_role'] = "admin";
|
||||
$_SESSION['acl']['tls_policy'] = "1";
|
||||
$_SESSION['acl']['quarantine_notification'] = "1";
|
||||
$_SESSION['acl']['quarantine_category'] = "1";
|
||||
$_SESSION['acl']['ratelimit'] = "1";
|
||||
$_SESSION['acl']['sogo_access'] = "1";
|
||||
$_SESSION['acl']['protocol_access'] = "1";
|
||||
$_SESSION['acl']['mailbox_relayhost'] = "1";
|
||||
$_SESSION['acl']['unlimited_quota'] = "1";
|
||||
|
||||
$iam_settings = identity_provider('get');
|
||||
if ($iam_settings['authsource'] != "keycloak" || (intval($iam_settings['periodic_sync']) != 1 && intval($iam_settings['import_users']) != 1)) {
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set pagination variables
|
||||
$start = 0;
|
||||
$max = 100;
|
||||
|
||||
// lock sync if already running
|
||||
$lock_file = '/tmp/iam-sync.lock';
|
||||
if (file_exists($lock_file)) {
|
||||
$lock_file_parts = explode("\n", file_get_contents($lock_file));
|
||||
$pid = $lock_file_parts[0];
|
||||
if (count($lock_file_parts) > 1){
|
||||
$last_execution = $lock_file_parts[1];
|
||||
$elapsed_time = (time() - $last_execution) / 60;
|
||||
if ($elapsed_time < intval($iam_settings['sync_interval'])) {
|
||||
logMsg("warning", "Sync not ready (".number_format((float)$elapsed_time, 2, '.', '')."min / ".$iam_settings['sync_interval']."min)");
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (posix_kill($pid, 0)) {
|
||||
logMsg("warning", "Sync is already running");
|
||||
session_destroy();
|
||||
exit;
|
||||
} else {
|
||||
unlink($lock_file);
|
||||
}
|
||||
}
|
||||
$lock_file_handle = fopen($lock_file, 'w');
|
||||
fwrite($lock_file_handle, getmypid());
|
||||
fclose($lock_file_handle);
|
||||
|
||||
// Init Keycloak Provider
|
||||
$iam_provider = identity_provider('init');
|
||||
|
||||
// Loop until all users have been retrieved
|
||||
while (true) {
|
||||
// Get admin access token
|
||||
$admin_token = identity_provider("get-keycloak-admin-token");
|
||||
|
||||
// Make the API request to retrieve the users
|
||||
$url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users?first=$start&max=$max";
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Content-Type: application/json",
|
||||
"Authorization: Bearer " . $admin_token
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($code != 200){
|
||||
logMsg("err", "Received HTTP {$code}");
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
try {
|
||||
$response = json_decode($response, true);
|
||||
} catch (Exception $e) {
|
||||
logMsg("err", $e->getMessage());
|
||||
break;
|
||||
}
|
||||
if (!is_array($response)){
|
||||
logMsg("err", "Received malformed response from keycloak api");
|
||||
break;
|
||||
}
|
||||
if (count($response) == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Process the batch of users
|
||||
foreach ($response as $user) {
|
||||
if (empty($user['email'])){
|
||||
logMsg("warning", "No email address in keycloak found for user " . $user['name']);
|
||||
continue;
|
||||
}
|
||||
|
||||
// try get mailbox user
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
mailbox.*,
|
||||
domain.active AS d_active
|
||||
FROM `mailbox`
|
||||
INNER JOIN domain on mailbox.domain = domain.domain
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $user['email']));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// check if matching attribute mapping exists
|
||||
$user_template = $user['attributes']['mailcow_template'][0];
|
||||
$mapper_key = array_search($user_template, $iam_settings['mappers']);
|
||||
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
if (!$row && intval($iam_settings['import_users']) == 1){
|
||||
if ($mapper_key === false){
|
||||
if (!empty($iam_settings['default_template'])) {
|
||||
$mbox_template = $iam_settings['default_template'];
|
||||
logMsg("warning", "Using default template for user " . $user['email']);
|
||||
} else {
|
||||
logMsg("warning", "No matching attribute mapping found for user " . $user['email']);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
}
|
||||
// mailbox user does not exist, create...
|
||||
logMsg("info", "Creating user " . $user['email']);
|
||||
$create_res = mailbox('add', 'mailbox_from_template', array(
|
||||
'domain' => explode('@', $user['email'])[1],
|
||||
'local_part' => explode('@', $user['email'])[0],
|
||||
'name' => $user['firstName'] . " " . $user['lastName'],
|
||||
'authsource' => 'keycloak',
|
||||
'template' => $mbox_template
|
||||
));
|
||||
if (!$create_res){
|
||||
logMsg("err", "Could not create user " . $user['email']);
|
||||
continue;
|
||||
}
|
||||
} else if ($row && intval($iam_settings['periodic_sync']) == 1 && $row['authsource'] == "keycloak") {
|
||||
if ($mapper_key === false){
|
||||
logMsg("warning", "No matching attribute mapping found for user " . $user['email']);
|
||||
continue;
|
||||
}
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
// mailbox user does exist, sync attribtues...
|
||||
logMsg("info", "Syncing attributes for user " . $user['email']);
|
||||
mailbox('edit', 'mailbox_from_template', array(
|
||||
'username' => $user['email'],
|
||||
'name' => $user['firstName'] . " " . $user['lastName'],
|
||||
'template' => $mbox_template
|
||||
));
|
||||
} else {
|
||||
// skip mailbox user
|
||||
logMsg("info", "Skipping user " . $user['email']);
|
||||
}
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
|
||||
sleep(0.025);
|
||||
}
|
||||
|
||||
// Update the pagination variables for the next batch
|
||||
$start += $max;
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
logMsg("info", "DONE!");
|
||||
// add last execution time to lock file
|
||||
$lock_file_handle = fopen($lock_file, 'w');
|
||||
fwrite($lock_file_handle, getmypid() . "\n" . time());
|
||||
fclose($lock_file_handle);
|
||||
session_destroy();
|
||||
198
data/conf/phpfpm/crons/ldap-sync.php
Normal file
198
data/conf/phpfpm/crons/ldap-sync.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
require_once(__DIR__ . '/../web/inc/vars.inc.php');
|
||||
if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
|
||||
include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
|
||||
}
|
||||
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
|
||||
|
||||
// Init database
|
||||
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
|
||||
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
try {
|
||||
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
logMsg("err", $e->getMessage());
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Init Valkey
|
||||
$valkey = new Redis();
|
||||
try {
|
||||
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
|
||||
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
|
||||
}
|
||||
else {
|
||||
$valkey->connect('valkey-mailcow', 6379);
|
||||
}
|
||||
$valkey->auth(getenv("VALKEYPASS"));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
echo "Exiting: " . $e->getMessage();
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
function logMsg($priority, $message, $task = "LDAP Sync") {
|
||||
global $valkey;
|
||||
|
||||
$finalMsg = array(
|
||||
"time" => time(),
|
||||
"priority" => $priority,
|
||||
"task" => $task,
|
||||
"message" => $message
|
||||
);
|
||||
$valkey->lPush('CRON_LOG', json_encode($finalMsg));
|
||||
}
|
||||
|
||||
// Load core functions first
|
||||
require_once __DIR__ . '/../web/inc/functions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/sessions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.ratelimit.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.acl.inc.php';
|
||||
|
||||
$_SESSION['mailcow_cc_username'] = "admin";
|
||||
$_SESSION['mailcow_cc_role'] = "admin";
|
||||
$_SESSION['acl']['tls_policy'] = "1";
|
||||
$_SESSION['acl']['quarantine_notification'] = "1";
|
||||
$_SESSION['acl']['quarantine_category'] = "1";
|
||||
$_SESSION['acl']['ratelimit'] = "1";
|
||||
$_SESSION['acl']['sogo_access'] = "1";
|
||||
$_SESSION['acl']['protocol_access'] = "1";
|
||||
$_SESSION['acl']['mailbox_relayhost'] = "1";
|
||||
$_SESSION['acl']['unlimited_quota'] = "1";
|
||||
|
||||
$iam_settings = identity_provider('get');
|
||||
if ($iam_settings['authsource'] != "ldap" || (intval($iam_settings['periodic_sync']) != 1 && intval($iam_settings['import_users']) != 1)) {
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set pagination variables
|
||||
$start = 0;
|
||||
$max = 100;
|
||||
|
||||
// lock sync if already running
|
||||
$lock_file = '/tmp/iam-sync.lock';
|
||||
if (file_exists($lock_file)) {
|
||||
$lock_file_parts = explode("\n", file_get_contents($lock_file));
|
||||
$pid = $lock_file_parts[0];
|
||||
if (count($lock_file_parts) > 1){
|
||||
$last_execution = $lock_file_parts[1];
|
||||
$elapsed_time = (time() - $last_execution) / 60;
|
||||
if ($elapsed_time < intval($iam_settings['sync_interval'])) {
|
||||
logMsg("warning", "Sync not ready (".number_format((float)$elapsed_time, 2, '.', '')."min / ".$iam_settings['sync_interval']."min)");
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (posix_kill($pid, 0)) {
|
||||
logMsg("warning", "Sync is already running");
|
||||
session_destroy();
|
||||
exit;
|
||||
} else {
|
||||
unlink($lock_file);
|
||||
}
|
||||
}
|
||||
$lock_file_handle = fopen($lock_file, 'w');
|
||||
fwrite($lock_file_handle, getmypid());
|
||||
fclose($lock_file_handle);
|
||||
|
||||
// Init Provider
|
||||
$iam_provider = identity_provider('init');
|
||||
|
||||
// Get ldap users
|
||||
$ldap_query = $iam_provider->query();
|
||||
if (!empty($iam_settings['filter'])) {
|
||||
$ldap_query = $ldap_query->rawFilter($iam_settings['filter']);
|
||||
}
|
||||
$response = $ldap_query->where($iam_settings['username_field'], "*")
|
||||
->where($iam_settings['attribute_field'], "*")
|
||||
->select([$iam_settings['username_field'], $iam_settings['attribute_field'], 'displayname'])
|
||||
->paginate($max);
|
||||
|
||||
// Process the users
|
||||
foreach ($response as $user) {
|
||||
// try get mailbox user
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
mailbox.*,
|
||||
domain.active AS d_active
|
||||
FROM `mailbox`
|
||||
INNER JOIN domain on mailbox.domain = domain.domain
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $user[$iam_settings['username_field']][0]));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// check if matching attribute mapping exists
|
||||
$user_template = $user[$iam_settings['attribute_field']][0];
|
||||
$mapper_key = array_search($user_template, $iam_settings['mappers']);
|
||||
|
||||
if (empty($user[$iam_settings['username_field']][0])){
|
||||
logMsg("warning", "Skipping user " . $user['displayname'][0] . " due to empty LDAP ". $iam_settings['username_field'] . " property.");
|
||||
continue;
|
||||
}
|
||||
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
if (!$row && intval($iam_settings['import_users']) == 1){
|
||||
if ($mapper_key === false){
|
||||
if (!empty($iam_settings['default_template'])) {
|
||||
$mbox_template = $iam_settings['default_template'];
|
||||
} else {
|
||||
logMsg("warning", "No matching attribute mapping found for user " . $user[$iam_settings['username_field']][0]);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
}
|
||||
// mailbox user does not exist, create...
|
||||
logMsg("info", "Creating user " . $user[$iam_settings['username_field']][0]);
|
||||
$create_res = mailbox('add', 'mailbox_from_template', array(
|
||||
'domain' => explode('@', $user[$iam_settings['username_field']][0])[1],
|
||||
'local_part' => explode('@', $user[$iam_settings['username_field']][0])[0],
|
||||
'name' => $user['displayname'][0],
|
||||
'authsource' => 'ldap',
|
||||
'template' => $mbox_template
|
||||
));
|
||||
if (!$create_res){
|
||||
logMsg("err", "Could not create user " . $user[$iam_settings['username_field']][0]);
|
||||
continue;
|
||||
}
|
||||
} else if ($row && intval($iam_settings['periodic_sync']) == 1 && $row['authsource'] == "ldap") {
|
||||
if ($mapper_key === false){
|
||||
logMsg("warning", "No matching attribute mapping found for user " . $user[$iam_settings['username_field']][0]);
|
||||
continue;
|
||||
}
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
// mailbox user does exist, sync attribtues...
|
||||
logMsg("info", "Syncing attributes for user " . $user[$iam_settings['username_field']][0]);
|
||||
mailbox('edit', 'mailbox_from_template', array(
|
||||
'username' => $user[$iam_settings['username_field']][0],
|
||||
'name' => $user['displayname'][0],
|
||||
'template' => $mbox_template
|
||||
));
|
||||
} else {
|
||||
// skip mailbox user
|
||||
logMsg("info", "Skipping user " . $user[$iam_settings['username_field']][0]);
|
||||
}
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
|
||||
sleep(0.025);
|
||||
}
|
||||
|
||||
logMsg("info", "DONE!");
|
||||
// add last execution time to lock file
|
||||
$lock_file_handle = fopen($lock_file, 'w');
|
||||
fwrite($lock_file_handle, getmypid() . "\n" . time());
|
||||
fclose($lock_file_handle);
|
||||
session_destroy();
|
||||
@@ -152,7 +152,7 @@ smtp_sasl_auth_enable = yes
|
||||
smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
|
||||
smtp_sasl_security_options =
|
||||
smtp_sasl_mechanism_filter = plain, login
|
||||
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf
|
||||
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf socketmap:inet:postfix-tlspol:8642:QUERY
|
||||
smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
|
||||
mail_name = Postcow
|
||||
# local_transport map catches local destinations and prevents routing local dests when the next map would route "*"
|
||||
|
||||
@@ -1,32 +1,71 @@
|
||||
# Whitelist generated by Postwhite v3.4 on Sat Feb 1 00:18:03 UTC 2025
|
||||
# Whitelist generated by Postwhite v3.4 on Wed Oct 1 00:21:33 UTC 2025
|
||||
# https://github.com/stevejenkins/postwhite/
|
||||
# 1984 total rules
|
||||
# 2216 total rules
|
||||
2a00:1450:4000::/36 permit
|
||||
2a01:111:f400::/48 permit
|
||||
2a01:111:f403:8000::/50 permit
|
||||
2a01:111:f403:2800::/53 permit
|
||||
2a01:111:f403:8000::/51 permit
|
||||
2a01:111:f403::/49 permit
|
||||
2a01:111:f403:c000::/51 permit
|
||||
2a01:111:f403:d000::/53 permit
|
||||
2a01:111:f403:f000::/52 permit
|
||||
2a01:238:20a:202:5370::1 permit
|
||||
2a01:238:20a:202:5372::1 permit
|
||||
2a01:238:20a:202:5373::1 permit
|
||||
2a01:238:400:101:53::1 permit
|
||||
2a01:238:400:102:53::1 permit
|
||||
2a01:238:400:103:53::1 permit
|
||||
2a01:238:400:301:53::1 permit
|
||||
2a01:238:400:302:53::1 permit
|
||||
2a01:238:400:303:53::1 permit
|
||||
2a01:238:400:470:53::1 permit
|
||||
2a01:238:400:471:53::1 permit
|
||||
2a01:238:400:472:53::1 permit
|
||||
2a01:b747:3000:200::/56 permit
|
||||
2a01:b747:3001:200::/56 permit
|
||||
2a01:b747:3002:200::/56 permit
|
||||
2a01:b747:3003:200::/56 permit
|
||||
2a01:b747:3004:200::/56 permit
|
||||
2a01:b747:3005:200::/56 permit
|
||||
2a01:b747:3006:200::/56 permit
|
||||
2a02:a60:0:5::/64 permit
|
||||
2c0f:fb50:4000::/36 permit
|
||||
2.207.151.53 permit
|
||||
2.207.217.30 permit
|
||||
3.64.237.68 permit
|
||||
3.65.3.180 permit
|
||||
3.70.123.177 permit
|
||||
3.72.182.33 permit
|
||||
3.74.81.189 permit
|
||||
3.74.125.228 permit
|
||||
3.75.33.185 permit
|
||||
3.93.157.0/24 permit
|
||||
3.94.40.108 permit
|
||||
3.121.107.214 permit
|
||||
3.129.120.190 permit
|
||||
3.210.190.0/24 permit
|
||||
3.211.80.218 permit
|
||||
3.216.221.67 permit
|
||||
3.221.209.22 permit
|
||||
8.20.114.31 permit
|
||||
8.25.194.0/23 permit
|
||||
8.25.196.0/23 permit
|
||||
10.162.0.0/16 permit
|
||||
8.36.116.0/24 permit
|
||||
8.39.54.0/23 permit
|
||||
8.39.54.250/31 permit
|
||||
8.39.144.0/24 permit
|
||||
8.40.222.0/23 permit
|
||||
8.40.222.250/31 permit
|
||||
12.130.86.238 permit
|
||||
13.107.213.41 permit
|
||||
13.107.246.41 permit
|
||||
13.110.208.0/21 permit
|
||||
13.110.209.0/24 permit
|
||||
13.110.216.0/22 permit
|
||||
13.110.224.0/20 permit
|
||||
13.111.0.0/16 permit
|
||||
13.111.191.0/24 permit
|
||||
13.216.7.111 permit
|
||||
13.216.54.180 permit
|
||||
15.200.21.50 permit
|
||||
15.200.44.248 permit
|
||||
15.200.201.185 permit
|
||||
@@ -35,25 +74,30 @@
|
||||
17.57.156.0/24 permit
|
||||
17.58.0.0/16 permit
|
||||
17.142.0.0/15 permit
|
||||
17.143.234.140/30 permit
|
||||
18.97.0.8/30 permit
|
||||
18.97.1.184/29 permit
|
||||
18.97.2.64/26 permit
|
||||
18.156.89.250 permit
|
||||
18.156.205.64 permit
|
||||
18.157.70.148 permit
|
||||
18.157.114.255 permit
|
||||
18.157.243.190 permit
|
||||
18.158.153.154 permit
|
||||
18.194.95.56 permit
|
||||
18.197.217.180 permit
|
||||
18.198.96.88 permit
|
||||
18.199.210.3 permit
|
||||
18.207.52.234 permit
|
||||
18.208.124.128/25 permit
|
||||
18.216.232.154 permit
|
||||
18.235.27.253 permit
|
||||
18.236.40.242 permit
|
||||
18.236.56.161 permit
|
||||
20.51.6.32/30 permit
|
||||
20.51.98.61 permit
|
||||
20.52.52.2 permit
|
||||
20.52.128.133 permit
|
||||
20.59.80.4/30 permit
|
||||
20.63.210.192/28 permit
|
||||
20.69.8.108/30 permit
|
||||
20.70.246.20 permit
|
||||
20.76.201.171 permit
|
||||
20.83.222.104/30 permit
|
||||
20.88.157.184/30 permit
|
||||
20.94.180.64/28 permit
|
||||
@@ -62,14 +106,11 @@
|
||||
20.98.194.68/30 permit
|
||||
20.105.209.76/30 permit
|
||||
20.107.239.64/30 permit
|
||||
20.112.250.133 permit
|
||||
20.118.139.208/30 permit
|
||||
20.141.10.196 permit
|
||||
20.185.214.0/27 permit
|
||||
20.185.214.32/27 permit
|
||||
20.185.214.64/27 permit
|
||||
20.231.239.246 permit
|
||||
20.236.44.162 permit
|
||||
23.103.224.0/19 permit
|
||||
23.249.208.0/20 permit
|
||||
23.251.224.0/19 permit
|
||||
@@ -81,6 +122,7 @@
|
||||
23.253.183.147 permit
|
||||
23.253.183.148 permit
|
||||
23.253.183.150 permit
|
||||
24.110.64.0/18 permit
|
||||
27.123.204.128/30 permit
|
||||
27.123.204.132/31 permit
|
||||
27.123.204.148/30 permit
|
||||
@@ -93,19 +135,51 @@
|
||||
27.123.206.56/29 permit
|
||||
27.123.206.76/30 permit
|
||||
27.123.206.80/28 permit
|
||||
31.25.48.222 permit
|
||||
31.47.251.17 permit
|
||||
31.186.239.0/24 permit
|
||||
34.2.64.0/22 permit
|
||||
34.2.68.0/23 permit
|
||||
34.2.70.0/23 permit
|
||||
34.2.71.64/26 permit
|
||||
34.2.72.0/22 permit
|
||||
34.2.75.0/26 permit
|
||||
34.2.78.0/23 permit
|
||||
34.2.80.0/23 permit
|
||||
34.2.82.0/23 permit
|
||||
34.2.84.0/24 permit
|
||||
34.2.84.64/26 permit
|
||||
34.2.85.0/24 permit
|
||||
34.2.85.64/26 permit
|
||||
34.2.86.0/23 permit
|
||||
34.2.88.0/23 permit
|
||||
34.2.90.0/23 permit
|
||||
34.2.92.0/23 permit
|
||||
34.2.94.0/23 permit
|
||||
34.70.158.162 permit
|
||||
34.74.74.140 permit
|
||||
34.83.159.189 permit
|
||||
34.141.160.224 permit
|
||||
34.193.58.168 permit
|
||||
34.195.217.107 permit
|
||||
34.197.10.50 permit
|
||||
34.197.254.9 permit
|
||||
34.198.94.229 permit
|
||||
34.198.218.121 permit
|
||||
34.212.163.75 permit
|
||||
34.215.104.144 permit
|
||||
34.218.116.3 permit
|
||||
34.218.115.239 permit
|
||||
34.225.212.172 permit
|
||||
34.241.242.183 permit
|
||||
35.83.148.184 permit
|
||||
35.155.198.111 permit
|
||||
35.158.23.94 permit
|
||||
35.161.32.253 permit
|
||||
35.162.73.231 permit
|
||||
35.167.93.243 permit
|
||||
35.174.145.124 permit
|
||||
35.176.132.251 permit
|
||||
35.190.247.0/24 permit
|
||||
35.191.0.0/16 permit
|
||||
35.205.92.9 permit
|
||||
35.228.216.85 permit
|
||||
35.242.169.159 permit
|
||||
37.188.97.188 permit
|
||||
37.218.248.47 permit
|
||||
@@ -119,12 +193,21 @@
|
||||
40.233.64.216 permit
|
||||
40.233.83.78 permit
|
||||
40.233.88.28 permit
|
||||
43.239.212.33 permit
|
||||
44.206.138.57 permit
|
||||
44.210.169.44 permit
|
||||
44.217.45.156 permit
|
||||
44.236.56.93 permit
|
||||
44.238.220.251 permit
|
||||
44.245.243.92 permit
|
||||
44.246.1.125 permit
|
||||
44.246.68.102 permit
|
||||
44.246.77.92 permit
|
||||
45.14.148.0/22 permit
|
||||
46.19.170.16 permit
|
||||
45.143.132.0/24 permit
|
||||
45.143.133.0/24 permit
|
||||
45.143.134.0/24 permit
|
||||
45.143.135.0/24 permit
|
||||
46.226.48.0/21 permit
|
||||
46.228.36.37 permit
|
||||
46.228.36.38/31 permit
|
||||
@@ -175,6 +258,7 @@
|
||||
46.243.88.177 permit
|
||||
46.243.95.179 permit
|
||||
46.243.95.180 permit
|
||||
50.16.246.183 permit
|
||||
50.18.45.249 permit
|
||||
50.18.121.236 permit
|
||||
50.18.121.248 permit
|
||||
@@ -188,14 +272,24 @@
|
||||
50.56.130.220 permit
|
||||
50.56.130.221 permit
|
||||
50.56.130.222 permit
|
||||
50.112.246.219 permit
|
||||
52.1.14.157 permit
|
||||
52.5.230.59 permit
|
||||
52.6.74.205 permit
|
||||
52.12.53.23 permit
|
||||
52.13.214.179 permit
|
||||
52.26.1.71 permit
|
||||
52.27.5.72 permit
|
||||
52.27.28.47 permit
|
||||
52.28.63.81 permit
|
||||
52.28.197.132 permit
|
||||
52.34.181.151 permit
|
||||
52.35.192.45 permit
|
||||
52.36.138.31 permit
|
||||
52.37.142.146 permit
|
||||
52.42.203.116 permit
|
||||
52.50.24.208 permit
|
||||
52.57.120.243 permit
|
||||
52.58.216.183 permit
|
||||
52.59.143.3 permit
|
||||
52.60.41.5 permit
|
||||
@@ -238,23 +332,24 @@
|
||||
54.174.63.0/24 permit
|
||||
54.186.193.102 permit
|
||||
54.191.223.56 permit
|
||||
54.211.126.101 permit
|
||||
54.213.20.246 permit
|
||||
54.214.39.184 permit
|
||||
54.240.0.0/18 permit
|
||||
54.240.64.0/19 permit
|
||||
54.240.96.0/19 permit
|
||||
54.240.64.0/18 permit
|
||||
54.241.16.209 permit
|
||||
54.244.54.130 permit
|
||||
54.244.242.0/24 permit
|
||||
54.255.61.23 permit
|
||||
56.124.6.228 permit
|
||||
57.103.64.0/18 permit
|
||||
57.129.93.249 permit
|
||||
62.13.128.0/24 permit
|
||||
62.13.129.128/25 permit
|
||||
62.13.136.0/21 permit
|
||||
62.13.144.0/21 permit
|
||||
62.13.152.0/21 permit
|
||||
62.17.146.128/26 permit
|
||||
62.179.121.0/24 permit
|
||||
62.201.172.0/27 permit
|
||||
62.201.172.32/27 permit
|
||||
62.253.227.114 permit
|
||||
@@ -262,6 +357,9 @@
|
||||
63.128.21.0/24 permit
|
||||
63.143.57.128/25 permit
|
||||
63.143.59.128/25 permit
|
||||
63.176.194.123 permit
|
||||
63.178.132.221 permit
|
||||
63.178.143.178 permit
|
||||
64.18.0.0/20 permit
|
||||
64.20.241.45 permit
|
||||
64.69.212.0/24 permit
|
||||
@@ -274,6 +372,7 @@
|
||||
64.127.115.252 permit
|
||||
64.132.88.0/23 permit
|
||||
64.132.92.0/24 permit
|
||||
64.181.194.190 permit
|
||||
64.207.219.7 permit
|
||||
64.207.219.8 permit
|
||||
64.207.219.9 permit
|
||||
@@ -327,10 +426,10 @@
|
||||
65.110.161.77 permit
|
||||
65.123.29.213 permit
|
||||
65.123.29.220 permit
|
||||
65.154.166.0/24 permit
|
||||
65.212.180.36 permit
|
||||
66.102.0.0/20 permit
|
||||
66.119.150.192/26 permit
|
||||
66.162.193.226/31 permit
|
||||
66.163.184.0/24 permit
|
||||
66.163.185.0/24 permit
|
||||
66.163.186.0/24 permit
|
||||
@@ -536,7 +635,6 @@
|
||||
74.86.241.250/31 permit
|
||||
74.112.67.243 permit
|
||||
74.125.0.0/16 permit
|
||||
74.202.227.40 permit
|
||||
74.208.4.200 permit
|
||||
74.208.4.201 permit
|
||||
74.208.4.220 permit
|
||||
@@ -565,6 +663,11 @@
|
||||
77.238.189.142 permit
|
||||
77.238.189.146/31 permit
|
||||
77.238.189.148/30 permit
|
||||
79.135.106.0/24 permit
|
||||
79.135.107.0/24 permit
|
||||
81.169.146.243 permit
|
||||
81.169.146.245 permit
|
||||
81.169.146.246 permit
|
||||
81.223.46.0/27 permit
|
||||
82.165.159.2 permit
|
||||
82.165.159.3 permit
|
||||
@@ -580,10 +683,17 @@
|
||||
82.165.159.45 permit
|
||||
82.165.159.130 permit
|
||||
82.165.159.131 permit
|
||||
84.116.6.0/23 permit
|
||||
84.116.36.0/24 permit
|
||||
84.116.50.0/23 permit
|
||||
85.9.206.169 permit
|
||||
85.9.210.45 permit
|
||||
85.158.136.0/21 permit
|
||||
85.215.255.39 permit
|
||||
85.215.255.40 permit
|
||||
85.215.255.41 permit
|
||||
85.215.255.45 permit
|
||||
85.215.255.46 permit
|
||||
85.215.255.47 permit
|
||||
85.215.255.48 permit
|
||||
85.215.255.49 permit
|
||||
86.61.88.25 permit
|
||||
87.238.80.0/21 permit
|
||||
87.248.103.12 permit
|
||||
@@ -623,12 +733,13 @@
|
||||
87.248.117.205 permit
|
||||
87.253.232.0/21 permit
|
||||
89.22.108.0/24 permit
|
||||
91.198.2.0/24 permit
|
||||
91.211.240.0/22 permit
|
||||
94.169.2.0/23 permit
|
||||
94.236.119.0/26 permit
|
||||
94.245.112.0/27 permit
|
||||
94.245.112.10/31 permit
|
||||
95.131.104.0/21 permit
|
||||
95.217.114.154 permit
|
||||
96.43.144.0/20 permit
|
||||
96.43.144.64/28 permit
|
||||
96.43.144.64/31 permit
|
||||
@@ -1122,15 +1233,14 @@
|
||||
99.83.190.102 permit
|
||||
103.9.96.0/22 permit
|
||||
103.28.42.0/24 permit
|
||||
103.84.217.238 permit
|
||||
103.89.75.238 permit
|
||||
103.151.192.0/23 permit
|
||||
103.168.172.128/27 permit
|
||||
103.237.104.0/22 permit
|
||||
104.43.243.237 permit
|
||||
104.44.112.128/25 permit
|
||||
104.47.0.0/17 permit
|
||||
104.47.20.0/23 permit
|
||||
104.47.75.0/24 permit
|
||||
104.47.108.0/23 permit
|
||||
104.130.96.0/28 permit
|
||||
104.130.122.0/23 permit
|
||||
106.10.144.64/27 permit
|
||||
@@ -1256,6 +1366,7 @@
|
||||
106.50.16.0/28 permit
|
||||
107.20.18.111 permit
|
||||
107.20.210.250 permit
|
||||
107.22.191.150 permit
|
||||
108.174.0.0/24 permit
|
||||
108.174.0.215 permit
|
||||
108.174.3.0/24 permit
|
||||
@@ -1264,9 +1375,8 @@
|
||||
108.174.6.215 permit
|
||||
108.175.18.45 permit
|
||||
108.175.30.45 permit
|
||||
108.177.8.0/21 permit
|
||||
108.177.96.0/19 permit
|
||||
108.179.144.0/20 permit
|
||||
109.224.244.0/24 permit
|
||||
109.237.142.0/24 permit
|
||||
111.221.23.128/25 permit
|
||||
111.221.26.0/27 permit
|
||||
@@ -1290,6 +1400,9 @@
|
||||
117.120.16.0/21 permit
|
||||
119.42.242.52/31 permit
|
||||
119.42.242.156 permit
|
||||
121.244.91.48 permit
|
||||
121.244.91.52 permit
|
||||
122.15.156.182 permit
|
||||
123.126.78.64/29 permit
|
||||
124.108.96.24/31 permit
|
||||
124.108.96.28/31 permit
|
||||
@@ -1338,7 +1451,6 @@
|
||||
129.213.195.191 permit
|
||||
130.61.9.72 permit
|
||||
130.162.39.83 permit
|
||||
130.211.0.0/22 permit
|
||||
130.248.172.0/24 permit
|
||||
130.248.173.0/24 permit
|
||||
131.253.30.0/24 permit
|
||||
@@ -1347,12 +1459,28 @@
|
||||
132.226.26.225 permit
|
||||
132.226.49.32 permit
|
||||
132.226.56.24 permit
|
||||
134.128.64.0/19 permit
|
||||
134.128.96.0/19 permit
|
||||
134.170.27.8 permit
|
||||
134.170.113.0/26 permit
|
||||
134.170.141.64/26 permit
|
||||
134.170.143.0/24 permit
|
||||
134.170.174.0/24 permit
|
||||
135.84.80.0/24 permit
|
||||
135.84.81.0/24 permit
|
||||
135.84.82.0/24 permit
|
||||
135.84.83.0/24 permit
|
||||
135.84.216.0/22 permit
|
||||
136.143.160.0/24 permit
|
||||
136.143.161.0/24 permit
|
||||
136.143.162.0/24 permit
|
||||
136.143.176.0/24 permit
|
||||
136.143.177.0/24 permit
|
||||
136.143.178.49 permit
|
||||
136.143.182.0/23 permit
|
||||
136.143.184.0/24 permit
|
||||
136.143.188.0/24 permit
|
||||
136.143.190.0/23 permit
|
||||
136.147.128.0/20 permit
|
||||
136.147.135.0/24 permit
|
||||
136.147.176.0/20 permit
|
||||
@@ -1367,6 +1495,7 @@
|
||||
139.138.46.219 permit
|
||||
139.138.57.55 permit
|
||||
139.138.58.119 permit
|
||||
139.167.79.86 permit
|
||||
139.180.17.0/24 permit
|
||||
140.238.148.191 permit
|
||||
141.148.159.229 permit
|
||||
@@ -1401,6 +1530,7 @@
|
||||
146.20.215.0/24 permit
|
||||
146.20.215.182 permit
|
||||
146.88.28.0/24 permit
|
||||
146.148.116.76 permit
|
||||
147.154.32.0/25 permit
|
||||
147.243.1.47 permit
|
||||
147.243.1.48 permit
|
||||
@@ -1410,7 +1540,7 @@
|
||||
148.105.0.0/16 permit
|
||||
148.105.8.0/21 permit
|
||||
149.72.0.0/16 permit
|
||||
149.72.223.204 permit
|
||||
149.72.234.184 permit
|
||||
149.72.248.236 permit
|
||||
149.97.173.180 permit
|
||||
150.230.98.160 permit
|
||||
@@ -1464,6 +1594,9 @@
|
||||
159.135.224.0/20 permit
|
||||
159.135.228.10 permit
|
||||
159.183.0.0/16 permit
|
||||
159.183.68.71 permit
|
||||
159.183.79.38 permit
|
||||
159.183.129.172 permit
|
||||
160.1.62.192 permit
|
||||
161.38.192.0/20 permit
|
||||
161.38.204.0/22 permit
|
||||
@@ -1480,8 +1613,14 @@
|
||||
163.114.132.120 permit
|
||||
163.114.134.16 permit
|
||||
163.114.135.16 permit
|
||||
163.116.128.0/17 permit
|
||||
163.192.116.87 permit
|
||||
164.152.23.32 permit
|
||||
164.152.25.241 permit
|
||||
164.177.132.168/30 permit
|
||||
165.173.128.0/24 permit
|
||||
165.173.180.250/31 permit
|
||||
165.173.182.250/31 permit
|
||||
166.78.68.0/22 permit
|
||||
166.78.68.221 permit
|
||||
166.78.69.169 permit
|
||||
@@ -1506,20 +1645,28 @@
|
||||
168.138.5.36 permit
|
||||
168.138.73.51 permit
|
||||
168.138.77.31 permit
|
||||
168.138.237.153 permit
|
||||
168.245.0.0/17 permit
|
||||
168.245.12.252 permit
|
||||
168.245.46.9 permit
|
||||
168.245.127.231 permit
|
||||
169.148.129.0/24 permit
|
||||
169.148.131.0/24 permit
|
||||
169.148.138.0/24 permit
|
||||
169.148.142.10 permit
|
||||
169.148.142.33 permit
|
||||
169.148.144.0/25 permit
|
||||
169.148.144.10 permit
|
||||
169.148.146.0/23 permit
|
||||
169.148.175.3 permit
|
||||
169.148.188.0/24 permit
|
||||
169.148.188.182 permit
|
||||
170.10.128.0/24 permit
|
||||
170.10.129.0/24 permit
|
||||
170.10.132.56/29 permit
|
||||
170.10.132.64/29 permit
|
||||
170.10.133.0/24 permit
|
||||
172.217.0.0/19 permit
|
||||
172.217.32.0/20 permit
|
||||
172.217.128.0/19 permit
|
||||
172.217.160.0/20 permit
|
||||
172.217.192.0/19 permit
|
||||
172.253.56.0/21 permit
|
||||
172.253.112.0/20 permit
|
||||
173.0.84.0/29 permit
|
||||
@@ -1549,9 +1696,14 @@
|
||||
182.50.78.64/28 permit
|
||||
183.240.219.64/29 permit
|
||||
185.4.120.0/22 permit
|
||||
185.11.253.128/27 permit
|
||||
185.11.255.0/24 permit
|
||||
185.12.80.0/22 permit
|
||||
185.28.196.0/22 permit
|
||||
185.58.84.93 permit
|
||||
185.70.40.0/24 permit
|
||||
185.70.41.0/24 permit
|
||||
185.70.43.0/24 permit
|
||||
185.80.93.204 permit
|
||||
185.80.93.227 permit
|
||||
185.80.95.31 permit
|
||||
@@ -1559,6 +1711,8 @@
|
||||
185.138.56.128/25 permit
|
||||
185.189.236.0/22 permit
|
||||
185.211.120.0/22 permit
|
||||
185.233.188.0/23 permit
|
||||
185.233.190.0/23 permit
|
||||
185.250.236.0/22 permit
|
||||
185.250.239.148 permit
|
||||
185.250.239.168 permit
|
||||
@@ -1611,6 +1765,7 @@
|
||||
188.125.85.234/31 permit
|
||||
188.125.85.236/31 permit
|
||||
188.125.85.238 permit
|
||||
188.165.51.139 permit
|
||||
188.172.128.0/20 permit
|
||||
192.0.64.0/18 permit
|
||||
192.18.139.154 permit
|
||||
@@ -1633,8 +1788,28 @@
|
||||
193.109.254.0/23 permit
|
||||
193.122.128.100 permit
|
||||
193.123.56.63 permit
|
||||
193.142.157.0/24 permit
|
||||
193.142.157.191 permit
|
||||
193.142.157.198 permit
|
||||
194.19.134.0/25 permit
|
||||
194.25.134.16/28 permit
|
||||
194.25.134.80/28 permit
|
||||
194.64.234.129 permit
|
||||
194.97.196.0/24 permit
|
||||
194.97.196.3 permit
|
||||
194.97.196.4 permit
|
||||
194.97.196.11 permit
|
||||
194.97.196.12 permit
|
||||
194.97.204.0/24 permit
|
||||
194.97.204.3 permit
|
||||
194.97.204.4 permit
|
||||
194.97.204.11 permit
|
||||
194.97.204.12 permit
|
||||
194.97.212.0/24 permit
|
||||
194.97.212.3 permit
|
||||
194.97.212.4 permit
|
||||
194.97.212.11 permit
|
||||
194.97.212.12 permit
|
||||
194.106.220.0/23 permit
|
||||
194.113.24.0/22 permit
|
||||
194.154.193.192/27 permit
|
||||
@@ -1651,6 +1826,14 @@
|
||||
198.61.254.231 permit
|
||||
198.178.234.57 permit
|
||||
198.244.48.0/20 permit
|
||||
198.244.56.107 permit
|
||||
198.244.56.108 permit
|
||||
198.244.56.109 permit
|
||||
198.244.56.111 permit
|
||||
198.244.56.112 permit
|
||||
198.244.56.113 permit
|
||||
198.244.56.114 permit
|
||||
198.244.56.115 permit
|
||||
198.244.59.30 permit
|
||||
198.244.59.33 permit
|
||||
198.244.59.35 permit
|
||||
@@ -1663,7 +1846,16 @@
|
||||
199.16.156.0/22 permit
|
||||
199.33.145.1 permit
|
||||
199.33.145.32 permit
|
||||
199.34.22.36 permit
|
||||
199.59.148.0/22 permit
|
||||
199.67.80.2 permit
|
||||
199.67.80.20 permit
|
||||
199.67.82.2 permit
|
||||
199.67.82.20 permit
|
||||
199.67.84.0/24 permit
|
||||
199.67.86.0/24 permit
|
||||
199.67.88.0/24 permit
|
||||
199.67.90.0/24 permit
|
||||
199.101.161.130 permit
|
||||
199.101.162.0/25 permit
|
||||
199.122.120.0/21 permit
|
||||
@@ -1720,9 +1912,13 @@
|
||||
204.92.114.187 permit
|
||||
204.92.114.203 permit
|
||||
204.92.114.204/31 permit
|
||||
204.141.32.0/23 permit
|
||||
204.141.42.0/23 permit
|
||||
204.216.164.202 permit
|
||||
204.220.160.0/21 permit
|
||||
204.220.168.0/21 permit
|
||||
204.220.176.0/20 permit
|
||||
204.220.181.105 permit
|
||||
204.232.168.0/24 permit
|
||||
205.139.110.0/24 permit
|
||||
205.201.128.0/20 permit
|
||||
@@ -1797,8 +1993,6 @@
|
||||
208.71.42.212/31 permit
|
||||
208.71.42.214 permit
|
||||
208.72.249.240/29 permit
|
||||
208.74.204.5 permit
|
||||
208.74.204.9 permit
|
||||
208.75.120.0/22 permit
|
||||
208.76.62.0/24 permit
|
||||
208.76.63.0/24 permit
|
||||
@@ -1862,6 +2056,8 @@
|
||||
212.227.15.4 permit
|
||||
212.227.15.5 permit
|
||||
212.227.15.6 permit
|
||||
212.227.15.7 permit
|
||||
212.227.15.8 permit
|
||||
212.227.15.14 permit
|
||||
212.227.15.15 permit
|
||||
212.227.15.18 permit
|
||||
@@ -1878,21 +2074,36 @@
|
||||
212.227.15.53 permit
|
||||
212.227.15.54 permit
|
||||
212.227.15.55 permit
|
||||
212.227.17.1 permit
|
||||
212.227.17.2 permit
|
||||
212.227.17.7 permit
|
||||
212.227.17.11 permit
|
||||
212.227.17.12 permit
|
||||
212.227.17.16 permit
|
||||
212.227.17.17 permit
|
||||
212.227.17.18 permit
|
||||
212.227.17.19 permit
|
||||
212.227.17.20 permit
|
||||
212.227.17.21 permit
|
||||
212.227.17.22 permit
|
||||
212.227.17.26 permit
|
||||
212.227.17.27 permit
|
||||
212.227.17.28 permit
|
||||
212.227.17.29 permit
|
||||
212.227.126.206 permit
|
||||
212.227.126.207 permit
|
||||
212.227.126.208 permit
|
||||
212.227.126.209 permit
|
||||
212.227.126.220 permit
|
||||
212.227.126.221 permit
|
||||
212.227.126.222 permit
|
||||
212.227.126.223 permit
|
||||
212.227.126.224 permit
|
||||
212.227.126.225 permit
|
||||
212.227.126.226 permit
|
||||
212.227.126.227 permit
|
||||
213.46.255.0/24 permit
|
||||
213.95.19.64/27 permit
|
||||
213.95.135.4 permit
|
||||
213.199.128.139 permit
|
||||
213.199.128.145 permit
|
||||
213.199.138.181 permit
|
||||
@@ -1902,6 +2113,7 @@
|
||||
216.17.150.242 permit
|
||||
216.17.150.251 permit
|
||||
216.24.224.0/20 permit
|
||||
216.27.86.152/31 permit
|
||||
216.39.60.154/31 permit
|
||||
216.39.60.156/30 permit
|
||||
216.39.60.160/30 permit
|
||||
@@ -1939,6 +2151,8 @@
|
||||
216.99.5.68 permit
|
||||
216.109.114.32/27 permit
|
||||
216.109.114.64/29 permit
|
||||
216.113.162.65 permit
|
||||
216.113.163.65 permit
|
||||
216.128.126.97 permit
|
||||
216.136.162.65 permit
|
||||
216.136.162.120/29 permit
|
||||
@@ -1966,12 +2180,30 @@
|
||||
2001:0868:0100:0600::/64 permit
|
||||
2001:4860:4000::/36 permit
|
||||
2001:748:100:40::2:0/112 permit
|
||||
2001:748:400:1300::3 permit
|
||||
2001:748:400:1300::4 permit
|
||||
2001:748:400:1301::0/64 permit
|
||||
2001:748:400:1301::3 permit
|
||||
2001:748:400:1301::4 permit
|
||||
2001:748:400:2300::3 permit
|
||||
2001:748:400:2300::4 permit
|
||||
2001:748:400:2301::0/64 permit
|
||||
2001:748:400:2301::3 permit
|
||||
2001:748:400:2301::4 permit
|
||||
2001:748:400:3300::3 permit
|
||||
2001:748:400:3300::4 permit
|
||||
2001:748:400:3301::0/64 permit
|
||||
2001:748:400:3301::3 permit
|
||||
2001:748:400:3301::4 permit
|
||||
2404:6800:4000::/36 permit
|
||||
2603:1010:3:3::5b permit
|
||||
2603:1020:201:10::10f permit
|
||||
2603:1030:20e:3::23c permit
|
||||
2603:1030:b:3::152 permit
|
||||
2603:1030:c02:8::14 permit
|
||||
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
|
||||
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
|
||||
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
|
||||
2607:f8b0:4000::/36 permit
|
||||
2620:109:c003:104::/64 permit
|
||||
2620:109:c003:104::215 permit
|
||||
@@ -1985,4 +2217,5 @@
|
||||
2620:119:50c0:207::/64 permit
|
||||
2620:119:50c0:207::215 permit
|
||||
2800:3f0:4000::/36 permit
|
||||
194.25.134.0/24 permit # t-online.de
|
||||
49.12.4.251 permit # checks.mailcow.email
|
||||
2a01:4f8:c17:7906::10 permit # checks.mailcow.email
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
/.+\.guru$/i
|
||||
/.+\.icu$/i
|
||||
/.+\.id$/i
|
||||
/.+\.info$/i
|
||||
/.+\.in.net$/i
|
||||
/.+\.ir$/i
|
||||
/.+\.jetzt$/i
|
||||
|
||||
@@ -133,7 +133,7 @@ try {
|
||||
error_log("ALIAS EXPANDER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
||||
$goto_branch_array = explode(',', $goto_branch);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
||||
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
||||
if ($goto_branch) {
|
||||
|
||||
@@ -56,7 +56,7 @@ function normalize_email($email) {
|
||||
$email = explode('@', $email);
|
||||
$email[0] = str_replace('.', '', $email[0]);
|
||||
$email = implode('@', $email);
|
||||
}
|
||||
}
|
||||
$gm_alt = "@googlemail.com";
|
||||
if (substr_compare($email, $gm_alt, -strlen($gm_alt)) == 0) {
|
||||
$email = explode('@', $email);
|
||||
@@ -114,7 +114,7 @@ function ucl_rcpts($object, $type) {
|
||||
$rcpt[] = str_replace('/', '\/', $row['address']);
|
||||
}
|
||||
// Aliases by alias domains
|
||||
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox`
|
||||
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox`
|
||||
LEFT OUTER JOIN `alias_domain` ON `mailbox`.`domain` = `alias_domain`.`target_domain`
|
||||
WHERE `mailbox`.`username` = :object");
|
||||
$stmt->execute(array(
|
||||
@@ -184,7 +184,7 @@ while ($row = array_shift($rows)) {
|
||||
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
|
||||
<?php
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf`
|
||||
$stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf`
|
||||
WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel')
|
||||
AND `object`= :object");
|
||||
$stmt->execute(array(':object' => $row['object']));
|
||||
@@ -468,4 +468,36 @@ while ($row = array_shift($rows)) {
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
<?php
|
||||
// Start internal aliases
|
||||
|
||||
$stmt = $pdo->query("SELECT `id`, `address`, `domain` FROM `alias` WHERE `active` = '1' AND `internal` = '1'");
|
||||
$aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($alias = array_shift($aliases)) {
|
||||
// build allowed_domains regex and add target domain and alias domains
|
||||
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `active` = '1' AND `target_domain` = :target_domain");
|
||||
$stmt->execute(array(':target_domain' => $alias['domain']));
|
||||
$allowed_domains = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$allowed_domains = array_map(function($item) {
|
||||
return str_replace('.', '\.', $item['alias_domain']);
|
||||
}, $allowed_domains);
|
||||
$allowed_domains[] = str_replace('.', '\.', $alias['domain']);
|
||||
$allowed_domains = implode('|', $allowed_domains);
|
||||
?>
|
||||
internal_alias_<?=$alias['id'];?> {
|
||||
priority = 10;
|
||||
rcpt = "<?=$alias['address'];?>";
|
||||
from = "/^((?!.*@(<?=$allowed_domains;?>)).)*$/";
|
||||
apply "default" {
|
||||
MAILCOW_INTERNAL_ALIAS = 9999.0;
|
||||
}
|
||||
symbols [
|
||||
"MAILCOW_INTERNAL_ALIAS"
|
||||
]
|
||||
}
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ VIRUS_FOUND {
|
||||
}
|
||||
# Bad policy from free mail providers
|
||||
FREEMAIL_POLICY_FAILURE {
|
||||
expression = "FREEMAIL_FROM & !DMARC_POLICY_ALLOW & !MAILLIST& !WHITELISTED_FWD_HOST & -g+:policies";
|
||||
expression = "FREEMAIL_FROM & !DMARC_POLICY_ALLOW & !MAILLIST & !WHITELISTED_FWD_HOST & -g+:policies";
|
||||
score = 16.0;
|
||||
}
|
||||
# Applies to freemail with undisclosed recipients
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
oletools {
|
||||
# default olefy settings
|
||||
servers = "olefy:10055";
|
||||
# needs to be set explicitly for Rspamd < 1.9.5
|
||||
scan_mime_parts = true;
|
||||
# mime-part regex matching in content-type or filename
|
||||
# block all macros
|
||||
extended = true;
|
||||
max_size = 3145728;
|
||||
timeout = 20.0;
|
||||
retransmits = 1;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
servers = "redis:6379";
|
||||
timeout = 10;
|
||||
@@ -102,7 +102,7 @@ rspamd_config:register_symbol({
|
||||
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
|
||||
if #rcpt_split == 2 then
|
||||
if rcpt_split[1] == 'postmaster' then
|
||||
task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt')
|
||||
task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt', 'postmaster')
|
||||
return
|
||||
end
|
||||
end
|
||||
@@ -167,7 +167,7 @@ rspamd_config:register_symbol({
|
||||
for k,v in pairs(data) do
|
||||
if (v and v ~= userdata and v == '1') then
|
||||
rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result")
|
||||
task:set_pre_result('accept', 'ip matched with forward hosts')
|
||||
task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -454,12 +454,18 @@ rspamd_config:register_symbol({
|
||||
local redis_params = rspamd_parse_redis_server('dyn_rl')
|
||||
local rspamd_logger = require "rspamd_logger"
|
||||
local envfrom = task:get_from(1)
|
||||
local envrcpt = task:get_recipients(1) or {}
|
||||
local uname = task:get_user()
|
||||
if not envfrom or not uname then
|
||||
return false
|
||||
end
|
||||
|
||||
local uname = uname:lower()
|
||||
|
||||
if #envrcpt == 1 and envrcpt[1].addr:lower() == uname then
|
||||
return false
|
||||
end
|
||||
|
||||
local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case
|
||||
|
||||
local function redis_cb_user(err, data)
|
||||
@@ -544,13 +550,13 @@ rspamd_config:register_symbol({
|
||||
-- determine newline type
|
||||
local function newline(task)
|
||||
local t = task:get_newlines_type()
|
||||
|
||||
|
||||
if t == 'cr' then
|
||||
return '\r'
|
||||
elseif t == 'lf' then
|
||||
return '\n'
|
||||
end
|
||||
|
||||
|
||||
return '\r\n'
|
||||
end
|
||||
-- retrieve footer
|
||||
@@ -558,7 +564,7 @@ rspamd_config:register_symbol({
|
||||
if err or type(data) ~= 'string' then
|
||||
rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
|
||||
else
|
||||
|
||||
|
||||
-- parse json string
|
||||
local footer = cjson.decode(data)
|
||||
if not footer then
|
||||
@@ -607,26 +613,30 @@ rspamd_config:register_symbol({
|
||||
if footer.plain and footer.plain ~= "" then
|
||||
footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
|
||||
end
|
||||
|
||||
|
||||
-- add footer
|
||||
local out = {}
|
||||
local rewrite = lua_mime.add_text_footer(task, footer.html, footer.plain) or {}
|
||||
|
||||
|
||||
local seen_cte
|
||||
local newline_s = newline(task)
|
||||
|
||||
|
||||
local function rewrite_ct_cb(name, hdr)
|
||||
if rewrite.need_rewrite_ct then
|
||||
if name:lower() == 'content-type' then
|
||||
local nct = string.format('%s: %s/%s; charset=utf-8',
|
||||
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
|
||||
-- include boundary if present
|
||||
local boundary_part = rewrite.new_ct.boundary and
|
||||
string.format('; boundary="%s"', rewrite.new_ct.boundary) or ''
|
||||
local nct = string.format('%s: %s/%s; charset=utf-8%s',
|
||||
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)
|
||||
out[#out + 1] = nct
|
||||
-- update Content-Type header
|
||||
-- update Content-Type header (include boundary if present)
|
||||
task:set_milter_reply({
|
||||
remove_headers = {['Content-Type'] = 0},
|
||||
})
|
||||
task:set_milter_reply({
|
||||
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8', rewrite.new_ct.type, rewrite.new_ct.subtype)}
|
||||
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8%s',
|
||||
rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)}
|
||||
})
|
||||
return
|
||||
elseif name:lower() == 'content-transfer-encoding' then
|
||||
@@ -645,16 +655,16 @@ rspamd_config:register_symbol({
|
||||
end
|
||||
out[#out + 1] = hdr.raw:gsub('\r?\n?$', '')
|
||||
end
|
||||
|
||||
|
||||
task:headers_foreach(rewrite_ct_cb, {full = true})
|
||||
|
||||
|
||||
if not seen_cte and rewrite.need_rewrite_ct then
|
||||
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable')
|
||||
end
|
||||
|
||||
|
||||
-- End of headers
|
||||
out[#out + 1] = newline_s
|
||||
|
||||
|
||||
if rewrite.out then
|
||||
for _,o in ipairs(rewrite.out) do
|
||||
out[#out + 1] = o
|
||||
|
||||
@@ -182,7 +182,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
||||
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
||||
$goto_branch_array = explode(',', $goto_branch);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
||||
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
||||
if ($goto_branch) {
|
||||
@@ -236,6 +236,9 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) {
|
||||
':action' => $action,
|
||||
':fuzzy_hashes' => $fuzzy
|
||||
));
|
||||
$lastId = $pdo->lastInsertId();
|
||||
$stmt_update = $pdo->prepare("UPDATE `quarantine` SET `qhash` = SHA2(CONCAT(`id`, `qid`), 256) WHERE `id` = :id");
|
||||
$stmt_update->execute(array(':id' => $lastId));
|
||||
$stmt = $pdo->prepare('DELETE FROM `quarantine` WHERE `rcpt` = :rcpt AND `id` NOT IN (
|
||||
SELECT `id`
|
||||
FROM (
|
||||
|
||||
@@ -167,7 +167,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
||||
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
||||
$goto_branch_array = explode(',', $goto_branch);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
||||
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
||||
if ($goto_branch) {
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// redirect to mailcow login form
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var loginForm = document.forms.namedItem("loginForm");
|
||||
if (loginForm) {
|
||||
window.location.href = '/user';
|
||||
}
|
||||
});
|
||||
// logout function
|
||||
function mc_logout() {
|
||||
fetch("/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
body: "logout=1"
|
||||
}).then(() => window.location.href = '/');
|
||||
}
|
||||
|
||||
// Custom SOGo JS
|
||||
|
||||
// Change the visible font-size in the editor, this does not change the font of a html message by default
|
||||
@@ -5,3 +23,4 @@ CKEDITOR.addCss("body {font-size: 16px !important}");
|
||||
|
||||
// Enable scayt by default
|
||||
//CKEDITOR.config.scayt_autoStartup = true;
|
||||
|
||||
|
||||
62
data/conf/sogo/plist_ldap → data/conf/sogo/plist_ldap.sh
Normal file → Executable file
62
data/conf/sogo/plist_ldap → data/conf/sogo/plist_ldap.sh
Normal file → Executable file
@@ -1,28 +1,34 @@
|
||||
<!--
|
||||
<example>
|
||||
<key>canAuthenticate</key>
|
||||
<string>YES</string>
|
||||
<key>id</key>
|
||||
<string>${line}_ldap</string>
|
||||
<key>isAddressBook</key>
|
||||
<string>NO</string>
|
||||
<key>IDFieldName</key>
|
||||
<string>mail</string>
|
||||
<key>UIDFieldName</key>
|
||||
<string>uid</string>
|
||||
<key>bindFields</key>
|
||||
<array>
|
||||
<string>mail</string>
|
||||
</array>
|
||||
<key>type</key>
|
||||
<string>ldap</string>
|
||||
<key>bindDN</key>
|
||||
<string>cn=admin,dc=example,dc=local</string>
|
||||
<key>bindPassword</key>
|
||||
<string>password</string>
|
||||
<key>baseDN</key>
|
||||
<string>ou=People,dc=example,dc=local</string>
|
||||
<key>hostname</key>
|
||||
<string>ldap://1.2.3.4:389</string>
|
||||
</example>
|
||||
-->
|
||||
#!/bin/bash
|
||||
|
||||
domain="$1"
|
||||
gal_status="$2"
|
||||
|
||||
echo "
|
||||
<!--
|
||||
<example>
|
||||
<key>canAuthenticate</key>
|
||||
<string>YES</string>
|
||||
<key>id</key>
|
||||
<string>"${domain}"_ldap</string>
|
||||
<key>isAddressBook</key>
|
||||
<string>"${gal_status}"</string>
|
||||
<key>IDFieldName</key>
|
||||
<string>mail</string>
|
||||
<key>UIDFieldName</key>
|
||||
<string>uid</string>
|
||||
<key>bindFields</key>
|
||||
<array>
|
||||
<string>mail</string>
|
||||
</array>
|
||||
<key>type</key>
|
||||
<string>ldap</string>
|
||||
<key>bindDN</key>
|
||||
<string>cn=admin,dc=example,dc=local</string>
|
||||
<key>bindPassword</key>
|
||||
<string>password</string>
|
||||
<key>baseDN</key>
|
||||
<string>ou=People,dc=example,dc=local</string>
|
||||
<key>hostname</key>
|
||||
<string>ldap://1.2.3.4:389</string>
|
||||
</example>
|
||||
-->"
|
||||
@@ -16,6 +16,9 @@
|
||||
SOGoFoldersSendEMailNotifications = YES;
|
||||
SOGoForwardEnabled = YES;
|
||||
|
||||
// Added with SOGo 5.12 - Allows users to cleanup there maildirectories by deleting mails oder than X
|
||||
SOGoEnableMailCleaning = YES;
|
||||
|
||||
// Fixes "MODIFICATION_FAILED" error (HTTP 412) in Clients when accepting invitations from external services
|
||||
SOGoDisableOrganizerEventCheck = YES;
|
||||
|
||||
@@ -91,7 +94,7 @@
|
||||
//SoDebugBaseURL = YES;
|
||||
//ImapDebugEnabled = YES;
|
||||
//SOGoEASDebugEnabled = YES;
|
||||
SOGoEASSearchInBody = YES; // Experimental. Enabled since 2023-10
|
||||
SOGoEASSearchInBody = YES;
|
||||
//LDAPDebugEnabled = YES;
|
||||
//PGDebugEnabled = YES;
|
||||
//MySQL4DebugEnabled = YES;
|
||||
|
||||
@@ -9,4 +9,4 @@ if [ -n "$VALKEYMASTERPASS" ]; then
|
||||
echo "masterauth $VALKEYMASTERPASS" >> /valkey.conf
|
||||
fi
|
||||
|
||||
exec valkey-server /valkey.conf
|
||||
exec "$@"
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /');
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
$clamd_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_CLAMD"])) ? false : true;
|
||||
$olefy_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_OLEFY"])) ? false : true;
|
||||
|
||||
|
||||
if (!isset($_SESSION['gal']) && $license_cache = $valkey->Get('LICENSE_STATUS_CACHE')) {
|
||||
$_SESSION['gal'] = json_decode($license_cache, true);
|
||||
}
|
||||
|
||||
$js_minifier->add('/web/js/site/debug.js');
|
||||
$js_minifier->add('/web/js/site/dashboard.js');
|
||||
|
||||
// vmail df
|
||||
$exec_fields = array('cmd' => 'system', 'task' => 'df', 'dir' => '/var/vmail');
|
||||
@@ -24,6 +34,7 @@ $vmail_df = explode(',', (string)json_decode(docker('post', 'dovecot-mailcow', '
|
||||
// containers
|
||||
$containers_info = (array) docker('info');
|
||||
if ($clamd_status === false) unset($containers_info['clamd-mailcow']);
|
||||
if ($olefy_status === false) unset($containers_info['olefy-mailcow']);
|
||||
ksort($containers_info);
|
||||
$containers = array();
|
||||
foreach ($containers_info as $container => $container_info) {
|
||||
@@ -59,7 +70,7 @@ foreach ($containers_info as $container => $container_info) {
|
||||
$hostname = getenv('MAILCOW_HOSTNAME');
|
||||
$timezone = getenv('TZ');
|
||||
|
||||
$template = 'debug.twig';
|
||||
$template = 'dashboard.twig';
|
||||
$template_data = [
|
||||
'log_lines' => getenv('LOG_LINES'),
|
||||
'vmail_df' => $vmail_df,
|
||||
@@ -68,6 +79,7 @@ $template_data = [
|
||||
'gal' => @$_SESSION['gal'],
|
||||
'license_guid' => license('guid'),
|
||||
'clamd_status' => $clamd_status,
|
||||
'olefy_status' => $olefy_status,
|
||||
'containers' => $containers,
|
||||
'ip_check' => customize('get', 'ip_check'),
|
||||
'lang_admin' => json_encode($lang['admin']),
|
||||
30
data/web/admin/index.php
Normal file
30
data/web/admin/index.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
$_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
|
||||
|
||||
|
||||
$template = 'admin_index.twig';
|
||||
$template_data = [
|
||||
'login_delay' => @$_SESSION['ldelay'],
|
||||
'custom_login' => customize('get', 'custom_login'),
|
||||
];
|
||||
|
||||
$js_minifier->add('/web/js/site/index.js');
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
@@ -1,10 +1,20 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
header('Location: /');
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
@@ -14,7 +24,7 @@ $js_minifier->add('/web/js/site/mailbox.js');
|
||||
$js_minifier->add('/web/js/presets/sieveMailbox.js');
|
||||
$js_minifier->add('/web/js/site/pwgen.js');
|
||||
|
||||
$role = ($_SESSION['mailcow_cc_role'] == "admin") ? 'admin' : 'domainadmin';
|
||||
$role = "admin";
|
||||
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? 'true' : 'false';
|
||||
$allow_admin_email_login = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"])) ? 'true' : 'false';
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /');
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
|
||||
@@ -11,7 +20,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$js_minifier->add('/web/js/site/queue.js');
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
$role = ($_SESSION['mailcow_cc_role'] == "admin") ? 'admin' : 'domainadmin';
|
||||
$role = "admin";
|
||||
|
||||
$template = 'queue.twig';
|
||||
$template_data = [
|
||||
@@ -1,8 +1,17 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /');
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
|
||||
@@ -86,6 +95,8 @@ $cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allo
|
||||
$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
|
||||
|
||||
$f2b_data = fail2ban('get');
|
||||
// mbox templates
|
||||
$mbox_templates = mailbox('get', 'mailbox_templates');
|
||||
|
||||
$template = 'admin.twig';
|
||||
$template_data = [
|
||||
@@ -114,10 +125,13 @@ $template_data = [
|
||||
'logo_specs' => customize('get', 'main_logo_specs'),
|
||||
'logo_dark_specs' => customize('get', 'main_logo_dark_specs'),
|
||||
'ip_check' => customize('get', 'ip_check'),
|
||||
'custom_login' => customize('get', 'custom_login'),
|
||||
'password_complexity' => password_complexity('get'),
|
||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||
'cors_settings' => $cors_settings,
|
||||
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
|
||||
'iam_settings' => $iam_settings,
|
||||
'mbox_templates' => $mbox_templates,
|
||||
'lang_admin' => json_encode($lang['admin']),
|
||||
'lang_datatables' => json_encode($lang['datatables'])
|
||||
];
|
||||
@@ -346,7 +346,8 @@ paths:
|
||||
description: the domain which emails should be forwarded
|
||||
type: string
|
||||
type:
|
||||
description: the type of bcc map can be `sender` or `recipient`
|
||||
description: the type of bcc map can be `sender` or `rcpt`
|
||||
enum: [sender, rcpt]
|
||||
type: string
|
||||
type: object
|
||||
summary: Create BCC Map
|
||||
@@ -409,7 +410,7 @@ paths:
|
||||
description: a list of domains for which a dkim key should be generated
|
||||
type: string
|
||||
key_size:
|
||||
description: the key size (1024 or 2048)
|
||||
description: the key size (1024, 2048, 3072 or 4096)
|
||||
type: number
|
||||
type: object
|
||||
summary: Generate DKIM Key
|
||||
@@ -1112,6 +1113,7 @@ paths:
|
||||
domain: domain.tld
|
||||
local_part: info
|
||||
name: Full name
|
||||
authsource: mailcow
|
||||
password: atedismonsin
|
||||
password2: atedismonsin
|
||||
quota: "3072"
|
||||
@@ -1132,11 +1134,16 @@ paths:
|
||||
name:
|
||||
description: Full name of the mailbox user
|
||||
type: string
|
||||
authsource:
|
||||
description: Specifies the authentication source for the mailbox.
|
||||
type: string
|
||||
enum: [mailcow, ldap, keycloak, generic-oidc]
|
||||
default: mailcow
|
||||
password2:
|
||||
description: mailbox password for confirmation
|
||||
type: string
|
||||
password:
|
||||
description: mailbox password
|
||||
description: mailbox password when using `mailcow` as the authentication source.
|
||||
type: string
|
||||
quota:
|
||||
description: mailbox quota
|
||||
@@ -3374,6 +3381,7 @@ paths:
|
||||
active: "1"
|
||||
force_pw_update: "0"
|
||||
name: Full name
|
||||
authsource: mailcow
|
||||
password: ""
|
||||
password2: ""
|
||||
quota: "3072"
|
||||
@@ -3398,11 +3406,15 @@ paths:
|
||||
name:
|
||||
description: Full name of the mailbox user
|
||||
type: string
|
||||
authsource:
|
||||
description: Specifies the authentication source for the mailbox.
|
||||
type: string
|
||||
enum: [mailcow, ldap, keycloak, generic-oidc]
|
||||
password2:
|
||||
description: new mailbox password for confirmation
|
||||
type: string
|
||||
password:
|
||||
description: new mailbox password
|
||||
description: new mailbox password when using `mailcow` as the authentication source.
|
||||
type: string
|
||||
quota:
|
||||
description: mailbox quota
|
||||
@@ -5687,7 +5699,7 @@ paths:
|
||||
- description: name of domain
|
||||
in: path
|
||||
name: domain
|
||||
required: false
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- description: e.g. api-key-string
|
||||
@@ -5814,6 +5826,226 @@ paths:
|
||||
Using this endpoint you can get the global spam filter score or the spam filter score of a certain mailbox.
|
||||
operationId: Get mailbox or global spam filter score
|
||||
summary: Get mailbox or global spam filter score
|
||||
/api/v1/edit/identity-provider:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- type: "success"
|
||||
log:
|
||||
- "identity_provider"
|
||||
- "edit"
|
||||
- authsource: "keycloak"
|
||||
server_url: "https://auth.mailcow.tld"
|
||||
realm: "mailcow"
|
||||
client_id: "mailcow_client"
|
||||
client_secret: "*"
|
||||
redirect_url: "https://mail.mailcow.tld"
|
||||
redirect_url_extra: ["https://extramail.mailcow.tld"]
|
||||
version: "26.1.3"
|
||||
default_template: "Default"
|
||||
mappers:
|
||||
- "small_mbox"
|
||||
- "medium_mbox"
|
||||
templates:
|
||||
- "small"
|
||||
- "medium"
|
||||
ignore_ssl_error: true
|
||||
mailpassword_flow: true
|
||||
periodic_sync: true
|
||||
import_users: true
|
||||
sync_interval: 30
|
||||
msg:
|
||||
- "object_modified"
|
||||
- ""
|
||||
description: OK
|
||||
headers: { }
|
||||
tags:
|
||||
- Identity Provider
|
||||
description: >-
|
||||
Configure an external Identity Provider to use as user authentication
|
||||
operationId: Edit external Identity Provider settings
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
default: ["identity-provider"]
|
||||
attr:
|
||||
type: object
|
||||
properties:
|
||||
authsource:
|
||||
description: Specifies the type of the Identity Provider
|
||||
type: string
|
||||
enum: [ldap, keycloak, generic-oidc]
|
||||
server_url:
|
||||
description: The base URL of your Keycloak server. Required if `authsource` is keycloak.
|
||||
type: string
|
||||
realm:
|
||||
description: The Keycloak realm where the mailcow client is configured. Required if `authsource` is keycloak.
|
||||
type: string
|
||||
client_id:
|
||||
description: The Client ID assigned to mailcow Client in OIDC Provider. Required if `authsource` is keycloak or generic-oidc.
|
||||
type: string
|
||||
client_secret:
|
||||
description: The Client Secret assigned to mailcow Client in OIDC Provider. Required if `authsource` is keycloak or generic-oidc.
|
||||
type: string
|
||||
redirect_url:
|
||||
description: The redirect URL that OIDC Provider will use after authentication. Required if `authsource` is keycloak or generic-oidc.
|
||||
type: string
|
||||
redirect_url_extra:
|
||||
description: Additional redirect URLs that OIDC Provider can use after authentication if valid.
|
||||
type: array
|
||||
version:
|
||||
description: Specifies the Keycloak version. Required if `authsource` is keycloak.
|
||||
type: string
|
||||
default_template:
|
||||
description: (Optional) If no matching Attribute Mapping exists for a User, the default template will be used for creating the mailbox, but not for updating the mailbox.
|
||||
type: string
|
||||
mappers:
|
||||
description: (Optional) Attribute values used to match a mailbox template. Each element corresponds to the respective index in the templates array (i.e., the first element matches the first element of templates, the second matches the second, and so on).
|
||||
type: array
|
||||
templates:
|
||||
description: (Optional) Defines the mailbox templates to be assigned. Each element corresponds to the respective index in the `mappers` array.
|
||||
type: array
|
||||
ignore_ssl_error:
|
||||
description: If enabled, SSL certificate validation is bypassed
|
||||
type: boolean
|
||||
default: false
|
||||
mailpassword_flow:
|
||||
description: If enabled, mailcow will attempt to validate user credentials using the Keycloak Admin REST API instead of relying solely on the Authorization Code Flow.
|
||||
type: boolean
|
||||
default: false
|
||||
periodic_sync:
|
||||
description: If enabled, mailcow periodically performs a full sync of all users from Keycloak or LDAP.
|
||||
type: boolean
|
||||
default: false
|
||||
import_users:
|
||||
description: If enabled, new users are automatically imported from Keycloak or LDAP into mailcow.
|
||||
type: boolean
|
||||
default: false
|
||||
sync_interval:
|
||||
description: Defines the time interval (in minutes) for periodic synchronization and user imports.
|
||||
type: number
|
||||
default: 15
|
||||
host:
|
||||
description: The address of your LDAP server. You can provide a single hostname or a comma-separated list of hosts for fallback in case the primary server is unreachable. Required if `authsource` is ldap.
|
||||
type: string
|
||||
port:
|
||||
description: The port used to connect to the LDAP server. Required if `authsource` is ldap.
|
||||
type: string
|
||||
use_ssl:
|
||||
description: enable LDAPS connection. If Port is set to 389 it will be overriden to 636.
|
||||
type: boolean
|
||||
default: false
|
||||
use_tls:
|
||||
description: enable TLS connection. TLS is recommended over SSL. SSL Ports cannot be used.
|
||||
type: boolean
|
||||
default: false
|
||||
basedn:
|
||||
description: The Distinguished Name (DN) from which searches will be performed. Required if `authsource` is ldap.
|
||||
type: string
|
||||
username_field:
|
||||
description: The LDAP attribute used to identify users during authentication. Required if `authsource` is ldap.
|
||||
type: string
|
||||
default: mail
|
||||
filter:
|
||||
description: An optional LDAP search filter to refine which users can authenticate.
|
||||
type: string
|
||||
attribute_field:
|
||||
description: Specifies an LDAP attribute that holds a specific value which can be mapped to a mailbox template using the Attribute Mapping section. Required if `authsource` is ldap.
|
||||
type: string
|
||||
binddn:
|
||||
description: The Distinguished Name (DN) of the LDAP user that will be used to authenticate and perform LDAP searches. This account should have sufficient permissions to read the required attributes. Required if `authsource` is ldap.
|
||||
type: string
|
||||
bindpass:
|
||||
description: The password for the Bind DN user. It is required for authentication when connecting to the LDAP server. Required if `authsource` is ldap.
|
||||
type: string
|
||||
authorize_url:
|
||||
description: The OIDC provider's authorization server URL. Required if `authsource` is generic-oidc.
|
||||
type: string
|
||||
token_url:
|
||||
description: The OIDC provider's token server URL. Required if `authsource` is generic-oidc.
|
||||
type: string
|
||||
userinfo_url:
|
||||
description: The OIDC provider's user info server URL. Required if `authsource` is generic-oidc.
|
||||
type: string
|
||||
client_scopes:
|
||||
description: Specifies the OIDC scopes requested during authentication.
|
||||
type: string
|
||||
default: "openid profile email mailcow_template"
|
||||
examples:
|
||||
keycloak:
|
||||
value:
|
||||
items:
|
||||
- "identity-provider"
|
||||
attr:
|
||||
authsource: "keycloak"
|
||||
server_url: "https://auth.mailcow.tld"
|
||||
realm: "mailcow"
|
||||
client_id: "mailcow_client"
|
||||
client_secret: "Xy7GdPqvJ9m3R8sT2LkVZ5W1oNbCaYQf"
|
||||
redirect_url: "https://mail.mailcow.tld"
|
||||
redirect_url_extra: ["https://extramail.mailcow.tld"]
|
||||
version: "26.1.3"
|
||||
default_template: "Default"
|
||||
mappers: ["small_mbox", "medium_mbox"]
|
||||
templates: ["small", "medium"]
|
||||
ignore_ssl_error: true
|
||||
mailpassword_flow: true
|
||||
periodic_sync: true
|
||||
import_users: true
|
||||
sync_interval: 30
|
||||
ldap:
|
||||
value:
|
||||
items:
|
||||
- "identity-provider"
|
||||
attr:
|
||||
authsource: "ldap"
|
||||
host: "127.0.0.1"
|
||||
port: "389"
|
||||
use_ssl: false
|
||||
use_tls: false
|
||||
ignore_ssl_error: false
|
||||
basedn: "DC=mailcow,DC=local"
|
||||
username_field: "mail"
|
||||
filter: "(memberOf:1.2.840.113556.1.4.1941:=DC=mailcow,DC=local)"
|
||||
attribute_field: "othermailbox"
|
||||
binddn: "CN=LDAP Read Only,CN=Users,DC=mailcow,DC=local"
|
||||
bindpass: "moohoo"
|
||||
default_template: "Default"
|
||||
mappers: ["small_mbox", "medium_mbox"]
|
||||
templates: ["small", "medium"]
|
||||
periodic_sync: true
|
||||
import_users: true
|
||||
sync_interval: 30
|
||||
generic-oidc:
|
||||
value:
|
||||
items:
|
||||
- "identity-provider"
|
||||
attr:
|
||||
authsource: "generic-oidc"
|
||||
authorize_url: "https://auth.mailcow.tld/application/o/authorize/"
|
||||
token_url: "https://auth.mailcow.tld/application/o/token/"
|
||||
userinfo_url: "https://auth.mailcow.tld/application/o/userinfo/"
|
||||
client_id: "mailcow_client"
|
||||
client_secret: "Xy7GdPqvJ9m3R8sT2LkVZ5W1oNbCaYQf"
|
||||
redirect_url: "https://mail.mailcow.tld"
|
||||
redirect_url_extra: ["https://extramail.mailcow.tld"]
|
||||
client_scopes: "openid profile email mailcow_template"
|
||||
default_template: "Default"
|
||||
mappers: ["small_mbox", "medium_mbox"]
|
||||
templates: ["small", "medium"]
|
||||
ignore_ssl_error: true
|
||||
summary: Edit external Identity Provider
|
||||
|
||||
tags:
|
||||
- name: Domains
|
||||
@@ -5860,3 +6092,5 @@ tags:
|
||||
description: Edit domain ratelimits
|
||||
- name: Cross-Origin Resource Sharing (CORS)
|
||||
description: Manage Cross-Origin Resource Sharing (CORS) settings
|
||||
- name: Identity Provider
|
||||
description: Manage external Identity Provider settings
|
||||
|
||||
@@ -85,7 +85,7 @@ if (count($records) == 0 || $records[0]['target'] != '') { ?>
|
||||
<authentication>password-cleartext</authentication>
|
||||
</outgoingServer>
|
||||
|
||||
<enable visiturl="https://<?=$mailcow_hostname; ?><?php if ($port != 443) echo ':'.$port; ?>/admin.php">
|
||||
<enable visiturl="https://<?=$mailcow_hostname; ?><?php if ($port != 443) echo ':'.$port; ?>/admin">
|
||||
<instruction>If you didn't change the password given to you by the administrator or if you didn't change it in a long time, please consider doing that now.</instruction>
|
||||
<instruction lang="de">Sollten Sie das Ihnen durch den Administrator vergebene Passwort noch nicht geändert haben, empfehlen wir dies nun zu tun. Auch ein altes Passwort sollte aus Sicherheitsgründen geändert werden.</instruction>
|
||||
</enable>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/vendor/autoload.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/vars.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
|
||||
$default_autodiscover_config = $autodiscover_config;
|
||||
if(file_exists('inc/vars.local.inc.php')) {
|
||||
include_once 'inc/vars.local.inc.php';
|
||||
}
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
|
||||
$default_autodiscover_config = $autodiscover_config;
|
||||
$autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
|
||||
|
||||
// Valkey
|
||||
@@ -50,6 +53,11 @@ $opt = [
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
||||
|
||||
// Init Identity Provider
|
||||
$iam_provider = identity_provider('init');
|
||||
$iam_settings = identity_provider('get');
|
||||
|
||||
$login_user = strtolower(trim($_SERVER['PHP_AUTH_USER']));
|
||||
$login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW']));
|
||||
|
||||
|
||||
2
data/web/css/build/007-languages.min.css
vendored
2
data/web/css/build/007-languages.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -8,9 +8,6 @@
|
||||
.dtr-details {
|
||||
width: 100%;
|
||||
}
|
||||
.table-striped>tbody>tr:nth-of-type(odd) {
|
||||
background-color: #F2F2F2;
|
||||
}
|
||||
td.child>ul>li {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,13 @@
|
||||
url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff2') format('woff2'),
|
||||
url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff') format('woff');
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
#maxmsgsize { min-width: 80px; }
|
||||
#slider1 .slider-selection {
|
||||
background: #FFD700;
|
||||
@@ -74,10 +81,23 @@
|
||||
align-items: center;
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
.navbar-fixed-bottom .navbar-collapse,
|
||||
.navbar-fixed-bottom .navbar-collapse,
|
||||
.navbar-fixed-top .navbar-collapse {
|
||||
max-height: 1000px
|
||||
}
|
||||
.nav-tabs .nav-link, .nav-tabs .nav-link.disabled, .nav-tabs .nav-link.disabled:hover, .nav-tabs .nav-link.disabled:focus {
|
||||
border-color: #dfdfdf;
|
||||
}
|
||||
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
|
||||
border-color: #dfdfdf;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
}
|
||||
.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {
|
||||
border-color: #dfdfdf;
|
||||
}
|
||||
.nav-tabs {
|
||||
border-bottom: 1px solid #dfdfdf;
|
||||
}
|
||||
.bi {
|
||||
display: inline-block;
|
||||
font-size: 12pt;
|
||||
@@ -123,18 +143,18 @@
|
||||
}
|
||||
}
|
||||
@keyframes blink {
|
||||
50% {
|
||||
color: transparent
|
||||
50% {
|
||||
color: transparent
|
||||
}
|
||||
}
|
||||
.loader-dot {
|
||||
animation: 1s blink infinite
|
||||
.loader-dot {
|
||||
animation: 1s blink infinite
|
||||
}
|
||||
.loader-dot:nth-child(2) {
|
||||
animation-delay: 250ms
|
||||
.loader-dot:nth-child(2) {
|
||||
animation-delay: 250ms
|
||||
}
|
||||
.loader-dot:nth-child(3) {
|
||||
animation-delay: 500ms
|
||||
.loader-dot:nth-child(3) {
|
||||
animation-delay: 500ms
|
||||
}
|
||||
|
||||
pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;}
|
||||
@@ -200,13 +220,13 @@ legend {
|
||||
}
|
||||
.haveibeenpwned {
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.full-width-select {
|
||||
width: 100%!important;
|
||||
width: 100%!important;
|
||||
}
|
||||
.tooltip {
|
||||
font-family: inherit;
|
||||
@@ -330,7 +350,7 @@ code {
|
||||
.caret {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
a[aria-expanded='true'] > .caret,
|
||||
a[aria-expanded='true'] > .caret,
|
||||
button[aria-expanded='true'] > .caret {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
@@ -340,7 +360,7 @@ button[aria-expanded='true'] > .caret {
|
||||
}
|
||||
.list-group-header {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bg-primary, .alert-primary, .btn-primary {
|
||||
@@ -366,12 +386,13 @@ button[aria-expanded='true'] > .caret {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.btn.btn-outline-secondary {
|
||||
border-color: #cfcfcf !important;
|
||||
color: #000000 !important;
|
||||
border-color: #cfcfcf !important;
|
||||
}
|
||||
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
|
||||
background-color: #f0f0f0 !important;
|
||||
}
|
||||
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
|
||||
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
|
||||
color: #fff;
|
||||
background-color: #555;
|
||||
background-image: none;
|
||||
@@ -389,4 +410,26 @@ button[aria-expanded='true'] > .caret {
|
||||
.badge.bg-danger > a {
|
||||
color: #fff !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.hr-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.hr-title::before,
|
||||
.hr-title::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.hr-title:not(:empty)::before {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.hr-title:not(:empty)::after {
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -6,15 +6,9 @@
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.card-login .apps .btn {
|
||||
width: auto;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
margin-top: auto;
|
||||
}
|
||||
.card-login .apps .btn:hover {
|
||||
margin-top: 1px !important;
|
||||
border-bottom-width: 3px;
|
||||
.card .apps {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.responsive-tabs .nav-tabs {
|
||||
@@ -43,16 +37,6 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card-login .apps .btn {
|
||||
width: 100%;
|
||||
float: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-login .apps .btn {
|
||||
border-bottom-width: 4px;
|
||||
}
|
||||
|
||||
.xs-show {
|
||||
display: block !important;
|
||||
}
|
||||
@@ -113,9 +97,6 @@
|
||||
.btn-group.nowrap .dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
.card-login .btn-group {
|
||||
display: block;
|
||||
}
|
||||
.mass-actions-user .btn-group {
|
||||
float: none;
|
||||
}
|
||||
@@ -191,9 +172,6 @@
|
||||
.btn-group .btn i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.card-login .btn-group .btn {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.dt-sm-head-hidden .dtr-title {
|
||||
display: none !important;
|
||||
@@ -206,7 +184,7 @@
|
||||
.senders-mw220 {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before,
|
||||
table.dataTable td.dt-control:before {
|
||||
@@ -215,7 +193,7 @@
|
||||
line-height: 2rem;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
|
||||
li .dtr-data {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -59,9 +59,6 @@ body.modal-open {
|
||||
.table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td {
|
||||
padding: 3px;
|
||||
}
|
||||
table tbody tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
table tbody tr td input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
29
data/web/domainadmin/index.php
Normal file
29
data/web/domainadmin/index.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
$_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
|
||||
|
||||
$template = 'domainadmin_index.twig';
|
||||
$template_data = [
|
||||
'login_delay' => @$_SESSION['ldelay'],
|
||||
'custom_login' => customize('get', 'custom_login'),
|
||||
];
|
||||
|
||||
$js_minifier->add('/web/js/site/index.js');
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
58
data/web/domainadmin/mailbox.php
Normal file
58
data/web/domainadmin/mailbox.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
header('Location: /domainadmin');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
|
||||
|
||||
$js_minifier->add('/web/js/site/mailbox.js');
|
||||
$js_minifier->add('/web/js/presets/sieveMailbox.js');
|
||||
$js_minifier->add('/web/js/site/pwgen.js');
|
||||
|
||||
$role = "domainadmin";
|
||||
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? 'true' : 'false';
|
||||
$allow_admin_email_login = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"])) ? 'true' : 'false';
|
||||
|
||||
// domains
|
||||
$domains = mailbox('get', 'domains');
|
||||
|
||||
// mailboxes
|
||||
$mailboxes = [];
|
||||
foreach ($domains as $domain) {
|
||||
foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) {
|
||||
$mailboxes[] = $mailbox;
|
||||
}
|
||||
}
|
||||
|
||||
$template = 'mailbox.twig';
|
||||
$template_data = [
|
||||
'acl' => $_SESSION['acl'],
|
||||
'acl_json' => json_encode($_SESSION['acl']),
|
||||
'role' => $role,
|
||||
'is_dual' => $is_dual,
|
||||
'allow_admin_email_login' => $allow_admin_email_login,
|
||||
'global_filters' => mailbox('get', 'global_filter_details'),
|
||||
'domains' => $domains,
|
||||
'mailboxes' => $mailboxes,
|
||||
'lang_mailbox' => json_encode($lang['mailbox']),
|
||||
'lang_rl' => json_encode($lang['ratelimit']),
|
||||
'lang_edit' => json_encode($lang['edit']),
|
||||
'lang_datatables' => json_encode($lang['datatables']),
|
||||
];
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
44
data/web/domainadmin/user.php
Normal file
44
data/web/domainadmin/user.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
|
||||
/*
|
||||
/ DOMAIN ADMIN
|
||||
*/
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
$tfa_data = get_tfa();
|
||||
$fido2_data = fido2(array("action" => "get_friendly_names"));
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
|
||||
$template = 'domainadmin.twig';
|
||||
$template_data = [
|
||||
'acl' => $_SESSION['acl'],
|
||||
'acl_json' => json_encode($_SESSION['acl']),
|
||||
'user_spam_score' => mailbox('get', 'spam_score', $username),
|
||||
'tfa_data' => $tfa_data,
|
||||
'fido2_data' => $fido2_data,
|
||||
'lang_user' => json_encode($lang['user']),
|
||||
'lang_datatables' => json_encode($lang['datatables']),
|
||||
];
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
else {
|
||||
header('Location: /domainadmin');
|
||||
exit();
|
||||
}
|
||||
|
||||
$js_minifier->add('/web/js/site/user.js');
|
||||
$js_minifier->add('/web/js/site/pwgen.js');
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
@@ -48,6 +48,12 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
$rl = ratelimit('get', 'domain', $domain);
|
||||
$rlyhosts = relayhost('get');
|
||||
$domain_footer = mailbox('get', 'domain_wide_footer', $domain);
|
||||
$mta_sts = mailbox('get', 'mta_sts', $domain);
|
||||
if (count($mta_sts) == 0) {
|
||||
$mta_sts = false;
|
||||
} elseif (isset($mta_sts['mx'])) {
|
||||
$mta_sts['mx'] = implode(',', $mta_sts['mx']);
|
||||
}
|
||||
$template = 'edit/domain.twig';
|
||||
$template_data = [
|
||||
'acl' => $_SESSION['acl'],
|
||||
@@ -58,6 +64,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
'dkim' => dkim('details', $domain),
|
||||
'domain_details' => $result,
|
||||
'domain_footer' => $domain_footer,
|
||||
'mta_sts' => $mta_sts,
|
||||
'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]),
|
||||
'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address'),
|
||||
'alias_domains' => mailbox('get', 'alias_domains', $_GET["domain"])
|
||||
@@ -125,13 +132,15 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
'mailbox' => $mailbox,
|
||||
'rl' => $rl,
|
||||
'pushover_data' => $pushover_data,
|
||||
'get_tagging_options' => mailbox('get', 'delimiter_action', $mailbox),
|
||||
'quarantine_notification' => $quarantine_notification,
|
||||
'quarantine_category' => $quarantine_category,
|
||||
'get_tls_policy' => $get_tls_policy,
|
||||
'rlyhosts' => $rlyhosts,
|
||||
'sender_acl_handles' => mailbox('get', 'sender_acl_handles', $mailbox),
|
||||
'user_acls' => acl('get', 'user', $mailbox),
|
||||
'mailbox_details' => $result
|
||||
'mailbox_details' => $result,
|
||||
'iam_settings' => $iam_settings,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 17 KiB |
@@ -71,6 +71,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
// Init records array
|
||||
$spf_link = '<a href="http://www.open-spf.org/SPF_Record_Syntax/" target="_blank">SPF Record Syntax</a><br />';
|
||||
$dmarc_link = '<a href="https://www.kitterman.com/dmarc/assistant.html" target="_blank">DMARC Assistant</a>';
|
||||
$mtasts_report_link = '<a href="https://mxtoolbox.com/dmarc/smtp-tls/how-to-setup-smtp-tls-reports" target="_blank">TLS Report Record Syntax</a>';
|
||||
|
||||
$records = array();
|
||||
|
||||
@@ -128,6 +129,27 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
);
|
||||
}
|
||||
|
||||
$mta_sts = mailbox('get', 'mta_sts', $domain);
|
||||
if (count($mta_sts) > 0 && $mta_sts['active'] == 1) {
|
||||
if (!in_array($domain, $alias_domains)) {
|
||||
$records[] = array(
|
||||
'mta-sts.' . $domain,
|
||||
'CNAME',
|
||||
$mailcow_hostname
|
||||
);
|
||||
}
|
||||
$records[] = array(
|
||||
'_mta-sts.' . $domain,
|
||||
'TXT',
|
||||
"v={$mta_sts['version']};id={$mta_sts['id']};",
|
||||
);
|
||||
$records[] = array(
|
||||
'_smtp._tls.' . $domain,
|
||||
'TXT',
|
||||
$mtasts_report_link,
|
||||
);
|
||||
}
|
||||
|
||||
$records[] = array(
|
||||
$domain,
|
||||
'TXT',
|
||||
@@ -341,15 +363,25 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
}
|
||||
|
||||
foreach ($currents as &$current) {
|
||||
if ($current['type'] == "TXT" &&
|
||||
stripos(strtolower($current['txt']), 'v=sts') === 0) {
|
||||
if (strtolower($current[$data_field[$current['type']]]) == strtolower($record[2])) {
|
||||
$state = state_good;
|
||||
}
|
||||
else {
|
||||
$state = state_nomatch;
|
||||
}
|
||||
$state .= '<br />' . $current[$data_field[$current['type']]];
|
||||
}
|
||||
if ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=dmarc') === 0 &&
|
||||
$record[2] == $dmarc_link) {
|
||||
stripos($current['txt'], 'v=dmarc') === 0 &&
|
||||
$record[2] == $dmarc_link) {
|
||||
$current['txt'] = str_replace(' ', '', $current['txt']);
|
||||
$state = $current[$data_field[$current['type']]] . state_optional;
|
||||
}
|
||||
elseif ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=spf') === 0 &&
|
||||
$record[2] == $spf_link) {
|
||||
stripos($current['txt'], 'v=spf') === 0 &&
|
||||
$record[2] == $spf_link) {
|
||||
$state = state_nomatch;
|
||||
$rslt = get_spf_allowed_hosts($record[0], true);
|
||||
if (in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)) {
|
||||
@@ -358,8 +390,8 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
$state .= '<br />' . $current[$data_field[$current['type']]] . state_optional;
|
||||
}
|
||||
elseif ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=dkim') === 0 &&
|
||||
stripos($record[2], 'v=dkim') === 0) {
|
||||
stripos($current['txt'], 'v=dkim') === 0 &&
|
||||
stripos($record[2], 'v=dkim') === 0) {
|
||||
preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $current[$data_field[$current['type']]], $dkim_matches_current);
|
||||
preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $record[2], $dkim_matches_good);
|
||||
if ($dkim_matches_current[1] == $dkim_matches_good[1]) {
|
||||
@@ -367,7 +399,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
}
|
||||
}
|
||||
elseif ($current['type'] != 'TXT' &&
|
||||
isset($data_field[$current['type']]) && $state != state_good) {
|
||||
isset($data_field[$current['type']]) && $state != state_good) {
|
||||
$state = state_nomatch;
|
||||
if ($current[$data_field[$current['type']]] == $record[2]) {
|
||||
$state = state_good;
|
||||
|
||||
@@ -26,23 +26,25 @@ if (is_array($alertbox_log_parser)) {
|
||||
|
||||
// map tfa details for twig
|
||||
$pending_tfa_authmechs = [];
|
||||
foreach($_SESSION['pending_tfa_methods'] as $authdata){
|
||||
$pending_tfa_authmechs[$authdata['authmech']] = false;
|
||||
}
|
||||
if (isset($pending_tfa_authmechs['webauthn'])) {
|
||||
$pending_tfa_authmechs['webauthn'] = true;
|
||||
}
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
&& isset($pending_tfa_authmechs['yubi_otp'])) {
|
||||
$pending_tfa_authmechs['yubi_otp'] = true;
|
||||
}
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
&& !isset($pending_tfa_authmechs['yubi_otp'])
|
||||
&& isset($pending_tfa_authmechs['totp'])) {
|
||||
$pending_tfa_authmechs['totp'] = true;
|
||||
}
|
||||
if (isset($pending_tfa_authmechs['u2f'])) {
|
||||
$pending_tfa_authmechs['u2f'] = true;
|
||||
if (array_key_exists('pending_tfa_methods', $_SESSION)) {
|
||||
foreach($_SESSION['pending_tfa_methods'] as $authdata){
|
||||
$pending_tfa_authmechs[$authdata['authmech']] = false;
|
||||
}
|
||||
if (isset($pending_tfa_authmechs['webauthn'])) {
|
||||
$pending_tfa_authmechs['webauthn'] = true;
|
||||
}
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
&& isset($pending_tfa_authmechs['yubi_otp'])) {
|
||||
$pending_tfa_authmechs['yubi_otp'] = true;
|
||||
}
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
&& !isset($pending_tfa_authmechs['yubi_otp'])
|
||||
&& isset($pending_tfa_authmechs['totp'])) {
|
||||
$pending_tfa_authmechs['totp'] = true;
|
||||
}
|
||||
if (isset($pending_tfa_authmechs['u2f'])) {
|
||||
$pending_tfa_authmechs['u2f'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// globals
|
||||
@@ -66,6 +68,8 @@ $globalVariables = [
|
||||
'lang_acl' => json_encode($lang['acl']),
|
||||
'lang_tfa' => json_encode($lang['tfa']),
|
||||
'lang_fido2' => json_encode($lang['fido2']),
|
||||
'lang_success' => json_encode($lang['success']),
|
||||
'lang_danger' => json_encode($lang['danger']),
|
||||
'docker_timeout' => $DOCKER_TIMEOUT,
|
||||
'session_lifetime' => (int)$SESSION_LIFETIME,
|
||||
'csrf_token' => $_SESSION['CSRF']['TOKEN'],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
function acl($_action, $_scope = null, $_data = null) {
|
||||
function acl($_action, $_scope = null, $_data = null, $_extra = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
@@ -24,7 +24,7 @@ function acl($_action, $_scope = null, $_data = null) {
|
||||
}
|
||||
// Users cannot change their own ACL
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)
|
||||
|| ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin')) {
|
||||
|| ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin' && $_SESSION['access_all_exception'] != '1')) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
@@ -34,7 +34,7 @@ function acl($_action, $_scope = null, $_data = null) {
|
||||
}
|
||||
// Read all available acl options by calling acl(get)
|
||||
// Set all available acl options we cannot find in the post data to 0, else 1
|
||||
$is_now = acl('get', 'user', $username);
|
||||
$is_now = acl('get', 'user', $username, $_extra);
|
||||
if (!empty($is_now)) {
|
||||
foreach ($is_now as $acl_now_name => $acl_now_val) {
|
||||
$set_acls[$acl_now_name] = (isset($acl_post[$acl_now_name])) ? 1 : 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
function app_passwd($_action, $_data = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
!isset($_data_log['app_passwd']) ?: $_data_log['app_passwd'] = '*';
|
||||
!isset($_data_log['app_passwd2']) ?: $_data_log['app_passwd2'] = '*';
|
||||
@@ -43,20 +43,7 @@ function app_passwd($_action, $_data = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'password_complexity'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($password != $password2) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'password_mismatch'
|
||||
);
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
@@ -88,15 +75,15 @@ function app_passwd($_action, $_data = null) {
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'app_passwd_added'
|
||||
);
|
||||
break;
|
||||
break;
|
||||
case 'edit':
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
$is_now = app_passwd('details', $id);
|
||||
if (!empty($is_now)) {
|
||||
$app_name = (!empty($_data['app_name'])) ? $_data['app_name'] : $is_now['name'];
|
||||
$password = (!empty($_data['password'])) ? $_data['password'] : null;
|
||||
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
|
||||
$password = (!empty($_data['app_passwd'])) ? $_data['app_passwd'] : null;
|
||||
$password2 = (!empty($_data['app_passwd2'])) ? $_data['app_passwd2'] : null;
|
||||
if (isset($_data['protocols'])) {
|
||||
$protocols = (array)$_data['protocols'];
|
||||
$imap_access = (in_array('imap_access', $protocols)) ? 1 : 0;
|
||||
@@ -126,20 +113,7 @@ function app_passwd($_action, $_data = null) {
|
||||
}
|
||||
$app_name = htmlspecialchars(trim($app_name));
|
||||
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'
|
||||
);
|
||||
if (password_check($password, $password2) !== true) {
|
||||
continue;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
@@ -182,7 +156,7 @@ function app_passwd($_action, $_data = null) {
|
||||
'msg' => array('object_modified', htmlspecialchars(implode(', ', $ids)))
|
||||
);
|
||||
}
|
||||
break;
|
||||
break;
|
||||
case 'delete':
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
@@ -213,19 +187,17 @@ function app_passwd($_action, $_data = null) {
|
||||
'msg' => array('app_passwd_removed', htmlspecialchars($id))
|
||||
);
|
||||
}
|
||||
break;
|
||||
break;
|
||||
case 'get':
|
||||
$app_passwds = array();
|
||||
$stmt = $pdo->prepare("SELECT `id`, `name` FROM `app_passwd` WHERE `mailbox` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$app_passwds = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $app_passwds;
|
||||
break;
|
||||
break;
|
||||
case 'details':
|
||||
$app_passwd_data = array();
|
||||
$stmt = $pdo->prepare("SELECT *
|
||||
FROM `app_passwd`
|
||||
WHERE `id` = :id");
|
||||
$stmt = $pdo->prepare("SELECT * FROM `app_passwd` WHERE `id` = :id");
|
||||
$stmt->execute(array(':id' => $_data));
|
||||
$app_passwd_data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($app_passwd_data)) {
|
||||
@@ -237,6 +209,6 @@ function app_passwd($_action, $_data = null) {
|
||||
}
|
||||
$app_passwd_data['name'] = htmlspecialchars(trim($app_passwd_data['name']));
|
||||
return $app_passwd_data;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
740
data/web/inc/functions.auth.inc.php
Normal file
740
data/web/inc/functions.auth.inc.php
Normal file
@@ -0,0 +1,740 @@
|
||||
<?php
|
||||
function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
|
||||
global $pdo;
|
||||
global $valkey;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
$role = $extra['role'];
|
||||
|
||||
// Try validate admin
|
||||
if (!isset($role) || $role == "admin") {
|
||||
$result = admin_login($user, $pass);
|
||||
if ($result !== false){
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Try validate domain admin
|
||||
if (!isset($role) || $role == "domain_admin") {
|
||||
$result = domainadmin_login($user, $pass);
|
||||
if ($result !== false) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Try validate app password
|
||||
if (!isset($role) || $role == "app") {
|
||||
$result = apppass_login($user, $pass, $app_passwd_data);
|
||||
if ($result !== false) {
|
||||
if ($app_passwd_data['eas'] === true) {
|
||||
$service = 'EAS';
|
||||
} elseif ($app_passwd_data['dav'] === true) {
|
||||
$service = 'DAV';
|
||||
} else {
|
||||
$service = 'NONE';
|
||||
}
|
||||
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
|
||||
set_sasl_log($user, $real_rip, $service, $pass);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Try validate user
|
||||
if (!isset($role) || $role == "user") {
|
||||
$result = user_login($user, $pass);
|
||||
if ($result !== false) {
|
||||
if ($app_passwd_data['eas'] === true) {
|
||||
$service = 'EAS';
|
||||
} elseif ($app_passwd_data['dav'] === true) {
|
||||
$service = 'DAV';
|
||||
} else {
|
||||
$service = 'MAILCOWUI';
|
||||
}
|
||||
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
|
||||
set_sasl_log($user, $real_rip, $service);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// skip log and only return false if it's an internal request
|
||||
if ($is_internal == true) return false;
|
||||
|
||||
if (!isset($_SESSION['ldelay'])) {
|
||||
$_SESSION['ldelay'] = "0";
|
||||
$valkey->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;
|
||||
$valkey->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 admin_login($user, $pass){
|
||||
global $pdo;
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
$_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));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass)) {
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
|
||||
// active tfa authenticators found, set pending user login
|
||||
$_SESSION['pending_mailcow_cc_username'] = $user;
|
||||
$_SESSION['pending_mailcow_cc_role'] = "admin";
|
||||
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
function domainadmin_login($user, $pass){
|
||||
global $pdo;
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => 'malformed_username'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
WHERE `superadmin` = '0'
|
||||
AND `active`='1'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass) !== false) {
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
|
||||
$_SESSION['pending_mailcow_cc_username'] = $user;
|
||||
$_SESSION['pending_mailcow_cc_role'] = "domainadmin";
|
||||
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
function user_login($user, $pass, $extra = null){
|
||||
global $pdo;
|
||||
global $iam_provider;
|
||||
global $iam_settings;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
$service = $extra['service'];
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => 'malformed_username'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
mailbox.*,
|
||||
domain.active AS d_active
|
||||
FROM `mailbox`
|
||||
INNER JOIN domain on mailbox.domain = domain.domain
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// user does not exist, try call idp login and create user if possible via rest flow
|
||||
if (!$row){
|
||||
$result = false;
|
||||
if ($iam_settings['authsource'] == 'keycloak' && intval($iam_settings['mailpassword_flow']) == 1){
|
||||
$result = keycloak_mbox_login_rest($user, $pass, array('is_internal' => $is_internal, 'create' => true));
|
||||
} else if ($iam_settings['authsource'] == 'ldap') {
|
||||
$result = ldap_mbox_login($user, $pass, array('is_internal' => $is_internal, 'create' => true));
|
||||
}
|
||||
if ($result !== false){
|
||||
// double check if mailbox is active
|
||||
$stmt = $pdo->prepare("SELECT * 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));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($row)) {
|
||||
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
|
||||
$row['attributes'] = json_decode($row['attributes'], true);
|
||||
if (isset($service)) {
|
||||
$key = strtolower($service) . "_access";
|
||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
clear_session();
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
|
||||
$row['attributes'] = json_decode($row['attributes'], true);
|
||||
if (isset($service)) {
|
||||
$key = strtolower($service) . "_access";
|
||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
switch ($row['authsource']) {
|
||||
case 'keycloak':
|
||||
// user authsource is keycloak, try using via rest flow
|
||||
if (intval($iam_settings['mailpassword_flow']) == 1){
|
||||
$result = keycloak_mbox_login_rest($user, $pass, array('is_internal' => $is_internal));
|
||||
if ($result !== false) {
|
||||
// double check if mailbox and domain is active
|
||||
$stmt = $pdo->prepare("SELECT * 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));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($row)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
|
||||
// authenticators found, init TFA flow
|
||||
$_SESSION['pending_mailcow_cc_username'] = $user;
|
||||
$_SESSION['pending_mailcow_cc_role'] = "user";
|
||||
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
|
||||
unset($_SESSION['ldelay']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: Keycloak'),
|
||||
'msg' => array('logged_in_as', $user)
|
||||
);
|
||||
return "pending";
|
||||
} else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
|
||||
// no authenticators found, login successfull
|
||||
if (!$is_internal){
|
||||
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, '*', 'Provider: Keycloak'),
|
||||
'msg' => array('logged_in_as', $user)
|
||||
);
|
||||
}
|
||||
return "user";
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'ldap':
|
||||
// user authsource is ldap
|
||||
$result = ldap_mbox_login($user, $pass, array('is_internal' => $is_internal));
|
||||
if ($result !== false) {
|
||||
// double check if mailbox and domain is active
|
||||
$stmt = $pdo->prepare("SELECT * 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));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($row)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
|
||||
// authenticators found, init TFA flow
|
||||
$_SESSION['pending_mailcow_cc_username'] = $user;
|
||||
$_SESSION['pending_mailcow_cc_role'] = "user";
|
||||
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
|
||||
unset($_SESSION['ldelay']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: LDAP'),
|
||||
'msg' => array('logged_in_as', $user)
|
||||
);
|
||||
return "pending";
|
||||
} else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
|
||||
// no authenticators found, login successfull
|
||||
if (!$is_internal){
|
||||
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, '*', 'Provider: LDAP'),
|
||||
'msg' => array('logged_in_as', $user)
|
||||
);
|
||||
}
|
||||
return "user";
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
break;
|
||||
case 'mailcow':
|
||||
if ($row['active'] != 1 || $row['d_active'] != 1) {
|
||||
return false;
|
||||
}
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass) !== false) {
|
||||
|
||||
if (intval($row['attributes']['force_pw_update']) == 1) {
|
||||
$_SESSION['pending_pw_update'] = true;
|
||||
}
|
||||
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
|
||||
// authenticators found, init TFA flow
|
||||
$_SESSION['pending_mailcow_cc_username'] = $user;
|
||||
$_SESSION['pending_mailcow_cc_role'] = "user";
|
||||
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
|
||||
unset($_SESSION['ldelay']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: mailcow'),
|
||||
'msg' => array('logged_in_as', $user)
|
||||
);
|
||||
return "pending";
|
||||
} else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
|
||||
// no authenticators found, login successfull
|
||||
if (!$is_internal){
|
||||
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, '*', 'Provider: mailcow'),
|
||||
'msg' => array('logged_in_as', $user)
|
||||
);
|
||||
}
|
||||
return "user";
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
function apppass_login($user, $pass, $app_passwd_data, $extra = null){
|
||||
global $pdo;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => 'malformed_username'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$protocol = false;
|
||||
if ($app_passwd_data['eas']){
|
||||
$protocol = 'eas';
|
||||
} else if ($app_passwd_data['dav']){
|
||||
$protocol = 'dav';
|
||||
} else if ($app_passwd_data['smtp']){
|
||||
$protocol = 'smtp';
|
||||
} else if ($app_passwd_data['imap']){
|
||||
$protocol = 'imap';
|
||||
} else if ($app_passwd_data['sieve']){
|
||||
$protocol = 'sieve';
|
||||
} else if ($app_passwd_data['pop3']){
|
||||
$protocol = 'pop3';
|
||||
} else if (!$is_internal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// fetch app password data
|
||||
$stmt = $pdo->prepare("SELECT `app_passwd`.*, `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
|
||||
INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
|
||||
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
|
||||
WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
|
||||
AND `mailbox`.`active` = '1'
|
||||
AND `domain`.`active` = '1'
|
||||
AND `app_passwd`.`active` = '1'
|
||||
AND `app_passwd`.`mailbox` = :user"
|
||||
);
|
||||
// fetch password data
|
||||
$stmt->execute(array(
|
||||
':user' => $user,
|
||||
));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if ($protocol && $row[$protocol . '_access'] != '1'){
|
||||
continue;
|
||||
}
|
||||
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass) !== false) {
|
||||
$_SESSION['app_passwd_id'] = $row['app_passwd_id'];
|
||||
unset($_SESSION['ldelay']);
|
||||
return "user";
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
// Keycloak REST Api Flow - auth user by mailcow_password attribute
|
||||
// This password will be used for direct UI, IMAP and SMTP Auth
|
||||
// To use direct user credentials, only Authorization Code Flow is valid
|
||||
function keycloak_mbox_login_rest($user, $pass, $extra = null){
|
||||
global $pdo;
|
||||
global $iam_provider;
|
||||
global $iam_settings;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
$create = $extra['create'];
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => 'malformed_username'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!$iam_provider) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// get access_token for service account of mailcow client
|
||||
$admin_token = identity_provider("get-keycloak-admin-token");
|
||||
|
||||
// get the mailcow_password attribute from keycloak user
|
||||
$url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users";
|
||||
$queryParams = array('email' => $user, 'exact' => true);
|
||||
$queryString = http_build_query($queryParams);
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 7);
|
||||
curl_setopt($curl, CURLOPT_URL, $url . '?' . $queryString);
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, array(
|
||||
'Authorization: Bearer ' . $admin_token,
|
||||
'Content-Type: application/json'
|
||||
));
|
||||
$user_res = json_decode(curl_exec($curl), true)[0];
|
||||
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
curl_close($curl);
|
||||
if ($code != 200) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Identity Provider returned HTTP ' . $code),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!isset($user_res['attributes']['mailcow_password']) || !is_array($user_res['attributes']['mailcow_password'])){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'User has no mailcow_password attribute'),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (empty($user_res['attributes']['mailcow_password'][0])){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', "User's mailcow_password attribute is empty"),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// validate mailcow_password
|
||||
$mailcow_password = $user_res['attributes']['mailcow_password'][0];
|
||||
if (!verify_hash($mailcow_password, $pass)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// get mapped template
|
||||
$user_template = $user_res['attributes']['mailcow_template'][0];
|
||||
$mapper_key = array_search($user_template, $iam_settings['mappers']);
|
||||
|
||||
if (!$create) {
|
||||
// login success
|
||||
if ($mapper_key !== false) {
|
||||
// update user
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
mailbox('edit', 'mailbox_from_template', array(
|
||||
'username' => $user,
|
||||
'name' => $user_res['name'],
|
||||
'template' => $iam_settings['templates'][$mapper_key]
|
||||
));
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
}
|
||||
return 'user';
|
||||
}
|
||||
|
||||
// check if login provisioning is enabled before creating user
|
||||
if (!$iam_settings['login_provisioning']){
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, "Auto-create users on login is deactivated"),
|
||||
'msg' => 'login_failed'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// check if matching attribute exist
|
||||
if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) {
|
||||
if (!empty($iam_settings['default_template'])) {
|
||||
$mbox_template = $iam_settings['default_template'];
|
||||
} else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'No matching attribute mapping was found'),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
}
|
||||
|
||||
// create mailbox
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
$create_res = mailbox('add', 'mailbox_from_template', array(
|
||||
'domain' => explode('@', $user)[1],
|
||||
'local_part' => explode('@', $user)[0],
|
||||
'name' => $user_res['name'],
|
||||
'authsource' => 'keycloak',
|
||||
'template' => $mbox_template
|
||||
));
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
if (!$create_res){
|
||||
clear_session();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Could not create mailbox on login'),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return 'user';
|
||||
}
|
||||
function ldap_mbox_login($user, $pass, $extra = null){
|
||||
global $pdo;
|
||||
global $iam_provider;
|
||||
global $iam_settings;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
$create = $extra['create'];
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => 'malformed_username'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!$iam_provider) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$ldap_query = $iam_provider->query();
|
||||
if (!empty($iam_settings['filter'])) {
|
||||
$ldap_query = $ldap_query->rawFilter($iam_settings['filter']);
|
||||
}
|
||||
$ldap_query = $ldap_query->where($iam_settings['username_field'], '=', $user)
|
||||
->select([$iam_settings['username_field'], $iam_settings['attribute_field'], 'displayname', 'distinguishedname', 'dn']);
|
||||
|
||||
$user_res = $ldap_query->firstOrFail();
|
||||
} catch (Exception $e) {
|
||||
// clear $_SESSION['return'] to not leak data
|
||||
$_SESSION['return'] = array();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (!$iam_provider->auth()->attempt($user_res['dn'], $pass)) {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// clear $_SESSION['return'] to not leak data
|
||||
$_SESSION['return'] = array();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// get mapped template
|
||||
$user_template = $user_res[$iam_settings['attribute_field']][0];
|
||||
$mapper_key = array_search($user_template, $iam_settings['mappers']);
|
||||
|
||||
if (!$create) {
|
||||
// login success
|
||||
if ($mapper_key !== false) {
|
||||
// update user
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
mailbox('edit', 'mailbox_from_template', array(
|
||||
'username' => $user,
|
||||
'name' => $user_res['displayname'][0],
|
||||
'template' => $iam_settings['templates'][$mapper_key]
|
||||
));
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
}
|
||||
return 'user';
|
||||
}
|
||||
|
||||
// check if login provisioning is enabled before creating user
|
||||
if (!$iam_settings['login_provisioning']){
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, "Auto-create users on login is deactivated"),
|
||||
'msg' => 'login_failed'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// check if matching attribute exist
|
||||
if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) {
|
||||
if (!empty($iam_settings['default_template'])) {
|
||||
$mbox_template = $iam_settings['default_template'];
|
||||
} else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'No matching attribute mapping was found'),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
}
|
||||
|
||||
// create mailbox
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
$create_res = mailbox('add', 'mailbox_from_template', array(
|
||||
'domain' => explode('@', $user)[1],
|
||||
'local_part' => explode('@', $user)[0],
|
||||
'name' => $user_res['displayname'][0],
|
||||
'authsource' => 'ldap',
|
||||
'template' => $mbox_template
|
||||
));
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
if (!$create_res){
|
||||
clear_session();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Could not create mailbox on login'),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return 'user';
|
||||
}
|
||||
@@ -122,10 +122,16 @@ function customize($_action, $_item, $_data = null) {
|
||||
case 'app_links':
|
||||
$apps = (array)$_data['app'];
|
||||
$links = (array)$_data['href'];
|
||||
$user_links = (array)$_data['user_href'];
|
||||
$hide = (array)$_data['hide'];
|
||||
$out = array();
|
||||
if (count($apps) == count($links)) {
|
||||
if (count($apps) == count($links) && count($apps) == count($user_links) && count($apps) == count($hide)) {
|
||||
for ($i = 0; $i < count($apps); $i++) {
|
||||
$out[] = array($apps[$i] => $links[$i]);
|
||||
$out[] = array($apps[$i] => array(
|
||||
'link' => $links[$i],
|
||||
'user_link' => $user_links[$i],
|
||||
'hide' => ($hide[$i] === '0' || $hide[$i] === 0) ? false : true
|
||||
));
|
||||
}
|
||||
try {
|
||||
$valkey->set('APP_LINKS', json_encode($out));
|
||||
@@ -198,6 +204,35 @@ function customize($_action, $_item, $_data = null) {
|
||||
'msg' => 'ip_check_opt_in_modified'
|
||||
);
|
||||
break;
|
||||
case 'custom_login':
|
||||
$hide_user_quicklink = ($_data['hide_user_quicklink'] == "1") ? 1 : 0;
|
||||
$hide_domainadmin_quicklink = ($_data['hide_domainadmin_quicklink'] == "1") ? 1 : 0;
|
||||
$hide_admin_quicklink = ($_data['hide_admin_quicklink'] == "1") ? 1 : 0;
|
||||
$force_sso = ($_data['force_sso'] == "1") ? 1 : 0;
|
||||
|
||||
$custom_login = array(
|
||||
"hide_user_quicklink" => $hide_user_quicklink,
|
||||
"hide_domainadmin_quicklink" => $hide_domainadmin_quicklink,
|
||||
"hide_admin_quicklink" => $hide_admin_quicklink,
|
||||
"force_sso" => $force_sso,
|
||||
);
|
||||
try {
|
||||
$valkey->set('CUSTOM_LOGIN', json_encode($custom_login));
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'custom_login_modified'
|
||||
);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
@@ -256,7 +291,23 @@ function customize($_action, $_item, $_data = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return ($app_links) ? $app_links : false;
|
||||
|
||||
if (empty($app_links)){
|
||||
return [];
|
||||
}
|
||||
|
||||
// convert from old style
|
||||
foreach($app_links as $i => $entry){
|
||||
foreach($entry as $app => $link){
|
||||
if (empty($link['link']) && empty($link['user_link'])){
|
||||
$app_links[$i][$app] = array();
|
||||
$app_links[$i][$app]['link'] = $link;
|
||||
$app_links[$i][$app]['user_link'] = $link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $app_links;
|
||||
break;
|
||||
case 'main_logo':
|
||||
case 'main_logo_dark':
|
||||
@@ -274,8 +325,10 @@ function customize($_action, $_item, $_data = null) {
|
||||
break;
|
||||
case 'ui_texts':
|
||||
try {
|
||||
$data['title_name'] = ($title_name = $valkey->get('TITLE_NAME')) ? $title_name : 'mailcow UI';
|
||||
$data['main_name'] = ($main_name = $valkey->get('MAIN_NAME')) ? $main_name : 'mailcow UI';
|
||||
$mailcow_hostname = strtolower(getenv("MAILCOW_HOSTNAME"));
|
||||
|
||||
$data['title_name'] = ($title_name = $valkey->get('TITLE_NAME')) ? $title_name : "$mailcow_hostname - mail UI";
|
||||
$data['main_name'] = ($main_name = $valkey->get('MAIN_NAME')) ? $main_name : "$mailcow_hostname - mail UI";
|
||||
$data['apps_name'] = ($apps_name = $valkey->get('APPS_NAME')) ? $apps_name : $lang['header']['apps'];
|
||||
$data['help_text'] = ($help_text = $valkey->get('HELP_TEXT')) ? $help_text : false;
|
||||
if (!empty($valkey->get('UI_IMPRESS'))) {
|
||||
@@ -335,6 +388,20 @@ function customize($_action, $_item, $_data = null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'custom_login':
|
||||
try {
|
||||
$custom_login = $valkey->get('CUSTOM_LOGIN');
|
||||
return $custom_login ? json_decode($custom_login, true) : array();
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -240,9 +240,12 @@ function dkim($_action, $_data = null, $privkey = false) {
|
||||
if (strlen($dkimdata['pubkey']) < 391) {
|
||||
$dkimdata['length'] = "1024";
|
||||
}
|
||||
elseif (strlen($dkimdata['pubkey']) < 736) {
|
||||
elseif (strlen($dkimdata['pubkey']) < 564) {
|
||||
$dkimdata['length'] = "2048";
|
||||
}
|
||||
elseif (strlen($dkimdata['pubkey']) < 736) {
|
||||
$dkimdata['length'] = "3072";
|
||||
}
|
||||
elseif (strlen($dkimdata['pubkey']) < 1416) {
|
||||
$dkimdata['length'] = "4096";
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
global $valkey;
|
||||
global $lang;
|
||||
global $MAILBOX_DEFAULT_ATTRIBUTES;
|
||||
global $iam_settings;
|
||||
|
||||
$_data_log = $_data;
|
||||
!isset($_data_log['password']) ?: $_data_log['password'] = '*';
|
||||
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
|
||||
@@ -682,15 +684,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
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']);
|
||||
$addresses = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['address']));
|
||||
$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto']));
|
||||
$internal = intval($_data['internal']);
|
||||
$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'];
|
||||
$public_comment = $_data['public_comment'];
|
||||
if (strlen($private_comment) > 160 | strlen($public_comment) > 160){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
@@ -840,8 +843,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
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)");
|
||||
$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `active`)
|
||||
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :active)");
|
||||
if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
|
||||
$stmt->execute(array(
|
||||
':address' => '@'.$domain,
|
||||
@@ -851,6 +854,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':goto' => $goto,
|
||||
':domain' => $domain,
|
||||
':sogo_visible' => $sogo_visible,
|
||||
':internal' => $internal,
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
@@ -862,6 +866,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':goto' => $goto,
|
||||
':domain' => $domain,
|
||||
':sogo_visible' => $sogo_visible,
|
||||
':internal' => $internal,
|
||||
':active' => $active
|
||||
));
|
||||
}
|
||||
@@ -1005,6 +1010,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$local_part = strtolower(trim($_data['local_part']));
|
||||
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
$username = $local_part . '@' . $domain;
|
||||
$authsource = 'mailcow';
|
||||
if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
@@ -1021,15 +1027,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($_data['authsource'] == "mailcow" ||
|
||||
in_array($_data['authsource'], array('keycloak', 'generic-oidc', 'ldap')) && $iam_settings['authsource'] == $_data['authsource']){
|
||||
$authsource = $_data['authsource'];
|
||||
}
|
||||
if (empty($name)) {
|
||||
$name = $local_part;
|
||||
}
|
||||
$template_attr = null;
|
||||
if ($_data['template']){
|
||||
$template_attr = mailbox('get', 'mailbox_templates', $_data['template'])['attributes'];
|
||||
$template_attr = mailbox('get', 'mailbox_templates', $_data['template'], $_extra)['attributes'];
|
||||
}
|
||||
if (empty($template_attr)) {
|
||||
$template_attr = mailbox('get', 'mailbox_templates')[0]['attributes'];
|
||||
$template_attr = mailbox('get', 'mailbox_templates', null, $_extra)[0]['attributes'];
|
||||
}
|
||||
$MAILBOX_DEFAULT_ATTRIBUTES = array_merge($MAILBOX_DEFAULT_ATTRIBUTES, $template_attr);
|
||||
|
||||
@@ -1038,7 +1048,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$name = ltrim(rtrim($_data['name'], '>'), '<');
|
||||
$tags = (isset($_data['tags'])) ? $_data['tags'] : $MAILBOX_DEFAULT_ATTRIBUTES['tags'];
|
||||
$quota_m = (isset($_data['quota'])) ? intval($_data['quota']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['quota']) / 1024 ** 2;
|
||||
if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) {
|
||||
if ($authsource != 'mailcow'){
|
||||
$password = '';
|
||||
$password2 = '';
|
||||
$password_hashed = '';
|
||||
}
|
||||
if (!hasACLAccess("unlimited_quota") && $quota_m === 0) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
@@ -1067,6 +1082,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$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);
|
||||
$attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
|
||||
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
|
||||
$force_pw_update = 0;
|
||||
}
|
||||
$mailbox_attrs = json_encode(
|
||||
array(
|
||||
'force_pw_update' => strval($force_pw_update),
|
||||
@@ -1081,7 +1100,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'passwd_update' => time(),
|
||||
'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']),
|
||||
'quarantine_notification' => strval($quarantine_notification),
|
||||
'quarantine_category' => strval($quarantine_category)
|
||||
'quarantine_category' => strval($quarantine_category),
|
||||
'attribute_hash' => $attribute_hash
|
||||
)
|
||||
);
|
||||
if (!is_valid_domain_name($domain)) {
|
||||
@@ -1156,10 +1176,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
if ($authsource == 'mailcow'){
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
if ($MailboxData['count'] >= $DomainData['mailboxes']) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
@@ -1185,8 +1207,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
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 = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `quota`, `local_part`, `domain`, `attributes`, `authsource`, `active`)
|
||||
VALUES (:username, :password_hashed, :name, :quota_b, :local_part, :domain, :mailbox_attrs, :authsource, :active)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':password_hashed' => $password_hashed,
|
||||
@@ -1195,6 +1217,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':local_part' => $local_part,
|
||||
':domain' => $domain,
|
||||
':mailbox_attrs' => $mailbox_attrs,
|
||||
':authsource' => $authsource,
|
||||
':active' => $active
|
||||
));
|
||||
$stmt = $pdo->prepare("UPDATE `mailbox` SET
|
||||
@@ -1203,6 +1226,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$stmt->execute(array(
|
||||
':username' => $username
|
||||
));
|
||||
// save delimiter_action
|
||||
if (isset($_data['tagged_mail_handler'])) {
|
||||
mailbox('edit', 'delimiter_action', array(
|
||||
'username' => $username,
|
||||
'tagged_mail_handler' => $_data['tagged_mail_handler']
|
||||
));
|
||||
}
|
||||
|
||||
// save tags
|
||||
foreach($tags as $index => $tag){
|
||||
if (empty($tag)) continue;
|
||||
@@ -1214,11 +1245,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
break;
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `tags_mailbox` (`username`, `tag_name`) VALUES (:username, :tag_name)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':tag_name' => $tag,
|
||||
));
|
||||
try {
|
||||
$stmt = $pdo->prepare("INSERT INTO `tags_mailbox` (`username`, `tag_name`) VALUES (:username, :tag_name)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':tag_name' => $tag,
|
||||
));
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `quota2` (`username`, `bytes`, `messages`)
|
||||
VALUES (:username, '0', '0') ON DUPLICATE KEY UPDATE `bytes` = '0', `messages` = '0';");
|
||||
@@ -1312,16 +1346,136 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'object' => $username,
|
||||
'rl_frame' => $_data['rl_frame'],
|
||||
'rl_value' => $_data['rl_value']
|
||||
));
|
||||
), $_extra);
|
||||
}
|
||||
|
||||
update_sogo_static_view($username);
|
||||
try {
|
||||
update_sogo_static_view($username);
|
||||
} catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => $e->getMessage()
|
||||
);
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => array('mailbox_added', htmlspecialchars($username))
|
||||
);
|
||||
return true;
|
||||
break;
|
||||
case 'mailbox_from_template':
|
||||
$stmt = $pdo->prepare("SELECT * FROM `templates`
|
||||
WHERE `template` = :template AND type = 'mailbox'");
|
||||
$stmt->execute(array(
|
||||
":template" => $_data['template']
|
||||
));
|
||||
$mbox_template_data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($mbox_template_data)){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'template_missing'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$attribute_hash = sha1(json_encode($mbox_template_data["attributes"]));
|
||||
$mbox_template_data = json_decode($mbox_template_data["attributes"], true);
|
||||
$mbox_template_data['domain'] = $_data['domain'];
|
||||
$mbox_template_data['name'] = $_data['name'];
|
||||
$mbox_template_data['local_part'] = $_data['local_part'];
|
||||
$mbox_template_data['authsource'] = $_data['authsource'];
|
||||
$mbox_template_data['attribute_hash'] = $attribute_hash;
|
||||
$mbox_template_data['quota'] = intval($mbox_template_data['quota'] / 1048576);
|
||||
|
||||
$mailbox_attributes = array('acl' => array());
|
||||
foreach ($mbox_template_data as $key => $value){
|
||||
switch (true) {
|
||||
case (strpos($key, 'acl_') === 0 && $value != 0):
|
||||
array_push($mailbox_attributes['acl'], str_replace('acl_' , '', $key));
|
||||
break;
|
||||
default:
|
||||
$mailbox_attributes[$key] = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return mailbox('add', 'mailbox', $mailbox_attributes);
|
||||
break;
|
||||
case 'mta_sts':
|
||||
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
$version = strtolower($_data['version']);
|
||||
$mode = strtolower($_data['mode']);
|
||||
$mx = explode(",", preg_replace('/\s+/', '', $_data['mx']));
|
||||
$max_age = intval($_data['max_age']);
|
||||
$active = (intval($_data['active']) == 1) ? 1 : 0;
|
||||
$id = date('YmdHis');
|
||||
|
||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (empty($version) || !in_array($version, array('stsv1'))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
|
||||
'msg' => array('version_invalid', htmlspecialchars($domain))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (empty($mode) || !in_array($mode, array('enforce', 'testing', 'none'))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
|
||||
'msg' => array('mode_invalid', htmlspecialchars($domain))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (empty($max_age) || $max_age < 0 || $max_age > 31536000) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
|
||||
'msg' => array('max_age_invalid', htmlspecialchars($domain))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
foreach ($mx as $index => $mx_domain) {
|
||||
$mx_domain = idn_to_ascii(strtolower(trim($mx_domain)), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
if (!is_valid_domain_name($mx_domain, array('allow_wildcard' => true))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
|
||||
'msg' => array('mx_invalid', htmlspecialchars($mx_domain))
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("INSERT INTO `mta_sts` (`id`, `domain`, `version`, `mode`, `mx`, `max_age`, `active`)
|
||||
VALUES (:id, :domain, :version, :mode, :mx, :max_age, :active)");
|
||||
$stmt->execute(array(
|
||||
':id' => $id,
|
||||
':domain' => $domain,
|
||||
':version' => $version,
|
||||
':mode' => $mode,
|
||||
':mx' => implode(",", $mx),
|
||||
':max_age' => $max_age,
|
||||
':active' => $active
|
||||
));
|
||||
} catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => $e->getMessage()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'resource':
|
||||
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
@@ -1544,6 +1698,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attr = array();
|
||||
$attr["quota"] = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0;
|
||||
$attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array();
|
||||
$attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['tagged_mail_handler']);
|
||||
$attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
|
||||
$attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
|
||||
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
|
||||
@@ -1689,7 +1844,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
else {
|
||||
$usernames = $_data['username'];
|
||||
}
|
||||
if (!isset($_SESSION['acl']['tls_policy']) || $_SESSION['acl']['tls_policy'] != "1" ) {
|
||||
if (!hasACLAccess("tls_policy")) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
@@ -1698,7 +1853,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
return false;
|
||||
}
|
||||
foreach ($usernames as $username) {
|
||||
if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $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),
|
||||
@@ -1706,7 +1861,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$is_now = mailbox('get', 'tls_policy', $username);
|
||||
$is_now = mailbox('get', 'tls_policy', $username, $_extra);
|
||||
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'];
|
||||
@@ -1743,7 +1898,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
else {
|
||||
$usernames = $_data['username'];
|
||||
}
|
||||
if (!isset($_SESSION['acl']['quarantine_notification']) || $_SESSION['acl']['quarantine_notification'] != "1" ) {
|
||||
if (!hasACLAccess("quarantine_notification")) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
@@ -1752,7 +1907,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
return false;
|
||||
}
|
||||
foreach ($usernames as $username) {
|
||||
if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $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),
|
||||
@@ -1760,7 +1915,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$is_now = mailbox('get', 'quarantine_notification', $username);
|
||||
$is_now = mailbox('get', 'quarantine_notification', $username, $_extra);
|
||||
if (!empty($is_now)) {
|
||||
$quarantine_notification = (isset($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification'];
|
||||
}
|
||||
@@ -1802,7 +1957,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
else {
|
||||
$usernames = $_data['username'];
|
||||
}
|
||||
if (!isset($_SESSION['acl']['quarantine_category']) || $_SESSION['acl']['quarantine_category'] != "1" ) {
|
||||
if (!hasACLAccess("quarantine_category")) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
@@ -1811,7 +1966,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
return false;
|
||||
}
|
||||
foreach ($usernames as $username) {
|
||||
if (!filter_var($username, FILTER_VALIDATE_EMAIL) || !hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $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),
|
||||
@@ -1819,7 +1974,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$is_now = mailbox('get', 'quarantine_category', $username);
|
||||
$is_now = mailbox('get', 'quarantine_category', $username, $_extra);
|
||||
if (!empty($is_now)) {
|
||||
$quarantine_category = (isset($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category'];
|
||||
}
|
||||
@@ -2329,6 +2484,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
foreach ($ids as $id) {
|
||||
$is_now = mailbox('get', 'alias_details', $id);
|
||||
if (!empty($is_now)) {
|
||||
$internal = (isset($_data['internal'])) ? intval($_data['internal']) : $is_now['internal'];
|
||||
$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;
|
||||
@@ -2514,6 +2670,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`domain` = :domain,
|
||||
`goto` = :goto,
|
||||
`sogo_visible`= :sogo_visible,
|
||||
`internal`= :internal,
|
||||
`active`= :active
|
||||
WHERE `id` = :id");
|
||||
$stmt->execute(array(
|
||||
@@ -2523,6 +2680,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':domain' => $domain,
|
||||
':goto' => $goto,
|
||||
':sogo_visible' => $sogo_visible,
|
||||
':internal' => $internal,
|
||||
':active' => $active,
|
||||
':id' => $is_now['id']
|
||||
));
|
||||
@@ -2863,7 +3021,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$is_now = mailbox('get', 'mailbox_details', $username);
|
||||
$is_now = mailbox('get', 'mailbox_details', $username, $_extra);
|
||||
if (isset($_data['protocol_access'])) {
|
||||
$_data['protocol_access'] = (array)$_data['protocol_access'];
|
||||
$_data['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
|
||||
@@ -2874,20 +3032,29 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
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']['sogo_access']) && $_SESSION['acl']['sogo_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)$sieve_access = (isset($_data['sieve_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
|
||||
(int)$relayhost = (isset($_data['relayhost']) && isset($_SESSION['acl']['mailbox_relayhost']) && $_SESSION['acl']['mailbox_relayhost'] == "1") ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
|
||||
(int)$sogo_access = (isset($_data['sogo_access']) && hasACLAccess("sogo_access")) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
|
||||
(int)$imap_access = (isset($_data['imap_access']) && hasACLAccess("protocol_access")) ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
|
||||
(int)$pop3_access = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
|
||||
(int)$smtp_access = (isset($_data['smtp_access']) && hasACLAccess("protocol_access")) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
|
||||
(int)$sieve_access = (isset($_data['sieve_access']) && hasACLAccess("protocol_access")) ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
|
||||
(int)$relayhost = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
|
||||
(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;
|
||||
$pw_recovery_email = (isset($_data['pw_recovery_email'])) ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
|
||||
$tags = (is_array($_data['tags']) ? $_data['tags'] : array());
|
||||
$attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
|
||||
$authsource = $is_now['authsource'];
|
||||
if ($_data['authsource'] == "mailcow" ||
|
||||
in_array($_data['authsource'], array('keycloak', 'generic-oidc', 'ldap')) && $iam_settings['authsource'] == $_data['authsource']){
|
||||
$authsource = $_data['authsource'];
|
||||
}
|
||||
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
|
||||
$force_pw_update = 0;
|
||||
}
|
||||
$pw_recovery_email = (isset($_data['pw_recovery_email']) && $authsource == 'mailcow') ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
|
||||
}
|
||||
else {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -2898,7 +3065,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
continue;
|
||||
}
|
||||
// if already 0 == ok
|
||||
if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && ($quota_m == 0 && $is_now['quota'] != 0)) {
|
||||
if (!hasACLAccess("unlimited_quota") && ($quota_m == 0 && $is_now['quota'] != 0)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
@@ -2914,7 +3081,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
$DomainData = mailbox('get', 'domain_details', $domain);
|
||||
$DomainData = mailbox('get', 'domain_details', $domain, $_extra);
|
||||
if ($quota_m > ($is_now['max_new_quota'] / 1048576)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
@@ -2933,7 +3100,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
}
|
||||
$extra_acls = array();
|
||||
if (isset($_data['extended_sender_acl'])) {
|
||||
if (!isset($_SESSION['acl']['extend_sender_acl']) || $_SESSION['acl']['extend_sender_acl'] != "1" ) {
|
||||
if (!hasACLAccess("extend_sender_acl")) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
@@ -3126,7 +3293,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$stmt = $pdo->prepare("UPDATE `mailbox` SET
|
||||
`password` = :password_hashed,
|
||||
`attributes` = JSON_SET(`attributes`, '$.passwd_update', NOW())
|
||||
WHERE `username` = :username");
|
||||
WHERE `username` = :username AND authsource = 'mailcow'");
|
||||
$stmt->execute(array(
|
||||
':password_hashed' => $password_hashed,
|
||||
':username' => $username
|
||||
@@ -3145,6 +3312,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`active` = :active,
|
||||
`name`= :name,
|
||||
`quota` = :quota_b,
|
||||
`authsource` = :authsource,
|
||||
`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),
|
||||
@@ -3152,22 +3320,25 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
|
||||
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email)
|
||||
`attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email),
|
||||
`attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash)
|
||||
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,
|
||||
':sieve_access' => $sieve_access,
|
||||
':smtp_access' => $smtp_access,
|
||||
':recovery_email' => $pw_recovery_email,
|
||||
':relayhost' => $relayhost,
|
||||
':username' => $username
|
||||
));
|
||||
$stmt->execute(array(
|
||||
':active' => $active,
|
||||
':name' => $name,
|
||||
':quota_b' => $quota_b,
|
||||
':attribute_hash' => $attribute_hash,
|
||||
':force_pw_update' => $force_pw_update,
|
||||
':sogo_access' => $sogo_access,
|
||||
':imap_access' => $imap_access,
|
||||
':pop3_access' => $pop3_access,
|
||||
':sieve_access' => $sieve_access,
|
||||
':smtp_access' => $smtp_access,
|
||||
':recovery_email' => $pw_recovery_email,
|
||||
':relayhost' => $relayhost,
|
||||
':username' => $username,
|
||||
':authsource' => $authsource
|
||||
));
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -3177,6 +3348,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
// save delimiter_action
|
||||
if (isset($_data['tagged_mail_handler'])) {
|
||||
mailbox('edit', 'delimiter_action', array(
|
||||
'username' => $username,
|
||||
'tagged_mail_handler' => $_data['tagged_mail_handler']
|
||||
));
|
||||
}
|
||||
// save tags
|
||||
foreach($tags as $index => $tag){
|
||||
if (empty($tag)) continue;
|
||||
@@ -3188,11 +3366,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
);
|
||||
break;
|
||||
}
|
||||
$stmt = $pdo->prepare("INSERT INTO `tags_mailbox` (`username`, `tag_name`) VALUES (:username, :tag_name)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':tag_name' => $tag,
|
||||
));
|
||||
try {
|
||||
$stmt = $pdo->prepare("INSERT INTO `tags_mailbox` (`username`, `tag_name`) VALUES (:username, :tag_name)");
|
||||
$stmt->execute(array(
|
||||
':username' => $username,
|
||||
':tag_name' => $tag,
|
||||
));
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -3201,7 +3382,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'msg' => array('mailbox_modified', $username)
|
||||
);
|
||||
|
||||
update_sogo_static_view($username);
|
||||
try {
|
||||
update_sogo_static_view($username);
|
||||
} catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => $e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
break;
|
||||
@@ -3231,7 +3420,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
}
|
||||
|
||||
$is_now = mailbox('get', 'mailbox_details', $old_username);
|
||||
if (empty($is_now)) {
|
||||
if (empty($is_now) || ($is_now['active'] != '1' && $is_now['active'] != '2')) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
@@ -3401,6 +3590,75 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'msg' => array('mailbox_renamed', $old_username, $new_username)
|
||||
);
|
||||
break;
|
||||
case 'mailbox_from_template':
|
||||
$stmt = $pdo->prepare("SELECT * FROM `templates`
|
||||
WHERE `template` = :template AND type = 'mailbox'");
|
||||
$stmt->execute(array(
|
||||
":template" => $_data['template']
|
||||
));
|
||||
$mbox_template_data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($mbox_template_data)){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'template_missing'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$attribute_hash = sha1(json_encode($mbox_template_data["attributes"]));
|
||||
$is_now = mailbox('get', 'mailbox_details', $_data['username']);
|
||||
$name = ltrim(rtrim($_data['name'], '>'), '<');
|
||||
if ($is_now['attributes']['attribute_hash'] == $attribute_hash && $is_now['name'] == $name)
|
||||
return true;
|
||||
|
||||
$mbox_template_data = json_decode($mbox_template_data["attributes"], true);
|
||||
$mbox_template_data['attribute_hash'] = $attribute_hash;
|
||||
$mbox_template_data['name'] = $name;
|
||||
$quarantine_attributes = array('username' => $_data['username']);
|
||||
$tls_attributes = array('username' => $_data['username']);
|
||||
$ratelimit_attributes = array('object' => $_data['username']);
|
||||
$acl_attributes = array('username' => $_data['username'], 'user_acl' => array());
|
||||
$mailbox_attributes = array('username' => $_data['username']);
|
||||
foreach ($mbox_template_data as $key => $value){
|
||||
switch (true) {
|
||||
case (strpos($key, 'quarantine_') === 0):
|
||||
$quarantine_attributes[$key] = $value;
|
||||
break;
|
||||
case (strpos($key, 'tls_') === 0):
|
||||
if ($value == null)
|
||||
$value = 0;
|
||||
$tls_attributes[$key] = $value;
|
||||
break;
|
||||
case (strpos($key, 'rl_') === 0):
|
||||
$ratelimit_attributes[$key] = $value;
|
||||
break;
|
||||
case (strpos($key, 'acl_') === 0 && $value != 0):
|
||||
array_push($acl_attributes['user_acl'], str_replace('acl_' , '', $key));
|
||||
break;
|
||||
default:
|
||||
$mailbox_attributes[$key] = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$mailbox_attributes['quota'] = intval($mailbox_attributes['quota'] / 1048576);
|
||||
$result = mailbox('edit', 'mailbox', $mailbox_attributes);
|
||||
if ($result === false) return $result;
|
||||
$result = mailbox('edit', 'tls_policy', $tls_attributes);
|
||||
if ($result === false) return $result;
|
||||
$result = mailbox('edit', 'quarantine_notification', $quarantine_attributes);
|
||||
if ($result === false) return $result;
|
||||
$result = mailbox('edit', 'quarantine_category', $quarantine_attributes);
|
||||
if ($result === false) return $result;
|
||||
$result = ratelimit('edit', 'mailbox', $ratelimit_attributes);
|
||||
if ($result === false) return $result;
|
||||
$result = acl('edit', 'user', $acl_attributes);
|
||||
if ($result === false) return $result;
|
||||
|
||||
$_SESSION['return'] = array();
|
||||
return true;
|
||||
break;
|
||||
case 'mailbox_templates':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||
$_SESSION['return'][] = array(
|
||||
@@ -3442,6 +3700,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$attr = array();
|
||||
$attr["quota"] = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0;
|
||||
$attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : $is_now['tags'];
|
||||
$attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : $is_now['tagged_mail_handler'];
|
||||
$attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification'];
|
||||
$attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category'];
|
||||
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : $is_now['rl_frame'];
|
||||
@@ -3562,6 +3821,125 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
|
||||
return true;
|
||||
break;
|
||||
case 'mta_sts':
|
||||
if (!is_array($_data['domains'])) {
|
||||
$domains = array();
|
||||
$domains[] = $_data['domains'];
|
||||
}
|
||||
else {
|
||||
$domains = $_data['domains'];
|
||||
}
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
$domain = idn_to_ascii(strtolower(trim($domain)), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
|
||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$is_now = mailbox('get', 'mta_sts', $domain);
|
||||
if (!empty($is_now)) {
|
||||
$version = (isset($_data['version'])) ? strtolower($_data['version']) : $is_now['version'];
|
||||
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
|
||||
$active = ($active == 1) ? 1 : 0;
|
||||
$mode = (isset($_data['mode'])) ? strtolower($_data['mode']) : $is_now['mode'];
|
||||
$mx = (isset($_data['mx'])) ? explode(",", preg_replace('/\s+/', '', $_data['mx'])) : $is_now['mx'];
|
||||
$max_age = (isset($_data['max_age'])) ? intval($_data['max_age']) : $is_now['max_age'];
|
||||
|
||||
// Update ID if neccesary
|
||||
if ($version != strtolower($is_now['version']) ||
|
||||
$mode != strtolower($is_now['mode']) ||
|
||||
$mx != $is_now['mx'] ||
|
||||
$max_age != $is_now['max_age']) {
|
||||
$id = date('YmdHis');
|
||||
} else {
|
||||
$id = $is_now['id'];
|
||||
}
|
||||
|
||||
} else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'access_denied'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($version) || !in_array($version, array('stsv1'))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
|
||||
'msg' => array('version_invalid', htmlspecialchars($version))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (empty($mode) || !in_array($mode, array('enforce', 'testing', 'none'))) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
|
||||
'msg' => array('mode_invalid', htmlspecialchars($domain))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (empty($max_age) || $max_age < 0 || $max_age > 31557600) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
|
||||
'msg' => array('max_age_invalid', htmlspecialchars($domain))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
foreach ($mx as $index => $mx_domain) {
|
||||
$mx_domain = idn_to_ascii(strtolower(trim($mx_domain)), 0, INTL_IDNA_VARIANT_UTS46);
|
||||
$invalid_mx = false;
|
||||
if (!is_valid_domain_name($mx_domain, array('allow_wildcard' => true))) {
|
||||
$invalid_mx = $mx_domain;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($invalid_mx) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
|
||||
'msg' => array('mx_invalid', htmlspecialchars($invalid_mx))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE `mta_sts` SET `id` = :id, `version` = :version, `mode` = :mode, `mx` = :mx, `max_age` = :max_age, `active` = :active WHERE `domain` = :domain");
|
||||
$stmt->execute(array(
|
||||
':id' => $id,
|
||||
':domain' => $domain,
|
||||
':version' => $version,
|
||||
':mode' => $mode,
|
||||
':mx' => implode(",", $mx),
|
||||
':max_age' => $max_age,
|
||||
':active' => $active
|
||||
));
|
||||
} catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data),
|
||||
'msg' => $e->getMessage()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
|
||||
'msg' => array('object_modified', $domain)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
break;
|
||||
case 'resource':
|
||||
if (!is_array($_data['name'])) {
|
||||
$names = array();
|
||||
@@ -4328,6 +4706,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`address`,
|
||||
`public_comment`,
|
||||
`private_comment`,
|
||||
`internal`,
|
||||
`active`,
|
||||
`sogo_visible`,
|
||||
`created`,
|
||||
@@ -4358,6 +4737,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$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['internal'] = $row['internal'];
|
||||
$aliasdata['active'] = $row['active'];
|
||||
$aliasdata['active_int'] = $row['active'];
|
||||
$aliasdata['sogo_visible'] = $row['sogo_visible'];
|
||||
@@ -4666,6 +5046,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`mailbox`.`quota`,
|
||||
`mailbox`.`created`,
|
||||
`mailbox`.`modified`,
|
||||
`mailbox`.`authsource`,
|
||||
`quota2`.`bytes`,
|
||||
`attributes`,
|
||||
`custom_attributes`,
|
||||
@@ -4687,6 +5068,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`mailbox`.`quota`,
|
||||
`mailbox`.`created`,
|
||||
`mailbox`.`modified`,
|
||||
`mailbox`.`authsource`,
|
||||
`quota2replica`.`bytes`,
|
||||
`attributes`,
|
||||
`custom_attributes`,
|
||||
@@ -4716,6 +5098,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
|
||||
$mailboxdata['created'] = $row['created'];
|
||||
$mailboxdata['modified'] = $row['modified'];
|
||||
$mailboxdata['authsource'] = ($row['authsource']) ? $row['authsource'] : 'mailcow';
|
||||
|
||||
if ($mailboxdata['percent_in_use'] === '- ') {
|
||||
$mailboxdata['percent_class'] = "info";
|
||||
@@ -4746,7 +5129,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
else if ($SaslLogs['service'] == 'pop3') {
|
||||
$last_pop3_login = strtotime($SaslLogs['datetime']);
|
||||
}
|
||||
else if ($SaslLogs['service'] == 'SSO') {
|
||||
else if ($SaslLogs['service'] == 'SSO') {
|
||||
$last_sso_login = strtotime($SaslLogs['datetime']);
|
||||
}
|
||||
}
|
||||
@@ -4759,7 +5142,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
if (!isset($last_pop3_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
|
||||
$last_pop3_login = 0;
|
||||
}
|
||||
if (!isset($last_sso_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
|
||||
if (!isset($last_sso_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
|
||||
$last_sso_login = 0;
|
||||
}
|
||||
$mailboxdata['last_imap_login'] = $last_imap_login;
|
||||
@@ -4811,7 +5194,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
return $mailboxdata;
|
||||
break;
|
||||
case 'mailbox_templates':
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin" && $_SESSION['access_all_exception'] != "1") {
|
||||
return false;
|
||||
}
|
||||
$_data = (isset($_data)) ? intval($_data) : null;
|
||||
@@ -4847,6 +5230,20 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
return $rows;
|
||||
}
|
||||
break;
|
||||
case 'mta_sts':
|
||||
$stmt = $pdo->prepare("SELECT * FROM `mta_sts` WHERE `domain` = :domain");
|
||||
$stmt->execute(array(
|
||||
':domain' => $_data,
|
||||
));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($row)){
|
||||
return [];
|
||||
}
|
||||
$row['mx'] = explode(',', $row['mx']);
|
||||
$row['version'] = strtoupper(substr($row['version'], 0, 3)) . substr($row['version'], 3);
|
||||
|
||||
return $row;
|
||||
break;
|
||||
case 'resource_details':
|
||||
$resourcedata = array();
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
|
||||
@@ -5232,6 +5629,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$stmt->execute(array(
|
||||
':domain' => $domain,
|
||||
));
|
||||
$stmt = $pdo->prepare("DELETE FROM `mta_sts` WHERE `domain` = :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 {
|
||||
@@ -5565,7 +5966,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
update_sogo_static_view($username);
|
||||
try {
|
||||
update_sogo_static_view($username);
|
||||
}catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => $e->getMessage()
|
||||
);
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
@@ -5779,6 +6188,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
break;
|
||||
}
|
||||
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'resource')) && getenv('SKIP_SOGO') != "y") {
|
||||
update_sogo_static_view();
|
||||
try {
|
||||
update_sogo_static_view();
|
||||
}catch (PDOException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => $e->getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ function quarantine($_action, $_data = null) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
|
||||
WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash
|
||||
WHERE `qhash` = :hash
|
||||
AND user_acl.quarantine = 1
|
||||
AND rcpt IN (SELECT username FROM mailbox)');
|
||||
$stmt->execute(array(':hash' => $hash));
|
||||
@@ -65,7 +65,7 @@ function quarantine($_action, $_data = null) {
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
|
||||
WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash
|
||||
WHERE `qhash` = :hash
|
||||
AND `user_acl`.`quarantine` = 1
|
||||
AND `username` IN (SELECT `username` FROM `mailbox`)');
|
||||
$stmt->execute(array(':hash' => $hash));
|
||||
@@ -169,7 +169,7 @@ function quarantine($_action, $_data = null) {
|
||||
}
|
||||
}
|
||||
elseif ($release_format == 'raw') {
|
||||
$detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/', 'X-Pre-Release-Spam-Flag $1', $detail_row['msg']);
|
||||
$detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $detail_row['msg']);
|
||||
$postfix_talk = array(
|
||||
array('220', 'HELO quarantine' . chr(10)),
|
||||
array('250', 'MAIL FROM: ' . $sender . chr(10)),
|
||||
@@ -464,7 +464,7 @@ function quarantine($_action, $_data = null) {
|
||||
}
|
||||
}
|
||||
elseif ($release_format == 'raw') {
|
||||
$row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/', 'X-Pre-Release-Spam-Flag $1', $row['msg']);
|
||||
$row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $row['msg']);
|
||||
$postfix_talk = array(
|
||||
array('220', 'HELO quarantine' . chr(10)),
|
||||
array('250', 'MAIL FROM: ' . $sender . chr(10)),
|
||||
@@ -833,7 +833,7 @@ function quarantine($_action, $_data = null) {
|
||||
)));
|
||||
return false;
|
||||
}
|
||||
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash');
|
||||
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash');
|
||||
$stmt->execute(array(':hash' => $hash));
|
||||
return $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
break;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php
|
||||
function ratelimit($_action, $_scope, $_data = null) {
|
||||
function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
|
||||
global $valkey;
|
||||
$_data_log = $_data;
|
||||
switch ($_action) {
|
||||
case 'edit':
|
||||
if (!isset($_SESSION['acl']['ratelimit']) || $_SESSION['acl']['ratelimit'] != "1" ) {
|
||||
if (!hasACLAccess("ratelimit")) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
@@ -93,7 +93,7 @@ function ratelimit($_action, $_scope, $_data = null) {
|
||||
continue;
|
||||
}
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $object)
|
||||
|| ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin')) {
|
||||
|| ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin' && $_SESSION['access_all_exception'] != '1')) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
|
||||
@@ -30,6 +30,44 @@ if(!file_exists($CSSPath)) {
|
||||
cleanupCSS($hash);
|
||||
}
|
||||
|
||||
$mailcow_apps_processed = $MAILCOW_APPS;
|
||||
$app_links = customize('get', 'app_links');
|
||||
$app_links_processed = $app_links;
|
||||
$hide_mailcow_apps = true;
|
||||
for ($i = 0; $i < count($mailcow_apps_processed); $i++) {
|
||||
if ($hide_mailcow_apps && !$mailcow_apps_processed[$i]['hide']){
|
||||
$hide_mailcow_apps = false;
|
||||
}
|
||||
if (!empty($_SESSION['mailcow_cc_username'])){
|
||||
if ($app_links_processed[$i]['user_link']) {
|
||||
$mailcow_apps_processed[$i]['user_link'] = str_replace('%u', $_SESSION['mailcow_cc_username'], $mailcow_apps_processed[$i]['user_link']);
|
||||
} else {
|
||||
$mailcow_apps_processed[$i]['user_link'] = $mailcow_apps_processed[$i]['link'];
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($app_links_processed){
|
||||
for ($i = 0; $i < count($app_links_processed); $i++) {
|
||||
$key = array_key_first($app_links_processed[$i]);
|
||||
if ($hide_mailcow_apps && !$app_links_processed[$i][$key]['hide']){
|
||||
$hide_mailcow_apps = false;
|
||||
}
|
||||
if (!empty($_SESSION['mailcow_cc_username'])){
|
||||
if ($app_links_processed[$i][$key]['user_link']) {
|
||||
$app_links_processed[$i][$key]['user_link'] = str_replace('%u', $_SESSION['mailcow_cc_username'], $app_links_processed[$i][$key]['user_link']);
|
||||
} else {
|
||||
$app_links_processed[$i][$key]['user_link'] = $app_links_processed[$i][$key]['link'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround to get text with <br> straight to twig.
|
||||
// Using "nl2br" doesn't work with Twig as it would escape everything by default.
|
||||
if (isset($UI_TEXTS["ui_footer"])) {
|
||||
$UI_TEXTS["ui_footer"] = nl2br($UI_TEXTS["ui_footer"]);
|
||||
}
|
||||
|
||||
$globalVariables = [
|
||||
'mailcow_hostname' => getenv('MAILCOW_HOSTNAME'),
|
||||
'mailcow_locale' => @$_SESSION['mailcow_locale'],
|
||||
@@ -45,8 +83,11 @@ $globalVariables = [
|
||||
'lang' => $lang,
|
||||
'skip_sogo' => (getenv('SKIP_SOGO') == 'y'),
|
||||
'allow_admin_email_login' => (getenv('ALLOW_ADMIN_EMAIL_LOGIN') == 'n'),
|
||||
'hide_mailcow_apps' => $hide_mailcow_apps,
|
||||
'mailcow_apps' => $MAILCOW_APPS,
|
||||
'app_links' => customize('get', 'app_links'),
|
||||
'mailcow_apps_processed' => $mailcow_apps_processed,
|
||||
'app_links' => $app_links,
|
||||
'app_links_processed' => $app_links_processed,
|
||||
'is_root_uri' => (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) == '/'),
|
||||
'uri' => $_SERVER['REQUEST_URI'],
|
||||
];
|
||||
|
||||
@@ -4,7 +4,7 @@ function init_db_schema()
|
||||
try {
|
||||
global $pdo;
|
||||
|
||||
$db_version = "20112024_1105";
|
||||
$db_version = "07102025_1015";
|
||||
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||
@@ -184,6 +184,7 @@ function init_db_schema()
|
||||
"private_comment" => "TEXT",
|
||||
"public_comment" => "TEXT",
|
||||
"sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"internal" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||
),
|
||||
"keys" => array(
|
||||
@@ -345,10 +346,14 @@ function init_db_schema()
|
||||
"notified" => "TINYINT(1) NOT NULL DEFAULT '0'",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"user" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'",
|
||||
"qhash" => "VARCHAR(64)",
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("id")
|
||||
),
|
||||
"key" => array(
|
||||
"qhash" => array("qhash")
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
@@ -368,6 +373,7 @@ function init_db_schema()
|
||||
"custom_attributes" => "JSON NOT NULL DEFAULT ('{}')",
|
||||
"kind" => "VARCHAR(100) NOT NULL DEFAULT ''",
|
||||
"multiple_bookings" => "INT NOT NULL DEFAULT -1",
|
||||
"authsource" => "ENUM('mailcow', 'keycloak', 'generic-oidc', 'ldap') DEFAULT 'mailcow'",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP",
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||
@@ -470,6 +476,23 @@ function init_db_schema()
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"mta_sts" => array(
|
||||
"cols" => array(
|
||||
"id" => "BIGINT NOT NULL",
|
||||
"domain" => "VARCHAR(255) NOT NULL",
|
||||
"version" => "VARCHAR(255) NOT NULL",
|
||||
"mode" => "VARCHAR(255) NOT NULL",
|
||||
"mx" => "VARCHAR(255) NOT NULL",
|
||||
"max_age" => "VARCHAR(255) NOT NULL",
|
||||
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("domain")
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"user_acl" => array(
|
||||
"cols" => array(
|
||||
"username" => "VARCHAR(255) NOT NULL",
|
||||
@@ -575,6 +598,20 @@ function init_db_schema()
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"identity_provider" => array(
|
||||
"cols" => array(
|
||||
"key" => "VARCHAR(255) NOT NULL",
|
||||
"value" => "TEXT NOT NULL",
|
||||
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
|
||||
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
"" => array("key")
|
||||
)
|
||||
),
|
||||
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
|
||||
),
|
||||
"logs" => array(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
@@ -1300,6 +1337,14 @@ function init_db_schema()
|
||||
$pdo->query($create);
|
||||
}
|
||||
|
||||
// Clear old app_passwd log entries
|
||||
$pdo->exec("DELETE FROM logs
|
||||
WHERE role != 'unauthenticated'
|
||||
AND JSON_EXTRACT(`call`, '$[0]') = 'app_passwd'
|
||||
AND JSON_EXTRACT(`call`, '$[1]') = 'edit'
|
||||
AND (JSON_CONTAINS_PATH(`call`, 'one', '$[2].password')
|
||||
OR JSON_CONTAINS_PATH(`call`, 'one', '$[2].password2'));");
|
||||
|
||||
// Mitigate imapsync argument injection issue
|
||||
$pdo->query("UPDATE `imapsync` SET `custom_params` = ''
|
||||
WHERE `custom_params` LIKE '%pipemess%'
|
||||
@@ -1455,6 +1500,9 @@ function init_db_schema()
|
||||
));
|
||||
}
|
||||
|
||||
// remove old sogo views and triggers
|
||||
$pdo->query("DROP TRIGGER IF EXISTS sogo_update_password");
|
||||
|
||||
if (php_sapi_name() == "cli") {
|
||||
echo "DB initialization completed" . PHP_EOL;
|
||||
} else {
|
||||
@@ -1464,6 +1512,10 @@ function init_db_schema()
|
||||
'msg' => 'db_init_complete'
|
||||
);
|
||||
}
|
||||
|
||||
// fill quarantine.qhash
|
||||
$pdo->query("UPDATE `quarantine` SET `qhash` = SHA2(CONCAT(`id`, `qid`), 256) WHERE ISNULL(`qhash`)");
|
||||
|
||||
} catch (PDOException $e) {
|
||||
if (php_sapi_name() == "cli") {
|
||||
echo "DB initialization failed: " . print_r($e, true) . PHP_EOL;
|
||||
@@ -1478,6 +1530,7 @@ function init_db_schema()
|
||||
}
|
||||
if (php_sapi_name() == "cli") {
|
||||
include '/web/inc/vars.inc.php';
|
||||
include '/web/inc/functions.inc.php';
|
||||
include '/web/inc/functions.docker.inc.php';
|
||||
// $now = new DateTime();
|
||||
// $mins = $now->getOffset() / 60;
|
||||
@@ -1499,9 +1552,7 @@ if (php_sapi_name() == "cli") {
|
||||
if (intval($res['OK_C']) === 2) {
|
||||
// Be more precise when replacing into _sogo_static_view, col orders may change
|
||||
try {
|
||||
$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');");
|
||||
update_sogo_static_view();
|
||||
echo "Fixed _sogo_static_view" . PHP_EOL;
|
||||
} catch (Exception $e) {
|
||||
// Dunno
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"require": {
|
||||
"robthree/twofactorauth": "^1.6",
|
||||
"yubico/u2flib-server": "^1.0",
|
||||
"phpmailer/phpmailer": "^6.1",
|
||||
"php-mime-mail-parser/php-mime-mail-parser": "^7",
|
||||
"soundasleep/html2text": "^0.5.0",
|
||||
@@ -9,7 +8,9 @@
|
||||
"matthiasmullie/minify": "^1.3",
|
||||
"bshaffer/oauth2-server-php": "^1.11",
|
||||
"mustangostang/spyc": "^0.6.3",
|
||||
"directorytree/ldaprecord": "^2.4",
|
||||
"twig/twig": "^3.0"
|
||||
"directorytree/ldaprecord": "^3.3",
|
||||
"twig/twig": "^3.0",
|
||||
"stevenmaguire/oauth2-keycloak": "^4.0",
|
||||
"league/oauth2-client": "^2.7"
|
||||
}
|
||||
}
|
||||
|
||||
1097
data/web/inc/lib/composer.lock
generated
1097
data/web/inc/lib/composer.lock
generated
File diff suppressed because it is too large
Load Diff
46
data/web/inc/lib/vendor/bin/carbon
vendored
46
data/web/inc/lib/vendor/bin/carbon
vendored
@@ -12,6 +12,7 @@
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
@@ -23,18 +24,17 @@ if (PHP_VERSION_ID < 80000) {
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of composer-bin-proxy:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 21);
|
||||
$opened_path = realpath($opened_path) ?: $opened_path;
|
||||
$this->handle = fopen($opened_path, $mode);
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
// remove all traces of this stream wrapper once it has been used
|
||||
stream_wrapper_unregister('composer-bin-proxy');
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,16 @@ if (PHP_VERSION_ID < 80000) {
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
@@ -78,20 +88,32 @@ if (PHP_VERSION_ID < 80000) {
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return fstat($this->handle);
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('stream_wrapper_register') && stream_wrapper_register('composer-bin-proxy', 'Composer\BinProxyWrapper')) {
|
||||
include("composer-bin-proxy://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon');
|
||||
exit(0);
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/nesbot/carbon/bin/carbon');
|
||||
}
|
||||
}
|
||||
|
||||
include __DIR__ . '/..'.'/nesbot/carbon/bin/carbon';
|
||||
return include __DIR__ . '/..'.'/nesbot/carbon/bin/carbon';
|
||||
|
||||
46
data/web/inc/lib/vendor/bin/var-dump-server
vendored
46
data/web/inc/lib/vendor/bin/var-dump-server
vendored
@@ -12,6 +12,7 @@
|
||||
|
||||
namespace Composer;
|
||||
|
||||
$GLOBALS['_composer_bin_dir'] = __DIR__;
|
||||
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
|
||||
|
||||
if (PHP_VERSION_ID < 80000) {
|
||||
@@ -23,18 +24,17 @@ if (PHP_VERSION_ID < 80000) {
|
||||
{
|
||||
private $handle;
|
||||
private $position;
|
||||
private $realpath;
|
||||
|
||||
public function stream_open($path, $mode, $options, &$opened_path)
|
||||
{
|
||||
// get rid of composer-bin-proxy:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 21);
|
||||
$opened_path = realpath($opened_path) ?: $opened_path;
|
||||
$this->handle = fopen($opened_path, $mode);
|
||||
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
|
||||
$opened_path = substr($path, 17);
|
||||
$this->realpath = realpath($opened_path) ?: $opened_path;
|
||||
$opened_path = $this->realpath;
|
||||
$this->handle = fopen($this->realpath, $mode);
|
||||
$this->position = 0;
|
||||
|
||||
// remove all traces of this stream wrapper once it has been used
|
||||
stream_wrapper_unregister('composer-bin-proxy');
|
||||
|
||||
return (bool) $this->handle;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,16 @@ if (PHP_VERSION_ID < 80000) {
|
||||
return $operation ? flock($this->handle, $operation) : true;
|
||||
}
|
||||
|
||||
public function stream_seek($offset, $whence)
|
||||
{
|
||||
if (0 === fseek($this->handle, $offset, $whence)) {
|
||||
$this->position = ftell($this->handle);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function stream_tell()
|
||||
{
|
||||
return $this->position;
|
||||
@@ -78,20 +88,32 @@ if (PHP_VERSION_ID < 80000) {
|
||||
|
||||
public function stream_stat()
|
||||
{
|
||||
return fstat($this->handle);
|
||||
return array();
|
||||
}
|
||||
|
||||
public function stream_set_option($option, $arg1, $arg2)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function url_stat($path, $flags)
|
||||
{
|
||||
$path = substr($path, 17);
|
||||
if (file_exists($path)) {
|
||||
return stat($path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (function_exists('stream_wrapper_register') && stream_wrapper_register('composer-bin-proxy', 'Composer\BinProxyWrapper')) {
|
||||
include("composer-bin-proxy://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
|
||||
exit(0);
|
||||
if (
|
||||
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|
||||
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
|
||||
) {
|
||||
return include("phpvfscomposer://" . __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server');
|
||||
}
|
||||
}
|
||||
|
||||
include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';
|
||||
return include __DIR__ . '/..'.'/symfony/var-dumper/Resources/bin/var-dump-server';
|
||||
|
||||
21
data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/LICENSE
vendored
Normal file
21
data/web/inc/lib/vendor/carbonphp/carbon-doctrine-types/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Carbon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user