Compare commits

..

7 Commits

Author SHA1 Message Date
DerLinkman
804582ebc4 declare better sieve script paths per user (for after and before) 2025-08-28 12:46:53 +02:00
DerLinkman
fa952004e8 dovecot: fix smaller config changes 2025-08-28 10:26:56 +02:00
DerLinkman
dd17e8c0b6 dovecot: rechanged id type from uuid to string 2025-08-28 10:26:56 +02:00
DerLinkman
73f0c61a0e dovecot: change dict declarations for before scripts 2025-08-28 10:26:56 +02:00
DerLinkman
b3e8697c4b dovecot: changes to config aligning to 2.4 2025-08-28 10:26:55 +02:00
DerLinkman
a77e699c53 dovecot: migrated config to 2.4 + config splitting 2025-08-28 10:26:55 +02:00
DerLinkman
630e58e226 indev: dovecot 2.4 config migration 2025-08-28 10:26:54 +02:00
164 changed files with 2044 additions and 3056 deletions

View File

@@ -11,35 +11,22 @@ body:
required: true
- type: checkboxes
attributes:
label: Checklist prior issue creation
description: Prior to creating the issue...
label: I've found a bug and checked that ...
description: Prior to placing the issue, please check following:** *(fill out each checkbox with an `X` once done)*
options:
- label: I understand that failure to follow below instructions may cause this issue to be closed.
- label: ... I understand that not following the below instructions will result in immediate closure and/or deletion of my issue.
required: true
- label: I understand that vague, incomplete or inaccurate information may cause this issue to be closed.
- label: ... I have understood that this bug report is dedicated for bugs, and not for support-related inquiries.
required: true
- label: I understand that this form is intended solely for reporting software bugs and not for support-related inquiries.
- label: ... I have understood that answers are voluntary and community-driven, and not commercial support.
required: true
- label: I understand that all responses are voluntary and community-driven, and do not constitute commercial support.
required: true
- label: I confirm that I have reviewed previous [issues](https://github.com/mailcow/mailcow-dockerized/issues) to ensure this matter has not already been addressed.
required: true
- label: I confirm that my environment meets all [prerequisite requirements](https://docs.mailcow.email/getstarted/prerequisite-system/) as specified in the official documentation.
- label: ... I have verified that my issue has not been already answered in the past. I also checked previous [issues](https://github.com/mailcow/mailcow-dockerized/issues).
required: true
- type: textarea
attributes:
label: Description
description: Please provide a brief description of the bug. If applicable, add screenshots to help explain your problem. (Very useful for bugs in mailcow UI.)
validations:
required: true
- type: textarea
attributes:
label: "Steps to reproduce:"
description: "Please describe the steps to reproduce the bug. Screenshots can be added, if helpful."
placeholder: |-
1. ...
2. ...
3. ...
description: Please provide a brief description of the bug in 1-2 sentences. If applicable, add screenshots to help explain your problem. Very useful for bugs in mailcow UI.
render: plain text
validations:
required: true
- type: textarea
@@ -49,36 +36,45 @@ body:
render: plain text
validations:
required: true
- type: textarea
attributes:
label: "Steps to reproduce:"
description: "Please describe the steps to reproduce the bug. Screenshots can be added, if helpful."
render: plain text
placeholder: |-
1. ...
2. ...
3. ...
validations:
required: true
- type: markdown
attributes:
value: |
## System information
In this stage we would kindly ask you to attach general system information about your setup.
### In this stage we would kindly ask you to attach general system information about your setup.
- type: dropdown
attributes:
label: "Which branch are you using?"
description: "#### Run: `git rev-parse --abbrev-ref HEAD`"
description: "#### `git rev-parse --abbrev-ref HEAD`"
multiple: false
options:
- master (stable)
- staging
- master
- nightly
validations:
required: true
- type: dropdown
attributes:
label: "Which architecture are you using?"
description: "#### Run: `uname -m`"
description: "#### `uname -m`"
multiple: false
options:
- x86_64
- x86
- ARM64 (aarch64)
validations:
required: true
- type: input
attributes:
label: "Operating System:"
description: "#### Run: `lsb_release -ds`"
placeholder: "e.g. Ubuntu 22.04 LTS"
validations:
required: true
@@ -97,44 +93,43 @@ body:
- type: input
attributes:
label: "Virtualization technology:"
description: "LXC and OpenVZ are not supported!"
placeholder: "KVM, VMware ESXi, Xen, etc"
placeholder: "KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported**"
validations:
required: true
- type: input
attributes:
label: "Docker version:"
description: "#### Run: `docker version`"
description: "#### `docker version`"
placeholder: "20.10.21"
validations:
required: true
- type: input
attributes:
label: "docker-compose version or docker compose version:"
description: "#### Run: `docker-compose version` or `docker compose version`"
description: "#### `docker-compose version` or `docker compose version`"
placeholder: "v2.12.2"
validations:
required: true
- type: input
attributes:
label: "mailcow version:"
description: "#### Run: ```git describe --tags `git rev-list --tags --max-count=1` ```"
placeholder: "2022-08x"
description: "#### ```git describe --tags `git rev-list --tags --max-count=1` ```"
placeholder: "2022-08"
validations:
required: true
- type: input
attributes:
label: "Reverse proxy:"
placeholder: "e.g. nginx/Traefik, or none"
placeholder: "e.g. Nginx/Traefik"
validations:
required: true
- type: textarea
attributes:
label: "Logs of git diff:"
description: "#### Output of `git diff origin/master`, any other changes to the code? Sanitize if needed. If so, **please post them**:"
description: "#### Output of `git diff origin/master`, any other changes to the code? If so, **please post them**:"
render: plain text
validations:
required: false
required: true
- type: textarea
attributes:
label: "Logs of iptables -L -vn:"

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write
steps:
- name: Mark/Close Stale Issues and Pull Requests 🗑️
uses: actions/stale@v10.1.0
uses: actions/stale@v9.1.0
with:
repo-token: ${{ secrets.STALE_ACTION_PAT }}
days-before-stale: 60

View File

@@ -27,7 +27,7 @@ jobs:
- "watchdog-mailcow"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Setup Docker
run: |
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run the Action

View File

@@ -13,7 +13,7 @@ jobs:
packages: write
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Generate postscreen_access.cidr
run: |

View File

@@ -23,6 +23,9 @@ A big thank you to everyone supporting us on GitHub Sponsors—your contribution
<a href="https://www.maehdros.com/" target=_blank><img
src="https://avatars.githubusercontent.com/u/173894712" height="58"
/></a>
<a href="https://macarne.com/" target=_blank><img
src="https://avatars.githubusercontent.com/u/149550368?s=200&v=4" height="58"
/></a>
### 50$/Month Sponsors
<a href="https://github.com/vnukhr" target=_blank><img

View File

@@ -17,13 +17,7 @@ caller="${BASH_SOURCE[1]##*/}"
get_installed_tools(){
for bin in openssl curl docker git awk sha1sum grep cut jq; do
if [[ -z $(command -v ${bin}) ]]; then
echo "Error: Cannot find command '${bin}'. Cannot proceed."
echo "Solution: Please review system requirements and install requirements. Then, re-run the script."
echo "See System Requirements: https://docs.mailcow.email/getstarted/install/"
echo "Exiting..."
exit 1
fi
if [[ -z $(command -v ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
done
if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo -e "${LIGHT_RED}BusyBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\"${NC}"; exit 1; fi
@@ -191,7 +185,7 @@ detect_major_update() {
MAJOR_VERSIONS=(
"2025-02"
"2025-03"
"2025-09"
"2025-08"
)
current_version=""
@@ -227,4 +221,4 @@ detect_major_update() {
fi
fi
fi
}
}

View File

@@ -5,65 +5,14 @@
# 1) Check if the host supports IPv6
get_ipv6_support() {
# ---- helper: probe external IPv6 connectivity without DNS ----
_probe_ipv6_connectivity() {
# Use literal, always-on IPv6 echo responders (no DNS required)
local PROBE_IPS=("2001:4860:4860::8888" "2606:4700:4700::1111")
local ip rc=1
for ip in "${PROBE_IPS[@]}"; do
if command -v ping6 &>/dev/null; then
ping6 -c1 -W2 "$ip" &>/dev/null || ping6 -c1 -w2 "$ip" &>/dev/null
rc=$?
elif command -v ping &>/dev/null; then
ping -6 -c1 -W2 "$ip" &>/dev/null || ping -6 -c1 -w2 "$ip" &>/dev/null
rc=$?
else
rc=1
fi
[[ $rc -eq 0 ]] && return 0
done
return 1
}
if [[ ! -f /proc/net/if_inet6 ]] || grep -qs '^1' /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null; then
if grep -qs '^1' /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null \
|| ! ip -6 route show default &>/dev/null; then
DETECTED_IPV6=false
echo -e "${YELLOW}IPv6 not detected on host ${LIGHT_RED}IPv6 is administratively disabled${YELLOW}.${NC}"
return
echo -e "${YELLOW}IPv6 not detected on host ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
else
DETECTED_IPV6=true
echo -e "IPv6 detected on host ${LIGHT_GREEN}leaving IPv6 support enabled${YELLOW}.${NC}"
fi
if ip -6 route show default 2>/dev/null | grep -qE '^default'; then
echo -e "${YELLOW}Default IPv6 route found testing external IPv6 connectivity...${NC}"
if _probe_ipv6_connectivity; then
DETECTED_IPV6=true
echo -e "IPv6 detected on host ${LIGHT_GREEN}leaving IPv6 support enabled${YELLOW}.${NC}"
else
DETECTED_IPV6=false
echo -e "${YELLOW}Default IPv6 route present but external IPv6 connectivity failed ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
fi
return
fi
if ip -6 addr show scope global 2>/dev/null | grep -q 'inet6'; then
DETECTED_IPV6=false
echo -e "${YELLOW}Global IPv6 address present but no default route ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
return
fi
if ip -6 addr show scope link 2>/dev/null | grep -q 'inet6'; then
echo -e "${YELLOW}Only link-local IPv6 addresses found testing external IPv6 connectivity...${NC}"
if _probe_ipv6_connectivity; then
DETECTED_IPV6=true
echo -e "External IPv6 connectivity available ${LIGHT_GREEN}leaving IPv6 support enabled${YELLOW}.${NC}"
else
DETECTED_IPV6=false
echo -e "${YELLOW}Only link-local IPv6 present and no external connectivity ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
fi
return
fi
DETECTED_IPV6=false
echo -e "${YELLOW}IPv6 not detected on host ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
}
# 2) Ensure Docker daemon.json has (or create) the required IPv6 settings
@@ -72,7 +21,7 @@ docker_daemon_edit(){
DOCKER_MAJOR=$(docker version --format '{{.Server.Version}}' 2>/dev/null | cut -d. -f1)
MISSING=()
_has_kv() { grep -Eq "\"$1\"[[:space:]]*:[[:space:]]*$2" "$DOCKER_DAEMON_CONFIG" 2>/dev/null; }
_has_kv() { grep -Eq "\"$1\"\s*:\s*$2" "$DOCKER_DAEMON_CONFIG" 2>/dev/null; }
if [[ -f "$DOCKER_DAEMON_CONFIG" ]]; then
@@ -89,18 +38,12 @@ docker_daemon_edit(){
fi
# Gather missing keys
! _has_kv ipv6 true && MISSING+=("ipv6: true")
# For Docker < 28, keep requiring fixed-cidr-v6 (default bridge needs it on old engines)
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 28 ]]; then
! grep -Eq '"fixed-cidr-v6"[[:space:]]*:[[:space:]]*".+"' "$DOCKER_DAEMON_CONFIG" \
&& MISSING+=('fixed-cidr-v6: "fd00:dead:beef:c0::/80"')
fi
# For Docker < 27, ip6tables needed and was tied to experimental in older releases
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then
! _has_kv ipv6 true && MISSING+=("ipv6: true")
! grep -Eq '"fixed-cidr-v6"\s*:\s*".+"' "$DOCKER_DAEMON_CONFIG" \
&& MISSING+=('fixed-cidr-v6: "fd00:dead:beef:c0::/80"')
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -le 27 ]]; then
_has_kv ipv6 true && ! _has_kv ip6tables true && MISSING+=("ip6tables: true")
! _has_kv experimental true && MISSING+=("experimental: true")
! _has_kv experimental true && MISSING+=("experimental: true")
fi
# Fix if needed
@@ -117,19 +60,9 @@ docker_daemon_edit(){
cp "$DOCKER_DAEMON_CONFIG" "${DOCKER_DAEMON_CONFIG}.bak"
if command -v jq &>/dev/null; then
TMP=$(mktemp)
# Base filter: ensure ipv6 = true
JQ_FILTER='.ipv6 = true'
# Add fixed-cidr-v6 only for Docker < 28
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 28 ]]; then
JQ_FILTER+=' | .["fixed-cidr-v6"] = (.["fixed-cidr-v6"] // "fd00:dead:beef:c0::/80")'
fi
# Add ip6tables/experimental only for Docker < 27
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then
JQ_FILTER+=' | .ip6tables = true | .experimental = true'
fi
JQ_FILTER='.ipv6 = true | .["fixed-cidr-v6"] = "fd00:dead:beef:c0::/80"'
[[ "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]] \
&& JQ_FILTER+=' | .ip6tables = true | .experimental = true'
jq "$JQ_FILTER" "$DOCKER_DAEMON_CONFIG" >"$TMP" && mv "$TMP" "$DOCKER_DAEMON_CONFIG"
echo -e "${LIGHT_GREEN}daemon.json updated. Restarting Docker...${NC}"
(command -v systemctl &>/dev/null && systemctl restart docker) || service docker restart
@@ -155,7 +88,6 @@ docker_daemon_edit(){
fi
if [[ $ans =~ ^[Yy]$ ]]; then
mkdir -p "$(dirname "$DOCKER_DAEMON_CONFIG")"
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then
cat > "$DOCKER_DAEMON_CONFIG" <<EOF
{
@@ -165,19 +97,12 @@ docker_daemon_edit(){
"experimental": true
}
EOF
elif [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 28 ]]; then
else
cat > "$DOCKER_DAEMON_CONFIG" <<EOF
{
"ipv6": true,
"fixed-cidr-v6": "fd00:dead:beef:c0::/80"
}
EOF
else
# Docker 28+: ipv6 works without fixed-cidr-v6
cat > "$DOCKER_DAEMON_CONFIG" <<EOF
{
"ipv6": true
}
EOF
fi
echo -e "${GREEN}Created $DOCKER_DAEMON_CONFIG with IPv6 settings.${NC}"
@@ -197,7 +122,7 @@ configure_ipv6() {
# detect manual override if mailcow.conf is present
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]] && grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
MANUAL_SETTING=$(grep '^ENABLE_IPV6=' "$MAILCOW_CONF" | cut -d= -f2)
elif [[ -z "$MAILCOW_CONF" ]] && [[ -n "${ENABLE_IPV6:-}" ]]; then
elif [[ -z "$MAILCOW_CONF" ]] && [[ ! -z "${ENABLE_IPV6:-}" ]]; then
MANUAL_SETTING="$ENABLE_IPV6"
else
MANUAL_SETTING=""
@@ -206,34 +131,38 @@ configure_ipv6() {
get_ipv6_support
# if user manually set it, check for mismatch
if [[ "$DETECTED_IPV6" != "true" ]]; then
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]]; then
if grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
sed -i 's/^ENABLE_IPV6=.*/ENABLE_IPV6=false/' "$MAILCOW_CONF"
else
echo "ENABLE_IPV6=false" >> "$MAILCOW_CONF"
fi
if [[ -n "$MANUAL_SETTING" ]]; then
if [[ "$MANUAL_SETTING" == "false" && "$DETECTED_IPV6" == "true" ]]; then
echo -e "${RED}ERROR: You have ENABLE_IPV6=false but your host and Docker support IPv6.${NC}"
echo -e "${RED}This can create an open relay. Please set ENABLE_IPV6=true in your mailcow.conf and re-run.${NC}"
exit 1
elif [[ "$MANUAL_SETTING" == "true" && "$DETECTED_IPV6" == "false" ]]; then
echo -e "${RED}ERROR: You have ENABLE_IPV6=true but your host does not support IPv6.${NC}"
echo -e "${RED}Please disable or fix your host/Docker IPv6 support, or set ENABLE_IPV6=false.${NC}"
exit 1
else
export IPV6_BOOL=false
return
fi
echo "Skipping Docker IPv6 configuration because host does not support IPv6."
echo "Make sure to check if your docker daemon.json does not include \"enable_ipv6\": true if you do not want IPv6."
echo "IPv6 configuration complete: ENABLE_IPV6=false"
sleep 2
return
fi
docker_daemon_edit
# no manual override: proceed to set or export
if [[ "$DETECTED_IPV6" == "true" ]]; then
docker_daemon_edit
else
echo "Skipping Docker IPv6 configuration because host does not support IPv6."
fi
# now write into mailcow.conf or export
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]]; then
LINE="ENABLE_IPV6=$DETECTED_IPV6"
if grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
sed -i 's/^ENABLE_IPV6=.*/ENABLE_IPV6=true/' "$MAILCOW_CONF"
sed -i "s/^ENABLE_IPV6=.*/$LINE/" "$MAILCOW_CONF"
else
echo "ENABLE_IPV6=true" >> "$MAILCOW_CONF"
echo "$LINE" >> "$MAILCOW_CONF"
fi
else
export IPV6_BOOL=true
export IPV6_BOOL="$DETECTED_IPV6"
fi
echo "IPv6 configuration complete: ENABLE_IPV6=true"
echo "IPv6 configuration complete: ENABLE_IPV6=$DETECTED_IPV6"
}

View File

@@ -43,7 +43,6 @@ adapt_new_options() {
"ALLOW_ADMIN_EMAIL_LOGIN"
"SKIP_HTTP_VERIFICATION"
"SOGO_EXPIRE_SESSION"
"SOGO_URL_ENCRYPTION_KEY"
"REDIS_PORT"
"REDISPASS"
"DOVECOT_MASTER_USER"
@@ -95,6 +94,7 @@ adapt_new_options() {
echo '# Max log lines per service to keep in Redis logs' >> mailcow.conf
echo "LOG_LINES=9999" >> mailcow.conf
;;
IPV4_NETWORK)
echo '# Internal IPv4 /24 subnet, format n.n.n. (expands to n.n.n.0/24)' >> mailcow.conf
echo "IPV4_NETWORK=172.22.1" >> mailcow.conf
@@ -276,22 +276,21 @@ adapt_new_options() {
echo '# A COMPLETE DOCKER STACK REBUILD (compose down && compose up -d) IS NEEDED TO APPLY THIS.' >> mailcow.conf
echo ENABLE_IPV6=${IPV6_BOOL} >> mailcow.conf
;;
SKIP_CLAMD)
echo '# Skip ClamAV (clamd-mailcow) anti-virus (Rspamd will auto-detect a missing ClamAV container) - y/n' >> mailcow.conf
echo 'SKIP_CLAMD=n' >> mailcow.conf
;;
SKIP_OLEFY)
echo '# Skip Olefy (olefy-mailcow) anti-virus for Office documents (Rspamd will auto-detect a missing Olefy container) - y/n' >> mailcow.conf
echo 'SKIP_OLEFY=n' >> mailcow.conf
;;
REDISPASS)
echo "REDISPASS=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 28)" >> mailcow.conf
;;
SOGO_URL_ENCRYPTION_KEY)
echo '# SOGo URL encryption key (exactly 16 characters, limited to AZ, az, 09)' >> mailcow.conf
echo '# This key is used to encrypt email addresses within SOGo URLs' >> mailcow.conf
echo "SOGO_URL_ENCRYPTION_KEY=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 16)" >> mailcow.conf
;;
*)
echo "${option}=" >> mailcow.conf
;;

View File

@@ -3,14 +3,14 @@ set -o pipefail
exec 5>&1
# 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"
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
else
export VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning"
export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
fi
until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Valkey..."
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..."
sleep 2
done
@@ -348,7 +348,7 @@ while true; do
if [[ -z ${VALIDATED_CERTIFICATES[*]} ]]; then
log_f "Cannot validate any hostnames, skipping Let's Encrypt for 1 hour."
log_f "Use SKIP_LETS_ENCRYPT=y in mailcow.conf to skip it permanently."
${VALKEY_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
sleep 1h
exec $(readlink -f "$0")
fi
@@ -389,7 +389,7 @@ while true; do
DOVECOT_CERT_SERIAL_NEW="$(echo | openssl s_client -connect dovecot:143 -starttls imap 2>/dev/null | openssl x509 -inform pem -noout -serial | cut -d "=" -f 2)"
if [[ ${RELOAD_LOOP_C} -gt 3 ]]; then
log_f "Some services do return old end dates, something went wrong!"
${VALKEY_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
break;
fi
done
@@ -410,7 +410,7 @@ while true; do
;;
*) # non-zero
log_f "Some errors occurred, retrying in 30 minutes..."
${VALKEY_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
sleep 30m
exec $(readlink -f "$0")
;;

View File

@@ -5,13 +5,13 @@ log_f() {
echo -n "$(date) - ${1}"
elif [[ ${2} == "no_date" ]]; then
echo "${1}"
elif [[ ${2} != "valkey_only" ]]; then
elif [[ ${2} != "redis_only" ]]; then
echo "$(date) - ${1}"
fi
if [[ ${3} == "b64" ]]; then
${VALKEY_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"base64,$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}")\"}" > /dev/null
${REDIS_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"base64,$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}")\"}" > /dev/null
else
${VALKEY_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}" | \
${REDIS_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}" | \
tr '%&;$"[]{}-\r\n' ' ')\"}" > /dev/null
fi
}

View File

@@ -101,7 +101,7 @@ ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} \
--acme-dir /var/www/acme/ 2>&1 > /tmp/_cert.pem | tee /dev/fd/5; exit ${PIPESTATUS[0]})
SUCCESS="$?"
ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
log_f "${ACME_RESPONSE_B64}" valkey_only b64
log_f "${ACME_RESPONSE_B64}" redis_only b64
case "$SUCCESS" in
0) # cert requested
log_f "Deploying certificate ${CERT}..."
@@ -124,7 +124,7 @@ case "$SUCCESS" in
;;
*) # non-zero is non-fun
log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}'"
redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)"
redis-cli -h redis -a ${REDISPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)"
exit 100${SUCCESS}
;;
esac

View File

@@ -8,7 +8,7 @@ fi
# Cleaning up garbage
echo "Cleaning up tmp files..."
rm -rf /var/lib/clamav/tmp.*
rm -rf /var/lib/clamav/clamav-*.tmp
# Prepare whitelist

View File

@@ -32,21 +32,21 @@ async def lifespan(app: FastAPI):
logger.info("Init APP")
# Init valkey client
if os.environ['VALKEY_SLAVEOF_IP'] != "":
valkey_client = valkey = await aioredis.from_url(f"redis://{os.environ['VALKEY_SLAVEOF_IP']}:{os.environ['VALKEY_SLAVEOF_PORT']}/0", password=os.environ['VALKEYPASS'])
# Init redis client
if os.environ['REDIS_SLAVEOF_IP'] != "":
redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0", password=os.environ['REDISPASS'])
else:
valkey_client = valkey = await aioredis.from_url("redis://valkey-mailcow:6379/0", password=os.environ['VALKEYPASS'])
redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0", password=os.environ['REDISPASS'])
# Init docker clients
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
dockerapi = DockerApi(valkey_client, sync_docker_client, async_docker_client, logger)
dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
logger.info("Subscribe to valkey channel")
# Subscribe to valkey channel
dockerapi.pubsub = valkey.pubsub()
logger.info("Subscribe to redis channel")
# Subscribe to redis channel
dockerapi.pubsub = redis.pubsub()
await dockerapi.pubsub.subscribe("MC_CHANNEL")
asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
@@ -57,9 +57,9 @@ async def lifespan(app: FastAPI):
dockerapi.sync_docker_client.close()
await dockerapi.async_docker_client.close()
# Close valkey
# Close redis
await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
await dockerapi.valkey_client.close()
await dockerapi.redis_client.close()
app = FastAPI(lifespan=lifespan)
@@ -73,11 +73,11 @@ async def get_host_update_stats():
dockerapi.host_stats_isUpdating = True
while True:
if await dockerapi.valkey_client.exists('host_stats'):
if await dockerapi.redis_client.exists('host_stats'):
break
await asyncio.sleep(1.5)
stats = json.loads(await dockerapi.valkey_client.get('host_stats'))
stats = json.loads(await dockerapi.redis_client.get('host_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
@app.get("/containers/{container_id}/json")
@@ -185,11 +185,11 @@ async def post_container_update_stats(container_id : str):
dockerapi.containerIds_to_update.append(container_id)
while True:
if await dockerapi.valkey_client.exists(container_id + '_stats'):
if await dockerapi.redis_client.exists(container_id + '_stats'):
break
await asyncio.sleep(1.5)
stats = json.loads(await dockerapi.valkey_client.get(container_id + '_stats'))
stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json")

View File

@@ -10,8 +10,8 @@ from datetime import datetime
from fastapi import FastAPI, Response, Request
class DockerApi:
def __init__(self, valkey_client, sync_docker_client, async_docker_client, logger):
self.valkey_client = valkey_client
def __init__(self, redis_client, sync_docker_client, async_docker_client, logger):
self.redis_client = redis_client
self.sync_docker_client = sync_docker_client
self.async_docker_client = async_docker_client
self.logger = logger
@@ -533,7 +533,7 @@ class DockerApi:
"architecture": platform.machine()
}
await self.valkey_client.set('host_stats', json.dumps(host_stats), ex=10)
await self.redis_client.set('host_stats', json.dumps(host_stats), ex=10)
except Exception as e:
res = {
"type": "danger",
@@ -550,14 +550,14 @@ class DockerApi:
if container._id == container_id:
res = await container.stats(stream=False)
if await self.valkey_client.exists(container_id + '_stats'):
stats = json.loads(await self.valkey_client.get(container_id + '_stats'))
if await self.redis_client.exists(container_id + '_stats'):
stats = json.loads(await self.redis_client.get(container_id + '_stats'))
else:
stats = []
stats.append(res[0])
if len(stats) > 3:
del stats[0]
await self.valkey_client.set(container_id + '_stats', json.dumps(stats), ex=60)
await self.redis_client.set(container_id + '_stats', json.dumps(stats), ex=60)
except Exception as e:
res = {
"type": "danger",

View File

@@ -1,4 +1,4 @@
FROM alpine:3.21
FROM alpine:3.22
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
@@ -118,7 +118,7 @@ RUN addgroup -g 5000 vmail \
COPY trim_logs.sh /usr/local/bin/trim_logs.sh
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
COPY syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
COPY imapsync /usr/local/bin/imapsync
COPY imapsync_runner.pl /usr/local/bin/imapsync_runner.pl
COPY report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve

View File

@@ -2,7 +2,7 @@
source /source_env.sh
MAX_AGE=$(redis-cli --raw -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET Q_MAX_AGE)
MAX_AGE=$(redis-cli --raw -h redis-mailcow -a ${REDISPASS} --no-auth-warning GET Q_MAX_AGE)
if [[ -z ${MAX_AGE} ]]; then
echo "Max age for quarantine items not defined"

View File

@@ -13,18 +13,18 @@ until dig +short mailcow.email > /dev/null; do
done
# Do not attempt to write to slave
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
else
VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning"
REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
fi
until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Valkey..."
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..."
sleep 2
done
${VALKEY_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
# Create missing directories
[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
@@ -44,90 +44,109 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
else
QUOTA_TABLE=quota2replica
fi
cat <<EOF > /etc/dovecot/conf.d/12-mysql.conf
# Autogenerated by mailcow - DO NOT TOUCH!
mysql /var/run/mysqld/mysqld.sock {
dbname=${DBNAME}
user=${DBUSER}
password=${DBPASS}
ssl = no
}
EOF
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-quota.conf
# Autogenerated by mailcow
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
map {
pattern = priv/quota/storage
table = ${QUOTA_TABLE}
dict_map priv/quota/storage {
sql_table = ${QUOTA_TABLE}
username_field = username
value_field = bytes
value_field bytes {
}
}
map {
pattern = priv/quota/messages
table = ${QUOTA_TABLE}
dict_map priv/quota/messages {
sql_table = ${QUOTA_TABLE}
username_field = username
value_field = messages
value_field messages {
}
}
EOF
# Create dict used for sieve pre and postfilters
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
# Autogenerated by mailcow
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
map {
pattern = priv/sieve/name/\$script_name
table = sieve_before
dict_map priv/sieve/name/\$script_name {
sql_table = sieve_before
username_field = username
value_field = id
fields {
script_name = \$script_name
value_field id {
}
# The script name field in the table to query
key_field script_name {
value = \$script_name
}
}
map {
pattern = priv/sieve/data/\$id
table = sieve_before
dict_map priv/sieve/data/\$id {
sql_table = sieve_before
username_field = username
value_field = script_data
fields {
id = \$id
value_field script_data {
}
key_field id {
value = \$id
}
}
EOF
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
# Autogenerated by mailcow
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
map {
pattern = priv/sieve/name/\$script_name
table = sieve_after
dict_map priv/sieve/name/\$script_name {
sql_table = sieve_after
username_field = username
value_field = id
fields {
script_name = \$script_name
value_field id {
}
key_field script_name {
value = \$script_name
}
}
map {
pattern = priv/sieve/data/\$id
table = sieve_after
dict_map priv/sieve/data/\$id {
sql_table = sieve_after
username_field = username
value_field = script_data
fields {
id = \$id
value_field script_data {
}
key_field id {
value = \$id
}
}
EOF
echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
if [[ "${ACL_ANYONE}" == "allow" ]]; then
echo -n "yes" > /etc/dovecot/acl_anyone
else
echo -n "no" > /etc/dovecot/acl_anyone
fi
if [[ "${SKIP_FTS}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo -e "\e[33mDetecting SKIP_FTS=y... not enabling Flatcurve (FTS) then...\e[0m"
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication lazy_expunge' > /etc/dovecot/mail_plugins
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify listescape replication mail_log' > /etc/dovecot/mail_plugins_imap
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
echo -n 'quota quota_clone acl mail_crypt mail_crypt_acl mail_log mail_compress notify lazy_expunge' > /etc/dovecot/mail_plugins
echo -n 'quota quota_clone imap_quota imap_acl acl imap_sieve mail_crypt mail_crypt_acl mail_compress notify mail_log' > /etc/dovecot/mail_plugins_imap
echo -n 'quota quota_clone sieve acl mail_crypt mail_crypt_acl mail_compress notify' > /etc/dovecot/mail_plugins_lmtp
else
echo -e "\e[32mDetecting SKIP_FTS=n... enabling Flatcurve (FTS)\e[0m"
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication lazy_expunge' > /etc/dovecot/mail_plugins
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins_imap
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_flatcurve notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
echo -n 'quota quota_clone acl mail_crypt mail_crypt_acl mail_log mail_compress notify fts fts_flatcurve lazy_expunge' > /etc/dovecot/mail_plugins
echo -n 'quota quota_clone imap_quota imap_acl acl imap_sieve mail_crypt mail_crypt_acl mail_compress notify mail_log fts fts_flatcurve' > /etc/dovecot/mail_plugins_imap
echo -n 'quota quota_clone sieve acl mail_crypt mail_crypt_acl mail_compress fts fts_flatcurve notify' > /etc/dovecot/mail_plugins_lmtp
fi
chmod 644 /etc/dovecot/mail_plugins /etc/dovecot/mail_plugins_imap /etc/dovecot/mail_plugins_lmtp /templates/quarantine.tpl
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
# Autogenerated by mailcow
driver = mysql
connect = "host=/var/run/mysqld/mysqld.sock dbname=${DBNAME} user=${DBUSER} password=${DBPASS}"
user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%d/%n/${MAILDIR_SUB}:VOLATILEDIR=/var/volatile/%u:INDEX=/var/vmail_index/%u') AS mail, '%s' AS protocol, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND (active = '1' OR active = '2')
query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format')), mailbox_path_prefix, '%{user | domain }}/%{user | username }/Maildir:VOLATILEDIR=/var/volatile/%{user}:INDEX=/var/vmail_index/%{user}') AS mail, '%{protocol}' AS protocol, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%{user}' AND (active = '1' OR active = '2')
iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
EOF
@@ -158,8 +177,8 @@ for cert_dir in /etc/ssl/mail/*/ ; do
domains=($(cat ${cert_dir}domains))
for domain in ${domains[@]}; do
echo 'local_name '${domain}' {' >> /etc/dovecot/sni.conf;
echo ' ssl_cert = <'${cert_dir}'cert.pem' >> /etc/dovecot/sni.conf;
echo ' ssl_key = <'${cert_dir}'key.pem' >> /etc/dovecot/sni.conf;
echo ' ssl_server_cert_file = '${cert_dir}'cert.pem' >> /etc/dovecot/sni.conf;
echo ' ssl_server_key_file = '${cert_dir}'key.pem' >> /etc/dovecot/sni.conf;
echo '}' >> /etc/dovecot/sni.conf;
done
done
@@ -183,11 +202,13 @@ else
fi
cat <<EOF > /etc/dovecot/shared_namespace.conf
# Autogenerated by mailcow
namespace {
namespace shared {
type = shared
separator = /
prefix = Shared/%%u/
location = maildir:%%h${MAILDIR_SUB_SHARED}:INDEX=~${MAILDIR_SUB_SHARED}/Shared/%%u
prefix = Shared/\$user/
mail_driver = maildir
mail_path = %{owner_home}${MAILDIR_SUB_SHARED}
mail_index_private_path = ~${MAILDIR_SUB_SHARED}/Shared/%{owner_user}
subscriptions = no
list = children
}
@@ -197,7 +218,7 @@ EOF
cat <<EOF > /etc/dovecot/sogo_trusted_ip.conf
# Autogenerated by mailcow
remote ${IPV4_NETWORK}.248 {
disable_plaintext_auth = no
auth_allow_cleartext = yes
}
EOF
@@ -208,9 +229,13 @@ echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
cat <<EOF > /etc/dovecot/sogo-sso.conf
# Autogenerated by mailcow
passdb {
driver = static
args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
passdb static {
fields {
allow_real_nets=${IPV4_NETWORK}.248/32
}
password={plain}${RAND_PASS}
}
EOF
@@ -235,9 +260,9 @@ fi
if [[ "${SKIP_FTS}" =~ ^([nN][oO]|[nN])+$ ]]; then
echo -e "\e[94mConfiguring FTS Settings...\e[0m"
echo -e "\e[94mSetting FTS Memory Limit (per process) to ${FTS_HEAP} MB\e[0m"
sed -i "s/vsz_limit\s*=\s*[0-9]*\s*MB*/vsz_limit=${FTS_HEAP} MB/" /etc/dovecot/conf.d/fts.conf
sed -i "s/vsz_limit\s*=\s*[0-9]*\s*MB*/vsz_limit=${FTS_HEAP} MB/" /etc/dovecot/conf.d/35-fts.conf
echo -e "\e[94mSetting FTS Process Limit to ${FTS_PROCS}\e[0m"
sed -i "s/process_limit\s*=\s*[0-9]*/process_limit=${FTS_PROCS}/" /etc/dovecot/conf.d/fts.conf
sed -i "s/process_limit\s*=\s*[0-9]*/process_limit=${FTS_PROCS}/" /etc/dovecot/conf.d/35-fts.conf
fi
# 401 is user dovecot
@@ -249,16 +274,16 @@ else
chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem
fi
# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
if grep -qE 'ssl_min_protocol\s*=\s*(TLSv1|TLSv1\.1)\s*$' /etc/dovecot/dovecot.conf /etc/dovecot/extra.conf; then
sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf
# # Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
# if grep -qE 'ssl_min_protocol\s*=\s*(TLSv1|TLSv1\.1)\s*$' /etc/dovecot/dovecot.conf /etc/dovecot/extra.conf; then
# sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf
echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf
echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf
echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
fi
# echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf
# echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf
# echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
# echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
# echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
# fi
# Compile sieve scripts
sievec /var/vmail/sieve/global_sieve_before.sieve
@@ -341,8 +366,8 @@ done
# May be related to something inside Docker, I seriously don't know
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
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
fi
exec "$@"

View File

@@ -132,8 +132,8 @@ while ($row = $sth->fetchrow_arrayref()) {
"--tmpdir", "/tmp",
"--nofoldersizes",
"--addheader",
($timeout1 le "0" ? () : ('--timeout1', $timeout1)),
($timeout2 le "0" ? () : ('--timeout2', $timeout2)),
($timeout1 gt "0" ? () : ('--timeout1', $timeout1)),
($timeout2 gt "0" ? () : ('--timeout2', $timeout2)),
($exclude eq "" ? () : ("--exclude", $exclude)),
($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
($maxage eq "0" ? () : ('--maxage', $maxage)),

View File

@@ -32,7 +32,7 @@ try:
while True:
try:
r = redis.StrictRedis(host='valkey-mailcow', decode_responses=True, port=6379, db=0, password=os.environ['VALKEYPASS'])
r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS'])
r.ping()
except Exception as ex:
print('%s - trying again...' % (ex))

View File

@@ -23,7 +23,7 @@ else:
while True:
try:
r = redis.StrictRedis(host='valkey-mailcow', decode_responses=True, port=6379, db=0, username='quota_notify', password='')
r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0, username='quota_notify', password='')
r.ping()
except Exception as ex:
print('%s - trying again...' % (ex))

View File

@@ -3,16 +3,16 @@
source /source_env.sh
# Do not attempt to write to slave
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
else
VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning"
REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
fi
# Is replication active?
# grep on file is less expensive than doveconf
if [ -n ${MAILCOW_REPLICA_IP} ]; then
${VALKEY_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
exit
fi
@@ -22,7 +22,7 @@ FAILED_SYNCS=$(doveadm replicator status | grep "Waiting 'failed' requests" | gr
# 1 failed job for mailcow.local is expected and healthy
if [[ "${FAILED_SYNCS}" != 0 ]] && [[ "${FAILED_SYNCS}" != 1 ]]; then
printf "Dovecot replicator has %d failed jobs\n" "${FAILED_SYNCS}"
${VALKEY_CMDLINE} SET DOVECOT_REPL_HEALTH "${FAILED_SYNCS}" > /dev/null
${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH "${FAILED_SYNCS}" > /dev/null
else
${VALKEY_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
fi

View File

@@ -15,21 +15,21 @@ source s_dgram {
internal();
};
destination d_stdout { pipe("/dev/stdout"); };
destination d_valkey_ui_log {
destination d_redis_ui_log {
redis(
host("`VALKEY_SLAVEOF_IP`")
persist-name("valkey1")
port(`VALKEY_SLAVEOF_PORT`)
auth("`VALKEYPASS`")
host("`REDIS_SLAVEOF_IP`")
persist-name("redis1")
port(`REDIS_SLAVEOF_PORT`)
auth("`REDISPASS`")
command("LPUSH" "DOVECOT_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
destination d_valkey_f2b_channel {
destination d_redis_f2b_channel {
redis(
host("`VALKEY_SLAVEOF_IP`")
persist-name("valkey2")
port(`VALKEY_SLAVEOF_PORT`)
auth("`VALKEYPASS`")
host("`REDIS_SLAVEOF_IP`")
persist-name("redis2")
port(`REDIS_SLAVEOF_PORT`)
auth("`REDISPASS`")
command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
);
};
@@ -48,6 +48,6 @@ log {
filter(f_replica);
destination(d_stdout);
filter(f_mail);
destination(d_valkey_ui_log);
destination(d_valkey_f2b_channel);
destination(d_redis_ui_log);
destination(d_redis_f2b_channel);
};

View File

@@ -15,21 +15,21 @@ source s_dgram {
internal();
};
destination d_stdout { pipe("/dev/stdout"); };
destination d_valkey_ui_log {
destination d_redis_ui_log {
redis(
host("valkey-mailcow")
persist-name("valkey1")
host("redis-mailcow")
persist-name("redis1")
port(6379)
auth("`VALKEYPASS`")
auth("`REDISPASS`")
command("LPUSH" "DOVECOT_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
destination d_valkey_f2b_channel {
destination d_redis_f2b_channel {
redis(
host("valkey-mailcow")
persist-name("valkey2")
host("redis-mailcow")
persist-name("redis2")
port(6379)
auth("`VALKEYPASS`")
auth("`REDISPASS`")
command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
);
};
@@ -48,6 +48,6 @@ log {
filter(f_replica);
destination(d_stdout);
filter(f_mail);
destination(d_valkey_ui_log);
destination(d_valkey_f2b_channel);
destination(d_redis_ui_log);
destination(d_redis_f2b_channel);
};

View File

@@ -9,17 +9,18 @@ catch_non_zero() {
}
source /source_env.sh
# Do not attempt to write to slave
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
else
VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning"
REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
fi
catch_non_zero "${VALKEY_CMDLINE} LTRIM ACME_LOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM POSTFIX_MAILLOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM DOVECOT_MAILLOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM SOGO_LOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM NETFILTER_LOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM AUTODISCOVER_LOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM API_LOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM RL_LOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM WATCHDOG_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM ACME_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM POSTFIX_MAILLOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM DOVECOT_MAILLOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM SOGO_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM NETFILTER_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM AUTODISCOVER_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM API_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM RL_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM WATCHDOG_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM CRON_LOG 0 ${LOG_LINES}"

View File

@@ -1,6 +1,6 @@
#!/bin/sh
backend=nftables
backend=iptables
nft list table ip filter &>/dev/null
nftables_found=$?

View File

@@ -44,24 +44,25 @@ def refreshF2boptions():
global exit_code
f2boptions = {}
if not valkey.get('F2B_OPTIONS'):
f2boptions['ban_time'] = valkey.get('F2B_BAN_TIME')
f2boptions['max_ban_time'] = valkey.get('F2B_MAX_BAN_TIME')
f2boptions['ban_time_increment'] = valkey.get('F2B_BAN_TIME_INCREMENT')
f2boptions['max_attempts'] = valkey.get('F2B_MAX_ATTEMPTS')
f2boptions['retry_window'] = valkey.get('F2B_RETRY_WINDOW')
f2boptions['netban_ipv4'] = valkey.get('F2B_NETBAN_IPV4')
f2boptions['netban_ipv6'] = valkey.get('F2B_NETBAN_IPV6')
if not r.get('F2B_OPTIONS'):
f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
else:
try:
f2boptions = json.loads(valkey.get('F2B_OPTIONS'))
except ValueError:
logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json')
f2boptions = json.loads(r.get('F2B_OPTIONS'))
except ValueError as e:
logger.logCrit(
'Error loading F2B options: F2B_OPTIONS is not json. Exception: %s' % e)
quit_now = True
exit_code = 2
verifyF2boptions(f2boptions)
valkey.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
def verifyF2boptions(f2boptions):
verifyF2boption(f2boptions, 'ban_time', 1800)
@@ -81,7 +82,7 @@ def refreshF2bregex():
global f2bregex
global quit_now
global exit_code
if not valkey.get('F2B_REGEX'):
if not r.get('F2B_REGEX'):
f2bregex = {}
f2bregex[1] = r'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
f2bregex[2] = r'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
@@ -92,11 +93,11 @@ def refreshF2bregex():
f2bregex[7] = r'\w+\([^,]+,([0-9a-f\.:]+),<[^>]+>\): unknown user \(SHA1 of given password: [a-f0-9]+\)'
f2bregex[8] = r'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
f2bregex[9] = r'([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
valkey.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
else:
try:
f2bregex = {}
f2bregex = json.loads(valkey.get('F2B_REGEX'))
f2bregex = json.loads(r.get('F2B_REGEX'))
except ValueError:
logger.logCrit('Error loading F2B options: F2B_REGEX is not json')
quit_now = True
@@ -175,7 +176,7 @@ def ban(address):
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)
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
else:
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (
MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
@@ -186,7 +187,7 @@ def unban(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)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
return
logger.logInfo('Unbanning %s' % net)
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
@@ -197,8 +198,8 @@ def unban(net):
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)
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
if net in bans:
logdebug("Unban for %s, setting attempts=0, ban_counter+=1" % net)
bans[net]['attempts'] = 0
@@ -225,10 +226,10 @@ def permBan(net, unban=False):
if is_unbanned:
valkey.hdel('F2B_PERM_BANS', '%s' % net)
r.hdel('F2B_PERM_BANS', '%s' % net)
logger.logCrit('Removed host/network %s from denylist' % net)
elif is_banned:
valkey.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
logger.logCrit('Added host/network %s to denylist' % net)
def clear():
@@ -243,17 +244,17 @@ def clear():
tables.clearIPv6Table()
try:
if r is not None:
valkey.delete('F2B_ACTIVE_BANS')
valkey.delete('F2B_PERM_BANS')
r.delete('F2B_ACTIVE_BANS')
r.delete('F2B_PERM_BANS')
except Exception as ex:
logger.logWarn('Error clearing valkey keys F2B_ACTIVE_BANS and F2B_PERM_BANS: %s' % ex)
logger.logWarn('Error clearing redis keys F2B_ACTIVE_BANS and F2B_PERM_BANS: %s' % ex)
def watch():
global pubsub
global quit_now
global exit_code
logger.logInfo('Watching Valkey channel F2B_CHANNEL')
logger.logInfo('Watching Redis channel F2B_CHANNEL')
pubsub.subscribe('F2B_CHANNEL')
while not quit_now:
@@ -305,7 +306,7 @@ def autopurge():
time.sleep(10)
refreshF2boptions()
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
QUEUE_UNBAN = valkey.hgetall('F2B_QUEUE_UNBAN')
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
logdebug("QUEUE_UNBAN: %s" % QUEUE_UNBAN)
if QUEUE_UNBAN:
for net in QUEUE_UNBAN:
@@ -390,7 +391,7 @@ def whitelistUpdate():
global WHITELIST
while not quit_now:
start_time = time.time()
list = valkey.hgetall('F2B_WHITELIST')
list = r.hgetall('F2B_WHITELIST')
new_whitelist = []
if list:
new_whitelist = genNetworkList(list)
@@ -405,7 +406,7 @@ def blacklistUpdate():
global BLACKLIST
while not quit_now:
start_time = time.time()
list = valkey.hgetall('F2B_BLACKLIST')
list = r.hgetall('F2B_BLACKLIST')
new_blacklist = []
if list:
new_blacklist = genNetworkList(list)
@@ -448,11 +449,6 @@ if __name__ == '__main__':
tables = NFTables(chain_name, logger)
else:
logger.logInfo('Using IPTables backend')
logger.logWarn(
"DEPRECATION: iptables-legacy is deprecated and will be removed in future releases. "
"Please switch to nftables on your host to ensure complete compatibility."
)
time.sleep(5)
tables = IPTables(chain_name, logger)
clear()
@@ -466,35 +462,35 @@ if __name__ == '__main__':
logger.logInfo(f"Setting {chain_name} isolation")
tables.create_mailcow_isolation_rule("br-mailcow", [3306, 6379, 8983, 12345], os.getenv("MAILCOW_REPLICA_IP"))
# connect to valkey
# connect to redis
while True:
try:
valkey_slaveof_ip = os.getenv('VALKEY_SLAVEOF_IP', '')
valkey_slaveof_port = os.getenv('VALKEY_SLAVEOF_PORT', '')
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
redis_slaveof_port = os.getenv('REDIS_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'])
"Connecting redis (SLAVEOF_IP:%s, PORT:%s)" % (redis_slaveof_ip, redis_slaveof_port))
if "".__eq__(redis_slaveof_ip):
r = redis.StrictRedis(
host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS'])
else:
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()
r = redis.StrictRedis(
host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS'])
r.ping()
pubsub = r.pubsub()
except Exception as ex:
logdebug(
'Redis connection failed: %s - trying again in 3 seconds' % (ex))
time.sleep(3)
else:
break
logger.set_valkey(valkey)
logdebug("Valkey connection established, setting up F2B keys")
logger.set_redis(r)
logdebug("Redis connection established, setting up F2B keys")
if valkey.exists('F2B_LOG'):
if r.exists('F2B_LOG'):
logdebug("Renaming F2B_LOG to NETFILTER_LOG")
valkey.rename('F2B_LOG', 'NETFILTER_LOG')
valkey.delete('F2B_ACTIVE_BANS')
valkey.delete('F2B_PERM_BANS')
r.rename('F2B_LOG', 'NETFILTER_LOG')
r.delete('F2B_ACTIVE_BANS')
r.delete('F2B_PERM_BANS')
refreshF2boptions()

View File

@@ -1,36 +1,24 @@
import time
import json
import datetime
class Logger:
def __init__(self):
self.valkey = None
self.r = None
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 set_redis(self, redis):
self.r = redis
def log(self, priority, 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:
tolog = {}
tolog['time'] = int(round(time.time()))
tolog['priority'] = priority
tolog['message'] = message
print(message)
if self.r is not None:
try:
self.valkey.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
except Exception as ex:
print(f'{ts} WARN: Failed logging to valkey: {ex}', flush=True)
print('Failed logging to redis: %s' % (ex))
def logWarn(self, message):
self.log('warn', message)
@@ -39,4 +27,4 @@ class Logger:
self.log('crit', message)
def logInfo(self, message):
self.log('info', message)
self.log('info', message)

View File

@@ -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 template_vars['ENABLE_IPV6']:
if not 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;"

View File

@@ -9,24 +9,24 @@ while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u
done
# Do not attempt to write to slave
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
VALKEY_HOST=$VALKEY_SLAVEOF_IP
VALKEY_PORT=$VALKEY_SLAVEOF_PORT
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
REDIS_HOST=$REDIS_SLAVEOF_IP
REDIS_PORT=$REDIS_SLAVEOF_PORT
else
VALKEY_HOST="valkey-mailcow"
VALKEY_PORT="6379"
REDIS_HOST="redis"
REDIS_PORT="6379"
fi
VALKEY_CMDLINE="redis-cli -h ${VALKEY_HOST} -p ${VALKEY_PORT} -a ${VALKEYPASS} --no-auth-warning"
REDIS_CMDLINE="redis-cli -h ${REDIS_HOST} -p ${REDIS_PORT} -a ${REDISPASS} --no-auth-warning"
until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Valkey..."
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..."
sleep 2
done
# Set valkey session store
# Set redis session store
echo -n '
session.save_handler = redis
session.save_path = "tcp://'${VALKEY_HOST}':'${VALKEY_PORT}'?auth='${VALKEYPASS}'"
session.save_path = "tcp://'${REDIS_HOST}':'${REDIS_PORT}'?auth='${REDISPASS}'"
' > /usr/local/etc/php/conf.d/session_store.ini
# Check mysql_upgrade (master and slave)
@@ -91,22 +91,22 @@ fi
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "We are master, preparing..."
# Set a default release format
if [[ -z $(${VALKEY_CMDLINE} --raw GET Q_RELEASE_FORMAT) ]]; then
${VALKEY_CMDLINE} --raw SET Q_RELEASE_FORMAT raw
if [[ -z $(${REDIS_CMDLINE} --raw GET Q_RELEASE_FORMAT) ]]; then
${REDIS_CMDLINE} --raw SET Q_RELEASE_FORMAT raw
fi
# Set max age of q items - if unset
if [[ -z $(${VALKEY_CMDLINE} --raw GET Q_MAX_AGE) ]]; then
${VALKEY_CMDLINE} --raw SET Q_MAX_AGE 365
if [[ -z $(${REDIS_CMDLINE} --raw GET Q_MAX_AGE) ]]; then
${REDIS_CMDLINE} --raw SET Q_MAX_AGE 365
fi
# Set default password policy - if unset
if [[ -z $(${VALKEY_CMDLINE} --raw HGET PASSWD_POLICY length) ]]; then
${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY length 6
${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY chars 0
${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY special_chars 0
${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY lowerupper 0
${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY numbers 0
if [[ -z $(${REDIS_CMDLINE} --raw HGET PASSWD_POLICY length) ]]; then
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY length 6
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY chars 0
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY special_chars 0
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY lowerupper 0
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY numbers 0
fi
# Trigger db init
@@ -114,9 +114,9 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
php -c /usr/local/etc/php -f /web/inc/init_db.inc.php
# Recreating domain map
echo "Rebuilding domain map in Valkey..."
echo "Rebuilding domain map in Redis..."
declare -a DOMAIN_ARR
${VALKEY_CMDLINE} DEL DOMAIN_MAP > /dev/null
${REDIS_CMDLINE} DEL DOMAIN_MAP > /dev/null
while read line
do
DOMAIN_ARR+=("$line")
@@ -128,7 +128,7 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
if [[ ! -z ${DOMAIN_ARR} ]]; then
for domain in "${DOMAIN_ARR[@]}"; do
${VALKEY_CMDLINE} HSET DOMAIN_MAP ${domain} 1 > /dev/null
${REDIS_CMDLINE} HSET DOMAIN_MAP ${domain} 1 > /dev/null
done
fi

View File

@@ -3,7 +3,6 @@ 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 && \
@@ -34,7 +33,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
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 syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_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

View File

@@ -1,7 +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
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
fi
exec "$@"

View File

@@ -17,14 +17,14 @@ until dig +short mailcow.email > /dev/null; do
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"
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
else
export VALKEY_CMDLINE="redis-cli -h valkey -p 6379 -a ${VALKEYPASS} --no-auth-warning"
export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
fi
until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Valkey..."
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..."
sleep 2
done

View File

@@ -15,12 +15,12 @@ source s_src {
internal();
};
destination d_stdout { pipe("/dev/stdout"); };
destination d_valkey_ui_log {
destination d_redis_ui_log {
redis(
host("`VALKEY_SLAVEOF_IP`")
persist-name("valkey1")
port(`VALKEY_SLAVEOF_PORT`)
auth("`VALKEYPASS`")
host("`REDIS_SLAVEOF_IP`")
persist-name("redis1")
port(`REDIS_SLAVEOF_PORT`)
auth("`REDISPASS`")
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
@@ -41,5 +41,5 @@ log {
filter(f_ignore);
destination(d_stdout);
filter(f_mail);
destination(d_valkey_ui_log);
destination(d_redis_ui_log);
};

View File

@@ -15,12 +15,12 @@ source s_src {
internal();
};
destination d_stdout { pipe("/dev/stdout"); };
destination d_valkey_ui_log {
destination d_redis_ui_log {
redis(
host("valkey-mailcow")
persist-name("valkey1")
host("redis-mailcow")
persist-name("redis1")
port(6379)
auth("`VALKEYPASS`")
auth("`REDISPASS`")
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
@@ -41,5 +41,5 @@ log {
filter(f_ignore);
destination(d_stdout);
filter(f_mail);
destination(d_valkey_ui_log);
destination(d_redis_ui_log);
};

View File

@@ -41,7 +41,7 @@ RUN groupadd -g 102 postfix \
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 syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
COPY postfix.sh /opt/postfix.sh
COPY rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham
COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam

View File

@@ -8,8 +8,8 @@ for file in /hooks/*; do
fi
done
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
fi
# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
@@ -21,6 +21,6 @@ if grep -qE '\!SSLv2|\!SSLv3|>=TLSv1(\.[0-1])?$' /opt/postfix/conf/main.cf /opt/
echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
fi
fi
exec "$@"

View File

@@ -15,21 +15,21 @@ source s_src {
internal();
};
destination d_stdout { pipe("/dev/stdout"); };
destination d_valkey_ui_log {
destination d_redis_ui_log {
redis(
host("`VALKEY_SLAVEOF_IP`")
persist-name("valkey1")
port(`VALKEY_SLAVEOF_PORT`)
auth("`VALKEYPASS`")
host("`REDIS_SLAVEOF_IP`")
persist-name("redis1")
port(`REDIS_SLAVEOF_PORT`)
auth("`REDISPASS`")
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
destination d_valkey_f2b_channel {
destination d_redis_f2b_channel {
redis(
host("`VALKEY_SLAVEOF_IP`")
persist-name("valkey2")
port(`VALKEY_SLAVEOF_PORT`)
auth("`VALKEYPASS`")
host("`REDIS_SLAVEOF_IP`")
persist-name("redis2")
port(`REDIS_SLAVEOF_PORT`)
auth("`REDISPASS`")
command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
);
};
@@ -50,6 +50,6 @@ log {
filter(f_ignore);
destination(d_stdout);
filter(f_mail);
destination(d_valkey_ui_log);
destination(d_valkey_f2b_channel);
destination(d_redis_ui_log);
destination(d_redis_f2b_channel);
};

View File

@@ -15,21 +15,21 @@ source s_src {
internal();
};
destination d_stdout { pipe("/dev/stdout"); };
destination d_valkey_ui_log {
destination d_redis_ui_log {
redis(
host("valkey-mailcow")
persist-name("valkey1")
host("redis-mailcow")
persist-name("redis1")
port(6379)
auth("`VALKEYPASS`")
auth("`REDISPASS`")
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
destination d_valkey_f2b_channel {
destination d_redis_f2b_channel {
redis(
host("valkey-mailcow")
persist-name("valkey2")
host("redis-mailcow")
persist-name("redis2")
port(6379)
auth("`VALKEYPASS`")
auth("`REDISPASS`")
command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
);
};
@@ -50,6 +50,6 @@ log {
filter(f_ignore);
destination(d_stdout);
filter(f_mail);
destination(d_valkey_ui_log);
destination(d_valkey_f2b_channel);
destination(d_redis_ui_log);
destination(d_redis_f2b_channel);
};

View File

@@ -52,33 +52,33 @@ if [[ ! -z ${RSPAMD_V6} ]]; then
echo ${RSPAMD_V6}/128 >> /etc/rspamd/custom/rspamd_trusted.map
fi
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
cat <<EOF > /etc/rspamd/local.d/redis.conf
read_servers = "valkey-mailcow:6379";
write_servers = "${VALKEY_SLAVEOF_IP}:${VALKEY_SLAVEOF_PORT}";
password = "${VALKEYPASS}";
read_servers = "redis:6379";
write_servers = "${REDIS_SLAVEOF_IP}:${REDIS_SLAVEOF_PORT}";
password = "${REDISPASS}";
timeout = 10;
EOF
until [[ $(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Valkey @valkey-mailcow..."
until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Redis @redis-mailcow..."
sleep 2
done
until [[ $(redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Valkey @${VALKEY_SLAVEOF_IP}..."
until [[ $(redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Redis @${REDIS_SLAVEOF_IP}..."
sleep 2
done
redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning SLAVEOF ${VALKEY_SLAVEOF_IP} ${VALKEY_SLAVEOF_PORT}
redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF ${REDIS_SLAVEOF_IP} ${REDIS_SLAVEOF_PORT}
else
cat <<EOF > /etc/rspamd/local.d/redis.conf
servers = "valkey-mailcow:6379";
password = "${VALKEYPASS}";
servers = "redis:6379";
password = "${REDISPASS}";
timeout = 10;
EOF
until [[ $(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Valkey slave..."
until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Redis slave..."
sleep 2
done
redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning SLAVEOF NO ONE
redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF NO ONE
fi
if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
@@ -86,8 +86,7 @@ if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; 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
cat <<EOF > /etc/rspamd/local.d/external_services.conf
oletools {
# default olefy settings
servers = "olefy:10055";
@@ -101,7 +100,6 @@ oletools {
retransmits = 1;
}
EOF
fi
fi
# Provide additional lua modules

View File

@@ -44,7 +44,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
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 syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
COPY supervisord.conf /etc/supervisor/supervisord.conf
COPY acl.diff /acl.diff
COPY navMailcowBtns.diff /navMailcowBtns.diff

View File

@@ -24,10 +24,6 @@ while [[ "${DBV_NOW}" != "${DBV_NEW}" ]]; do
done
echo "DB schema is ${DBV_NOW}"
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
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
RAND_PASS=$(openssl rand -base64 16 | tr -dc _A-Z-a-z-0-9)
@@ -50,10 +46,6 @@ 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>

View File

@@ -6,8 +6,8 @@ if [[ "${SKIP_SOGO}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
exit 0
fi
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
fi
echo "$TZ" > /etc/timezone

View File

@@ -17,28 +17,28 @@ source s_sogo {
pipe("/dev/sogo_log" owner(sogo) group(sogo));
};
destination d_stdout { pipe("/dev/stdout"); };
destination d_valkey_ui_log {
destination d_redis_ui_log {
redis(
host("`VALKEY_SLAVEOF_IP`")
persist-name("valkey1")
port(`VALKEY_SLAVEOF_PORT`)
auth("`VALKEYPASS`")
host("`REDIS_SLAVEOF_IP`")
persist-name("redis1")
port(`REDIS_SLAVEOF_PORT`)
auth("`REDISPASS`")
command("LPUSH" "SOGO_LOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
destination d_valkey_f2b_channel {
destination d_redis_f2b_channel {
redis(
host("`VALKEY_SLAVEOF_IP`")
persist-name("valkey2")
port(`VALKEY_SLAVEOF_PORT`)
auth("`VALKEYPASS`")
host("`REDIS_SLAVEOF_IP`")
persist-name("redis2")
port(`REDIS_SLAVEOF_PORT`)
auth("`REDISPASS`")
command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
);
};
log {
source(s_sogo);
destination(d_valkey_ui_log);
destination(d_valkey_f2b_channel);
destination(d_redis_ui_log);
destination(d_redis_f2b_channel);
};
log {
source(s_sogo);

View File

@@ -17,28 +17,28 @@ source s_sogo {
pipe("/dev/sogo_log" owner(sogo) group(sogo));
};
destination d_stdout { pipe("/dev/stdout"); };
destination d_valkey_ui_log {
destination d_redis_ui_log {
redis(
host("valkey-mailcow")
persist-name("valkey1")
host("redis-mailcow")
persist-name("redis1")
port(6379)
auth("`VALKEYPASS`")
auth("`REDISPASS`")
command("LPUSH" "SOGO_LOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
destination d_valkey_f2b_channel {
destination d_redis_f2b_channel {
redis(
host("valkey-mailcow")
persist-name("valkey2")
host("redis-mailcow")
persist-name("redis2")
port(6379)
auth("`VALKEYPASS`")
auth("`REDISPASS`")
command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
);
};
log {
source(s_sogo);
destination(d_valkey_ui_log);
destination(d_valkey_f2b_channel);
destination(d_redis_ui_log);
destination(d_redis_f2b_channel);
};
log {
source(s_sogo);

View File

@@ -1,8 +0,0 @@
FROM python:3.13.2-alpine3.21
WORKDIR /app
COPY migrate.py /app/migrate.py
RUN pip install --no-cache-dir redis
CMD ["python", "/app/migrate.py"]

View File

@@ -1,78 +0,0 @@
import subprocess
import redis
import time
import os
# Container names
SOURCE_CONTAINER = "redis-old-mailcow"
DEST_CONTAINER = "valkey-mailcow"
VALKEYPASS = os.getenv("VALKEYPASS")
def migrate_redis():
src_redis = redis.StrictRedis(host=SOURCE_CONTAINER, port=6379, db=0, password=VALKEYPASS, decode_responses=False)
dest_redis = redis.StrictRedis(host=DEST_CONTAINER, port=6379, db=0, password=VALKEYPASS, decode_responses=False)
cursor = 0
batch_size = 100
migrated_count = 0
print("Starting migration...")
while True:
cursor, keys = src_redis.scan(cursor=cursor, match="*", count=batch_size)
keys_to_migrate = [key for key in keys if not key.startswith(b"PHPREDIS_SESSION:")]
for key in keys_to_migrate:
key_type = src_redis.type(key)
print(f"Import {key} of type {key_type}")
if key_type == b"string":
value = src_redis.get(key)
dest_redis.set(key, value)
elif key_type == b"hash":
value = src_redis.hgetall(key)
dest_redis.hset(key, mapping=value)
elif key_type == b"list":
value = src_redis.lrange(key, 0, -1)
for v in value:
dest_redis.rpush(key, v)
elif key_type == b"set":
value = src_redis.smembers(key)
for v in value:
dest_redis.sadd(key, v)
elif key_type == b"zset":
value = src_redis.zrange(key, 0, -1, withscores=True)
for v, score in value:
dest_redis.zadd(key, {v: score})
# Preserve TTL if exists
ttl = src_redis.ttl(key)
if ttl > 0:
dest_redis.expire(key, ttl)
migrated_count += 1
if cursor == 0:
break # No more keys to scan
print(f"Migration completed! {migrated_count} keys migrated.")
print("Forcing Valkey to save data...")
try:
dest_redis.save() # Immediate RDB save (blocking)
dest_redis.bgrewriteaof() # Rewrites the AOF file in the background
print("Data successfully saved to disk.")
except Exception as e:
print(f"Failed to save data: {e}")
# Main script execution
if __name__ == "__main__":
try:
migrate_redis()
finally:
pass

View File

@@ -16,6 +16,7 @@ RUN apk add --update \
fcgi \
openssl \
nagios-plugins-mysql \
nagios-plugins-dns \
nagios-plugins-disk \
bind-tools \
redis \
@@ -31,11 +32,9 @@ 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 \
&& mkdir /usr/lib/mailcow
&& chmod +x smtp-cli
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"]

View File

@@ -1,39 +0,0 @@
#!/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

View File

@@ -1,10 +1,5 @@
#!/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
@@ -44,18 +39,18 @@ while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u
done
# Do not attempt to write to slave
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
else
VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning"
REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
fi
until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Valkey..."
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..."
sleep 2
done
${VALKEY_CMDLINE} DEL F2B_RES > /dev/null
${REDIS_CMDLINE} DEL F2B_RES > /dev/null
# Common functions
get_ipv6(){
@@ -90,15 +85,15 @@ progress() {
[[ ${CURRENT} -gt ${TOTAL} ]] && return
[[ ${CURRENT} -lt 0 ]] && CURRENT=0
PERCENT=$(( 200 * ${CURRENT} / ${TOTAL} % 2 + 100 * ${CURRENT} / ${TOTAL} ))
${VALKEY_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"service\":\"${SERVICE}\",\"lvl\":\"${PERCENT}\",\"hpnow\":\"${CURRENT}\",\"hptotal\":\"${TOTAL}\",\"hpdiff\":\"${DIFF}\"}" > /dev/null
log_msg "${SERVICE} health level: ${PERCENT}% (${CURRENT}/${TOTAL}), health trend: ${DIFF}" no_valkey
${REDIS_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"service\":\"${SERVICE}\",\"lvl\":\"${PERCENT}\",\"hpnow\":\"${CURRENT}\",\"hptotal\":\"${TOTAL}\",\"hpdiff\":\"${DIFF}\"}" > /dev/null
log_msg "${SERVICE} health level: ${PERCENT}% (${CURRENT}/${TOTAL}), health trend: ${DIFF}" no_redis
# Return 10 to indicate a dead service
[ ${CURRENT} -le 0 ] && return 10
}
log_msg() {
if [[ ${2} != "no_valkey" ]]; then
${VALKEY_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \
if [[ ${2} != "no_redis" ]]; then
${REDIS_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \
tr '\r\n%&;$"_[]{}-' ' ')\"}" > /dev/null
fi
echo $(date) $(printf '%s\n' "${1}")
@@ -114,10 +109,10 @@ function notify_error() {
# If exists, mail will be throttled by argument in seconds
[[ ! -z ${3} ]] && THROTTLE=${3}
if [[ ! -z ${THROTTLE} ]]; then
TTL_LEFT="$(${VALKEY_CMDLINE} TTL THROTTLE_${1} 2> /dev/null)"
TTL_LEFT="$(${REDIS_CMDLINE} TTL THROTTLE_${1} 2> /dev/null)"
if [[ "${TTL_LEFT}" == "-2" ]]; then
# Delay key not found, setting a delay key now
${VALKEY_CMDLINE} SET THROTTLE_${1} 1 EX ${THROTTLE}
${REDIS_CMDLINE} SET THROTTLE_${1} 1 EX ${THROTTLE}
else
log_msg "Not sending notification email now, blocked for ${TTL_LEFT} seconds..."
return 1
@@ -302,7 +297,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/mailcow/check_dns.sh -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
/usr/lib/nagios/plugins/check_dns -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
DNSSEC=$(dig com +dnssec | egrep 'flags:.+ad')
if [[ -z ${DNSSEC} ]]; then
echo "DNSSEC failure" 2>> /tmp/unbound-mailcow 1>&2
@@ -324,21 +319,21 @@ unbound_checks() {
return 1
}
valkey_checks() {
# A check for the local valkey container
redis_checks() {
# A check for the local redis container
err_count=0
diff_c=0
THRESHOLD=${VALKEY_THRESHOLD}
THRESHOLD=${REDIS_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/valkey-mailcow; echo "$(tail -50 /tmp/valkey-mailcow)" > /tmp/valkey-mailcow
host_ip=$(get_container_ip valkey-mailcow)
touch /tmp/redis-mailcow; echo "$(tail -50 /tmp/redis-mailcow)" > /tmp/redis-mailcow
host_ip=$(get_container_ip redis-mailcow)
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_tcp -4 -H valkey-mailcow -p 6379 -E -s "AUTH ${VALKEYPASS}\nPING\n" -q "QUIT" -e "PONG" 2>> /tmp/valkey-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
/usr/lib/nagios/plugins/check_tcp -4 -H redis-mailcow -p 6379 -E -s "AUTH ${REDISPASS}\nPING\n" -q "QUIT" -e "PONG" 2>> /tmp/redis-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Valkey" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
progress "Redis" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
@@ -450,31 +445,6 @@ 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
@@ -533,12 +503,12 @@ dovecot_repl_checks() {
err_count=0
diff_c=0
THRESHOLD=${DOVECOT_REPL_THRESHOLD}
D_REPL_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning -r GET DOVECOT_REPL_HEALTH)
D_REPL_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning -r GET DOVECOT_REPL_HEALTH)
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count}
D_REPL_STATUS=$(redis-cli --raw -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET DOVECOT_REPL_HEALTH)
D_REPL_STATUS=$(redis-cli --raw -h redis -a ${REDISPASS} --no-auth-warning GET DOVECOT_REPL_HEALTH)
if [[ "${D_REPL_STATUS}" != "1" ]]; then
err_count=$(( ${err_count} + 1 ))
fi
@@ -608,19 +578,19 @@ ratelimit_checks() {
err_count=0
diff_c=0
THRESHOLD=${RATELIMIT_THRESHOLD}
RL_LOG_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid)
RL_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid)
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count}
RL_LOG_STATUS_PREV=${RL_LOG_STATUS}
RL_LOG_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid)
RL_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid)
if [[ ${RL_LOG_STATUS_PREV} != ${RL_LOG_STATUS} ]]; then
err_count=$(( ${err_count} + 1 ))
echo 'Last 10 applied ratelimits (may overlap with previous reports).' > /tmp/ratelimit
echo 'Full ratelimit buckets can be emptied by deleting the ratelimit hash from within mailcow UI (see /debug -> Protocols -> Ratelimit):' >> /tmp/ratelimit
echo >> /tmp/ratelimit
redis-cli --raw -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning LRANGE RL_LOG 0 10 | jq . >> /tmp/ratelimit
redis-cli --raw -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 10 | jq . >> /tmp/ratelimit
fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
@@ -669,20 +639,20 @@ fail2ban_checks() {
err_count=0
diff_c=0
THRESHOLD=${FAIL2BAN_THRESHOLD}
F2B_LOG_STATUS=($(${VALKEY_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS))
F2B_LOG_STATUS=($(${REDIS_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS))
F2B_RES=
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count}
F2B_LOG_STATUS_PREV=(${F2B_LOG_STATUS[@]})
F2B_LOG_STATUS=($(${VALKEY_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS))
F2B_LOG_STATUS=($(${REDIS_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS))
array_diff F2B_RES F2B_LOG_STATUS F2B_LOG_STATUS_PREV
if [[ ! -z "${F2B_RES}" ]]; then
err_count=$(( ${err_count} + 1 ))
echo -n "${F2B_RES[@]}" | tr -cd "[a-fA-F0-9.:/] " | timeout 3s ${VALKEY_CMDLINE} -x SET F2B_RES > /dev/null
echo -n "${F2B_RES[@]}" | tr -cd "[a-fA-F0-9.:/] " | timeout 3s ${REDIS_CMDLINE} -x SET F2B_RES > /dev/null
if [ $? -ne 0 ]; then
${VALKEY_CMDLINE} -x DEL F2B_RES
${REDIS_CMDLINE} -x DEL F2B_RES
fi
fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
@@ -703,9 +673,9 @@ acme_checks() {
err_count=0
diff_c=0
THRESHOLD=${ACME_THRESHOLD}
ACME_LOG_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET ACME_FAIL_TIME)
ACME_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning GET ACME_FAIL_TIME)
if [[ -z "${ACME_LOG_STATUS}" ]]; then
${VALKEY_CMDLINE} SET ACME_FAIL_TIME 0
${REDIS_CMDLINE} SET ACME_FAIL_TIME 0
ACME_LOG_STATUS=0
fi
# Reduce error count by 2 after restarting an unhealthy container
@@ -715,7 +685,7 @@ acme_checks() {
ACME_LOG_STATUS_PREV=${ACME_LOG_STATUS}
ACME_LC=0
until [[ ! -z ${ACME_LOG_STATUS} ]] || [ ${ACME_LC} -ge 3 ]; do
ACME_LOG_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET ACME_FAIL_TIME 2> /dev/null)
ACME_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning GET ACME_FAIL_TIME 2> /dev/null)
sleep 3
ACME_LC=$((ACME_LC+1))
done
@@ -864,14 +834,14 @@ BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! valkey_checks; then
log_msg "Local Valkey hit error limit"
echo valkey-mailcow > /tmp/com_pipe
if ! redis_checks; then
log_msg "Local Redis hit error limit"
echo redis-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned valkey_checks with PID ${PID}"
echo "Spawned redis_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
@@ -952,18 +922,6 @@ 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
@@ -1129,9 +1087,9 @@ while true; do
# Define $2 to override message text, else print service was restarted at ...
notify_error "${com_pipe_answer}" "Please check acme-mailcow for further information."
elif [[ ${com_pipe_answer} == "fail2ban" ]]; then
F2B_RES=($(timeout 4s ${VALKEY_CMDLINE} --raw GET F2B_RES 2> /dev/null))
F2B_RES=($(timeout 4s ${REDIS_CMDLINE} --raw GET F2B_RES 2> /dev/null))
if [[ ! -z "${F2B_RES}" ]]; then
${VALKEY_CMDLINE} DEL F2B_RES > /dev/null
${REDIS_CMDLINE} DEL F2B_RES > /dev/null
host=
for host in "${F2B_RES[@]}"; do
log_msg "Banned ${host}"

View File

@@ -23,16 +23,16 @@ if (file_exists('../../../web/inc/vars.local.inc.php')) {
require_once '../../../web/inc/lib/vendor/autoload.php';
// Init Valkey
$valkey = new Redis();
// Init Redis
$redis = new Redis();
try {
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
}
else {
$valkey->connect('valkey-mailcow', 6379);
$redis->connect('redis-mailcow', 6379);
}
$valkey->auth(getenv("VALKEYPASS"));
$redis->auth(getenv("REDISPASS"));
}
catch (Exception $e) {
error_log("MAILCOWAUTH: " . $e . PHP_EOL);
@@ -86,7 +86,7 @@ if ($result === false){
'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']);
error_log('MAILCOWAUTH: App auth for user ' . $post['username']);
set_sasl_log($post['username'], $post['real_rip'], $post['service']);
}
}
@@ -94,9 +94,9 @@ 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']));
$result = user_login($post['username'], $post['password'], array('is_internal' => true));
if ($result) {
error_log('MAILCOWAUTH: User auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
error_log('MAILCOWAUTH: User auth for user ' . $post['username']);
set_sasl_log($post['username'], $post['real_rip'], $post['service']);
}
}
@@ -105,7 +105,7 @@ 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']);
error_log("MAILCOWAUTH: Login failed for user " . $post['username']);
http_response_code(401); // Unauthorized
}

View File

@@ -1,4 +1,5 @@
function auth_password_verify(request, password)
request.domain = request.auth_user:match("@(.+)") or nil
if request.domain == nil then
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
end
@@ -9,10 +10,10 @@ function auth_password_verify(request, password)
https.TIMEOUT = 30
local req = {
username = request.user,
username = request.auth_user,
password = password,
real_rip = request.real_rip,
service = request.service
real_rip = request.remote_ip,
service = request.protocol
}
local req_json = json.encode(req)
local res = {}
@@ -33,7 +34,6 @@ function auth_password_verify(request, password)
-- 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
@@ -46,7 +46,7 @@ function auth_password_verify(request, password)
end
if response_json.success == true then
return dovecot.auth.PASSDB_RESULT_OK, ""
return dovecot.auth.PASSDB_RESULT_OK, { msg = "" }
end
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
@@ -55,3 +55,7 @@ end
function auth_passdb_lookup(req)
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
end
function auth_passdb_get_cache_key()
return "%{protocol}:%{user | username}\t:%{password}"
end

View File

@@ -0,0 +1,3 @@
# /etc/dovecot/conf.d/05-core.conf
# Core, single-line settings that don't fit elsewhere.
recipient_delimiter = +

View File

@@ -0,0 +1,13 @@
# /etc/dovecot/conf.d/10-logging.conf
# Logging and debug.
#mail_debug = yes
#auth_debug = yes
#log_debug = category=fts-flatcurve
log_path = syslog
log_timestamp = "%Y-%m-%d %H:%M:%S "
login_log_format_elements = "user=<%{user}> method=%{mechanism} rip=%{remote_ip} lip=%{local_ip} mpid=%{mail_pid} %{secured} session=<%{session}>"
# Mail event logging.
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
mail_log_fields = uid box msgid size
mail_log_cached_only = yes

View File

@@ -0,0 +1,10 @@
# /etc/dovecot/conf.d/10-mail.conf
# Mail storage paths and core mail settings.
mail_home = /var/vmail/%{user | domain }/%{user | username }
mail_driver = maildir
mail_path = ~/Maildir
mail_index_path = /var/vmail_index/%{user}
mail_plugins = </etc/dovecot/mail_plugins
mail_shared_explicit_inbox = yes
mailbox_list_storage_escape_char = "\\"
mail_prefetch_count = 30

View File

@@ -0,0 +1,13 @@
# /etc/dovecot/conf.d/10-ssl.conf
# TLS/SSL settings.
ssl_min_protocol = TLSv1.2
ssl_cipher_list = ALL:!ADH:!LOW:!SSLv2:!SSLv3:!EXP:!aNULL:!eNULL:!3DES:!MD5:!PSK:!DSS:!RC4:!SEED:!IDEA:+HIGH:+MEDIUM
ssl_options = no_ticket
#ssl_dh_parameters_length = 2048
ssl_server {
prefer_ciphers = server
dh_file = /etc/ssl/mail/dhparams.pem
cert_file = /etc/ssl/mail/cert.pem
key_file = /etc/ssl/mail/key.pem
}

View File

@@ -0,0 +1,3 @@
# /etc/dovecot/conf.d/11-sql.conf
# Default SQL driver used by SQL-based dicts/userdb.
sql_driver = mysql

View File

@@ -0,0 +1,8 @@
# Autogenerated by mailcow - DO NOT TOUCH!
mysql /var/run/mysqld/mysqld.sock {
dbname=mailcow
user=mailcow
password=D8O9BIivJc7Pb2VCfpAeLbAzUOZ0
ssl = no
}

View File

@@ -0,0 +1,7 @@
# /etc/dovecot/conf.d/12-storage-attachments.conf
# External attachment storage.
fs mail_ext_attachment {
fs_driver = posix
mail_ext_attachment_path = /var/attachments
mail_ext_attachment_min_size = 128k
}

View File

@@ -0,0 +1,10 @@
# /etc/dovecot/conf.d/15-performance.conf
# Performance and mailbox tuning.
# Enable only when you do not manually touch cur/.
maildir_very_dirty_syncs = yes
# NFS examples | Only modify if using NFS!:
#mm ap_disable = yes
#mail_fsync = always
#mail_nfs_index = yes
#mail_nfs_storage = yes

View File

@@ -0,0 +1,40 @@
# /etc/dovecot/conf.d/20-auth.conf
# Authentication mechanisms, master/user separation, passdb chain, auth cache.
auth_mechanisms = plain login
auth_allow_cleartext = yes
auth_master_user_separator = *
auth_cache_verify_password_with_worker = yes
auth_cache_negative_ttl = 60s
auth_cache_ttl = 300s
auth_cache_size = 10M
auth_verbose_passwords = sha1:6
# 1) Lua password verification (blocking, return mapping).
passdb lua {
driver = lua
lua_file = /etc/dovecot/auth/passwd-verify.lua
lua_settings {
blocking=yes
result_success = return-ok
result_failure = continue
result_internalfail = continue
}
}
# 2) Master password for master user logins.
passdb master {
driver = passwd-file
passwd_file_path = /etc/dovecot/dovecot-master.passwd
master = yes
skip = authenticated
}
# 3) Mandatory return layer: empty Lua (e.g. for forced reset).
passdb empty-lua {
driver = lua
lua_file = /etc/dovecot/auth/passwd-verify.lua
lua_settings {
blocking = yes
}
}

View File

@@ -0,0 +1,11 @@
# /etc/dovecot/conf.d/20-userdb.conf
# User database chain.
userdb passwd {
driver = passwd-file
passwd_file_path = /etc/dovecot/dovecot-master.userdb
}
userdb sql {
!include /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
skip = found
}

View File

@@ -0,0 +1,144 @@
# /etc/dovecot/conf.d/25-services.conf
# All service listeners and workers.
# doveadm remote admin
# Set doveadm_password in extra.conf.
service doveadm {
inet_listener doveadm {
port = 12345
}
vsz_limit = 2048 MB
}
# dict
service dict {
unix_listener dict {
mode = 0660
user = vmail
group = vmail
}
}
# log
service log {
user = dovenull
}
# config socket
service config {
unix_listener config {
user = root
group = vmail
mode = 0660
}
}
# anvil socket
service anvil {
unix_listener anvil {
user = vmail
group = vmail
mode = 0660
}
}
# auth sockets and inet
service auth {
inet_listener auth-inet {
port = 10001
}
unix_listener auth-master {
mode = 0600
user = vmail
}
unix_listener auth-userdb {
mode = 0600
user = vmail
}
vsz_limit = 2G
}
# managesieve login
service managesieve-login {
inet_listener sieve {
port = 4190
}
inet_listener sieve_haproxy {
port = 14190
haproxy = yes
}
service_restart_request_count = 1
process_min_avail = 2
vsz_limit = 1G
}
# imap login
service imap-login {
service_restart_request_count = 1
process_min_avail = 2
process_limit = 10000
vsz_limit = 1G
user = dovenull
inet_listener imap_haproxy {
port = 10143
haproxy = yes
}
inet_listener imaps_haproxy {
port = 10993
ssl = yes
haproxy = yes
}
}
# pop3 login
service pop3-login {
service_restart_request_count = 1
process_min_avail = 1
vsz_limit = 1G
inet_listener pop3_haproxy {
port = 10110
haproxy = yes
}
inet_listener pop3s_haproxy {
port = 10995
ssl = yes
haproxy = yes
}
}
# imap worker
service imap {
executable = imap
user = vmail
vsz_limit = 1G
}
# managesieve worker
service managesieve {
process_limit = 256
}
# lmtp
service lmtp {
inet_listener lmtp-inet {
port = 24
}
user = vmail
}
# quota warning hook
service quota-warning {
executable = script /usr/local/bin/quota_notify.py
user = vmail
unix_listener quota-warning {
user = vmail
}
}
# stats
service stats {
unix_listener stats-writer {
mode = 0660
user = vmail
}
}

View File

@@ -0,0 +1,17 @@
# /etc/dovecot/conf.d/30-protocols.conf
# IMAP protocol specifics.
protocol imap {
mail_plugins = </etc/dovecot/mail_plugins_imap
imap_metadata = yes
}
# LMTP protocol specifics.
protocol lmtp {
mail_plugins = </etc/dovecot/mail_plugins_lmtp
auth_socket_path = /var/run/dovecot/auth-master
}
# ManageSieve protocol specifics.
protocol sieve {
managesieve_logout_format = bytes=%i/%o
}

View File

@@ -0,0 +1,45 @@
# mailcow FTS Flatcurve Settings, change them as you like.
# Maximum term length can be set via the 'maxlen' argument (maxlen is
# specified in bytes, not number of UTF-8 characters)
language_tokenizer_address_token_maxlen = 100
language_tokenizer_generic_algorithm = simple
language_tokenizer_generic_token_maxlen = 30
# These are not flatcurve settings, but required for Dovecot FTS. See
# Dovecot FTS Configuration link above for further information.
language en {
default = yes
language_filters = lowercase snowball english-possessive stopwords
}
language de {
language_filters = lowercase snowball stopwords
}
language es {
language_filters = lowercase snowball stopwords
}
language_tokenizers = generic email-address
fts_search_timeout = 300s
fts_autoindex = yes
# Tweak this setting if you only want to ensure big and frequent folders are indexed, not all.
fts_autoindex_max_recent_msgs = 20
fts flatcurve {
substring_search = no
}
### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###
service indexer-worker {
# Max amount of simultaniously running indexer jobs.
process_limit=1
# Max amount of RAM used by EACH indexer process.
vsz_limit=128 MB
}
### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###

View File

@@ -0,0 +1,12 @@
# /etc/dovecot/conf.d/40-acl.conf
# ACL and shared mailboxes.
imap_acl_allow_anyone = </etc/dovecot/acl_anyone
acl_sharing_map {
dict file {
path = /var/vmail/shared-mailboxes.db
}
}
acl_driver = vfile
acl_user = %{user}

View File

@@ -0,0 +1,7 @@
# /etc/dovecot/conf.d/40-attributes.conf
# User/mail attributes.
mail_attribute {
dict file {
path = /etc/dovecot/dovecot-attributes
}
}

View File

@@ -0,0 +1,25 @@
# /etc/dovecot/conf.d/50-quota.conf
# Quota configuration and notifications.
quota "User quota" {
driver = count
warning warn-95 {
quota_storage_percentage = 95
execute quota-warning {
args = 95 %{user}
}
}
warning warn-80 {
quota_storage_percentage = 80
execute quota-warning {
args = 80 %{user}
}
}
}
quota_clone {
dict proxy {
name = mysql_quota
}
}

View File

@@ -0,0 +1,97 @@
# /etc/dovecot/conf.d/60-sieve-pipeline.conf
# Complete Sieve pipeline: personal/global scripts, plugins, limits, training.
# Global before/after (file and dict)
sieve_script before {
type = before
driver = file
path = /var/vmail/sieve/global_sieve_before.sieve
}
sieve_script before2 {
type = before
driver = dict
name = active
dict proxy {
name = sieve_before
}
bin_path = /var/vmail/sieve_before_bindir/%{user}
}
sieve_script after {
type = after
driver = file
path = /var/vmail/sieve/global_sieve_after.sieve
}
sieve_script after2 {
type = after
driver = dict
name = active
dict proxy {
name = sieve_after
}
bin_path = /var/vmail/sieve_after_bindir/%{user}
}
# Personal scripts
sieve_script personal {
type = personal
driver = file
path = ~/sieve
active_path = ~/.dovecot.sieve
}
# Plugins and behavior
sieve_plugins = sieve_imapsieve sieve_extprograms
sieve_vacation_send_from_recipient = yes
sieve_redirect_envelope_from = recipient
# IMAPSieve training
imapsieve_from Junk {
sieve_script ham {
type = before
cause = copy
path = /usr/lib/dovecot/sieve/report-ham.sieve
}
}
mailbox Junk {
sieve_script spam {
type = before
cause = copy
path = /usr/lib/dovecot/sieve/report-spam.sieve
}
}
# Extprograms and extensions
sieve_pipe_bin_dir = /usr/lib/dovecot/sieve
sieve_plugins {
sieve_extprograms = yes
}
sieve_global_extensions {
vnd.dovecot.pipe = yes
vnd.dovecot.execute = yes
}
# Limits and duplicate handling
sieve_max_script_size = 1M
sieve_max_redirects = 100
sieve_max_actions = 101
sieve_quota_script_count = 0
sieve_quota_storage_size = 0
sieve_vacation_min_period = 5s
sieve_vacation_max_period = 365d
sieve_vacation_default_period = 60s
sieve_duplicate_default_period = 1m
sieve_duplicate_max_period = 7d
sieve_extensions {
vacation-seconds = yes
editheader = yes
}
# pipe sockets in /var/run/dovecot/sieve-pipe
sieve_pipe_socket_dir = sieve-pipe
# execute sockets in /var/run/dovecot/sieve-execute
sieve_execute_socket_dir = sieve-execute

View File

@@ -0,0 +1,6 @@
# /etc/dovecot/conf.d/70-crypto.conf
# Global mail-crypt keys.
crypt_global_private_key global {
crypt_private_key_file = /mail_crypt/ecprivkey.pem
}
crypt_global_public_key_file = /mail_crypt/ecpubkey.pem

View File

@@ -0,0 +1,3 @@
# /etc/dovecot/conf.d/80-compress.conf
# Compression settings.
mail_compress_write_method = lz4

View File

@@ -0,0 +1,18 @@
# /etc/dovecot/conf.d/90-dict.conf
# Dict declarations and SQL bindings.
dict_server {
dict sieve_after {
driver = sql
!include /etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
}
dict sieve_before {
driver = sql
!include /etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
}
dict mysql_quota {
driver = sql
!include /etc/dovecot/sql/dovecot-dict-sql-quota.conf
}
}

View File

@@ -0,0 +1,7 @@
# /etc/dovecot/conf.d/90-limits.conf
# Connection and memory limits; doveadm port.
mail_max_userip_connections = 500
imap_max_line_length = 2 M
default_client_limit = 10400
default_vsz_limit = 1024 M
doveadm_port = 12345

View File

@@ -0,0 +1,22 @@
# /etc/dovecot/conf.d/99-includes.conf
# Late includes and site-specific bits.
# Mailbox layout includes (if used)
!include /etc/dovecot/dovecot.folders.conf
# Optional replication
!include_try /etc/dovecot/mail_replica.conf
# Existing includes you already had
!include_try /etc/dovecot/sni.conf
!include_try /etc/dovecot/sogo_trusted_ip.conf
!include_try /etc/dovecot/shared_namespace.conf
!include_try /etc/dovecot/conf.d/fts.conf
# Remote auth override
remote 127.0.0.1 {
auth_allow_cleartext = yes
}
# Outbound submission target
submission_host = postfix:588

View File

@@ -1,37 +0,0 @@
# mailcow FTS Flatcurve Settings, change them as you like.
plugin {
fts_autoindex = yes
fts_autoindex_exclude = \Junk
fts_autoindex_exclude2 = \Trash
# Tweak this setting if you only want to ensure big and frequent folders are indexed, not all.
fts_autoindex_max_recent_msgs = 20
fts = flatcurve
# Maximum term length can be set via the 'maxlen' argument (maxlen is
# specified in bytes, not number of UTF-8 characters)
fts_tokenizer_email_address = maxlen=100
fts_tokenizer_generic = algorithm=simple maxlen=30
# These are not flatcurve settings, but required for Dovecot FTS. See
# Dovecot FTS Configuration link above for further information.
fts_languages = en es de
fts_tokenizers = generic email-address
# OPTIONAL: Recommended default FTS core configuration
fts_filters = normalizer-icu snowball stopwords
fts_filters_en = lowercase snowball english-possessive stopwords
fts_index_timeout = 300s
}
### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###
service indexer-worker {
# Max amount of simultaniously running indexer jobs.
process_limit=1
# Max amount of RAM used by EACH indexer process.
vsz_limit=128 MB
}
### THIS PART WILL BE CHANGED BY MODIFYING mailcow.conf AUTOMATICALLY DURING RUNTIME! ###

View File

@@ -1,311 +1,34 @@
# --------------------------------------------------------------------------
# Please create a file "extra.conf" for persistent overrides to dovecot.conf
# --------------------------------------------------------------------------
# LDAP example:
#passdb {
# args = /etc/dovecot/ldap/passdb.conf
# driver = ldap
#}
# /etc/dovecot/dovecot.conf
# Base file kept minimal. All real config lives under conf.d/.
dovecot_config_version = 2.4.0
dovecot_storage_version = 2.4.0
auth_mechanisms = plain login
#mail_debug = yes
#auth_debug = yes
#log_debug = category=fts-flatcurve # Activate Logging for Flatcurve FTS Searchings
log_path = syslog
disable_plaintext_auth = yes
# Uncomment on NFS share
#mmap_disable = yes
#mail_fsync = always
#mail_nfs_index = yes
#mail_nfs_storage = yes
login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k"
mail_home = /var/vmail/%d/%n
mail_location = maildir:~/
mail_plugins = </etc/dovecot/mail_plugins
mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
mail_attachment_dir = /var/attachments
mail_attachment_min_size = 128k
# Significantly speeds up very large mailboxes, but is only safe to enable if
# you do not manually modify the files in the `cur` directories in
# mailcowdockerized_vmail-vol-1.
# https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-performance/
maildir_very_dirty_syncs = yes
# Dovecot 2.2
#ssl_protocols = !SSLv3
# Dovecot 2.3
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes
ssl_cipher_list = ALL:!ADH:!LOW:!SSLv2:!SSLv3:!EXP:!aNULL:!eNULL:!3DES:!MD5:!PSK:!DSS:!RC4:!SEED:!IDEA:+HIGH:+MEDIUM
# Default in Dovecot 2.3
ssl_options = no_compression no_ticket
# New in Dovecot 2.3
ssl_dh = </etc/ssl/mail/dhparams.pem
# Dovecot 2.2
#ssl_dh_parameters_length = 2048
log_timestamp = "%Y-%m-%d %H:%M:%S "
recipient_delimiter = +
auth_master_user_separator = *
mail_shared_explicit_inbox = yes
mail_prefetch_count = 30
passdb {
driver = lua
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes cache_key=%s:%u:%w
result_success = return-ok
result_failure = continue
result_internalfail = continue
}
# try a master passwd
passdb {
driver = passwd-file
args = /etc/dovecot/dovecot-master.passwd
master = yes
skip = authenticated
}
# check for regular password - if empty (e.g. force-passwd-reset), previous pass=yes passdbs also fail
# a return of the following passdb is mandatory
passdb {
driver = lua
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes
}
# Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
service doveadm {
inet_listener {
port = 12345
}
vsz_limit=2048 MB
}
!include /etc/dovecot/dovecot.folders.conf
protocols = imap sieve lmtp pop3
service dict {
unix_listener dict {
mode = 0660
user = vmail
group = vmail
}
}
service log {
user = dovenull
}
service config {
unix_listener config {
user = root
group = vmail
mode = 0660
}
}
service auth {
inet_listener auth-inet {
port = 10001
}
unix_listener auth-master {
mode = 0600
user = vmail
}
unix_listener auth-userdb {
mode = 0600
user = vmail
}
vsz_limit = 2G
}
service managesieve-login {
inet_listener sieve {
port = 4190
}
inet_listener sieve_haproxy {
port = 14190
haproxy = yes
}
service_count = 1
process_min_avail = 2
vsz_limit = 1G
}
service imap-login {
service_count = 1
process_min_avail = 2
process_limit = 10000
vsz_limit = 1G
user = dovenull
inet_listener imap_haproxy {
port = 10143
haproxy = yes
}
inet_listener imaps_haproxy {
port = 10993
ssl = yes
haproxy = yes
}
}
service pop3-login {
service_count = 1
process_min_avail = 1
vsz_limit = 1G
inet_listener pop3_haproxy {
port = 10110
haproxy = yes
}
inet_listener pop3s_haproxy {
port = 10995
ssl = yes
haproxy = yes
}
}
service imap {
executable = imap
user = vmail
vsz_limit = 1G
}
service managesieve {
process_limit = 256
}
service lmtp {
inet_listener lmtp-inet {
port = 24
}
user = vmail
}
listen = *,[::]
ssl_cert = </etc/ssl/mail/cert.pem
ssl_key = </etc/ssl/mail/key.pem
userdb {
driver = passwd-file
args = /etc/dovecot/dovecot-master.userdb
}
userdb {
args = /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
driver = sql
skip = found
}
protocol imap {
mail_plugins = </etc/dovecot/mail_plugins_imap
imap_metadata = yes
}
mail_attribute_dict = file:%h/dovecot-attributes
protocol lmtp {
mail_plugins = </etc/dovecot/mail_plugins_lmtp
auth_socket_path = /var/run/dovecot/auth-master
}
protocol sieve {
managesieve_logout_format = bytes=%i/%o
}
plugin {
# Allow "any" or "authenticated" to be used in ACLs
acl_anyone = </etc/dovecot/acl_anyone
acl_shared_dict = file:/var/vmail/shared-mailboxes.db
acl = vfile
acl_user = %u
quota = dict:Userquota::proxy::sqlquota
quota_rule2 = Trash:storage=+100%%
sieve = /var/vmail/sieve/%u.sieve
sieve_plugins = sieve_imapsieve sieve_extprograms
sieve_vacation_send_from_recipient = yes
sieve_redirect_envelope_from = recipient
# From elsewhere to Spam folder
imapsieve_mailbox1_name = Junk
imapsieve_mailbox1_causes = COPY
imapsieve_mailbox1_before = file:/usr/lib/dovecot/sieve/report-spam.sieve
# END
# From Spam folder to elsewhere
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = Junk
imapsieve_mailbox2_causes = COPY
imapsieve_mailbox2_before = file:/usr/lib/dovecot/sieve/report-ham.sieve
# END
master_user = %u
quota_warning = storage=95%% quota-warning 95 %u
quota_warning2 = storage=80%% quota-warning 80 %u
sieve_pipe_bin_dir = /usr/lib/dovecot/sieve
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.execute
sieve_extensions = +notify +imapflags +vacation-seconds +editheader
sieve_max_script_size = 1M
sieve_max_redirects = 100
sieve_max_actions = 101
sieve_quota_max_scripts = 0
sieve_quota_max_storage = 0
listescape_char = "\\"
sieve_vacation_min_period = 5s
sieve_vacation_max_period = 0
sieve_vacation_default_period = 60s
sieve_before = /var/vmail/sieve/global_sieve_before.sieve
sieve_before2 = dict:proxy::sieve_before;name=active;bindir=/var/vmail/sieve_before_bindir
sieve_after = dict:proxy::sieve_after;name=active;bindir=/var/vmail/sieve_after_bindir
sieve_after2 = /var/vmail/sieve/global_sieve_after.sieve
sieve_duplicate_default_period = 1m
sieve_duplicate_max_period = 7d
protocols = imap sieve lmtp pop3
# -- Global keys
mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem
mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem
mail_crypt_save_version = 2
!include_try /etc/dovecot/conf.d/05-core.conf
!include_try /etc/dovecot/conf.d/10-logging.conf
!include_try /etc/dovecot/conf.d/10-mail.conf
!include_try /etc/dovecot/conf.d/10-ssl.conf
!include_try /etc/dovecot/conf.d/11-sql.conf
!include_try /etc/dovecot/conf.d/12-mysql.conf
!include_try /etc/dovecot/conf.d/12-storage-attachments.conf
!include_try /etc/dovecot/conf.d/15-performance.conf
!include_try /etc/dovecot/conf.d/20-auth.conf
!include_try /etc/dovecot/conf.d/20-userdb.conf
!include_try /etc/dovecot/conf.d/25-services.conf
!include_try /etc/dovecot/conf.d/30-protocols.conf
!include_try /etc/dovecot/conf.d/35-fts.conf
!include_try /etc/dovecot/conf.d/40-acl.conf
!include_try /etc/dovecot/conf.d/40-attributes.conf
!include_try /etc/dovecot/conf.d/50-quota.conf
!include_try /etc/dovecot/conf.d/60-sieve-pipeline.conf
!include_try /etc/dovecot/conf.d/70-crypto.conf
!include_try /etc/dovecot/conf.d/80-compress.conf
!include_try /etc/dovecot/conf.d/80-mail-logging.conf
!include_try /etc/dovecot/conf.d/90-limits.conf
!include_try /etc/dovecot/conf.d/90-dict.conf
!include_try /etc/dovecot/conf.d/99-includes.conf
# Enable compression while saving, lz4 Dovecot v2.3.17+
zlib_save = lz4
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
mail_log_fields = uid box msgid size
mail_log_cached_only = yes
# Try set mail_replica
!include_try /etc/dovecot/mail_replica.conf
}
service quota-warning {
executable = script /usr/local/bin/quota_notify.py
# use some unprivileged user for executing the quota warnings
user = vmail
unix_listener quota-warning {
user = vmail
}
}
dict {
sqlquota = mysql:/etc/dovecot/sql/dovecot-dict-sql-quota.conf
sieve_after = mysql:/etc/dovecot/sql/dovecot-dict-sql-sieve_after.conf
sieve_before = mysql:/etc/dovecot/sql/dovecot-dict-sql-sieve_before.conf
}
remote 127.0.0.1 {
disable_plaintext_auth = no
}
submission_host = postfix:588
mail_max_userip_connections = 500
service stats {
unix_listener stats-writer {
mode = 0660
user = vmail
}
}
imap_max_line_length = 2 M
auth_cache_verify_password_with_worker = yes
auth_cache_negative_ttl = 60s
auth_cache_ttl = 300s
auth_cache_size = 10M
auth_verbose_passwords = sha1:6
service replicator {
process_min_avail = 1
}
service aggregator {
fifo_listener replication-notify-fifo {
user = vmail
}
unix_listener replication-notify {
user = vmail
}
}
service replicator {
unix_listener replicator-doveadm {
mode = 0666
}
}
replication_max_conns = 10
doveadm_port = 12345
replication_dsync_parameters = -d -l 30 -U -n INBOX
# <Includes>
!include_try /etc/dovecot/sni.conf
!include_try /etc/dovecot/sogo_trusted_ip.conf
!include_try /etc/dovecot/extra.conf
!include_try /etc/dovecot/shared_namespace.conf
!include_try /etc/dovecot/conf.d/fts.conf
# </Includes>
default_client_limit = 10400
default_vsz_limit = 1024 M
# Last: local overrides
!include_try /etc/dovecot/extra.conf

View File

@@ -1,10 +1,14 @@
namespace inbox {
inbox = yes
location =
separator = /
mailbox storage/* {
quota_storage_extra = 100M
}
mailbox "Trash" {
auto = subscribe
special_use = \Trash
quota_storage_percentage = 100
fts_autoindex = no
}
mailbox "Deleted Messages" {
special_use = \Trash
@@ -195,6 +199,7 @@ namespace inbox {
mailbox "Junk" {
auto = subscribe
special_use = \Junk
fts_autoindex = no
}
mailbox "Junk-E-Mail" {
special_use = \Junk

View File

@@ -78,7 +78,7 @@ http {
{%endif%}
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
{% if ENABLE_IPV6 %}
{% if not DISABLE_IPv6 %}
{% if not HTTP_REDIRECT %}
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
{%endif%}
@@ -105,7 +105,7 @@ http {
{%endif%}
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
{% if ENABLE_IPV6 %}
{% if not DISABLE_IPv6 %}
{% if not HTTP_REDIRECT %}
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
{%endif%}
@@ -126,7 +126,7 @@ http {
# rspamd dynmaps:
server {
listen 8081;
{% if ENABLE_IPV6 %}
{% if not DISABLE_IPv6 %}
listen [::]:8081;
{%endif%}
index index.php index.html;
@@ -199,7 +199,7 @@ http {
{%endif%}
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
{% if ENABLE_IPV6 %}
{% if not DISABLE_IPv6 %}
{% if not HTTP_REDIRECT %}
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
{%endif%}

View File

@@ -23,16 +23,16 @@ catch (PDOException $e) {
exit;
}
// Init Valkey
$valkey = new Redis();
// Init Redis
$redis = new Redis();
try {
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
}
else {
$valkey->connect('valkey-mailcow', 6379);
$redis->connect('redis-mailcow', 6379);
}
$valkey->auth(getenv("VALKEYPASS"));
$redis->auth(getenv("REDISPASS"));
}
catch (Exception $e) {
echo "Exiting: " . $e->getMessage();
@@ -41,7 +41,7 @@ catch (Exception $e) {
}
function logMsg($priority, $message, $task = "Keycloak Sync") {
global $valkey;
global $redis;
$finalMsg = array(
"time" => time(),
@@ -49,7 +49,7 @@ function logMsg($priority, $message, $task = "Keycloak Sync") {
"task" => $task,
"message" => $message
);
$valkey->lPush('CRON_LOG', json_encode($finalMsg));
$redis->lPush('CRON_LOG', json_encode($finalMsg));
}
// Load core functions first

View File

@@ -23,16 +23,16 @@ catch (PDOException $e) {
exit;
}
// Init Valkey
$valkey = new Redis();
// Init Redis
$redis = new Redis();
try {
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
}
else {
$valkey->connect('valkey-mailcow', 6379);
$redis->connect('redis-mailcow', 6379);
}
$valkey->auth(getenv("VALKEYPASS"));
$redis->auth(getenv("REDISPASS"));
}
catch (Exception $e) {
echo "Exiting: " . $e->getMessage();
@@ -41,7 +41,7 @@ catch (Exception $e) {
}
function logMsg($priority, $message, $task = "LDAP Sync") {
global $valkey;
global $redis;
$finalMsg = array(
"time" => time(),
@@ -49,7 +49,7 @@ function logMsg($priority, $message, $task = "LDAP Sync") {
"task" => $task,
"message" => $message
);
$valkey->lPush('CRON_LOG', json_encode($finalMsg));
$redis->lPush('CRON_LOG', json_encode($finalMsg));
}
// Load core functions first

View File

@@ -1,26 +1,13 @@
# Whitelist generated by Postwhite v3.4 on Wed Oct 1 00:21:33 UTC 2025
# Whitelist generated by Postwhite v3.4 on Fri Aug 1 00:24:14 UTC 2025
# https://github.com/stevejenkins/postwhite/
# 2216 total rules
# 2166 total rules
2a00:1450:4000::/36 permit
2a01:111:f400::/48 permit
2a01:111:f403:2800::/53 permit
2a01:111:f403:8000::/50 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
@@ -30,17 +17,16 @@
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
@@ -56,8 +42,7 @@
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.107.253.40 permit
13.110.208.0/21 permit
13.110.209.0/24 permit
13.110.216.0/22 permit
@@ -79,8 +64,6 @@
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
@@ -92,7 +75,9 @@
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
@@ -168,6 +153,7 @@
34.212.163.75 permit
34.215.104.144 permit
34.218.115.239 permit
34.218.116.3 permit
34.225.212.172 permit
34.241.242.183 permit
35.83.148.184 permit
@@ -176,7 +162,6 @@
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.205.92.9 permit
35.228.216.85 permit
@@ -193,7 +178,6 @@
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
@@ -204,10 +188,7 @@
44.246.68.102 permit
44.246.77.92 permit
45.14.148.0/22 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.19.170.16 permit
46.226.48.0/21 permit
46.228.36.37 permit
46.228.36.38/31 permit
@@ -343,7 +324,6 @@
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
@@ -663,11 +643,6 @@
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
@@ -683,17 +658,7 @@
82.165.159.45 permit
82.165.159.130 permit
82.165.159.131 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
@@ -1233,14 +1198,16 @@
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.122.78.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
@@ -1375,8 +1342,9 @@
108.174.6.215 permit
108.175.18.45 permit
108.175.30.45 permit
108.177.8.0/22 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
@@ -1540,7 +1508,7 @@
148.105.0.0/16 permit
148.105.8.0/21 permit
149.72.0.0/16 permit
149.72.234.184 permit
149.72.223.204 permit
149.72.248.236 permit
149.97.173.180 permit
150.230.98.160 permit
@@ -1596,7 +1564,6 @@
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
@@ -1614,7 +1581,6 @@
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
@@ -1654,10 +1620,10 @@
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.174.33 permit
169.148.175.3 permit
169.148.188.0/24 permit
169.148.188.182 permit
@@ -1666,7 +1632,11 @@
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
@@ -1701,9 +1671,6 @@
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
@@ -1765,7 +1732,6 @@
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
@@ -1792,8 +1758,6 @@
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
@@ -1993,6 +1957,8 @@
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
@@ -2056,8 +2022,6 @@
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
@@ -2074,30 +2038,16 @@
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
@@ -2217,5 +2167,4 @@
2620:119:50c0:207::/64 permit
2620:119:50c0:207::215 permit
2800:3f0:4000::/36 permit
49.12.4.251 permit # checks.mailcow.email
2a01:4f8:c17:7906::10 permit # checks.mailcow.email
194.25.134.0/24 permit # t-online.de

12
data/conf/redis/redis-conf.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
cat <<EOF > /redis.conf
requirepass $REDISPASS
user quota_notify on nopass ~QW_* -@all +get +hget +ping
EOF
if [ -n "$REDISMASTERPASS" ]; then
echo "masterauth $REDISMASTERPASS" >> /redis.conf
fi
exec redis-server /redis.conf

View File

@@ -22,10 +22,10 @@ catch (PDOException $e) {
exit;
}
// Init Valkey
$valkey = new Redis();
$valkey->connect('valkey-mailcow', 6379);
$valkey->auth(getenv("VALKEYPASS"));
// Init Redis
$redis = new Redis();
$redis->connect('redis-mailcow', 6379);
$redis->auth(getenv("REDISPASS"));
function parse_email($email) {
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
@@ -60,7 +60,7 @@ $rcpt_final_mailboxes = array();
// Skip if not a mailcow handled domain
try {
if (!$valkey->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
exit;
}
}
@@ -122,7 +122,7 @@ try {
}
else {
$parsed_goto = parse_email($goto);
if (!$valkey->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
error_log("ALIAS EXPANDER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
}
else {
@@ -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` = '1'");
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
$stmt->execute(array(':domain' => $parsed_goto['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) {

View File

@@ -2,9 +2,9 @@
header('Content-Type: text/plain');
ini_set('error_reporting', 0);
$valkey = new Redis();
$valkey->connect('valkey-mailcow', 6379);
$valkey->auth(getenv("VALKEYPASS"));
$redis = new Redis();
$redis->connect('redis-mailcow', 6379);
$redis->auth(getenv("REDISPASS"));
function in_net($addr, $net) {
$net = explode('/', $net);
@@ -31,7 +31,7 @@ function in_net($addr, $net) {
if (isset($_GET['host'])) {
try {
foreach ($valkey->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) {
foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) {
if (in_net($_GET['host'], $host)) {
echo '200 PERMIT';
exit;
@@ -46,7 +46,7 @@ if (isset($_GET['host'])) {
} else {
try {
echo '240.240.240.240' . PHP_EOL;
foreach ($valkey->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) {
foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) {
echo $host . PHP_EOL;
}
}

View File

@@ -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,36 +468,4 @@ 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
}
?>
}

View File

@@ -454,18 +454,12 @@ 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)
@@ -550,13 +544,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
@@ -564,7 +558,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
@@ -613,30 +607,26 @@ 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
-- 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)
local nct = string.format('%s: %s/%s; charset=utf-8',
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
out[#out + 1] = nct
-- update Content-Type header (include boundary if present)
-- update Content-Type header
task:set_milter_reply({
remove_headers = {['Content-Type'] = 0},
})
task:set_milter_reply({
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8%s',
rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)}
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8', rewrite.new_ct.type, rewrite.new_ct.subtype)}
})
return
elseif name:lower() == 'content-transfer-encoding' then
@@ -655,16 +645,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

View File

@@ -21,10 +21,10 @@ catch (PDOException $e) {
http_response_code(501);
exit;
}
// Init Valkey
$valkey = new Redis();
$valkey->connect('valkey-mailcow', 6379);
$valkey->auth(getenv("VALKEYPASS"));
// Init Redis
$redis = new Redis();
$redis->connect('redis-mailcow', 6379);
$redis->auth(getenv("REDISPASS"));
// Functions
function parse_email($email) {
@@ -74,16 +74,16 @@ if ($fuzzy == 'unknown') {
}
try {
$max_size = (int)$valkey->Get('Q_MAX_SIZE');
$max_size = (int)$redis->Get('Q_MAX_SIZE');
if (($max_size * 1048576) < $raw_size) {
error_log(sprintf("QUARANTINE: Message too large: %d b exceeds %d b", $raw_size, ($max_size * 1048576)) . PHP_EOL);
http_response_code(505);
exit;
}
if ($exclude_domains = $valkey->Get('Q_EXCLUDE_DOMAINS')) {
if ($exclude_domains = $redis->Get('Q_EXCLUDE_DOMAINS')) {
$exclude_domains = json_decode($exclude_domains, true);
}
$retention_size = (int)$valkey->Get('Q_RETENTION_SIZE');
$retention_size = (int)$redis->Get('Q_RETENTION_SIZE');
}
catch (RedisException $e) {
error_log("QUARANTINE: " . $e . PHP_EOL);
@@ -103,7 +103,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
// Skip if not a mailcow handled domain
try {
if (!$valkey->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
continue;
}
}
@@ -171,7 +171,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
}
else {
$parsed_goto = parse_email($goto);
if (!$valkey->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
}
else {
@@ -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` = '1'");
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
$stmt->execute(array(':domain' => $parsed_goto['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) {

View File

@@ -5,16 +5,16 @@ header('Content-Type: text/plain');
require_once "vars.inc.php";
// Do not show errors, we log to using error_log
ini_set('error_reporting', 0);
// Init Valkey
$valkey = new Redis();
// Init Redis
$redis = new Redis();
try {
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
}
else {
$valkey->connect('valkey-mailcow', 6379);
$redis->connect('redis-mailcow', 6379);
}
$valkey->auth(getenv("VALKEYPASS"));
$redis->auth(getenv("REDISPASS"));
}
catch (Exception $e) {
exit;
@@ -44,6 +44,6 @@ $data['message_id'] = $raw_data_decoded['message_id'];
$data['header_subject'] = implode(' ', $raw_data_decoded['header_subject']);
$data['header_from'] = implode(', ', $raw_data_decoded['header_from']);
$valkey->lpush('RL_LOG', json_encode($data));
$redis->lpush('RL_LOG', json_encode($data));
exit;

View File

@@ -21,10 +21,10 @@ catch (PDOException $e) {
http_response_code(501);
exit;
}
// Init Valkey
$valkey = new Redis();
$valkey->connect('valkey-mailcow', 6379);
$valkey->auth(getenv("VALKEYPASS"));
// Init Redis
$redis = new Redis();
$redis->connect('redis-mailcow', 6379);
$redis->auth(getenv("REDISPASS"));
// Functions
function parse_email($email) {
@@ -94,7 +94,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
// Skip if not a mailcow handled domain
try {
if (!$valkey->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
continue;
}
}
@@ -156,7 +156,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
}
else {
$parsed_goto = parse_email($goto);
if (!$valkey->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
}
else {
@@ -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` = '1'");
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
$stmt->execute(array(':domain' => $parsed_goto['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) {

View File

@@ -1,12 +0,0 @@
#!/bin/sh
cat <<EOF > /valkey.conf
requirepass $VALKEYPASS
user quota_notify on nopass ~QW_* -@all +get +hget +ping
EOF
if [ -n "$VALKEYMASTERPASS" ]; then
echo "masterauth $VALKEYMASTERPASS" >> /valkey.conf
fi
exec "$@"

View File

@@ -1,13 +1,13 @@
<?php
$valkey = new Redis();
$redis = new Redis();
try {
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
}
else {
$valkey->connect('valkey-mailcow', 6379);
$redis->connect('redis-mailcow', 6379);
}
$valkey->auth(getenv("VALKEYPASS"));
$redis->auth(getenv("REDISPASS"));
}
catch (Exception $e) {
exit;
@@ -15,4 +15,4 @@ catch (Exception $e) {
header('Content-Type: application/json');
echo '{"error":"Unauthorized"}';
error_log("Rspamd UI: Invalid password by " . $_SERVER['REMOTE_ADDR']);
$valkey->publish("F2B_CHANNEL", "Rspamd UI: Invalid password by " . $_SERVER['REMOTE_ADDR']);
$redis->publish("F2B_CHANNEL", "Rspamd UI: Invalid password by " . $_SERVER['REMOTE_ADDR']);

View File

@@ -21,7 +21,7 @@ $clamd_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_CLAMD"])) ?
$olefy_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_OLEFY"])) ? false : true;
if (!isset($_SESSION['gal']) && $license_cache = $valkey->Get('LICENSE_STATUS_CACHE')) {
if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) {
$_SESSION['gal'] = json_decode($license_cache, true);
}

View File

@@ -5412,9 +5412,9 @@ paths:
started_at: "2019-12-22T21:00:07.186717617Z"
state: running
type: info
valkey-mailcow:
container: valkey-mailcow
image: "valkey:7.2.8-alpine"
redis-mailcow:
container: redis-mailcow
image: "redis:5-alpine"
started_at: "2019-12-22T20:59:56.827166834Z"
state: running
type: info

View File

@@ -10,16 +10,16 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
$default_autodiscover_config = $autodiscover_config;
$autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
// Valkey
$valkey = new Redis();
// Redis
$redis = new Redis();
try {
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
}
else {
$valkey->connect('valkey-mailcow', 6379);
$redis->connect('redis-mailcow', 6379);
}
$valkey->auth(getenv("VALKEYPASS"));
$redis->auth(getenv("REDISPASS"));
}
catch (Exception $e) {
exit;
@@ -71,7 +71,7 @@ if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) {
"service" => "Error: must be authenticated"
)
);
$valkey->lPush('AUTODISCOVER_LOG', $json);
$redis->lPush('AUTODISCOVER_LOG', $json);
header('WWW-Authenticate: Basic realm="' . $_SERVER['HTTP_HOST'] . '"');
header('HTTP/1.0 401 Unauthorized');
exit(0);
@@ -96,13 +96,13 @@ if ($login_role === "user") {
"service" => "Error: invalid or missing request data"
)
);
$valkey->lPush('AUTODISCOVER_LOG', $json);
$valkey->lTrim('AUTODISCOVER_LOG', 0, 100);
$redis->lPush('AUTODISCOVER_LOG', $json);
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'msg' => 'Valkey: '.$e
'msg' => 'Redis: '.$e
);
return false;
}
@@ -151,13 +151,13 @@ if ($login_role === "user") {
"service" => $autodiscover_config['autodiscoverType']
)
);
$valkey->lPush('AUTODISCOVER_LOG', $json);
$valkey->lTrim('AUTODISCOVER_LOG', 0, 100);
$redis->lPush('AUTODISCOVER_LOG', $json);
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'msg' => 'Valkey: '.$e
'msg' => 'Redis: '.$e
);
return false;
}

View File

@@ -26,25 +26,23 @@ if (is_array($alertbox_log_parser)) {
// map tfa details for twig
$pending_tfa_authmechs = [];
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;
}
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

View File

@@ -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,7 +43,20 @@ function app_passwd($_action, $_data = null) {
);
return false;
}
if (password_check($password, $password2) !== true) {
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'
);
return false;
}
$password_hashed = hash_password($password);
@@ -75,15 +88,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['app_passwd'])) ? $_data['app_passwd'] : null;
$password2 = (!empty($_data['app_passwd2'])) ? $_data['app_passwd2'] : null;
$password = (!empty($_data['password'])) ? $_data['password'] : null;
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
if (isset($_data['protocols'])) {
$protocols = (array)$_data['protocols'];
$imap_access = (in_array('imap_access', $protocols)) ? 1 : 0;
@@ -113,7 +126,20 @@ function app_passwd($_action, $_data = null) {
}
$app_name = htmlspecialchars(trim($app_name));
if (!empty($password) && !empty($password2)) {
if (password_check($password, $password2) !== true) {
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_complexity'
);
continue;
}
if ($password != $password2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_mismatch'
);
continue;
}
$password_hashed = hash_password($password);
@@ -156,7 +182,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) {
@@ -187,17 +213,19 @@ 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)) {
@@ -209,6 +237,6 @@ function app_passwd($_action, $_data = null) {
}
$app_passwd_data['name'] = htmlspecialchars(trim($app_passwd_data['name']));
return $app_passwd_data;
break;
break;
}
}

View File

@@ -1,7 +1,7 @@
<?php
function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
global $pdo;
global $valkey;
global $redis;
$is_internal = $extra['is_internal'];
$role = $extra['role'];
@@ -62,12 +62,12 @@ function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
if (!isset($_SESSION['ldelay'])) {
$_SESSION['ldelay'] = "0";
$valkey->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
}
elseif (!isset($_SESSION['mailcow_cc_username'])) {
$_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
$valkey->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
}
$_SESSION['return'][] = array(
@@ -193,7 +193,6 @@ function user_login($user, $pass, $extra = null){
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){
@@ -236,14 +235,6 @@ function user_login($user, $pass, $extra = null){
$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;
}
}
@@ -251,14 +242,7 @@ function user_login($user, $pass, $extra = null){
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

View File

@@ -1,6 +1,6 @@
<?php
function customize($_action, $_item, $_data = null) {
global $valkey;
global $redis;
global $lang;
global $LOGO_LIMITS;
@@ -82,13 +82,13 @@ function customize($_action, $_item, $_data = null) {
return false;
}
try {
$valkey->Set(strtoupper($_item), 'data:' . $_data[$_item]['type'] . ';base64,' . base64_encode(file_get_contents($_data[$_item]['tmp_name'])));
$redis->Set(strtoupper($_item), 'data:' . $_data[$_item]['type'] . ';base64,' . base64_encode(file_get_contents($_data[$_item]['tmp_name'])));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
return false;
}
@@ -134,13 +134,13 @@ function customize($_action, $_item, $_data = null) {
));
}
try {
$valkey->set('APP_LINKS', json_encode($out));
$redis->set('APP_LINKS', json_encode($out));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
return false;
}
@@ -162,20 +162,20 @@ function customize($_action, $_item, $_data = null) {
$ui_announcement_active = (!empty($_data['ui_announcement_active']) ? 1 : 0);
try {
$valkey->set('TITLE_NAME', htmlspecialchars($title_name));
$valkey->set('MAIN_NAME', htmlspecialchars($main_name));
$valkey->set('APPS_NAME', htmlspecialchars($apps_name));
$valkey->set('HELP_TEXT', $help_text);
$valkey->set('UI_FOOTER', $ui_footer);
$valkey->set('UI_ANNOUNCEMENT_TEXT', $ui_announcement_text);
$valkey->set('UI_ANNOUNCEMENT_TYPE', $ui_announcement_type);
$valkey->set('UI_ANNOUNCEMENT_ACTIVE', $ui_announcement_active);
$redis->set('TITLE_NAME', htmlspecialchars($title_name));
$redis->set('MAIN_NAME', htmlspecialchars($main_name));
$redis->set('APPS_NAME', htmlspecialchars($apps_name));
$redis->set('HELP_TEXT', $help_text);
$redis->set('UI_FOOTER', $ui_footer);
$redis->set('UI_ANNOUNCEMENT_TEXT', $ui_announcement_text);
$redis->set('UI_ANNOUNCEMENT_TYPE', $ui_announcement_type);
$redis->set('UI_ANNOUNCEMENT_ACTIVE', $ui_announcement_active);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
return false;
}
@@ -188,13 +188,13 @@ function customize($_action, $_item, $_data = null) {
case 'ip_check':
$ip_check = ($_data['ip_check_opt_in'] == "1") ? 1 : 0;
try {
$valkey->set('IP_CHECK', $ip_check);
$redis->set('IP_CHECK', $ip_check);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
return false;
}
@@ -217,7 +217,7 @@ function customize($_action, $_item, $_data = null) {
"force_sso" => $force_sso,
);
try {
$valkey->set('CUSTOM_LOGIN', json_encode($custom_login));
$redis->set('CUSTOM_LOGIN', json_encode($custom_login));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
@@ -257,7 +257,7 @@ function customize($_action, $_item, $_data = null) {
case 'main_logo':
case 'main_logo_dark':
try {
if ($valkey->del(strtoupper($_item))) {
if ($redis->del(strtoupper($_item))) {
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
@@ -270,7 +270,7 @@ function customize($_action, $_item, $_data = null) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
return false;
}
@@ -281,19 +281,19 @@ function customize($_action, $_item, $_data = null) {
switch ($_item) {
case 'app_links':
try {
$app_links = json_decode($valkey->get('APP_LINKS'), true);
$app_links = json_decode($redis->get('APP_LINKS'), true);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
return false;
}
if (empty($app_links)){
return [];
return false;
}
// convert from old style
@@ -312,40 +312,38 @@ function customize($_action, $_item, $_data = null) {
case 'main_logo':
case 'main_logo_dark':
try {
return $valkey->get(strtoupper($_item));
return $redis->get(strtoupper($_item));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
return false;
}
break;
case 'ui_texts':
try {
$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'))) {
$valkey->set('UI_FOOTER', $valkey->get('UI_IMPRESS'));
$valkey->del('UI_IMPRESS');
$data['title_name'] = ($title_name = $redis->get('TITLE_NAME')) ? $title_name : 'mailcow UI';
$data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : 'mailcow UI';
$data['apps_name'] = ($apps_name = $redis->get('APPS_NAME')) ? $apps_name : $lang['header']['apps'];
$data['help_text'] = ($help_text = $redis->get('HELP_TEXT')) ? $help_text : false;
if (!empty($redis->get('UI_IMPRESS'))) {
$redis->set('UI_FOOTER', $redis->get('UI_IMPRESS'));
$redis->del('UI_IMPRESS');
}
$data['ui_footer'] = ($ui_footer = $valkey->get('UI_FOOTER')) ? $ui_footer : false;
$data['ui_announcement_text'] = ($ui_announcement_text = $valkey->get('UI_ANNOUNCEMENT_TEXT')) ? $ui_announcement_text : false;
$data['ui_announcement_type'] = ($ui_announcement_type = $valkey->get('UI_ANNOUNCEMENT_TYPE')) ? $ui_announcement_type : false;
$data['ui_announcement_active'] = ($valkey->get('UI_ANNOUNCEMENT_ACTIVE') == 1) ? 1 : 0;
$data['ui_footer'] = ($ui_footer = $redis->get('UI_FOOTER')) ? $ui_footer : false;
$data['ui_announcement_text'] = ($ui_announcement_text = $redis->get('UI_ANNOUNCEMENT_TEXT')) ? $ui_announcement_text : false;
$data['ui_announcement_type'] = ($ui_announcement_type = $redis->get('UI_ANNOUNCEMENT_TYPE')) ? $ui_announcement_type : false;
$data['ui_announcement_active'] = ($redis->get('UI_ANNOUNCEMENT_ACTIVE') == 1) ? 1 : 0;
return $data;
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
return false;
}
@@ -376,21 +374,21 @@ function customize($_action, $_item, $_data = null) {
break;
case 'ip_check':
try {
$ip_check = ($ip_check = $valkey->get('IP_CHECK')) ? $ip_check : 0;
$ip_check = ($ip_check = $redis->get('IP_CHECK')) ? $ip_check : 0;
return $ip_check;
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
return false;
}
break;
case 'custom_login':
try {
$custom_login = $valkey->get('CUSTOM_LOGIN');
$custom_login = $redis->get('CUSTOM_LOGIN');
return $custom_login ? json_decode($custom_login, true) : array();
}
catch (RedisException $e) {

View File

@@ -1,7 +1,7 @@
<?php
function dkim($_action, $_data = null, $privkey = false) {
global $valkey;
global $redis;
global $lang;
switch ($_action) {
case 'add':
@@ -18,7 +18,7 @@ function dkim($_action, $_data = null, $privkey = false) {
);
continue;
}
if ($valkey->hGet('DKIM_PUB_KEYS', $domain)) {
if ($redis->hGet('DKIM_PUB_KEYS', $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
@@ -54,30 +54,30 @@ function dkim($_action, $_data = null, $privkey = false) {
explode(PHP_EOL, $key_details['key'])
), 1, -1)
);
// Save public key and selector to valkey
// Save public key and selector to redis
try {
$valkey->hSet('DKIM_PUB_KEYS', $domain, $pubKey);
$valkey->hSet('DKIM_SELECTORS', $domain, $dkim_selector);
$redis->hSet('DKIM_PUB_KEYS', $domain, $pubKey);
$redis->hSet('DKIM_SELECTORS', $domain, $dkim_selector);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
continue;
}
// Export private key and save private key to valkey
// Export private key and save private key to redis
openssl_pkey_export($keypair_ressource, $privKey);
if (isset($privKey) && !empty($privKey)) {
try {
$valkey->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, trim($privKey));
$redis->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, trim($privKey));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
continue;
}
@@ -121,15 +121,15 @@ function dkim($_action, $_data = null, $privkey = false) {
$to_domains = array_filter($to_domains);
foreach ($to_domains as $to_domain) {
try {
$valkey->hSet('DKIM_PUB_KEYS', $to_domain, $from_domain_dkim['pubkey']);
$valkey->hSet('DKIM_SELECTORS', $to_domain, $from_domain_dkim['dkim_selector']);
$valkey->hSet('DKIM_PRIV_KEYS', $from_domain_dkim['dkim_selector'] . '.' . $to_domain, base64_decode(trim($from_domain_dkim['privkey'])));
$redis->hSet('DKIM_PUB_KEYS', $to_domain, $from_domain_dkim['pubkey']);
$redis->hSet('DKIM_SELECTORS', $to_domain, $from_domain_dkim['dkim_selector']);
$redis->hSet('DKIM_PRIV_KEYS', $from_domain_dkim['dkim_selector'] . '.' . $to_domain, base64_decode(trim($from_domain_dkim['privkey'])));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
continue;
}
@@ -178,7 +178,7 @@ function dkim($_action, $_data = null, $privkey = false) {
);
return false;
}
if ($valkey->hGet('DKIM_PUB_KEYS', $domain)) {
if ($redis->hGet('DKIM_PUB_KEYS', $domain)) {
if ($overwrite_existing == 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
@@ -198,15 +198,15 @@ function dkim($_action, $_data = null, $privkey = false) {
}
try {
dkim('delete', array('domains' => $domain));
$valkey->hSet('DKIM_PUB_KEYS', $domain, $pem_public_key);
$valkey->hSet('DKIM_SELECTORS', $domain, $dkim_selector);
$valkey->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, $private_key_normalized);
$redis->hSet('DKIM_PUB_KEYS', $domain, $pem_public_key);
$redis->hSet('DKIM_SELECTORS', $domain, $dkim_selector);
$redis->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, $private_key_normalized);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
return false;
}
@@ -219,7 +219,7 @@ function dkim($_action, $_data = null, $privkey = false) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
return false;
}
@@ -235,8 +235,8 @@ function dkim($_action, $_data = null, $privkey = false) {
return false;
}
$dkimdata = array();
if ($valkey_dkim_key_data = $valkey->hGet('DKIM_PUB_KEYS', $_data)) {
$dkimdata['pubkey'] = $valkey_dkim_key_data;
if ($redis_dkim_key_data = $redis->hGet('DKIM_PUB_KEYS', $_data)) {
$dkimdata['pubkey'] = $redis_dkim_key_data;
if (strlen($dkimdata['pubkey']) < 391) {
$dkimdata['length'] = "1024";
}
@@ -253,15 +253,15 @@ function dkim($_action, $_data = null, $privkey = false) {
$dkimdata['length'] = ">= 8192";
}
if ($GLOBALS['SPLIT_DKIM_255'] === true) {
$dkim_txt_tmp = str_split('v=DKIM1;k=rsa;t=s;s=email;p=' . $valkey_dkim_key_data, 255);
$dkim_txt_tmp = str_split('v=DKIM1;k=rsa;t=s;s=email;p=' . $redis_dkim_key_data, 255);
$dkimdata['dkim_txt'] = sprintf('"%s"', implode('" "', (array)$dkim_txt_tmp ) );
}
else {
$dkimdata['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . $valkey_dkim_key_data;
$dkimdata['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . $redis_dkim_key_data;
}
$dkimdata['dkim_selector'] = $valkey->hGet('DKIM_SELECTORS', $_data);
$dkimdata['dkim_selector'] = $redis->hGet('DKIM_SELECTORS', $_data);
if ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] || $privkey == true) {
$dkimdata['privkey'] = base64_encode($valkey->hGet('DKIM_PRIV_KEYS', $dkimdata['dkim_selector'] . '.' . $_data));
$dkimdata['privkey'] = base64_encode($redis->hGet('DKIM_PRIV_KEYS', $dkimdata['dkim_selector'] . '.' . $_data));
}
else {
$dkimdata['privkey'] = '';
@@ -279,8 +279,8 @@ function dkim($_action, $_data = null, $privkey = false) {
return false;
}
$blinddkim = array();
foreach ($valkey->hKeys('DKIM_PUB_KEYS') as $valkey_dkim_domain) {
$blinddkim[] = $valkey_dkim_domain;
foreach ($redis->hKeys('DKIM_PUB_KEYS') as $redis_dkim_domain) {
$blinddkim[] = $redis_dkim_domain;
}
return array_diff($blinddkim, array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains')));
break;
@@ -304,16 +304,16 @@ function dkim($_action, $_data = null, $privkey = false) {
continue;
}
try {
$selector = $valkey->hGet('DKIM_SELECTORS', $domain);
$valkey->hDel('DKIM_PUB_KEYS', $domain);
$valkey->hDel('DKIM_PRIV_KEYS', $selector . '.' . $domain);
$valkey->hDel('DKIM_SELECTORS', $domain);
$selector = $redis->hGet('DKIM_SELECTORS', $domain);
$redis->hDel('DKIM_PUB_KEYS', $domain);
$redis->hDel('DKIM_PRIV_KEYS', $selector . '.' . $domain);
$redis->hDel('DKIM_SELECTORS', $domain);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('valkey_error', $e)
'msg' => array('redis_error', $e)
);
continue;
}

Some files were not shown because too many files have changed in this diff Show More