Compare commits

..

3 Commits

Author SHA1 Message Date
FreddleSpl0it
c0f06bfc52 Merge branch 'staging' into feat/valkey 2025-10-10 12:40:41 +02:00
FreddleSpl0it
0698159f07 Add redis-to-valkey migratior 2025-02-28 15:49:28 +01:00
FreddleSpl0it
c27000215e migrate from redis to valkey 2025-02-28 15:36:19 +01:00
514 changed files with 20749 additions and 23276 deletions

View File

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

View File

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

View File

@@ -8,11 +8,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Run the Action - name: Run the Action
uses: devops-infra/action-pull-request@v1.0.2 uses: devops-infra/action-pull-request@v0.6.1
with: with:
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }} github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}} title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}

View File

@@ -13,24 +13,24 @@ jobs:
packages: write packages: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v5
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v3
- name: Login to GHCR - name: Login to GHCR
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v4 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v7 uses: docker/build-push-action@v6
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64

View File

@@ -15,14 +15,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v5
- name: Generate postscreen_access.cidr - name: Generate postscreen_access.cidr
run: | run: |
bash helper-scripts/update_postscreen_whitelist.sh bash helper-scripts/update_postscreen_whitelist.sh
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v8 uses: peter-evans/create-pull-request@v7
with: with:
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }} token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
commit-message: update postscreen_access.cidr commit-message: update postscreen_access.cidr

1
.gitignore vendored
View File

@@ -51,7 +51,6 @@ data/conf/sogo/cron.creds
data/conf/sogo/custom-fulllogo.svg data/conf/sogo/custom-fulllogo.svg
data/conf/sogo/custom-shortlogo.svg data/conf/sogo/custom-shortlogo.svg
data/conf/sogo/custom-fulllogo.png data/conf/sogo/custom-fulllogo.png
data/conf/acme/dns-01.conf
data/gitea/ data/gitea/
data/gogs/ data/gogs/
data/hooks/dovecot/* data/hooks/dovecot/*

View File

@@ -1,11 +1,11 @@
# Contribution Guidelines # Contribution Guidelines
**_Last modified on 12th November 2025_** **_Last modified on 15th August 2024_**
First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow! First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow!
As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly. As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly.
**PLEASE NOTE, THAT WE WILL CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULFILL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request. **PLEASE NOTE, THAT WE MIGHT CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULLFIL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
## Topics ## Topics
@@ -27,18 +27,14 @@ However, please note the following regarding pull requests:
6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.* 6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.*
7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project. 7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project.
8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort! 8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort!
9. If your PR requires a Docker image rebuild (changes to Dockerfiles or files in data/Dockerfiles/), update the image tag in docker-compose.yml. Use the base-image versioning (e.g. ghcr.io/mailcow/sogo:5.12.4 → :5.12.5 for version bumps; append a letter for patch fixes, e.g. :5.12.4a). Follow this scheme.
--- ---
## Issue Reporting ## Issue Reporting
**_Last modified on 12th November 2025_** **_Last modified on 15th August 2024_**
If you plan to report a issue within mailcow please read and understand the following rules: If you plan to report a issue within mailcow please read and understand the following rules:
### Security disclosures / Security-related fixes
- Security vulnerabilities and security fixes must always be reported confidentially first to the contact address specified in SECURITY.md before they are integrated, published, or publicly disclosed in issues/PRs. Please wait for a response from the specified contact to ensure coordinated and responsible disclosure.
### Issue Reporting Guidelines ### Issue Reporting Guidelines
1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support). 1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support).

View File

@@ -38,45 +38,45 @@ get_docker_version(){
} }
get_compose_type(){ get_compose_type(){
if docker compose > /dev/null 2>&1; then if docker compose > /dev/null 2>&1; then
if docker compose version --short | grep -e "^[2-9]\." -e "^v[2-9]\." -e "^[1-9][0-9]\." -e "^v[1-9][0-9]\." > /dev/null 2>&1; then if docker compose version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then
COMPOSE_VERSION=native COMPOSE_VERSION=native
COMPOSE_COMMAND="docker compose" COMPOSE_COMMAND="docker compose"
if [[ "$caller" == "update.sh" ]]; then if [[ "$caller" == "update.sh" ]]; then
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf" sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf"
fi fi
echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m" echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m"
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m" echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
sleep 2 sleep 2
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m" echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
else else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m" echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1 exit 1
fi
elif docker-compose > /dev/null 2>&1; then
if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
if docker-compose version --short | grep "^2." > /dev/null 2>&1; then
COMPOSE_VERSION=standalone
COMPOSE_COMMAND="docker-compose"
if [[ "$caller" == "update.sh" ]]; then
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf"
fi
echo -e "\e[33mFound Docker Compose Standalone.\e[0m"
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
sleep 2
echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
fi fi
elif docker-compose > /dev/null 2>&1; then
if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
if docker-compose version --short | grep -e "^[2-9]\." -e "^[1-9][0-9]\." > /dev/null 2>&1; then
COMPOSE_VERSION=standalone
COMPOSE_COMMAND="docker-compose"
if [[ "$caller" == "update.sh" ]]; then
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf"
fi
echo -e "\e[33mFound Docker Compose Standalone.\e[0m"
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
sleep 2
echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
else else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" echo -e "\e[31mCannot find Docker Compose.\e[0m"
echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m" echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1 exit 1
fi fi
fi
else
echo -e "\e[31mCannot find Docker Compose.\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
} }
detect_bad_asn() { detect_bad_asn() {

View File

@@ -57,14 +57,11 @@ adapt_new_options() {
"DISABLE_NETFILTER_ISOLATION_RULE" "DISABLE_NETFILTER_ISOLATION_RULE"
"HTTP_REDIRECT" "HTTP_REDIRECT"
"ENABLE_IPV6" "ENABLE_IPV6"
"ACME_DNS_CHALLENGE"
"ACME_DNS_PROVIDER"
"ACME_ACCOUNT_EMAIL"
) )
sed -i --follow-symlinks '$a\' mailcow.conf sed -i --follow-symlinks '$a\' mailcow.conf
for option in ${CONFIG_ARRAY[@]}; do for option in ${CONFIG_ARRAY[@]}; do
if grep -q "^#\?${option}=" mailcow.conf; then if grep -q "${option}" mailcow.conf; then
continue continue
fi fi
@@ -295,20 +292,6 @@ adapt_new_options() {
echo '# This key is used to encrypt email addresses within SOGo URLs' >> mailcow.conf echo '# This key is used to encrypt email addresses within SOGo URLs' >> mailcow.conf
echo "SOGO_URL_ENCRYPTION_KEY=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 16)" >> mailcow.conf echo "SOGO_URL_ENCRYPTION_KEY=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 16)" >> mailcow.conf
;; ;;
ACME_DNS_CHALLENGE)
echo '# Enable DNS-01 challenge for ACME (acme-mailcow) - y/n' >> mailcow.conf
echo '# This requires you to set ACME_DNS_PROVIDER and ACME_ACCOUNT_EMAIL below' >> mailcow.conf
echo 'ACME_DNS_CHALLENGE=n' >> mailcow.conf
;;
ACME_DNS_PROVIDER)
echo '# DNS provider for DNS-01 challenge (e.g. dns_cf, dns_azure, dns_gd, etc.)' >> mailcow.conf
echo '# See the dns-01 provider documentation for more information.' >> mailcow.conf
echo 'ACME_DNS_PROVIDER=dns_xxx' >> mailcow.conf
;;
ACME_ACCOUNT_EMAIL)
echo '# Account email for ACME DNS-01 challenge registration' >> mailcow.conf
echo 'ACME_ACCOUNT_EMAIL=me@example.com' >> mailcow.conf
;;
*) *)
echo "${option}=" >> mailcow.conf echo "${option}=" >> mailcow.conf
;; ;;

View File

@@ -1,4 +1,4 @@
FROM alpine:3.23 FROM alpine:3.21
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
@@ -14,22 +14,11 @@ RUN apk upgrade --no-cache \
tini \ tini \
tzdata \ tzdata \
python3 \ python3 \
acme-tiny \ acme-tiny
git \
socat \
&& git clone --depth 1 https://github.com/acmesh-official/acme.sh.git /opt/acme.sh \
&& chmod +x /opt/acme.sh/acme.sh \
&& mkdir -p /var/lib/acme/acme-sh
ENV ACME_SH_BIN=/opt/acme.sh/acme.sh \
ACME_SH_HOME=/opt/acme.sh \
ACME_SH_CONFIG_HOME=/var/lib/acme/acme-sh
COPY acme.sh /srv/acme.sh COPY acme.sh /srv/acme.sh
COPY functions.sh /srv/functions.sh COPY functions.sh /srv/functions.sh
COPY obtain-certificate.sh /srv/obtain-certificate.sh COPY obtain-certificate.sh /srv/obtain-certificate.sh
COPY obtain-certificate-dns.sh /srv/obtain-certificate-dns.sh
COPY load-dns-config.sh /srv/load-dns-config.sh
COPY reload-configurations.sh /srv/reload-configurations.sh COPY reload-configurations.sh /srv/reload-configurations.sh
COPY expand6.sh /srv/expand6.sh COPY expand6.sh /srv/expand6.sh

View File

@@ -3,28 +3,17 @@ set -o pipefail
exec 5>&1 exec 5>&1
# Do not attempt to write to slave # Do not attempt to write to slave
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" export VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
else else
export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" export VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning"
fi fi
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..." echo "Waiting for Valkey..."
sleep 2 sleep 2
done done
# Create DNS-01 configuration template if it doesn't exist
if [[ ! -f /etc/acme/dns-01.conf ]]; then
mkdir -p /etc/acme
cat > /etc/acme/dns-01.conf <<'EOF'
# Add here your DNS-01 challenge configuration
# For more information, visit the acme.sh documentation:
# https://github.com/acmesh-official/acme.sh/wiki/dnsapi
EOF
echo "Created DNS-01 configuration template at /etc/acme/dns-01.conf"
fi
source /srv/functions.sh source /srv/functions.sh
# Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6 # Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6
source /srv/expand6.sh source /srv/expand6.sh
@@ -53,10 +42,6 @@ if [[ "${ENABLE_SSL_SNI}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
ENABLE_SSL_SNI=y ENABLE_SSL_SNI=y
fi fi
if [[ "${ACME_DNS_CHALLENGE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
ACME_DNS_CHALLENGE=y
fi
if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..." log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
sleep 365d sleep 365d
@@ -253,46 +238,14 @@ while true; do
unset VALIDATED_CONFIG_DOMAINS_SUBDOMAINS unset VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
declare -a VALIDATED_CONFIG_DOMAINS_SUBDOMAINS declare -a VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do
FULL_SUBDOMAIN="${SUBDOMAIN}.${SQL_DOMAIN}" if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then
if check_domain "${SUBDOMAIN}.${SQL_DOMAIN}"; then
# Skip if subdomain matches MAILCOW_HOSTNAME VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}")
if [[ "${FULL_SUBDOMAIN}" == "${MAILCOW_HOSTNAME}" ]]; then fi
continue
fi
# Skip if subdomain is covered by a wildcard in ADDITIONAL_SAN
if is_covered_by_wildcard "${FULL_SUBDOMAIN}"; then
log_f "Subdomain '${FULL_SUBDOMAIN}' is covered by wildcard - skipping explicit subdomain"
continue
fi
# Validate and add subdomain
if check_domain "${FULL_SUBDOMAIN}"; then
VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${FULL_SUBDOMAIN}")
fi fi
done done
VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}") VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}")
done done
# Fetch alias domains where target domain has MTA-STS enabled
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
SQL_ALIAS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT ad.alias_domain FROM alias_domain ad INNER JOIN mta_sts m ON ad.target_domain = m.domain WHERE ad.active = 1 AND m.active = 1" -Bs)
if [[ $? -eq 0 ]]; then
while read alias_domain; do
if [[ -z "${alias_domain}" ]]; then
# ignore empty lines
continue
fi
# Only add mta-sts subdomain for alias domains
if [[ "mta-sts.${alias_domain}" != "${MAILCOW_HOSTNAME}" ]]; then
# Skip if mta-sts subdomain is covered by a wildcard
if is_covered_by_wildcard "mta-sts.${alias_domain}"; then
log_f "Alias domain mta-sts subdomain 'mta-sts.${alias_domain}' is covered by wildcard - skipping"
elif check_domain "mta-sts.${alias_domain}"; then
VALIDATED_CONFIG_DOMAINS+=("mta-sts.${alias_domain}")
fi
fi
done <<< "${SQL_ALIAS_DOMAINS}"
fi
fi
fi fi
if check_domain ${MAILCOW_HOSTNAME}; then if check_domain ${MAILCOW_HOSTNAME}; then
@@ -321,31 +274,13 @@ while true; do
done done
fi fi
# Check if MAILCOW_HOSTNAME is covered by a wildcard in ADDITIONAL_SAN
MAILCOW_HOSTNAME_COVERED=0
if [[ ! -z ${VALIDATED_MAILCOW_HOSTNAME} ]]; then
if is_covered_by_wildcard "${VALIDATED_MAILCOW_HOSTNAME}"; then
MAILCOW_PARENT_DOMAIN=$(echo ${VALIDATED_MAILCOW_HOSTNAME} | cut -d. -f2-)
log_f "MAILCOW_HOSTNAME '${VALIDATED_MAILCOW_HOSTNAME}' is covered by wildcard '*.${MAILCOW_PARENT_DOMAIN}' - skipping explicit hostname"
MAILCOW_HOSTNAME_COVERED=1
fi
fi
# Unique domains for server certificate # Unique domains for server certificate
if [[ ${ENABLE_SSL_SNI} == "y" ]]; then if [[ ${ENABLE_SSL_SNI} == "y" ]]; then
# create certificate for server name and fqdn SANs only # create certificate for server name and fqdn SANs only
if [[ ${MAILCOW_HOSTNAME_COVERED} == "1" ]]; then SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
SERVER_SAN_VALIDATED=($(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
else
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
fi
else else
# create certificate for all domains, including all subdomains from other domains [*] # create certificate for all domains, including all subdomains from other domains [*]
if [[ ${MAILCOW_HOSTNAME_COVERED} == "1" ]]; then SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
SERVER_SAN_VALIDATED=($(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
else
SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs))
fi
fi fi
if [[ ! -z ${SERVER_SAN_VALIDATED[*]} ]]; then if [[ ! -z ${SERVER_SAN_VALIDATED[*]} ]]; then
CERT_NAME=${SERVER_SAN_VALIDATED[0]} CERT_NAME=${SERVER_SAN_VALIDATED[0]}
@@ -413,7 +348,7 @@ while true; do
if [[ -z ${VALIDATED_CERTIFICATES[*]} ]]; then if [[ -z ${VALIDATED_CERTIFICATES[*]} ]]; then
log_f "Cannot validate any hostnames, skipping Let's Encrypt for 1 hour." 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." log_f "Use SKIP_LETS_ENCRYPT=y in mailcow.conf to skip it permanently."
${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)" ${VALKEY_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
sleep 1h sleep 1h
exec $(readlink -f "$0") exec $(readlink -f "$0")
fi fi
@@ -454,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)" 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 if [[ ${RELOAD_LOOP_C} -gt 3 ]]; then
log_f "Some services do return old end dates, something went wrong!" log_f "Some services do return old end dates, something went wrong!"
${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)" ${VALKEY_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
break; break;
fi fi
done done
@@ -475,7 +410,7 @@ while true; do
;; ;;
*) # non-zero *) # non-zero
log_f "Some errors occurred, retrying in 30 minutes..." log_f "Some errors occurred, retrying in 30 minutes..."
${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)" ${VALKEY_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
sleep 30m sleep 30m
exec $(readlink -f "$0") exec $(readlink -f "$0")
;; ;;

View File

@@ -5,13 +5,13 @@ log_f() {
echo -n "$(date) - ${1}" echo -n "$(date) - ${1}"
elif [[ ${2} == "no_date" ]]; then elif [[ ${2} == "no_date" ]]; then
echo "${1}" echo "${1}"
elif [[ ${2} != "redis_only" ]]; then elif [[ ${2} != "valkey_only" ]]; then
echo "$(date) - ${1}" echo "$(date) - ${1}"
fi fi
if [[ ${3} == "b64" ]]; then if [[ ${3} == "b64" ]]; then
${REDIS_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"base64,$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}")\"}" > /dev/null ${VALKEY_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"base64,$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}")\"}" > /dev/null
else else
${REDIS_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}" | \ ${VALKEY_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}" | \
tr '%&;$"[]{}-\r\n' ' ')\"}" > /dev/null tr '%&;$"[]{}-\r\n' ' ')\"}" > /dev/null
fi fi
} }
@@ -80,11 +80,6 @@ check_domain(){
return 1 return 1
fi fi
fi fi
if [[ ${ACME_DNS_CHALLENGE} == "y" ]]; then
log_f "ACME_DNS_CHALLENGE=y - skipping IP and HTTP validation for ${DOMAIN}"
return 0
fi
# Check if CNAME without v6 enabled target # Check if CNAME without v6 enabled target
if [[ ! -z ${AAAA_DOMAIN} ]] && [[ -z $(echo ${AAAA_DOMAIN} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") ]]; then if [[ ! -z ${AAAA_DOMAIN} ]] && [[ -z $(echo ${AAAA_DOMAIN} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") ]]; then
AAAA_DOMAIN= AAAA_DOMAIN=
@@ -135,32 +130,3 @@ verify_challenge_path(){
return 1 return 1
fi fi
} }
# Check if a domain is covered by a wildcard (*.example.com) in ADDITIONAL_SAN
# Usage: is_covered_by_wildcard "subdomain.example.com"
# Returns: 0 if covered, 1 if not covered
# Note: Only returns 0 (covered) when DNS-01 challenge is enabled,
# as wildcards cannot be validated with HTTP-01 challenge
is_covered_by_wildcard() {
local DOMAIN=$1
# Only skip if DNS challenge is enabled (wildcards require DNS-01)
if [[ ${ACME_DNS_CHALLENGE} != "y" ]]; then
return 1
fi
# Return early if no ADDITIONAL_SAN is set
if [[ -z ${ADDITIONAL_SAN} ]]; then
return 1
fi
# Extract parent domain (e.g., mail.example.com -> example.com)
local PARENT_DOMAIN=$(echo ${DOMAIN} | cut -d. -f2-)
# Check if ADDITIONAL_SAN contains a wildcard for this parent domain
if [[ "${ADDITIONAL_SAN}" == *"*.${PARENT_DOMAIN}"* ]]; then
return 0 # Covered by wildcard
fi
return 1 # Not covered
}

View File

@@ -1,57 +0,0 @@
#!/bin/bash
SCRIPT_SOURCE="${BASH_SOURCE[0]:-${0}}"
if [[ "${SCRIPT_SOURCE}" == "${0}" ]]; then
__dns_loader_standalone=1
else
__dns_loader_standalone=0
fi
CONFIG_PATH="${ACME_DNS_CONFIG_FILE:-/etc/acme/dns-01.conf}"
if [[ ! -f "${CONFIG_PATH}" ]]; then
if [[ $__dns_loader_standalone -eq 1 ]]; then
exit 0
else
return 0
fi
fi
source /srv/functions.sh
log_f "Loading DNS-01 configuration from ${CONFIG_PATH}"
LINE_NO=0
while IFS= read -r line || [[ -n "${line}" ]]; do
LINE_NO=$((LINE_NO+1))
line="${line%$'\r'}"
line_trimmed="$(printf '%s' "${line}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
[[ -z "${line_trimmed}" ]] && continue
[[ "${line_trimmed:0:1}" == "#" ]] && continue
if [[ "${line_trimmed}" != *=* ]]; then
log_f "Skipping invalid DNS config line ${LINE_NO} (missing key=value)"
continue
fi
KEY="${line_trimmed%%=*}"
VALUE="${line_trimmed#*=}"
KEY="$(printf '%s' "${KEY}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
VALUE="$(printf '%s' "${VALUE}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
if [[ -z "${KEY}" ]]; then
log_f "Skipping invalid DNS config line ${LINE_NO} (empty key)"
continue
fi
if [[ "${VALUE}" =~ ^\".*\"$ ]]; then
VALUE="${VALUE:1:-1}"
elif [[ "${VALUE}" =~ ^\'.*\'$ ]]; then
VALUE="${VALUE:1:-1}"
fi
export "${KEY}"="${VALUE}"
log_f "Exported DNS config key ${KEY}"
done < "${CONFIG_PATH}"
if [[ $__dns_loader_standalone -eq 1 ]]; then
exit 0
else
return 0
fi

View File

@@ -1,177 +0,0 @@
#!/bin/bash
# Return values / exit codes
# 0 = cert created successfully
# 1 = cert renewed successfully
# 2 = cert not due for renewal
# * = errors
source /srv/functions.sh
CERT_DOMAINS=(${DOMAINS[@]})
CERT_DOMAIN=${CERT_DOMAINS[0]}
ACME_BASE=/var/lib/acme
# Load optional DNS provider secrets from /etc/acme/dns-01.conf
if [[ -f /srv/load-dns-config.sh ]]; then
source /srv/load-dns-config.sh
if declare -F log_f >/dev/null; then
log_f "ACME_DNS_CHALLENGE is enabled, DNS provider secrets loaded"
fi
fi
TYPE=${1}
PREFIX=""
# only support rsa certificates for now
if [[ "${TYPE}" != "rsa" ]]; then
log_f "Unknown certificate type '${TYPE}' requested"
exit 5
fi
if [[ -z "${ACME_DNS_PROVIDER}" ]]; then
log_f "ACME_DNS_PROVIDER is required when ACME_DNS_CHALLENGE is enabled"
exit 6
fi
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
KEY=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}key.pem
CSR=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}acme.csr
if [[ -z ${CERT_DOMAINS[*]} ]]; then
log_f "Missing CERT_DOMAINS to obtain a certificate"
exit 3
fi
if [[ "${LE_STAGING}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
if [[ ! -z "${DIRECTORY_URL}" ]]; then
log_f "Cannot use DIRECTORY_URL with LE_STAGING=y - ignoring DIRECTORY_URL"
fi
log_f "Using Let's Encrypt staging servers"
ACME_SH_SERVER_ARGS=("--staging")
elif [[ ! -z "${DIRECTORY_URL}" ]]; then
log_f "Using custom directory URL ${DIRECTORY_URL}"
ACME_SH_SERVER_ARGS=("--server" "${DIRECTORY_URL}")
else
log_f "Using Let's Encrypt production servers"
ACME_SH_SERVER_ARGS=("--server" "letsencrypt")
fi
if [[ -f ${DOMAINS_FILE} && "$(cat ${DOMAINS_FILE})" == "${CERT_DOMAINS[*]}" ]]; then
if [[ ! -f ${CERT} || ! -f "${KEY}" || -f "${ACME_BASE}/force_renew" ]]; then
log_f "Certificate ${CERT} doesn't exist yet or forced renewal - start obtaining"
elif ! openssl x509 -checkend 2592000 -noout -in ${CERT} > /dev/null; then
log_f "Certificate ${CERT} is due for renewal (< 30 days) - start renewing"
else
log_f "Certificate ${CERT} validation done, neither changed nor due for renewal."
exit 2
fi
else
log_f "Certificate ${CERT} missing or changed domains '${CERT_DOMAINS[*]}' - start obtaining"
fi
# Make backup
if [[ -f ${CERT} ]]; then
DATE=$(date +%Y-%m-%d_%H_%M_%S)
BACKUP_DIR=${ACME_BASE}/backups/${CERT_DOMAIN}/${PREFIX}${DATE}
log_f "Creating backups in ${BACKUP_DIR} ..."
mkdir -p ${BACKUP_DIR}/
[[ -f ${DOMAINS_FILE} ]] && cp ${DOMAINS_FILE} ${BACKUP_DIR}/
[[ -f ${CERT} ]] && cp ${CERT} ${BACKUP_DIR}/
[[ -f ${KEY} ]] && cp ${KEY} ${BACKUP_DIR}/
[[ -f ${CSR} ]] && cp ${CSR} ${BACKUP_DIR}/
fi
mkdir -p ${ACME_BASE}/${CERT_DOMAIN}
if [[ ! -f ${KEY} ]]; then
log_f "Copying shared private key for this certificate..."
cp ${SHARED_KEY} ${KEY}
chmod 600 ${KEY}
fi
# Generating CSR to keep layout parity with HTTP challenge flow
printf "[SAN]\nsubjectAltName=" > /tmp/_SAN
printf "DNS:%s," "${CERT_DOMAINS[@]}" >> /tmp/_SAN
sed -i '$s/,$//' /tmp/_SAN
openssl req -new -sha256 -key ${KEY} -subj "/" -reqexts SAN -config <(cat "$(openssl version -d | sed 's/.*\"\(.*\)\"/\1/g')/openssl.cnf" /tmp/_SAN) > ${CSR}
log_f "Checking resolver..."
until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do
sleep 2
done
log_f "Resolver OK"
ACME_SH_BIN_PATH=${ACME_SH_BIN:-/opt/acme.sh/acme.sh}
ACME_SH_WORK_HOME=${ACME_SH_CONFIG_HOME:-/var/lib/acme/acme-sh}
mkdir -p ${ACME_SH_WORK_HOME}
if [[ ! -x ${ACME_SH_BIN_PATH} ]]; then
log_f "acme.sh binary not found at ${ACME_SH_BIN_PATH}"
exit 7
fi
if [[ ! -f ${ACME_SH_WORK_HOME}/account.conf ]]; then
if [[ -z "${ACME_ACCOUNT_EMAIL}" ]]; then
log_f "ACME_ACCOUNT_EMAIL is required to register a new acme.sh account"
exit 8
fi
log_f "Registering acme.sh account for ${ACME_ACCOUNT_EMAIL}"
REGISTER_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}" "--register-account" "-m" "${ACME_ACCOUNT_EMAIL}")
REGISTER_CMD+=("${ACME_SH_SERVER_ARGS[@]}")
REGISTER_RESPONSE=$("${REGISTER_CMD[@]}" 2>&1)
if [[ $? -ne 0 ]]; then
log_f "Failed to register acme.sh account: ${REGISTER_RESPONSE}"
exit 9
fi
fi
TMP_CERT=$(mktemp /tmp/acme-cert.XXXXXX)
TMP_FULLCHAIN=$(mktemp /tmp/acme-fullchain.XXXXXX)
ACME_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}")
ACME_CMD+=("${ACME_SH_SERVER_ARGS[@]}")
ACME_CMD+=("--issue" "--dns" "${ACME_DNS_PROVIDER}" "--key-file" "${KEY}" "--cert-file" "${TMP_CERT}" "--fullchain-file" "${TMP_FULLCHAIN}" "--force")
for domain in "${CERT_DOMAINS[@]}"; do
ACME_CMD+=("-d" "${domain}")
done
log_f "Using command ${ACME_CMD[*]}"
if [[ -n "${ACME_DNS_PROVIDER}" ]]; then
log_f "DNS provider: ${ACME_DNS_PROVIDER}"
fi
if compgen -A variable | grep -Eq "^DNS_|^ACME_"; then
LOG_KEYS=$(env | grep -E "^(DNS_|ACME_)" | cut -d= -f1 | tr '\n' ' ')
log_f "Available DNS/ACME env keys: ${LOG_KEYS}" redis_only
fi
ACME_RESPONSE=$("${ACME_CMD[@]}" 2>&1 | tee /dev/fd/5; exit ${PIPESTATUS[0]})
SUCCESS="$?"
ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
log_f "${ACME_RESPONSE_B64}" redis_only b64
case "$SUCCESS" in
0)
log_f "Deploying certificate ${CERT}..."
if verify_hash_match ${TMP_FULLCHAIN} ${KEY}; then
RETURN=0
if [[ -f ${CERT} ]]; then
RETURN=1
fi
mv -f ${TMP_FULLCHAIN} ${CERT}
rm -f ${TMP_CERT}
echo -n ${CERT_DOMAINS[*]} > ${DOMAINS_FILE}
log_f "Certificate successfully obtained via DNS challenge"
exit ${RETURN}
else
log_f "Certificate was requested, but key and certificate hashes do not match"
rm -f ${TMP_CERT} ${TMP_FULLCHAIN}
exit 4
fi
;;
*)
log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}' via DNS challenge"
redis-cli -h redis -a ${REDISPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)"
rm -f ${TMP_CERT} ${TMP_FULLCHAIN}
exit 100${SUCCESS}
;;
esac

View File

@@ -20,10 +20,6 @@ if [[ "${TYPE}" != "rsa" ]]; then
log_f "Unknown certificate type '${TYPE}' requested" log_f "Unknown certificate type '${TYPE}' requested"
exit 5 exit 5
fi fi
if [[ "${ACME_DNS_CHALLENGE}" == "y" ]]; then
exec /srv/obtain-certificate-dns.sh "$@"
fi
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
@@ -105,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]}) --acme-dir /var/www/acme/ 2>&1 > /tmp/_cert.pem | tee /dev/fd/5; exit ${PIPESTATUS[0]})
SUCCESS="$?" SUCCESS="$?"
ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64) ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
log_f "${ACME_RESPONSE_B64}" redis_only b64 log_f "${ACME_RESPONSE_B64}" valkey_only b64
case "$SUCCESS" in case "$SUCCESS" in
0) # cert requested 0) # cert requested
log_f "Deploying certificate ${CERT}..." log_f "Deploying certificate ${CERT}..."
@@ -128,7 +124,7 @@ case "$SUCCESS" in
;; ;;
*) # non-zero is non-fun *) # non-zero is non-fun
log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}'" log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}'"
redis-cli -h redis -a ${REDISPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)" redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)"
exit 100${SUCCESS} exit 100${SUCCESS}
;; ;;
esac esac

View File

@@ -1,3 +1,3 @@
FROM debian:trixie-slim FROM debian:bookworm-slim
RUN apt update && apt install pigz zstd -y --no-install-recommends RUN apt update && apt install pigz -y --no-install-recommends

View File

@@ -1,4 +1,4 @@
FROM alpine:3.23 FROM alpine:3.21
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"

View File

@@ -32,21 +32,21 @@ async def lifespan(app: FastAPI):
logger.info("Init APP") logger.info("Init APP")
# Init redis client # Init valkey client
if os.environ['REDIS_SLAVEOF_IP'] != "": if os.environ['VALKEY_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']) valkey_client = valkey = await aioredis.from_url(f"redis://{os.environ['VALKEY_SLAVEOF_IP']}:{os.environ['VALKEY_SLAVEOF_PORT']}/0", password=os.environ['VALKEYPASS'])
else: else:
redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0", password=os.environ['REDISPASS']) valkey_client = valkey = await aioredis.from_url("redis://valkey-mailcow:6379/0", password=os.environ['VALKEYPASS'])
# Init docker clients # Init docker clients
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto') 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') async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger) dockerapi = DockerApi(valkey_client, sync_docker_client, async_docker_client, logger)
logger.info("Subscribe to redis channel") logger.info("Subscribe to valkey channel")
# Subscribe to redis channel # Subscribe to valkey channel
dockerapi.pubsub = redis.pubsub() dockerapi.pubsub = valkey.pubsub()
await dockerapi.pubsub.subscribe("MC_CHANNEL") await dockerapi.pubsub.subscribe("MC_CHANNEL")
asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub)) asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
@@ -57,9 +57,9 @@ async def lifespan(app: FastAPI):
dockerapi.sync_docker_client.close() dockerapi.sync_docker_client.close()
await dockerapi.async_docker_client.close() await dockerapi.async_docker_client.close()
# Close redis # Close valkey
await dockerapi.pubsub.unsubscribe("MC_CHANNEL") await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
await dockerapi.redis_client.close() await dockerapi.valkey_client.close()
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
@@ -73,11 +73,11 @@ async def get_host_update_stats():
dockerapi.host_stats_isUpdating = True dockerapi.host_stats_isUpdating = True
while True: while True:
if await dockerapi.redis_client.exists('host_stats'): if await dockerapi.valkey_client.exists('host_stats'):
break break
await asyncio.sleep(1.5) await asyncio.sleep(1.5)
stats = json.loads(await dockerapi.redis_client.get('host_stats')) stats = json.loads(await dockerapi.valkey_client.get('host_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json") return Response(content=json.dumps(stats, indent=4), media_type="application/json")
@app.get("/containers/{container_id}/json") @app.get("/containers/{container_id}/json")
@@ -110,12 +110,12 @@ async def get_container(container_id : str):
return Response(content=json.dumps(res, indent=4), media_type="application/json") return Response(content=json.dumps(res, indent=4), media_type="application/json")
@app.get("/containers/json") @app.get("/containers/json")
async def get_containers(all: bool = False): async def get_containers():
global dockerapi global dockerapi
containers = {} containers = {}
try: try:
for container in (await dockerapi.async_docker_client.containers.list(all=all)): for container in (await dockerapi.async_docker_client.containers.list()):
container_info = await container.show() container_info = await container.show()
containers.update({container_info['Id']: container_info}) containers.update({container_info['Id']: container_info})
return Response(content=json.dumps(containers, indent=4), media_type="application/json") return Response(content=json.dumps(containers, indent=4), media_type="application/json")
@@ -185,11 +185,11 @@ async def post_container_update_stats(container_id : str):
dockerapi.containerIds_to_update.append(container_id) dockerapi.containerIds_to_update.append(container_id)
while True: while True:
if await dockerapi.redis_client.exists(container_id + '_stats'): if await dockerapi.valkey_client.exists(container_id + '_stats'):
break break
await asyncio.sleep(1.5) await asyncio.sleep(1.5)
stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats')) stats = json.loads(await dockerapi.valkey_client.get(container_id + '_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json") 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 from fastapi import FastAPI, Response, Request
class DockerApi: class DockerApi:
def __init__(self, redis_client, sync_docker_client, async_docker_client, logger): def __init__(self, valkey_client, sync_docker_client, async_docker_client, logger):
self.redis_client = redis_client self.valkey_client = valkey_client
self.sync_docker_client = sync_docker_client self.sync_docker_client = sync_docker_client
self.async_docker_client = async_docker_client self.async_docker_client = async_docker_client
self.logger = logger self.logger = logger
@@ -533,7 +533,7 @@ class DockerApi:
"architecture": platform.machine() "architecture": platform.machine()
} }
await self.redis_client.set('host_stats', json.dumps(host_stats), ex=10) await self.valkey_client.set('host_stats', json.dumps(host_stats), ex=10)
except Exception as e: except Exception as e:
res = { res = {
"type": "danger", "type": "danger",
@@ -550,14 +550,14 @@ class DockerApi:
if container._id == container_id: if container._id == container_id:
res = await container.stats(stream=False) res = await container.stats(stream=False)
if await self.redis_client.exists(container_id + '_stats'): if await self.valkey_client.exists(container_id + '_stats'):
stats = json.loads(await self.redis_client.get(container_id + '_stats')) stats = json.loads(await self.valkey_client.get(container_id + '_stats'))
else: else:
stats = [] stats = []
stats.append(res[0]) stats.append(res[0])
if len(stats) > 3: if len(stats) > 3:
del stats[0] del stats[0]
await self.redis_client.set(container_id + '_stats', json.dumps(stats), ex=60) await self.valkey_client.set(container_id + '_stats', json.dumps(stats), ex=60)
except Exception as e: except Exception as e:
res = { res = {
"type": "danger", "type": "danger",

View File

@@ -3,7 +3,7 @@ FROM alpine:3.21
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$ # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.19 ARG GOSU_VERSION=1.17
ENV LANG=C.UTF-8 ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8 ENV LC_ALL=C.UTF-8
@@ -118,7 +118,7 @@ RUN addgroup -g 5000 vmail \
COPY trim_logs.sh /usr/local/bin/trim_logs.sh COPY trim_logs.sh /usr/local/bin/trim_logs.sh
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.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.conf /etc/syslog-ng/syslog-ng.conf
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf COPY syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf
COPY imapsync /usr/local/bin/imapsync COPY imapsync /usr/local/bin/imapsync
COPY imapsync_runner.pl /usr/local/bin/imapsync_runner.pl COPY imapsync_runner.pl /usr/local/bin/imapsync_runner.pl
COPY report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve COPY report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve

View File

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

View File

@@ -13,18 +13,18 @@ until dig +short mailcow.email > /dev/null; do
done done
# Do not attempt to write to slave # Do not attempt to write to slave
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
else else
REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning"
fi fi
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..." echo "Waiting for Valkey..."
sleep 2 sleep 2
done done
${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null ${VALKEY_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
# Create missing directories # Create missing directories
[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/ [[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
@@ -204,17 +204,16 @@ EOF
# Create random master Password for SOGo SSO # Create random master Password for SOGo SSO
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1) RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
# Creating additional creds file for SOGo notify crons (calendars, etc)
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
cat <<EOF > /etc/dovecot/sogo-sso.conf cat <<EOF > /etc/dovecot/sogo-sso.conf
# Autogenerated by mailcow # Autogenerated by mailcow
passdb { passdb {
driver = static driver = static
args = allow_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS} args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
} }
EOF EOF
# Creating additional creds file for SOGo notify crons (calendars, etc) (dummy user, sso password)
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then
# Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated # Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated
cat <<'EOF' > /usr/local/bin/quota_notify.py cat <<'EOF' > /usr/local/bin/quota_notify.py
@@ -342,8 +341,8 @@ done
# May be related to something inside Docker, I seriously don't know # May be related to something inside Docker, I seriously don't know
touch /etc/dovecot/auth/passwd-verify.lua touch /etc/dovecot/auth/passwd-verify.lua
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf
fi fi
exec "$@" exec "$@"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
FROM alpine:3.23 FROM alpine:3.21
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"

View File

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

View File

@@ -4,19 +4,19 @@ import datetime
class Logger: class Logger:
def __init__(self): def __init__(self):
self.r = None self.valkey = None
def set_redis(self, redis): def set_valkey(self, valkey):
self.r = redis self.valkey = valkey
def _format_timestamp(self): def _format_timestamp(self):
# Local time with milliseconds # Local time with milliseconds
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def log(self, priority, message): def log(self, priority, message):
# build redis-friendly dict # build valkey-friendly dict
tolog = { tolog = {
'time': int(round(time.time())), # keep raw timestamp for Redis 'time': int(round(time.time())), # keep raw timestamp for Valkey
'priority': priority, 'priority': priority,
'message': message 'message': message
} }
@@ -26,11 +26,11 @@ class Logger:
print(f"{ts} {priority.upper()}: {message}", flush=True) print(f"{ts} {priority.upper()}: {message}", flush=True)
# also push JSON to Redis if connected # also push JSON to Redis if connected
if self.r is not None: if self.valkey is not None:
try: try:
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False)) self.valkey.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
except Exception as ex: except Exception as ex:
print(f'{ts} WARN: Failed logging to redis: {ex}', flush=True) print(f'{ts} WARN: Failed logging to valkey: {ex}', flush=True)
def logWarn(self, message): def logWarn(self, message):
self.log('warn', message) self.log('warn', message)

View File

@@ -3,17 +3,17 @@ FROM php:8.2-fpm-alpine3.21
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG APCU_PECL_VERSION=5.1.28 ARG APCU_PECL_VERSION=5.1.26
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$ # renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
ARG IMAGICK_PECL_VERSION=3.8.1 ARG IMAGICK_PECL_VERSION=3.8.0
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MAILPARSE_PECL_VERSION=3.1.9 ARG MAILPARSE_PECL_VERSION=3.1.8
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MEMCACHED_PECL_VERSION=3.4.0 ARG MEMCACHED_PECL_VERSION=3.3.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$ # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
ARG REDIS_PECL_VERSION=6.3.0 ARG REDIS_PECL_VERSION=6.2.0
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$ # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
ARG COMPOSER_VERSION=2.9.5 ARG COMPOSER_VERSION=2.8.6
RUN apk add -U --no-cache autoconf \ RUN apk add -U --no-cache autoconf \
aspell-dev \ aspell-dev \

View File

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

View File

@@ -4,7 +4,7 @@ WORKDIR /src
ENV CGO_ENABLED=0 \ ENV CGO_ENABLED=0 \
GO111MODULE=on \ GO111MODULE=on \
NOOPT=1 \ NOOPT=1 \
VERSION=1.8.22 VERSION=1.8.14
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \ RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
cd /src/postfix-tlspol && \ cd /src/postfix-tlspol && \
@@ -34,7 +34,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
COPY supervisord.conf /etc/supervisor/supervisord.conf COPY supervisord.conf /etc/supervisor/supervisord.conf
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf COPY syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf
COPY postfix-tlspol.sh /opt/postfix-tlspol.sh COPY postfix-tlspol.sh /opt/postfix-tlspol.sh
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
COPY docker-entrypoint.sh /docker-entrypoint.sh COPY docker-entrypoint.sh /docker-entrypoint.sh

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf
fi fi
exec "$@" exec "$@"

View File

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

View File

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

View File

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

View File

@@ -41,7 +41,7 @@ RUN groupadd -g 102 postfix \
COPY supervisord.conf /etc/supervisor/supervisord.conf COPY supervisord.conf /etc/supervisor/supervisord.conf
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf COPY syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf
COPY postfix.sh /opt/postfix.sh COPY postfix.sh /opt/postfix.sh
COPY rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham COPY rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham
COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam

View File

@@ -8,8 +8,8 @@ for file in /hooks/*; do
fi fi
done done
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf
fi fi
# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20) # Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)

View File

@@ -329,17 +329,14 @@ query = SELECT goto FROM alias
SELECT id FROM alias SELECT id FROM alias
WHERE address='%s' WHERE address='%s'
AND (active='1' OR active='2') AND (active='1' OR active='2')
AND sender_allowed='1'
), ( ), (
SELECT id FROM alias SELECT id FROM alias
WHERE address='@%d' WHERE address='@%d'
AND (active='1' OR active='2') AND (active='1' OR active='2')
AND sender_allowed='1'
) )
) )
) )
AND active='1' AND active='1'
AND sender_allowed='1'
AND (domain IN AND (domain IN
(SELECT domain FROM domain (SELECT domain FROM domain
WHERE domain='%d' WHERE domain='%d'
@@ -393,7 +390,7 @@ hosts = unix:/var/run/mysqld/mysqld.sock
dbname = ${DBNAME} dbname = ${DBNAME}
query = SELECT goto FROM spamalias query = SELECT goto FROM spamalias
WHERE address='%s' WHERE address='%s'
AND (validity >= UNIX_TIMESTAMP() OR permanent != 0) AND validity >= UNIX_TIMESTAMP()
EOF EOF
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
FROM debian:trixie-slim FROM debian:bookworm-slim
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG RSPAMD_VER=rspamd_3.14.3-1~236eb65 ARG RSPAMD_VER=rspamd_3.12.1-1~6dbfca2fa
ARG CODENAME=trixie ARG CODENAME=bookworm
ENV LC_ALL=C ENV LC_ALL=C
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \

View File

@@ -52,33 +52,33 @@ if [[ ! -z ${RSPAMD_V6} ]]; then
echo ${RSPAMD_V6}/128 >> /etc/rspamd/custom/rspamd_trusted.map echo ${RSPAMD_V6}/128 >> /etc/rspamd/custom/rspamd_trusted.map
fi fi
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
cat <<EOF > /etc/rspamd/local.d/redis.conf cat <<EOF > /etc/rspamd/local.d/redis.conf
read_servers = "redis:6379"; read_servers = "valkey-mailcow:6379";
write_servers = "${REDIS_SLAVEOF_IP}:${REDIS_SLAVEOF_PORT}"; write_servers = "${VALKEY_SLAVEOF_IP}:${VALKEY_SLAVEOF_PORT}";
password = "${REDISPASS}"; password = "${VALKEYPASS}";
timeout = 10; timeout = 10;
EOF EOF
until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do until [[ $(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Redis @redis-mailcow..." echo "Waiting for Valkey @valkey-mailcow..."
sleep 2 sleep 2
done done
until [[ $(redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do until [[ $(redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Redis @${REDIS_SLAVEOF_IP}..." echo "Waiting for Valkey @${VALKEY_SLAVEOF_IP}..."
sleep 2 sleep 2
done done
redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF ${REDIS_SLAVEOF_IP} ${REDIS_SLAVEOF_PORT} redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning SLAVEOF ${VALKEY_SLAVEOF_IP} ${VALKEY_SLAVEOF_PORT}
else else
cat <<EOF > /etc/rspamd/local.d/redis.conf cat <<EOF > /etc/rspamd/local.d/redis.conf
servers = "redis:6379"; servers = "valkey-mailcow:6379";
password = "${REDISPASS}"; password = "${VALKEYPASS}";
timeout = 10; timeout = 10;
EOF EOF
until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do until [[ $(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Redis slave..." echo "Waiting for Valkey slave..."
sleep 2 sleep 2
done done
redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF NO ONE redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning SLAVEOF NO ONE
fi fi
if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then

View File

@@ -1,170 +1,50 @@
# SOGo built from source to enable security patch application FROM debian:bookworm-slim
# Repository: https://github.com/Alinto/sogo
# Version: SOGo-5.12.4
#
# Applied security patches:
# -
#
# To add new patches, modify SOGO_SECURITY_PATCHES ARG below with space-separated commit hashes
FROM debian:bookworm
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG SOGO_VERSION=SOGo-5.12.5 ARG DEBIAN_VERSION=bookworm
ARG SOPE_VERSION=SOPE-5.12.5 ARG SOGO_DEBIAN_REPOSITORY=https://packagingv2.sogo.nu/sogo-nightly-debian/
# Security patches to apply (space-separated commit hashes)
ARG SOGO_SECURITY_PATCHES=""
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$ # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.19 ARG GOSU_VERSION=1.17
ENV LC_ALL=C ENV LC_ALL=C
# Install dependencies, build SOPE and SOGo, then clean up (all in one layer to minimize image size) # Prerequisites
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
# Build dependencies && apt-get update && apt-get install -y --no-install-recommends \
git \ apt-transport-https \
build-essential \ ca-certificates \
gobjc \ gettext \
pkg-config \ gnupg \
gnustep-make \ mariadb-client \
gnustep-base-runtime \ rsync \
libgnustep-base-dev \ supervisor \
libxml2-dev \ syslog-ng \
libldap2-dev \ syslog-ng-core \
libssl-dev \ syslog-ng-mod-redis \
zlib1g-dev \ dirmngr \
libpq-dev \ netcat-traditional \
libmariadb-dev-compat \ psmisc \
libmemcached-dev \ wget \
libsodium-dev \ patch \
libcurl4-openssl-dev \
libzip-dev \
libytnef0-dev \
libwbxml2-dev \
curl \
ca-certificates \
# Runtime dependencies
apt-transport-https \
gettext \
gnupg \
mariadb-client \
rsync \
supervisor \
syslog-ng \
syslog-ng-core \
syslog-ng-mod-redis \
dirmngr \
netcat-traditional \
psmisc \
wget \
patch \
libobjc4 \
libxml2 \
libldap-2.5-0 \
libssl3 \
zlib1g \
libmariadb3 \
libmemcached11 \
libsodium23 \
libcurl4 \
libzip4 \
libytnef0 \
libwbxml2-1 \
# Download gosu
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \ && dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \ && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
&& chmod +x /usr/local/bin/gosu \ && chmod +x /usr/local/bin/gosu \
&& gosu nobody true \ && gosu nobody true \
# Build SOPE && mkdir /usr/share/doc/sogo \
&& git clone --depth 1 --branch ${SOPE_VERSION} https://github.com/Alinto/sope.git /tmp/sope \
&& cd /tmp/sope \
&& rm -rf .git \
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
&& ./configure --prefix=/usr --disable-debug --disable-strip \
&& make -j$(nproc) \
&& make install \
&& cd / \
&& rm -rf /tmp/sope \
# Build SOGo with security patches
&& git clone --depth 1 --branch ${SOGO_VERSION} https://github.com/Alinto/sogo.git /tmp/sogo \
&& cd /tmp/sogo \
&& git config user.email "builder@mailcow.local" \
&& git config user.name "SOGo Builder" \
&& for patch in ${SOGO_SECURITY_PATCHES}; do \
echo "Applying security patch: ${patch}"; \
git fetch origin ${patch} && git cherry-pick ${patch}; \
done \
&& rm -rf .git \
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
&& ./configure --disable-debug --disable-strip \
&& make -j$(nproc) \
&& make install \
&& cd /tmp/sogo/ActiveSync \
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
&& make -j$(nproc) install \
&& cd / \
&& rm -rf /tmp/sogo \
# Strip binaries
&& strip --strip-unneeded /usr/local/sbin/sogod 2>/dev/null || true \
&& strip --strip-unneeded /usr/local/sbin/sogo-tool 2>/dev/null || true \
&& strip --strip-unneeded /usr/local/sbin/sogo-ealarms-notify 2>/dev/null || true \
&& strip --strip-unneeded /usr/local/sbin/sogo-slapd-sockd 2>/dev/null || true \
# Remove build dependencies and clean up
&& apt-get purge -y --auto-remove \
git \
build-essential \
gobjc \
gnustep-make \
libgnustep-base-dev \
libxml2-dev \
libldap2-dev \
libssl-dev \
zlib1g-dev \
libpq-dev \
libmariadb-dev-compat \
libmemcached-dev \
libsodium-dev \
libcurl4-openssl-dev \
libzip-dev \
libytnef0-dev \
curl \
&& apt-get autoremove -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /usr/share/doc/* \
&& rm -rf /usr/share/man/* \
&& rm -rf /var/cache/debconf/* \
&& rm -rf /tmp/* \
&& rm -rf /root/.cache \
&& find /usr/local/lib -name '*.a' -delete \
&& find /usr/lib -name '*.a' -delete \
&& mkdir -p /usr/share/doc/sogo \
&& touch /usr/share/doc/sogo/empty.sh \ && touch /usr/share/doc/sogo/empty.sh \
&& wget -O- https://keys.openpgp.org/vks/v1/by-fingerprint/74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 | gpg --dearmor | apt-key add - \
&& echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} main" > /etc/apt/sources.list.d/sogo.list \
&& apt-get update && apt-get install -y --no-install-recommends \
sogo \
sogo-activesync \
&& apt-get autoclean \
&& rm -rf /var/lib/apt/lists/* \
&& touch /etc/default/locale && touch /etc/default/locale
# Configure library paths
RUN echo "/usr/lib64" > /etc/ld.so.conf.d/sogo.conf \
&& echo "/usr/local/lib/sogo" >> /etc/ld.so.conf.d/sogo.conf \
&& echo "/usr/local/lib/GNUstep/Frameworks/SOGo.framework/Versions/5/sogo" >> /etc/ld.so.conf.d/sogo.conf \
&& ldconfig
# Create sogo user and group
RUN groupadd -r -g 999 sogo \
&& useradd -r -u 999 -g sogo -d /var/lib/sogo -s /bin/bash -c "SOGo Daemon" sogo \
&& mkdir -p /var/lib/sogo /var/run/sogo /var/log/sogo /var/spool/sogo \
&& chown -R sogo:sogo /var/lib/sogo /var/run/sogo /var/log/sogo /var/spool/sogo
# Create symlinks for SOGo binaries
RUN ln -s /usr/local/sbin/sogod /usr/sbin/sogod \
&& ln -s /usr/local/sbin/sogo-tool /usr/sbin/sogo-tool \
&& ln -s /usr/local/sbin/sogo-ealarms-notify /usr/sbin/sogo-ealarms-notify \
&& ln -s /usr/local/sbin/sogo-slapd-sockd /usr/sbin/sogo-slapd-sockd
# Copy configuration files and scripts
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf COPY syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf
COPY supervisord.conf /etc/supervisor/supervisord.conf COPY supervisord.conf /etc/supervisor/supervisord.conf
COPY acl.diff /acl.diff COPY acl.diff /acl.diff
COPY navMailcowBtns.diff /navMailcowBtns.diff COPY navMailcowBtns.diff /navMailcowBtns.diff

View File

@@ -1,5 +1,5 @@
--- /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200 --- /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200
+++ /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200 +++ /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200
@@ -46,7 +46,7 @@ @@ -46,7 +46,7 @@
</md-item-template> </md-item-template>
</md-autocomplete> </md-autocomplete>

View File

@@ -50,6 +50,10 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
<string>YES</string> <string>YES</string>
<key>SOGoEncryptionKey</key> <key>SOGoEncryptionKey</key>
<string>${RAND_PASS}</string> <string>${RAND_PASS}</string>
<key>SOGoURLEncryptionEnabled</key>
<string>YES</string>
<key>SOGoURLEncryptionPassphrase</key>
<string>${SOGO_URL_ENCRYPTION_KEY}</string>
<key>OCSAdminURL</key> <key>OCSAdminURL</key>
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string> <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
<key>OCSCacheFolderURL</key> <key>OCSCacheFolderURL</key>
@@ -130,22 +134,18 @@ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
# Patch ACLs # Patch ACLs
#if [[ ${ACL_ANYONE} == 'allow' ]]; then #if [[ ${ACL_ANYONE} == 'allow' ]]; then
# #enable any or authenticated targets for ACL # #enable any or authenticated targets for ACL
# if patch -R -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then # if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
# patch -R /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; # patch -R /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
# fi # fi
#else #else
# #disable any or authenticated targets for ACL # #disable any or authenticated targets for ACL
# if patch -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then # if patch -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
# patch /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; # patch /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
# fi # fi
#fi #fi
# Apply custom UI patch (reverse patch to ADD buttons) if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then
if patch -R -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then patch -R /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
echo "Applying navMailcowBtns patch (reverse to add buttons)..."
patch -R /usr/local/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
else
echo "navMailcowBtns patch already applied or cannot be applied"
fi fi
# Rename custom logo, if any # Rename custom logo, if any
@@ -153,7 +153,7 @@ fi
# Rsync web content # Rsync web content
echo "Syncing web content with named volume" echo "Syncing web content with named volume"
rsync -a /usr/local/lib/GNUstep/SOGo/. /sogo_web/ rsync -a /usr/lib/GNUstep/SOGo/. /sogo_web/
# Chown backup path # Chown backup path
chown -R sogo:sogo /sogo_backup chown -R sogo:sogo /sogo_backup

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
FROM alpine:3.23 FROM alpine:3.21
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"

View File

@@ -0,0 +1,8 @@
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

@@ -0,0 +1,78 @@
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

@@ -1,4 +1,4 @@
FROM alpine:3.23 FROM alpine:3.21
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
@@ -37,6 +37,5 @@ RUN apk add --update \
COPY watchdog.sh /watchdog.sh COPY watchdog.sh /watchdog.sh
COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
COPY check_dns.sh /usr/lib/mailcow/check_dns.sh COPY check_dns.sh /usr/lib/mailcow/check_dns.sh
COPY client.cnf /etc/my.cnf.d/client.cnf
CMD ["/watchdog.sh"] CMD ["/watchdog.sh"]

View File

@@ -19,19 +19,19 @@ if [ -z "$HOST" ]; then
fi fi
# run dig and measure the time it takes to run # run dig and measure the time it takes to run
START_TIME=$(perl -MTime::HiRes -e 'print Time::HiRes::time') START_TIME=$(date +%s%3N)
dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null) dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null)
dig_rc=$? dig_rc=$?
END_TIME=$(perl -MTime::HiRes -e 'print Time::HiRes::time')
dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -) dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -)
ELAPSED_TIME=$(perl -e "printf('%.3f', $END_TIME - $START_TIME)") END_TIME=$(date +%s%3N)
ELAPSED_TIME=$((END_TIME - START_TIME))
# validate and perform nagios like output and exit codes # validate and perform nagios like output and exit codes
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
echo "Domain $HOST was not found by the server" echo "Domain $HOST was not found by the server"
exit 2 exit 2
elif [ $dig_rc -eq 0 ]; then elif [ $dig_rc -eq 0 ]; then
echo "DNS OK: $ELAPSED_TIME seconds response time. $HOST returns $dig_output_ips" echo "DNS OK: $ELAPSED_TIME ms response time. $HOST returns $dig_output_ips"
exit 0 exit 0
else else
echo "Unknown error" echo "Unknown error"

View File

@@ -1,3 +0,0 @@
[client]
ssl = false
ssl-verify-server-cert = false

View File

@@ -38,24 +38,24 @@ if [[ ! -p /tmp/com_pipe ]]; then
fi fi
# Wait for containers # Wait for containers
while ! mariadb-admin status --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
echo "Waiting for SQL..." echo "Waiting for SQL..."
sleep 2 sleep 2
done done
# Do not attempt to write to slave # Do not attempt to write to slave
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
else else
REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning"
fi fi
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..." echo "Waiting for Valkey..."
sleep 2 sleep 2
done done
${REDIS_CMDLINE} DEL F2B_RES > /dev/null ${VALKEY_CMDLINE} DEL F2B_RES > /dev/null
# Common functions # Common functions
get_ipv6(){ get_ipv6(){
@@ -90,15 +90,15 @@ progress() {
[[ ${CURRENT} -gt ${TOTAL} ]] && return [[ ${CURRENT} -gt ${TOTAL} ]] && return
[[ ${CURRENT} -lt 0 ]] && CURRENT=0 [[ ${CURRENT} -lt 0 ]] && CURRENT=0
PERCENT=$(( 200 * ${CURRENT} / ${TOTAL} % 2 + 100 * ${CURRENT} / ${TOTAL} )) PERCENT=$(( 200 * ${CURRENT} / ${TOTAL} % 2 + 100 * ${CURRENT} / ${TOTAL} ))
${REDIS_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"service\":\"${SERVICE}\",\"lvl\":\"${PERCENT}\",\"hpnow\":\"${CURRENT}\",\"hptotal\":\"${TOTAL}\",\"hpdiff\":\"${DIFF}\"}" > /dev/null ${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_redis log_msg "${SERVICE} health level: ${PERCENT}% (${CURRENT}/${TOTAL}), health trend: ${DIFF}" no_valkey
# Return 10 to indicate a dead service # Return 10 to indicate a dead service
[ ${CURRENT} -le 0 ] && return 10 [ ${CURRENT} -le 0 ] && return 10
} }
log_msg() { log_msg() {
if [[ ${2} != "no_redis" ]]; then if [[ ${2} != "no_valkey" ]]; then
${REDIS_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \ ${VALKEY_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \
tr '\r\n%&;$"_[]{}-' ' ')\"}" > /dev/null tr '\r\n%&;$"_[]{}-' ' ')\"}" > /dev/null
fi fi
echo $(date) $(printf '%s\n' "${1}") echo $(date) $(printf '%s\n' "${1}")
@@ -114,10 +114,10 @@ function notify_error() {
# If exists, mail will be throttled by argument in seconds # If exists, mail will be throttled by argument in seconds
[[ ! -z ${3} ]] && THROTTLE=${3} [[ ! -z ${3} ]] && THROTTLE=${3}
if [[ ! -z ${THROTTLE} ]]; then if [[ ! -z ${THROTTLE} ]]; then
TTL_LEFT="$(${REDIS_CMDLINE} TTL THROTTLE_${1} 2> /dev/null)" TTL_LEFT="$(${VALKEY_CMDLINE} TTL THROTTLE_${1} 2> /dev/null)"
if [[ "${TTL_LEFT}" == "-2" ]]; then if [[ "${TTL_LEFT}" == "-2" ]]; then
# Delay key not found, setting a delay key now # Delay key not found, setting a delay key now
${REDIS_CMDLINE} SET THROTTLE_${1} 1 EX ${THROTTLE} ${VALKEY_CMDLINE} SET THROTTLE_${1} 1 EX ${THROTTLE}
else else
log_msg "Not sending notification email now, blocked for ${TTL_LEFT} seconds..." log_msg "Not sending notification email now, blocked for ${TTL_LEFT} seconds..."
return 1 return 1
@@ -324,21 +324,21 @@ unbound_checks() {
return 1 return 1
} }
redis_checks() { valkey_checks() {
# A check for the local redis container # A check for the local valkey container
err_count=0 err_count=0
diff_c=0 diff_c=0
THRESHOLD=${REDIS_THRESHOLD} THRESHOLD=${VALKEY_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container # Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/redis-mailcow; echo "$(tail -50 /tmp/redis-mailcow)" > /tmp/redis-mailcow touch /tmp/valkey-mailcow; echo "$(tail -50 /tmp/valkey-mailcow)" > /tmp/valkey-mailcow
host_ip=$(get_container_ip redis-mailcow) host_ip=$(get_container_ip valkey-mailcow)
err_c_cur=${err_count} err_c_cur=${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} + $? )) /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} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 [ ${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} )) [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Redis" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} progress "Valkey" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then if [[ $? == 10 ]]; then
diff_c=0 diff_c=0
sleep 1 sleep 1
@@ -359,8 +359,8 @@ mysql_checks() {
while [ ${err_count} -lt ${THRESHOLD} ]; do while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/mysql-mailcow; echo "$(tail -50 /tmp/mysql-mailcow)" > /tmp/mysql-mailcow touch /tmp/mysql-mailcow; echo "$(tail -50 /tmp/mysql-mailcow)" > /tmp/mysql-mailcow
err_c_cur=${err_count} err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_mysql -f /etc/my.cnf.d/client.cnf -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? )) /usr/lib/nagios/plugins/check_mysql -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
/usr/lib/nagios/plugins/check_mysql_query -f /etc/my.cnf.d/client.cnf -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? )) /usr/lib/nagios/plugins/check_mysql_query -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 [ ${err_c_cur} -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} )) [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "MySQL/MariaDB" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} progress "MySQL/MariaDB" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
@@ -384,7 +384,7 @@ mysql_repl_checks() {
while [ ${err_count} -lt ${THRESHOLD} ]; do while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/mysql_repl_checks; echo "$(tail -50 /tmp/mysql_repl_checks)" > /tmp/mysql_repl_checks touch /tmp/mysql_repl_checks; echo "$(tail -50 /tmp/mysql_repl_checks)" > /tmp/mysql_repl_checks
err_c_cur=${err_count} err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_mysql_slavestatus.sh -o /etc/my.cnf.d/client.cnf -S /var/run/mysqld/mysqld.sock -u root -p ${DBROOT} 2>> /tmp/mysql_repl_checks 1>&2; err_count=$(( ${err_count} + $? )) /usr/lib/nagios/plugins/check_mysql_slavestatus.sh -S /var/run/mysqld/mysqld.sock -u root -p ${DBROOT} 2>> /tmp/mysql_repl_checks 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 [ ${err_c_cur} -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} )) [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "MySQL/MariaDB replication" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} progress "MySQL/MariaDB replication" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
@@ -533,12 +533,12 @@ dovecot_repl_checks() {
err_count=0 err_count=0
diff_c=0 diff_c=0
THRESHOLD=${DOVECOT_REPL_THRESHOLD} THRESHOLD=${DOVECOT_REPL_THRESHOLD}
D_REPL_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning -r GET DOVECOT_REPL_HEALTH) D_REPL_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning -r GET DOVECOT_REPL_HEALTH)
# Reduce error count by 2 after restarting an unhealthy container # Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count} err_c_cur=${err_count}
D_REPL_STATUS=$(redis-cli --raw -h redis -a ${REDISPASS} --no-auth-warning GET DOVECOT_REPL_HEALTH) D_REPL_STATUS=$(redis-cli --raw -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET DOVECOT_REPL_HEALTH)
if [[ "${D_REPL_STATUS}" != "1" ]]; then if [[ "${D_REPL_STATUS}" != "1" ]]; then
err_count=$(( ${err_count} + 1 )) err_count=$(( ${err_count} + 1 ))
fi fi
@@ -608,19 +608,19 @@ ratelimit_checks() {
err_count=0 err_count=0
diff_c=0 diff_c=0
THRESHOLD=${RATELIMIT_THRESHOLD} THRESHOLD=${RATELIMIT_THRESHOLD}
RL_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid) RL_LOG_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid)
# Reduce error count by 2 after restarting an unhealthy container # Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count} err_c_cur=${err_count}
RL_LOG_STATUS_PREV=${RL_LOG_STATUS} RL_LOG_STATUS_PREV=${RL_LOG_STATUS}
RL_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid) RL_LOG_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid)
if [[ ${RL_LOG_STATUS_PREV} != ${RL_LOG_STATUS} ]]; then if [[ ${RL_LOG_STATUS_PREV} != ${RL_LOG_STATUS} ]]; then
err_count=$(( ${err_count} + 1 )) err_count=$(( ${err_count} + 1 ))
echo 'Last 10 applied ratelimits (may overlap with previous reports).' > /tmp/ratelimit 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 'Full ratelimit buckets can be emptied by deleting the ratelimit hash from within mailcow UI (see /debug -> Protocols -> Ratelimit):' >> /tmp/ratelimit
echo >> /tmp/ratelimit echo >> /tmp/ratelimit
redis-cli --raw -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 10 | jq . >> /tmp/ratelimit redis-cli --raw -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning LRANGE RL_LOG 0 10 | jq . >> /tmp/ratelimit
fi fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 [ ${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} )) [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
@@ -669,20 +669,20 @@ fail2ban_checks() {
err_count=0 err_count=0
diff_c=0 diff_c=0
THRESHOLD=${FAIL2BAN_THRESHOLD} THRESHOLD=${FAIL2BAN_THRESHOLD}
F2B_LOG_STATUS=($(${REDIS_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS)) F2B_LOG_STATUS=($(${VALKEY_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS))
F2B_RES= F2B_RES=
# Reduce error count by 2 after restarting an unhealthy container # Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count} err_c_cur=${err_count}
F2B_LOG_STATUS_PREV=(${F2B_LOG_STATUS[@]}) F2B_LOG_STATUS_PREV=(${F2B_LOG_STATUS[@]})
F2B_LOG_STATUS=($(${REDIS_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS)) F2B_LOG_STATUS=($(${VALKEY_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS))
array_diff F2B_RES F2B_LOG_STATUS F2B_LOG_STATUS_PREV array_diff F2B_RES F2B_LOG_STATUS F2B_LOG_STATUS_PREV
if [[ ! -z "${F2B_RES}" ]]; then if [[ ! -z "${F2B_RES}" ]]; then
err_count=$(( ${err_count} + 1 )) err_count=$(( ${err_count} + 1 ))
echo -n "${F2B_RES[@]}" | tr -cd "[a-fA-F0-9.:/] " | timeout 3s ${REDIS_CMDLINE} -x SET F2B_RES > /dev/null echo -n "${F2B_RES[@]}" | tr -cd "[a-fA-F0-9.:/] " | timeout 3s ${VALKEY_CMDLINE} -x SET F2B_RES > /dev/null
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
${REDIS_CMDLINE} -x DEL F2B_RES ${VALKEY_CMDLINE} -x DEL F2B_RES
fi fi
fi fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
@@ -703,9 +703,9 @@ acme_checks() {
err_count=0 err_count=0
diff_c=0 diff_c=0
THRESHOLD=${ACME_THRESHOLD} THRESHOLD=${ACME_THRESHOLD}
ACME_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning GET ACME_FAIL_TIME) ACME_LOG_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET ACME_FAIL_TIME)
if [[ -z "${ACME_LOG_STATUS}" ]]; then if [[ -z "${ACME_LOG_STATUS}" ]]; then
${REDIS_CMDLINE} SET ACME_FAIL_TIME 0 ${VALKEY_CMDLINE} SET ACME_FAIL_TIME 0
ACME_LOG_STATUS=0 ACME_LOG_STATUS=0
fi fi
# Reduce error count by 2 after restarting an unhealthy container # Reduce error count by 2 after restarting an unhealthy container
@@ -715,7 +715,7 @@ acme_checks() {
ACME_LOG_STATUS_PREV=${ACME_LOG_STATUS} ACME_LOG_STATUS_PREV=${ACME_LOG_STATUS}
ACME_LC=0 ACME_LC=0
until [[ ! -z ${ACME_LOG_STATUS} ]] || [ ${ACME_LC} -ge 3 ]; do until [[ ! -z ${ACME_LOG_STATUS} ]] || [ ${ACME_LC} -ge 3 ]; do
ACME_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning GET ACME_FAIL_TIME 2> /dev/null) ACME_LOG_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET ACME_FAIL_TIME 2> /dev/null)
sleep 3 sleep 3
ACME_LC=$((ACME_LC+1)) ACME_LC=$((ACME_LC+1))
done done
@@ -864,14 +864,14 @@ BACKGROUND_TASKS+=(${PID})
( (
while true; do while true; do
if ! redis_checks; then if ! valkey_checks; then
log_msg "Local Redis hit error limit" log_msg "Local Valkey hit error limit"
echo redis-mailcow > /tmp/com_pipe echo valkey-mailcow > /tmp/com_pipe
fi fi
done done
) & ) &
PID=$! PID=$!
echo "Spawned redis_checks with PID ${PID}" echo "Spawned valkey_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID}) BACKGROUND_TASKS+=(${PID})
( (
@@ -1129,9 +1129,9 @@ while true; do
# Define $2 to override message text, else print service was restarted at ... # Define $2 to override message text, else print service was restarted at ...
notify_error "${com_pipe_answer}" "Please check acme-mailcow for further information." notify_error "${com_pipe_answer}" "Please check acme-mailcow for further information."
elif [[ ${com_pipe_answer} == "fail2ban" ]]; then elif [[ ${com_pipe_answer} == "fail2ban" ]]; then
F2B_RES=($(timeout 4s ${REDIS_CMDLINE} --raw GET F2B_RES 2> /dev/null)) F2B_RES=($(timeout 4s ${VALKEY_CMDLINE} --raw GET F2B_RES 2> /dev/null))
if [[ ! -z "${F2B_RES}" ]]; then if [[ ! -z "${F2B_RES}" ]]; then
${REDIS_CMDLINE} DEL F2B_RES > /dev/null ${VALKEY_CMDLINE} DEL F2B_RES > /dev/null
host= host=
for host in "${F2B_RES[@]}"; do for host in "${F2B_RES[@]}"; do
log_msg "Banned ${host}" 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'; require_once '../../../web/inc/lib/vendor/autoload.php';
// Init Redis // Init Valkey
$redis = new Redis(); $valkey = new Redis();
try { try {
if (!empty(getenv('REDIS_SLAVEOF_IP'))) { if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); $valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
} }
else { else {
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
} }
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
} }
catch (Exception $e) { catch (Exception $e) {
error_log("MAILCOWAUTH: " . $e . PHP_EOL); error_log("MAILCOWAUTH: " . $e . PHP_EOL);
@@ -80,21 +80,14 @@ if ($isSOGoRequest) {
} }
if ($result === false){ if ($result === false){
// If it's a SOGo Request, don't check for protocol access // If it's a SOGo Request, don't check for protocol access
if ($isSOGoRequest) { $service = ($isSOGoRequest) ? false : array($post['service'] => true);
$service = 'SOGO'; $result = apppass_login($post['username'], $post['password'], $service, array(
$post['service'] = 'NONE';
} else {
$service = $post['service'];
}
$result = apppass_login($post['username'], $post['password'], array(
'service' => $post['service'],
'is_internal' => true, 'is_internal' => true,
'remote_addr' => $post['real_rip'] 'remote_addr' => $post['real_rip']
)); ));
if ($result) { if ($result) {
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $service . " from IP " . $post['real_rip']); error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
set_sasl_log($post['username'], $post['real_rip'], $service); set_sasl_log($post['username'], $post['real_rip'], $post['service']);
} }
} }
if ($result === false){ if ($result === false){

View File

@@ -13,7 +13,6 @@ events {
http { http {
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
default_type application/octet-stream; default_type application/octet-stream;
server_tokens off;
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" ' '$status $body_bytes_sent "$http_referer" '

View File

@@ -14,6 +14,7 @@ ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=15768000;"; add_header Strict-Transport-Security "max-age=15768000;";
add_header X-Content-Type-Options nosniff; add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none; add_header X-Robots-Tag none;
add_header X-Download-Options noopen; add_header X-Download-Options noopen;
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
@@ -261,19 +262,19 @@ location ~* /sogo$ {
} }
location /SOGo.woa/WebServerResources/ { location /SOGo.woa/WebServerResources/ {
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/; alias /usr/lib/GNUstep/SOGo/WebServerResources/;
} }
location /.woa/WebServerResources/ { location /.woa/WebServerResources/ {
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/; alias /usr/lib/GNUstep/SOGo/WebServerResources/;
} }
location /SOGo/WebServerResources/ { location /SOGo/WebServerResources/ {
alias /usr/local/lib/GNUstep/SOGo/WebServerResources/; alias /usr/lib/GNUstep/SOGo/WebServerResources/;
} }
location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) { location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) {
alias /usr/local/lib/GNUstep/SOGo/$1.SOGo/Resources/$2; alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
} }
{% endif %} {% endif %}

View File

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

View File

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

View File

@@ -1,16 +1,7 @@
; NOTE: Restart phpfpm on ANY manual changes to PHP files!
; opcache
opcache.enable=1 opcache.enable=1
opcache.enable_cli=1 opcache.enable_cli=1
opcache.interned_strings_buffer=16 opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000 opcache.max_accelerated_files=10000
opcache.memory_consumption=128 opcache.memory_consumption=128
opcache.save_comments=1 opcache.save_comments=1
opcache.validate_timestamps=0 opcache.revalidate_freq=1
; JIT
; Disabled for now due to some PHP segmentation faults observed
; in certain environments. Possibly some PHP or PHP extension bug.
opcache.jit=disable
opcache.jit_buffer_size=0

View File

@@ -1,6 +1,6 @@
# Whitelist generated by Postwhite v3.4 on Sun Mar 1 00:29:01 UTC 2026 # Whitelist generated by Postwhite v3.4 on Wed Oct 1 00:21:33 UTC 2025
# https://github.com/stevejenkins/postwhite/ # https://github.com/stevejenkins/postwhite/
# 2174 total rules # 2216 total rules
2a00:1450:4000::/36 permit 2a00:1450:4000::/36 permit
2a01:111:f400::/48 permit 2a01:111:f400::/48 permit
2a01:111:f403:2800::/53 permit 2a01:111:f403:2800::/53 permit
@@ -29,9 +29,7 @@
2a01:b747:3005:200::/56 permit 2a01:b747:3005:200::/56 permit
2a01:b747:3006:200::/56 permit 2a01:b747:3006:200::/56 permit
2a02:a60:0:5::/64 permit 2a02:a60:0:5::/64 permit
2a0f:f640::/56 permit
2c0f:fb50:4000::/36 permit 2c0f:fb50:4000::/36 permit
2.207.151.53 permit
2.207.217.30 permit 2.207.217.30 permit
3.64.237.68 permit 3.64.237.68 permit
3.65.3.180 permit 3.65.3.180 permit
@@ -58,9 +56,8 @@
8.40.222.0/23 permit 8.40.222.0/23 permit
8.40.222.250/31 permit 8.40.222.250/31 permit
12.130.86.238 permit 12.130.86.238 permit
13.107.213.51 permit 13.107.213.41 permit
13.107.246.51 permit 13.107.246.41 permit
13.108.16.0/20 permit
13.110.208.0/21 permit 13.110.208.0/21 permit
13.110.209.0/24 permit 13.110.209.0/24 permit
13.110.216.0/22 permit 13.110.216.0/22 permit
@@ -69,7 +66,6 @@
13.111.191.0/24 permit 13.111.191.0/24 permit
13.216.7.111 permit 13.216.7.111 permit
13.216.54.180 permit 13.216.54.180 permit
13.247.164.219 permit
15.200.21.50 permit 15.200.21.50 permit
15.200.44.248 permit 15.200.44.248 permit
15.200.201.185 permit 15.200.201.185 permit
@@ -303,6 +299,15 @@
52.94.124.0/28 permit 52.94.124.0/28 permit
52.95.48.152/29 permit 52.95.48.152/29 permit
52.95.49.88/29 permit 52.95.49.88/29 permit
52.96.91.34 permit
52.96.111.82 permit
52.96.172.98 permit
52.96.214.50 permit
52.96.222.194 permit
52.96.222.226 permit
52.96.223.2 permit
52.96.228.130 permit
52.96.229.242 permit
52.100.0.0/15 permit 52.100.0.0/15 permit
52.102.0.0/16 permit 52.102.0.0/16 permit
52.103.0.0/17 permit 52.103.0.0/17 permit
@@ -397,8 +402,27 @@
64.207.219.143 permit 64.207.219.143 permit
64.233.160.0/19 permit 64.233.160.0/19 permit
65.52.80.137 permit 65.52.80.137 permit
65.54.51.64/26 permit
65.54.61.64/26 permit
65.54.121.120/29 permit
65.54.190.0/24 permit
65.54.241.0/24 permit
65.55.29.77 permit 65.55.29.77 permit
65.55.33.64/28 permit
65.55.34.0/24 permit
65.55.42.224/28 permit 65.55.42.224/28 permit
65.55.52.224/27 permit
65.55.78.128/25 permit
65.55.81.48/28 permit
65.55.90.0/24 permit
65.55.94.0/25 permit
65.55.111.0/24 permit
65.55.113.64/26 permit
65.55.116.0/25 permit
65.55.126.0/25 permit
65.55.174.0/25 permit
65.55.178.128/27 permit
65.55.234.192/26 permit
65.110.161.77 permit 65.110.161.77 permit
65.123.29.213 permit 65.123.29.213 permit
65.123.29.220 permit 65.123.29.220 permit
@@ -519,6 +543,7 @@
69.169.224.0/20 permit 69.169.224.0/20 permit
69.171.232.0/24 permit 69.171.232.0/24 permit
69.171.244.0/23 permit 69.171.244.0/23 permit
70.37.151.128/25 permit
70.42.149.35 permit 70.42.149.35 permit
72.3.185.0/24 permit 72.3.185.0/24 permit
72.14.192.0/18 permit 72.14.192.0/18 permit
@@ -615,6 +640,7 @@
74.208.4.220 permit 74.208.4.220 permit
74.208.4.221 permit 74.208.4.221 permit
74.209.250.0/24 permit 74.209.250.0/24 permit
75.2.70.75 permit
76.223.128.0/19 permit 76.223.128.0/19 permit
76.223.176.0/20 permit 76.223.176.0/20 permit
77.238.176.0/24 permit 77.238.176.0/24 permit
@@ -707,11 +733,11 @@
87.248.117.205 permit 87.248.117.205 permit
87.253.232.0/21 permit 87.253.232.0/21 permit
89.22.108.0/24 permit 89.22.108.0/24 permit
91.198.2.177 permit 91.198.2.0/24 permit
91.198.2.217 permit
91.198.2.222 permit
91.211.240.0/22 permit 91.211.240.0/22 permit
94.236.119.0/26 permit 94.236.119.0/26 permit
94.245.112.0/27 permit
94.245.112.10/31 permit
95.131.104.0/21 permit 95.131.104.0/21 permit
95.217.114.154 permit 95.217.114.154 permit
96.43.144.0/20 permit 96.43.144.0/20 permit
@@ -1204,9 +1230,9 @@
98.139.245.208/30 permit 98.139.245.208/30 permit
98.139.245.212/31 permit 98.139.245.212/31 permit
99.78.197.208/28 permit 99.78.197.208/28 permit
99.83.190.102 permit
103.9.96.0/22 permit 103.9.96.0/22 permit
103.28.42.0/24 permit 103.28.42.0/24 permit
103.84.217.15 permit
103.84.217.238 permit 103.84.217.238 permit
103.89.75.238 permit 103.89.75.238 permit
103.151.192.0/23 permit 103.151.192.0/23 permit
@@ -1352,6 +1378,11 @@
108.179.144.0/20 permit 108.179.144.0/20 permit
109.224.244.0/24 permit 109.224.244.0/24 permit
109.237.142.0/24 permit 109.237.142.0/24 permit
111.221.23.128/25 permit
111.221.26.0/27 permit
111.221.66.0/25 permit
111.221.69.128/25 permit
111.221.112.0/21 permit
112.19.199.64/29 permit 112.19.199.64/29 permit
112.19.242.64/29 permit 112.19.242.64/29 permit
116.214.12.47 permit 116.214.12.47 permit
@@ -1399,7 +1430,6 @@
128.245.248.0/21 permit 128.245.248.0/21 permit
129.41.77.70 permit 129.41.77.70 permit
129.41.169.249 permit 129.41.169.249 permit
129.77.16.0/20 permit
129.80.5.164 permit 129.80.5.164 permit
129.80.64.36 permit 129.80.64.36 permit
129.80.67.121 permit 129.80.67.121 permit
@@ -1416,7 +1446,6 @@
129.153.194.228 permit 129.153.194.228 permit
129.154.255.129 permit 129.154.255.129 permit
129.158.56.255 permit 129.158.56.255 permit
129.158.62.153 permit
129.159.22.159 permit 129.159.22.159 permit
129.159.87.137 permit 129.159.87.137 permit
129.213.195.191 permit 129.213.195.191 permit
@@ -1452,7 +1481,6 @@
136.143.184.0/24 permit 136.143.184.0/24 permit
136.143.188.0/24 permit 136.143.188.0/24 permit
136.143.190.0/23 permit 136.143.190.0/23 permit
136.146.128.0/20 permit
136.147.128.0/20 permit 136.147.128.0/20 permit
136.147.135.0/24 permit 136.147.135.0/24 permit
136.147.176.0/20 permit 136.147.176.0/20 permit
@@ -1470,8 +1498,6 @@
139.167.79.86 permit 139.167.79.86 permit
139.180.17.0/24 permit 139.180.17.0/24 permit
140.238.148.191 permit 140.238.148.191 permit
141.148.55.217 permit
141.148.91.244 permit
141.148.159.229 permit 141.148.159.229 permit
141.193.32.0/23 permit 141.193.32.0/23 permit
141.193.184.32/27 permit 141.193.184.32/27 permit
@@ -1517,7 +1543,6 @@
149.72.234.184 permit 149.72.234.184 permit
149.72.248.236 permit 149.72.248.236 permit
149.97.173.180 permit 149.97.173.180 permit
150.136.21.199 permit
150.230.98.160 permit 150.230.98.160 permit
151.145.38.14 permit 151.145.38.14 permit
152.67.105.195 permit 152.67.105.195 permit
@@ -1527,7 +1552,20 @@
155.248.220.138 permit 155.248.220.138 permit
155.248.234.149 permit 155.248.234.149 permit
155.248.237.141 permit 155.248.237.141 permit
157.55.0.192/26 permit
157.55.1.128/26 permit
157.55.2.0/25 permit
157.55.9.128/25 permit
157.55.11.0/25 permit
157.55.49.0/25 permit
157.55.61.0/24 permit
157.55.157.128/25 permit
157.55.225.0/25 permit
157.56.24.0/25 permit
157.56.120.128/26 permit 157.56.120.128/26 permit
157.56.232.0/21 permit
157.56.240.0/20 permit
157.56.248.0/21 permit
157.58.30.128/25 permit 157.58.30.128/25 permit
157.58.196.96/29 permit 157.58.196.96/29 permit
157.58.249.3 permit 157.58.249.3 permit
@@ -1556,10 +1594,8 @@
159.135.224.0/20 permit 159.135.224.0/20 permit
159.135.228.10 permit 159.135.228.10 permit
159.183.0.0/16 permit 159.183.0.0/16 permit
159.183.14.233 permit
159.183.68.71 permit 159.183.68.71 permit
159.183.79.38 permit 159.183.79.38 permit
159.183.121.182 permit
159.183.129.172 permit 159.183.129.172 permit
160.1.62.192 permit 160.1.62.192 permit
161.38.192.0/20 permit 161.38.192.0/20 permit
@@ -1579,14 +1615,10 @@
163.114.135.16 permit 163.114.135.16 permit
163.116.128.0/17 permit 163.116.128.0/17 permit
163.192.116.87 permit 163.192.116.87 permit
163.192.125.176 permit
163.192.196.146 permit
163.192.204.161 permit
164.152.23.32 permit 164.152.23.32 permit
164.152.25.241 permit 164.152.25.241 permit
164.177.132.168/30 permit 164.177.132.168/30 permit
165.173.128.0/24 permit 165.173.128.0/24 permit
165.173.180.1 permit
165.173.180.250/31 permit 165.173.180.250/31 permit
165.173.182.250/31 permit 165.173.182.250/31 permit
166.78.68.0/22 permit 166.78.68.0/22 permit
@@ -1627,15 +1659,16 @@
169.148.144.10 permit 169.148.144.10 permit
169.148.146.0/23 permit 169.148.146.0/23 permit
169.148.175.3 permit 169.148.175.3 permit
169.148.179.3 permit
169.148.188.0/24 permit 169.148.188.0/24 permit
169.148.188.182 permit 169.148.188.182 permit
170.9.232.254 permit
170.10.128.0/24 permit 170.10.128.0/24 permit
170.10.129.0/24 permit 170.10.129.0/24 permit
170.10.132.56/29 permit 170.10.132.56/29 permit
170.10.132.64/29 permit 170.10.132.64/29 permit
170.10.133.0/24 permit 170.10.133.0/24 permit
172.217.32.0/20 permit
172.253.56.0/21 permit
172.253.112.0/20 permit
173.0.84.0/29 permit 173.0.84.0/29 permit
173.0.84.224/27 permit 173.0.84.224/27 permit
173.0.94.244/30 permit 173.0.94.244/30 permit
@@ -1663,7 +1696,8 @@
182.50.78.64/28 permit 182.50.78.64/28 permit
183.240.219.64/29 permit 183.240.219.64/29 permit
185.4.120.0/22 permit 185.4.120.0/22 permit
185.11.255.144 permit 185.11.253.128/27 permit
185.11.255.0/24 permit
185.12.80.0/22 permit 185.12.80.0/22 permit
185.28.196.0/22 permit 185.28.196.0/22 permit
185.58.84.93 permit 185.58.84.93 permit
@@ -1677,16 +1711,8 @@
185.138.56.128/25 permit 185.138.56.128/25 permit
185.189.236.0/22 permit 185.189.236.0/22 permit
185.211.120.0/22 permit 185.211.120.0/22 permit
185.233.188.68 permit 185.233.188.0/23 permit
185.233.188.75 permit 185.233.190.0/23 permit
185.233.188.84 permit
185.233.188.160 permit
185.233.188.176 permit
185.233.188.247 permit
185.233.189.44 permit
185.233.189.98 permit
185.233.189.122 permit
185.233.189.228 permit
185.250.236.0/22 permit 185.250.236.0/22 permit
185.250.239.148 permit 185.250.239.148 permit
185.250.239.168 permit 185.250.239.168 permit
@@ -1762,9 +1788,7 @@
193.109.254.0/23 permit 193.109.254.0/23 permit
193.122.128.100 permit 193.122.128.100 permit
193.123.56.63 permit 193.123.56.63 permit
193.142.157.15 permit 193.142.157.0/24 permit
193.142.157.125 permit
193.142.157.158 permit
193.142.157.191 permit 193.142.157.191 permit
193.142.157.198 permit 193.142.157.198 permit
194.19.134.0/25 permit 194.19.134.0/25 permit
@@ -1788,7 +1812,6 @@
194.97.212.12 permit 194.97.212.12 permit
194.106.220.0/23 permit 194.106.220.0/23 permit
194.113.24.0/22 permit 194.113.24.0/22 permit
194.113.42.0/26 permit
194.154.193.192/27 permit 194.154.193.192/27 permit
195.4.92.0/23 permit 195.4.92.0/23 permit
195.54.172.0/23 permit 195.54.172.0/23 permit
@@ -1802,7 +1825,6 @@
198.61.254.21 permit 198.61.254.21 permit
198.61.254.231 permit 198.61.254.231 permit
198.178.234.57 permit 198.178.234.57 permit
198.202.211.1 permit
198.244.48.0/20 permit 198.244.48.0/20 permit
198.244.56.107 permit 198.244.56.107 permit
198.244.56.108 permit 198.244.56.108 permit
@@ -1886,6 +1908,7 @@
204.14.232.64/28 permit 204.14.232.64/28 permit
204.14.234.64/28 permit 204.14.234.64/28 permit
204.75.142.0/24 permit 204.75.142.0/24 permit
204.79.197.212 permit
204.92.114.187 permit 204.92.114.187 permit
204.92.114.203 permit 204.92.114.203 permit
204.92.114.204/31 permit 204.92.114.204/31 permit
@@ -1913,13 +1936,24 @@
206.165.246.80/29 permit 206.165.246.80/29 permit
206.191.224.0/19 permit 206.191.224.0/19 permit
206.246.157.1 permit 206.246.157.1 permit
207.46.4.128/25 permit
207.46.22.35 permit 207.46.22.35 permit
207.46.50.72 permit 207.46.50.72 permit
207.46.50.82 permit 207.46.50.82 permit
207.46.50.192/26 permit
207.46.50.224 permit
207.46.52.71 permit 207.46.52.71 permit
207.46.52.79 permit 207.46.52.79 permit
207.46.58.128/25 permit
207.46.116.128/29 permit
207.46.117.0/24 permit
207.46.132.128/27 permit
207.46.198.0/25 permit
207.46.200.0/27 permit
207.67.38.0/24 permit 207.67.38.0/24 permit
207.67.98.192/27 permit 207.67.98.192/27 permit
207.68.176.0/26 permit
207.68.176.96/27 permit
207.97.204.96/29 permit 207.97.204.96/29 permit
207.126.144.0/20 permit 207.126.144.0/20 permit
207.171.160.0/19 permit 207.171.160.0/19 permit
@@ -2068,10 +2102,14 @@
212.227.126.225 permit 212.227.126.225 permit
212.227.126.226 permit 212.227.126.226 permit
212.227.126.227 permit 212.227.126.227 permit
213.95.19.64/27 permit
213.95.135.4 permit
213.199.128.139 permit 213.199.128.139 permit
213.199.128.145 permit 213.199.128.145 permit
213.199.138.181 permit 213.199.138.181 permit
213.199.138.191 permit 213.199.138.191 permit
213.199.161.128/27 permit
213.199.177.0/26 permit
216.17.150.242 permit 216.17.150.242 permit
216.17.150.251 permit 216.17.150.251 permit
216.24.224.0/20 permit 216.24.224.0/20 permit
@@ -2099,6 +2137,7 @@
216.39.62.60/31 permit 216.39.62.60/31 permit
216.39.62.136/29 permit 216.39.62.136/29 permit
216.39.62.144/31 permit 216.39.62.144/31 permit
216.58.192.0/19 permit
216.66.217.240/29 permit 216.66.217.240/29 permit
216.71.138.33 permit 216.71.138.33 permit
216.71.152.207 permit 216.71.152.207 permit
@@ -2157,6 +2196,11 @@
2001:748:400:3301::3 permit 2001:748:400:3301::3 permit
2001:748:400:3301::4 permit 2001:748:400:3301::4 permit
2404:6800:4000::/36 permit 2404:6800:4000::/36 permit
2603:1010:3:3::5b permit
2603:1020:201:10::10f permit
2603:1030:20e:3::23c permit
2603:1030:b:3::152 permit
2603:1030:c02:8::14 permit
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit 2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit 2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit 2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
@@ -2172,8 +2216,6 @@
2620:10d:c09c:400::8:1 permit 2620:10d:c09c:400::8:1 permit
2620:119:50c0:207::/64 permit 2620:119:50c0:207::/64 permit
2620:119:50c0:207::215 permit 2620:119:50c0:207::215 permit
2620:1ec:46::51 permit
2620:1ec:bdf::51 permit
2800:3f0:4000::/36 permit 2800:3f0:4000::/36 permit
49.12.4.251 permit # checks.mailcow.email 49.12.4.251 permit # checks.mailcow.email
2a01:4f8:c17:7906::10 permit # checks.mailcow.email 2a01:4f8:c17:7906::10 permit # checks.mailcow.email

View File

@@ -1,12 +0,0 @@
#!/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; exit;
} }
// Init Redis // Init Valkey
$redis = new Redis(); $valkey = new Redis();
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
function parse_email($email) { function parse_email($email) {
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false; if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
@@ -60,7 +60,7 @@ $rcpt_final_mailboxes = array();
// Skip if not a mailcow handled domain // Skip if not a mailcow handled domain
try { try {
if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { if (!$valkey->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
exit; exit;
} }
} }
@@ -122,7 +122,7 @@ try {
} }
else { else {
$parsed_goto = parse_email($goto); $parsed_goto = parse_email($goto);
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { if (!$valkey->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
error_log("ALIAS EXPANDER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL); error_log("ALIAS EXPANDER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
} }
else { else {

View File

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

View File

@@ -146,171 +146,8 @@ rspamd_config:register_symbol({
return false return false
end end
-- Helper function to parse IPv6 into 8 segments
local function ipv6_to_segments(ip_str)
-- Remove zone identifier if present (e.g., %eth0)
ip_str = ip_str:gsub("%%.*$", "")
local segments = {}
-- Handle :: compression
if ip_str:find('::') then
local before, after = ip_str:match('^(.*)::(.*)$')
before = before or ''
after = after or ''
local before_parts = {}
local after_parts = {}
if before ~= '' then
for seg in before:gmatch('[^:]+') do
table.insert(before_parts, tonumber(seg, 16) or 0)
end
end
if after ~= '' then
for seg in after:gmatch('[^:]+') do
table.insert(after_parts, tonumber(seg, 16) or 0)
end
end
-- Add before segments
for _, seg in ipairs(before_parts) do
table.insert(segments, seg)
end
-- Add compressed zeros
local zeros_needed = 8 - #before_parts - #after_parts
for i = 1, zeros_needed do
table.insert(segments, 0)
end
-- Add after segments
for _, seg in ipairs(after_parts) do
table.insert(segments, seg)
end
else
-- No compression
for seg in ip_str:gmatch('[^:]+') do
table.insert(segments, tonumber(seg, 16) or 0)
end
end
-- Ensure we have exactly 8 segments
while #segments < 8 do
table.insert(segments, 0)
end
return segments
end
-- Generate all common IPv6 notations
local function get_ipv6_variants(ip_str)
local variants = {}
local seen = {}
local function add_variant(v)
if v and not seen[v] then
table.insert(variants, v)
seen[v] = true
end
end
-- For IPv4, just return the original
if not ip_str:find(':') then
add_variant(ip_str)
return variants
end
local segments = ipv6_to_segments(ip_str)
-- 1. Fully expanded form (all zeros shown as 0000)
local expanded_parts = {}
for _, seg in ipairs(segments) do
table.insert(expanded_parts, string.format('%04x', seg))
end
add_variant(table.concat(expanded_parts, ':'))
-- 2. Standard form (no leading zeros, but all segments present)
local standard_parts = {}
for _, seg in ipairs(segments) do
table.insert(standard_parts, string.format('%x', seg))
end
add_variant(table.concat(standard_parts, ':'))
-- 3. Find all possible :: compressions
-- RFC 5952: compress the longest run of consecutive zeros
-- But we need to check all possibilities since Redis might have any form
-- Find all zero runs
local zero_runs = {}
local in_run = false
local run_start = 0
local run_length = 0
for i = 1, 8 do
if segments[i] == 0 then
if not in_run then
in_run = true
run_start = i
run_length = 1
else
run_length = run_length + 1
end
else
if in_run then
if run_length >= 1 then -- Allow single zero compression too
table.insert(zero_runs, {start = run_start, length = run_length})
end
in_run = false
end
end
end
-- Don't forget the last run
if in_run and run_length >= 1 then
table.insert(zero_runs, {start = run_start, length = run_length})
end
-- Generate variant for each zero run compression
for _, run in ipairs(zero_runs) do
local parts = {}
-- Before compression
for i = 1, run.start - 1 do
table.insert(parts, string.format('%x', segments[i]))
end
-- The compression
if run.start == 1 then
table.insert(parts, '')
table.insert(parts, '')
elseif run.start + run.length - 1 == 8 then
table.insert(parts, '')
table.insert(parts, '')
else
table.insert(parts, '')
end
-- After compression
for i = run.start + run.length, 8 do
table.insert(parts, string.format('%x', segments[i]))
end
local compressed = table.concat(parts, ':'):gsub('::+', '::')
add_variant(compressed)
end
return variants
end
local from_ip_string = tostring(ip) local from_ip_string = tostring(ip)
local ip_check_table = {} ip_check_table = {from_ip_string}
-- Add all variants of the exact IP
for _, variant in ipairs(get_ipv6_variants(from_ip_string)) do
table.insert(ip_check_table, variant)
end
local maxbits = 128 local maxbits = 128
local minbits = 32 local minbits = 32
@@ -318,18 +155,10 @@ rspamd_config:register_symbol({
maxbits = 32 maxbits = 32
minbits = 8 minbits = 8
end end
-- Add all CIDR notations with variants
for i=maxbits,minbits,-1 do for i=maxbits,minbits,-1 do
local masked_ip = ip:apply_mask(i) local nip = ip:apply_mask(i):to_string() .. "/" .. i
local cidr_base = masked_ip:to_string() table.insert(ip_check_table, nip)
for _, variant in ipairs(get_ipv6_variants(cidr_base)) do
local cidr = variant .. "/" .. i
table.insert(ip_check_table, cidr)
end
end end
local function keep_spam_cb(err, data) local function keep_spam_cb(err, data)
if err then if err then
rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err) rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
@@ -337,15 +166,12 @@ rspamd_config:register_symbol({
else else
for k,v in pairs(data) do for k,v in pairs(data) do
if (v and v ~= userdata and v == '1') then if (v and v ~= userdata and v == '1') then
rspamd_logger.infox(rspamd_config, "found ip %s (checked as: %s) in keep_spam map, setting pre-result accept", from_ip_string, ip_check_table[k]) rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result")
task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam') task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam')
task:set_flag('no_stat')
return
end end
end end
end end
end end
table.insert(ip_check_table, 1, 'KEEP_SPAM') table.insert(ip_check_table, 1, 'KEEP_SPAM')
local redis_ret_user = rspamd_redis_make_request(task, local redis_ret_user = rspamd_redis_make_request(task,
redis_params, -- connect params redis_params, -- connect params
@@ -384,7 +210,6 @@ rspamd_config:register_symbol({
rspamd_config:register_symbol({ rspamd_config:register_symbol({
name = 'TAG_MOO', name = 'TAG_MOO',
type = 'postfilter', type = 'postfilter',
flags = 'ignore_passthrough',
callback = function(task) callback = function(task)
local util = require("rspamd_util") local util = require("rspamd_util")
local rspamd_logger = require "rspamd_logger" local rspamd_logger = require "rspamd_logger"
@@ -392,7 +217,9 @@ rspamd_config:register_symbol({
local rspamd_http = require "rspamd_http" local rspamd_http = require "rspamd_http"
local rcpts = task:get_recipients('smtp') local rcpts = task:get_recipients('smtp')
local lua_util = require "lua_util" local lua_util = require "lua_util"
local tagged_rcpt = task:get_symbol("TAGGED_RCPT") local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
local function remove_moo_tag() local function remove_moo_tag()
local moo_tag_header = task:get_header('X-Moo-Tag', false) local moo_tag_header = task:get_header('X-Moo-Tag', false)
@@ -404,147 +231,101 @@ rspamd_config:register_symbol({
return true return true
end end
-- Check if we have exactly one recipient if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then
if not (rcpts and #rcpts == 1) then local tag = tagged_rcpt[1].options[1]
rspamd_logger.infox("TAG_MOO: not exactly one rcpt (%s), removing moo tag", rcpts and #rcpts or 0) rspamd_logger.infox("found tag: %s", tag)
remove_moo_tag() local action = task:get_metric_action('default')
return rspamd_logger.infox("metric action now: %s", action)
end
local rcpt_addr = rcpts[1]['addr'] if action ~= 'no action' and action ~= 'greylist' then
local rcpt_user = rcpts[1]['user'] rspamd_logger.infox("skipping tag handler for action: %s", action)
local rcpt_domain = rcpts[1]['domain'] remove_moo_tag()
return true
end
-- Check if recipient has a tag (contains '+') local function http_callback(err_message, code, body, headers)
local tag = nil if body ~= nil and body ~= "" then
if tagged_rcpt ~= nil then rspamd_logger.infox(rspamd_config, "expanding rcpt to \"%s\"", body)
tag = tagged_rcpt
rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag)
end
if not tag then local function tag_callback_subject(err, data)
rspamd_logger.infox("TAG_MOO: no tag found in recipient %s, removing moo tag", rcpt_addr) if err or type(data) ~= 'string' then
remove_moo_tag() rspamd_logger.infox(rspamd_config, "subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
return
end
-- Optional: Check if domain is a mailcow domain local function tag_callback_subfolder(err, data)
-- When KEEP_SPAM is active, RCPT_MAILCOW_DOMAIN might not be set if err or type(data) ~= 'string' then
-- If the mail is being delivered, we can assume it's valid rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN") remove_moo_tag()
if not mailcow_domain then else
rspamd_logger.infox("TAG_MOO: RCPT_MAILCOW_DOMAIN not set (possibly due to pre-result), proceeding anyway for domain %s", rcpt_domain) rspamd_logger.infox("Add X-Moo-Tag header")
end task:set_milter_reply({
add_headers = {['X-Moo-Tag'] = 'YES'}
})
end
end
local action = task:get_metric_action('default') local redis_ret_subfolder = rspamd_redis_make_request(task,
rspamd_logger.infox("TAG_MOO: metric action: %s", action) redis_params, -- connect params
body, -- hash key
false, -- is write
tag_callback_subfolder, --callback
'HGET', -- command
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
)
if not redis_ret_subfolder then
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
remove_moo_tag()
end
-- Check if we have a pre-result (e.g., from KEEP_SPAM or POSTMASTER_HANDLER) else
local allow_processing = false rspamd_logger.infox("user wants subject modified for tagged mail")
local sbj = task:get_header('Subject')
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
task:set_milter_reply({
remove_headers = {
['Subject'] = 1,
['X-Moo-Tag'] = 0
},
add_headers = {['Subject'] = new_sbj}
})
end
end
local redis_ret_subject = rspamd_redis_make_request(task,
redis_params, -- connect params
body, -- hash key
false, -- is write
tag_callback_subject, --callback
'HGET', -- command
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
)
if not redis_ret_subject then
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
remove_moo_tag()
end
if task.has_pre_result then
local has_pre, pre_action = task:has_pre_result()
if has_pre then
rspamd_logger.infox("TAG_MOO: pre-result detected: %s", tostring(pre_action))
if pre_action == 'accept' then
allow_processing = true
rspamd_logger.infox("TAG_MOO: pre-result is accept, will process")
end end
end end
end
-- Allow processing for mild actions or when we have pre-result accept if rcpts and #rcpts == 1 then
if not allow_processing and action ~= 'no action' and action ~= 'greylist' then for _,rcpt in ipairs(rcpts) do
rspamd_logger.infox("TAG_MOO: skipping tag handler for action: %s", action) local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
remove_moo_tag() if #rcpt_split == 2 then
return true if rcpt_split[1] == 'postmaster' then
end rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
rspamd_logger.infox("TAG_MOO: processing allowed")
local function http_callback(err_message, code, body, headers)
if body ~= nil and body ~= "" then
rspamd_logger.infox(rspamd_config, "TAG_MOO: expanding rcpt to \"%s\"", body)
local function tag_callback_subject(err, data)
if err or type(data) ~= 'string' or data == '' then
rspamd_logger.infox(rspamd_config, "TAG_MOO: subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
local function tag_callback_subfolder(err, data)
if err or type(data) ~= 'string' or data == '' then
rspamd_logger.infox(rspamd_config, "TAG_MOO: subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
remove_moo_tag()
else
rspamd_logger.infox("TAG_MOO: User wants subfolder tag, adding X-Moo-Tag header")
task:set_milter_reply({
add_headers = {['X-Moo-Tag'] = 'YES'}
})
end
end
local redis_ret_subfolder = rspamd_redis_make_request(task,
redis_params, -- connect params
body, -- hash key
false, -- is write
tag_callback_subfolder, --callback
'HGET', -- command
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
)
if not redis_ret_subfolder then
rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
remove_moo_tag() remove_moo_tag()
else
rspamd_http.request({
task=task,
url='http://nginx:8081/aliasexp.php',
body='',
callback=http_callback,
headers={Rcpt=rcpt['addr']},
})
end end
else
rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
local sbj = task:get_header('Subject') or ''
local tag_value = tag[1] and tag[1].options and tag[1].options[1] or ''
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag_value .. '] ' .. sbj)) .. '?='
task:set_milter_reply({
remove_headers = {
['Subject'] = 1,
['X-Moo-Tag'] = 0
},
add_headers = {['Subject'] = new_sbj}
})
end end
end end
local redis_ret_subject = rspamd_redis_make_request(task,
redis_params, -- connect params
body, -- hash key
false, -- is write
tag_callback_subject, --callback
'HGET', -- command
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
)
if not redis_ret_subject then
rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
remove_moo_tag()
end
else
rspamd_logger.infox("TAG_MOO: alias expansion returned empty body")
remove_moo_tag()
end
end
local rcpt_split = rspamd_str_split(rcpt_addr, '@')
if #rcpt_split == 2 then
if rcpt_split[1]:match('^postmaster') then
rspamd_logger.infox(rspamd_config, "TAG_MOO: not expanding postmaster alias")
remove_moo_tag()
else
rspamd_logger.infox("TAG_MOO: requesting alias expansion for %s", rcpt_addr)
rspamd_http.request({
task=task,
url='http://nginx:8081/aliasexp.php',
body='',
callback=http_callback,
headers={Rcpt=rcpt_addr},
})
end end
else else
rspamd_logger.infox("TAG_MOO: invalid rcpt format")
remove_moo_tag() remove_moo_tag()
end end
end, end,
@@ -554,7 +335,6 @@ rspamd_config:register_symbol({
rspamd_config:register_symbol({ rspamd_config:register_symbol({
name = 'BCC', name = 'BCC',
type = 'postfilter', type = 'postfilter',
flags = 'ignore_passthrough',
callback = function(task) callback = function(task)
local util = require("rspamd_util") local util = require("rspamd_util")
local rspamd_http = require "rspamd_http" local rspamd_http = require "rspamd_http"
@@ -583,13 +363,11 @@ rspamd_config:register_symbol({
local email_content = tostring(task:get_content()) local email_content = tostring(task:get_content())
email_content = string.gsub(email_content, "\r\n%.", "\r\n..") email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
-- send mail -- send mail
local from_smtp = task:get_from('smtp')
local from_addr = (from_smtp and from_smtp[1] and from_smtp[1].addr) or 'mailer-daemon@localhost'
lua_smtp.sendmail({ lua_smtp.sendmail({
task = task, task = task,
host = os.getenv("IPV4_NETWORK") .. '.253', host = os.getenv("IPV4_NETWORK") .. '.253',
port = 591, port = 591,
from = from_addr, from = task:get_from(stp)[1].addr,
recipients = bcc_dest, recipients = bcc_dest,
helo = 'bcc', helo = 'bcc',
timeout = 20, timeout = 20,
@@ -619,41 +397,27 @@ rspamd_config:register_symbol({
end end
local action = task:get_metric_action('default') local action = task:get_metric_action('default')
rspamd_logger.infox("BCC: metric action: %s", action) rspamd_logger.infox("metric action now: %s", action)
-- Check for pre-result accept (e.g., from KEEP_SPAM)
local allow_bcc = false
if task.has_pre_result then
local has_pre, pre_action = task:has_pre_result()
if has_pre and pre_action == 'accept' then
allow_bcc = true
rspamd_logger.infox("BCC: pre-result accept detected, will send BCC")
end
end
-- Allow BCC for mild actions or when we have pre-result accept
if not allow_bcc and action ~= 'no action' and action ~= 'add header' and action ~= 'rewrite subject' then
rspamd_logger.infox("BCC: skipping for action: %s", action)
return
end
local function rcpt_callback(err_message, code, body, headers) local function rcpt_callback(err_message, code, body, headers)
if err_message == nil and code == 201 and body ~= nil then if err_message == nil and code == 201 and body ~= nil then
rspamd_logger.infox("BCC: sending BCC to %s for rcpt match", body) if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
send_mail(task, body) send_mail(task, body)
end
end end
end end
local function from_callback(err_message, code, body, headers) local function from_callback(err_message, code, body, headers)
if err_message == nil and code == 201 and body ~= nil then if err_message == nil and code == 201 and body ~= nil then
rspamd_logger.infox("BCC: sending BCC to %s for from match", body) if action == 'no action' or action == 'add header' or action == 'rewrite subject' then
send_mail(task, body) send_mail(task, body)
end
end end
end end
if rcpt_table then if rcpt_table then
for _,e in ipairs(rcpt_table) do for _,e in ipairs(rcpt_table) do
rspamd_logger.infox(rspamd_config, "BCC: checking bcc for rcpt address %s", e) rspamd_logger.infox(rspamd_config, "checking bcc for rcpt address %s", e)
rspamd_http.request({ rspamd_http.request({
task=task, task=task,
url='http://nginx:8081/bcc.php', url='http://nginx:8081/bcc.php',
@@ -666,7 +430,7 @@ rspamd_config:register_symbol({
if from_table then if from_table then
for _,e in ipairs(from_table) do for _,e in ipairs(from_table) do
rspamd_logger.infox(rspamd_config, "BCC: checking bcc for from address %s", e) rspamd_logger.infox(rspamd_config, "checking bcc for from address %s", e)
rspamd_http.request({ rspamd_http.request({
task=task, task=task,
url='http://nginx:8081/bcc.php', url='http://nginx:8081/bcc.php',
@@ -677,7 +441,7 @@ rspamd_config:register_symbol({
end end
end end
-- Don't return true to avoid symbol being logged return true
end, end,
priority = 20 priority = 20
}) })

View File

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

View File

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

View File

@@ -21,10 +21,10 @@ catch (PDOException $e) {
http_response_code(501); http_response_code(501);
exit; exit;
} }
// Init Redis // Init Valkey
$redis = new Redis(); $valkey = new Redis();
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
// Functions // Functions
function parse_email($email) { function parse_email($email) {
@@ -94,7 +94,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
// Skip if not a mailcow handled domain // Skip if not a mailcow handled domain
try { try {
if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { if (!$valkey->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
continue; continue;
} }
} }
@@ -156,7 +156,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
} }
else { else {
$parsed_goto = parse_email($goto); $parsed_goto = parse_email($goto);
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { if (!$valkey->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL); error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
} }
else { else {

View File

@@ -86,12 +86,6 @@
SOGoMaximumFailedLoginInterval = 900; SOGoMaximumFailedLoginInterval = 900;
SOGoFailedLoginBlockInterval = 900; SOGoFailedLoginBlockInterval = 900;
// Enable SOGo URL Description for GDPR compliance, this may cause some issues with calendars and contacts. Also uncomment the encryption key below to use it.
//SOGoURLEncryptionEnabled = NO;
// Set a 16 character encryption key for SOGo URL Description, change this to your own value
//SOGoURLPathEncryptionKey = "SOGoSuperSecret0";
GCSChannelCollectionTimer = 60; GCSChannelCollectionTimer = 60;
GCSChannelExpireAge = 60; GCSChannelExpireAge = 60;

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

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

View File

@@ -2,7 +2,18 @@
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
protect_route(['admin']); if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
header('Location: /domainadmin/mailbox');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
header('Location: /admin');
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
@@ -10,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; $olefy_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_OLEFY"])) ? false : true;
if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) { if (!isset($_SESSION['gal']) && $license_cache = $valkey->Get('LICENSE_STATUS_CACHE')) {
$_SESSION['gal'] = json_decode($license_cache, true); $_SESSION['gal'] = json_decode($license_cache, true);
} }

View File

@@ -3,11 +3,8 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
// Only redirect to dashboard if NO pending actions header('Location: /admin/dashboard');
if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) { exit();
header('Location: /admin/dashboard');
exit();
}
} }
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
header('Location: /domainadmin/mailbox'); header('Location: /domainadmin/mailbox');

View File

@@ -2,7 +2,18 @@
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
protect_route(['admin']); if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
header('Location: /domainadmin/mailbox');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
header('Location: /admin');
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];

View File

@@ -2,7 +2,19 @@
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
protect_route(['admin']); if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
header('Location: /domainadmin/mailbox');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
header('Location: /admin');
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$js_minifier->add('/web/js/site/queue.js'); $js_minifier->add('/web/js/site/queue.js');

View File

@@ -2,7 +2,18 @@
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
protect_route(['admin']); if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
header('Location: /domainadmin/mailbox');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
header('Location: /admin');
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];

View File

@@ -2454,90 +2454,6 @@ paths:
type: object type: object
type: object type: object
summary: Delete mails in Quarantine summary: Delete mails in Quarantine
/api/v1/edit/qitem:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
release:
value:
- log:
- quarantine
- edit
- id:
- "33"
action: release
msg:
- item_released
- "33"
type: success
learnham:
value:
- log:
- quarantine
- edit
- id:
- "34"
action: learnham
msg:
- item_learned
- "34"
type: success
schema:
properties:
log:
description: contains request object
items: {}
type: array
msg:
items: {}
type: array
type:
enum:
- success
- danger
- error
type: string
type: object
description: OK
headers: {}
tags:
- Quarantine
description: >-
Using this endpoint you can perform actions on quarantine items. It is possible to release
emails from quarantine into to the inbox, or learn them as ham to improve Rspamd filtering.
You must provide the quarantine item IDs. You can get the IDs using the GET method.
operationId: Edit mails in Quarantine
requestBody:
content:
application/json:
schema:
example:
items:
- "33"
- "34"
attr:
action: release
properties:
items:
description: contains list of quarantine item IDs to release or learn as ham
type: object
attr:
description: attributes for the action
type: object
properties:
action:
type: string
enum:
- release
- learnham
description: "release - return email to inbox; learnham - learn as ham to improve filtering"
type: object
summary: Edit mails in Quarantine
/api/v1/delete/recipient_map: /api/v1/delete/recipient_map:
post: post:
responses: responses:
@@ -5496,9 +5412,9 @@ paths:
started_at: "2019-12-22T21:00:07.186717617Z" started_at: "2019-12-22T21:00:07.186717617Z"
state: running state: running
type: info type: info
redis-mailcow: valkey-mailcow:
container: redis-mailcow container: valkey-mailcow
image: "redis:5-alpine" image: "valkey:7.2.8-alpine"
started_at: "2019-12-22T20:59:56.827166834Z" started_at: "2019-12-22T20:59:56.827166834Z"
state: running state: running
type: info type: info

View File

@@ -29,8 +29,8 @@ header('Content-Type: application/xml');
<clientConfig version="1.1"> <clientConfig version="1.1">
<emailProvider id="<?=$mailcow_hostname; ?>"> <emailProvider id="<?=$mailcow_hostname; ?>">
<domain>%EMAILDOMAIN%</domain> <domain>%EMAILDOMAIN%</domain>
<displayName><?=$autodiscover_config['displayName']; ?></displayName> <displayName>A mailcow mail server</displayName>
<displayShortName><?=$autodiscover_config['displayShortName']; ?></displayShortName> <displayShortName>mail server</displayShortName>
<incomingServer type="imap"> <incomingServer type="imap">
<hostname><?=$autodiscover_config['imap']['server']; ?></hostname> <hostname><?=$autodiscover_config['imap']['server']; ?></hostname>

View File

@@ -7,21 +7,19 @@ if(file_exists('inc/vars.local.inc.php')) {
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.inc.php';
$default_autodiscover_config = $autodiscover_config; $default_autodiscover_config = $autodiscover_config;
$autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config); $autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
// Redis // Valkey
$redis = new Redis(); $valkey = new Redis();
try { try {
if (!empty(getenv('REDIS_SLAVEOF_IP'))) { if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); $valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
} }
else { else {
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
} }
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
} }
catch (Exception $e) { catch (Exception $e) {
exit; exit;
@@ -60,176 +58,110 @@ $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
$iam_provider = identity_provider('init'); $iam_provider = identity_provider('init');
$iam_settings = identity_provider('get'); $iam_settings = identity_provider('get');
// Passwordless autodiscover - no authentication required $login_user = strtolower(trim($_SERVER['PHP_AUTH_USER']));
// Email will be extracted from the request body $login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW']));
$login_user = null;
$login_role = null;
header("Content-Type: application/xml"); if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) {
echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
<?php
if(!$data) {
try {
$json = json_encode(
array(
"time" => time(),
"ua" => $_SERVER['HTTP_USER_AGENT'],
"user" => "none",
"ip" => $_SERVER['REMOTE_ADDR'],
"service" => "Error: invalid or missing request data"
)
);
$redis->lPush('AUTODISCOVER_LOG', $json);
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
error_log("Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'msg' => 'Redis: '.$e
);
return false;
}
list($usec, $sec) = explode(' ', microtime());
?>
<Response>
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
<ErrorCode>600</ErrorCode>
<Message>Invalid Request</Message>
<DebugData />
</Error>
</Response>
</Autodiscover>
<?php
exit(0);
}
try {
$discover = new SimpleXMLElement($data);
$email = $discover->Request->EMailAddress;
} catch (Exception $e) {
// If parsing fails, return error
try {
$json = json_encode(
array(
"time" => time(),
"ua" => $_SERVER['HTTP_USER_AGENT'],
"user" => "none",
"ip" => $_SERVER['REMOTE_ADDR'],
"service" => "Error: could not parse email from request"
)
);
$redis->lPush('AUTODISCOVER_LOG', $json);
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
$redis->publish("F2B_CHANNEL", "Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
error_log("Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
}
catch (RedisException $e) {
// Silently fail
}
list($usec, $sec) = explode(' ', microtime());
?>
<Response>
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
<ErrorCode>600</ErrorCode>
<Message>Invalid Request</Message>
<DebugData />
</Error>
</Response>
</Autodiscover>
<?php
exit(0);
}
$username = trim((string)$email);
try {
$stmt = $pdo->prepare("SELECT `mailbox`.`name`, `mailbox`.`active` FROM `mailbox`
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
WHERE `mailbox`.`username` = :username
AND `mailbox`.`active` = '1'
AND `domain`.`active` = '1'");
$stmt->execute(array(':username' => $username));
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
}
catch(PDOException $e) {
// Database error - return error response with complete XML
list($usec, $sec) = explode(' ', microtime());
?>
<Response>
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
<ErrorCode>500</ErrorCode>
<Message>Database Error</Message>
<DebugData />
</Error>
</Response>
</Autodiscover>
<?php
exit(0);
}
// Mailbox not found or not active - return generic error to prevent user enumeration
if (empty($MailboxData)) {
try {
$json = json_encode(
array(
"time" => time(),
"ua" => $_SERVER['HTTP_USER_AGENT'],
"user" => $email,
"ip" => $_SERVER['REMOTE_ADDR'],
"service" => "Error: mailbox not found or inactive"
)
);
$redis->lPush('AUTODISCOVER_LOG', $json);
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
error_log("Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
}
catch (RedisException $e) {
// Silently fail
}
list($usec, $sec) = explode(' ', microtime());
?>
<Response>
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
<ErrorCode>600</ErrorCode>
<Message>Invalid Request</Message>
<DebugData />
</Error>
</Response>
</Autodiscover>
<?php
exit(0);
}
if (!empty($MailboxData['name'])) {
$displayname = $MailboxData['name'];
}
else {
$displayname = $email;
}
try {
$json = json_encode( $json = json_encode(
array( array(
"time" => time(), "time" => time(),
"ua" => $_SERVER['HTTP_USER_AGENT'], "ua" => $_SERVER['HTTP_USER_AGENT'],
"user" => $email, "user" => "none",
"ip" => $_SERVER['REMOTE_ADDR'], "ip" => $_SERVER['REMOTE_ADDR'],
"service" => $autodiscover_config['autodiscoverType'] "service" => "Error: must be authenticated"
) )
); );
$redis->lPush('AUTODISCOVER_LOG', $json); $valkey->lPush('AUTODISCOVER_LOG', $json);
$redis->lTrim('AUTODISCOVER_LOG', 0, 100); header('WWW-Authenticate: Basic realm="' . $_SERVER['HTTP_HOST'] . '"');
header('HTTP/1.0 401 Unauthorized');
exit(0);
} }
catch (RedisException $e) {
$_SESSION['return'][] = array( $login_role = check_login($login_user, $login_pass, array('eas' => TRUE));
'type' => 'danger',
'msg' => 'Redis: '.$e if ($login_role === "user") {
); header("Content-Type: application/xml");
return false; echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
} ?>
if ($autodiscover_config['autodiscoverType'] == 'imap') { <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
<?php
if(!$data) {
try {
$json = json_encode(
array(
"time" => time(),
"ua" => $_SERVER['HTTP_USER_AGENT'],
"user" => $_SERVER['PHP_AUTH_USER'],
"ip" => $_SERVER['REMOTE_ADDR'],
"service" => "Error: invalid or missing request data"
)
);
$valkey->lPush('AUTODISCOVER_LOG', $json);
$valkey->lTrim('AUTODISCOVER_LOG', 0, 100);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'msg' => 'Valkey: '.$e
);
return false;
}
list($usec, $sec) = explode(' ', microtime());
?>
<Response>
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="2477272013">
<ErrorCode>600</ErrorCode>
<Message>Invalid Request</Message>
<DebugData />
</Error>
</Response>
</Autodiscover>
<?php
exit(0);
}
try {
$discover = new SimpleXMLElement($data);
$email = $discover->Request->EMailAddress;
} catch (Exception $e) {
$email = $_SERVER['PHP_AUTH_USER'];
}
$username = trim($email);
try {
$stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username");
$stmt->execute(array(':username' => $username));
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
}
catch(PDOException $e) {
die("Failed to determine name from SQL");
}
if (!empty($MailboxData['name'])) {
$displayname = $MailboxData['name'];
}
else {
$displayname = $email;
}
try {
$json = json_encode(
array(
"time" => time(),
"ua" => $_SERVER['HTTP_USER_AGENT'],
"user" => $_SERVER['PHP_AUTH_USER'],
"ip" => $_SERVER['REMOTE_ADDR'],
"service" => $autodiscover_config['autodiscoverType']
)
);
$valkey->lPush('AUTODISCOVER_LOG', $json);
$valkey->lTrim('AUTODISCOVER_LOG', 0, 100);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'msg' => 'Valkey: '.$e
);
return false;
}
if ($autodiscover_config['autodiscoverType'] == 'imap') {
?> ?>
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"> <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
<User> <User>
@@ -304,3 +236,6 @@ if ($autodiscover_config['autodiscoverType'] == 'imap') {
} }
?> ?>
</Autodiscover> </Autodiscover>
<?php
}
?>

View File

@@ -3,11 +3,8 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
// Only redirect to mailbox if NO pending actions header('Location: /domainadmin/mailbox');
if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) { exit();
header('Location: /domainadmin/mailbox');
exit();
}
} }
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
header('Location: /admin/dashboard'); header('Location: /admin/dashboard');

View File

@@ -2,7 +2,18 @@
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
protect_route(['domainadmin']); if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
header('Location: /admin/dashboard');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "domainadmin") {
header('Location: /domainadmin');
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];

View File

@@ -2,28 +2,41 @@
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
/* if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
/ DOMAIN ADMIN
*/
protect_route(['domainadmin']); /*
/ DOMAIN ADMIN
*/
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
$tfa_data = get_tfa(); $tfa_data = get_tfa();
$fido2_data = fido2(array("action" => "get_friendly_names")); $fido2_data = fido2(array("action" => "get_friendly_names"));
$username = $_SESSION['mailcow_cc_username']; $username = $_SESSION['mailcow_cc_username'];
$template = 'domainadmin.twig'; $template = 'domainadmin.twig';
$template_data = [ $template_data = [
'acl' => $_SESSION['acl'], 'acl' => $_SESSION['acl'],
'acl_json' => json_encode($_SESSION['acl']), 'acl_json' => json_encode($_SESSION['acl']),
'user_spam_score' => mailbox('get', 'spam_score', $username), 'user_spam_score' => mailbox('get', 'spam_score', $username),
'tfa_data' => $tfa_data, 'tfa_data' => $tfa_data,
'fido2_data' => $fido2_data, 'fido2_data' => $fido2_data,
'lang_user' => json_encode($lang['user']), 'lang_user' => json_encode($lang['user']),
'lang_datatables' => json_encode($lang['datatables']), 'lang_datatables' => json_encode($lang['datatables']),
]; ];
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
header('Location: /admin/dashboard');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
else {
header('Location: /domainadmin');
exit();
}
$js_minifier->add('/web/js/site/user.js'); $js_minifier->add('/web/js/site/user.js');
$js_minifier->add('/web/js/site/pwgen.js'); $js_minifier->add('/web/js/site/pwgen.js');

View File

@@ -1,8 +1,10 @@
<?php <?php
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
$AuthUsers = array("admin", "domainadmin", "user");
protect_route(); if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) {
header('Location: /');
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$template = 'edit.twig'; $template = 'edit.twig';

View File

@@ -129,16 +129,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
); );
} }
// Check if domain is an alias domain and get target domain's MTA-STS $mta_sts = mailbox('get', 'mta_sts', $domain);
$alias_domain_details = mailbox('get', 'alias_domain_details', $domain);
$mta_sts_domain = $domain;
if ($alias_domain_details !== false && !empty($alias_domain_details['target_domain'])) {
// This is an alias domain, check target domain for MTA-STS
$mta_sts_domain = $alias_domain_details['target_domain'];
}
$mta_sts = mailbox('get', 'mta_sts', $mta_sts_domain);
if (count($mta_sts) > 0 && $mta_sts['active'] == 1) { if (count($mta_sts) > 0 && $mta_sts['active'] == 1) {
if (!in_array($domain, $alias_domains)) { if (!in_array($domain, $alias_domains)) {
$records[] = array( $records[] = array(

View File

@@ -64,8 +64,6 @@ $globalVariables = [
'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'], 'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'],
'pending_tfa_authmechs' => $pending_tfa_authmechs, 'pending_tfa_authmechs' => $pending_tfa_authmechs,
'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'], 'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'],
'pending_tfa_setup' => !empty($_SESSION['pending_tfa_setup']),
'pending_pw_update_modal' => !empty($_SESSION['pending_pw_update']),
'lang_footer' => json_encode($lang['footer']), 'lang_footer' => json_encode($lang['footer']),
'lang_acl' => json_encode($lang['acl']), 'lang_acl' => json_encode($lang['acl']),
'lang_tfa' => json_encode($lang['tfa']), 'lang_tfa' => json_encode($lang['tfa']),

View File

@@ -121,56 +121,34 @@ function admin($_action, $_data = null) {
continue; continue;
} }
} }
// Check if this is a self password change via forced update if (!empty($password)) {
if ($username == $_SESSION['mailcow_cc_username'] && !empty($_SESSION['pending_pw_update'])) { if (password_check($password, $password2) !== true) {
// Forced password update: only change password and clear force_pw_update flag return false;
if (!empty($password)) {
if (password_check($password, $_data['password2']) !== true) {
return false;
}
$password_hashed = hash_password($password);
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed,
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_pw_update', '0')
WHERE `username` = :username");
$stmt->execute(array(
':password_hashed' => $password_hashed,
':username' => $username
));
unset($_SESSION['pending_pw_update']);
} }
} else { $password_hashed = hash_password($password);
// Normal admin edit: update all attributes $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
$force_tfa = intval($_data['force_tfa'] ?? 0) ? 1 : 0; $stmt->execute(array(
$force_pw_update = intval($_data['force_pw_update'] ?? 0) ? 1 : 0; ':password_hashed' => $password_hashed,
if (!empty($password)) { ':username_new' => $username_new,
if (password_check($password, $password2) !== true) { ':username' => $username,
return false; ':active' => $active
} ));
$password_hashed = hash_password($password); if (isset($_data['disable_tfa'])) {
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed, $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update) $stmt->execute(array(':username' => $username));
WHERE `username` = :username");
$stmt->execute(array(
':password_hashed' => $password_hashed,
':username_new' => $username_new,
':username' => $username,
':active' => $active,
':force_tfa' => strval($force_tfa),
':force_pw_update' => strval($force_pw_update)
));
} }
else { else {
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update) $stmt->execute(array(':username_new' => $username_new, ':username' => $username));
WHERE `username` = :username");
$stmt->execute(array(
':username_new' => $username_new,
':username' => $username,
':active' => $active,
':force_tfa' => strval($force_tfa),
':force_pw_update' => strval($force_pw_update)
));
} }
}
else {
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
$stmt->execute(array(
':username_new' => $username_new,
':username' => $username,
':active' => $active
));
if (isset($_data['disable_tfa'])) { if (isset($_data['disable_tfa'])) {
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
$stmt->execute(array(':username' => $username)); $stmt->execute(array(':username' => $username));
@@ -245,8 +223,7 @@ function admin($_action, $_data = null) {
`tfa`.`active` AS `tfa_active`, `tfa`.`active` AS `tfa_active`,
`admin`.`username`, `admin`.`username`,
`admin`.`created`, `admin`.`created`,
`admin`.`active` AS `active`, `admin`.`active` AS `active`
`admin`.`attributes` AS `attributes`
FROM `admin` FROM `admin`
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`admin`.`username` LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`admin`.`username`
WHERE `admin`.`username`= :admin AND `superadmin` = '1'"); WHERE `admin`.`username`= :admin AND `superadmin` = '1'");
@@ -263,7 +240,6 @@ function admin($_action, $_data = null) {
$admindata['active'] = $row['active']; $admindata['active'] = $row['active'];
$admindata['active_int'] = $row['active']; $admindata['active_int'] = $row['active'];
$admindata['created'] = $row['created']; $admindata['created'] = $row['created'];
$admindata['attributes'] = json_decode($row['attributes'], true) ?? array('force_tfa' => '0', 'force_pw_update' => '0');
return $admindata; return $admindata;
break; break;
} }

View File

@@ -1,11 +1,10 @@
<?php <?php
function check_login($user, $pass, $extra = null) { function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
global $pdo; global $pdo;
global $redis; global $valkey;
$is_internal = $extra['is_internal']; $is_internal = $extra['is_internal'];
$role = $extra['role']; $role = $extra['role'];
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
// Try validate admin // Try validate admin
if (!isset($role) || $role == "admin") { if (!isset($role) || $role == "admin") {
@@ -26,20 +25,34 @@ function check_login($user, $pass, $extra = null) {
// Try validate app password // Try validate app password
if (!isset($role) || $role == "app") { if (!isset($role) || $role == "app") {
$result = apppass_login($user, $pass, $extra); $result = apppass_login($user, $pass, $app_passwd_data);
if ($result !== false) { if ($result !== false) {
if ($app_passwd_data['eas'] === true) {
$service = 'EAS';
} elseif ($app_passwd_data['dav'] === true) {
$service = 'DAV';
} else {
$service = 'NONE';
}
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']); $real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
set_sasl_log($user, $real_rip, $extra['service'], $pass); set_sasl_log($user, $real_rip, $service, $pass);
return $result; return $result;
} }
} }
// Try validate user // Try validate user
if (!isset($role) || $role == "user") { if (!isset($role) || $role == "user") {
$result = user_login($user, $pass, $extra); $result = user_login($user, $pass);
if ($result !== false) { if ($result !== false) {
if ($app_passwd_data['eas'] === true) {
$service = 'EAS';
} elseif ($app_passwd_data['dav'] === true) {
$service = 'DAV';
} else {
$service = 'MAILCOWUI';
}
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']); $real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
set_sasl_log($user, $real_rip, $extra['service']); set_sasl_log($user, $real_rip, $service);
return $result; return $result;
} }
} }
@@ -49,12 +62,12 @@ function check_login($user, $pass, $extra = null) {
if (!isset($_SESSION['ldelay'])) { if (!isset($_SESSION['ldelay'])) {
$_SESSION['ldelay'] = "0"; $_SESSION['ldelay'] = "0";
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); $valkey->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
} }
elseif (!isset($_SESSION['mailcow_cc_username'])) { elseif (!isset($_SESSION['mailcow_cc_username'])) {
$_SESSION['ldelay'] = $_SESSION['ldelay']+0.5; $_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); $valkey->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
} }
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -82,7 +95,7 @@ function admin_login($user, $pass){
} }
$user = strtolower(trim($user)); $user = strtolower(trim($user));
$stmt = $pdo->prepare("SELECT `password`, `attributes` FROM `admin` $stmt = $pdo->prepare("SELECT `password` FROM `admin`
WHERE `superadmin` = '1' WHERE `superadmin` = '1'
AND `active` = '1' AND `active` = '1'
AND `username` = :user"); AND `username` = :user");
@@ -91,13 +104,6 @@ function admin_login($user, $pass){
// verify password // verify password
if (verify_hash($row['password'], $pass)) { if (verify_hash($row['password'], $pass)) {
$admin_attrs = json_decode($row['attributes'], true) ?? [];
// Check force_pw_update
if (intval($admin_attrs['force_pw_update'] ?? 0) == 1) {
$_SESSION['pending_pw_update'] = true;
}
// check for tfa authenticators // check for tfa authenticators
$authenticators = get_tfa($user); $authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
@@ -117,10 +123,6 @@ function admin_login($user, $pass){
// Reactivate TFA if it was set to "deactivate TFA for next login" // Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
// Check force_tfa: only force setup if NO TFA exists at all
if (intval($admin_attrs['force_tfa'] ?? 0) == 1 && !tfa_exists($user)) {
$_SESSION['pending_tfa_setup'] = true;
}
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'), 'log' => array(__FUNCTION__, $user, '*'),
@@ -146,7 +148,7 @@ function domainadmin_login($user, $pass){
return false; return false;
} }
$stmt = $pdo->prepare("SELECT `password`, `attributes` FROM `admin` $stmt = $pdo->prepare("SELECT `password` FROM `admin`
WHERE `superadmin` = '0' WHERE `superadmin` = '0'
AND `active`='1' AND `active`='1'
AND `username` = :user"); AND `username` = :user");
@@ -155,13 +157,6 @@ function domainadmin_login($user, $pass){
// verify password // verify password
if (verify_hash($row['password'], $pass) !== false) { if (verify_hash($row['password'], $pass) !== false) {
$admin_attrs = json_decode($row['attributes'], true) ?? [];
// Check force_pw_update
if (intval($admin_attrs['force_pw_update'] ?? 0) == 1) {
$_SESSION['pending_pw_update'] = true;
}
// check for tfa authenticators // check for tfa authenticators
$authenticators = get_tfa($user); $authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
@@ -181,10 +176,6 @@ function domainadmin_login($user, $pass){
// Reactivate TFA if it was set to "deactivate TFA for next login" // Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
// Check force_tfa: only force setup if NO TFA exists at all
if (intval($admin_attrs['force_tfa'] ?? 0) == 1 && !tfa_exists($user)) {
$_SESSION['pending_tfa_setup'] = true;
}
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'), 'log' => array(__FUNCTION__, $user, '*'),
@@ -202,7 +193,7 @@ function user_login($user, $pass, $extra = null){
global $iam_settings; global $iam_settings;
$is_internal = $extra['is_internal']; $is_internal = $extra['is_internal'];
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service']; $service = $extra['service'];
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) { if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
if (!$is_internal){ if (!$is_internal){
@@ -245,10 +236,10 @@ function user_login($user, $pass, $extra = null){
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!empty($row)) { if (!empty($row)) {
// check if user has access to service (imap, smtp, pop3, sieve, dav, eas) if service is set // check if user has access to service (imap, smtp, pop3, sieve) if service is set
$row['attributes'] = json_decode($row['attributes'], true); $row['attributes'] = json_decode($row['attributes'], true);
if ($extra['service'] != 'NONE') { if (isset($service)) {
$key = strtolower($extra['service']) . "_access"; $key = strtolower($service) . "_access";
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') { if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
return false; return false;
} }
@@ -262,8 +253,8 @@ function user_login($user, $pass, $extra = null){
// check if user has access to service (imap, smtp, pop3, sieve) if service is set // check if user has access to service (imap, smtp, pop3, sieve) if service is set
$row['attributes'] = json_decode($row['attributes'], true); $row['attributes'] = json_decode($row['attributes'], true);
if ($extra['service'] != 'NONE') { if (isset($service)) {
$key = strtolower($extra['service']) . "_access"; $key = strtolower($service) . "_access";
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') { if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
return false; return false;
} }
@@ -287,8 +278,6 @@ function user_login($user, $pass, $extra = null){
return false; return false;
} }
$row['attributes'] = json_decode($row['attributes'], true);
// check for tfa authenticators // check for tfa authenticators
$authenticators = get_tfa($user); $authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) { if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
@@ -310,10 +299,6 @@ function user_login($user, $pass, $extra = null){
// Reactivate TFA if it was set to "deactivate TFA for next login" // Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
// Check force_tfa: only force setup if NO TFA exists at all
if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) {
$_SESSION['pending_tfa_setup'] = true;
}
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $user, '*', 'Provider: Keycloak'), 'log' => array(__FUNCTION__, $user, '*', 'Provider: Keycloak'),
@@ -345,8 +330,6 @@ function user_login($user, $pass, $extra = null){
return false; return false;
} }
$row['attributes'] = json_decode($row['attributes'], true);
// check for tfa authenticators // check for tfa authenticators
$authenticators = get_tfa($user); $authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) { if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
@@ -368,10 +351,6 @@ function user_login($user, $pass, $extra = null){
// Reactivate TFA if it was set to "deactivate TFA for next login" // Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
// Check force_tfa: only force setup if NO TFA exists at all
if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) {
$_SESSION['pending_tfa_setup'] = true;
}
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $user, '*', 'Provider: LDAP'), 'log' => array(__FUNCTION__, $user, '*', 'Provider: LDAP'),
@@ -415,10 +394,6 @@ function user_login($user, $pass, $extra = null){
// Reactivate TFA if it was set to "deactivate TFA for next login" // Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
// Check force_tfa: only force setup if NO TFA exists at all
if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) {
$_SESSION['pending_tfa_setup'] = true;
}
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $user, '*', 'Provider: mailcow'), 'log' => array(__FUNCTION__, $user, '*', 'Provider: mailcow'),
@@ -433,7 +408,7 @@ function user_login($user, $pass, $extra = null){
return false; return false;
} }
function apppass_login($user, $pass, $extra = null){ function apppass_login($user, $pass, $app_passwd_data, $extra = null){
global $pdo; global $pdo;
$is_internal = $extra['is_internal']; $is_internal = $extra['is_internal'];
@@ -449,8 +424,20 @@ function apppass_login($user, $pass, $extra = null){
return false; return false;
} }
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service']; $protocol = false;
if (!$is_internal && $extra['service'] == 'NONE') { if ($app_passwd_data['eas']){
$protocol = 'eas';
} else if ($app_passwd_data['dav']){
$protocol = 'dav';
} else if ($app_passwd_data['smtp']){
$protocol = 'smtp';
} else if ($app_passwd_data['imap']){
$protocol = 'imap';
} else if ($app_passwd_data['sieve']){
$protocol = 'sieve';
} else if ($app_passwd_data['pop3']){
$protocol = 'pop3';
} else if (!$is_internal) {
return false; return false;
} }
@@ -471,7 +458,7 @@ function apppass_login($user, $pass, $extra = null){
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) { foreach ($rows as $row) {
if ($extra['service'] != 'NONE' && $row[strtolower($extra['service']) . '_access'] != '1'){ if ($protocol && $row[$protocol . '_access'] != '1'){
continue; continue;
} }

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<?php <?php
function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $extra_headers = null) { function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $extra_headers = null) {
global $DOCKER_TIMEOUT; global $DOCKER_TIMEOUT;
global $redis; global $valkey;
$curl = curl_init(); $curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: application/json' )); curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: application/json' ));
// We are using our mail certificates for dockerapi, the names will not match, the certs are trusted anyway // We are using our mail certificates for dockerapi, the names will not match, the certs are trusted anyway
@@ -63,7 +63,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
break; break;
case 'info': case 'info':
if (empty($service_name)) { if (empty($service_name)) {
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json?all=true'); curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 0); curl_setopt($curl, CURLOPT_POST, 0);
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT); curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
@@ -200,7 +200,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
"request" => $attr2 "request" => $attr2
); );
$redis->publish("MC_CHANNEL", json_encode($request)); $valkey->publish("MC_CHANNEL", json_encode($request));
return true; return true;
break; break;
} }

View File

@@ -195,23 +195,17 @@ function domain_admin($_action, $_data = null) {
)); ));
} }
} }
$force_tfa = intval($_data['force_tfa'] ?? 0) ? 1 : 0;
$force_pw_update = intval($_data['force_pw_update'] ?? 0) ? 1 : 0;
if (!empty($password)) { if (!empty($password)) {
if (password_check($password, $password2) !== true) { if (password_check($password, $password2) !== true) {
return false; return false;
} }
$password_hashed = hash_password($password); $password_hashed = hash_password($password);
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed, $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':password_hashed' => $password_hashed, ':password_hashed' => $password_hashed,
':username_new' => $username_new, ':username_new' => $username_new,
':username' => $username, ':username' => $username,
':active' => $active, ':active' => $active
':force_tfa' => strval($force_tfa),
':force_pw_update' => strval($force_pw_update)
)); ));
if (isset($_data['disable_tfa'])) { if (isset($_data['disable_tfa'])) {
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
@@ -223,15 +217,11 @@ function domain_admin($_action, $_data = null) {
} }
} }
else { else {
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':username_new' => $username_new, ':username_new' => $username_new,
':username' => $username, ':username' => $username,
':active' => $active, ':active' => $active
':force_tfa' => strval($force_tfa),
':force_pw_update' => strval($force_pw_update)
)); ));
if (isset($_data['disable_tfa'])) { if (isset($_data['disable_tfa'])) {
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
@@ -254,37 +244,31 @@ function domain_admin($_action, $_data = null) {
// Can only edit itself // Can only edit itself
elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") { elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
$username = $_SESSION['mailcow_cc_username']; $username = $_SESSION['mailcow_cc_username'];
$password_old = $_data['user_old_pass'] ?? ''; $password_old = $_data['user_old_pass'];
$password_new = $_data['user_new_pass']; $password_new = $_data['user_new_pass'];
$password_new2 = $_data['user_new_pass2']; $password_new2 = $_data['user_new_pass2'];
// Only verify old password if this is NOT a forced password update $stmt = $pdo->prepare("SELECT `password` FROM `admin`
if (empty($_SESSION['pending_pw_update'])) { WHERE `username` = :user");
$stmt = $pdo->prepare("SELECT `password` FROM `admin` $stmt->execute(array(':user' => $username));
WHERE `username` = :user"); $row = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt->execute(array(':user' => $username)); if (!verify_hash($row['password'], $password_old)) {
$row = $stmt->fetch(PDO::FETCH_ASSOC); $_SESSION['return'][] = array(
if (!verify_hash($row['password'], $password_old)) { 'type' => 'danger',
$_SESSION['return'][] = array( 'log' => array(__FUNCTION__, $_action, $_data_log),
'type' => 'danger', 'msg' => 'access_denied'
'log' => array(__FUNCTION__, $_action, $_data_log), );
'msg' => 'access_denied' return false;
);
return false;
}
} }
if (password_check($password_new, $password_new2) !== true) { if (password_check($password_new, $password_new2) !== true) {
return false; return false;
} }
$password_hashed = hash_password($password_new); $password_hashed = hash_password($password_new);
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed, $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_pw_update', '0')
WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':password_hashed' => $password_hashed, ':password_hashed' => $password_hashed,
':username' => $username ':username' => $username
)); ));
unset($_SESSION['pending_pw_update']);
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
@@ -376,11 +360,9 @@ function domain_admin($_action, $_data = null) {
`tfa`.`active` AS `tfa_active`, `tfa`.`active` AS `tfa_active`,
`domain_admins`.`username`, `domain_admins`.`username`,
`domain_admins`.`created`, `domain_admins`.`created`,
`domain_admins`.`active` AS `active`, `domain_admins`.`active` AS `active`
`admin`.`attributes` AS `attributes`
FROM `domain_admins` FROM `domain_admins`
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username` LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
LEFT OUTER JOIN `admin` ON `admin`.`username`=`domain_admins`.`username`
WHERE `domain_admins`.`username`= :domain_admin"); WHERE `domain_admins`.`username`= :domain_admin");
$stmt->execute(array( $stmt->execute(array(
':domain_admin' => $_data ':domain_admin' => $_data
@@ -395,7 +377,6 @@ function domain_admin($_action, $_data = null) {
$domainadmindata['active'] = $row['active']; $domainadmindata['active'] = $row['active'];
$domainadmindata['active_int'] = $row['active']; $domainadmindata['active_int'] = $row['active'];
$domainadmindata['created'] = $row['created']; $domainadmindata['created'] = $row['created'];
$domainadmindata['attributes'] = json_decode($row['attributes'], true) ?? array('force_tfa' => '0', 'force_pw_update' => '0');
// GET SELECTED // GET SELECTED
$stmt = $pdo->prepare("SELECT `domain` FROM `domain` $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain` IN ( WHERE `domain` IN (

View File

@@ -1,6 +1,6 @@
<?php <?php
function fail2ban($_action, $_data = null, $_extra = null) { function fail2ban($_action, $_data = null, $_extra = null) {
global $redis; global $valkey;
$_data_log = $_data; $_data_log = $_data;
switch ($_action) { switch ($_action) {
case 'get': case 'get':
@@ -9,9 +9,9 @@ function fail2ban($_action, $_data = null, $_extra = null) {
return false; return false;
} }
try { try {
$f2b_options = json_decode($redis->Get('F2B_OPTIONS'), true); $f2b_options = json_decode($valkey->Get('F2B_OPTIONS'), true);
$f2b_options['regex'] = json_decode($redis->Get('F2B_REGEX'), true); $f2b_options['regex'] = json_decode($valkey->Get('F2B_REGEX'), true);
$wl = $redis->hGetAll('F2B_WHITELIST'); $wl = $valkey->hGetAll('F2B_WHITELIST');
if (is_array($wl)) { if (is_array($wl)) {
foreach ($wl as $key => $value) { foreach ($wl as $key => $value) {
$tmp_wl_data[] = $key; $tmp_wl_data[] = $key;
@@ -27,7 +27,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
else { else {
$f2b_options['whitelist'] = ""; $f2b_options['whitelist'] = "";
} }
$bl = $redis->hGetAll('F2B_BLACKLIST'); $bl = $valkey->hGetAll('F2B_BLACKLIST');
if (is_array($bl)) { if (is_array($bl)) {
foreach ($bl as $key => $value) { foreach ($bl as $key => $value) {
$tmp_bl_data[] = $key; $tmp_bl_data[] = $key;
@@ -43,7 +43,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
else { else {
$f2b_options['blacklist'] = ""; $f2b_options['blacklist'] = "";
} }
$pb = $redis->hGetAll('F2B_PERM_BANS'); $pb = $valkey->hGetAll('F2B_PERM_BANS');
if (is_array($pb)) { if (is_array($pb)) {
foreach ($pb as $key => $value) { foreach ($pb as $key => $value) {
$f2b_options['perm_bans'][] = array( $f2b_options['perm_bans'][] = array(
@@ -56,8 +56,8 @@ function fail2ban($_action, $_data = null, $_extra = null) {
else { else {
$f2b_options['perm_bans'] = ""; $f2b_options['perm_bans'] = "";
} }
$active_bans = $redis->hGetAll('F2B_ACTIVE_BANS'); $active_bans = $valkey->hGetAll('F2B_ACTIVE_BANS');
$queue_unban = $redis->hGetAll('F2B_QUEUE_UNBAN'); $queue_unban = $valkey->hGetAll('F2B_QUEUE_UNBAN');
if (is_array($active_bans)) { if (is_array($active_bans)) {
foreach ($active_bans as $network => $banned_until) { foreach ($active_bans as $network => $banned_until) {
$queued_for_unban = (isset($queue_unban[$network]) && $queue_unban[$network] == 1) ? 1 : 0; $queued_for_unban = (isset($queue_unban[$network]) && $queue_unban[$network] == 1) ? 1 : 0;
@@ -78,7 +78,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -98,22 +98,22 @@ function fail2ban($_action, $_data = null, $_extra = null) {
// Reset regex filters // Reset regex filters
if ($_data['action'] == "reset-regex") { if ($_data['action'] == "reset-regex") {
try { try {
$redis->Del('F2B_REGEX'); $valkey->Del('F2B_REGEX');
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
// Rules will also be recreated on log events, but rules may seem empty for a second in the UI // Rules will also be recreated on log events, but rules may seem empty for a second in the UI
docker('post', 'netfilter-mailcow', 'restart'); docker('post', 'netfilter-mailcow', 'restart');
$fail_count = 0; $fail_count = 0;
$regex_result = json_decode($redis->Get('F2B_REGEX'), true); $regex_result = json_decode($valkey->Get('F2B_REGEX'), true);
while (empty($regex_result) && $fail_count < 10) { while (empty($regex_result) && $fail_count < 10) {
$regex_result = json_decode($redis->Get('F2B_REGEX'), true); $regex_result = json_decode($valkey->Get('F2B_REGEX'), true);
$fail_count++; $fail_count++;
sleep(1); sleep(1);
} }
@@ -135,7 +135,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$rule_id++; $rule_id++;
} }
if (!empty($regex_array)) { if (!empty($regex_array)) {
$redis->Set('F2B_REGEX', json_encode($regex_array, JSON_UNESCAPED_SLASHES)); $valkey->Set('F2B_REGEX', json_encode($regex_array, JSON_UNESCAPED_SLASHES));
} }
} }
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -154,13 +154,13 @@ function fail2ban($_action, $_data = null, $_extra = null) {
if ($_data['action'] == "unban") { if ($_data['action'] == "unban") {
if (valid_network($network)) { if (valid_network($network)) {
try { try {
$redis->hSet('F2B_QUEUE_UNBAN', $network, 1); $valkey->hSet('F2B_QUEUE_UNBAN', $network, 1);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -171,15 +171,15 @@ function fail2ban($_action, $_data = null, $_extra = null) {
if (empty($network)) { continue; } if (empty($network)) { continue; }
if (valid_network($network)) { if (valid_network($network)) {
try { try {
$redis->hSet('F2B_WHITELIST', $network, 1); $valkey->hSet('F2B_WHITELIST', $network, 1);
$redis->hDel('F2B_BLACKLIST', $network, 1); $valkey->hDel('F2B_BLACKLIST', $network, 1);
$redis->hSet('F2B_QUEUE_UNBAN', $network, 1); $valkey->hSet('F2B_QUEUE_UNBAN', $network, 1);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -204,15 +204,15 @@ function fail2ban($_action, $_data = null, $_extra = null) {
getenv('IPV6_NETWORK') getenv('IPV6_NETWORK')
))) { ))) {
try { try {
$redis->hSet('F2B_BLACKLIST', $network, 1); $valkey->hSet('F2B_BLACKLIST', $network, 1);
$redis->hDel('F2B_WHITELIST', $network, 1); $valkey->hDel('F2B_WHITELIST', $network, 1);
//$response = docker('post', 'netfilter-mailcow', 'restart'); //$response = docker('post', 'netfilter-mailcow', 'restart');
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -270,16 +270,16 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$f2b_options['banlist_id'] = $is_now['banlist_id']; $f2b_options['banlist_id'] = $is_now['banlist_id'];
$f2b_options['manage_external'] = ($manage_external > 0) ? 1 : 0; $f2b_options['manage_external'] = ($manage_external > 0) ? 1 : 0;
try { try {
$redis->Set('F2B_OPTIONS', json_encode($f2b_options)); $valkey->Set('F2B_OPTIONS', json_encode($f2b_options));
$redis->Del('F2B_WHITELIST'); $valkey->Del('F2B_WHITELIST');
$redis->Del('F2B_BLACKLIST'); $valkey->Del('F2B_BLACKLIST');
if(!empty($wl)) { if(!empty($wl)) {
$wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl)); $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
$wl_array = array_filter($wl_array); $wl_array = array_filter($wl_array);
if (is_array($wl_array)) { if (is_array($wl_array)) {
foreach ($wl_array as $wl_item) { foreach ($wl_array as $wl_item) {
if (valid_network($wl_item) || valid_hostname($wl_item)) { if (valid_network($wl_item) || valid_hostname($wl_item)) {
$redis->hSet('F2B_WHITELIST', $wl_item, 1); $valkey->hSet('F2B_WHITELIST', $wl_item, 1);
} }
else { else {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -304,7 +304,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
getenv('IPV4_NETWORK') . '0', getenv('IPV4_NETWORK') . '0',
getenv('IPV6_NETWORK') getenv('IPV6_NETWORK')
))) { ))) {
$redis->hSet('F2B_BLACKLIST', $bl_item, 1); $valkey->hSet('F2B_BLACKLIST', $bl_item, 1);
} }
else { else {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -322,7 +322,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -334,13 +334,13 @@ function fail2ban($_action, $_data = null, $_extra = null) {
break; break;
case 'banlist': case 'banlist':
try { try {
$f2b_options = json_decode($redis->Get('F2B_OPTIONS'), true); $f2b_options = json_decode($valkey->Get('F2B_OPTIONS'), true);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra), 'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
http_response_code(500); http_response_code(500);
return false; return false;
@@ -356,14 +356,14 @@ function fail2ban($_action, $_data = null, $_extra = null) {
switch ($_data) { switch ($_data) {
case 'get': case 'get':
try { try {
$bl = $redis->hKeys('F2B_BLACKLIST'); $bl = $valkey->hKeys('F2B_BLACKLIST');
$active_bans = $redis->hKeys('F2B_ACTIVE_BANS'); $active_bans = $valkey->hKeys('F2B_ACTIVE_BANS');
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra), 'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
http_response_code(500); http_response_code(500);
return false; return false;
@@ -378,13 +378,13 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$f2b_options['banlist_id'] = uuid4(); $f2b_options['banlist_id'] = uuid4();
try { try {
$redis->Set('F2B_OPTIONS', json_encode($f2b_options)); $valkey->Set('F2B_OPTIONS', json_encode($f2b_options));
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra), 'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }

View File

@@ -1,7 +1,7 @@
<?php <?php
function fwdhost($_action, $_data = null) { function fwdhost($_action, $_data = null) {
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/spf.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/spf.inc.php';
global $redis; global $valkey;
global $lang; global $lang;
$_data_log = $_data; $_data_log = $_data;
switch ($_action) { switch ($_action) {
@@ -37,19 +37,19 @@ function fwdhost($_action, $_data = null) {
} }
foreach ($hosts as $host) { foreach ($hosts as $host) {
try { try {
$redis->hSet('WHITELISTED_FWD_HOST', $host, $source); $valkey->hSet('WHITELISTED_FWD_HOST', $host, $source);
if ($filter_spam == 0) { if ($filter_spam == 0) {
$redis->hSet('KEEP_SPAM', $host, 1); $valkey->hSet('KEEP_SPAM', $host, 1);
} }
elseif ($redis->hGet('KEEP_SPAM', $host)) { elseif ($valkey->hGet('KEEP_SPAM', $host)) {
$redis->hDel('KEEP_SPAM', $host); $valkey->hDel('KEEP_SPAM', $host);
} }
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -86,17 +86,17 @@ function fwdhost($_action, $_data = null) {
} }
try { try {
if ($keep_spam == 1) { if ($keep_spam == 1) {
$redis->hSet('KEEP_SPAM', $fwdhost, 1); $valkey->hSet('KEEP_SPAM', $fwdhost, 1);
} }
else { else {
$redis->hDel('KEEP_SPAM', $fwdhost); $valkey->hDel('KEEP_SPAM', $fwdhost);
} }
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -111,14 +111,14 @@ function fwdhost($_action, $_data = null) {
$hosts = (array)$_data['forwardinghost']; $hosts = (array)$_data['forwardinghost'];
foreach ($hosts as $host) { foreach ($hosts as $host) {
try { try {
$redis->hDel('WHITELISTED_FWD_HOST', $host); $valkey->hDel('WHITELISTED_FWD_HOST', $host);
$redis->hDel('KEEP_SPAM', $host); $valkey->hDel('KEEP_SPAM', $host);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -135,10 +135,10 @@ function fwdhost($_action, $_data = null) {
} }
$fwdhostsdata = array(); $fwdhostsdata = array();
try { try {
$fwd_hosts = $redis->hGetAll('WHITELISTED_FWD_HOST'); $fwd_hosts = $valkey->hGetAll('WHITELISTED_FWD_HOST');
if (!empty($fwd_hosts)) { if (!empty($fwd_hosts)) {
foreach ($fwd_hosts as $fwd_host => $source) { foreach ($fwd_hosts as $fwd_host => $source) {
$keep_spam = ($redis->hGet('KEEP_SPAM', $fwd_host)) ? "yes" : "no"; $keep_spam = ($valkey->hGet('KEEP_SPAM', $fwd_host)) ? "yes" : "no";
$fwdhostsdata[] = array( $fwdhostsdata[] = array(
'host' => $fwd_host, 'host' => $fwd_host,
'source' => $source, 'source' => $source,
@@ -151,7 +151,7 @@ function fwdhost($_action, $_data = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -163,17 +163,17 @@ function fwdhost($_action, $_data = null) {
return false; return false;
} }
try { try {
if ($source = $redis->hGet('WHITELISTED_FWD_HOST', $_data)) { if ($source = $valkey->hGet('WHITELISTED_FWD_HOST', $_data)) {
$fwdhostdetails['host'] = $_data; $fwdhostdetails['host'] = $_data;
$fwdhostdetails['source'] = $source; $fwdhostdetails['source'] = $source;
$fwdhostdetails['keep_spam'] = ($redis->hGet('KEEP_SPAM', $_data)) ? "yes" : "no"; $fwdhostdetails['keep_spam'] = ($valkey->hGet('KEEP_SPAM', $_data)) ? "yes" : "no";
} }
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }

View File

@@ -126,7 +126,7 @@ function hash_password($password) {
return $pw_hash; return $pw_hash;
} }
function password_complexity($_action, $_data = null) { function password_complexity($_action, $_data = null) {
global $redis; global $valkey;
global $lang; global $lang;
switch ($_action) { switch ($_action) {
case 'edit': case 'edit':
@@ -147,7 +147,7 @@ function password_complexity($_action, $_data = null) {
$numbers = (isset($_data['numbers'])) ? intval($_data['numbers']) : $is_now['numbers']; $numbers = (isset($_data['numbers'])) ? intval($_data['numbers']) : $is_now['numbers'];
} }
try { try {
$redis->hMSet('PASSWD_POLICY', [ $valkey->hMSet('PASSWD_POLICY', [
'length' => $length, 'length' => $length,
'chars' => $chars, 'chars' => $chars,
'special_chars' => $special_chars, 'special_chars' => $special_chars,
@@ -159,7 +159,7 @@ function password_complexity($_action, $_data = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data), 'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -171,11 +171,11 @@ function password_complexity($_action, $_data = null) {
break; break;
case 'get': case 'get':
try { try {
$length = $redis->hGet('PASSWD_POLICY', 'length'); $length = $valkey->hGet('PASSWD_POLICY', 'length');
$chars = $redis->hGet('PASSWD_POLICY', 'chars'); $chars = $valkey->hGet('PASSWD_POLICY', 'chars');
$special_chars = $redis->hGet('PASSWD_POLICY', 'special_chars'); $special_chars = $valkey->hGet('PASSWD_POLICY', 'special_chars');
$lowerupper = $redis->hGet('PASSWD_POLICY', 'lowerupper'); $lowerupper = $valkey->hGet('PASSWD_POLICY', 'lowerupper');
$numbers = $redis->hGet('PASSWD_POLICY', 'numbers'); $numbers = $valkey->hGet('PASSWD_POLICY', 'numbers');
return array( return array(
'length' => $length, 'length' => $length,
'chars' => $chars, 'chars' => $chars,
@@ -188,7 +188,7 @@ function password_complexity($_action, $_data = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data), 'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -205,42 +205,6 @@ function password_complexity($_action, $_data = null) {
break; break;
} }
} }
function password_generate(){
$password_complexity = password_complexity('get');
$min_length = max(16, intval($password_complexity['length']));
$lowercase = range('a', 'z');
$uppercase = range('A', 'Z');
$digits = range(0, 9);
$special_chars = str_split('!@#$%^&*()?=');
$password = [
$lowercase[random_int(0, count($lowercase) - 1)],
$uppercase[random_int(0, count($uppercase) - 1)],
$digits[random_int(0, count($digits) - 1)],
$special_chars[random_int(0, count($special_chars) - 1)],
];
$all = array_merge($lowercase, $uppercase, $digits, $special_chars);
while (count($password) < $min_length) {
$password[] = $all[random_int(0, count($all) - 1)];
}
// Cryptographically secure shuffle using Fisher-Yates algorithm
$count = count($password);
for ($i = $count - 1; $i > 0; $i--) {
$j = random_int(0, $i);
$temp = $password[$i];
$password[$i] = $password[$j];
$password[$j] = $temp;
}
return implode('', $password);
}
function password_check($password1, $password2) { function password_check($password1, $password2) {
$password_complexity = password_complexity('get'); $password_complexity = password_complexity('get');
@@ -289,7 +253,7 @@ function password_check($password1, $password2) {
} }
function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) { function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {
global $pdo; global $pdo;
global $redis; global $valkey;
$sasl_limit_days = intval($sasl_limit_days); $sasl_limit_days = intval($sasl_limit_days);
switch ($action) { switch ($action) {
case 'get': case 'get':
@@ -308,13 +272,13 @@ function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {
} }
elseif (filter_var($sasl[$k]['real_rip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { elseif (filter_var($sasl[$k]['real_rip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
try { try {
$sasl[$k]['location'] = $redis->hGet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip']); $sasl[$k]['location'] = $valkey->hGet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip']);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -330,13 +294,13 @@ function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {
if ($ip_data_array !== false and !empty($ip_data_array['shortcountry'])) { if ($ip_data_array !== false and !empty($ip_data_array['shortcountry'])) {
$sasl[$k]['location'] = $ip_data_array['shortcountry']; $sasl[$k]['location'] = $ip_data_array['shortcountry'];
try { try {
$redis->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['shortcountry']); $valkey->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['shortcountry']);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
curl_close($curl); curl_close($curl);
return false; return false;
@@ -850,32 +814,6 @@ function verify_hash($hash, $password) {
$hash = $components[4]; $hash = $components[4];
return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash); return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash);
case "PBKDF2-SHA512":
// Handle FreeIPA-style hash: {PBKDF2-SHA512}10000$<base64_salt>$<base64_hash>
$components = explode('$', $hash);
if (count($components) !== 3) return false;
// 1st part: iteration count (integer)
$iterations = intval($components[0]);
if ($iterations <= 0) return false;
// 2nd part: salt (base64-encoded)
$salt = $components[1];
// 3rd part: hash (base64-encoded)
$stored_hash_b64 = $components[2];
// Decode salt and hash from base64
$salt_bin = base64_decode($salt, true);
$hash_bin = base64_decode($stored_hash_b64, true);
if ($salt_bin === false || $hash_bin === false) return false;
// Get length of hash in bytes
$hash_len = strlen($hash_bin);
if ($hash_len === 0) return false;
// Calculate PBKDF2-SHA512 hash for provided password
$test_hash = hash_pbkdf2('sha512', $password, $salt_bin, $iterations, $hash_len, true);
return hash_equals($hash_bin, $test_hash);
case "PLAIN-MD4": case "PLAIN-MD4":
return hash_equals(hash('md4', $password), $hash); return hash_equals(hash('md4', $password), $hash);
@@ -1033,24 +971,20 @@ function edit_user_account($_data) {
} }
// edit password // edit password
$is_forced_pw_update = !empty($_SESSION['pending_pw_update']); if (!empty($password_old) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
if (((!empty($password_old) || $is_forced_pw_update) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2']))) { $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
// Only verify old password if this is NOT a forced password update WHERE `kind` NOT REGEXP 'location|thing|group'
if (!$is_forced_pw_update) { AND `username` = :user AND authsource = 'mailcow'");
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox` $stmt->execute(array(':user' => $username));
WHERE `kind` NOT REGEXP 'location|thing|group' $row = $stmt->fetch(PDO::FETCH_ASSOC);
AND `username` = :user AND authsource = 'mailcow'");
$stmt->execute(array(':user' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!verify_hash($row['password'], $password_old)) { if (!verify_hash($row['password'], $password_old)) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log), 'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied' 'msg' => 'access_denied'
); );
return false; return false;
}
} }
$password_new = $_data['user_new_pass']; $password_new = $_data['user_new_pass'];
@@ -1072,7 +1006,7 @@ function edit_user_account($_data) {
update_sogo_static_view(); update_sogo_static_view();
} }
// edit password recovery email // edit password recovery email
elseif (!empty($password_old) && isset($pw_recovery_email)) { elseif (isset($pw_recovery_email)) {
if (!isset($_SESSION['acl']['pw_reset']) || $_SESSION['acl']['pw_reset'] != "1" ) { if (!isset($_SESSION['acl']['pw_reset']) || $_SESSION['acl']['pw_reset'] != "1" ) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@@ -1082,21 +1016,6 @@ function edit_user_account($_data) {
return false; return false;
} }
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
WHERE `kind` NOT REGEXP 'location|thing|group'
AND `username` = :user AND authsource = 'mailcow'");
$stmt->execute(array(':user' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!verify_hash($row['password'], $password_old)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$pw_recovery_email = (!filter_var($pw_recovery_email, FILTER_VALIDATE_EMAIL)) ? '' : $pw_recovery_email; $pw_recovery_email = (!filter_var($pw_recovery_email, FILTER_VALIDATE_EMAIL)) ? '' : $pw_recovery_email;
$stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email) $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email)
WHERE `username` = :username AND authsource = 'mailcow'"); WHERE `username` = :username AND authsource = 'mailcow'");
@@ -1214,52 +1133,50 @@ function set_tfa($_data) {
global $iam_settings; global $iam_settings;
$_data_log = $_data; $_data_log = $_data;
$access_denied = null;
!isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*'; !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
$username = $_SESSION['mailcow_cc_username'];
// skip password check if this is a forced TFA enrollment after login // check for empty user and role
if (!empty($_SESSION['pending_tfa_setup'])) { if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
$username = $_SESSION['mailcow_cc_username'];
if (empty($username) || !isset($_SESSION['mailcow_cc_role'])) { // check admin confirm password
$_SESSION['return'][] = array('type' => 'danger', 'log' => array(__FUNCTION__, $_data_log), 'msg' => 'access_denied'); if ($access_denied === null) {
return false; $stmt = $pdo->prepare("SELECT `password` FROM `admin`
WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
else $access_denied = false;
} }
} else { }
$username = $_SESSION['mailcow_cc_username'];
$access_denied = null;
if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true; // check mailbox confirm password
if ($access_denied === null) {
// check admin password $stmt = $pdo->prepare("SELECT `password`, `authsource` FROM `mailbox`
if ($access_denied === null) { WHERE `username` = :username");
$stmt = $pdo->prepare("SELECT `password` FROM `admin` WHERE `username` = :username"); $stmt->execute(array(':username' => $username));
$stmt->execute(array(':username' => $username)); $row = $stmt->fetch(PDO::FETCH_ASSOC);
$row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row) {
if ($row) { if ($row['authsource'] == 'ldap'){
if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true;
else $access_denied = false;
} else {
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true; if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
else $access_denied = false; else $access_denied = false;
} }
} }
}
// check mailbox password // set access_denied error
if ($access_denied === null) { if ($access_denied){
$stmt = $pdo->prepare("SELECT `password`, `authsource` FROM `mailbox` WHERE `username` = :username"); $_SESSION['return'][] = array(
$stmt->execute(array(':username' => $username)); 'type' => 'danger',
$row = $stmt->fetch(PDO::FETCH_ASSOC); 'log' => array(__FUNCTION__, $_data_log),
if ($row) { 'msg' => 'access_denied'
if ($row['authsource'] == 'ldap'){ );
if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true; return false;
else $access_denied = false;
} else {
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
else $access_denied = false;
}
}
}
if ($access_denied) {
$_SESSION['return'][] = array('type' => 'danger', 'log' => array(__FUNCTION__, $_data_log), 'msg' => 'access_denied');
return false;
}
} }
switch ($_data["tfa_method"]) { switch ($_data["tfa_method"]) {
@@ -1312,7 +1229,6 @@ function set_tfa($_data) {
); );
return false; return false;
} }
unset($_SESSION['pending_tfa_setup']);
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_data_log), 'log' => array(__FUNCTION__, $_data_log),
@@ -1326,7 +1242,6 @@ function set_tfa($_data) {
//$stmt->execute(array(':username' => $username)); //$stmt->execute(array(':username' => $username));
$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')"); $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
$stmt->execute(array($username, $key_id, $_POST['totp_secret'])); $stmt->execute(array($username, $key_id, $_POST['totp_secret']));
unset($_SESSION['pending_tfa_setup']);
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_data_log), 'log' => array(__FUNCTION__, $_data_log),
@@ -1355,7 +1270,6 @@ function set_tfa($_data) {
0 0
)); ));
unset($_SESSION['pending_tfa_setup']);
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_data_log), 'log' => array(__FUNCTION__, $_data_log),
@@ -1363,25 +1277,6 @@ function set_tfa($_data) {
); );
break; break;
case "none": case "none":
// Block TFA removal if force_tfa policy is active
$is_forced_tfa = false;
if ($_SESSION['mailcow_cc_role'] === 'user') {
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
$stmt_check->execute(array($username));
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
} else {
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
$stmt_check->execute(array($username));
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
}
if ($is_forced_tfa) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'tfa_removal_blocked'
);
return false;
}
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username"); $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
$stmt->execute(array(':username' => $username)); $stmt->execute(array(':username' => $username));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -1634,26 +1529,6 @@ function unset_tfa_key($_data) {
return false; return false;
} }
// Block key removal if force_tfa policy is active
$is_forced_tfa = false;
if ($_SESSION['mailcow_cc_role'] === 'user') {
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
$stmt_check->execute(array($username));
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
} else {
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
$stmt_check->execute(array($username));
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
}
if ($is_forced_tfa) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'tfa_removal_blocked'
);
return false;
}
// check if it's last key // check if it's last key
$stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa` $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
WHERE `username` = :username AND `active` = '1'"); WHERE `username` = :username AND `active` = '1'");
@@ -1686,15 +1561,6 @@ function unset_tfa_key($_data) {
return false; return false;
} }
} }
function tfa_exists($username) {
global $pdo;
if (empty($username)) {
return false;
}
$stmt = $pdo->prepare("SELECT COUNT(*) as count FROM `tfa` WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
return $stmt->fetch(PDO::FETCH_ASSOC)['count'] > 0;
}
function get_tfa($username = null, $id = null) { function get_tfa($username = null, $id = null) {
global $pdo; global $pdo;
if (empty($username) && isset($_SESSION['mailcow_cc_username'])) { if (empty($username) && isset($_SESSION['mailcow_cc_username'])) {
@@ -2133,7 +1999,7 @@ function admin_api($access, $action, $data = null) {
} }
function license($action, $data = null) { function license($action, $data = null) {
global $pdo; global $pdo;
global $redis; global $valkey;
global $lang; global $lang;
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -2183,13 +2049,13 @@ function license($action, $data = null) {
} }
try { try {
// json_encode needs "true"/"false" instead of true/false, to not encode it to 0 or 1 // json_encode needs "true"/"false" instead of true/false, to not encode it to 0 or 1
$redis->Set('LICENSE_STATUS_CACHE', json_encode($_SESSION['gal'])); $valkey->Set('LICENSE_STATUS_CACHE', json_encode($_SESSION['gal']));
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -2270,7 +2136,7 @@ function rspamd_ui($action, $data = null) {
} }
} }
function cors($action, $data = null) { function cors($action, $data = null) {
global $redis; global $valkey;
switch ($action) { switch ($action) {
case "edit": case "edit":
@@ -2311,7 +2177,7 @@ function cors($action, $data = null) {
} }
try { try {
$redis->hMSet('CORS_SETTINGS', array( $valkey->hMSet('CORS_SETTINGS', array(
'allowed_origins' => implode(', ', $allowed_origins), 'allowed_origins' => implode(', ', $allowed_origins),
'allowed_methods' => implode(', ', $allowed_methods) 'allowed_methods' => implode(', ', $allowed_methods)
)); ));
@@ -2319,7 +2185,7 @@ function cors($action, $data = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $action, $data), 'log' => array(__FUNCTION__, $action, $data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -2333,12 +2199,12 @@ function cors($action, $data = null) {
break; break;
case "get": case "get":
try { try {
$cors_settings = $redis->hMGet('CORS_SETTINGS', array('allowed_origins', 'allowed_methods')); $cors_settings = $valkey->hMGet('CORS_SETTINGS', array('allowed_origins', 'allowed_methods'));
} catch (RedisException $e) { } catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $action, $data), 'log' => array(__FUNCTION__, $action, $data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
} }
@@ -3101,7 +2967,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
} }
function reset_password($action, $data = null) { function reset_password($action, $data = null) {
global $pdo; global $pdo;
global $redis; global $valkey;
global $mailcow_hostname; global $mailcow_hostname;
global $PW_RESET_TOKEN_LIMIT; global $PW_RESET_TOKEN_LIMIT;
global $PW_RESET_TOKEN_LIFETIME; global $PW_RESET_TOKEN_LIFETIME;
@@ -3337,10 +3203,10 @@ function reset_password($action, $data = null) {
$type = $data; $type = $data;
try { try {
$settings['from'] = $redis->Get('PW_RESET_FROM'); $settings['from'] = $valkey->Get('PW_RESET_FROM');
$settings['subject'] = $redis->Get('PW_RESET_SUBJ'); $settings['subject'] = $valkey->Get('PW_RESET_SUBJ');
$settings['html_tmpl'] = $redis->Get('PW_RESET_HTML'); $settings['html_tmpl'] = $valkey->Get('PW_RESET_HTML');
$settings['text_tmpl'] = $redis->Get('PW_RESET_TEXT'); $settings['text_tmpl'] = $valkey->Get('PW_RESET_TEXT');
if (empty($settings['html_tmpl']) && empty($settings['text_tmpl'])) { if (empty($settings['html_tmpl']) && empty($settings['text_tmpl'])) {
$settings['html_tmpl'] = file_get_contents("/tpls/pw_reset_html.tpl"); $settings['html_tmpl'] = file_get_contents("/tpls/pw_reset_html.tpl");
$settings['text_tmpl'] = file_get_contents("/tpls/pw_reset_text.tpl"); $settings['text_tmpl'] = file_get_contents("/tpls/pw_reset_text.tpl");
@@ -3355,7 +3221,7 @@ function reset_password($action, $data = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log), 'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -3458,16 +3324,16 @@ function reset_password($action, $data = null) {
$html = (empty($data['html_tmpl'])) ? "" : $data['html_tmpl']; $html = (empty($data['html_tmpl'])) ? "" : $data['html_tmpl'];
try { try {
$redis->Set('PW_RESET_FROM', $from); $valkey->Set('PW_RESET_FROM', $from);
$redis->Set('PW_RESET_SUBJ', $subject); $valkey->Set('PW_RESET_SUBJ', $subject);
$redis->Set('PW_RESET_HTML', $html); $valkey->Set('PW_RESET_HTML', $html);
$redis->Set('PW_RESET_TEXT', $text); $valkey->Set('PW_RESET_TEXT', $text);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log), 'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -3497,49 +3363,6 @@ function set_user_loggedin_session($user) {
unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_methods']); unset($_SESSION['pending_tfa_methods']);
} }
function protect_route($allowed_roles = ['admin', 'domainadmin', 'user'], $redirects = []) {
// Check if user is authenticated
if (!isset($_SESSION['mailcow_cc_role'])) {
if (isset($redirects['unauthenticated'])) {
header('Location: ' . $redirects['unauthenticated']);
} else {
header('Location: /');
}
exit();
}
// Check for pending actions (2FA setup, password update)
if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) {
$pending_redirect = '/';
if ($_SESSION['mailcow_cc_role'] === 'admin') {
$pending_redirect = '/admin';
} elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
$pending_redirect = '/domainadmin';
}
header('Location: ' . $pending_redirect);
exit();
}
// Check if user's role is in the allowed roles for the route
if (!in_array($_SESSION['mailcow_cc_role'], $allowed_roles)) {
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
header('Location: /admin/dashboard');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
header('Location: /domainadmin/mailbox');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
else {
header('Location: /');
exit();
}
}
}
function get_logs($application, $lines = false) { function get_logs($application, $lines = false) {
if ($lines === false) { if ($lines === false) {
$lines = $GLOBALS['LOG_LINES'] - 1; $lines = $GLOBALS['LOG_LINES'] - 1;
@@ -3553,7 +3376,7 @@ function get_logs($application, $lines = false) {
$to = intval($to); $to = intval($to);
if ($from < 1 || $to < $from) { return false; } if ($from < 1 || $to < $from) { return false; }
} }
global $redis; global $valkey;
global $pdo; global $pdo;
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
return false; return false;
@@ -3602,10 +3425,10 @@ function get_logs($application, $lines = false) {
// Redis // Redis
if ($application == "dovecot-mailcow") { if ($application == "dovecot-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('DOVECOT_MAILLOG', $from - 1, $to - 1); $data = $valkey->lRange('DOVECOT_MAILLOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('DOVECOT_MAILLOG', 0, $lines); $data = $valkey->lRange('DOVECOT_MAILLOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3616,10 +3439,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "cron-mailcow") { if ($application == "cron-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('CRON_LOG', $from - 1, $to - 1); $data = $valkey->lRange('CRON_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('CRON_LOG', 0, $lines); $data = $valkey->lRange('CRON_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3630,10 +3453,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "postfix-mailcow") { if ($application == "postfix-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('POSTFIX_MAILLOG', $from - 1, $to - 1); $data = $valkey->lRange('POSTFIX_MAILLOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('POSTFIX_MAILLOG', 0, $lines); $data = $valkey->lRange('POSTFIX_MAILLOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3644,10 +3467,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "sogo-mailcow") { if ($application == "sogo-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('SOGO_LOG', $from - 1, $to - 1); $data = $valkey->lRange('SOGO_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('SOGO_LOG', 0, $lines); $data = $valkey->lRange('SOGO_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3658,10 +3481,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "watchdog-mailcow") { if ($application == "watchdog-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('WATCHDOG_LOG', $from - 1, $to - 1); $data = $valkey->lRange('WATCHDOG_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('WATCHDOG_LOG', 0, $lines); $data = $valkey->lRange('WATCHDOG_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3672,10 +3495,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "acme-mailcow") { if ($application == "acme-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('ACME_LOG', $from - 1, $to - 1); $data = $valkey->lRange('ACME_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('ACME_LOG', 0, $lines); $data = $valkey->lRange('ACME_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3686,10 +3509,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "ratelimited") { if ($application == "ratelimited") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('RL_LOG', $from - 1, $to - 1); $data = $valkey->lRange('RL_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('RL_LOG', 0, $lines); $data = $valkey->lRange('RL_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3700,10 +3523,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "api-mailcow") { if ($application == "api-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('API_LOG', $from - 1, $to - 1); $data = $valkey->lRange('API_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('API_LOG', 0, $lines); $data = $valkey->lRange('API_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3714,10 +3537,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "netfilter-mailcow") { if ($application == "netfilter-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('NETFILTER_LOG', $from - 1, $to - 1); $data = $valkey->lRange('NETFILTER_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('NETFILTER_LOG', 0, $lines); $data = $valkey->lRange('NETFILTER_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3728,10 +3551,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "autodiscover-mailcow") { if ($application == "autodiscover-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('AUTODISCOVER_LOG', $from - 1, $to - 1); $data = $valkey->lRange('AUTODISCOVER_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('AUTODISCOVER_LOG', 0, $lines); $data = $valkey->lRange('AUTODISCOVER_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {

View File

@@ -1,7 +1,7 @@
<?php <?php
function mailbox($_action, $_type, $_data = null, $_extra = null) { function mailbox($_action, $_type, $_data = null, $_extra = null) {
global $pdo; global $pdo;
global $redis; global $valkey;
global $lang; global $lang;
global $MAILBOX_DEFAULT_ATTRIBUTES; global $MAILBOX_DEFAULT_ATTRIBUTES;
global $iam_settings; global $iam_settings;
@@ -9,10 +9,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data_log = $_data; $_data_log = $_data;
!isset($_data_log['password']) ?: $_data_log['password'] = '*'; !isset($_data_log['password']) ?: $_data_log['password'] = '*';
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*'; !isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
// Track mailboxes affected by alias operations for incremental SOGo updates
$update_sogo_mailboxes = array();
switch ($_action) { switch ($_action) {
case 'add': case 'add':
switch ($_type) { switch ($_type) {
@@ -53,12 +49,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
// Default to 1 yr // Default to 1 yr
$_data["validity"] = 8760; $_data["validity"] = 8760;
} }
if (isset($_data["permanent"]) && filter_var($_data["permanent"], FILTER_VALIDATE_BOOL)) {
$permanent = 1;
}
else {
$permanent = 0;
}
$domain = $_data['domain']; $domain = $_data['domain'];
$description = $_data['description']; $description = $_data['description'];
$valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain']; $valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain'];
@@ -75,14 +65,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return false; return false;
} }
$validity = strtotime("+" . $_data["validity"] . " hour"); $validity = strtotime("+" . $_data["validity"] . " hour");
$stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`, `permanent`) VALUES $stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`) VALUES
(:address, :description, :goto, :validity, :permanent)"); (:address, :description, :goto, :validity)");
$stmt->execute(array( $stmt->execute(array(
':address' => readable_random_string(rand(rand(3, 9), rand(3, 9))) . '.' . readable_random_string(rand(rand(3, 9), rand(3, 9))) . '@' . $domain, ':address' => readable_random_string(rand(rand(3, 9), rand(3, 9))) . '.' . readable_random_string(rand(rand(3, 9), rand(3, 9))) . '@' . $domain,
':description' => $description, ':description' => $description,
':goto' => $username, ':goto' => $username,
':validity' => $validity, ':validity' => $validity
':permanent' => $permanent
)); ));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
@@ -639,13 +628,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
try { try {
$redis->hSet('DOMAIN_MAP', $domain, 1); $valkey->hSet('DOMAIN_MAP', $domain, 1);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -657,7 +646,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['key_size'] = (isset($_data['key_size'])) ? intval($_data['key_size']) : $DOMAIN_DEFAULT_ATTRIBUTES['key_size']; $_data['key_size'] = (isset($_data['key_size'])) ? intval($_data['key_size']) : $DOMAIN_DEFAULT_ATTRIBUTES['key_size'];
$_data['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : $DOMAIN_DEFAULT_ATTRIBUTES['dkim_selector']; $_data['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : $DOMAIN_DEFAULT_ATTRIBUTES['dkim_selector'];
if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) { if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) {
if (!empty($redis->hGet('DKIM_SELECTORS', $domain))) { if (!empty($valkey->hGet('DKIM_SELECTORS', $domain))) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -699,7 +688,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto'])); $gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto']));
$internal = intval($_data['internal']); $internal = intval($_data['internal']);
$active = intval($_data['active']); $active = intval($_data['active']);
$sender_allowed = intval($_data['sender_allowed']);
$sogo_visible = intval($_data['sogo_visible']); $sogo_visible = intval($_data['sogo_visible']);
$goto_null = intval($_data['goto_null']); $goto_null = intval($_data['goto_null']);
$goto_spam = intval($_data['goto_spam']); $goto_spam = intval($_data['goto_spam']);
@@ -855,8 +843,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
continue; continue;
} }
$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `sender_allowed`, `active`) $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `active`)
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :sender_allowed, :active)"); VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :active)");
if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) { if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
$stmt->execute(array( $stmt->execute(array(
':address' => '@'.$domain, ':address' => '@'.$domain,
@@ -867,7 +855,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':domain' => $domain, ':domain' => $domain,
':sogo_visible' => $sogo_visible, ':sogo_visible' => $sogo_visible,
':internal' => $internal, ':internal' => $internal,
':sender_allowed' => $sender_allowed,
':active' => $active ':active' => $active
)); ));
} }
@@ -880,7 +867,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':domain' => $domain, ':domain' => $domain,
':sogo_visible' => $sogo_visible, ':sogo_visible' => $sogo_visible,
':internal' => $internal, ':internal' => $internal,
':sender_allowed' => $sender_allowed,
':active' => $active ':active' => $active
)); ));
} }
@@ -890,17 +876,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('alias_added', $address, $id) 'msg' => array('alias_added', $address, $id)
); );
// Track affected mailboxes for SOGo update
if (!empty($goto)) {
$gotos = array_map('trim', explode(',', $goto));
foreach ($gotos as $g) {
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
$update_sogo_mailboxes[] = $g;
}
}
}
} }
break; break;
case 'alias_domain': case 'alias_domain':
@@ -999,13 +974,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':active' => $active ':active' => $active
)); ));
try { try {
$redis->hSet('DOMAIN_MAP', $alias_domain, 1); $valkey->hSet('DOMAIN_MAP', $alias_domain, 1);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -1013,7 +988,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
ratelimit('edit', 'domain', array('rl_value' => $_data['rl_value'], 'rl_frame' => $_data['rl_frame'], 'object' => $alias_domain)); ratelimit('edit', 'domain', array('rl_value' => $_data['rl_value'], 'rl_frame' => $_data['rl_frame'], 'object' => $alias_domain));
} }
if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) { if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) {
if (!empty($redis->hGet('DKIM_SELECTORS', $alias_domain))) { if (!empty($valkey->hGet('DKIM_SELECTORS', $alias_domain))) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1093,12 +1068,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$_data['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$_data['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
} }
$active = (isset($_data['active'])) ? intval($_data['active']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['active']); $active = (isset($_data['active'])) ? intval($_data['active']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['active']);
$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']); $force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
$force_tfa = (isset($_data['force_tfa'])) ? intval($_data['force_tfa']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_tfa']);
$tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']); $tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
$tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']); $tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
$sogo_access = (isset($_data['sogo_access'])) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']); $sogo_access = (isset($_data['sogo_access'])) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
@@ -1106,8 +1078,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$pop3_access = (isset($_data['pop3_access'])) ? intval($_data['pop3_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']); $pop3_access = (isset($_data['pop3_access'])) ? intval($_data['pop3_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
$smtp_access = (isset($_data['smtp_access'])) ? intval($_data['smtp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']); $smtp_access = (isset($_data['smtp_access'])) ? intval($_data['smtp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
$sieve_access = (isset($_data['sieve_access'])) ? intval($_data['sieve_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']); $sieve_access = (isset($_data['sieve_access'])) ? intval($_data['sieve_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
$eas_access = (isset($_data['eas_access'])) ? intval($_data['eas_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['eas_access']);
$dav_access = (isset($_data['dav_access'])) ? intval($_data['dav_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['dav_access']);
$relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : 0; $relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : 0;
$quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']); $quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
$quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']); $quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
@@ -1116,13 +1086,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){ if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
$force_pw_update = 0; $force_pw_update = 0;
} }
if ($authsource == 'generic-oidc'){
$force_tfa = 0;
}
$mailbox_attrs = json_encode( $mailbox_attrs = json_encode(
array( array(
'force_pw_update' => strval($force_pw_update), 'force_pw_update' => strval($force_pw_update),
'force_tfa' => strval($force_tfa),
'tls_enforce_in' => strval($tls_enforce_in), 'tls_enforce_in' => strval($tls_enforce_in),
'tls_enforce_out' => strval($tls_enforce_out), 'tls_enforce_out' => strval($tls_enforce_out),
'sogo_access' => strval($sogo_access), 'sogo_access' => strval($sogo_access),
@@ -1130,8 +1096,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'pop3_access' => strval($pop3_access), 'pop3_access' => strval($pop3_access),
'smtp_access' => strval($smtp_access), 'smtp_access' => strval($smtp_access),
'sieve_access' => strval($sieve_access), 'sieve_access' => strval($sieve_access),
'eas_access' => strval($eas_access),
'dav_access' => strval($dav_access),
'relayhost' => strval($relayhost), 'relayhost' => strval($relayhost),
'passwd_update' => time(), 'passwd_update' => time(),
'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']), 'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']),
@@ -1385,8 +1349,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
), $_extra); ), $_extra);
} }
// Track affected mailboxes for SOGo update try {
$update_sogo_mailboxes[] = $username; update_sogo_static_view($username);
} catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => $e->getMessage()
);
}
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1617,9 +1588,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('resource_added', htmlspecialchars($name)) 'msg' => array('resource_added', htmlspecialchars($name))
); );
// Track affected mailboxes for SOGo update
$update_sogo_mailboxes[] = $name;
break; break;
case 'domain_templates': case 'domain_templates':
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
@@ -1736,7 +1704,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s"; $attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : ""; $attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : "";
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']); $attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
$attr["force_tfa"] = isset($_data['force_tfa']) ? intval($_data['force_tfa']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_tfa']);
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']); $attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : 1; $attr["active"] = isset($_data['active']) ? intval($_data['active']) : 1;
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']); $attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
@@ -1747,16 +1714,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$attr['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$attr['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
} }
else { else {
$attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']); $attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
$attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']); $attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
$attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']); $attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
$attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']); $attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
$attr['eas_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['eas_access']);
$attr['dav_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['dav_access']);
} }
if (isset($_data['acl'])) { if (isset($_data['acl'])) {
$_data['acl'] = (array)$_data['acl']; $_data['acl'] = (array)$_data['acl'];
@@ -2140,23 +2103,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
continue; continue;
} }
if (empty($_data['validity']) && empty($_data['permanent'])) { if (empty($_data['validity'])) {
continue; continue;
} }
if (isset($_data['permanent']) && filter_var($_data['permanent'], FILTER_VALIDATE_BOOL)) { $validity = round((int)time() + ($_data['validity'] * 3600));
$permanent = 1; $stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity WHERE
$validity = 0;
}
else if (isset($_data['validity'])) {
$permanent = 0;
$validity = round((int)time() + ($_data['validity'] * 3600));
}
$stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity, `permanent` = :permanent WHERE
`address` = :address"); `address` = :address");
$stmt->execute(array( $stmt->execute(array(
':address' => $address, ':address' => $address,
':validity' => $validity, ':validity' => $validity
':permanent' => $permanent
)); ));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
@@ -2192,42 +2147,42 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
if (isset($_data['tagged_mail_handler']) && $_data['tagged_mail_handler'] == "subject") { if (isset($_data['tagged_mail_handler']) && $_data['tagged_mail_handler'] == "subject") {
try { try {
$redis->hSet('RCPT_WANTS_SUBJECT_TAG', $username, 1); $valkey->hSet('RCPT_WANTS_SUBJECT_TAG', $username, 1);
$redis->hDel('RCPT_WANTS_SUBFOLDER_TAG', $username); $valkey->hDel('RCPT_WANTS_SUBFOLDER_TAG', $username);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
} }
else if (isset($_data['tagged_mail_handler']) && $_data['tagged_mail_handler'] == "subfolder") { else if (isset($_data['tagged_mail_handler']) && $_data['tagged_mail_handler'] == "subfolder") {
try { try {
$redis->hSet('RCPT_WANTS_SUBFOLDER_TAG', $username, 1); $valkey->hSet('RCPT_WANTS_SUBFOLDER_TAG', $username, 1);
$redis->hDel('RCPT_WANTS_SUBJECT_TAG', $username); $valkey->hDel('RCPT_WANTS_SUBJECT_TAG', $username);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
} }
else { else {
try { try {
$redis->hDel('RCPT_WANTS_SUBJECT_TAG', $username); $valkey->hDel('RCPT_WANTS_SUBJECT_TAG', $username);
$redis->hDel('RCPT_WANTS_SUBFOLDER_TAG', $username); $valkey->hDel('RCPT_WANTS_SUBFOLDER_TAG', $username);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -2531,7 +2486,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
if (!empty($is_now)) { if (!empty($is_now)) {
$internal = (isset($_data['internal'])) ? intval($_data['internal']) : $is_now['internal']; $internal = (isset($_data['internal'])) ? intval($_data['internal']) : $is_now['internal'];
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$sender_allowed = (isset($_data['sender_allowed'])) ? intval($_data['sender_allowed']) : $is_now['sender_allowed'];
$sogo_visible = (isset($_data['sogo_visible'])) ? intval($_data['sogo_visible']) : $is_now['sogo_visible']; $sogo_visible = (isset($_data['sogo_visible'])) ? intval($_data['sogo_visible']) : $is_now['sogo_visible'];
$goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0; $goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0;
$goto_spam = (isset($_data['goto_spam'])) ? intval($_data['goto_spam']) : 0; $goto_spam = (isset($_data['goto_spam'])) ? intval($_data['goto_spam']) : 0;
@@ -2717,7 +2671,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`goto` = :goto, `goto` = :goto,
`sogo_visible`= :sogo_visible, `sogo_visible`= :sogo_visible,
`internal`= :internal, `internal`= :internal,
`sender_allowed`= :sender_allowed,
`active`= :active `active`= :active
WHERE `id` = :id"); WHERE `id` = :id");
$stmt->execute(array( $stmt->execute(array(
@@ -2728,7 +2681,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':goto' => $goto, ':goto' => $goto,
':sogo_visible' => $sogo_visible, ':sogo_visible' => $sogo_visible,
':internal' => $internal, ':internal' => $internal,
':sender_allowed' => $sender_allowed,
':active' => $active, ':active' => $active,
':id' => $is_now['id'] ':id' => $is_now['id']
)); ));
@@ -2738,28 +2690,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('alias_modified', htmlspecialchars($address)) 'msg' => array('alias_modified', htmlspecialchars($address))
); );
// Track affected mailboxes for SOGo update (both old and new goto addresses)
// Old goto: to remove alias from their view
if (!empty($is_now['goto'])) {
$old_gotos = array_map('trim', explode(',', $is_now['goto']));
foreach ($old_gotos as $g) {
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
$update_sogo_mailboxes[] = $g;
}
}
}
// New goto: to add alias to their view
if (!empty($goto)) {
$new_gotos = array_map('trim', explode(',', $goto));
foreach ($new_gotos as $g) {
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
$update_sogo_mailboxes[] = $g;
}
}
}
} }
break; break;
case 'domain': case 'domain':
@@ -3098,20 +3028,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$_data['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$_data['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
} }
if (!empty($is_now)) { if (!empty($is_now)) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
(int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']); (int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
(int)$force_tfa = (isset($_data['force_tfa'])) ? intval($_data['force_tfa']) : intval($is_now['attributes']['force_tfa']);
(int)$sogo_access = (isset($_data['sogo_access']) && hasACLAccess("sogo_access")) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']); (int)$sogo_access = (isset($_data['sogo_access']) && hasACLAccess("sogo_access")) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
(int)$imap_access = (isset($_data['imap_access']) && hasACLAccess("protocol_access")) ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']); (int)$imap_access = (isset($_data['imap_access']) && hasACLAccess("protocol_access")) ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
(int)$pop3_access = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']); (int)$pop3_access = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
(int)$smtp_access = (isset($_data['smtp_access']) && hasACLAccess("protocol_access")) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']); (int)$smtp_access = (isset($_data['smtp_access']) && hasACLAccess("protocol_access")) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
(int)$sieve_access = (isset($_data['sieve_access']) && hasACLAccess("protocol_access")) ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']); (int)$sieve_access = (isset($_data['sieve_access']) && hasACLAccess("protocol_access")) ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
(int)$eas_access = (isset($_data['eas_access']) && hasACLAccess("protocol_access")) ? intval($_data['eas_access']) : intval($is_now['attributes']['eas_access']);
(int)$dav_access = (isset($_data['dav_access']) && hasACLAccess("protocol_access")) ? intval($_data['dav_access']) : intval($is_now['attributes']['dav_access']);
(int)$relayhost = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']); (int)$relayhost = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
(int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576); (int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
$name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name']; $name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
@@ -3129,9 +3054,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){ if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
$force_pw_update = 0; $force_pw_update = 0;
} }
if ($authsource == 'generic-oidc'){
$force_tfa = 0;
}
$pw_recovery_email = (isset($_data['pw_recovery_email']) && $authsource == 'mailcow') ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email']; $pw_recovery_email = (isset($_data['pw_recovery_email']) && $authsource == 'mailcow') ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
} }
else { else {
@@ -3248,10 +3170,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
if (isset($_data['sender_acl'])) { if (isset($_data['sender_acl'])) {
// Get sender_acl items set by admin // Get sender_acl items set by admin
$current_sender_acls = mailbox('get', 'sender_acl_handles', $username);
$sender_acl_admin = array_merge( $sender_acl_admin = array_merge(
$current_sender_acls['sender_acl_domains']['ro'], mailbox('get', 'sender_acl_handles', $username)['sender_acl_domains']['ro'],
$current_sender_acls['sender_acl_addresses']['ro'] mailbox('get', 'sender_acl_handles', $username)['sender_acl_addresses']['ro']
); );
// Get sender_acl items from POST array // Get sender_acl items from POST array
// Set sender_acl_domain_admin to empty array if sender_acl contains "default" to trigger a reset // Set sender_acl_domain_admin to empty array if sender_acl contains "default" to trigger a reset
@@ -3339,25 +3260,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array( $stmt->execute(array(
':username' => $username ':username' => $username
)); ));
$sender_acl_handles = mailbox('get', 'sender_acl_handles', $username); $fixed_sender_aliases = mailbox('get', 'sender_acl_handles', $username)['fixed_sender_aliases'];
$fixed_sender_aliases_allowed = $sender_acl_handles['fixed_sender_aliases_allowed'];
$fixed_sender_aliases_blocked = $sender_acl_handles['fixed_sender_aliases_blocked'];
foreach ($sender_acl_merged as $sender_acl) { foreach ($sender_acl_merged as $sender_acl) {
$domain = ltrim($sender_acl, '@'); $domain = ltrim($sender_acl, '@');
if (is_valid_domain_name($domain)) { if (is_valid_domain_name($domain)) {
$sender_acl = '@' . $domain; $sender_acl = '@' . $domain;
} }
// Don't add if allowed by alias
// Always add to sender_acl table to create explicit permission if (in_array($sender_acl, $fixed_sender_aliases)) {
// Skip only if it's in allowed list (would be redundant)
// But DO add if it's in blocked list (creates override)
if (in_array($sender_acl, $fixed_sender_aliases_allowed)) {
// Skip: already allowed by sender_allowed=1, no need for sender_acl entry
continue; continue;
} }
// Add to sender_acl (either override for blocked aliases, or grant for selectable ones)
$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`) $stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`)
VALUES (:sender_acl, :username)"); VALUES (:sender_acl, :username)");
$stmt->execute(array( $stmt->execute(array(
@@ -3402,15 +3314,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`quota` = :quota_b, `quota` = :quota_b,
`authsource` = :authsource, `authsource` = :authsource,
`attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update), `attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update),
`attributes` = JSON_SET(`attributes`, '$.force_tfa', :force_tfa),
`attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access), `attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
`attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access), `attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
`attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access), `attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access),
`attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access), `attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
`attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost), `attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access), `attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
`attributes` = JSON_SET(`attributes`, '$.eas_access', :eas_access),
`attributes` = JSON_SET(`attributes`, '$.dav_access', :dav_access),
`attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email), `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email),
`attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash) `attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash)
WHERE `username` = :username"); WHERE `username` = :username");
@@ -3420,14 +3329,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':quota_b' => $quota_b, ':quota_b' => $quota_b,
':attribute_hash' => $attribute_hash, ':attribute_hash' => $attribute_hash,
':force_pw_update' => $force_pw_update, ':force_pw_update' => $force_pw_update,
':force_tfa' => $force_tfa,
':sogo_access' => $sogo_access, ':sogo_access' => $sogo_access,
':imap_access' => $imap_access, ':imap_access' => $imap_access,
':pop3_access' => $pop3_access, ':pop3_access' => $pop3_access,
':sieve_access' => $sieve_access, ':sieve_access' => $sieve_access,
':smtp_access' => $smtp_access, ':smtp_access' => $smtp_access,
':eas_access' => $eas_access,
':dav_access' => $dav_access,
':recovery_email' => $pw_recovery_email, ':recovery_email' => $pw_recovery_email,
':relayhost' => $relayhost, ':relayhost' => $relayhost,
':username' => $username, ':username' => $username,
@@ -3476,8 +3382,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'msg' => array('mailbox_modified', $username) 'msg' => array('mailbox_modified', $username)
); );
// Track affected mailboxes for SOGo update try {
$update_sogo_mailboxes[] = $username; update_sogo_static_view($username);
} catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => $e->getMessage()
);
}
} }
return true; return true;
break; break;
@@ -3803,8 +3716,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$attr['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$attr['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
} }
else { else {
foreach ($is_now as $key => $value){ foreach ($is_now as $key => $value){
@@ -4106,9 +4017,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('resource_modified', htmlspecialchars($name)) 'msg' => array('resource_modified', htmlspecialchars($name))
); );
// Track affected mailboxes for SOGo update
$update_sogo_mailboxes[] = $name;
} }
break; break;
case 'domain_wide_footer': case 'domain_wide_footer':
@@ -4237,22 +4145,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$data['sender_acl_addresses']['rw'] = array(); $data['sender_acl_addresses']['rw'] = array();
$data['sender_acl_addresses']['selectable'] = array(); $data['sender_acl_addresses']['selectable'] = array();
$data['fixed_sender_aliases'] = array(); $data['fixed_sender_aliases'] = array();
$data['fixed_sender_aliases_allowed'] = array();
$data['fixed_sender_aliases_blocked'] = array();
$data['external_sender_aliases'] = array(); $data['external_sender_aliases'] = array();
// Fixed addresses - split by sender_allowed status // Fixed addresses
$stmt = $pdo->prepare("SELECT `address`, `sender_allowed` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'"); $stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
$stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)')); $stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)'));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) { while ($row = array_shift($rows)) {
// Keep old array for backward compatibility
$data['fixed_sender_aliases'][] = $row['address']; $data['fixed_sender_aliases'][] = $row['address'];
// Split into allowed/blocked for proper display
if ($row['sender_allowed'] == '1') {
$data['fixed_sender_aliases_allowed'][] = $row['address'];
} else {
$data['fixed_sender_aliases_blocked'][] = $row['address'];
}
} }
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias_domain_alias` FROM `mailbox`, `alias_domain` $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias_domain_alias` FROM `mailbox`, `alias_domain`
WHERE `alias_domain`.`target_domain` = `mailbox`.`domain` WHERE `alias_domain`.`target_domain` = `mailbox`.`domain`
@@ -4685,12 +4584,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`description`, `description`,
`validity`, `validity`,
`created`, `created`,
`modified`, `modified`
`permanent`
FROM `spamalias` FROM `spamalias`
WHERE `goto` = :username WHERE `goto` = :username
AND (`validity` >= :unixnow AND `validity` >= :unixnow");
OR `permanent` != 0)");
$stmt->execute(array(':username' => $_data, ':unixnow' => time())); $stmt->execute(array(':username' => $_data, ':unixnow' => time()));
$tladata = $stmt->fetchAll(PDO::FETCH_ASSOC); $tladata = $stmt->fetchAll(PDO::FETCH_ASSOC);
return $tladata; return $tladata;
@@ -4706,10 +4603,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data = $_SESSION['mailcow_cc_username']; $_data = $_SESSION['mailcow_cc_username'];
} }
try { try {
if ($redis->hGet('RCPT_WANTS_SUBJECT_TAG', $_data)) { if ($valkey->hGet('RCPT_WANTS_SUBJECT_TAG', $_data)) {
return "subject"; return "subject";
} }
elseif ($redis->hGet('RCPT_WANTS_SUBFOLDER_TAG', $_data)) { elseif ($valkey->hGet('RCPT_WANTS_SUBFOLDER_TAG', $_data)) {
return "subfolder"; return "subfolder";
} }
else { else {
@@ -4720,7 +4617,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -4812,7 +4709,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`internal`, `internal`,
`active`, `active`,
`sogo_visible`, `sogo_visible`,
`sender_allowed`,
`created`, `created`,
`modified` `modified`
FROM `alias` FROM `alias`
@@ -4846,7 +4742,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$aliasdata['active_int'] = $row['active']; $aliasdata['active_int'] = $row['active'];
$aliasdata['sogo_visible'] = $row['sogo_visible']; $aliasdata['sogo_visible'] = $row['sogo_visible'];
$aliasdata['sogo_visible_int'] = $row['sogo_visible']; $aliasdata['sogo_visible_int'] = $row['sogo_visible'];
$aliasdata['sender_allowed'] = $row['sender_allowed'];
$aliasdata['created'] = $row['created']; $aliasdata['created'] = $row['created'];
$aliasdata['modified'] = $row['modified']; $aliasdata['modified'] = $row['modified'];
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) { if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) {
@@ -5267,7 +5162,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt = $pdo->prepare("SELECT COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE (`kind` = '' OR `kind` = NULL) AND `domain` = :domain AND `username` != :username"); $stmt = $pdo->prepare("SELECT COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE (`kind` = '' OR `kind` = NULL) AND `domain` = :domain AND `username` != :username");
$stmt->execute(array(':domain' => $row['domain'], ':username' => $_data)); $stmt->execute(array(':domain' => $row['domain'], ':username' => $_data));
$MailboxUsage = $stmt->fetch(PDO::FETCH_ASSOC); $MailboxUsage = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND (`validity` >= :unixnow OR `permanent` != 0)"); $stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND `validity` >= :unixnow");
$stmt->execute(array(':address' => $_data, ':unixnow' => time())); $stmt->execute(array(':address' => $_data, ':unixnow' => time()));
$SpamaliasUsage = $stmt->fetch(PDO::FETCH_ASSOC); $SpamaliasUsage = $stmt->fetch(PDO::FETCH_ASSOC);
$mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use']; $mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use'];
@@ -5741,14 +5636,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt = $pdo->query("DELETE FROM `admin` WHERE `superadmin` = 0 AND `username` NOT IN (SELECT `username`FROM `domain_admins`);"); $stmt = $pdo->query("DELETE FROM `admin` WHERE `superadmin` = 0 AND `username` NOT IN (SELECT `username`FROM `domain_admins`);");
$stmt = $pdo->query("DELETE FROM `da_acl` WHERE `username` NOT IN (SELECT `username`FROM `domain_admins`);"); $stmt = $pdo->query("DELETE FROM `da_acl` WHERE `username` NOT IN (SELECT `username`FROM `domain_admins`);");
try { try {
$redis->hDel('DOMAIN_MAP', $domain); $valkey->hDel('DOMAIN_MAP', $domain);
$redis->hDel('RL_VALUE', $domain); $valkey->hDel('RL_VALUE', $domain);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -5813,18 +5708,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
continue; continue;
} }
// Track affected mailboxes for SOGo update (capture before deletion)
if (!empty($alias_data['goto'])) {
$gotos = array_map('trim', explode(',', $alias_data['goto']));
foreach ($gotos as $g) {
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
$update_sogo_mailboxes[] = $g;
}
}
}
$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `id` = :id"); $stmt = $pdo->prepare("DELETE FROM `alias` WHERE `id` = :id");
$stmt->execute(array( $stmt->execute(array(
':id' => $alias_data['id'] ':id' => $alias_data['id']
@@ -5886,14 +5769,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':alias_domain' => $alias_domain, ':alias_domain' => $alias_domain,
)); ));
try { try {
$redis->hDel('DOMAIN_MAP', $alias_domain); $valkey->hDel('DOMAIN_MAP', $alias_domain);
$redis->hDel('RL_VALUE', $domain); $valkey->hDel('RL_VALUE', $domain);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -6072,25 +5955,31 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
)); ));
} }
try { try {
$redis->hDel('RL_VALUE', $username); $valkey->hDel('RL_VALUE', $username);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
try {
update_sogo_static_view($username);
}catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => $e->getMessage()
);
}
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_removed', htmlspecialchars($username)) 'msg' => array('mailbox_removed', htmlspecialchars($username))
); );
// Track affected mailboxes for SOGo update
$update_sogo_mailboxes[] = $username;
} }
return true; return true;
break; break;
@@ -6192,9 +6081,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('resource_removed', htmlspecialchars($name)) 'msg' => array('resource_removed', htmlspecialchars($name))
); );
// Track affected mailboxes for SOGo update
$update_sogo_mailboxes[] = $name;
} }
break; break;
case 'tags_domain': case 'tags_domain':
@@ -6301,21 +6187,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
break; break;
} }
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'resource', 'mailbox')) && getenv('SKIP_SOGO') != "y") { if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'resource')) && getenv('SKIP_SOGO') != "y") {
try { try {
if (($_type == 'alias' || $_type == 'resource' || $_type == 'mailbox') && !empty($update_sogo_mailboxes)) { update_sogo_static_view();
// INCREMENTAL UPDATE: Update only affected mailboxes/resources
$update_sogo_mailboxes = array_unique($update_sogo_mailboxes);
foreach ($update_sogo_mailboxes as $mailbox) {
update_sogo_static_view($mailbox);
}
}
else {
// FULL REBUILD: For domain and alias_domain operations or if no tracked mailboxes
// Domain operations affect all mailboxes
// Alias_domain operations affect entire target domain
update_sogo_static_view();
}
}catch (PDOException $e) { }catch (PDOException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',

View File

@@ -1,7 +1,7 @@
<?php <?php
function oauth2($_action, $_type, $_data = null) { function oauth2($_action, $_type, $_data = null) {
global $pdo; global $pdo;
global $redis; global $valkey;
global $lang; global $lang;
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(

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