mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2025-12-12 17:36:01 +00:00
Merge branch 'staging' into feat/valkey
This commit is contained in:
69
.github/ISSUE_TEMPLATE/Bug_report.yml
vendored
69
.github/ISSUE_TEMPLATE/Bug_report.yml
vendored
@@ -11,22 +11,35 @@ body:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: I've found a bug and checked that ...
|
||||
description: Prior to placing the issue, please check following:** *(fill out each checkbox with an `X` once done)*
|
||||
label: Checklist prior issue creation
|
||||
description: Prior to creating the issue...
|
||||
options:
|
||||
- label: ... I understand that not following the below instructions will result in immediate closure and/or deletion of my issue.
|
||||
- label: I understand that failure to follow below instructions may cause this issue to be closed.
|
||||
required: true
|
||||
- label: ... I have understood that this bug report is dedicated for bugs, and not for support-related inquiries.
|
||||
- label: I understand that vague, incomplete or inaccurate information may cause this issue to be closed.
|
||||
required: true
|
||||
- label: ... I have understood that answers are voluntary and community-driven, and not commercial support.
|
||||
- label: I understand that this form is intended solely for reporting software bugs and not for support-related inquiries.
|
||||
required: true
|
||||
- label: ... I have verified that my issue has not been already answered in the past. I also checked previous [issues](https://github.com/mailcow/mailcow-dockerized/issues).
|
||||
- label: I understand that all responses are voluntary and community-driven, and do not constitute commercial support.
|
||||
required: true
|
||||
- label: I confirm that I have reviewed previous [issues](https://github.com/mailcow/mailcow-dockerized/issues) to ensure this matter has not already been addressed.
|
||||
required: true
|
||||
- label: I confirm that my environment meets all [prerequisite requirements](https://docs.mailcow.email/getstarted/prerequisite-system/) as specified in the official documentation.
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
description: Please provide a brief description of the bug in 1-2 sentences. If applicable, add screenshots to help explain your problem. Very useful for bugs in mailcow UI.
|
||||
render: plain text
|
||||
description: Please provide a brief description of the bug. If applicable, add screenshots to help explain your problem. (Very useful for bugs in mailcow UI.)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Steps to reproduce:"
|
||||
description: "Please describe the steps to reproduce the bug. Screenshots can be added, if helpful."
|
||||
placeholder: |-
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -36,45 +49,36 @@ body:
|
||||
render: plain text
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Steps to reproduce:"
|
||||
description: "Please describe the steps to reproduce the bug. Screenshots can be added, if helpful."
|
||||
render: plain text
|
||||
placeholder: |-
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## System information
|
||||
### In this stage we would kindly ask you to attach general system information about your setup.
|
||||
In this stage we would kindly ask you to attach general system information about your setup.
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: "Which branch are you using?"
|
||||
description: "#### `git rev-parse --abbrev-ref HEAD`"
|
||||
description: "#### Run: `git rev-parse --abbrev-ref HEAD`"
|
||||
multiple: false
|
||||
options:
|
||||
- master
|
||||
- master (stable)
|
||||
- staging
|
||||
- nightly
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: "Which architecture are you using?"
|
||||
description: "#### `uname -m`"
|
||||
description: "#### Run: `uname -m`"
|
||||
multiple: false
|
||||
options:
|
||||
- x86
|
||||
- x86_64
|
||||
- ARM64 (aarch64)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Operating System:"
|
||||
description: "#### Run: `lsb_release -ds`"
|
||||
placeholder: "e.g. Ubuntu 22.04 LTS"
|
||||
validations:
|
||||
required: true
|
||||
@@ -93,43 +97,44 @@ body:
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Virtualization technology:"
|
||||
placeholder: "KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported**"
|
||||
description: "LXC and OpenVZ are not supported!"
|
||||
placeholder: "KVM, VMware ESXi, Xen, etc"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Docker version:"
|
||||
description: "#### `docker version`"
|
||||
description: "#### Run: `docker version`"
|
||||
placeholder: "20.10.21"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "docker-compose version or docker compose version:"
|
||||
description: "#### `docker-compose version` or `docker compose version`"
|
||||
description: "#### Run: `docker-compose version` or `docker compose version`"
|
||||
placeholder: "v2.12.2"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "mailcow version:"
|
||||
description: "#### ```git describe --tags `git rev-list --tags --max-count=1` ```"
|
||||
placeholder: "2022-08"
|
||||
description: "#### Run: ```git describe --tags `git rev-list --tags --max-count=1` ```"
|
||||
placeholder: "2022-08x"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Reverse proxy:"
|
||||
placeholder: "e.g. Nginx/Traefik"
|
||||
placeholder: "e.g. nginx/Traefik, or none"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Logs of git diff:"
|
||||
description: "#### Output of `git diff origin/master`, any other changes to the code? If so, **please post them**:"
|
||||
description: "#### Output of `git diff origin/master`, any other changes to the code? Sanitize if needed. If so, **please post them**:"
|
||||
render: plain text
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Logs of iptables -L -vn:"
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
||||
uses: actions/stale@v9.1.0
|
||||
uses: actions/stale@v10.1.0
|
||||
with:
|
||||
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
||||
days-before-stale: 60
|
||||
|
||||
2
.github/workflows/image_builds.yml
vendored
2
.github/workflows/image_builds.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
- "watchdog-mailcow"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Setup Docker
|
||||
run: |
|
||||
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
|
||||
|
||||
4
.github/workflows/pr_to_nightly.yml
vendored
4
.github/workflows/pr_to_nightly.yml
vendored
@@ -8,11 +8,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Run the Action
|
||||
uses: devops-infra/action-pull-request@v0.6.0
|
||||
uses: devops-infra/action-pull-request@v0.6.1
|
||||
with:
|
||||
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
|
||||
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}
|
||||
|
||||
14
.github/workflows/rebuild_backup_image.yml
vendored
14
.github/workflows/rebuild_backup_image.yml
vendored
@@ -9,9 +9,11 @@ on:
|
||||
jobs:
|
||||
docker_image_build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -19,11 +21,13 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
- name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }}
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -32,4 +36,4 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: data/Dockerfiles/backup/Dockerfile
|
||||
push: true
|
||||
tags: mailcow/backup:latest
|
||||
tags: ghcr.io/mailcow/backup:latest
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Generate postscreen_access.cidr
|
||||
run: |
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -45,6 +45,7 @@ data/conf/rspamd/local.d/*
|
||||
data/conf/rspamd/override.d/*
|
||||
data/conf/sogo/custom-theme.js
|
||||
data/conf/sogo/plist_ldap
|
||||
data/conf/sogo/plist_ldap.sh
|
||||
data/conf/sogo/sieve.creds
|
||||
data/conf/sogo/cron.creds
|
||||
data/conf/sogo/custom-fulllogo.svg
|
||||
@@ -73,3 +74,5 @@ rebuild-images.sh
|
||||
refresh_images.sh
|
||||
update_diffs/
|
||||
create_cold_standby.sh
|
||||
!data/conf/nginx/mailcow_auth.conf
|
||||
data/conf/postfix/postfix-tlspol
|
||||
16
README.md
16
README.md
@@ -13,6 +13,22 @@ You can also [get a SAL](https://www.servercow.de/mailcow?lang=en#sal) which is
|
||||
|
||||
Or just spread the word: moo.
|
||||
|
||||
## Many thanks to our GitHub Sponsors ❤️
|
||||
A big thank you to everyone supporting us on GitHub Sponsors—your contributions mean the world to us! Special thanks to the following amazing supporters:
|
||||
|
||||
### 100$/Month Sponsors
|
||||
<a href="https://www.colba.net/" target=_blank><img
|
||||
src="https://avatars.githubusercontent.com/u/204464723" height="58"
|
||||
/></a>
|
||||
<a href="https://www.maehdros.com/" target=_blank><img
|
||||
src="https://avatars.githubusercontent.com/u/173894712" height="58"
|
||||
/></a>
|
||||
|
||||
### 50$/Month Sponsors
|
||||
<a href="https://github.com/vnukhr" target=_blank><img
|
||||
src="https://avatars.githubusercontent.com/u/7805987?s=52&v=4" height="58"
|
||||
/></a>
|
||||
|
||||
## Info, documentation and support
|
||||
|
||||
Please see [the official documentation](https://docs.mailcow.email/) for installation and support instructions. 🐄
|
||||
|
||||
230
_modules/scripts/core.sh
Normal file
230
_modules/scripts/core.sh
Normal file
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env bash
|
||||
# _modules/scripts/core.sh
|
||||
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
|
||||
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
|
||||
|
||||
# ANSI color for red errors
|
||||
RED='\e[31m'
|
||||
GREEN='\e[32m'
|
||||
YELLOW='\e[33m'
|
||||
BLUE='\e[34m'
|
||||
MAGENTA='\e[35m'
|
||||
LIGHT_RED='\e[91m'
|
||||
LIGHT_GREEN='\e[92m'
|
||||
NC='\e[0m'
|
||||
|
||||
caller="${BASH_SOURCE[1]##*/}"
|
||||
|
||||
get_installed_tools(){
|
||||
for bin in openssl curl docker git awk sha1sum grep cut jq; do
|
||||
if [[ -z $(command -v ${bin}) ]]; then
|
||||
echo "Error: Cannot find command '${bin}'. Cannot proceed."
|
||||
echo "Solution: Please review system requirements and install requirements. Then, re-run the script."
|
||||
echo "See System Requirements: https://docs.mailcow.email/getstarted/install/"
|
||||
echo "Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo -e "${LIGHT_RED}BusyBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\"${NC}"; exit 1; fi
|
||||
# This will also cover sort
|
||||
if cp --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo -e "${LIGHT_RED}BusyBox cp detected, please install coreutils, \"apk add --no-cache --upgrade coreutils\"${NC}"; exit 1; fi
|
||||
if sed --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo -e "${LIGHT_RED}BusyBox sed detected, please install gnu sed, \"apk add --no-cache --upgrade sed\"${NC}"; exit 1; fi
|
||||
}
|
||||
|
||||
get_docker_version(){
|
||||
# Check Docker Version (need at least 24.X)
|
||||
docker_version=$(docker version --format '{{.Server.Version}}' | cut -d '.' -f 1)
|
||||
}
|
||||
|
||||
get_compose_type(){
|
||||
if docker compose > /dev/null 2>&1; then
|
||||
if docker compose version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then
|
||||
COMPOSE_VERSION=native
|
||||
COMPOSE_COMMAND="docker compose"
|
||||
if [[ "$caller" == "update.sh" ]]; then
|
||||
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf"
|
||||
fi
|
||||
echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m"
|
||||
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
|
||||
sleep 2
|
||||
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\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 it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
|
||||
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
|
||||
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() {
|
||||
echo -e "\e[33mDetecting if your IP is listed on Spamhaus Bad ASN List...\e[0m"
|
||||
response=$(curl --connect-timeout 15 --max-time 30 -s -o /dev/null -w "%{http_code}" "https://asn-check.mailcow.email")
|
||||
if [ "$response" -eq 503 ]; then
|
||||
if [ -z "$SPAMHAUS_DQS_KEY" ]; then
|
||||
echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m"
|
||||
echo -e "\e[33mmailcow did not detected a value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf!\e[0m"
|
||||
sleep 2
|
||||
echo ""
|
||||
echo -e "\e[33mTo use the Spamhaus DNS Blocklists again, you will need to create a FREE account for their Data Query Service (DQS) at: https://www.spamhaus.com/free-trial/sign-up-for-a-free-data-query-service-account\e[0m"
|
||||
echo -e "\e[33mOnce done, enter your DQS API key in mailcow.conf and mailcow will do the rest for you!\e[0m"
|
||||
echo ""
|
||||
sleep 2
|
||||
else
|
||||
echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m"
|
||||
echo -e "\e[32mmailcow detected a Value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf. Postfix will use DQS with the given API key...\e[0m"
|
||||
fi
|
||||
elif [ "$response" -eq 200 ]; then
|
||||
echo -e "\e[33mCheck completed! Your IP is \e[32mclean\e[0m"
|
||||
elif [ "$response" -eq 429 ]; then
|
||||
echo -e "\e[33mCheck completed! \e[31mYour IP seems to be rate limited on the ASN Check service... please try again later!\e[0m"
|
||||
else
|
||||
echo -e "\e[31mCheck failed! \e[0mMaybe a DNS or Network problem?\e[0m"
|
||||
fi
|
||||
}
|
||||
|
||||
check_online_status() {
|
||||
CHECK_ONLINE_DOMAINS=('https://github.com' 'https://hub.docker.com')
|
||||
for domain in "${CHECK_ONLINE_DOMAINS[@]}"; do
|
||||
if timeout 6 curl --head --silent --output /dev/null ${domain}; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
prefetch_images() {
|
||||
[[ -z ${BRANCH} ]] && { echo -e "\e[33m\nUnknown branch...\e[0m"; exit 1; }
|
||||
git fetch origin #${BRANCH}
|
||||
while read image; do
|
||||
RET_C=0
|
||||
until docker pull "${image}"; do
|
||||
RET_C=$((RET_C + 1))
|
||||
echo -e "\e[33m\nError pulling $image, retrying...\e[0m"
|
||||
[ ${RET_C} -gt 3 ] && { echo -e "\e[31m\nToo many failed retries, exiting\e[0m"; exit 1; }
|
||||
sleep 1
|
||||
done
|
||||
done < <(git show "origin/${BRANCH}:docker-compose.yml" | grep "image:" | awk '{ gsub("image:","", $3); print $2 }')
|
||||
}
|
||||
|
||||
docker_garbage() {
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )"
|
||||
IMGS_TO_DELETE=()
|
||||
|
||||
declare -A IMAGES_INFO
|
||||
COMPOSE_IMAGES=($(grep -oP "image: \K(ghcr\.io/)?mailcow.+" "${SCRIPT_DIR}/docker-compose.yml"))
|
||||
|
||||
for existing_image in $(docker images --format "{{.ID}}:{{.Repository}}:{{.Tag}}" | grep -E '(mailcow/|ghcr\.io/mailcow/)'); do
|
||||
ID=$(echo "$existing_image" | cut -d ':' -f 1)
|
||||
REPOSITORY=$(echo "$existing_image" | cut -d ':' -f 2)
|
||||
TAG=$(echo "$existing_image" | cut -d ':' -f 3)
|
||||
|
||||
if [[ "$REPOSITORY" == "mailcow/backup" || "$REPOSITORY" == "ghcr.io/mailcow/backup" ]]; then
|
||||
if [[ "$TAG" != "<none>" ]]; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ " ${COMPOSE_IMAGES[@]} " =~ " ${REPOSITORY}:${TAG} " ]]; then
|
||||
continue
|
||||
else
|
||||
IMGS_TO_DELETE+=("$ID")
|
||||
IMAGES_INFO["$ID"]="$REPOSITORY:$TAG"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ! -z ${IMGS_TO_DELETE[*]} ]]; then
|
||||
echo "The following unused mailcow images were found:"
|
||||
for id in "${IMGS_TO_DELETE[@]}"; do
|
||||
echo " ${IMAGES_INFO[$id]} ($id)"
|
||||
done
|
||||
|
||||
if [ -z "$FORCE" ]; then
|
||||
read -r -p "Do you want to delete them to free up some space? [y/N] " response
|
||||
if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
docker rmi ${IMGS_TO_DELETE[*]}
|
||||
else
|
||||
echo "OK, skipped."
|
||||
fi
|
||||
else
|
||||
echo "Running in forced mode! Force removing old mailcow images..."
|
||||
docker rmi ${IMGS_TO_DELETE[*]}
|
||||
fi
|
||||
echo -e "\e[32mFurther cleanup...\e[0m"
|
||||
echo "If you want to cleanup further garbage collected by Docker, please make sure all containers are up and running before cleaning your system by executing \"docker system prune\""
|
||||
fi
|
||||
}
|
||||
|
||||
in_array() {
|
||||
local e match="$1"
|
||||
shift
|
||||
for e; do [[ "$e" == "$match" ]] && return 0; done
|
||||
return 1
|
||||
}
|
||||
|
||||
detect_major_update() {
|
||||
if [ ${BRANCH} == "master" ]; then
|
||||
# Array with major versions
|
||||
# Add major versions here
|
||||
MAJOR_VERSIONS=(
|
||||
"2025-02"
|
||||
"2025-03"
|
||||
"2025-09"
|
||||
)
|
||||
|
||||
current_version=""
|
||||
if [[ -f "${SCRIPT_DIR}/data/web/inc/app_info.inc.php" ]]; then
|
||||
current_version=$(grep 'MAILCOW_GIT_VERSION' ${SCRIPT_DIR}/data/web/inc/app_info.inc.php | sed -E 's/.*MAILCOW_GIT_VERSION="([^"]+)".*/\1/')
|
||||
fi
|
||||
if [[ -z "$current_version" ]]; then
|
||||
return 1
|
||||
fi
|
||||
release_url="https://github.com/mailcow/mailcow-dockerized/releases/tag"
|
||||
|
||||
updates_to_apply=()
|
||||
|
||||
for version in "${MAJOR_VERSIONS[@]}"; do
|
||||
if [[ "$current_version" < "$version" ]]; then
|
||||
updates_to_apply+=("$version")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#updates_to_apply[@]} -gt 0 ]]; then
|
||||
echo -e "\e[33m\nMAJOR UPDATES to be applied:\e[0m"
|
||||
for update in "${updates_to_apply[@]}"; do
|
||||
echo "$update - $release_url/$update"
|
||||
done
|
||||
|
||||
echo -e "\nPlease read the release notes before proceeding."
|
||||
read -p "Do you want to proceed with the update? [y/n] " response
|
||||
if [[ "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo "Proceeding with the update..."
|
||||
else
|
||||
echo "Update canceled. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
239
_modules/scripts/ipv6_controller.sh
Normal file
239
_modules/scripts/ipv6_controller.sh
Normal file
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env bash
|
||||
# _modules/scripts/ipv6_controller.sh
|
||||
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
|
||||
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
|
||||
|
||||
# 1) Check if the host supports IPv6
|
||||
get_ipv6_support() {
|
||||
# ---- helper: probe external IPv6 connectivity without DNS ----
|
||||
_probe_ipv6_connectivity() {
|
||||
# Use literal, always-on IPv6 echo responders (no DNS required)
|
||||
local PROBE_IPS=("2001:4860:4860::8888" "2606:4700:4700::1111")
|
||||
local ip rc=1
|
||||
|
||||
for ip in "${PROBE_IPS[@]}"; do
|
||||
if command -v ping6 &>/dev/null; then
|
||||
ping6 -c1 -W2 "$ip" &>/dev/null || ping6 -c1 -w2 "$ip" &>/dev/null
|
||||
rc=$?
|
||||
elif command -v ping &>/dev/null; then
|
||||
ping -6 -c1 -W2 "$ip" &>/dev/null || ping -6 -c1 -w2 "$ip" &>/dev/null
|
||||
rc=$?
|
||||
else
|
||||
rc=1
|
||||
fi
|
||||
[[ $rc -eq 0 ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ ! -f /proc/net/if_inet6 ]] || grep -qs '^1' /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null; then
|
||||
DETECTED_IPV6=false
|
||||
echo -e "${YELLOW}IPv6 not detected on host – ${LIGHT_RED}IPv6 is administratively disabled${YELLOW}.${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
if ip -6 route show default 2>/dev/null | grep -qE '^default'; then
|
||||
echo -e "${YELLOW}Default IPv6 route found – testing external IPv6 connectivity...${NC}"
|
||||
if _probe_ipv6_connectivity; then
|
||||
DETECTED_IPV6=true
|
||||
echo -e "IPv6 detected on host – ${LIGHT_GREEN}leaving IPv6 support enabled${YELLOW}.${NC}"
|
||||
else
|
||||
DETECTED_IPV6=false
|
||||
echo -e "${YELLOW}Default IPv6 route present but external IPv6 connectivity failed – ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
if ip -6 addr show scope global 2>/dev/null | grep -q 'inet6'; then
|
||||
DETECTED_IPV6=false
|
||||
echo -e "${YELLOW}Global IPv6 address present but no default route – ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
|
||||
return
|
||||
fi
|
||||
|
||||
if ip -6 addr show scope link 2>/dev/null | grep -q 'inet6'; then
|
||||
echo -e "${YELLOW}Only link-local IPv6 addresses found – testing external IPv6 connectivity...${NC}"
|
||||
if _probe_ipv6_connectivity; then
|
||||
DETECTED_IPV6=true
|
||||
echo -e "External IPv6 connectivity available – ${LIGHT_GREEN}leaving IPv6 support enabled${YELLOW}.${NC}"
|
||||
else
|
||||
DETECTED_IPV6=false
|
||||
echo -e "${YELLOW}Only link-local IPv6 present and no external connectivity – ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
DETECTED_IPV6=false
|
||||
echo -e "${YELLOW}IPv6 not detected on host – ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
|
||||
}
|
||||
|
||||
# 2) Ensure Docker daemon.json has (or create) the required IPv6 settings
|
||||
docker_daemon_edit(){
|
||||
DOCKER_DAEMON_CONFIG="/etc/docker/daemon.json"
|
||||
DOCKER_MAJOR=$(docker version --format '{{.Server.Version}}' 2>/dev/null | cut -d. -f1)
|
||||
MISSING=()
|
||||
|
||||
_has_kv() { grep -Eq "\"$1\"[[:space:]]*:[[:space:]]*$2" "$DOCKER_DAEMON_CONFIG" 2>/dev/null; }
|
||||
|
||||
if [[ -f "$DOCKER_DAEMON_CONFIG" ]]; then
|
||||
|
||||
# reject empty or whitespace-only file immediately
|
||||
if [[ ! -s "$DOCKER_DAEMON_CONFIG" ]] || ! grep -Eq '[{}]' "$DOCKER_DAEMON_CONFIG"; then
|
||||
echo -e "${RED}ERROR: $DOCKER_DAEMON_CONFIG exists but is empty or contains no JSON braces – please initialize it with valid JSON (e.g. {}).${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate JSON if jq is present
|
||||
if command -v jq &>/dev/null && ! jq empty "$DOCKER_DAEMON_CONFIG" &>/dev/null; then
|
||||
echo -e "${RED}ERROR: Invalid JSON in $DOCKER_DAEMON_CONFIG – please correct manually.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Gather missing keys
|
||||
! _has_kv ipv6 true && MISSING+=("ipv6: true")
|
||||
|
||||
# For Docker < 28, keep requiring fixed-cidr-v6 (default bridge needs it on old engines)
|
||||
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 28 ]]; then
|
||||
! grep -Eq '"fixed-cidr-v6"[[:space:]]*:[[:space:]]*".+"' "$DOCKER_DAEMON_CONFIG" \
|
||||
&& MISSING+=('fixed-cidr-v6: "fd00:dead:beef:c0::/80"')
|
||||
fi
|
||||
|
||||
# For Docker < 27, ip6tables needed and was tied to experimental in older releases
|
||||
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then
|
||||
_has_kv ipv6 true && ! _has_kv ip6tables true && MISSING+=("ip6tables: true")
|
||||
! _has_kv experimental true && MISSING+=("experimental: true")
|
||||
fi
|
||||
|
||||
# Fix if needed
|
||||
if ((${#MISSING[@]}>0)); then
|
||||
echo -e "${MAGENTA}Your daemon.json is missing: ${YELLOW}${MISSING[*]}${NC}"
|
||||
if [[ -n "$FORCE" ]]; then
|
||||
ans=Y
|
||||
else
|
||||
read -p "Would you like to update $DOCKER_DAEMON_CONFIG now? [Y/n] " ans
|
||||
ans=${ans:-Y}
|
||||
fi
|
||||
|
||||
if [[ $ans =~ ^[Yy]$ ]]; then
|
||||
cp "$DOCKER_DAEMON_CONFIG" "${DOCKER_DAEMON_CONFIG}.bak"
|
||||
if command -v jq &>/dev/null; then
|
||||
TMP=$(mktemp)
|
||||
# Base filter: ensure ipv6 = true
|
||||
JQ_FILTER='.ipv6 = true'
|
||||
|
||||
# Add fixed-cidr-v6 only for Docker < 28
|
||||
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 28 ]]; then
|
||||
JQ_FILTER+=' | .["fixed-cidr-v6"] = (.["fixed-cidr-v6"] // "fd00:dead:beef:c0::/80")'
|
||||
fi
|
||||
|
||||
# Add ip6tables/experimental only for Docker < 27
|
||||
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then
|
||||
JQ_FILTER+=' | .ip6tables = true | .experimental = true'
|
||||
fi
|
||||
|
||||
jq "$JQ_FILTER" "$DOCKER_DAEMON_CONFIG" >"$TMP" && mv "$TMP" "$DOCKER_DAEMON_CONFIG"
|
||||
echo -e "${LIGHT_GREEN}daemon.json updated. Restarting Docker...${NC}"
|
||||
(command -v systemctl &>/dev/null && systemctl restart docker) || service docker restart
|
||||
echo -e "${YELLOW}Docker restarted.${NC}"
|
||||
else
|
||||
echo -e "${RED}Please install jq or manually update daemon.json and restart Docker.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}User declined Docker update – please insert these changes manually:${NC}"
|
||||
echo "${MISSING[*]}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
else
|
||||
# Create new daemon.json if missing
|
||||
if [[ -n "$FORCE" ]]; then
|
||||
ans=Y
|
||||
else
|
||||
read -p "$DOCKER_DAEMON_CONFIG not found. Create it with IPv6 settings? [Y/n] " ans
|
||||
ans=${ans:-Y}
|
||||
fi
|
||||
|
||||
if [[ $ans =~ ^[Yy]$ ]]; then
|
||||
mkdir -p "$(dirname "$DOCKER_DAEMON_CONFIG")"
|
||||
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then
|
||||
cat > "$DOCKER_DAEMON_CONFIG" <<EOF
|
||||
{
|
||||
"ipv6": true,
|
||||
"fixed-cidr-v6": "fd00:dead:beef:c0::/80",
|
||||
"ip6tables": true,
|
||||
"experimental": true
|
||||
}
|
||||
EOF
|
||||
elif [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 28 ]]; then
|
||||
cat > "$DOCKER_DAEMON_CONFIG" <<EOF
|
||||
{
|
||||
"ipv6": true,
|
||||
"fixed-cidr-v6": "fd00:dead:beef:c0::/80"
|
||||
}
|
||||
EOF
|
||||
else
|
||||
# Docker 28+: ipv6 works without fixed-cidr-v6
|
||||
cat > "$DOCKER_DAEMON_CONFIG" <<EOF
|
||||
{
|
||||
"ipv6": true
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
echo -e "${GREEN}Created $DOCKER_DAEMON_CONFIG with IPv6 settings.${NC}"
|
||||
echo "Restarting Docker..."
|
||||
(command -v systemctl &>/dev/null && systemctl restart docker) || service docker restart
|
||||
echo "Docker restarted."
|
||||
else
|
||||
echo "User declined to create daemon.json – please manually merge the docker daemon with these configs:"
|
||||
echo "${MISSING[*]}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 3) Main wrapper for generate_config.sh and update.sh
|
||||
configure_ipv6() {
|
||||
# detect manual override if mailcow.conf is present
|
||||
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]] && grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
|
||||
MANUAL_SETTING=$(grep '^ENABLE_IPV6=' "$MAILCOW_CONF" | cut -d= -f2)
|
||||
elif [[ -z "$MAILCOW_CONF" ]] && [[ -n "${ENABLE_IPV6:-}" ]]; then
|
||||
MANUAL_SETTING="$ENABLE_IPV6"
|
||||
else
|
||||
MANUAL_SETTING=""
|
||||
fi
|
||||
|
||||
get_ipv6_support
|
||||
|
||||
# if user manually set it, check for mismatch
|
||||
if [[ "$DETECTED_IPV6" != "true" ]]; then
|
||||
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]]; then
|
||||
if grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
|
||||
sed -i 's/^ENABLE_IPV6=.*/ENABLE_IPV6=false/' "$MAILCOW_CONF"
|
||||
else
|
||||
echo "ENABLE_IPV6=false" >> "$MAILCOW_CONF"
|
||||
fi
|
||||
else
|
||||
export IPV6_BOOL=false
|
||||
fi
|
||||
echo "Skipping Docker IPv6 configuration because host does not support IPv6."
|
||||
echo "Make sure to check if your docker daemon.json does not include \"enable_ipv6\": true if you do not want IPv6."
|
||||
echo "IPv6 configuration complete: ENABLE_IPV6=false"
|
||||
sleep 2
|
||||
return
|
||||
fi
|
||||
|
||||
docker_daemon_edit
|
||||
|
||||
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]]; then
|
||||
if grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
|
||||
sed -i 's/^ENABLE_IPV6=.*/ENABLE_IPV6=true/' "$MAILCOW_CONF"
|
||||
else
|
||||
echo "ENABLE_IPV6=true" >> "$MAILCOW_CONF"
|
||||
fi
|
||||
else
|
||||
export IPV6_BOOL=true
|
||||
fi
|
||||
|
||||
echo "IPv6 configuration complete: ENABLE_IPV6=true"
|
||||
}
|
||||
96
_modules/scripts/migrate_options.sh
Normal file
96
_modules/scripts/migrate_options.sh
Normal file
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
# _modules/scripts/migrate_options.sh
|
||||
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
|
||||
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
|
||||
|
||||
migrate_config_options() {
|
||||
|
||||
sed -i --follow-symlinks '$a\' mailcow.conf
|
||||
|
||||
KEYS=(
|
||||
SOLR_HEAP
|
||||
SKIP_SOLR
|
||||
SOLR_PORT
|
||||
FLATCURVE_EXPERIMENTAL
|
||||
DISABLE_IPv6
|
||||
ACME_CONTACT
|
||||
)
|
||||
|
||||
for key in "${KEYS[@]}"; do
|
||||
if grep -q "${key}" mailcow.conf; then
|
||||
case "${key}" in
|
||||
SOLR_HEAP)
|
||||
echo "Removing ${key} in mailcow.conf"
|
||||
sed -i '/# Solr heap size in MB\b/d' mailcow.conf
|
||||
sed -i '/# Solr is a prone to run\b/d' mailcow.conf
|
||||
sed -i '/SOLR_HEAP\b/d' mailcow.conf
|
||||
;;
|
||||
SKIP_SOLR)
|
||||
echo "Removing ${key} in mailcow.conf"
|
||||
sed -i '/\bSkip Solr on low-memory\b/d' mailcow.conf
|
||||
sed -i '/\bSolr is disabled by default\b/d' mailcow.conf
|
||||
sed -i '/\bDisable Solr or\b/d' mailcow.conf
|
||||
sed -i '/\bSKIP_SOLR\b/d' mailcow.conf
|
||||
;;
|
||||
SOLR_PORT)
|
||||
echo "Removing ${key} in mailcow.conf"
|
||||
sed -i '/\bSOLR_PORT\b/d' mailcow.conf
|
||||
;;
|
||||
FLATCURVE_EXPERIMENTAL)
|
||||
echo "Removing ${key} in mailcow.conf"
|
||||
sed -i '/\bFLATCURVE_EXPERIMENTAL\b/d' mailcow.conf
|
||||
;;
|
||||
DISABLE_IPv6)
|
||||
echo "Migrating ${key} to ENABLE_IPv6 in mailcow.conf"
|
||||
local old=$(grep '^DISABLE_IPv6=' "mailcow.conf" | cut -d'=' -f2)
|
||||
local new
|
||||
if [[ "$old" == "y" ]]; then
|
||||
new="false"
|
||||
else
|
||||
new="true"
|
||||
fi
|
||||
sed -i '/^DISABLE_IPv6=/d' "mailcow.conf"
|
||||
echo "ENABLE_IPV6=$new" >> "mailcow.conf"
|
||||
;;
|
||||
ACME_CONTACT)
|
||||
echo "Deleting obsoleted ${key} in mailcow.conf"
|
||||
sed -i '/^# Lets Encrypt registration contact information/d' mailcow.conf
|
||||
sed -i '/^# Optional: Leave empty for none/d' mailcow.conf
|
||||
sed -i '/^# This value is only used on first order!/d' mailcow.conf
|
||||
sed -i '/^# Setting it at a later point will require the following steps:/d' mailcow.conf
|
||||
sed -i '/^# https:\/\/docs.mailcow.email\/troubleshooting\/debug-reset_tls\//d' mailcow.conf
|
||||
sed -i '/^ACME_CONTACT=.*/d' mailcow.conf
|
||||
sed -i '/^#ACME_CONTACT=.*/d' mailcow.conf
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
|
||||
solr_volume=$(docker volume ls -qf name=^${COMPOSE_PROJECT_NAME}_solr-vol-1)
|
||||
if [[ -n $solr_volume ]]; then
|
||||
echo -e "\e[34mSolr has been replaced within mailcow since 2025-01.\nThe volume $solr_volume is unused.\e[0m"
|
||||
sleep 1
|
||||
if [ ! "$FORCE" ]; then
|
||||
read -r -p "Remove $solr_volume? [y/N] " response
|
||||
if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo -e "\e[33mRemoving $solr_volume...\e[0m"
|
||||
docker volume rm $solr_volume || echo -e "\e[31mFailed to remove. Remove it manually!\e[0m"
|
||||
echo -e "\e[32mSuccessfully removed $solr_volume!\e[0m"
|
||||
else
|
||||
echo -e "Not removing $solr_volume. Run \`docker volume rm $solr_volume\` manually if needed."
|
||||
fi
|
||||
else
|
||||
echo -e "\e[33mForce removing $solr_volume...\e[0m"
|
||||
docker volume rm $solr_volume || echo -e "\e[31mFailed to remove. Remove it manually!\e[0m"
|
||||
echo -e "\e[32mSuccessfully removed $solr_volume!\e[0m"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Delete old fts.conf before forced switch to flatcurve to ensure update is working properly
|
||||
FTS_CONF_PATH="${SCRIPT_DIR}/data/conf/dovecot/conf.d/fts.conf"
|
||||
if [[ -f "$FTS_CONF_PATH" ]]; then
|
||||
if grep -q "Autogenerated by mailcow" "$FTS_CONF_PATH"; then
|
||||
rm -rf $FTS_CONF_PATH
|
||||
fi
|
||||
fi
|
||||
}
|
||||
300
_modules/scripts/new_options.sh
Normal file
300
_modules/scripts/new_options.sh
Normal file
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env bash
|
||||
# _modules/scripts/new_options.sh
|
||||
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
|
||||
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
|
||||
|
||||
adapt_new_options() {
|
||||
|
||||
CONFIG_ARRAY=(
|
||||
"AUTODISCOVER_SAN"
|
||||
"SKIP_LETS_ENCRYPT"
|
||||
"SKIP_SOGO"
|
||||
"USE_WATCHDOG"
|
||||
"WATCHDOG_NOTIFY_EMAIL"
|
||||
"WATCHDOG_NOTIFY_WEBHOOK"
|
||||
"WATCHDOG_NOTIFY_WEBHOOK_BODY"
|
||||
"WATCHDOG_NOTIFY_BAN"
|
||||
"WATCHDOG_NOTIFY_START"
|
||||
"WATCHDOG_EXTERNAL_CHECKS"
|
||||
"WATCHDOG_SUBJECT"
|
||||
"SKIP_CLAMD"
|
||||
"SKIP_OLEFY"
|
||||
"SKIP_IP_CHECK"
|
||||
"ADDITIONAL_SAN"
|
||||
"DOVEADM_PORT"
|
||||
"IPV4_NETWORK"
|
||||
"IPV6_NETWORK"
|
||||
"LOG_LINES"
|
||||
"SNAT_TO_SOURCE"
|
||||
"SNAT6_TO_SOURCE"
|
||||
"COMPOSE_PROJECT_NAME"
|
||||
"DOCKER_COMPOSE_VERSION"
|
||||
"SQL_PORT"
|
||||
"API_KEY"
|
||||
"API_KEY_READ_ONLY"
|
||||
"API_ALLOW_FROM"
|
||||
"MAILDIR_GC_TIME"
|
||||
"MAILDIR_SUB"
|
||||
"ACL_ANYONE"
|
||||
"FTS_HEAP"
|
||||
"FTS_PROCS"
|
||||
"SKIP_FTS"
|
||||
"ENABLE_SSL_SNI"
|
||||
"ALLOW_ADMIN_EMAIL_LOGIN"
|
||||
"SKIP_HTTP_VERIFICATION"
|
||||
"SOGO_EXPIRE_SESSION"
|
||||
"SOGO_URL_ENCRYPTION_KEY"
|
||||
"REDIS_PORT"
|
||||
"REDISPASS"
|
||||
"DOVECOT_MASTER_USER"
|
||||
"DOVECOT_MASTER_PASS"
|
||||
"MAILCOW_PASS_SCHEME"
|
||||
"ADDITIONAL_SERVER_NAMES"
|
||||
"WATCHDOG_VERBOSE"
|
||||
"WEBAUTHN_ONLY_TRUSTED_VENDORS"
|
||||
"SPAMHAUS_DQS_KEY"
|
||||
"SKIP_UNBOUND_HEALTHCHECK"
|
||||
"DISABLE_NETFILTER_ISOLATION_RULE"
|
||||
"HTTP_REDIRECT"
|
||||
"ENABLE_IPV6"
|
||||
)
|
||||
|
||||
sed -i --follow-symlinks '$a\' mailcow.conf
|
||||
for option in ${CONFIG_ARRAY[@]}; do
|
||||
if grep -q "${option}" mailcow.conf; then
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Adding new option \"${option}\" to mailcow.conf"
|
||||
|
||||
case "${option}" in
|
||||
AUTODISCOVER_SAN)
|
||||
echo '# Obtain certificates for autodiscover.* and autoconfig.* domains.' >> mailcow.conf
|
||||
echo '# This can be useful to switch off in case you are in a scenario where a reverse proxy already handles those.' >> mailcow.conf
|
||||
echo '# There are mixed scenarios where ports 80,443 are occupied and you do not want to share certs' >> mailcow.conf
|
||||
echo '# between services. So acme-mailcow obtains for maildomains and all web-things get handled' >> mailcow.conf
|
||||
echo '# in the reverse proxy.' >> mailcow.conf
|
||||
echo 'AUTODISCOVER_SAN=y' >> mailcow.conf
|
||||
;;
|
||||
|
||||
DOCKER_COMPOSE_VERSION)
|
||||
echo "# Used Docker Compose version" >> mailcow.conf
|
||||
echo "# Switch here between native (compose plugin) and standalone" >> mailcow.conf
|
||||
echo "# For more informations take a look at the mailcow docs regarding the configuration options." >> mailcow.conf
|
||||
echo "# Normally this should be untouched but if you decided to use either of those you can switch it manually here." >> mailcow.conf
|
||||
echo "# Please be aware that at least one of those variants should be installed on your machine or mailcow will fail." >> mailcow.conf
|
||||
echo "" >> mailcow.conf
|
||||
echo "DOCKER_COMPOSE_VERSION=${DOCKER_COMPOSE_VERSION}" >> mailcow.conf
|
||||
;;
|
||||
|
||||
DOVEADM_PORT)
|
||||
echo "DOVEADM_PORT=127.0.0.1:19991" >> mailcow.conf
|
||||
;;
|
||||
|
||||
LOG_LINES)
|
||||
echo '# Max log lines per service to keep in Redis logs' >> mailcow.conf
|
||||
echo "LOG_LINES=9999" >> mailcow.conf
|
||||
;;
|
||||
IPV4_NETWORK)
|
||||
echo '# Internal IPv4 /24 subnet, format n.n.n. (expands to n.n.n.0/24)' >> mailcow.conf
|
||||
echo "IPV4_NETWORK=172.22.1" >> mailcow.conf
|
||||
;;
|
||||
IPV6_NETWORK)
|
||||
echo '# Internal IPv6 subnet in fc00::/7' >> mailcow.conf
|
||||
echo "IPV6_NETWORK=fd4d:6169:6c63:6f77::/64" >> mailcow.conf
|
||||
;;
|
||||
SQL_PORT)
|
||||
echo '# Bind SQL to 127.0.0.1 on port 13306' >> mailcow.conf
|
||||
echo "SQL_PORT=127.0.0.1:13306" >> mailcow.conf
|
||||
;;
|
||||
API_KEY)
|
||||
echo '# Create or override API key for web UI' >> mailcow.conf
|
||||
echo "#API_KEY=" >> mailcow.conf
|
||||
;;
|
||||
API_KEY_READ_ONLY)
|
||||
echo '# Create or override read-only API key for web UI' >> mailcow.conf
|
||||
echo "#API_KEY_READ_ONLY=" >> mailcow.conf
|
||||
;;
|
||||
API_ALLOW_FROM)
|
||||
echo '# Must be set for API_KEY to be active' >> mailcow.conf
|
||||
echo '# IPs only, no networks (networks can be set via UI)' >> mailcow.conf
|
||||
echo "#API_ALLOW_FROM=" >> mailcow.conf
|
||||
;;
|
||||
SNAT_TO_SOURCE)
|
||||
echo '# Use this IPv4 for outgoing connections (SNAT)' >> mailcow.conf
|
||||
echo "#SNAT_TO_SOURCE=" >> mailcow.conf
|
||||
;;
|
||||
SNAT6_TO_SOURCE)
|
||||
echo '# Use this IPv6 for outgoing connections (SNAT)' >> mailcow.conf
|
||||
echo "#SNAT6_TO_SOURCE=" >> mailcow.conf
|
||||
;;
|
||||
MAILDIR_GC_TIME)
|
||||
echo '# Garbage collector cleanup' >> mailcow.conf
|
||||
echo '# Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring' >> mailcow.conf
|
||||
echo '# How long should objects remain in the garbage until they are being deleted? (value in minutes)' >> mailcow.conf
|
||||
echo '# Check interval is hourly' >> mailcow.conf
|
||||
echo 'MAILDIR_GC_TIME=1440' >> mailcow.conf
|
||||
;;
|
||||
ACL_ANYONE)
|
||||
echo '# Set this to "allow" to enable the anyone pseudo user. Disabled by default.' >> mailcow.conf
|
||||
echo '# When enabled, ACL can be created, that apply to "All authenticated users"' >> mailcow.conf
|
||||
echo '# This should probably only be activated on mail hosts, that are used exclusively by one organisation.' >> mailcow.conf
|
||||
echo '# Otherwise a user might share data with too many other users.' >> mailcow.conf
|
||||
echo 'ACL_ANYONE=disallow' >> mailcow.conf
|
||||
;;
|
||||
FTS_HEAP)
|
||||
echo '# Dovecot Indexing (FTS) Process maximum heap size in MB, there is no recommendation, please see Dovecot docs.' >> mailcow.conf
|
||||
echo '# Flatcurve is used as FTS Engine. It is supposed to be pretty efficient in CPU and RAM consumption.' >> mailcow.conf
|
||||
echo '# Please always monitor your Resource consumption!' >> mailcow.conf
|
||||
echo "FTS_HEAP=128" >> mailcow.conf
|
||||
;;
|
||||
SKIP_FTS)
|
||||
echo '# Skip FTS (Fulltext Search) for Dovecot on low-memory, low-threaded systems or if you simply want to disable it.' >> mailcow.conf
|
||||
echo "# Dovecot inside mailcow use Flatcurve as FTS Backend." >> mailcow.conf
|
||||
echo "SKIP_FTS=y" >> mailcow.conf
|
||||
;;
|
||||
FTS_PROCS)
|
||||
echo '# Controls how many processes the Dovecot indexing process can spawn at max.' >> mailcow.conf
|
||||
echo '# Too many indexing processes can use a lot of CPU and Disk I/O' >> mailcow.conf
|
||||
echo '# Please visit: https://doc.dovecot.org/configuration_manual/service_configuration/#indexer-worker for more informations' >> mailcow.conf
|
||||
echo "FTS_PROCS=1" >> mailcow.conf
|
||||
;;
|
||||
ENABLE_SSL_SNI)
|
||||
echo '# Create seperate certificates for all domains - y/n' >> mailcow.conf
|
||||
echo '# this will allow adding more than 100 domains, but some email clients will not be able to connect with alternative hostnames' >> mailcow.conf
|
||||
echo '# see https://wiki.dovecot.org/SSL/SNIClientSupport' >> mailcow.conf
|
||||
echo "ENABLE_SSL_SNI=n" >> mailcow.conf
|
||||
;;
|
||||
SKIP_SOGO)
|
||||
echo '# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n' >> mailcow.conf
|
||||
echo "SKIP_SOGO=n" >> mailcow.conf
|
||||
;;
|
||||
MAILDIR_SUB)
|
||||
echo '# MAILDIR_SUB defines a path in a users virtual home to keep the maildir in. Leave empty for updated setups.' >> mailcow.conf
|
||||
echo "#MAILDIR_SUB=Maildir" >> mailcow.conf
|
||||
echo "MAILDIR_SUB=" >> mailcow.conf
|
||||
;;
|
||||
WATCHDOG_NOTIFY_WEBHOOK)
|
||||
echo '# Send notifications to a webhook URL that receives a POST request with the content type "application/json".' >> mailcow.conf
|
||||
echo '# You can use this to send notifications to services like Discord, Slack and others.' >> mailcow.conf
|
||||
echo '#WATCHDOG_NOTIFY_WEBHOOK=https://discord.com/api/webhooks/XXXXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' >> mailcow.conf
|
||||
;;
|
||||
WATCHDOG_NOTIFY_WEBHOOK_BODY)
|
||||
echo '# JSON body included in the webhook POST request. Needs to be in single quotes.' >> mailcow.conf
|
||||
echo '# Following variables are available: SUBJECT, BODY' >> mailcow.conf
|
||||
WEBHOOK_BODY='{"username": "mailcow Watchdog", "content": "**${SUBJECT}**\n${BODY}"}'
|
||||
echo "#WATCHDOG_NOTIFY_WEBHOOK_BODY='${WEBHOOK_BODY}'" >> mailcow.conf
|
||||
;;
|
||||
WATCHDOG_NOTIFY_BAN)
|
||||
echo '# Notify about banned IP. Includes whois lookup.' >> mailcow.conf
|
||||
echo "WATCHDOG_NOTIFY_BAN=y" >> mailcow.conf
|
||||
;;
|
||||
WATCHDOG_NOTIFY_START)
|
||||
echo '# Send a notification when the watchdog is started.' >> mailcow.conf
|
||||
echo "WATCHDOG_NOTIFY_START=y" >> mailcow.conf
|
||||
;;
|
||||
WATCHDOG_SUBJECT)
|
||||
echo '# Subject for watchdog mails. Defaults to "Watchdog ALERT" followed by the error message.' >> mailcow.conf
|
||||
echo "#WATCHDOG_SUBJECT=" >> mailcow.conf
|
||||
;;
|
||||
WATCHDOG_EXTERNAL_CHECKS)
|
||||
echo '# Checks if mailcow is an open relay. Requires a SAL. More checks will follow.' >> mailcow.conf
|
||||
echo '# No data is collected. Opt-in and anonymous.' >> mailcow.conf
|
||||
echo '# Will only work with unmodified mailcow setups.' >> mailcow.conf
|
||||
echo "WATCHDOG_EXTERNAL_CHECKS=n" >> mailcow.conf
|
||||
;;
|
||||
SOGO_EXPIRE_SESSION)
|
||||
echo '# SOGo session timeout in minutes' >> mailcow.conf
|
||||
echo "SOGO_EXPIRE_SESSION=480" >> mailcow.conf
|
||||
;;
|
||||
REDIS_PORT)
|
||||
echo "REDIS_PORT=127.0.0.1:7654" >> mailcow.conf
|
||||
;;
|
||||
DOVECOT_MASTER_USER)
|
||||
echo '# DOVECOT_MASTER_USER and _PASS must _both_ be provided. No special chars.' >> mailcow.conf
|
||||
echo '# Empty by default to auto-generate master user and password on start.' >> mailcow.conf
|
||||
echo '# User expands to DOVECOT_MASTER_USER@mailcow.local' >> mailcow.conf
|
||||
echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf
|
||||
echo "DOVECOT_MASTER_USER=" >> mailcow.conf
|
||||
;;
|
||||
DOVECOT_MASTER_PASS)
|
||||
echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf
|
||||
echo "DOVECOT_MASTER_PASS=" >> mailcow.conf
|
||||
;;
|
||||
MAILCOW_PASS_SCHEME)
|
||||
echo '# Password hash algorithm' >> mailcow.conf
|
||||
echo '# Only certain password hash algorithm are supported. For a fully list of supported schemes,' >> mailcow.conf
|
||||
echo '# see https://docs.mailcow.email/models/model-passwd/' >> mailcow.conf
|
||||
echo "MAILCOW_PASS_SCHEME=BLF-CRYPT" >> mailcow.conf
|
||||
;;
|
||||
ADDITIONAL_SERVER_NAMES)
|
||||
echo '# Additional server names for mailcow UI' >> mailcow.conf
|
||||
echo '#' >> mailcow.conf
|
||||
echo '# Specify alternative addresses for the mailcow UI to respond to' >> mailcow.conf
|
||||
echo '# This is useful when you set mail.* as ADDITIONAL_SAN and want to make sure mail.maildomain.com will always point to the mailcow UI.' >> mailcow.conf
|
||||
echo '# If the server name does not match a known site, Nginx decides by best-guess and may redirect users to the wrong web root.' >> mailcow.conf
|
||||
echo '# You can understand this as server_name directive in Nginx.' >> mailcow.conf
|
||||
echo '# Comma separated list without spaces! Example: ADDITIONAL_SERVER_NAMES=a.b.c,d.e.f' >> mailcow.conf
|
||||
echo 'ADDITIONAL_SERVER_NAMES=' >> mailcow.conf
|
||||
;;
|
||||
WEBAUTHN_ONLY_TRUSTED_VENDORS)
|
||||
echo "# WebAuthn device manufacturer verification" >> mailcow.conf
|
||||
echo '# After setting WEBAUTHN_ONLY_TRUSTED_VENDORS=y only devices from trusted manufacturers are allowed' >> mailcow.conf
|
||||
echo '# root certificates can be placed for validation under mailcow-dockerized/data/web/inc/lib/WebAuthn/rootCertificates' >> mailcow.conf
|
||||
echo 'WEBAUTHN_ONLY_TRUSTED_VENDORS=n' >> mailcow.conf
|
||||
;;
|
||||
SPAMHAUS_DQS_KEY)
|
||||
echo "# Spamhaus Data Query Service Key" >> mailcow.conf
|
||||
echo '# Optional: Leave empty for none' >> mailcow.conf
|
||||
echo '# Enter your key here if you are using a blocked ASN (OVH, AWS, Cloudflare e.g) for the unregistered Spamhaus Blocklist.' >> mailcow.conf
|
||||
echo '# If empty, it will completely disable Spamhaus blocklists if it detects that you are running on a server using a blocked AS.' >> mailcow.conf
|
||||
echo '# Otherwise it will work as usual.' >> mailcow.conf
|
||||
echo 'SPAMHAUS_DQS_KEY=' >> mailcow.conf
|
||||
;;
|
||||
WATCHDOG_VERBOSE)
|
||||
echo '# Enable watchdog verbose logging' >> mailcow.conf
|
||||
echo 'WATCHDOG_VERBOSE=n' >> mailcow.conf
|
||||
;;
|
||||
SKIP_UNBOUND_HEALTHCHECK)
|
||||
echo '# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) - y/n' >> mailcow.conf
|
||||
echo 'SKIP_UNBOUND_HEALTHCHECK=n' >> mailcow.conf
|
||||
;;
|
||||
DISABLE_NETFILTER_ISOLATION_RULE)
|
||||
echo '# Prevent netfilter from setting an iptables/nftables rule to isolate the mailcow docker network - y/n' >> mailcow.conf
|
||||
echo '# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost' >> mailcow.conf
|
||||
echo 'DISABLE_NETFILTER_ISOLATION_RULE=n' >> mailcow.conf
|
||||
;;
|
||||
HTTP_REDIRECT)
|
||||
echo '# Redirect HTTP connections to HTTPS - y/n' >> mailcow.conf
|
||||
echo 'HTTP_REDIRECT=n' >> mailcow.conf
|
||||
;;
|
||||
ENABLE_IPV6)
|
||||
echo '# IPv6 Controller Section' >> mailcow.conf
|
||||
echo '# This variable controls the usage of IPv6 within mailcow.' >> mailcow.conf
|
||||
echo '# Can either be true or false | Defaults to true' >> mailcow.conf
|
||||
echo '# WARNING: MAKE SURE TO PROPERLY CONFIGURE IPv6 ON YOUR HOST FIRST BEFORE ENABLING THIS AS FAULTY CONFIGURATIONS CAN LEAD TO OPEN RELAYS!' >> mailcow.conf
|
||||
echo '# A COMPLETE DOCKER STACK REBUILD (compose down && compose up -d) IS NEEDED TO APPLY THIS.' >> mailcow.conf
|
||||
echo ENABLE_IPV6=${IPV6_BOOL} >> mailcow.conf
|
||||
;;
|
||||
SKIP_CLAMD)
|
||||
echo '# Skip ClamAV (clamd-mailcow) anti-virus (Rspamd will auto-detect a missing ClamAV container) - y/n' >> mailcow.conf
|
||||
echo 'SKIP_CLAMD=n' >> mailcow.conf
|
||||
;;
|
||||
SKIP_OLEFY)
|
||||
echo '# Skip Olefy (olefy-mailcow) anti-virus for Office documents (Rspamd will auto-detect a missing Olefy container) - y/n' >> mailcow.conf
|
||||
echo 'SKIP_OLEFY=n' >> mailcow.conf
|
||||
;;
|
||||
REDISPASS)
|
||||
echo "REDISPASS=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 28)" >> mailcow.conf
|
||||
;;
|
||||
SOGO_URL_ENCRYPTION_KEY)
|
||||
echo '# SOGo URL encryption key (exactly 16 characters, limited to A–Z, a–z, 0–9)' >> mailcow.conf
|
||||
echo '# This key is used to encrypt email addresses within SOGo URLs' >> mailcow.conf
|
||||
echo "SOGO_URL_ENCRYPTION_KEY=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 16)" >> mailcow.conf
|
||||
;;
|
||||
*)
|
||||
echo "${option}=" >> mailcow.conf
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
RUN apk upgrade --no-cache \
|
||||
&& apk add --update --no-cache \
|
||||
bash \
|
||||
@@ -15,7 +14,7 @@ RUN apk upgrade --no-cache \
|
||||
tini \
|
||||
tzdata \
|
||||
python3 \
|
||||
acme-tiny --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/
|
||||
acme-tiny
|
||||
|
||||
COPY acme.sh /srv/acme.sh
|
||||
COPY functions.sh /srv/functions.sh
|
||||
|
||||
@@ -138,7 +138,7 @@ log_f "Resolver OK"
|
||||
log_f "Waiting for domain table..."
|
||||
while [[ -z ${DOMAIN_TABLE} ]]; do
|
||||
curl --silent http://nginx.${COMPOSE_PROJECT_NAME}_mailcow-network/ >/dev/null 2>&1
|
||||
DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs)
|
||||
DOMAIN_TABLE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs)
|
||||
[[ -z ${DOMAIN_TABLE} ]] && sleep 10
|
||||
done
|
||||
log_f "OK" no_date
|
||||
@@ -159,18 +159,6 @@ while true; do
|
||||
fi
|
||||
if [[ ! -f ${ACME_BASE}/acme/account.pem ]]; then
|
||||
log_f "Generating missing Lets Encrypt account key..."
|
||||
if [[ ! -z ${ACME_CONTACT} ]]; then
|
||||
if ! verify_email "${ACME_CONTACT}"; then
|
||||
log_f "Invalid email address, will not start registration!"
|
||||
sleep 365d
|
||||
exec $(readlink -f "$0")
|
||||
else
|
||||
ACME_CONTACT_PARAMETER="--contact mailto:${ACME_CONTACT}"
|
||||
log_f "Valid email address, using ${ACME_CONTACT} for registration"
|
||||
fi
|
||||
else
|
||||
ACME_CONTACT_PARAMETER=""
|
||||
fi
|
||||
openssl genrsa 4096 > ${ACME_BASE}/acme/account.pem
|
||||
else
|
||||
log_f "Using existing Lets Encrypt account key ${ACME_BASE}/acme/account.pem"
|
||||
@@ -218,7 +206,7 @@ while true; do
|
||||
|
||||
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
|
||||
# Fetch certs for autoconfig and autodiscover subdomains
|
||||
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
|
||||
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig' 'mta-sts')
|
||||
fi
|
||||
|
||||
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
|
||||
@@ -231,7 +219,7 @@ while true; do
|
||||
|
||||
#########################################
|
||||
# IP and webroot challenge verification #
|
||||
SQL_DOMAINS=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0 and active=1" -Bs)
|
||||
SQL_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain WHERE backupmx=0 and active=1" -Bs)
|
||||
if [[ ! $? -eq 0 ]]; then
|
||||
log_f "Failed to read SQL domains, retrying in 1 minute..."
|
||||
sleep 1m
|
||||
@@ -299,7 +287,7 @@ while true; do
|
||||
VALIDATED_CERTIFICATES+=("${CERT_NAME}")
|
||||
|
||||
# obtain server certificate if required
|
||||
ACME_CONTACT_PARAMETER=${ACME_CONTACT_PARAMETER} DOMAINS=${SERVER_SAN_VALIDATED[@]} /srv/obtain-certificate.sh rsa
|
||||
DOMAINS=${SERVER_SAN_VALIDATED[@]} /srv/obtain-certificate.sh rsa
|
||||
RETURN="$?"
|
||||
if [[ "$RETURN" == "0" ]]; then # 0 = cert created successfully
|
||||
CERT_AMOUNT_CHANGED=1
|
||||
|
||||
@@ -93,8 +93,8 @@ until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do
|
||||
sleep 2
|
||||
done
|
||||
log_f "Resolver OK"
|
||||
log_f "Using command acme-tiny ${DIRECTORY_URL} ${ACME_CONTACT_PARAMETER} --account-key ${ACME_BASE}/acme/account.pem --disable-check --csr ${CSR} --acme-dir /var/www/acme/"
|
||||
ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} ${ACME_CONTACT_PARAMETER} \
|
||||
log_f "Using command acme-tiny ${DIRECTORY_URL} --account-key ${ACME_BASE}/acme/account.pem --disable-check --csr ${CSR} --acme-dir /var/www/acme/"
|
||||
ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} \
|
||||
--account-key ${ACME_BASE}/acme/account.pem \
|
||||
--disable-check \
|
||||
--csr ${CSR} \
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt update && apt install pigz
|
||||
RUN apt update && apt install pigz -y --no-install-recommends
|
||||
@@ -8,7 +8,7 @@ fi
|
||||
|
||||
# Cleaning up garbage
|
||||
echo "Cleaning up tmp files..."
|
||||
rm -rf /var/lib/clamav/clamav-*.tmp
|
||||
rm -rf /var/lib/clamav/tmp.*
|
||||
|
||||
# Prepare whitelist
|
||||
|
||||
|
||||
@@ -11,4 +11,4 @@ if [ "${CLAMAV_NO_CLAMD:-}" != "false" ]; then
|
||||
echo "Clamd is up"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
exit 0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -241,9 +241,9 @@ async def handle_pubsub_messages(channel: aioredis.client.PubSub):
|
||||
else:
|
||||
dockerapi.logger.error("api call: missing container_name, post_action or request")
|
||||
else:
|
||||
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
|
||||
dockerapi.logger.error("Unknown PubSub received - %s" % json.dumps(data_json))
|
||||
else:
|
||||
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
|
||||
dockerapi.logger.error("Unknown PubSub received - %s" % json.dumps(data_json))
|
||||
|
||||
await asyncio.sleep(0.0)
|
||||
except asyncio.TimeoutError:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||
ARG GOSU_VERSION=1.16
|
||||
ARG GOSU_VERSION=1.17
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
@@ -34,9 +34,13 @@ RUN addgroup -g 5000 vmail \
|
||||
lua5.3-sql-mysql \
|
||||
icu-data-full \
|
||||
mariadb-connector-c \
|
||||
lua-sec \
|
||||
mariadb-dev \
|
||||
glib-dev \
|
||||
gcompat \
|
||||
mariadb-client \
|
||||
perl \
|
||||
perl-dev \
|
||||
perl-ntlm \
|
||||
perl-cgi \
|
||||
perl-crypt-openssl-rsa \
|
||||
@@ -65,7 +69,7 @@ RUN addgroup -g 5000 vmail \
|
||||
perl-par-packer \
|
||||
perl-parse-recdescent \
|
||||
perl-lockfile-simple \
|
||||
libproc \
|
||||
libproc2 \
|
||||
perl-readonly \
|
||||
perl-regexp-common \
|
||||
perl-sys-meminfo \
|
||||
|
||||
@@ -15,6 +15,6 @@ if ! [[ ${MAX_AGE} =~ ${NUM_REGEXP} ]] ; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TO_DELETE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT COUNT(id) FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY" -BN)
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY"
|
||||
TO_DELETE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT COUNT(id) FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY" -BN)
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM quarantine WHERE created < NOW() - INTERVAL ${MAX_AGE//[!0-9]/} DAY"
|
||||
echo "Deleted ${TO_DELETE} items from quarantine table (max age is ${MAX_AGE//[!0-9]/} days)"
|
||||
|
||||
@@ -28,7 +28,7 @@ ${VALKEY_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
|
||||
|
||||
# Create missing directories
|
||||
[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
|
||||
[[ ! -d /etc/dovecot/lua/ ]] && mkdir -p /etc/dovecot/lua/
|
||||
[[ ! -d /etc/dovecot/auth/ ]] && mkdir -p /etc/dovecot/auth/
|
||||
[[ ! -d /etc/dovecot/conf.d/ ]] && mkdir -p /etc/dovecot/conf.d/
|
||||
[[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage
|
||||
[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
|
||||
@@ -131,123 +131,6 @@ user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format
|
||||
iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
|
||||
EOF
|
||||
|
||||
cat <<EOF > /etc/dovecot/lua/passwd-verify.lua
|
||||
function auth_password_verify(req, pass)
|
||||
|
||||
if req.domain == nil then
|
||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
|
||||
end
|
||||
|
||||
if cur == nil then
|
||||
script_init()
|
||||
end
|
||||
|
||||
if req.user == nil then
|
||||
req.user = ''
|
||||
end
|
||||
|
||||
respbody = {}
|
||||
|
||||
-- check against mailbox passwds
|
||||
local cur,errorString = con:execute(string.format([[SELECT password FROM mailbox
|
||||
WHERE username = '%s'
|
||||
AND active = '1'
|
||||
AND domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')
|
||||
AND IFNULL(JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.force_pw_update')), 0) != '1'
|
||||
AND IFNULL(JSON_UNQUOTE(JSON_VALUE(attributes, '$.%s_access')), 1) = '1']], con:escape(req.user), con:escape(req.domain), con:escape(req.service)))
|
||||
local row = cur:fetch ({}, "a")
|
||||
while row do
|
||||
if req.password_verify(req, row.password, pass) == 1 then
|
||||
con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
|
||||
VALUES ("%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip)))
|
||||
cur:close()
|
||||
con:close()
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
end
|
||||
row = cur:fetch (row, "a")
|
||||
end
|
||||
|
||||
-- check against app passwds for imap and smtp
|
||||
-- app passwords are only available for imap, smtp, sieve and pop3 when using sasl
|
||||
if req.service == "smtp" or req.service == "imap" or req.service == "sieve" or req.service == "pop3" then
|
||||
local cur,errorString = con:execute(string.format([[SELECT app_passwd.id, %s_access AS has_prot_access, app_passwd.password FROM app_passwd
|
||||
INNER JOIN mailbox ON mailbox.username = app_passwd.mailbox
|
||||
WHERE mailbox = '%s'
|
||||
AND app_passwd.active = '1'
|
||||
AND mailbox.active = '1'
|
||||
AND app_passwd.domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.service), con:escape(req.user), con:escape(req.domain)))
|
||||
local row = cur:fetch ({}, "a")
|
||||
while row do
|
||||
if req.password_verify(req, row.password, pass) == 1 then
|
||||
-- if password is valid and protocol access is 1 OR real_rip matches SOGo, proceed
|
||||
if tostring(req.real_rip) == "__IPV4_SOGO__" then
|
||||
cur:close()
|
||||
con:close()
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
elseif row.has_prot_access == "1" then
|
||||
con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
|
||||
VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
|
||||
cur:close()
|
||||
con:close()
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
end
|
||||
end
|
||||
row = cur:fetch (row, "a")
|
||||
end
|
||||
end
|
||||
|
||||
cur:close()
|
||||
con:close()
|
||||
|
||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
|
||||
|
||||
-- PoC
|
||||
-- local reqbody = string.format([[{
|
||||
-- "success":0,
|
||||
-- "service":"%s",
|
||||
-- "app_password":false,
|
||||
-- "username":"%s",
|
||||
-- "real_rip":"%s"
|
||||
-- }]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip))
|
||||
-- http.request {
|
||||
-- method = "POST",
|
||||
-- url = "http://nginx:8081/sasl_log.php",
|
||||
-- source = ltn12.source.string(reqbody),
|
||||
-- headers = {
|
||||
-- ["content-type"] = "application/json",
|
||||
-- ["content-length"] = tostring(#reqbody)
|
||||
-- },
|
||||
-- sink = ltn12.sink.table(respbody)
|
||||
-- }
|
||||
|
||||
end
|
||||
|
||||
function auth_passdb_lookup(req)
|
||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
|
||||
end
|
||||
|
||||
function script_init()
|
||||
mysql = require "luasql.mysql"
|
||||
http = require "socket.http"
|
||||
http.TIMEOUT = 5
|
||||
ltn12 = require "ltn12"
|
||||
env = mysql.mysql()
|
||||
con = env:connect("__DBNAME__","__DBUSER__","__DBPASS__","localhost")
|
||||
return 0
|
||||
end
|
||||
|
||||
function script_deinit()
|
||||
con:close()
|
||||
env:close()
|
||||
end
|
||||
EOF
|
||||
|
||||
# Replace patterns in app-passdb.lua
|
||||
sed -i "s/__DBUSER__/${DBUSER}/g" /etc/dovecot/lua/passwd-verify.lua
|
||||
sed -i "s/__DBPASS__/${DBPASS}/g" /etc/dovecot/lua/passwd-verify.lua
|
||||
sed -i "s/__DBNAME__/${DBNAME}/g" /etc/dovecot/lua/passwd-verify.lua
|
||||
sed -i "s/__IPV4_SOGO__/${IPV4_NETWORK}.248/g" /etc/dovecot/lua/passwd-verify.lua
|
||||
|
||||
|
||||
# Migrate old sieve_after file
|
||||
[[ -f /etc/dovecot/sieve_after ]] && mv /etc/dovecot/sieve_after /etc/dovecot/global_sieve_after
|
||||
@@ -385,8 +268,8 @@ sievec /usr/lib/dovecot/sieve/report-ham.sieve
|
||||
|
||||
# Fix permissions
|
||||
chown root:root /etc/dovecot/sql/*.conf
|
||||
chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua
|
||||
chmod 640 /etc/dovecot/sql/*.conf /etc/dovecot/lua/passwd-verify.lua
|
||||
chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/auth/passwd-verify.lua
|
||||
chmod 640 /etc/dovecot/sql/*.conf /etc/dovecot/auth/passwd-verify.lua
|
||||
chown -R vmail:vmail /var/vmail/sieve
|
||||
chown -R vmail:vmail /var/volatile
|
||||
chown -R vmail:vmail /var/vmail_index
|
||||
@@ -414,15 +297,15 @@ printenv | sed 's/^\(.*\)$/export \1/g' > /source_env.sh
|
||||
|
||||
# Clean stopped imapsync jobs
|
||||
rm -f /tmp/imapsync_busy.lock
|
||||
IMAPSYNC_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs)
|
||||
[[ ! -z ${IMAPSYNC_TABLE} ]] && mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'"
|
||||
IMAPSYNC_TABLE=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'imapsync'" -Bs)
|
||||
[[ ! -z ${IMAPSYNC_TABLE} ]] && mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "UPDATE imapsync SET is_running='0'"
|
||||
|
||||
# Envsubst maildir_gc
|
||||
echo "$(envsubst < /usr/local/bin/maildir_gc.sh)" > /usr/local/bin/maildir_gc.sh
|
||||
|
||||
# GUID generation
|
||||
while [[ ${VERSIONS_OK} != 'OK' ]]; do
|
||||
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = \"${DBNAME}\" AND TABLE_NAME = 'versions'") ]]; then
|
||||
if [[ ! -z $(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = \"${DBNAME}\" AND TABLE_NAME = 'versions'") ]]; then
|
||||
VERSIONS_OK=OK
|
||||
else
|
||||
echo "Waiting for versions table to be created..."
|
||||
@@ -433,11 +316,11 @@ PUBKEY_MCRYPT=$(doveconf -P 2> /dev/null | grep -i mail_crypt_global_public_key
|
||||
if [ -f ${PUBKEY_MCRYPT} ]; then
|
||||
GUID=$(cat <(echo ${MAILCOW_HOSTNAME}) /mail_crypt/ecpubkey.pem | sha256sum | cut -d ' ' -f1 | tr -cd "[a-fA-F0-9.:/] ")
|
||||
if [ ${#GUID} -eq 64 ]; then
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
REPLACE INTO versions (application, version) VALUES ("GUID", "${GUID}");
|
||||
EOF
|
||||
else
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
REPLACE INTO versions (application, version) VALUES ("GUID", "INVALID");
|
||||
EOF
|
||||
fi
|
||||
@@ -456,7 +339,7 @@ done
|
||||
|
||||
# For some strange, unknown and stupid reason, Dovecot may run into a race condition, when this file is not touched before it is read by dovecot/auth
|
||||
# May be related to something inside Docker, I seriously don't know
|
||||
touch /etc/dovecot/lua/passwd-verify.lua
|
||||
touch /etc/dovecot/auth/passwd-verify.lua
|
||||
|
||||
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
|
||||
cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf
|
||||
|
||||
@@ -132,8 +132,8 @@ while ($row = $sth->fetchrow_arrayref()) {
|
||||
"--tmpdir", "/tmp",
|
||||
"--nofoldersizes",
|
||||
"--addheader",
|
||||
($timeout1 gt "0" ? () : ('--timeout1', $timeout1)),
|
||||
($timeout2 gt "0" ? () : ('--timeout2', $timeout2)),
|
||||
($timeout1 le "0" ? () : ('--timeout1', $timeout1)),
|
||||
($timeout2 le "0" ? () : ('--timeout2', $timeout2)),
|
||||
($exclude eq "" ? () : ("--exclude", $exclude)),
|
||||
($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
|
||||
($maxage eq "0" ? () : ('--maxage', $maxage)),
|
||||
|
||||
@@ -8,7 +8,8 @@ from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import COMMASPACE, formatdate
|
||||
import jinja2
|
||||
from jinja2 import Template
|
||||
from jinja2 import TemplateError
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
import json
|
||||
import redis
|
||||
import time
|
||||
@@ -75,22 +76,27 @@ try:
|
||||
|
||||
def notify_rcpt(rcpt, msg_count, quarantine_acl, category):
|
||||
if category == "add_header": category = "add header"
|
||||
meta_query = query_mysql('SELECT SHA2(CONCAT(id, qid), 256) AS qhash, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category))
|
||||
meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category))
|
||||
print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count))
|
||||
if len(meta_query) == 0:
|
||||
return
|
||||
msg_count = len(meta_query)
|
||||
env = SandboxedEnvironment()
|
||||
if r.get('Q_HTML'):
|
||||
try:
|
||||
template = Template(r.get('Q_HTML'))
|
||||
except:
|
||||
print("Error: Cannot parse quarantine template, falling back to default template.")
|
||||
with open('/templates/quarantine.tpl') as file_:
|
||||
template = Template(file_.read())
|
||||
try:
|
||||
template = env.from_string(r.get('Q_HTML'))
|
||||
except Exception:
|
||||
print("Error: Cannot parse quarantine template, falling back to default template.")
|
||||
with open('/templates/quarantine.tpl') as file_:
|
||||
template = env.from_string(file_.read())
|
||||
else:
|
||||
with open('/templates/quarantine.tpl') as file_:
|
||||
template = Template(file_.read())
|
||||
html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl)
|
||||
with open('/templates/quarantine.tpl') as file_:
|
||||
template = env.from_string(file_.read())
|
||||
try:
|
||||
html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl)
|
||||
except (jinja2.exceptions.SecurityError, TemplateError) as ex:
|
||||
print(f"SecurityError or TemplateError in template rendering: {ex}")
|
||||
return
|
||||
text = html2text.html2text(html)
|
||||
count = 0
|
||||
while count < 15:
|
||||
|
||||
@@ -6,7 +6,7 @@ from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import COMMASPACE, formatdate
|
||||
import jinja2
|
||||
from jinja2 import Template
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
import redis
|
||||
import time
|
||||
import json
|
||||
@@ -33,16 +33,24 @@ while True:
|
||||
|
||||
if r.get('QW_HTML'):
|
||||
try:
|
||||
template = Template(r.get('QW_HTML'))
|
||||
except:
|
||||
print("Error: Cannot parse quarantine template, falling back to default template.")
|
||||
env = SandboxedEnvironment()
|
||||
template = env.from_string(r.get('QW_HTML'))
|
||||
except Exception:
|
||||
print("Error: Cannot parse quota template, falling back to default template.")
|
||||
with open('/templates/quota.tpl') as file_:
|
||||
template = Template(file_.read())
|
||||
env = SandboxedEnvironment()
|
||||
template = env.from_string(file_.read())
|
||||
else:
|
||||
with open('/templates/quota.tpl') as file_:
|
||||
template = Template(file_.read())
|
||||
env = SandboxedEnvironment()
|
||||
template = env.from_string(file_.read())
|
||||
|
||||
try:
|
||||
html = template.render(username=username, percent=percent)
|
||||
except (jinja2.exceptions.SecurityError, jinja2.TemplateError) as ex:
|
||||
print(f"SecurityError or TemplateError in template rendering: {ex}")
|
||||
sys.exit(1)
|
||||
|
||||
html = template.render(username=username, percent=percent)
|
||||
text = html2text.html2text(html)
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/sh
|
||||
|
||||
backend=iptables
|
||||
backend=nftables
|
||||
|
||||
nft list table ip filter &>/dev/null
|
||||
nftables_found=$?
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
DEBUG = False
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
@@ -20,10 +22,13 @@ from modules.Logger import Logger
|
||||
from modules.IPTables import IPTables
|
||||
from modules.NFTables import NFTables
|
||||
|
||||
def logdebug(msg):
|
||||
if DEBUG:
|
||||
logger.logInfo("DEBUG: %s" % msg)
|
||||
|
||||
# globals
|
||||
# Globals
|
||||
WHITELIST = []
|
||||
BLACKLIST= []
|
||||
BLACKLIST = []
|
||||
bans = {}
|
||||
quit_now = False
|
||||
exit_code = 0
|
||||
@@ -33,12 +38,10 @@ r = None
|
||||
pubsub = None
|
||||
clear_before_quit = False
|
||||
|
||||
|
||||
def refreshF2boptions():
|
||||
global f2boptions
|
||||
global quit_now
|
||||
global exit_code
|
||||
|
||||
f2boptions = {}
|
||||
|
||||
if not valkey.get('F2B_OPTIONS'):
|
||||
@@ -61,15 +64,15 @@ def refreshF2boptions():
|
||||
valkey.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
||||
|
||||
def verifyF2boptions(f2boptions):
|
||||
verifyF2boption(f2boptions,'ban_time', 1800)
|
||||
verifyF2boption(f2boptions,'max_ban_time', 10000)
|
||||
verifyF2boption(f2boptions,'ban_time_increment', True)
|
||||
verifyF2boption(f2boptions,'max_attempts', 10)
|
||||
verifyF2boption(f2boptions,'retry_window', 600)
|
||||
verifyF2boption(f2boptions,'netban_ipv4', 32)
|
||||
verifyF2boption(f2boptions,'netban_ipv6', 128)
|
||||
verifyF2boption(f2boptions,'banlist_id', str(uuid.uuid4()))
|
||||
verifyF2boption(f2boptions,'manage_external', 0)
|
||||
verifyF2boption(f2boptions, 'ban_time', 1800)
|
||||
verifyF2boption(f2boptions, 'max_ban_time', 10000)
|
||||
verifyF2boption(f2boptions, 'ban_time_increment', True)
|
||||
verifyF2boption(f2boptions, 'max_attempts', 10)
|
||||
verifyF2boption(f2boptions, 'retry_window', 600)
|
||||
verifyF2boption(f2boptions, 'netban_ipv4', 32)
|
||||
verifyF2boption(f2boptions, 'netban_ipv6', 128)
|
||||
verifyF2boption(f2boptions, 'banlist_id', str(uuid.uuid4()))
|
||||
verifyF2boption(f2boptions, 'manage_external', 0)
|
||||
|
||||
def verifyF2boption(f2boptions, f2boption, f2bdefault):
|
||||
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
|
||||
@@ -111,7 +114,7 @@ def get_ip(address):
|
||||
def ban(address):
|
||||
global f2boptions
|
||||
global lock
|
||||
|
||||
logdebug("ban() called with address=%s" % address)
|
||||
refreshF2boptions()
|
||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||
RETRY_WINDOW = int(f2boptions['retry_window'])
|
||||
@@ -119,31 +122,43 @@ def ban(address):
|
||||
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
|
||||
|
||||
ip = get_ip(address)
|
||||
if not ip: return
|
||||
if not ip:
|
||||
logdebug("No valid IP -- skipping ban()")
|
||||
return
|
||||
address = str(ip)
|
||||
self_network = ipaddress.ip_network(address)
|
||||
|
||||
with lock:
|
||||
temp_whitelist = set(WHITELIST)
|
||||
if temp_whitelist:
|
||||
for wl_key in temp_whitelist:
|
||||
wl_net = ipaddress.ip_network(wl_key, False)
|
||||
if wl_net.overlaps(self_network):
|
||||
logger.logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
|
||||
return
|
||||
logdebug("Checking if %s overlaps with any WHITELIST entries" % self_network)
|
||||
if temp_whitelist:
|
||||
for wl_key in temp_whitelist:
|
||||
wl_net = ipaddress.ip_network(wl_key, False)
|
||||
logdebug("Checking overlap between %s and %s" % (self_network, wl_net))
|
||||
if wl_net.overlaps(self_network):
|
||||
logger.logInfo(
|
||||
'Address %s is allowlisted by rule %s' % (self_network, wl_net))
|
||||
return
|
||||
|
||||
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
|
||||
net = ipaddress.ip_network(
|
||||
(address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
|
||||
net = str(net)
|
||||
logdebug("Ban net: %s" % net)
|
||||
|
||||
if not net in bans:
|
||||
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
|
||||
logdebug("Initing new ban counter for %s" % net)
|
||||
|
||||
current_attempt = time.time()
|
||||
logdebug("Current attempt ts=%s, previous: %s, retry_window: %s" %
|
||||
(current_attempt, bans[net]['last_attempt'], RETRY_WINDOW))
|
||||
if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW:
|
||||
bans[net]['attempts'] = 0
|
||||
logdebug("Ban counter for %s reset as window expired" % net)
|
||||
|
||||
bans[net]['attempts'] += 1
|
||||
bans[net]['last_attempt'] = current_attempt
|
||||
logdebug("%s attempts now %d" % (net, bans[net]['attempts']))
|
||||
|
||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||
cur_time = int(round(time.time()))
|
||||
@@ -151,34 +166,41 @@ def ban(address):
|
||||
logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
|
||||
if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1:
|
||||
with lock:
|
||||
logdebug("Calling tables.banIPv4(%s)" % net)
|
||||
tables.banIPv4(net)
|
||||
elif int(f2boptions['manage_external']) != 1:
|
||||
with lock:
|
||||
logdebug("Calling tables.banIPv6(%s)" % net)
|
||||
tables.banIPv6(net)
|
||||
|
||||
logdebug("Updating F2B_ACTIVE_BANS[%s]=%d" %
|
||||
(net, cur_time + NET_BAN_TIME))
|
||||
valkey.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
|
||||
else:
|
||||
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
||||
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (
|
||||
MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
||||
|
||||
def unban(net):
|
||||
global lock
|
||||
|
||||
logdebug("Calling unban() with net=%s" % net)
|
||||
if not net in bans:
|
||||
logger.logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
|
||||
valkey.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||
return
|
||||
|
||||
logger.logInfo(
|
||||
'%s is not banned, skipping unban and deleting from queue (if any)' % net)
|
||||
valkey.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||
return
|
||||
logger.logInfo('Unbanning %s' % net)
|
||||
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
|
||||
with lock:
|
||||
logdebug("Calling tables.unbanIPv4(%s)" % net)
|
||||
tables.unbanIPv4(net)
|
||||
else:
|
||||
with lock:
|
||||
logdebug("Calling tables.unbanIPv6(%s)" % net)
|
||||
tables.unbanIPv6(net)
|
||||
|
||||
valkey.hdel('F2B_ACTIVE_BANS', '%s' % net)
|
||||
valkey.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||
if net in bans:
|
||||
logdebug("Unban for %s, setting attempts=0, ban_counter+=1" % net)
|
||||
bans[net]['attempts'] = 0
|
||||
bans[net]['ban_counter'] += 1
|
||||
|
||||
@@ -204,17 +226,19 @@ def permBan(net, unban=False):
|
||||
|
||||
if is_unbanned:
|
||||
valkey.hdel('F2B_PERM_BANS', '%s' % net)
|
||||
logger.logCrit('Removed host/network %s from blacklist' % net)
|
||||
logger.logCrit('Removed host/network %s from denylist' % net)
|
||||
elif is_banned:
|
||||
valkey.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
||||
logger.logCrit('Added host/network %s to blacklist' % net)
|
||||
logger.logCrit('Added host/network %s to denylist' % net)
|
||||
|
||||
def clear():
|
||||
global lock
|
||||
logger.logInfo('Clearing all bans')
|
||||
for net in bans.copy():
|
||||
logdebug("Unbanning net: %s" % net)
|
||||
unban(net)
|
||||
with lock:
|
||||
logdebug("Clearing IPv4/IPv6 table")
|
||||
tables.clearIPv4Table()
|
||||
tables.clearIPv6Table()
|
||||
try:
|
||||
@@ -275,21 +299,35 @@ def snat6(snat_target):
|
||||
|
||||
def autopurge():
|
||||
global f2boptions
|
||||
|
||||
logdebug("autopurge thread started")
|
||||
while not quit_now:
|
||||
logdebug("autopurge tick")
|
||||
time.sleep(10)
|
||||
refreshF2boptions()
|
||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||
QUEUE_UNBAN = valkey.hgetall('F2B_QUEUE_UNBAN')
|
||||
logdebug("QUEUE_UNBAN: %s" % QUEUE_UNBAN)
|
||||
if QUEUE_UNBAN:
|
||||
for net in QUEUE_UNBAN:
|
||||
logdebug("Autopurge: unbanning queued net: %s" % net)
|
||||
unban(str(net))
|
||||
for net in bans.copy():
|
||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||
NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter'])
|
||||
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
|
||||
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME:
|
||||
unban(net)
|
||||
# Only check expiry for actively banned IPs:
|
||||
active_bans = r.hgetall('F2B_ACTIVE_BANS')
|
||||
now = time.time()
|
||||
for net_str, expire_str in active_bans.items():
|
||||
logdebug("Checking ban expiry for (actively banned): %s" % net_str)
|
||||
# Defensive: always process if timer missing or expired
|
||||
try:
|
||||
expire = float(expire_str)
|
||||
except Exception:
|
||||
logdebug("Invalid expire time for %s; unbanning" % net_str)
|
||||
unban(net_str)
|
||||
continue
|
||||
time_left = expire - now
|
||||
logdebug("Time left for %s: %.1f seconds" % (net_str, time_left))
|
||||
if time_left <= 0:
|
||||
logdebug("Ban expired for %s" % net_str)
|
||||
unban(net_str)
|
||||
|
||||
def mailcowChainOrder():
|
||||
global lock
|
||||
@@ -359,7 +397,7 @@ def whitelistUpdate():
|
||||
with lock:
|
||||
if Counter(new_whitelist) != Counter(WHITELIST):
|
||||
WHITELIST = new_whitelist
|
||||
logger.logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
|
||||
logger.logInfo('Allowlist was changed, it has %s entries' % len(WHITELIST))
|
||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||
|
||||
def blacklistUpdate():
|
||||
@@ -375,7 +413,7 @@ def blacklistUpdate():
|
||||
addban = set(new_blacklist).difference(BLACKLIST)
|
||||
delban = set(BLACKLIST).difference(new_blacklist)
|
||||
BLACKLIST = new_blacklist
|
||||
logger.logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
|
||||
logger.logInfo('Denylist was changed, it has %s entries' % len(BLACKLIST))
|
||||
if addban:
|
||||
for net in addban:
|
||||
permBan(net=net)
|
||||
@@ -386,42 +424,43 @@ def blacklistUpdate():
|
||||
|
||||
def sigterm_quit(signum, frame):
|
||||
global clear_before_quit
|
||||
logdebug("SIGTERM received, setting clear_before_quit to True and exiting")
|
||||
clear_before_quit = True
|
||||
sys.exit(exit_code)
|
||||
|
||||
def berfore_quit():
|
||||
def before_quit():
|
||||
logdebug("before_quit called, clear_before_quit=%s" % clear_before_quit)
|
||||
if clear_before_quit:
|
||||
clear()
|
||||
if pubsub is not None:
|
||||
pubsub.unsubscribe()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
atexit.register(berfore_quit)
|
||||
logger = Logger()
|
||||
logdebug("Sys.argv: %s" % sys.argv)
|
||||
atexit.register(before_quit)
|
||||
signal.signal(signal.SIGTERM, sigterm_quit)
|
||||
|
||||
# init Logger
|
||||
logger = Logger()
|
||||
|
||||
# init backend
|
||||
backend = sys.argv[1]
|
||||
logdebug("Backend: %s" % backend)
|
||||
if backend == "nftables":
|
||||
logger.logInfo('Using NFTables backend')
|
||||
tables = NFTables(chain_name, logger)
|
||||
else:
|
||||
logger.logInfo('Using IPTables backend')
|
||||
logger.logWarn(
|
||||
"DEPRECATION: iptables-legacy is deprecated and will be removed in future releases. "
|
||||
"Please switch to nftables on your host to ensure complete compatibility."
|
||||
)
|
||||
time.sleep(5)
|
||||
tables = IPTables(chain_name, logger)
|
||||
|
||||
# In case a previous session was killed without cleanup
|
||||
clear()
|
||||
|
||||
# Reinit MAILCOW chain
|
||||
# Is called before threads start, no locking
|
||||
logger.logInfo("Initializing mailcow netfilter chain")
|
||||
tables.initChainIPv4()
|
||||
tables.initChainIPv6()
|
||||
|
||||
if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE").lower() in ("y", "yes"):
|
||||
if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE", "").lower() in ("y", "yes"):
|
||||
logger.logInfo(f"Skipping {chain_name} isolation")
|
||||
else:
|
||||
logger.logInfo(f"Setting {chain_name} isolation")
|
||||
@@ -432,23 +471,28 @@ if __name__ == '__main__':
|
||||
try:
|
||||
valkey_slaveof_ip = os.getenv('VALKEY_SLAVEOF_IP', '')
|
||||
valkey_slaveof_port = os.getenv('VALKEY_SLAVEOF_PORT', '')
|
||||
logdebug(
|
||||
"Connecting valkey (SLAVEOF_IP:%s, PORT:%s)" % (valkey_slaveof_ip, valkey_slaveof_port))
|
||||
if "".__eq__(valkey_slaveof_ip):
|
||||
valkey = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['VALKEYPASS'])
|
||||
valkey = redis.StrictRedis(
|
||||
host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['VALKEYPASS'])
|
||||
else:
|
||||
valkey = redis.StrictRedis(host=valkey_slaveof_ip, decode_responses=True, port=valkey_slaveof_port, db=0, password=os.environ['VALKEYPASS'])
|
||||
valkey = redis.StrictRedis(
|
||||
host=valkey_slaveof_ip, decode_responses=True, port=valkey_slaveof_port, db=0, password=os.environ['VALKEYPASS'])
|
||||
valkey.ping()
|
||||
pubsub = valkey.pubsub()
|
||||
except Exception as ex:
|
||||
print('%s - trying again in 3 seconds' % (ex))
|
||||
logdebug(
|
||||
'Redis connection failed: %s - trying again in 3 seconds' % (ex))
|
||||
time.sleep(3)
|
||||
else:
|
||||
break
|
||||
logger.set_valkey(r)
|
||||
logger.set_valkey(valkey)
|
||||
logdebug("Valkey connection established, setting up F2B keys")
|
||||
|
||||
# rename fail2ban to netfilter
|
||||
if valkey.exists('F2B_LOG'):
|
||||
logdebug("Renaming F2B_LOG to NETFILTER_LOG")
|
||||
valkey.rename('F2B_LOG', 'NETFILTER_LOG')
|
||||
# clear bans in valkey
|
||||
valkey.delete('F2B_ACTIVE_BANS')
|
||||
valkey.delete('F2B_PERM_BANS')
|
||||
|
||||
@@ -463,7 +507,7 @@ if __name__ == '__main__':
|
||||
snat_ip = os.getenv('SNAT_TO_SOURCE')
|
||||
snat_ipo = ipaddress.ip_address(snat_ip)
|
||||
if type(snat_ipo) is ipaddress.IPv4Address:
|
||||
snat4_thread = Thread(target=snat4,args=(snat_ip,))
|
||||
snat4_thread = Thread(target=snat4, args=(snat_ip,))
|
||||
snat4_thread.daemon = True
|
||||
snat4_thread.start()
|
||||
except ValueError:
|
||||
@@ -499,4 +543,5 @@ if __name__ == '__main__':
|
||||
while not quit_now:
|
||||
time.sleep(0.5)
|
||||
|
||||
sys.exit(exit_code)
|
||||
logdebug("Exiting with code %s" % exit_code)
|
||||
sys.exit(exit_code)
|
||||
@@ -1,5 +1,6 @@
|
||||
import time
|
||||
import json
|
||||
import datetime
|
||||
|
||||
class Logger:
|
||||
def __init__(self):
|
||||
@@ -8,17 +9,28 @@ class Logger:
|
||||
def set_valkey(self, valkey):
|
||||
self.valkey = valkey
|
||||
|
||||
def _format_timestamp(self):
|
||||
# Local time with milliseconds
|
||||
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def log(self, priority, message):
|
||||
tolog = {}
|
||||
tolog['time'] = int(round(time.time()))
|
||||
tolog['priority'] = priority
|
||||
tolog['message'] = message
|
||||
print(message)
|
||||
# build valkey-friendly dict
|
||||
tolog = {
|
||||
'time': int(round(time.time())), # keep raw timestamp for Valkey
|
||||
'priority': priority,
|
||||
'message': message
|
||||
}
|
||||
|
||||
# print human-readable message with timestamp
|
||||
ts = self._format_timestamp()
|
||||
print(f"{ts} {priority.upper()}: {message}", flush=True)
|
||||
|
||||
# also push JSON to Redis if connected
|
||||
if self.valkey is not None:
|
||||
try:
|
||||
self.valkey.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
|
||||
except Exception as ex:
|
||||
print('Failed logging to valkey: %s' % (ex))
|
||||
print(f'{ts} WARN: Failed logging to valkey: {ex}', flush=True)
|
||||
|
||||
def logWarn(self, message):
|
||||
self.log('warn', message)
|
||||
@@ -27,4 +39,4 @@ class Logger:
|
||||
self.log('crit', message)
|
||||
|
||||
def logInfo(self, message):
|
||||
self.log('info', message)
|
||||
self.log('info', message)
|
||||
@@ -10,7 +10,7 @@ def includes_conf(env, template_vars):
|
||||
server_name_config = f"server_name {template_vars['MAILCOW_HOSTNAME']} autodiscover.* autoconfig.* {' '.join(template_vars['ADDITIONAL_SERVER_NAMES'])};"
|
||||
listen_plain_config = f"listen {template_vars['HTTP_PORT']};"
|
||||
listen_ssl_config = f"listen {template_vars['HTTPS_PORT']};"
|
||||
if not template_vars['DISABLE_IPv6']:
|
||||
if template_vars['ENABLE_IPV6']:
|
||||
listen_plain_config += f"\nlisten [::]:{template_vars['HTTP_PORT']};"
|
||||
listen_ssl_config += f"\nlisten [::]:{template_vars['HTTPS_PORT']} ssl;"
|
||||
listen_ssl_config += "\nhttp2 on;"
|
||||
@@ -58,7 +58,7 @@ def prepare_template_vars():
|
||||
'SOGOHOST': os.getenv("SOGOHOST", ipv4_network + ".248"),
|
||||
'RSPAMDHOST': os.getenv("RSPAMDHOST", "rspamd-mailcow"),
|
||||
'PHPFPMHOST': os.getenv("PHPFPMHOST", "php-fpm-mailcow"),
|
||||
'DISABLE_IPv6': os.getenv("DISABLE_IPv6", "n").lower() in ("y", "yes"),
|
||||
'ENABLE_IPV6': os.getenv("ENABLE_IPV6", "true").lower() != "false",
|
||||
'HTTP_REDIRECT': os.getenv("HTTP_REDIRECT", "n").lower() in ("y", "yes"),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -32,6 +32,13 @@ import time
|
||||
import magic
|
||||
import re
|
||||
|
||||
skip_olefy = os.getenv('SKIP_OLEFY', '')
|
||||
|
||||
if skip_olefy.lower() in ['yes', 'y']:
|
||||
print("SKIP_OLEFY=y, skipping Olefy...")
|
||||
time.sleep(365 * 24 * 60 * 60)
|
||||
sys.exit(0)
|
||||
|
||||
# merge variables from /etc/olefy.conf and the defaults
|
||||
olefy_listen_addr_string = os.getenv('OLEFY_BINDADDRESS', '127.0.0.1,::1')
|
||||
olefy_listen_port = int(os.getenv('OLEFY_BINDPORT', '10050'))
|
||||
@@ -113,7 +120,7 @@ def oletools( stream, tmp_file_name, lid ):
|
||||
out = bytes(out.decode('utf-8', 'ignore').replace(' ', ' ').replace('\t', '').replace('\n', '').replace('XLMMacroDeobfuscator: pywin32 is not installed (only is required if you want to use MS Excel)', ''), encoding="utf-8")
|
||||
failed = False
|
||||
if out.__len__() < 30:
|
||||
logger.error('{} olevba returned <30 chars - rc: {!r}, response: {!r}, error: {!r}'.format(lid,cmd_tmp.returncode,
|
||||
logger.error('{} olevba returned <30 chars - rc: {!r}, response: {!r}, error: {!r}'.format(lid,cmd_tmp.returncode,
|
||||
out.decode('utf-8', 'ignore'), err.decode('utf-8', 'ignore')))
|
||||
out = b'[ { "error": "Unhandled error - too short olevba response" } ]'
|
||||
failed = True
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
FROM php:8.2-fpm-alpine3.20
|
||||
FROM php:8.2-fpm-alpine3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG APCU_PECL_VERSION=5.1.24
|
||||
ARG APCU_PECL_VERSION=5.1.26
|
||||
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG IMAGICK_PECL_VERSION=3.7.0
|
||||
ARG IMAGICK_PECL_VERSION=3.8.0
|
||||
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG MAILPARSE_PECL_VERSION=3.1.8
|
||||
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||
ARG MEMCACHED_PECL_VERSION=3.2.0
|
||||
ARG MEMCACHED_PECL_VERSION=3.3.0
|
||||
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG REDIS_PECL_VERSION=6.1.0
|
||||
ARG REDIS_PECL_VERSION=6.2.0
|
||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||
ARG COMPOSER_VERSION=2.6.6
|
||||
ARG COMPOSER_VERSION=2.8.6
|
||||
|
||||
RUN apk add -U --no-cache autoconf \
|
||||
aspell-dev \
|
||||
|
||||
@@ -81,7 +81,7 @@ if [ ${SQL_CHANGED} -eq 1 ]; then
|
||||
fi
|
||||
|
||||
# Check mysql tz import (master and slave)
|
||||
TZ_CHECK=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null)
|
||||
TZ_CHECK=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null)
|
||||
if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then
|
||||
SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json')
|
||||
echo "MySQL mysql_tzinfo_to_sql - debug output:"
|
||||
@@ -120,11 +120,11 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
while read line
|
||||
do
|
||||
DOMAIN_ARR+=("$line")
|
||||
done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs)
|
||||
done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM domain" -Bs)
|
||||
while read line
|
||||
do
|
||||
DOMAIN_ARR+=("$line")
|
||||
done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs)
|
||||
done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT alias_domain FROM alias_domain" -Bs)
|
||||
|
||||
if [[ ! -z ${DOMAIN_ARR} ]]; then
|
||||
for domain in "${DOMAIN_ARR[@]}"; do
|
||||
@@ -146,13 +146,13 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
VALIDATED_IPS=$(array_by_comma ${VALIDATED_API_ALLOW_FROM_ARR[*]})
|
||||
if [[ ! -z ${VALIDATED_IPS} ]]; then
|
||||
if [[ ${API_KEY} != "invalid" ]] && [[ ! -z ${API_KEY} ]]; then
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
DELETE FROM api WHERE access = 'rw';
|
||||
INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY}", "1", "${VALIDATED_IPS}", "rw");
|
||||
EOF
|
||||
fi
|
||||
if [[ ${API_KEY_READ_ONLY} != "invalid" ]] && [[ ! -z ${API_KEY_READ_ONLY} ]]; then
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
DELETE FROM api WHERE access = 'ro';
|
||||
INSERT INTO api (api_key, active, allow_from, access) VALUES ("${API_KEY_READ_ONLY}", "1", "${VALIDATED_IPS}", "ro");
|
||||
EOF
|
||||
@@ -161,7 +161,7 @@ EOF
|
||||
fi
|
||||
|
||||
# Create events (master only, STATUS for event on slave will be SLAVESIDE_DISABLED)
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
DROP EVENT IF EXISTS clean_spamalias;
|
||||
DELIMITER //
|
||||
CREATE EVENT clean_spamalias
|
||||
|
||||
50
data/Dockerfiles/postfix-tlspol/Dockerfile
Normal file
50
data/Dockerfiles/postfix-tlspol/Dockerfile
Normal file
@@ -0,0 +1,50 @@
|
||||
FROM golang:1.25-bookworm AS builder
|
||||
WORKDIR /src
|
||||
|
||||
ENV CGO_ENABLED=0 \
|
||||
GO111MODULE=on \
|
||||
NOOPT=1 \
|
||||
VERSION=1.8.14
|
||||
|
||||
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
|
||||
cd /src/postfix-tlspol && \
|
||||
scripts/build.sh build-only
|
||||
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ENV LC_ALL=C
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
dirmngr \
|
||||
dnsutils \
|
||||
iputils-ping \
|
||||
sudo \
|
||||
supervisor \
|
||||
redis-tools \
|
||||
syslog-ng \
|
||||
syslog-ng-core \
|
||||
syslog-ng-mod-redis \
|
||||
tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& touch /etc/default/locale
|
||||
|
||||
COPY supervisord.conf /etc/supervisor/supervisord.conf
|
||||
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||
COPY syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf
|
||||
COPY postfix-tlspol.sh /opt/postfix-tlspol.sh
|
||||
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
COPY --from=builder /src/postfix-tlspol/build/postfix-tlspol /usr/local/bin/postfix-tlspol
|
||||
|
||||
RUN chmod +x /opt/postfix-tlspol.sh \
|
||||
/usr/local/sbin/stop-supervisor.sh \
|
||||
/docker-entrypoint.sh
|
||||
RUN rm -rf /tmp/* /var/tmp/*
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
7
data/Dockerfiles/postfix-tlspol/docker-entrypoint.sh
Executable file
7
data/Dockerfiles/postfix-tlspol/docker-entrypoint.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
|
||||
cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
52
data/Dockerfiles/postfix-tlspol/postfix-tlspol.sh
Executable file
52
data/Dockerfiles/postfix-tlspol/postfix-tlspol.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
LOGLVL=info
|
||||
|
||||
if [ ${DEV_MODE} != "n" ]; then
|
||||
echo -e "\e[31mEnabling debug mode\e[0m"
|
||||
set -x
|
||||
LOGLVL=debug
|
||||
fi
|
||||
|
||||
[[ ! -d /etc/postfix-tlspol ]] && mkdir -p /etc/postfix-tlspol
|
||||
[[ ! -d /var/lib/postfix-tlspol ]] && mkdir -p /var/lib/postfix-tlspol
|
||||
|
||||
until dig +short mailcow.email > /dev/null; do
|
||||
echo "Waiting for DNS..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Do not attempt to write to slave
|
||||
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
|
||||
export VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
|
||||
else
|
||||
export VALKEY_CMDLINE="redis-cli -h valkey -p 6379 -a ${VALKEYPASS} --no-auth-warning"
|
||||
fi
|
||||
|
||||
until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
|
||||
echo "Waiting for Valkey..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Waiting for Postfix..."
|
||||
until ping postfix -c1 > /dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
echo "Postfix OK"
|
||||
|
||||
cat <<EOF > /etc/postfix-tlspol/config.yaml
|
||||
server:
|
||||
address: 0.0.0.0:8642
|
||||
|
||||
log-level: ${LOGLVL}
|
||||
|
||||
prefetch: true
|
||||
|
||||
cache-file: /var/lib/postfix-tlspol/cache.db
|
||||
|
||||
dns:
|
||||
# must support DNSSEC
|
||||
address: 127.0.0.11:53
|
||||
EOF
|
||||
|
||||
/usr/local/bin/postfix-tlspol -config /etc/postfix-tlspol/config.yaml
|
||||
8
data/Dockerfiles/postfix-tlspol/stop-supervisor.sh
Executable file
8
data/Dockerfiles/postfix-tlspol/stop-supervisor.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
printf "READY\n";
|
||||
|
||||
while read line; do
|
||||
echo "Processing Event: $line" >&2;
|
||||
kill -3 $(cat "/var/run/supervisord.pid")
|
||||
done < /dev/stdin
|
||||
25
data/Dockerfiles/postfix-tlspol/supervisord.conf
Normal file
25
data/Dockerfiles/postfix-tlspol/supervisord.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
[supervisord]
|
||||
pidfile=/var/run/supervisord.pid
|
||||
nodaemon=true
|
||||
user=root
|
||||
|
||||
[program:syslog-ng]
|
||||
command=/usr/sbin/syslog-ng --foreground --no-caps
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
autostart=true
|
||||
|
||||
[program:postfix-tlspol]
|
||||
startsecs=10
|
||||
autorestart=true
|
||||
command=/opt/postfix-tlspol.sh
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[eventlistener:processes]
|
||||
command=/usr/local/sbin/stop-supervisor.sh
|
||||
events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL
|
||||
45
data/Dockerfiles/postfix-tlspol/syslog-ng-valkey_slave.conf
Normal file
45
data/Dockerfiles/postfix-tlspol/syslog-ng-valkey_slave.conf
Normal file
@@ -0,0 +1,45 @@
|
||||
@version: 3.38
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
flush_lines(0);
|
||||
use_dns(no);
|
||||
dns_cache(no);
|
||||
use_fqdn(no);
|
||||
owner("root"); group("adm"); perm(0640);
|
||||
stats_freq(0);
|
||||
bad_hostname("^gconfd$");
|
||||
};
|
||||
source s_src {
|
||||
unix-stream("/dev/log");
|
||||
internal();
|
||||
};
|
||||
destination d_stdout { pipe("/dev/stdout"); };
|
||||
destination d_valkey_ui_log {
|
||||
redis(
|
||||
host("`VALKEY_SLAVEOF_IP`")
|
||||
persist-name("valkey1")
|
||||
port(`VALKEY_SLAVEOF_PORT`)
|
||||
auth("`VALKEYPASS`")
|
||||
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
|
||||
);
|
||||
};
|
||||
filter f_mail { facility(mail); };
|
||||
# start
|
||||
# overriding warnings are still displayed when the entrypoint runs its initial check
|
||||
# warnings logged by postfix-mailcow to syslog are hidden to reduce repeating msgs
|
||||
# Some other warnings are ignored
|
||||
filter f_ignore {
|
||||
not match("overriding earlier entry" value("MESSAGE"));
|
||||
not match("TLS SNI from checks.mailcow.email" value("MESSAGE"));
|
||||
not match("no SASL support" value("MESSAGE"));
|
||||
not facility (local0, local1, local2, local3, local4, local5, local6, local7);
|
||||
};
|
||||
# end
|
||||
log {
|
||||
source(s_src);
|
||||
filter(f_ignore);
|
||||
destination(d_stdout);
|
||||
filter(f_mail);
|
||||
destination(d_valkey_ui_log);
|
||||
};
|
||||
45
data/Dockerfiles/postfix-tlspol/syslog-ng.conf
Normal file
45
data/Dockerfiles/postfix-tlspol/syslog-ng.conf
Normal file
@@ -0,0 +1,45 @@
|
||||
@version: 3.38
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
flush_lines(0);
|
||||
use_dns(no);
|
||||
dns_cache(no);
|
||||
use_fqdn(no);
|
||||
owner("root"); group("adm"); perm(0640);
|
||||
stats_freq(0);
|
||||
bad_hostname("^gconfd$");
|
||||
};
|
||||
source s_src {
|
||||
unix-stream("/dev/log");
|
||||
internal();
|
||||
};
|
||||
destination d_stdout { pipe("/dev/stdout"); };
|
||||
destination d_valkey_ui_log {
|
||||
redis(
|
||||
host("valkey-mailcow")
|
||||
persist-name("valkey1")
|
||||
port(6379)
|
||||
auth("`VALKEYPASS`")
|
||||
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
|
||||
);
|
||||
};
|
||||
filter f_mail { facility(mail); };
|
||||
# start
|
||||
# overriding warnings are still displayed when the entrypoint runs its initial check
|
||||
# warnings logged by postfix-mailcow to syslog are hidden to reduce repeating msgs
|
||||
# Some other warnings are ignored
|
||||
filter f_ignore {
|
||||
not match("overriding earlier entry" value("MESSAGE"));
|
||||
not match("TLS SNI from checks.mailcow.email" value("MESSAGE"));
|
||||
not match("no SASL support" value("MESSAGE"));
|
||||
not facility (local0, local1, local2, local3, local4, local5, local6, local7);
|
||||
};
|
||||
# end
|
||||
log {
|
||||
source(s_src);
|
||||
filter(f_ignore);
|
||||
destination(d_stdout);
|
||||
filter(f_mail);
|
||||
destination(d_valkey_ui_log);
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ENV LC_ALL C
|
||||
ENV LC_ALL=C
|
||||
|
||||
RUN dpkg-divert --local --rename --add /sbin/initctl \
|
||||
&& ln -sf /bin/true /sbin/initctl \
|
||||
|
||||
@@ -524,4 +524,4 @@ if [[ $? != 0 ]]; then
|
||||
else
|
||||
postfix -c /opt/postfix/conf start
|
||||
sleep 126144000
|
||||
fi
|
||||
fi
|
||||
@@ -2,11 +2,11 @@ FROM debian:bookworm-slim
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG RSPAMD_VER=rspamd_3.11.0-2~90a175b45
|
||||
ARG RSPAMD_VER=rspamd_3.12.1-1~6dbfca2fa
|
||||
ARG CODENAME=bookworm
|
||||
ENV LC_ALL=C
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
tzdata \
|
||||
ca-certificates \
|
||||
gnupg2 \
|
||||
|
||||
@@ -81,6 +81,29 @@ EOF
|
||||
redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning SLAVEOF NO ONE
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
if [[ -f /etc/rspamd/local.d/external_services.conf ]]; then
|
||||
rm /etc/rspamd/local.d/external_services.conf
|
||||
fi
|
||||
else
|
||||
if [[ ! -f /etc/rspamd/local.d/external_services.conf ]]; then
|
||||
cat <<EOF > /etc/rspamd/local.d/external_services.conf
|
||||
oletools {
|
||||
# default olefy settings
|
||||
servers = "olefy:10055";
|
||||
# needs to be set explicitly for Rspamd < 1.9.5
|
||||
scan_mime_parts = true;
|
||||
# mime-part regex matching in content-type or filename
|
||||
# block all macros
|
||||
extended = true;
|
||||
max_size = 3145728;
|
||||
timeout = 20.0;
|
||||
retransmits = 1;
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
# Provide additional lua modules
|
||||
ln -s /usr/lib/$(uname -m)-linux-gnu/liblua5.1-cjson.so.0.0.0 /usr/lib/rspamd/cjson.so
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
|
||||
COPY syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf
|
||||
COPY supervisord.conf /etc/supervisor/supervisord.conf
|
||||
COPY acl.diff /acl.diff
|
||||
COPY navMailcowBtns.diff /navMailcowBtns.diff
|
||||
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
|
||||
COPY docker-entrypoint.sh /
|
||||
|
||||
|
||||
@@ -14,118 +14,18 @@ do
|
||||
done
|
||||
|
||||
# Wait for updated schema
|
||||
DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
|
||||
DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
|
||||
DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
|
||||
while [[ "${DBV_NOW}" != "${DBV_NEW}" ]]; do
|
||||
echo "Waiting for schema update..."
|
||||
DBV_NOW=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
|
||||
DBV_NOW=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'db_schema';" -BN)
|
||||
DBV_NEW=$(grep -oE '\$db_version = .*;' init_db.inc.php | sed 's/$db_version = //g;s/;//g' | cut -d \" -f2)
|
||||
sleep 5
|
||||
done
|
||||
echo "DB schema is ${DBV_NOW}"
|
||||
|
||||
# Recreate view
|
||||
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo "We are master, preparing sogo_view..."
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS sogo_view"
|
||||
while [[ ${VIEW_OK} != 'OK' ]]; do
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings) AS
|
||||
SELECT
|
||||
mailbox.username,
|
||||
mailbox.domain,
|
||||
mailbox.username,
|
||||
IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.force_pw_update')) = '0', IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.sogo_access')) = 1, password, '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'), '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'),
|
||||
mailbox.name,
|
||||
mailbox.username,
|
||||
IFNULL(GROUP_CONCAT(ga.aliases ORDER BY ga.aliases SEPARATOR ' '), ''),
|
||||
IFNULL(gda.ad_alias, ''),
|
||||
IFNULL(external_acl.send_as_acl, ''),
|
||||
mailbox.kind,
|
||||
mailbox.multiple_bookings
|
||||
FROM
|
||||
mailbox
|
||||
LEFT OUTER JOIN
|
||||
grouped_mail_aliases ga
|
||||
ON ga.username REGEXP CONCAT('(^|,)', mailbox.username, '($|,)')
|
||||
LEFT OUTER JOIN
|
||||
grouped_domain_alias_address gda
|
||||
ON gda.username = mailbox.username
|
||||
LEFT OUTER JOIN
|
||||
grouped_sender_acl_external external_acl
|
||||
ON external_acl.username = mailbox.username
|
||||
WHERE
|
||||
mailbox.active = '1'
|
||||
GROUP BY
|
||||
mailbox.username;
|
||||
EOF
|
||||
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view'") ]]; then
|
||||
VIEW_OK=OK
|
||||
else
|
||||
echo "Will retry to setup SOGo view in 3s..."
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
else
|
||||
while [[ ${VIEW_OK} != 'OK' ]]; do
|
||||
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view'") ]]; then
|
||||
VIEW_OK=OK
|
||||
else
|
||||
echo "Waiting for SOGo view to be created by master..."
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Wait for static view table if missing after update and update content
|
||||
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo "We are master, preparing _sogo_static_view..."
|
||||
while [[ ${STATIC_VIEW_OK} != 'OK' ]]; do
|
||||
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then
|
||||
STATIC_VIEW_OK=OK
|
||||
echo "Updating _sogo_static_view content..."
|
||||
# If changed, also update init_db.inc.php
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "REPLACE INTO _sogo_static_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings) SELECT c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings from sogo_view;"
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "DELETE FROM _sogo_static_view WHERE c_uid NOT IN (SELECT username FROM mailbox WHERE active = '1')"
|
||||
else
|
||||
echo "Waiting for database initialization..."
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
else
|
||||
while [[ ${STATIC_VIEW_OK} != 'OK' ]]; do
|
||||
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then
|
||||
STATIC_VIEW_OK=OK
|
||||
else
|
||||
echo "Waiting for database initialization by master..."
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Recreate password update trigger
|
||||
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo "We are master, preparing update trigger..."
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TRIGGER IF EXISTS sogo_update_password"
|
||||
while [[ ${TRIGGER_OK} != 'OK' ]]; do
|
||||
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
|
||||
DELIMITER -
|
||||
CREATE TRIGGER sogo_update_password AFTER UPDATE ON _sogo_static_view
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE mailbox SET password = NEW.c_password WHERE NEW.c_uid = username;
|
||||
END;
|
||||
-
|
||||
DELIMITER ;
|
||||
EOF
|
||||
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_NAME = 'sogo_update_password'") ]]; then
|
||||
TRIGGER_OK=OK
|
||||
else
|
||||
echo "Will retry to setup SOGo password update trigger in 3s"
|
||||
sleep 3
|
||||
fi
|
||||
done
|
||||
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TRIGGER IF EXISTS sogo_update_password"
|
||||
fi
|
||||
|
||||
# cat /dev/urandom seems to hang here occasionally and is not recommended anyway, better use openssl
|
||||
@@ -150,6 +50,10 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
<string>YES</string>
|
||||
<key>SOGoEncryptionKey</key>
|
||||
<string>${RAND_PASS}</string>
|
||||
<key>SOGoURLEncryptionEnabled</key>
|
||||
<string>YES</string>
|
||||
<key>SOGoURLEncryptionPassphrase</key>
|
||||
<string>${SOGO_URL_ENCRYPTION_KEY}</string>
|
||||
<key>OCSAdminURL</key>
|
||||
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
|
||||
<key>OCSCacheFolderURL</key>
|
||||
@@ -213,10 +117,10 @@ while read -r line gal
|
||||
</dict>" >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
# Generate alternative LDAP authentication dict, when SQL authentication fails
|
||||
# This will nevertheless read attributes from LDAP
|
||||
line=${line} envsubst < /etc/sogo/plist_ldap >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
/etc/sogo/plist_ldap.sh ${line} ${gal} >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
echo " </array>
|
||||
</dict>" >> /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
done < <(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain, CASE gal WHEN '1' THEN 'YES' ELSE 'NO' END AS gal FROM domain;" -B -N)
|
||||
done < <(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain, CASE gal WHEN '1' THEN 'YES' ELSE 'NO' END AS gal FROM domain;" -B -N)
|
||||
|
||||
# Generate footer
|
||||
echo ' </dict>
|
||||
@@ -240,6 +144,10 @@ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
|
||||
# fi
|
||||
#fi
|
||||
|
||||
if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then
|
||||
patch -R /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
|
||||
fi
|
||||
|
||||
# Rename custom logo, if any
|
||||
[[ -f /etc/sogo/sogo-full.svg ]] && mv /etc/sogo/sogo-full.svg /etc/sogo/custom-fulllogo.svg
|
||||
|
||||
|
||||
15
data/Dockerfiles/sogo/navMailcowBtns.diff
Normal file
15
data/Dockerfiles/sogo/navMailcowBtns.diff
Normal file
@@ -0,0 +1,15 @@
|
||||
60,65d58
|
||||
< var:ng-click="navButtonClick"
|
||||
< ng-href="/user">
|
||||
< <md-icon>build</md-icon>
|
||||
< <md-tooltip>mailcow <var:string label:value="Preferences"/></md-tooltip>
|
||||
< </md-button>
|
||||
< <md-button class="md-icon-button"
|
||||
83c76
|
||||
< onclick="mc_logout();"
|
||||
---
|
||||
> ng-show="::activeUser.path.logoff.length"
|
||||
85c78
|
||||
< ng-href="#">
|
||||
---
|
||||
> ng-href="{{::activeUser.path.logoff}}">
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:3.20
|
||||
FROM alpine:3.21
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
@@ -16,7 +16,6 @@ RUN apk add --update \
|
||||
fcgi \
|
||||
openssl \
|
||||
nagios-plugins-mysql \
|
||||
nagios-plugins-dns \
|
||||
nagios-plugins-disk \
|
||||
bind-tools \
|
||||
redis \
|
||||
@@ -32,9 +31,11 @@ RUN apk add --update \
|
||||
tzdata \
|
||||
whois \
|
||||
&& curl https://raw.githubusercontent.com/mludvig/smtp-cli/v3.10/smtp-cli -o /smtp-cli \
|
||||
&& chmod +x smtp-cli
|
||||
&& chmod +x smtp-cli \
|
||||
&& mkdir /usr/lib/mailcow
|
||||
|
||||
COPY watchdog.sh /watchdog.sh
|
||||
COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
|
||||
COPY check_dns.sh /usr/lib/mailcow/check_dns.sh
|
||||
|
||||
CMD ["/watchdog.sh"]
|
||||
|
||||
39
data/Dockerfiles/watchdog/check_dns.sh
Executable file
39
data/Dockerfiles/watchdog/check_dns.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
|
||||
while getopts "H:s:" opt; do
|
||||
case "$opt" in
|
||||
H) HOST="$OPTARG" ;;
|
||||
s) SERVER="$OPTARG" ;;
|
||||
*) echo "Usage: $0 -H host -s server"; exit 3 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$SERVER" ]; then
|
||||
echo "No DNS Server provided"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if [ -z "$HOST" ]; then
|
||||
echo "No host to test provided"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# run dig and measure the time it takes to run
|
||||
START_TIME=$(date +%s%3N)
|
||||
dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null)
|
||||
dig_rc=$?
|
||||
dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -)
|
||||
END_TIME=$(date +%s%3N)
|
||||
ELAPSED_TIME=$((END_TIME - START_TIME))
|
||||
|
||||
# validate and perform nagios like output and exit codes
|
||||
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
|
||||
echo "Domain $HOST was not found by the server"
|
||||
exit 2
|
||||
elif [ $dig_rc -eq 0 ]; then
|
||||
echo "DNS OK: $ELAPSED_TIME ms response time. $HOST returns $dig_output_ips"
|
||||
exit 0
|
||||
else
|
||||
echo "Unknown error"
|
||||
exit 3
|
||||
fi
|
||||
@@ -49,7 +49,7 @@
|
||||
# 2013101601 Optical clean up #
|
||||
# 2013101602 Rewrite help output #
|
||||
# 2013101700 Handle Slave IO in 'Connecting' state #
|
||||
# 2013101701 Minor changes in output, handling UNKWNON situations now #
|
||||
# 2013101701 Minor changes in output, handling UNKNOWN situations now #
|
||||
# 2013101702 Exit CRITICAL when Slave IO in Connecting state #
|
||||
# 2013123000 Slave_SQL_Running also matched Slave_SQL_Running_State #
|
||||
# 2015011600 Added 'moving' check to catch possible connection issues #
|
||||
@@ -131,10 +131,10 @@ elif [[ -n "${socket}" && (-z "${user}" || -z "${password}") ]]; then
|
||||
fi
|
||||
|
||||
# Connect to the DB server and store output in vars
|
||||
if [[ -n $socket ]]; then
|
||||
ConnectionResult=$(mysql ${optfile} ${socket} ${user} -e "show slave ${connection} status\G" 2>&1)
|
||||
if [[ -n $socket ]]; then
|
||||
ConnectionResult=$(mariadb --skip-ssl ${optfile} ${socket} ${user} -e "show slave ${connection} status\G" 2>&1)
|
||||
else
|
||||
ConnectionResult=$(mysql ${optfile} ${host} ${port} ${user} -e "show slave ${connection} status\G" 2>&1)
|
||||
ConnectionResult=$(mariadb --skip-ssl ${optfile} ${host} ${port} ${user} -e "show slave ${connection} status\G" 2>&1)
|
||||
fi
|
||||
|
||||
if [ -z "`echo "${ConnectionResult}" |grep Slave_IO_State`" ]; then
|
||||
@@ -178,33 +178,33 @@ if [ ${check} = ${ok} ] && [ ${checkio} = ${ok} ]; then
|
||||
then echo "CRITICAL: Slave is ${delayinfo} seconds behind Master | delay=${delayinfo}s"; exit ${STATE_CRITICAL}
|
||||
elif [[ ${delayinfo} -ge ${warn_delay} ]]
|
||||
then echo "WARNING: Slave is ${delayinfo} seconds behind Master | delay=${delayinfo}s"; exit ${STATE_WARNING}
|
||||
else
|
||||
else
|
||||
# Everything looks OK here but now let us check if the replication is moving
|
||||
if [[ -n ${moving} ]] && [[ -n ${tmpfile} ]] && [[ $readpos -eq $execpos ]]
|
||||
then
|
||||
#echo "Debug: Read pos is $readpos - Exec pos is $execpos"
|
||||
then
|
||||
#echo "Debug: Read pos is $readpos - Exec pos is $execpos"
|
||||
# Check if tmp file exists
|
||||
curtime=`date +%s`
|
||||
if [[ -w $tmpfile ]]
|
||||
then
|
||||
if [[ -w $tmpfile ]]
|
||||
then
|
||||
tmpfiletime=`date +%s -r $tmpfile`
|
||||
if [[ `expr $curtime - $tmpfiletime` -gt ${moving} ]]
|
||||
then
|
||||
exectmp=`cat $tmpfile`
|
||||
#echo "Debug: Exec pos in tmpfile is $exectmp"
|
||||
if [[ $exectmp -eq $execpos ]]
|
||||
then
|
||||
then
|
||||
# The value read from the tmp file and from db are the same. Replication hasnt moved!
|
||||
echo "WARNING: Slave replication has not moved in ${moving} seconds. Manual check required."; exit ${STATE_WARNING}
|
||||
else
|
||||
else
|
||||
# Replication has moved since the tmp file was written. Delete tmp file and output OK.
|
||||
rm $tmpfile
|
||||
echo "OK: Slave SQL running: ${check} Slave IO running: ${checkio} / master: ${masterinfo} / slave is ${delayinfo} seconds behind master | delay=${delayinfo}s"; exit ${STATE_OK};
|
||||
fi
|
||||
else
|
||||
else
|
||||
echo "OK: Slave SQL running: ${check} Slave IO running: ${checkio} / master: ${masterinfo} / slave is ${delayinfo} seconds behind master | delay=${delayinfo}s"; exit ${STATE_OK};
|
||||
fi
|
||||
else
|
||||
else
|
||||
echo "$execpos" > $tmpfile
|
||||
echo "OK: Slave SQL running: ${check} Slave IO running: ${checkio} / master: ${masterinfo} / slave is ${delayinfo} seconds behind master | delay=${delayinfo}s"; exit ${STATE_OK};
|
||||
fi
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ "${DEV_MODE}" != "n" ]; then
|
||||
echo -e "\e[31mEnabled Debug Mode\e[0m"
|
||||
set -x
|
||||
fi
|
||||
|
||||
trap "exit" INT TERM
|
||||
trap "kill 0" EXIT
|
||||
|
||||
@@ -234,7 +239,7 @@ external_checks() {
|
||||
diff_c=0
|
||||
THRESHOLD=${EXTERNAL_CHECKS_THRESHOLD}
|
||||
# Reduce error count by 2 after restarting an unhealthy container
|
||||
GUID=$(mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'GUID'" -BN)
|
||||
GUID=$(mariadb --skip-ssl -u${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT version FROM versions WHERE application = 'GUID'" -BN)
|
||||
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
|
||||
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
||||
err_c_cur=${err_count}
|
||||
@@ -297,7 +302,7 @@ unbound_checks() {
|
||||
touch /tmp/unbound-mailcow; echo "$(tail -50 /tmp/unbound-mailcow)" > /tmp/unbound-mailcow
|
||||
host_ip=$(get_container_ip unbound-mailcow)
|
||||
err_c_cur=${err_count}
|
||||
/usr/lib/nagios/plugins/check_dns -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
/usr/lib/mailcow/check_dns.sh -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
DNSSEC=$(dig com +dnssec | egrep 'flags:.+ad')
|
||||
if [[ -z ${DNSSEC} ]]; then
|
||||
echo "DNSSEC failure" 2>> /tmp/unbound-mailcow 1>&2
|
||||
@@ -445,6 +450,31 @@ postfix_checks() {
|
||||
return 1
|
||||
}
|
||||
|
||||
postfix-tlspol_checks() {
|
||||
err_count=0
|
||||
diff_c=0
|
||||
THRESHOLD=${POSTFIX_TLSPOL_THRESHOLD}
|
||||
# Reduce error count by 2 after restarting an unhealthy container
|
||||
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
|
||||
while [ ${err_count} -lt ${THRESHOLD} ]; do
|
||||
touch /tmp/postfix-tlspol-mailcow; echo "$(tail -50 /tmp/postfix-tlspol-mailcow)" > /tmp/postfix-tlspol-mailcow
|
||||
host_ip=$(get_container_ip postfix-tlspol-mailcow)
|
||||
err_c_cur=${err_count}
|
||||
/usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 8642 2>> /tmp/postfix-tlspol-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
|
||||
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
|
||||
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
|
||||
progress "Postfix TLS Policy companion" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
|
||||
if [[ $? == 10 ]]; then
|
||||
diff_c=0
|
||||
sleep 1
|
||||
else
|
||||
diff_c=0
|
||||
sleep $(( ( RANDOM % 60 ) + 20 ))
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
clamd_checks() {
|
||||
err_count=0
|
||||
diff_c=0
|
||||
@@ -922,6 +952,18 @@ PID=$!
|
||||
echo "Spawned mailq_checks with PID ${PID}"
|
||||
BACKGROUND_TASKS+=(${PID})
|
||||
|
||||
(
|
||||
while true; do
|
||||
if ! postfix-tlspol_checks; then
|
||||
log_msg "Postfix TLS Policy hit error limit"
|
||||
echo postfix-tlspol-mailcow > /tmp/com_pipe
|
||||
fi
|
||||
done
|
||||
) &
|
||||
PID=$!
|
||||
echo "Spawned postfix-tlspol_checks with PID ${PID}"
|
||||
BACKGROUND_TASKS+=(${PID})
|
||||
|
||||
(
|
||||
while true; do
|
||||
if ! dovecot_checks; then
|
||||
@@ -994,6 +1036,7 @@ PID=$!
|
||||
echo "Spawned cert_checks with PID ${PID}"
|
||||
BACKGROUND_TASKS+=(${PID})
|
||||
|
||||
if [[ "${SKIP_OLEFY}" =~ ^([nN][oO]|[nN])+$ ]]; then
|
||||
(
|
||||
while true; do
|
||||
if ! olefy_checks; then
|
||||
@@ -1005,6 +1048,7 @@ done
|
||||
PID=$!
|
||||
echo "Spawned olefy_checks with PID ${PID}"
|
||||
BACKGROUND_TASKS+=(${PID})
|
||||
fi
|
||||
|
||||
(
|
||||
while true; do
|
||||
|
||||
115
data/conf/dovecot/auth/mailcowauth.php
Normal file
115
data/conf/dovecot/auth/mailcowauth.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
ini_set('error_reporting', 0);
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$post = trim(file_get_contents('php://input'));
|
||||
if ($post) {
|
||||
$post = json_decode($post, true);
|
||||
}
|
||||
|
||||
|
||||
$return = array("success" => false);
|
||||
if(!isset($post['username']) || !isset($post['password']) || !isset($post['real_rip'])){
|
||||
error_log("MAILCOWAUTH: Bad Request");
|
||||
http_response_code(400); // Bad Request
|
||||
echo json_encode($return);
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once('../../../web/inc/vars.inc.php');
|
||||
if (file_exists('../../../web/inc/vars.local.inc.php')) {
|
||||
include_once('../../../web/inc/vars.local.inc.php');
|
||||
}
|
||||
require_once '../../../web/inc/lib/vendor/autoload.php';
|
||||
|
||||
|
||||
// Init Valkey
|
||||
$valkey = new Redis();
|
||||
try {
|
||||
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
|
||||
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
|
||||
}
|
||||
else {
|
||||
$valkey->connect('valkey-mailcow', 6379);
|
||||
}
|
||||
$valkey->auth(getenv("VALKEYPASS"));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
error_log("MAILCOWAUTH: " . $e . PHP_EOL);
|
||||
http_response_code(500); // Internal Server Error
|
||||
echo json_encode($return);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Init database
|
||||
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
try {
|
||||
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
error_log("MAILCOWAUTH: " . $e . PHP_EOL);
|
||||
http_response_code(500); // Internal Server Error
|
||||
echo json_encode($return);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Load core functions first
|
||||
require_once 'functions.inc.php';
|
||||
require_once 'functions.auth.inc.php';
|
||||
require_once 'sessions.inc.php';
|
||||
require_once 'functions.mailbox.inc.php';
|
||||
require_once 'functions.ratelimit.inc.php';
|
||||
require_once 'functions.acl.inc.php';
|
||||
|
||||
|
||||
$isSOGoRequest = $post['real_rip'] == getenv('IPV4_NETWORK') . '.248';
|
||||
$result = false;
|
||||
if ($isSOGoRequest) {
|
||||
// This is a SOGo Auth request. First check for SSO password.
|
||||
$sogo_sso_pass = file_get_contents("/etc/sogo-sso/sogo-sso.pass");
|
||||
if ($sogo_sso_pass === $post['password']){
|
||||
error_log('MAILCOWAUTH: SOGo SSO auth for user ' . $post['username']);
|
||||
set_sasl_log($post['username'], $post['real_rip'], "SOGO");
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
if ($result === false){
|
||||
// If it's a SOGo Request, don't check for protocol access
|
||||
$service = ($isSOGoRequest) ? false : array($post['service'] => true);
|
||||
$result = apppass_login($post['username'], $post['password'], $service, array(
|
||||
'is_internal' => true,
|
||||
'remote_addr' => $post['real_rip']
|
||||
));
|
||||
if ($result) {
|
||||
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
|
||||
set_sasl_log($post['username'], $post['real_rip'], $post['service']);
|
||||
}
|
||||
}
|
||||
if ($result === false){
|
||||
// Init Identity Provider
|
||||
$iam_provider = identity_provider('init');
|
||||
$iam_settings = identity_provider('get');
|
||||
$result = user_login($post['username'], $post['password'], array('is_internal' => true, 'service' => $post['service']));
|
||||
if ($result) {
|
||||
error_log('MAILCOWAUTH: User auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
|
||||
set_sasl_log($post['username'], $post['real_rip'], $post['service']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
http_response_code(200); // OK
|
||||
$return['success'] = true;
|
||||
} else {
|
||||
error_log("MAILCOWAUTH: Login failed for user " . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
|
||||
http_response_code(401); // Unauthorized
|
||||
}
|
||||
|
||||
|
||||
echo json_encode($return);
|
||||
session_destroy();
|
||||
exit;
|
||||
57
data/conf/dovecot/auth/passwd-verify.lua
Normal file
57
data/conf/dovecot/auth/passwd-verify.lua
Normal file
@@ -0,0 +1,57 @@
|
||||
function auth_password_verify(request, password)
|
||||
if request.domain == nil then
|
||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
|
||||
end
|
||||
|
||||
local json = require "cjson"
|
||||
local ltn12 = require "ltn12"
|
||||
local https = require "ssl.https"
|
||||
https.TIMEOUT = 30
|
||||
|
||||
local req = {
|
||||
username = request.user,
|
||||
password = password,
|
||||
real_rip = request.real_rip,
|
||||
service = request.service
|
||||
}
|
||||
local req_json = json.encode(req)
|
||||
local res = {}
|
||||
|
||||
local b, c = https.request {
|
||||
method = "POST",
|
||||
url = "https://nginx:9082",
|
||||
source = ltn12.source.string(req_json),
|
||||
headers = {
|
||||
["content-type"] = "application/json",
|
||||
["content-length"] = tostring(#req_json)
|
||||
},
|
||||
sink = ltn12.sink.table(res),
|
||||
insecure = true
|
||||
}
|
||||
|
||||
-- Returning PASSDB_RESULT_PASSWORD_MISMATCH will reset the user's auth cache entry.
|
||||
-- Returning PASSDB_RESULT_INTERNAL_FAILURE keeps the existing cache entry,
|
||||
-- even if the TTL has expired. Useful to avoid cache eviction during backend issues.
|
||||
if c ~= 200 and c ~= 401 then
|
||||
dovecot.i_info("HTTP request failed with " .. c .. " for user " .. request.user)
|
||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error"
|
||||
end
|
||||
|
||||
local response_str = table.concat(res)
|
||||
local is_response_valid, response_json = pcall(json.decode, response_str)
|
||||
|
||||
if not is_response_valid then
|
||||
dovecot.i_info("Invalid JSON received: " .. response_str)
|
||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Invalid response format"
|
||||
end
|
||||
|
||||
if response_json.success == true then
|
||||
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||
end
|
||||
|
||||
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
|
||||
end
|
||||
|
||||
function auth_passdb_lookup(req)
|
||||
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
|
||||
end
|
||||
@@ -53,7 +53,7 @@ mail_shared_explicit_inbox = yes
|
||||
mail_prefetch_count = 30
|
||||
passdb {
|
||||
driver = lua
|
||||
args = file=/etc/dovecot/lua/passwd-verify.lua blocking=yes
|
||||
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes cache_key=%s:%u:%w
|
||||
result_success = return-ok
|
||||
result_failure = continue
|
||||
result_internalfail = continue
|
||||
@@ -69,7 +69,7 @@ passdb {
|
||||
# a return of the following passdb is mandatory
|
||||
passdb {
|
||||
driver = lua
|
||||
args = file=/etc/dovecot/lua/passwd-verify.lua blocking=yes
|
||||
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes
|
||||
}
|
||||
# Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
|
||||
service doveadm {
|
||||
@@ -125,6 +125,7 @@ service managesieve-login {
|
||||
}
|
||||
service imap-login {
|
||||
service_count = 1
|
||||
process_min_avail = 2
|
||||
process_limit = 10000
|
||||
vsz_limit = 1G
|
||||
user = dovenull
|
||||
@@ -140,6 +141,7 @@ service imap-login {
|
||||
}
|
||||
service pop3-login {
|
||||
service_count = 1
|
||||
process_min_avail = 1
|
||||
vsz_limit = 1G
|
||||
inet_listener pop3_haproxy {
|
||||
port = 10110
|
||||
@@ -239,7 +241,7 @@ plugin {
|
||||
mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem
|
||||
mail_crypt_save_version = 2
|
||||
|
||||
# Enable compression while saving, lz4 Dovecot v2.2.11+
|
||||
# Enable compression while saving, lz4 Dovecot v2.3.17+
|
||||
zlib_save = lz4
|
||||
|
||||
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
|
||||
@@ -274,10 +276,10 @@ service stats {
|
||||
}
|
||||
}
|
||||
imap_max_line_length = 2 M
|
||||
#auth_cache_verify_password_with_worker = yes
|
||||
#auth_cache_negative_ttl = 0
|
||||
#auth_cache_ttl = 30 s
|
||||
#auth_cache_size = 2 M
|
||||
auth_cache_verify_password_with_worker = yes
|
||||
auth_cache_negative_ttl = 60s
|
||||
auth_cache_ttl = 300s
|
||||
auth_cache_size = 10M
|
||||
auth_verbose_passwords = sha1:6
|
||||
service replicator {
|
||||
process_min_avail = 1
|
||||
@@ -302,7 +304,6 @@ replication_dsync_parameters = -d -l 30 -U -n INBOX
|
||||
!include_try /etc/dovecot/sni.conf
|
||||
!include_try /etc/dovecot/sogo_trusted_ip.conf
|
||||
!include_try /etc/dovecot/extra.conf
|
||||
!include_try /etc/dovecot/sogo-sso.conf
|
||||
!include_try /etc/dovecot/shared_namespace.conf
|
||||
!include_try /etc/dovecot/conf.d/fts.conf
|
||||
# </Includes>
|
||||
|
||||
@@ -48,13 +48,21 @@ http {
|
||||
listen {{ HTTP_PORT }} default_server;
|
||||
listen [::]:{{ HTTP_PORT }} default_server;
|
||||
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
|
||||
|
||||
if ( $request_uri ~* "%0A|%0D" ) { return 403; }
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
allow all;
|
||||
default_type "text/plain";
|
||||
}
|
||||
location ^~ /.well-known/mta-sts.txt {
|
||||
allow all;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass {{ PHPFPMHOST }}:9002;
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/mta-sts.php;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
location / {
|
||||
return 301 https://$host$uri$is_args$args;
|
||||
}
|
||||
@@ -70,7 +78,7 @@ http {
|
||||
{%endif%}
|
||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||
|
||||
{% if not DISABLE_IPv6 %}
|
||||
{% if ENABLE_IPV6 %}
|
||||
{% if not HTTP_REDIRECT %}
|
||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||
{%endif%}
|
||||
@@ -82,7 +90,7 @@ http {
|
||||
ssl_certificate /etc/ssl/mail/cert.pem;
|
||||
ssl_certificate_key /etc/ssl/mail/key.pem;
|
||||
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.*;
|
||||
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.*;
|
||||
|
||||
include /etc/nginx/includes/sites-default.conf;
|
||||
}
|
||||
@@ -97,7 +105,7 @@ http {
|
||||
{%endif%}
|
||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||
|
||||
{% if not DISABLE_IPv6 %}
|
||||
{% if ENABLE_IPV6 %}
|
||||
{% if not HTTP_REDIRECT %}
|
||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||
{%endif%}
|
||||
@@ -118,7 +126,7 @@ http {
|
||||
# rspamd dynmaps:
|
||||
server {
|
||||
listen 8081;
|
||||
{% if not DISABLE_IPv6 %}
|
||||
{% if ENABLE_IPV6 %}
|
||||
listen [::]:8081;
|
||||
{%endif%}
|
||||
index index.php index.html;
|
||||
@@ -159,6 +167,31 @@ http {
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 9082 ssl http2;
|
||||
|
||||
ssl_certificate /etc/ssl/mail/cert.pem;
|
||||
ssl_certificate_key /etc/ssl/mail/key.pem;
|
||||
|
||||
index mailcowauth.php;
|
||||
server_name _;
|
||||
error_log /var/log/nginx/error.log;
|
||||
access_log /var/log/nginx/access.log;
|
||||
root /mailcowauth;
|
||||
client_max_body_size 10M;
|
||||
location ~ \.php$ {
|
||||
client_max_body_size 10M;
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass phpfpm:9001;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
|
||||
{% for cert in valid_cert_dirs %}
|
||||
server {
|
||||
{% if not HTTP_REDIRECT %}
|
||||
@@ -166,7 +199,7 @@ http {
|
||||
{%endif%}
|
||||
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
|
||||
|
||||
{% if not DISABLE_IPv6 %}
|
||||
{% if ENABLE_IPV6 %}
|
||||
{% if not HTTP_REDIRECT %}
|
||||
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
|
||||
{%endif%}
|
||||
@@ -183,6 +216,4 @@ http {
|
||||
include /etc/nginx/includes/sites-default.conf;
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
|
||||
@@ -76,6 +76,14 @@ location ^~ /.well-known/acme-challenge/ {
|
||||
allow all;
|
||||
default_type "text/plain";
|
||||
}
|
||||
location ^~ /.well-known/mta-sts.txt {
|
||||
allow all;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass {{ PHPFPMHOST }}:9002;
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/mta-sts.php;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
|
||||
rewrite ^/.well-known/caldav$ /SOGo/dav/ permanent;
|
||||
rewrite ^/.well-known/carddav$ /SOGo/dav/ permanent;
|
||||
|
||||
231
data/conf/phpfpm/crons/keycloak-sync.php
Normal file
231
data/conf/phpfpm/crons/keycloak-sync.php
Normal file
@@ -0,0 +1,231 @@
|
||||
<?php
|
||||
|
||||
require_once(__DIR__ . '/../web/inc/vars.inc.php');
|
||||
if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
|
||||
include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
|
||||
}
|
||||
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
|
||||
|
||||
// Init database
|
||||
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
|
||||
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
try {
|
||||
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
logMsg("err", $e->getMessage());
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Init Valkey
|
||||
$valkey = new Redis();
|
||||
try {
|
||||
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
|
||||
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
|
||||
}
|
||||
else {
|
||||
$valkey->connect('valkey-mailcow', 6379);
|
||||
}
|
||||
$valkey->auth(getenv("VALKEYPASS"));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
echo "Exiting: " . $e->getMessage();
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
function logMsg($priority, $message, $task = "Keycloak Sync") {
|
||||
global $valkey;
|
||||
|
||||
$finalMsg = array(
|
||||
"time" => time(),
|
||||
"priority" => $priority,
|
||||
"task" => $task,
|
||||
"message" => $message
|
||||
);
|
||||
$valkey->lPush('CRON_LOG', json_encode($finalMsg));
|
||||
}
|
||||
|
||||
// Load core functions first
|
||||
require_once __DIR__ . '/../web/inc/functions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/sessions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.ratelimit.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.acl.inc.php';
|
||||
|
||||
$_SESSION['mailcow_cc_username'] = "admin";
|
||||
$_SESSION['mailcow_cc_role'] = "admin";
|
||||
$_SESSION['acl']['tls_policy'] = "1";
|
||||
$_SESSION['acl']['quarantine_notification'] = "1";
|
||||
$_SESSION['acl']['quarantine_category'] = "1";
|
||||
$_SESSION['acl']['ratelimit'] = "1";
|
||||
$_SESSION['acl']['sogo_access'] = "1";
|
||||
$_SESSION['acl']['protocol_access'] = "1";
|
||||
$_SESSION['acl']['mailbox_relayhost'] = "1";
|
||||
$_SESSION['acl']['unlimited_quota'] = "1";
|
||||
|
||||
$iam_settings = identity_provider('get');
|
||||
if ($iam_settings['authsource'] != "keycloak" || (intval($iam_settings['periodic_sync']) != 1 && intval($iam_settings['import_users']) != 1)) {
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set pagination variables
|
||||
$start = 0;
|
||||
$max = 100;
|
||||
|
||||
// lock sync if already running
|
||||
$lock_file = '/tmp/iam-sync.lock';
|
||||
if (file_exists($lock_file)) {
|
||||
$lock_file_parts = explode("\n", file_get_contents($lock_file));
|
||||
$pid = $lock_file_parts[0];
|
||||
if (count($lock_file_parts) > 1){
|
||||
$last_execution = $lock_file_parts[1];
|
||||
$elapsed_time = (time() - $last_execution) / 60;
|
||||
if ($elapsed_time < intval($iam_settings['sync_interval'])) {
|
||||
logMsg("warning", "Sync not ready (".number_format((float)$elapsed_time, 2, '.', '')."min / ".$iam_settings['sync_interval']."min)");
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (posix_kill($pid, 0)) {
|
||||
logMsg("warning", "Sync is already running");
|
||||
session_destroy();
|
||||
exit;
|
||||
} else {
|
||||
unlink($lock_file);
|
||||
}
|
||||
}
|
||||
$lock_file_handle = fopen($lock_file, 'w');
|
||||
fwrite($lock_file_handle, getmypid());
|
||||
fclose($lock_file_handle);
|
||||
|
||||
// Init Keycloak Provider
|
||||
$iam_provider = identity_provider('init');
|
||||
|
||||
// Loop until all users have been retrieved
|
||||
while (true) {
|
||||
// Get admin access token
|
||||
$admin_token = identity_provider("get-keycloak-admin-token");
|
||||
|
||||
// Make the API request to retrieve the users
|
||||
$url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users?first=$start&max=$max";
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
"Content-Type: application/json",
|
||||
"Authorization: Bearer " . $admin_token
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($code != 200){
|
||||
logMsg("err", "Received HTTP {$code}");
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
try {
|
||||
$response = json_decode($response, true);
|
||||
} catch (Exception $e) {
|
||||
logMsg("err", $e->getMessage());
|
||||
break;
|
||||
}
|
||||
if (!is_array($response)){
|
||||
logMsg("err", "Received malformed response from keycloak api");
|
||||
break;
|
||||
}
|
||||
if (count($response) == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Process the batch of users
|
||||
foreach ($response as $user) {
|
||||
if (empty($user['email'])){
|
||||
logMsg("warning", "No email address in keycloak found for user " . $user['name']);
|
||||
continue;
|
||||
}
|
||||
|
||||
// try get mailbox user
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
mailbox.*,
|
||||
domain.active AS d_active
|
||||
FROM `mailbox`
|
||||
INNER JOIN domain on mailbox.domain = domain.domain
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $user['email']));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// check if matching attribute mapping exists
|
||||
$user_template = $user['attributes']['mailcow_template'][0];
|
||||
$mapper_key = array_search($user_template, $iam_settings['mappers']);
|
||||
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
if (!$row && intval($iam_settings['import_users']) == 1){
|
||||
if ($mapper_key === false){
|
||||
if (!empty($iam_settings['default_template'])) {
|
||||
$mbox_template = $iam_settings['default_template'];
|
||||
logMsg("warning", "Using default template for user " . $user['email']);
|
||||
} else {
|
||||
logMsg("warning", "No matching attribute mapping found for user " . $user['email']);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
}
|
||||
// mailbox user does not exist, create...
|
||||
logMsg("info", "Creating user " . $user['email']);
|
||||
$create_res = mailbox('add', 'mailbox_from_template', array(
|
||||
'domain' => explode('@', $user['email'])[1],
|
||||
'local_part' => explode('@', $user['email'])[0],
|
||||
'name' => $user['firstName'] . " " . $user['lastName'],
|
||||
'authsource' => 'keycloak',
|
||||
'template' => $mbox_template
|
||||
));
|
||||
if (!$create_res){
|
||||
logMsg("err", "Could not create user " . $user['email']);
|
||||
continue;
|
||||
}
|
||||
} else if ($row && intval($iam_settings['periodic_sync']) == 1 && $row['authsource'] == "keycloak") {
|
||||
if ($mapper_key === false){
|
||||
logMsg("warning", "No matching attribute mapping found for user " . $user['email']);
|
||||
continue;
|
||||
}
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
// mailbox user does exist, sync attribtues...
|
||||
logMsg("info", "Syncing attributes for user " . $user['email']);
|
||||
mailbox('edit', 'mailbox_from_template', array(
|
||||
'username' => $user['email'],
|
||||
'name' => $user['firstName'] . " " . $user['lastName'],
|
||||
'template' => $mbox_template
|
||||
));
|
||||
} else {
|
||||
// skip mailbox user
|
||||
logMsg("info", "Skipping user " . $user['email']);
|
||||
}
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
|
||||
sleep(0.025);
|
||||
}
|
||||
|
||||
// Update the pagination variables for the next batch
|
||||
$start += $max;
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
logMsg("info", "DONE!");
|
||||
// add last execution time to lock file
|
||||
$lock_file_handle = fopen($lock_file, 'w');
|
||||
fwrite($lock_file_handle, getmypid() . "\n" . time());
|
||||
fclose($lock_file_handle);
|
||||
session_destroy();
|
||||
198
data/conf/phpfpm/crons/ldap-sync.php
Normal file
198
data/conf/phpfpm/crons/ldap-sync.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
require_once(__DIR__ . '/../web/inc/vars.inc.php');
|
||||
if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
|
||||
include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
|
||||
}
|
||||
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
|
||||
|
||||
// Init database
|
||||
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
|
||||
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
try {
|
||||
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
||||
}
|
||||
catch (PDOException $e) {
|
||||
logMsg("err", $e->getMessage());
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Init Valkey
|
||||
$valkey = new Redis();
|
||||
try {
|
||||
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
|
||||
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
|
||||
}
|
||||
else {
|
||||
$valkey->connect('valkey-mailcow', 6379);
|
||||
}
|
||||
$valkey->auth(getenv("VALKEYPASS"));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
echo "Exiting: " . $e->getMessage();
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
function logMsg($priority, $message, $task = "LDAP Sync") {
|
||||
global $valkey;
|
||||
|
||||
$finalMsg = array(
|
||||
"time" => time(),
|
||||
"priority" => $priority,
|
||||
"task" => $task,
|
||||
"message" => $message
|
||||
);
|
||||
$valkey->lPush('CRON_LOG', json_encode($finalMsg));
|
||||
}
|
||||
|
||||
// Load core functions first
|
||||
require_once __DIR__ . '/../web/inc/functions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/sessions.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.ratelimit.inc.php';
|
||||
require_once __DIR__ . '/../web/inc/functions.acl.inc.php';
|
||||
|
||||
$_SESSION['mailcow_cc_username'] = "admin";
|
||||
$_SESSION['mailcow_cc_role'] = "admin";
|
||||
$_SESSION['acl']['tls_policy'] = "1";
|
||||
$_SESSION['acl']['quarantine_notification'] = "1";
|
||||
$_SESSION['acl']['quarantine_category'] = "1";
|
||||
$_SESSION['acl']['ratelimit'] = "1";
|
||||
$_SESSION['acl']['sogo_access'] = "1";
|
||||
$_SESSION['acl']['protocol_access'] = "1";
|
||||
$_SESSION['acl']['mailbox_relayhost'] = "1";
|
||||
$_SESSION['acl']['unlimited_quota'] = "1";
|
||||
|
||||
$iam_settings = identity_provider('get');
|
||||
if ($iam_settings['authsource'] != "ldap" || (intval($iam_settings['periodic_sync']) != 1 && intval($iam_settings['import_users']) != 1)) {
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set pagination variables
|
||||
$start = 0;
|
||||
$max = 100;
|
||||
|
||||
// lock sync if already running
|
||||
$lock_file = '/tmp/iam-sync.lock';
|
||||
if (file_exists($lock_file)) {
|
||||
$lock_file_parts = explode("\n", file_get_contents($lock_file));
|
||||
$pid = $lock_file_parts[0];
|
||||
if (count($lock_file_parts) > 1){
|
||||
$last_execution = $lock_file_parts[1];
|
||||
$elapsed_time = (time() - $last_execution) / 60;
|
||||
if ($elapsed_time < intval($iam_settings['sync_interval'])) {
|
||||
logMsg("warning", "Sync not ready (".number_format((float)$elapsed_time, 2, '.', '')."min / ".$iam_settings['sync_interval']."min)");
|
||||
session_destroy();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (posix_kill($pid, 0)) {
|
||||
logMsg("warning", "Sync is already running");
|
||||
session_destroy();
|
||||
exit;
|
||||
} else {
|
||||
unlink($lock_file);
|
||||
}
|
||||
}
|
||||
$lock_file_handle = fopen($lock_file, 'w');
|
||||
fwrite($lock_file_handle, getmypid());
|
||||
fclose($lock_file_handle);
|
||||
|
||||
// Init Provider
|
||||
$iam_provider = identity_provider('init');
|
||||
|
||||
// Get ldap users
|
||||
$ldap_query = $iam_provider->query();
|
||||
if (!empty($iam_settings['filter'])) {
|
||||
$ldap_query = $ldap_query->rawFilter($iam_settings['filter']);
|
||||
}
|
||||
$response = $ldap_query->where($iam_settings['username_field'], "*")
|
||||
->where($iam_settings['attribute_field'], "*")
|
||||
->select([$iam_settings['username_field'], $iam_settings['attribute_field'], 'displayname'])
|
||||
->paginate($max);
|
||||
|
||||
// Process the users
|
||||
foreach ($response as $user) {
|
||||
// try get mailbox user
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
mailbox.*,
|
||||
domain.active AS d_active
|
||||
FROM `mailbox`
|
||||
INNER JOIN domain on mailbox.domain = domain.domain
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $user[$iam_settings['username_field']][0]));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// check if matching attribute mapping exists
|
||||
$user_template = $user[$iam_settings['attribute_field']][0];
|
||||
$mapper_key = array_search($user_template, $iam_settings['mappers']);
|
||||
|
||||
if (empty($user[$iam_settings['username_field']][0])){
|
||||
logMsg("warning", "Skipping user " . $user['displayname'][0] . " due to empty LDAP ". $iam_settings['username_field'] . " property.");
|
||||
continue;
|
||||
}
|
||||
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
if (!$row && intval($iam_settings['import_users']) == 1){
|
||||
if ($mapper_key === false){
|
||||
if (!empty($iam_settings['default_template'])) {
|
||||
$mbox_template = $iam_settings['default_template'];
|
||||
} else {
|
||||
logMsg("warning", "No matching attribute mapping found for user " . $user[$iam_settings['username_field']][0]);
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
}
|
||||
// mailbox user does not exist, create...
|
||||
logMsg("info", "Creating user " . $user[$iam_settings['username_field']][0]);
|
||||
$create_res = mailbox('add', 'mailbox_from_template', array(
|
||||
'domain' => explode('@', $user[$iam_settings['username_field']][0])[1],
|
||||
'local_part' => explode('@', $user[$iam_settings['username_field']][0])[0],
|
||||
'name' => $user['displayname'][0],
|
||||
'authsource' => 'ldap',
|
||||
'template' => $mbox_template
|
||||
));
|
||||
if (!$create_res){
|
||||
logMsg("err", "Could not create user " . $user[$iam_settings['username_field']][0]);
|
||||
continue;
|
||||
}
|
||||
} else if ($row && intval($iam_settings['periodic_sync']) == 1 && $row['authsource'] == "ldap") {
|
||||
if ($mapper_key === false){
|
||||
logMsg("warning", "No matching attribute mapping found for user " . $user[$iam_settings['username_field']][0]);
|
||||
continue;
|
||||
}
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
// mailbox user does exist, sync attribtues...
|
||||
logMsg("info", "Syncing attributes for user " . $user[$iam_settings['username_field']][0]);
|
||||
mailbox('edit', 'mailbox_from_template', array(
|
||||
'username' => $user[$iam_settings['username_field']][0],
|
||||
'name' => $user['displayname'][0],
|
||||
'template' => $mbox_template
|
||||
));
|
||||
} else {
|
||||
// skip mailbox user
|
||||
logMsg("info", "Skipping user " . $user[$iam_settings['username_field']][0]);
|
||||
}
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
|
||||
sleep(0.025);
|
||||
}
|
||||
|
||||
logMsg("info", "DONE!");
|
||||
// add last execution time to lock file
|
||||
$lock_file_handle = fopen($lock_file, 'w');
|
||||
fwrite($lock_file_handle, getmypid() . "\n" . time());
|
||||
fclose($lock_file_handle);
|
||||
session_destroy();
|
||||
@@ -152,7 +152,7 @@ smtp_sasl_auth_enable = yes
|
||||
smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
|
||||
smtp_sasl_security_options =
|
||||
smtp_sasl_mechanism_filter = plain, login
|
||||
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf
|
||||
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf socketmap:inet:postfix-tlspol:8642:QUERY
|
||||
smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
|
||||
mail_name = Postcow
|
||||
# local_transport map catches local destinations and prevents routing local dests when the next map would route "*"
|
||||
|
||||
@@ -1,32 +1,71 @@
|
||||
# Whitelist generated by Postwhite v3.4 on Sat Feb 1 00:18:03 UTC 2025
|
||||
# Whitelist generated by Postwhite v3.4 on Wed Oct 1 00:21:33 UTC 2025
|
||||
# https://github.com/stevejenkins/postwhite/
|
||||
# 1984 total rules
|
||||
# 2216 total rules
|
||||
2a00:1450:4000::/36 permit
|
||||
2a01:111:f400::/48 permit
|
||||
2a01:111:f403:8000::/50 permit
|
||||
2a01:111:f403:2800::/53 permit
|
||||
2a01:111:f403:8000::/51 permit
|
||||
2a01:111:f403::/49 permit
|
||||
2a01:111:f403:c000::/51 permit
|
||||
2a01:111:f403:d000::/53 permit
|
||||
2a01:111:f403:f000::/52 permit
|
||||
2a01:238:20a:202:5370::1 permit
|
||||
2a01:238:20a:202:5372::1 permit
|
||||
2a01:238:20a:202:5373::1 permit
|
||||
2a01:238:400:101:53::1 permit
|
||||
2a01:238:400:102:53::1 permit
|
||||
2a01:238:400:103:53::1 permit
|
||||
2a01:238:400:301:53::1 permit
|
||||
2a01:238:400:302:53::1 permit
|
||||
2a01:238:400:303:53::1 permit
|
||||
2a01:238:400:470:53::1 permit
|
||||
2a01:238:400:471:53::1 permit
|
||||
2a01:238:400:472:53::1 permit
|
||||
2a01:b747:3000:200::/56 permit
|
||||
2a01:b747:3001:200::/56 permit
|
||||
2a01:b747:3002:200::/56 permit
|
||||
2a01:b747:3003:200::/56 permit
|
||||
2a01:b747:3004:200::/56 permit
|
||||
2a01:b747:3005:200::/56 permit
|
||||
2a01:b747:3006:200::/56 permit
|
||||
2a02:a60:0:5::/64 permit
|
||||
2c0f:fb50:4000::/36 permit
|
||||
2.207.151.53 permit
|
||||
2.207.217.30 permit
|
||||
3.64.237.68 permit
|
||||
3.65.3.180 permit
|
||||
3.70.123.177 permit
|
||||
3.72.182.33 permit
|
||||
3.74.81.189 permit
|
||||
3.74.125.228 permit
|
||||
3.75.33.185 permit
|
||||
3.93.157.0/24 permit
|
||||
3.94.40.108 permit
|
||||
3.121.107.214 permit
|
||||
3.129.120.190 permit
|
||||
3.210.190.0/24 permit
|
||||
3.211.80.218 permit
|
||||
3.216.221.67 permit
|
||||
3.221.209.22 permit
|
||||
8.20.114.31 permit
|
||||
8.25.194.0/23 permit
|
||||
8.25.196.0/23 permit
|
||||
10.162.0.0/16 permit
|
||||
8.36.116.0/24 permit
|
||||
8.39.54.0/23 permit
|
||||
8.39.54.250/31 permit
|
||||
8.39.144.0/24 permit
|
||||
8.40.222.0/23 permit
|
||||
8.40.222.250/31 permit
|
||||
12.130.86.238 permit
|
||||
13.107.213.41 permit
|
||||
13.107.246.41 permit
|
||||
13.110.208.0/21 permit
|
||||
13.110.209.0/24 permit
|
||||
13.110.216.0/22 permit
|
||||
13.110.224.0/20 permit
|
||||
13.111.0.0/16 permit
|
||||
13.111.191.0/24 permit
|
||||
13.216.7.111 permit
|
||||
13.216.54.180 permit
|
||||
15.200.21.50 permit
|
||||
15.200.44.248 permit
|
||||
15.200.201.185 permit
|
||||
@@ -35,25 +74,30 @@
|
||||
17.57.156.0/24 permit
|
||||
17.58.0.0/16 permit
|
||||
17.142.0.0/15 permit
|
||||
17.143.234.140/30 permit
|
||||
18.97.0.8/30 permit
|
||||
18.97.1.184/29 permit
|
||||
18.97.2.64/26 permit
|
||||
18.156.89.250 permit
|
||||
18.156.205.64 permit
|
||||
18.157.70.148 permit
|
||||
18.157.114.255 permit
|
||||
18.157.243.190 permit
|
||||
18.158.153.154 permit
|
||||
18.194.95.56 permit
|
||||
18.197.217.180 permit
|
||||
18.198.96.88 permit
|
||||
18.199.210.3 permit
|
||||
18.207.52.234 permit
|
||||
18.208.124.128/25 permit
|
||||
18.216.232.154 permit
|
||||
18.235.27.253 permit
|
||||
18.236.40.242 permit
|
||||
18.236.56.161 permit
|
||||
20.51.6.32/30 permit
|
||||
20.51.98.61 permit
|
||||
20.52.52.2 permit
|
||||
20.52.128.133 permit
|
||||
20.59.80.4/30 permit
|
||||
20.63.210.192/28 permit
|
||||
20.69.8.108/30 permit
|
||||
20.70.246.20 permit
|
||||
20.76.201.171 permit
|
||||
20.83.222.104/30 permit
|
||||
20.88.157.184/30 permit
|
||||
20.94.180.64/28 permit
|
||||
@@ -62,14 +106,11 @@
|
||||
20.98.194.68/30 permit
|
||||
20.105.209.76/30 permit
|
||||
20.107.239.64/30 permit
|
||||
20.112.250.133 permit
|
||||
20.118.139.208/30 permit
|
||||
20.141.10.196 permit
|
||||
20.185.214.0/27 permit
|
||||
20.185.214.32/27 permit
|
||||
20.185.214.64/27 permit
|
||||
20.231.239.246 permit
|
||||
20.236.44.162 permit
|
||||
23.103.224.0/19 permit
|
||||
23.249.208.0/20 permit
|
||||
23.251.224.0/19 permit
|
||||
@@ -81,6 +122,7 @@
|
||||
23.253.183.147 permit
|
||||
23.253.183.148 permit
|
||||
23.253.183.150 permit
|
||||
24.110.64.0/18 permit
|
||||
27.123.204.128/30 permit
|
||||
27.123.204.132/31 permit
|
||||
27.123.204.148/30 permit
|
||||
@@ -93,19 +135,51 @@
|
||||
27.123.206.56/29 permit
|
||||
27.123.206.76/30 permit
|
||||
27.123.206.80/28 permit
|
||||
31.25.48.222 permit
|
||||
31.47.251.17 permit
|
||||
31.186.239.0/24 permit
|
||||
34.2.64.0/22 permit
|
||||
34.2.68.0/23 permit
|
||||
34.2.70.0/23 permit
|
||||
34.2.71.64/26 permit
|
||||
34.2.72.0/22 permit
|
||||
34.2.75.0/26 permit
|
||||
34.2.78.0/23 permit
|
||||
34.2.80.0/23 permit
|
||||
34.2.82.0/23 permit
|
||||
34.2.84.0/24 permit
|
||||
34.2.84.64/26 permit
|
||||
34.2.85.0/24 permit
|
||||
34.2.85.64/26 permit
|
||||
34.2.86.0/23 permit
|
||||
34.2.88.0/23 permit
|
||||
34.2.90.0/23 permit
|
||||
34.2.92.0/23 permit
|
||||
34.2.94.0/23 permit
|
||||
34.70.158.162 permit
|
||||
34.74.74.140 permit
|
||||
34.83.159.189 permit
|
||||
34.141.160.224 permit
|
||||
34.193.58.168 permit
|
||||
34.195.217.107 permit
|
||||
34.197.10.50 permit
|
||||
34.197.254.9 permit
|
||||
34.198.94.229 permit
|
||||
34.198.218.121 permit
|
||||
34.212.163.75 permit
|
||||
34.215.104.144 permit
|
||||
34.218.116.3 permit
|
||||
34.218.115.239 permit
|
||||
34.225.212.172 permit
|
||||
34.241.242.183 permit
|
||||
35.83.148.184 permit
|
||||
35.155.198.111 permit
|
||||
35.158.23.94 permit
|
||||
35.161.32.253 permit
|
||||
35.162.73.231 permit
|
||||
35.167.93.243 permit
|
||||
35.174.145.124 permit
|
||||
35.176.132.251 permit
|
||||
35.190.247.0/24 permit
|
||||
35.191.0.0/16 permit
|
||||
35.205.92.9 permit
|
||||
35.228.216.85 permit
|
||||
35.242.169.159 permit
|
||||
37.188.97.188 permit
|
||||
37.218.248.47 permit
|
||||
@@ -119,12 +193,21 @@
|
||||
40.233.64.216 permit
|
||||
40.233.83.78 permit
|
||||
40.233.88.28 permit
|
||||
43.239.212.33 permit
|
||||
44.206.138.57 permit
|
||||
44.210.169.44 permit
|
||||
44.217.45.156 permit
|
||||
44.236.56.93 permit
|
||||
44.238.220.251 permit
|
||||
44.245.243.92 permit
|
||||
44.246.1.125 permit
|
||||
44.246.68.102 permit
|
||||
44.246.77.92 permit
|
||||
45.14.148.0/22 permit
|
||||
46.19.170.16 permit
|
||||
45.143.132.0/24 permit
|
||||
45.143.133.0/24 permit
|
||||
45.143.134.0/24 permit
|
||||
45.143.135.0/24 permit
|
||||
46.226.48.0/21 permit
|
||||
46.228.36.37 permit
|
||||
46.228.36.38/31 permit
|
||||
@@ -175,6 +258,7 @@
|
||||
46.243.88.177 permit
|
||||
46.243.95.179 permit
|
||||
46.243.95.180 permit
|
||||
50.16.246.183 permit
|
||||
50.18.45.249 permit
|
||||
50.18.121.236 permit
|
||||
50.18.121.248 permit
|
||||
@@ -188,14 +272,24 @@
|
||||
50.56.130.220 permit
|
||||
50.56.130.221 permit
|
||||
50.56.130.222 permit
|
||||
50.112.246.219 permit
|
||||
52.1.14.157 permit
|
||||
52.5.230.59 permit
|
||||
52.6.74.205 permit
|
||||
52.12.53.23 permit
|
||||
52.13.214.179 permit
|
||||
52.26.1.71 permit
|
||||
52.27.5.72 permit
|
||||
52.27.28.47 permit
|
||||
52.28.63.81 permit
|
||||
52.28.197.132 permit
|
||||
52.34.181.151 permit
|
||||
52.35.192.45 permit
|
||||
52.36.138.31 permit
|
||||
52.37.142.146 permit
|
||||
52.42.203.116 permit
|
||||
52.50.24.208 permit
|
||||
52.57.120.243 permit
|
||||
52.58.216.183 permit
|
||||
52.59.143.3 permit
|
||||
52.60.41.5 permit
|
||||
@@ -238,23 +332,24 @@
|
||||
54.174.63.0/24 permit
|
||||
54.186.193.102 permit
|
||||
54.191.223.56 permit
|
||||
54.211.126.101 permit
|
||||
54.213.20.246 permit
|
||||
54.214.39.184 permit
|
||||
54.240.0.0/18 permit
|
||||
54.240.64.0/19 permit
|
||||
54.240.96.0/19 permit
|
||||
54.240.64.0/18 permit
|
||||
54.241.16.209 permit
|
||||
54.244.54.130 permit
|
||||
54.244.242.0/24 permit
|
||||
54.255.61.23 permit
|
||||
56.124.6.228 permit
|
||||
57.103.64.0/18 permit
|
||||
57.129.93.249 permit
|
||||
62.13.128.0/24 permit
|
||||
62.13.129.128/25 permit
|
||||
62.13.136.0/21 permit
|
||||
62.13.144.0/21 permit
|
||||
62.13.152.0/21 permit
|
||||
62.17.146.128/26 permit
|
||||
62.179.121.0/24 permit
|
||||
62.201.172.0/27 permit
|
||||
62.201.172.32/27 permit
|
||||
62.253.227.114 permit
|
||||
@@ -262,6 +357,9 @@
|
||||
63.128.21.0/24 permit
|
||||
63.143.57.128/25 permit
|
||||
63.143.59.128/25 permit
|
||||
63.176.194.123 permit
|
||||
63.178.132.221 permit
|
||||
63.178.143.178 permit
|
||||
64.18.0.0/20 permit
|
||||
64.20.241.45 permit
|
||||
64.69.212.0/24 permit
|
||||
@@ -274,6 +372,7 @@
|
||||
64.127.115.252 permit
|
||||
64.132.88.0/23 permit
|
||||
64.132.92.0/24 permit
|
||||
64.181.194.190 permit
|
||||
64.207.219.7 permit
|
||||
64.207.219.8 permit
|
||||
64.207.219.9 permit
|
||||
@@ -327,10 +426,10 @@
|
||||
65.110.161.77 permit
|
||||
65.123.29.213 permit
|
||||
65.123.29.220 permit
|
||||
65.154.166.0/24 permit
|
||||
65.212.180.36 permit
|
||||
66.102.0.0/20 permit
|
||||
66.119.150.192/26 permit
|
||||
66.162.193.226/31 permit
|
||||
66.163.184.0/24 permit
|
||||
66.163.185.0/24 permit
|
||||
66.163.186.0/24 permit
|
||||
@@ -536,7 +635,6 @@
|
||||
74.86.241.250/31 permit
|
||||
74.112.67.243 permit
|
||||
74.125.0.0/16 permit
|
||||
74.202.227.40 permit
|
||||
74.208.4.200 permit
|
||||
74.208.4.201 permit
|
||||
74.208.4.220 permit
|
||||
@@ -565,6 +663,11 @@
|
||||
77.238.189.142 permit
|
||||
77.238.189.146/31 permit
|
||||
77.238.189.148/30 permit
|
||||
79.135.106.0/24 permit
|
||||
79.135.107.0/24 permit
|
||||
81.169.146.243 permit
|
||||
81.169.146.245 permit
|
||||
81.169.146.246 permit
|
||||
81.223.46.0/27 permit
|
||||
82.165.159.2 permit
|
||||
82.165.159.3 permit
|
||||
@@ -580,10 +683,17 @@
|
||||
82.165.159.45 permit
|
||||
82.165.159.130 permit
|
||||
82.165.159.131 permit
|
||||
84.116.6.0/23 permit
|
||||
84.116.36.0/24 permit
|
||||
84.116.50.0/23 permit
|
||||
85.9.206.169 permit
|
||||
85.9.210.45 permit
|
||||
85.158.136.0/21 permit
|
||||
85.215.255.39 permit
|
||||
85.215.255.40 permit
|
||||
85.215.255.41 permit
|
||||
85.215.255.45 permit
|
||||
85.215.255.46 permit
|
||||
85.215.255.47 permit
|
||||
85.215.255.48 permit
|
||||
85.215.255.49 permit
|
||||
86.61.88.25 permit
|
||||
87.238.80.0/21 permit
|
||||
87.248.103.12 permit
|
||||
@@ -623,12 +733,13 @@
|
||||
87.248.117.205 permit
|
||||
87.253.232.0/21 permit
|
||||
89.22.108.0/24 permit
|
||||
91.198.2.0/24 permit
|
||||
91.211.240.0/22 permit
|
||||
94.169.2.0/23 permit
|
||||
94.236.119.0/26 permit
|
||||
94.245.112.0/27 permit
|
||||
94.245.112.10/31 permit
|
||||
95.131.104.0/21 permit
|
||||
95.217.114.154 permit
|
||||
96.43.144.0/20 permit
|
||||
96.43.144.64/28 permit
|
||||
96.43.144.64/31 permit
|
||||
@@ -1122,15 +1233,14 @@
|
||||
99.83.190.102 permit
|
||||
103.9.96.0/22 permit
|
||||
103.28.42.0/24 permit
|
||||
103.84.217.238 permit
|
||||
103.89.75.238 permit
|
||||
103.151.192.0/23 permit
|
||||
103.168.172.128/27 permit
|
||||
103.237.104.0/22 permit
|
||||
104.43.243.237 permit
|
||||
104.44.112.128/25 permit
|
||||
104.47.0.0/17 permit
|
||||
104.47.20.0/23 permit
|
||||
104.47.75.0/24 permit
|
||||
104.47.108.0/23 permit
|
||||
104.130.96.0/28 permit
|
||||
104.130.122.0/23 permit
|
||||
106.10.144.64/27 permit
|
||||
@@ -1256,6 +1366,7 @@
|
||||
106.50.16.0/28 permit
|
||||
107.20.18.111 permit
|
||||
107.20.210.250 permit
|
||||
107.22.191.150 permit
|
||||
108.174.0.0/24 permit
|
||||
108.174.0.215 permit
|
||||
108.174.3.0/24 permit
|
||||
@@ -1264,9 +1375,8 @@
|
||||
108.174.6.215 permit
|
||||
108.175.18.45 permit
|
||||
108.175.30.45 permit
|
||||
108.177.8.0/21 permit
|
||||
108.177.96.0/19 permit
|
||||
108.179.144.0/20 permit
|
||||
109.224.244.0/24 permit
|
||||
109.237.142.0/24 permit
|
||||
111.221.23.128/25 permit
|
||||
111.221.26.0/27 permit
|
||||
@@ -1290,6 +1400,9 @@
|
||||
117.120.16.0/21 permit
|
||||
119.42.242.52/31 permit
|
||||
119.42.242.156 permit
|
||||
121.244.91.48 permit
|
||||
121.244.91.52 permit
|
||||
122.15.156.182 permit
|
||||
123.126.78.64/29 permit
|
||||
124.108.96.24/31 permit
|
||||
124.108.96.28/31 permit
|
||||
@@ -1338,7 +1451,6 @@
|
||||
129.213.195.191 permit
|
||||
130.61.9.72 permit
|
||||
130.162.39.83 permit
|
||||
130.211.0.0/22 permit
|
||||
130.248.172.0/24 permit
|
||||
130.248.173.0/24 permit
|
||||
131.253.30.0/24 permit
|
||||
@@ -1347,12 +1459,28 @@
|
||||
132.226.26.225 permit
|
||||
132.226.49.32 permit
|
||||
132.226.56.24 permit
|
||||
134.128.64.0/19 permit
|
||||
134.128.96.0/19 permit
|
||||
134.170.27.8 permit
|
||||
134.170.113.0/26 permit
|
||||
134.170.141.64/26 permit
|
||||
134.170.143.0/24 permit
|
||||
134.170.174.0/24 permit
|
||||
135.84.80.0/24 permit
|
||||
135.84.81.0/24 permit
|
||||
135.84.82.0/24 permit
|
||||
135.84.83.0/24 permit
|
||||
135.84.216.0/22 permit
|
||||
136.143.160.0/24 permit
|
||||
136.143.161.0/24 permit
|
||||
136.143.162.0/24 permit
|
||||
136.143.176.0/24 permit
|
||||
136.143.177.0/24 permit
|
||||
136.143.178.49 permit
|
||||
136.143.182.0/23 permit
|
||||
136.143.184.0/24 permit
|
||||
136.143.188.0/24 permit
|
||||
136.143.190.0/23 permit
|
||||
136.147.128.0/20 permit
|
||||
136.147.135.0/24 permit
|
||||
136.147.176.0/20 permit
|
||||
@@ -1367,6 +1495,7 @@
|
||||
139.138.46.219 permit
|
||||
139.138.57.55 permit
|
||||
139.138.58.119 permit
|
||||
139.167.79.86 permit
|
||||
139.180.17.0/24 permit
|
||||
140.238.148.191 permit
|
||||
141.148.159.229 permit
|
||||
@@ -1401,6 +1530,7 @@
|
||||
146.20.215.0/24 permit
|
||||
146.20.215.182 permit
|
||||
146.88.28.0/24 permit
|
||||
146.148.116.76 permit
|
||||
147.154.32.0/25 permit
|
||||
147.243.1.47 permit
|
||||
147.243.1.48 permit
|
||||
@@ -1410,7 +1540,7 @@
|
||||
148.105.0.0/16 permit
|
||||
148.105.8.0/21 permit
|
||||
149.72.0.0/16 permit
|
||||
149.72.223.204 permit
|
||||
149.72.234.184 permit
|
||||
149.72.248.236 permit
|
||||
149.97.173.180 permit
|
||||
150.230.98.160 permit
|
||||
@@ -1464,6 +1594,9 @@
|
||||
159.135.224.0/20 permit
|
||||
159.135.228.10 permit
|
||||
159.183.0.0/16 permit
|
||||
159.183.68.71 permit
|
||||
159.183.79.38 permit
|
||||
159.183.129.172 permit
|
||||
160.1.62.192 permit
|
||||
161.38.192.0/20 permit
|
||||
161.38.204.0/22 permit
|
||||
@@ -1480,8 +1613,14 @@
|
||||
163.114.132.120 permit
|
||||
163.114.134.16 permit
|
||||
163.114.135.16 permit
|
||||
163.116.128.0/17 permit
|
||||
163.192.116.87 permit
|
||||
164.152.23.32 permit
|
||||
164.152.25.241 permit
|
||||
164.177.132.168/30 permit
|
||||
165.173.128.0/24 permit
|
||||
165.173.180.250/31 permit
|
||||
165.173.182.250/31 permit
|
||||
166.78.68.0/22 permit
|
||||
166.78.68.221 permit
|
||||
166.78.69.169 permit
|
||||
@@ -1506,20 +1645,28 @@
|
||||
168.138.5.36 permit
|
||||
168.138.73.51 permit
|
||||
168.138.77.31 permit
|
||||
168.138.237.153 permit
|
||||
168.245.0.0/17 permit
|
||||
168.245.12.252 permit
|
||||
168.245.46.9 permit
|
||||
168.245.127.231 permit
|
||||
169.148.129.0/24 permit
|
||||
169.148.131.0/24 permit
|
||||
169.148.138.0/24 permit
|
||||
169.148.142.10 permit
|
||||
169.148.142.33 permit
|
||||
169.148.144.0/25 permit
|
||||
169.148.144.10 permit
|
||||
169.148.146.0/23 permit
|
||||
169.148.175.3 permit
|
||||
169.148.188.0/24 permit
|
||||
169.148.188.182 permit
|
||||
170.10.128.0/24 permit
|
||||
170.10.129.0/24 permit
|
||||
170.10.132.56/29 permit
|
||||
170.10.132.64/29 permit
|
||||
170.10.133.0/24 permit
|
||||
172.217.0.0/19 permit
|
||||
172.217.32.0/20 permit
|
||||
172.217.128.0/19 permit
|
||||
172.217.160.0/20 permit
|
||||
172.217.192.0/19 permit
|
||||
172.253.56.0/21 permit
|
||||
172.253.112.0/20 permit
|
||||
173.0.84.0/29 permit
|
||||
@@ -1549,9 +1696,14 @@
|
||||
182.50.78.64/28 permit
|
||||
183.240.219.64/29 permit
|
||||
185.4.120.0/22 permit
|
||||
185.11.253.128/27 permit
|
||||
185.11.255.0/24 permit
|
||||
185.12.80.0/22 permit
|
||||
185.28.196.0/22 permit
|
||||
185.58.84.93 permit
|
||||
185.70.40.0/24 permit
|
||||
185.70.41.0/24 permit
|
||||
185.70.43.0/24 permit
|
||||
185.80.93.204 permit
|
||||
185.80.93.227 permit
|
||||
185.80.95.31 permit
|
||||
@@ -1559,6 +1711,8 @@
|
||||
185.138.56.128/25 permit
|
||||
185.189.236.0/22 permit
|
||||
185.211.120.0/22 permit
|
||||
185.233.188.0/23 permit
|
||||
185.233.190.0/23 permit
|
||||
185.250.236.0/22 permit
|
||||
185.250.239.148 permit
|
||||
185.250.239.168 permit
|
||||
@@ -1611,6 +1765,7 @@
|
||||
188.125.85.234/31 permit
|
||||
188.125.85.236/31 permit
|
||||
188.125.85.238 permit
|
||||
188.165.51.139 permit
|
||||
188.172.128.0/20 permit
|
||||
192.0.64.0/18 permit
|
||||
192.18.139.154 permit
|
||||
@@ -1633,8 +1788,28 @@
|
||||
193.109.254.0/23 permit
|
||||
193.122.128.100 permit
|
||||
193.123.56.63 permit
|
||||
193.142.157.0/24 permit
|
||||
193.142.157.191 permit
|
||||
193.142.157.198 permit
|
||||
194.19.134.0/25 permit
|
||||
194.25.134.16/28 permit
|
||||
194.25.134.80/28 permit
|
||||
194.64.234.129 permit
|
||||
194.97.196.0/24 permit
|
||||
194.97.196.3 permit
|
||||
194.97.196.4 permit
|
||||
194.97.196.11 permit
|
||||
194.97.196.12 permit
|
||||
194.97.204.0/24 permit
|
||||
194.97.204.3 permit
|
||||
194.97.204.4 permit
|
||||
194.97.204.11 permit
|
||||
194.97.204.12 permit
|
||||
194.97.212.0/24 permit
|
||||
194.97.212.3 permit
|
||||
194.97.212.4 permit
|
||||
194.97.212.11 permit
|
||||
194.97.212.12 permit
|
||||
194.106.220.0/23 permit
|
||||
194.113.24.0/22 permit
|
||||
194.154.193.192/27 permit
|
||||
@@ -1651,6 +1826,14 @@
|
||||
198.61.254.231 permit
|
||||
198.178.234.57 permit
|
||||
198.244.48.0/20 permit
|
||||
198.244.56.107 permit
|
||||
198.244.56.108 permit
|
||||
198.244.56.109 permit
|
||||
198.244.56.111 permit
|
||||
198.244.56.112 permit
|
||||
198.244.56.113 permit
|
||||
198.244.56.114 permit
|
||||
198.244.56.115 permit
|
||||
198.244.59.30 permit
|
||||
198.244.59.33 permit
|
||||
198.244.59.35 permit
|
||||
@@ -1663,7 +1846,16 @@
|
||||
199.16.156.0/22 permit
|
||||
199.33.145.1 permit
|
||||
199.33.145.32 permit
|
||||
199.34.22.36 permit
|
||||
199.59.148.0/22 permit
|
||||
199.67.80.2 permit
|
||||
199.67.80.20 permit
|
||||
199.67.82.2 permit
|
||||
199.67.82.20 permit
|
||||
199.67.84.0/24 permit
|
||||
199.67.86.0/24 permit
|
||||
199.67.88.0/24 permit
|
||||
199.67.90.0/24 permit
|
||||
199.101.161.130 permit
|
||||
199.101.162.0/25 permit
|
||||
199.122.120.0/21 permit
|
||||
@@ -1720,9 +1912,13 @@
|
||||
204.92.114.187 permit
|
||||
204.92.114.203 permit
|
||||
204.92.114.204/31 permit
|
||||
204.141.32.0/23 permit
|
||||
204.141.42.0/23 permit
|
||||
204.216.164.202 permit
|
||||
204.220.160.0/21 permit
|
||||
204.220.168.0/21 permit
|
||||
204.220.176.0/20 permit
|
||||
204.220.181.105 permit
|
||||
204.232.168.0/24 permit
|
||||
205.139.110.0/24 permit
|
||||
205.201.128.0/20 permit
|
||||
@@ -1797,8 +1993,6 @@
|
||||
208.71.42.212/31 permit
|
||||
208.71.42.214 permit
|
||||
208.72.249.240/29 permit
|
||||
208.74.204.5 permit
|
||||
208.74.204.9 permit
|
||||
208.75.120.0/22 permit
|
||||
208.76.62.0/24 permit
|
||||
208.76.63.0/24 permit
|
||||
@@ -1862,6 +2056,8 @@
|
||||
212.227.15.4 permit
|
||||
212.227.15.5 permit
|
||||
212.227.15.6 permit
|
||||
212.227.15.7 permit
|
||||
212.227.15.8 permit
|
||||
212.227.15.14 permit
|
||||
212.227.15.15 permit
|
||||
212.227.15.18 permit
|
||||
@@ -1878,21 +2074,36 @@
|
||||
212.227.15.53 permit
|
||||
212.227.15.54 permit
|
||||
212.227.15.55 permit
|
||||
212.227.17.1 permit
|
||||
212.227.17.2 permit
|
||||
212.227.17.7 permit
|
||||
212.227.17.11 permit
|
||||
212.227.17.12 permit
|
||||
212.227.17.16 permit
|
||||
212.227.17.17 permit
|
||||
212.227.17.18 permit
|
||||
212.227.17.19 permit
|
||||
212.227.17.20 permit
|
||||
212.227.17.21 permit
|
||||
212.227.17.22 permit
|
||||
212.227.17.26 permit
|
||||
212.227.17.27 permit
|
||||
212.227.17.28 permit
|
||||
212.227.17.29 permit
|
||||
212.227.126.206 permit
|
||||
212.227.126.207 permit
|
||||
212.227.126.208 permit
|
||||
212.227.126.209 permit
|
||||
212.227.126.220 permit
|
||||
212.227.126.221 permit
|
||||
212.227.126.222 permit
|
||||
212.227.126.223 permit
|
||||
212.227.126.224 permit
|
||||
212.227.126.225 permit
|
||||
212.227.126.226 permit
|
||||
212.227.126.227 permit
|
||||
213.46.255.0/24 permit
|
||||
213.95.19.64/27 permit
|
||||
213.95.135.4 permit
|
||||
213.199.128.139 permit
|
||||
213.199.128.145 permit
|
||||
213.199.138.181 permit
|
||||
@@ -1902,6 +2113,7 @@
|
||||
216.17.150.242 permit
|
||||
216.17.150.251 permit
|
||||
216.24.224.0/20 permit
|
||||
216.27.86.152/31 permit
|
||||
216.39.60.154/31 permit
|
||||
216.39.60.156/30 permit
|
||||
216.39.60.160/30 permit
|
||||
@@ -1939,6 +2151,8 @@
|
||||
216.99.5.68 permit
|
||||
216.109.114.32/27 permit
|
||||
216.109.114.64/29 permit
|
||||
216.113.162.65 permit
|
||||
216.113.163.65 permit
|
||||
216.128.126.97 permit
|
||||
216.136.162.65 permit
|
||||
216.136.162.120/29 permit
|
||||
@@ -1966,12 +2180,30 @@
|
||||
2001:0868:0100:0600::/64 permit
|
||||
2001:4860:4000::/36 permit
|
||||
2001:748:100:40::2:0/112 permit
|
||||
2001:748:400:1300::3 permit
|
||||
2001:748:400:1300::4 permit
|
||||
2001:748:400:1301::0/64 permit
|
||||
2001:748:400:1301::3 permit
|
||||
2001:748:400:1301::4 permit
|
||||
2001:748:400:2300::3 permit
|
||||
2001:748:400:2300::4 permit
|
||||
2001:748:400:2301::0/64 permit
|
||||
2001:748:400:2301::3 permit
|
||||
2001:748:400:2301::4 permit
|
||||
2001:748:400:3300::3 permit
|
||||
2001:748:400:3300::4 permit
|
||||
2001:748:400:3301::0/64 permit
|
||||
2001:748:400:3301::3 permit
|
||||
2001:748:400:3301::4 permit
|
||||
2404:6800:4000::/36 permit
|
||||
2603:1010:3:3::5b permit
|
||||
2603:1020:201:10::10f permit
|
||||
2603:1030:20e:3::23c permit
|
||||
2603:1030:b:3::152 permit
|
||||
2603:1030:c02:8::14 permit
|
||||
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
|
||||
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
|
||||
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
|
||||
2607:f8b0:4000::/36 permit
|
||||
2620:109:c003:104::/64 permit
|
||||
2620:109:c003:104::215 permit
|
||||
@@ -1985,4 +2217,5 @@
|
||||
2620:119:50c0:207::/64 permit
|
||||
2620:119:50c0:207::215 permit
|
||||
2800:3f0:4000::/36 permit
|
||||
194.25.134.0/24 permit # t-online.de
|
||||
49.12.4.251 permit # checks.mailcow.email
|
||||
2a01:4f8:c17:7906::10 permit # checks.mailcow.email
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
/.+\.guru$/i
|
||||
/.+\.icu$/i
|
||||
/.+\.id$/i
|
||||
/.+\.info$/i
|
||||
/.+\.in.net$/i
|
||||
/.+\.ir$/i
|
||||
/.+\.jetzt$/i
|
||||
|
||||
@@ -133,7 +133,7 @@ try {
|
||||
error_log("ALIAS EXPANDER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
||||
$goto_branch_array = explode(',', $goto_branch);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
||||
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
||||
if ($goto_branch) {
|
||||
|
||||
@@ -56,7 +56,7 @@ function normalize_email($email) {
|
||||
$email = explode('@', $email);
|
||||
$email[0] = str_replace('.', '', $email[0]);
|
||||
$email = implode('@', $email);
|
||||
}
|
||||
}
|
||||
$gm_alt = "@googlemail.com";
|
||||
if (substr_compare($email, $gm_alt, -strlen($gm_alt)) == 0) {
|
||||
$email = explode('@', $email);
|
||||
@@ -114,7 +114,7 @@ function ucl_rcpts($object, $type) {
|
||||
$rcpt[] = str_replace('/', '\/', $row['address']);
|
||||
}
|
||||
// Aliases by alias domains
|
||||
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox`
|
||||
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox`
|
||||
LEFT OUTER JOIN `alias_domain` ON `mailbox`.`domain` = `alias_domain`.`target_domain`
|
||||
WHERE `mailbox`.`username` = :object");
|
||||
$stmt->execute(array(
|
||||
@@ -184,7 +184,7 @@ while ($row = array_shift($rows)) {
|
||||
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
|
||||
<?php
|
||||
}
|
||||
$stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf`
|
||||
$stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf`
|
||||
WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel')
|
||||
AND `object`= :object");
|
||||
$stmt->execute(array(':object' => $row['object']));
|
||||
@@ -468,4 +468,36 @@ while ($row = array_shift($rows)) {
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
<?php
|
||||
// Start internal aliases
|
||||
|
||||
$stmt = $pdo->query("SELECT `id`, `address`, `domain` FROM `alias` WHERE `active` = '1' AND `internal` = '1'");
|
||||
$aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
while ($alias = array_shift($aliases)) {
|
||||
// build allowed_domains regex and add target domain and alias domains
|
||||
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `active` = '1' AND `target_domain` = :target_domain");
|
||||
$stmt->execute(array(':target_domain' => $alias['domain']));
|
||||
$allowed_domains = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$allowed_domains = array_map(function($item) {
|
||||
return str_replace('.', '\.', $item['alias_domain']);
|
||||
}, $allowed_domains);
|
||||
$allowed_domains[] = str_replace('.', '\.', $alias['domain']);
|
||||
$allowed_domains = implode('|', $allowed_domains);
|
||||
?>
|
||||
internal_alias_<?=$alias['id'];?> {
|
||||
priority = 10;
|
||||
rcpt = "<?=$alias['address'];?>";
|
||||
from = "/^((?!.*@(<?=$allowed_domains;?>)).)*$/";
|
||||
apply "default" {
|
||||
MAILCOW_INTERNAL_ALIAS = 9999.0;
|
||||
}
|
||||
symbols [
|
||||
"MAILCOW_INTERNAL_ALIAS"
|
||||
]
|
||||
}
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ VIRUS_FOUND {
|
||||
}
|
||||
# Bad policy from free mail providers
|
||||
FREEMAIL_POLICY_FAILURE {
|
||||
expression = "FREEMAIL_FROM & !DMARC_POLICY_ALLOW & !MAILLIST& !WHITELISTED_FWD_HOST & -g+:policies";
|
||||
expression = "FREEMAIL_FROM & !DMARC_POLICY_ALLOW & !MAILLIST & !WHITELISTED_FWD_HOST & -g+:policies";
|
||||
score = 16.0;
|
||||
}
|
||||
# Applies to freemail with undisclosed recipients
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
oletools {
|
||||
# default olefy settings
|
||||
servers = "olefy:10055";
|
||||
# needs to be set explicitly for Rspamd < 1.9.5
|
||||
scan_mime_parts = true;
|
||||
# mime-part regex matching in content-type or filename
|
||||
# block all macros
|
||||
extended = true;
|
||||
max_size = 3145728;
|
||||
timeout = 20.0;
|
||||
retransmits = 1;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
servers = "redis:6379";
|
||||
timeout = 10;
|
||||
@@ -102,7 +102,7 @@ rspamd_config:register_symbol({
|
||||
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
|
||||
if #rcpt_split == 2 then
|
||||
if rcpt_split[1] == 'postmaster' then
|
||||
task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt')
|
||||
task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt', 'postmaster')
|
||||
return
|
||||
end
|
||||
end
|
||||
@@ -167,7 +167,7 @@ rspamd_config:register_symbol({
|
||||
for k,v in pairs(data) do
|
||||
if (v and v ~= userdata and v == '1') then
|
||||
rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result")
|
||||
task:set_pre_result('accept', 'ip matched with forward hosts')
|
||||
task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -454,12 +454,18 @@ rspamd_config:register_symbol({
|
||||
local redis_params = rspamd_parse_redis_server('dyn_rl')
|
||||
local rspamd_logger = require "rspamd_logger"
|
||||
local envfrom = task:get_from(1)
|
||||
local envrcpt = task:get_recipients(1) or {}
|
||||
local uname = task:get_user()
|
||||
if not envfrom or not uname then
|
||||
return false
|
||||
end
|
||||
|
||||
local uname = uname:lower()
|
||||
|
||||
if #envrcpt == 1 and envrcpt[1].addr:lower() == uname then
|
||||
return false
|
||||
end
|
||||
|
||||
local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case
|
||||
|
||||
local function redis_cb_user(err, data)
|
||||
@@ -544,13 +550,13 @@ rspamd_config:register_symbol({
|
||||
-- determine newline type
|
||||
local function newline(task)
|
||||
local t = task:get_newlines_type()
|
||||
|
||||
|
||||
if t == 'cr' then
|
||||
return '\r'
|
||||
elseif t == 'lf' then
|
||||
return '\n'
|
||||
end
|
||||
|
||||
|
||||
return '\r\n'
|
||||
end
|
||||
-- retrieve footer
|
||||
@@ -558,7 +564,7 @@ rspamd_config:register_symbol({
|
||||
if err or type(data) ~= 'string' then
|
||||
rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
|
||||
else
|
||||
|
||||
|
||||
-- parse json string
|
||||
local footer = cjson.decode(data)
|
||||
if not footer then
|
||||
@@ -607,26 +613,30 @@ rspamd_config:register_symbol({
|
||||
if footer.plain and footer.plain ~= "" then
|
||||
footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
|
||||
end
|
||||
|
||||
|
||||
-- add footer
|
||||
local out = {}
|
||||
local rewrite = lua_mime.add_text_footer(task, footer.html, footer.plain) or {}
|
||||
|
||||
|
||||
local seen_cte
|
||||
local newline_s = newline(task)
|
||||
|
||||
|
||||
local function rewrite_ct_cb(name, hdr)
|
||||
if rewrite.need_rewrite_ct then
|
||||
if name:lower() == 'content-type' then
|
||||
local nct = string.format('%s: %s/%s; charset=utf-8',
|
||||
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
|
||||
-- include boundary if present
|
||||
local boundary_part = rewrite.new_ct.boundary and
|
||||
string.format('; boundary="%s"', rewrite.new_ct.boundary) or ''
|
||||
local nct = string.format('%s: %s/%s; charset=utf-8%s',
|
||||
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)
|
||||
out[#out + 1] = nct
|
||||
-- update Content-Type header
|
||||
-- update Content-Type header (include boundary if present)
|
||||
task:set_milter_reply({
|
||||
remove_headers = {['Content-Type'] = 0},
|
||||
})
|
||||
task:set_milter_reply({
|
||||
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8', rewrite.new_ct.type, rewrite.new_ct.subtype)}
|
||||
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8%s',
|
||||
rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)}
|
||||
})
|
||||
return
|
||||
elseif name:lower() == 'content-transfer-encoding' then
|
||||
@@ -645,16 +655,16 @@ rspamd_config:register_symbol({
|
||||
end
|
||||
out[#out + 1] = hdr.raw:gsub('\r?\n?$', '')
|
||||
end
|
||||
|
||||
|
||||
task:headers_foreach(rewrite_ct_cb, {full = true})
|
||||
|
||||
|
||||
if not seen_cte and rewrite.need_rewrite_ct then
|
||||
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable')
|
||||
end
|
||||
|
||||
|
||||
-- End of headers
|
||||
out[#out + 1] = newline_s
|
||||
|
||||
|
||||
if rewrite.out then
|
||||
for _,o in ipairs(rewrite.out) do
|
||||
out[#out + 1] = o
|
||||
|
||||
@@ -182,7 +182,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
||||
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
||||
$goto_branch_array = explode(',', $goto_branch);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
||||
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
||||
if ($goto_branch) {
|
||||
@@ -236,6 +236,9 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) {
|
||||
':action' => $action,
|
||||
':fuzzy_hashes' => $fuzzy
|
||||
));
|
||||
$lastId = $pdo->lastInsertId();
|
||||
$stmt_update = $pdo->prepare("UPDATE `quarantine` SET `qhash` = SHA2(CONCAT(`id`, `qid`), 256) WHERE `id` = :id");
|
||||
$stmt_update->execute(array(':id' => $lastId));
|
||||
$stmt = $pdo->prepare('DELETE FROM `quarantine` WHERE `rcpt` = :rcpt AND `id` NOT IN (
|
||||
SELECT `id`
|
||||
FROM (
|
||||
|
||||
@@ -167,7 +167,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
|
||||
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
|
||||
$goto_branch_array = explode(',', $goto_branch);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
|
||||
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
|
||||
$stmt->execute(array(':domain' => $parsed_goto['domain']));
|
||||
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
|
||||
if ($goto_branch) {
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
// redirect to mailcow login form
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var loginForm = document.forms.namedItem("loginForm");
|
||||
if (loginForm) {
|
||||
window.location.href = '/user';
|
||||
}
|
||||
});
|
||||
// logout function
|
||||
function mc_logout() {
|
||||
fetch("/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
body: "logout=1"
|
||||
}).then(() => window.location.href = '/');
|
||||
}
|
||||
|
||||
// Custom SOGo JS
|
||||
|
||||
// Change the visible font-size in the editor, this does not change the font of a html message by default
|
||||
@@ -5,3 +23,4 @@ CKEDITOR.addCss("body {font-size: 16px !important}");
|
||||
|
||||
// Enable scayt by default
|
||||
//CKEDITOR.config.scayt_autoStartup = true;
|
||||
|
||||
|
||||
62
data/conf/sogo/plist_ldap → data/conf/sogo/plist_ldap.sh
Normal file → Executable file
62
data/conf/sogo/plist_ldap → data/conf/sogo/plist_ldap.sh
Normal file → Executable file
@@ -1,28 +1,34 @@
|
||||
<!--
|
||||
<example>
|
||||
<key>canAuthenticate</key>
|
||||
<string>YES</string>
|
||||
<key>id</key>
|
||||
<string>${line}_ldap</string>
|
||||
<key>isAddressBook</key>
|
||||
<string>NO</string>
|
||||
<key>IDFieldName</key>
|
||||
<string>mail</string>
|
||||
<key>UIDFieldName</key>
|
||||
<string>uid</string>
|
||||
<key>bindFields</key>
|
||||
<array>
|
||||
<string>mail</string>
|
||||
</array>
|
||||
<key>type</key>
|
||||
<string>ldap</string>
|
||||
<key>bindDN</key>
|
||||
<string>cn=admin,dc=example,dc=local</string>
|
||||
<key>bindPassword</key>
|
||||
<string>password</string>
|
||||
<key>baseDN</key>
|
||||
<string>ou=People,dc=example,dc=local</string>
|
||||
<key>hostname</key>
|
||||
<string>ldap://1.2.3.4:389</string>
|
||||
</example>
|
||||
-->
|
||||
#!/bin/bash
|
||||
|
||||
domain="$1"
|
||||
gal_status="$2"
|
||||
|
||||
echo "
|
||||
<!--
|
||||
<example>
|
||||
<key>canAuthenticate</key>
|
||||
<string>YES</string>
|
||||
<key>id</key>
|
||||
<string>"${domain}"_ldap</string>
|
||||
<key>isAddressBook</key>
|
||||
<string>"${gal_status}"</string>
|
||||
<key>IDFieldName</key>
|
||||
<string>mail</string>
|
||||
<key>UIDFieldName</key>
|
||||
<string>uid</string>
|
||||
<key>bindFields</key>
|
||||
<array>
|
||||
<string>mail</string>
|
||||
</array>
|
||||
<key>type</key>
|
||||
<string>ldap</string>
|
||||
<key>bindDN</key>
|
||||
<string>cn=admin,dc=example,dc=local</string>
|
||||
<key>bindPassword</key>
|
||||
<string>password</string>
|
||||
<key>baseDN</key>
|
||||
<string>ou=People,dc=example,dc=local</string>
|
||||
<key>hostname</key>
|
||||
<string>ldap://1.2.3.4:389</string>
|
||||
</example>
|
||||
-->"
|
||||
@@ -16,6 +16,9 @@
|
||||
SOGoFoldersSendEMailNotifications = YES;
|
||||
SOGoForwardEnabled = YES;
|
||||
|
||||
// Added with SOGo 5.12 - Allows users to cleanup there maildirectories by deleting mails oder than X
|
||||
SOGoEnableMailCleaning = YES;
|
||||
|
||||
// Fixes "MODIFICATION_FAILED" error (HTTP 412) in Clients when accepting invitations from external services
|
||||
SOGoDisableOrganizerEventCheck = YES;
|
||||
|
||||
@@ -91,7 +94,7 @@
|
||||
//SoDebugBaseURL = YES;
|
||||
//ImapDebugEnabled = YES;
|
||||
//SOGoEASDebugEnabled = YES;
|
||||
SOGoEASSearchInBody = YES; // Experimental. Enabled since 2023-10
|
||||
SOGoEASSearchInBody = YES;
|
||||
//LDAPDebugEnabled = YES;
|
||||
//PGDebugEnabled = YES;
|
||||
//MySQL4DebugEnabled = YES;
|
||||
|
||||
@@ -9,4 +9,4 @@ if [ -n "$VALKEYMASTERPASS" ]; then
|
||||
echo "masterauth $VALKEYMASTERPASS" >> /valkey.conf
|
||||
fi
|
||||
|
||||
exec valkey-server /valkey.conf
|
||||
exec "$@"
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /');
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
$clamd_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_CLAMD"])) ? false : true;
|
||||
$olefy_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_OLEFY"])) ? false : true;
|
||||
|
||||
|
||||
if (!isset($_SESSION['gal']) && $license_cache = $valkey->Get('LICENSE_STATUS_CACHE')) {
|
||||
$_SESSION['gal'] = json_decode($license_cache, true);
|
||||
}
|
||||
|
||||
$js_minifier->add('/web/js/site/debug.js');
|
||||
$js_minifier->add('/web/js/site/dashboard.js');
|
||||
|
||||
// vmail df
|
||||
$exec_fields = array('cmd' => 'system', 'task' => 'df', 'dir' => '/var/vmail');
|
||||
@@ -24,6 +34,7 @@ $vmail_df = explode(',', (string)json_decode(docker('post', 'dovecot-mailcow', '
|
||||
// containers
|
||||
$containers_info = (array) docker('info');
|
||||
if ($clamd_status === false) unset($containers_info['clamd-mailcow']);
|
||||
if ($olefy_status === false) unset($containers_info['olefy-mailcow']);
|
||||
ksort($containers_info);
|
||||
$containers = array();
|
||||
foreach ($containers_info as $container => $container_info) {
|
||||
@@ -59,7 +70,7 @@ foreach ($containers_info as $container => $container_info) {
|
||||
$hostname = getenv('MAILCOW_HOSTNAME');
|
||||
$timezone = getenv('TZ');
|
||||
|
||||
$template = 'debug.twig';
|
||||
$template = 'dashboard.twig';
|
||||
$template_data = [
|
||||
'log_lines' => getenv('LOG_LINES'),
|
||||
'vmail_df' => $vmail_df,
|
||||
@@ -68,6 +79,7 @@ $template_data = [
|
||||
'gal' => @$_SESSION['gal'],
|
||||
'license_guid' => license('guid'),
|
||||
'clamd_status' => $clamd_status,
|
||||
'olefy_status' => $olefy_status,
|
||||
'containers' => $containers,
|
||||
'ip_check' => customize('get', 'ip_check'),
|
||||
'lang_admin' => json_encode($lang['admin']),
|
||||
30
data/web/admin/index.php
Normal file
30
data/web/admin/index.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
$_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
|
||||
|
||||
|
||||
$template = 'admin_index.twig';
|
||||
$template_data = [
|
||||
'login_delay' => @$_SESSION['ldelay'],
|
||||
'custom_login' => customize('get', 'custom_login'),
|
||||
];
|
||||
|
||||
$js_minifier->add('/web/js/site/index.js');
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
@@ -1,10 +1,20 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
header('Location: /');
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
@@ -14,7 +24,7 @@ $js_minifier->add('/web/js/site/mailbox.js');
|
||||
$js_minifier->add('/web/js/presets/sieveMailbox.js');
|
||||
$js_minifier->add('/web/js/site/pwgen.js');
|
||||
|
||||
$role = ($_SESSION['mailcow_cc_role'] == "admin") ? 'admin' : 'domainadmin';
|
||||
$role = "admin";
|
||||
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? 'true' : 'false';
|
||||
$allow_admin_email_login = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"])) ? 'true' : 'false';
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /');
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
|
||||
@@ -11,7 +20,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$js_minifier->add('/web/js/site/queue.js');
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
$role = ($_SESSION['mailcow_cc_role'] == "admin") ? 'admin' : 'domainadmin';
|
||||
$role = "admin";
|
||||
|
||||
$template = 'queue.twig';
|
||||
$template_data = [
|
||||
@@ -1,8 +1,17 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
|
||||
|
||||
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /');
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
|
||||
header('Location: /admin');
|
||||
exit();
|
||||
}
|
||||
|
||||
@@ -86,6 +95,8 @@ $cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allo
|
||||
$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
|
||||
|
||||
$f2b_data = fail2ban('get');
|
||||
// mbox templates
|
||||
$mbox_templates = mailbox('get', 'mailbox_templates');
|
||||
|
||||
$template = 'admin.twig';
|
||||
$template_data = [
|
||||
@@ -114,10 +125,13 @@ $template_data = [
|
||||
'logo_specs' => customize('get', 'main_logo_specs'),
|
||||
'logo_dark_specs' => customize('get', 'main_logo_dark_specs'),
|
||||
'ip_check' => customize('get', 'ip_check'),
|
||||
'custom_login' => customize('get', 'custom_login'),
|
||||
'password_complexity' => password_complexity('get'),
|
||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||
'cors_settings' => $cors_settings,
|
||||
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
|
||||
'iam_settings' => $iam_settings,
|
||||
'mbox_templates' => $mbox_templates,
|
||||
'lang_admin' => json_encode($lang['admin']),
|
||||
'lang_datatables' => json_encode($lang['datatables'])
|
||||
];
|
||||
@@ -346,7 +346,8 @@ paths:
|
||||
description: the domain which emails should be forwarded
|
||||
type: string
|
||||
type:
|
||||
description: the type of bcc map can be `sender` or `recipient`
|
||||
description: the type of bcc map can be `sender` or `rcpt`
|
||||
enum: [sender, rcpt]
|
||||
type: string
|
||||
type: object
|
||||
summary: Create BCC Map
|
||||
@@ -409,7 +410,7 @@ paths:
|
||||
description: a list of domains for which a dkim key should be generated
|
||||
type: string
|
||||
key_size:
|
||||
description: the key size (1024 or 2048)
|
||||
description: the key size (1024, 2048, 3072 or 4096)
|
||||
type: number
|
||||
type: object
|
||||
summary: Generate DKIM Key
|
||||
@@ -1112,6 +1113,7 @@ paths:
|
||||
domain: domain.tld
|
||||
local_part: info
|
||||
name: Full name
|
||||
authsource: mailcow
|
||||
password: atedismonsin
|
||||
password2: atedismonsin
|
||||
quota: "3072"
|
||||
@@ -1132,11 +1134,16 @@ paths:
|
||||
name:
|
||||
description: Full name of the mailbox user
|
||||
type: string
|
||||
authsource:
|
||||
description: Specifies the authentication source for the mailbox.
|
||||
type: string
|
||||
enum: [mailcow, ldap, keycloak, generic-oidc]
|
||||
default: mailcow
|
||||
password2:
|
||||
description: mailbox password for confirmation
|
||||
type: string
|
||||
password:
|
||||
description: mailbox password
|
||||
description: mailbox password when using `mailcow` as the authentication source.
|
||||
type: string
|
||||
quota:
|
||||
description: mailbox quota
|
||||
@@ -3374,6 +3381,7 @@ paths:
|
||||
active: "1"
|
||||
force_pw_update: "0"
|
||||
name: Full name
|
||||
authsource: mailcow
|
||||
password: ""
|
||||
password2: ""
|
||||
quota: "3072"
|
||||
@@ -3398,11 +3406,15 @@ paths:
|
||||
name:
|
||||
description: Full name of the mailbox user
|
||||
type: string
|
||||
authsource:
|
||||
description: Specifies the authentication source for the mailbox.
|
||||
type: string
|
||||
enum: [mailcow, ldap, keycloak, generic-oidc]
|
||||
password2:
|
||||
description: new mailbox password for confirmation
|
||||
type: string
|
||||
password:
|
||||
description: new mailbox password
|
||||
description: new mailbox password when using `mailcow` as the authentication source.
|
||||
type: string
|
||||
quota:
|
||||
description: mailbox quota
|
||||
@@ -5687,7 +5699,7 @@ paths:
|
||||
- description: name of domain
|
||||
in: path
|
||||
name: domain
|
||||
required: false
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- description: e.g. api-key-string
|
||||
@@ -5814,6 +5826,226 @@ paths:
|
||||
Using this endpoint you can get the global spam filter score or the spam filter score of a certain mailbox.
|
||||
operationId: Get mailbox or global spam filter score
|
||||
summary: Get mailbox or global spam filter score
|
||||
/api/v1/edit/identity-provider:
|
||||
post:
|
||||
responses:
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
examples:
|
||||
response:
|
||||
value:
|
||||
- type: "success"
|
||||
log:
|
||||
- "identity_provider"
|
||||
- "edit"
|
||||
- authsource: "keycloak"
|
||||
server_url: "https://auth.mailcow.tld"
|
||||
realm: "mailcow"
|
||||
client_id: "mailcow_client"
|
||||
client_secret: "*"
|
||||
redirect_url: "https://mail.mailcow.tld"
|
||||
redirect_url_extra: ["https://extramail.mailcow.tld"]
|
||||
version: "26.1.3"
|
||||
default_template: "Default"
|
||||
mappers:
|
||||
- "small_mbox"
|
||||
- "medium_mbox"
|
||||
templates:
|
||||
- "small"
|
||||
- "medium"
|
||||
ignore_ssl_error: true
|
||||
mailpassword_flow: true
|
||||
periodic_sync: true
|
||||
import_users: true
|
||||
sync_interval: 30
|
||||
msg:
|
||||
- "object_modified"
|
||||
- ""
|
||||
description: OK
|
||||
headers: { }
|
||||
tags:
|
||||
- Identity Provider
|
||||
description: >-
|
||||
Configure an external Identity Provider to use as user authentication
|
||||
operationId: Edit external Identity Provider settings
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
items:
|
||||
type: array
|
||||
default: ["identity-provider"]
|
||||
attr:
|
||||
type: object
|
||||
properties:
|
||||
authsource:
|
||||
description: Specifies the type of the Identity Provider
|
||||
type: string
|
||||
enum: [ldap, keycloak, generic-oidc]
|
||||
server_url:
|
||||
description: The base URL of your Keycloak server. Required if `authsource` is keycloak.
|
||||
type: string
|
||||
realm:
|
||||
description: The Keycloak realm where the mailcow client is configured. Required if `authsource` is keycloak.
|
||||
type: string
|
||||
client_id:
|
||||
description: The Client ID assigned to mailcow Client in OIDC Provider. Required if `authsource` is keycloak or generic-oidc.
|
||||
type: string
|
||||
client_secret:
|
||||
description: The Client Secret assigned to mailcow Client in OIDC Provider. Required if `authsource` is keycloak or generic-oidc.
|
||||
type: string
|
||||
redirect_url:
|
||||
description: The redirect URL that OIDC Provider will use after authentication. Required if `authsource` is keycloak or generic-oidc.
|
||||
type: string
|
||||
redirect_url_extra:
|
||||
description: Additional redirect URLs that OIDC Provider can use after authentication if valid.
|
||||
type: array
|
||||
version:
|
||||
description: Specifies the Keycloak version. Required if `authsource` is keycloak.
|
||||
type: string
|
||||
default_template:
|
||||
description: (Optional) If no matching Attribute Mapping exists for a User, the default template will be used for creating the mailbox, but not for updating the mailbox.
|
||||
type: string
|
||||
mappers:
|
||||
description: (Optional) Attribute values used to match a mailbox template. Each element corresponds to the respective index in the templates array (i.e., the first element matches the first element of templates, the second matches the second, and so on).
|
||||
type: array
|
||||
templates:
|
||||
description: (Optional) Defines the mailbox templates to be assigned. Each element corresponds to the respective index in the `mappers` array.
|
||||
type: array
|
||||
ignore_ssl_error:
|
||||
description: If enabled, SSL certificate validation is bypassed
|
||||
type: boolean
|
||||
default: false
|
||||
mailpassword_flow:
|
||||
description: If enabled, mailcow will attempt to validate user credentials using the Keycloak Admin REST API instead of relying solely on the Authorization Code Flow.
|
||||
type: boolean
|
||||
default: false
|
||||
periodic_sync:
|
||||
description: If enabled, mailcow periodically performs a full sync of all users from Keycloak or LDAP.
|
||||
type: boolean
|
||||
default: false
|
||||
import_users:
|
||||
description: If enabled, new users are automatically imported from Keycloak or LDAP into mailcow.
|
||||
type: boolean
|
||||
default: false
|
||||
sync_interval:
|
||||
description: Defines the time interval (in minutes) for periodic synchronization and user imports.
|
||||
type: number
|
||||
default: 15
|
||||
host:
|
||||
description: The address of your LDAP server. You can provide a single hostname or a comma-separated list of hosts for fallback in case the primary server is unreachable. Required if `authsource` is ldap.
|
||||
type: string
|
||||
port:
|
||||
description: The port used to connect to the LDAP server. Required if `authsource` is ldap.
|
||||
type: string
|
||||
use_ssl:
|
||||
description: enable LDAPS connection. If Port is set to 389 it will be overriden to 636.
|
||||
type: boolean
|
||||
default: false
|
||||
use_tls:
|
||||
description: enable TLS connection. TLS is recommended over SSL. SSL Ports cannot be used.
|
||||
type: boolean
|
||||
default: false
|
||||
basedn:
|
||||
description: The Distinguished Name (DN) from which searches will be performed. Required if `authsource` is ldap.
|
||||
type: string
|
||||
username_field:
|
||||
description: The LDAP attribute used to identify users during authentication. Required if `authsource` is ldap.
|
||||
type: string
|
||||
default: mail
|
||||
filter:
|
||||
description: An optional LDAP search filter to refine which users can authenticate.
|
||||
type: string
|
||||
attribute_field:
|
||||
description: Specifies an LDAP attribute that holds a specific value which can be mapped to a mailbox template using the Attribute Mapping section. Required if `authsource` is ldap.
|
||||
type: string
|
||||
binddn:
|
||||
description: The Distinguished Name (DN) of the LDAP user that will be used to authenticate and perform LDAP searches. This account should have sufficient permissions to read the required attributes. Required if `authsource` is ldap.
|
||||
type: string
|
||||
bindpass:
|
||||
description: The password for the Bind DN user. It is required for authentication when connecting to the LDAP server. Required if `authsource` is ldap.
|
||||
type: string
|
||||
authorize_url:
|
||||
description: The OIDC provider's authorization server URL. Required if `authsource` is generic-oidc.
|
||||
type: string
|
||||
token_url:
|
||||
description: The OIDC provider's token server URL. Required if `authsource` is generic-oidc.
|
||||
type: string
|
||||
userinfo_url:
|
||||
description: The OIDC provider's user info server URL. Required if `authsource` is generic-oidc.
|
||||
type: string
|
||||
client_scopes:
|
||||
description: Specifies the OIDC scopes requested during authentication.
|
||||
type: string
|
||||
default: "openid profile email mailcow_template"
|
||||
examples:
|
||||
keycloak:
|
||||
value:
|
||||
items:
|
||||
- "identity-provider"
|
||||
attr:
|
||||
authsource: "keycloak"
|
||||
server_url: "https://auth.mailcow.tld"
|
||||
realm: "mailcow"
|
||||
client_id: "mailcow_client"
|
||||
client_secret: "Xy7GdPqvJ9m3R8sT2LkVZ5W1oNbCaYQf"
|
||||
redirect_url: "https://mail.mailcow.tld"
|
||||
redirect_url_extra: ["https://extramail.mailcow.tld"]
|
||||
version: "26.1.3"
|
||||
default_template: "Default"
|
||||
mappers: ["small_mbox", "medium_mbox"]
|
||||
templates: ["small", "medium"]
|
||||
ignore_ssl_error: true
|
||||
mailpassword_flow: true
|
||||
periodic_sync: true
|
||||
import_users: true
|
||||
sync_interval: 30
|
||||
ldap:
|
||||
value:
|
||||
items:
|
||||
- "identity-provider"
|
||||
attr:
|
||||
authsource: "ldap"
|
||||
host: "127.0.0.1"
|
||||
port: "389"
|
||||
use_ssl: false
|
||||
use_tls: false
|
||||
ignore_ssl_error: false
|
||||
basedn: "DC=mailcow,DC=local"
|
||||
username_field: "mail"
|
||||
filter: "(memberOf:1.2.840.113556.1.4.1941:=DC=mailcow,DC=local)"
|
||||
attribute_field: "othermailbox"
|
||||
binddn: "CN=LDAP Read Only,CN=Users,DC=mailcow,DC=local"
|
||||
bindpass: "moohoo"
|
||||
default_template: "Default"
|
||||
mappers: ["small_mbox", "medium_mbox"]
|
||||
templates: ["small", "medium"]
|
||||
periodic_sync: true
|
||||
import_users: true
|
||||
sync_interval: 30
|
||||
generic-oidc:
|
||||
value:
|
||||
items:
|
||||
- "identity-provider"
|
||||
attr:
|
||||
authsource: "generic-oidc"
|
||||
authorize_url: "https://auth.mailcow.tld/application/o/authorize/"
|
||||
token_url: "https://auth.mailcow.tld/application/o/token/"
|
||||
userinfo_url: "https://auth.mailcow.tld/application/o/userinfo/"
|
||||
client_id: "mailcow_client"
|
||||
client_secret: "Xy7GdPqvJ9m3R8sT2LkVZ5W1oNbCaYQf"
|
||||
redirect_url: "https://mail.mailcow.tld"
|
||||
redirect_url_extra: ["https://extramail.mailcow.tld"]
|
||||
client_scopes: "openid profile email mailcow_template"
|
||||
default_template: "Default"
|
||||
mappers: ["small_mbox", "medium_mbox"]
|
||||
templates: ["small", "medium"]
|
||||
ignore_ssl_error: true
|
||||
summary: Edit external Identity Provider
|
||||
|
||||
tags:
|
||||
- name: Domains
|
||||
@@ -5860,3 +6092,5 @@ tags:
|
||||
description: Edit domain ratelimits
|
||||
- name: Cross-Origin Resource Sharing (CORS)
|
||||
description: Manage Cross-Origin Resource Sharing (CORS) settings
|
||||
- name: Identity Provider
|
||||
description: Manage external Identity Provider settings
|
||||
|
||||
@@ -85,7 +85,7 @@ if (count($records) == 0 || $records[0]['target'] != '') { ?>
|
||||
<authentication>password-cleartext</authentication>
|
||||
</outgoingServer>
|
||||
|
||||
<enable visiturl="https://<?=$mailcow_hostname; ?><?php if ($port != 443) echo ':'.$port; ?>/admin.php">
|
||||
<enable visiturl="https://<?=$mailcow_hostname; ?><?php if ($port != 443) echo ':'.$port; ?>/admin">
|
||||
<instruction>If you didn't change the password given to you by the administrator or if you didn't change it in a long time, please consider doing that now.</instruction>
|
||||
<instruction lang="de">Sollten Sie das Ihnen durch den Administrator vergebene Passwort noch nicht geändert haben, empfehlen wir dies nun zu tun. Auch ein altes Passwort sollte aus Sicherheitsgründen geändert werden.</instruction>
|
||||
</enable>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/vendor/autoload.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/vars.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
|
||||
$default_autodiscover_config = $autodiscover_config;
|
||||
if(file_exists('inc/vars.local.inc.php')) {
|
||||
include_once 'inc/vars.local.inc.php';
|
||||
}
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
|
||||
$default_autodiscover_config = $autodiscover_config;
|
||||
$autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
|
||||
|
||||
// Valkey
|
||||
@@ -50,6 +53,11 @@ $opt = [
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
||||
|
||||
// Init Identity Provider
|
||||
$iam_provider = identity_provider('init');
|
||||
$iam_settings = identity_provider('get');
|
||||
|
||||
$login_user = strtolower(trim($_SERVER['PHP_AUTH_USER']));
|
||||
$login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW']));
|
||||
|
||||
|
||||
2
data/web/css/build/007-languages.min.css
vendored
2
data/web/css/build/007-languages.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -8,9 +8,6 @@
|
||||
.dtr-details {
|
||||
width: 100%;
|
||||
}
|
||||
.table-striped>tbody>tr:nth-of-type(odd) {
|
||||
background-color: #F2F2F2;
|
||||
}
|
||||
td.child>ul>li {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,13 @@
|
||||
url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff2') format('woff2'),
|
||||
url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff') format('woff');
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fbfbfb;
|
||||
}
|
||||
#maxmsgsize { min-width: 80px; }
|
||||
#slider1 .slider-selection {
|
||||
background: #FFD700;
|
||||
@@ -74,10 +81,23 @@
|
||||
align-items: center;
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
.navbar-fixed-bottom .navbar-collapse,
|
||||
.navbar-fixed-bottom .navbar-collapse,
|
||||
.navbar-fixed-top .navbar-collapse {
|
||||
max-height: 1000px
|
||||
}
|
||||
.nav-tabs .nav-link, .nav-tabs .nav-link.disabled, .nav-tabs .nav-link.disabled:hover, .nav-tabs .nav-link.disabled:focus {
|
||||
border-color: #dfdfdf;
|
||||
}
|
||||
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
|
||||
border-color: #dfdfdf;
|
||||
border-bottom: 1px solid #ffffff;
|
||||
}
|
||||
.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {
|
||||
border-color: #dfdfdf;
|
||||
}
|
||||
.nav-tabs {
|
||||
border-bottom: 1px solid #dfdfdf;
|
||||
}
|
||||
.bi {
|
||||
display: inline-block;
|
||||
font-size: 12pt;
|
||||
@@ -123,18 +143,18 @@
|
||||
}
|
||||
}
|
||||
@keyframes blink {
|
||||
50% {
|
||||
color: transparent
|
||||
50% {
|
||||
color: transparent
|
||||
}
|
||||
}
|
||||
.loader-dot {
|
||||
animation: 1s blink infinite
|
||||
.loader-dot {
|
||||
animation: 1s blink infinite
|
||||
}
|
||||
.loader-dot:nth-child(2) {
|
||||
animation-delay: 250ms
|
||||
.loader-dot:nth-child(2) {
|
||||
animation-delay: 250ms
|
||||
}
|
||||
.loader-dot:nth-child(3) {
|
||||
animation-delay: 500ms
|
||||
.loader-dot:nth-child(3) {
|
||||
animation-delay: 500ms
|
||||
}
|
||||
|
||||
pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-o-pre-wrap;word-wrap:break-word;}
|
||||
@@ -200,13 +220,13 @@ legend {
|
||||
}
|
||||
.haveibeenpwned {
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.full-width-select {
|
||||
width: 100%!important;
|
||||
width: 100%!important;
|
||||
}
|
||||
.tooltip {
|
||||
font-family: inherit;
|
||||
@@ -330,7 +350,7 @@ code {
|
||||
.caret {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
a[aria-expanded='true'] > .caret,
|
||||
a[aria-expanded='true'] > .caret,
|
||||
button[aria-expanded='true'] > .caret {
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
@@ -340,7 +360,7 @@ button[aria-expanded='true'] > .caret {
|
||||
}
|
||||
.list-group-header {
|
||||
background: #f7f7f7;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bg-primary, .alert-primary, .btn-primary {
|
||||
@@ -366,12 +386,13 @@ button[aria-expanded='true'] > .caret {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.btn.btn-outline-secondary {
|
||||
border-color: #cfcfcf !important;
|
||||
color: #000000 !important;
|
||||
border-color: #cfcfcf !important;
|
||||
}
|
||||
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
|
||||
background-color: #f0f0f0 !important;
|
||||
}
|
||||
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
|
||||
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
|
||||
color: #fff;
|
||||
background-color: #555;
|
||||
background-image: none;
|
||||
@@ -389,4 +410,26 @@ button[aria-expanded='true'] > .caret {
|
||||
.badge.bg-danger > a {
|
||||
color: #fff !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.hr-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.hr-title::before,
|
||||
.hr-title::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.hr-title:not(:empty)::before {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.hr-title:not(:empty)::after {
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -6,15 +6,9 @@
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.card-login .apps .btn {
|
||||
width: auto;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
margin-top: auto;
|
||||
}
|
||||
.card-login .apps .btn:hover {
|
||||
margin-top: 1px !important;
|
||||
border-bottom-width: 3px;
|
||||
.card .apps {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.responsive-tabs .nav-tabs {
|
||||
@@ -43,16 +37,6 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card-login .apps .btn {
|
||||
width: 100%;
|
||||
float: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-login .apps .btn {
|
||||
border-bottom-width: 4px;
|
||||
}
|
||||
|
||||
.xs-show {
|
||||
display: block !important;
|
||||
}
|
||||
@@ -113,9 +97,6 @@
|
||||
.btn-group.nowrap .dropdown-menu {
|
||||
width: 100%;
|
||||
}
|
||||
.card-login .btn-group {
|
||||
display: block;
|
||||
}
|
||||
.mass-actions-user .btn-group {
|
||||
float: none;
|
||||
}
|
||||
@@ -191,9 +172,6 @@
|
||||
.btn-group .btn i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.card-login .btn-group .btn {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.dt-sm-head-hidden .dtr-title {
|
||||
display: none !important;
|
||||
@@ -206,7 +184,7 @@
|
||||
.senders-mw220 {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
|
||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before,
|
||||
table.dataTable td.dt-control:before {
|
||||
@@ -215,7 +193,7 @@
|
||||
line-height: 2rem;
|
||||
margin-top: -15px;
|
||||
}
|
||||
|
||||
|
||||
li .dtr-data {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@@ -59,9 +59,6 @@ body.modal-open {
|
||||
.table-condensed > thead > tr > th, .table-condensed > tbody > tr > th, .table-condensed > tfoot > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > td {
|
||||
padding: 3px;
|
||||
}
|
||||
table tbody tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
table tbody tr td input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
29
data/web/domainadmin/index.php
Normal file
29
data/web/domainadmin/index.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
header('Location: /domainadmin/mailbox');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
$_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
|
||||
|
||||
$template = 'domainadmin_index.twig';
|
||||
$template_data = [
|
||||
'login_delay' => @$_SESSION['ldelay'],
|
||||
'custom_login' => customize('get', 'custom_login'),
|
||||
];
|
||||
|
||||
$js_minifier->add('/web/js/site/index.js');
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
58
data/web/domainadmin/mailbox.php
Normal file
58
data/web/domainadmin/mailbox.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||
header('Location: /domainadmin');
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
|
||||
|
||||
|
||||
$js_minifier->add('/web/js/site/mailbox.js');
|
||||
$js_minifier->add('/web/js/presets/sieveMailbox.js');
|
||||
$js_minifier->add('/web/js/site/pwgen.js');
|
||||
|
||||
$role = "domainadmin";
|
||||
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? 'true' : 'false';
|
||||
$allow_admin_email_login = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["ALLOW_ADMIN_EMAIL_LOGIN"])) ? 'true' : 'false';
|
||||
|
||||
// domains
|
||||
$domains = mailbox('get', 'domains');
|
||||
|
||||
// mailboxes
|
||||
$mailboxes = [];
|
||||
foreach ($domains as $domain) {
|
||||
foreach (mailbox('get', 'mailboxes', $domain) as $mailbox) {
|
||||
$mailboxes[] = $mailbox;
|
||||
}
|
||||
}
|
||||
|
||||
$template = 'mailbox.twig';
|
||||
$template_data = [
|
||||
'acl' => $_SESSION['acl'],
|
||||
'acl_json' => json_encode($_SESSION['acl']),
|
||||
'role' => $role,
|
||||
'is_dual' => $is_dual,
|
||||
'allow_admin_email_login' => $allow_admin_email_login,
|
||||
'global_filters' => mailbox('get', 'global_filter_details'),
|
||||
'domains' => $domains,
|
||||
'mailboxes' => $mailboxes,
|
||||
'lang_mailbox' => json_encode($lang['mailbox']),
|
||||
'lang_rl' => json_encode($lang['ratelimit']),
|
||||
'lang_edit' => json_encode($lang['edit']),
|
||||
'lang_datatables' => json_encode($lang['datatables']),
|
||||
];
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
44
data/web/domainadmin/user.php
Normal file
44
data/web/domainadmin/user.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
|
||||
|
||||
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
|
||||
|
||||
/*
|
||||
/ DOMAIN ADMIN
|
||||
*/
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
|
||||
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
|
||||
$tfa_data = get_tfa();
|
||||
$fido2_data = fido2(array("action" => "get_friendly_names"));
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
|
||||
$template = 'domainadmin.twig';
|
||||
$template_data = [
|
||||
'acl' => $_SESSION['acl'],
|
||||
'acl_json' => json_encode($_SESSION['acl']),
|
||||
'user_spam_score' => mailbox('get', 'spam_score', $username),
|
||||
'tfa_data' => $tfa_data,
|
||||
'fido2_data' => $fido2_data,
|
||||
'lang_user' => json_encode($lang['user']),
|
||||
'lang_datatables' => json_encode($lang['datatables']),
|
||||
];
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
|
||||
header('Location: /admin/dashboard');
|
||||
exit();
|
||||
}
|
||||
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
|
||||
header('Location: /user');
|
||||
exit();
|
||||
}
|
||||
else {
|
||||
header('Location: /domainadmin');
|
||||
exit();
|
||||
}
|
||||
|
||||
$js_minifier->add('/web/js/site/user.js');
|
||||
$js_minifier->add('/web/js/site/pwgen.js');
|
||||
|
||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||
@@ -48,6 +48,12 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
$rl = ratelimit('get', 'domain', $domain);
|
||||
$rlyhosts = relayhost('get');
|
||||
$domain_footer = mailbox('get', 'domain_wide_footer', $domain);
|
||||
$mta_sts = mailbox('get', 'mta_sts', $domain);
|
||||
if (count($mta_sts) == 0) {
|
||||
$mta_sts = false;
|
||||
} elseif (isset($mta_sts['mx'])) {
|
||||
$mta_sts['mx'] = implode(',', $mta_sts['mx']);
|
||||
}
|
||||
$template = 'edit/domain.twig';
|
||||
$template_data = [
|
||||
'acl' => $_SESSION['acl'],
|
||||
@@ -58,6 +64,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
'dkim' => dkim('details', $domain),
|
||||
'domain_details' => $result,
|
||||
'domain_footer' => $domain_footer,
|
||||
'mta_sts' => $mta_sts,
|
||||
'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]),
|
||||
'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address'),
|
||||
'alias_domains' => mailbox('get', 'alias_domains', $_GET["domain"])
|
||||
@@ -125,13 +132,15 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
||||
'mailbox' => $mailbox,
|
||||
'rl' => $rl,
|
||||
'pushover_data' => $pushover_data,
|
||||
'get_tagging_options' => mailbox('get', 'delimiter_action', $mailbox),
|
||||
'quarantine_notification' => $quarantine_notification,
|
||||
'quarantine_category' => $quarantine_category,
|
||||
'get_tls_policy' => $get_tls_policy,
|
||||
'rlyhosts' => $rlyhosts,
|
||||
'sender_acl_handles' => mailbox('get', 'sender_acl_handles', $mailbox),
|
||||
'user_acls' => acl('get', 'user', $mailbox),
|
||||
'mailbox_details' => $result
|
||||
'mailbox_details' => $result,
|
||||
'iam_settings' => $iam_settings,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 17 KiB |
@@ -71,6 +71,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
// Init records array
|
||||
$spf_link = '<a href="http://www.open-spf.org/SPF_Record_Syntax/" target="_blank">SPF Record Syntax</a><br />';
|
||||
$dmarc_link = '<a href="https://www.kitterman.com/dmarc/assistant.html" target="_blank">DMARC Assistant</a>';
|
||||
$mtasts_report_link = '<a href="https://mxtoolbox.com/dmarc/smtp-tls/how-to-setup-smtp-tls-reports" target="_blank">TLS Report Record Syntax</a>';
|
||||
|
||||
$records = array();
|
||||
|
||||
@@ -128,6 +129,27 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
);
|
||||
}
|
||||
|
||||
$mta_sts = mailbox('get', 'mta_sts', $domain);
|
||||
if (count($mta_sts) > 0 && $mta_sts['active'] == 1) {
|
||||
if (!in_array($domain, $alias_domains)) {
|
||||
$records[] = array(
|
||||
'mta-sts.' . $domain,
|
||||
'CNAME',
|
||||
$mailcow_hostname
|
||||
);
|
||||
}
|
||||
$records[] = array(
|
||||
'_mta-sts.' . $domain,
|
||||
'TXT',
|
||||
"v={$mta_sts['version']};id={$mta_sts['id']};",
|
||||
);
|
||||
$records[] = array(
|
||||
'_smtp._tls.' . $domain,
|
||||
'TXT',
|
||||
$mtasts_report_link,
|
||||
);
|
||||
}
|
||||
|
||||
$records[] = array(
|
||||
$domain,
|
||||
'TXT',
|
||||
@@ -341,15 +363,25 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
}
|
||||
|
||||
foreach ($currents as &$current) {
|
||||
if ($current['type'] == "TXT" &&
|
||||
stripos(strtolower($current['txt']), 'v=sts') === 0) {
|
||||
if (strtolower($current[$data_field[$current['type']]]) == strtolower($record[2])) {
|
||||
$state = state_good;
|
||||
}
|
||||
else {
|
||||
$state = state_nomatch;
|
||||
}
|
||||
$state .= '<br />' . $current[$data_field[$current['type']]];
|
||||
}
|
||||
if ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=dmarc') === 0 &&
|
||||
$record[2] == $dmarc_link) {
|
||||
stripos($current['txt'], 'v=dmarc') === 0 &&
|
||||
$record[2] == $dmarc_link) {
|
||||
$current['txt'] = str_replace(' ', '', $current['txt']);
|
||||
$state = $current[$data_field[$current['type']]] . state_optional;
|
||||
}
|
||||
elseif ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=spf') === 0 &&
|
||||
$record[2] == $spf_link) {
|
||||
stripos($current['txt'], 'v=spf') === 0 &&
|
||||
$record[2] == $spf_link) {
|
||||
$state = state_nomatch;
|
||||
$rslt = get_spf_allowed_hosts($record[0], true);
|
||||
if (in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)) {
|
||||
@@ -358,8 +390,8 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
$state .= '<br />' . $current[$data_field[$current['type']]] . state_optional;
|
||||
}
|
||||
elseif ($current['type'] == 'TXT' &&
|
||||
stripos($current['txt'], 'v=dkim') === 0 &&
|
||||
stripos($record[2], 'v=dkim') === 0) {
|
||||
stripos($current['txt'], 'v=dkim') === 0 &&
|
||||
stripos($record[2], 'v=dkim') === 0) {
|
||||
preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $current[$data_field[$current['type']]], $dkim_matches_current);
|
||||
preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $record[2], $dkim_matches_good);
|
||||
if ($dkim_matches_current[1] == $dkim_matches_good[1]) {
|
||||
@@ -367,7 +399,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
|
||||
}
|
||||
}
|
||||
elseif ($current['type'] != 'TXT' &&
|
||||
isset($data_field[$current['type']]) && $state != state_good) {
|
||||
isset($data_field[$current['type']]) && $state != state_good) {
|
||||
$state = state_nomatch;
|
||||
if ($current[$data_field[$current['type']]] == $record[2]) {
|
||||
$state = state_good;
|
||||
|
||||
@@ -26,23 +26,25 @@ if (is_array($alertbox_log_parser)) {
|
||||
|
||||
// map tfa details for twig
|
||||
$pending_tfa_authmechs = [];
|
||||
foreach($_SESSION['pending_tfa_methods'] as $authdata){
|
||||
$pending_tfa_authmechs[$authdata['authmech']] = false;
|
||||
}
|
||||
if (isset($pending_tfa_authmechs['webauthn'])) {
|
||||
$pending_tfa_authmechs['webauthn'] = true;
|
||||
}
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
&& isset($pending_tfa_authmechs['yubi_otp'])) {
|
||||
$pending_tfa_authmechs['yubi_otp'] = true;
|
||||
}
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
&& !isset($pending_tfa_authmechs['yubi_otp'])
|
||||
&& isset($pending_tfa_authmechs['totp'])) {
|
||||
$pending_tfa_authmechs['totp'] = true;
|
||||
}
|
||||
if (isset($pending_tfa_authmechs['u2f'])) {
|
||||
$pending_tfa_authmechs['u2f'] = true;
|
||||
if (array_key_exists('pending_tfa_methods', $_SESSION)) {
|
||||
foreach($_SESSION['pending_tfa_methods'] as $authdata){
|
||||
$pending_tfa_authmechs[$authdata['authmech']] = false;
|
||||
}
|
||||
if (isset($pending_tfa_authmechs['webauthn'])) {
|
||||
$pending_tfa_authmechs['webauthn'] = true;
|
||||
}
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
&& isset($pending_tfa_authmechs['yubi_otp'])) {
|
||||
$pending_tfa_authmechs['yubi_otp'] = true;
|
||||
}
|
||||
if (!isset($pending_tfa_authmechs['webauthn'])
|
||||
&& !isset($pending_tfa_authmechs['yubi_otp'])
|
||||
&& isset($pending_tfa_authmechs['totp'])) {
|
||||
$pending_tfa_authmechs['totp'] = true;
|
||||
}
|
||||
if (isset($pending_tfa_authmechs['u2f'])) {
|
||||
$pending_tfa_authmechs['u2f'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// globals
|
||||
@@ -66,6 +68,8 @@ $globalVariables = [
|
||||
'lang_acl' => json_encode($lang['acl']),
|
||||
'lang_tfa' => json_encode($lang['tfa']),
|
||||
'lang_fido2' => json_encode($lang['fido2']),
|
||||
'lang_success' => json_encode($lang['success']),
|
||||
'lang_danger' => json_encode($lang['danger']),
|
||||
'docker_timeout' => $DOCKER_TIMEOUT,
|
||||
'session_lifetime' => (int)$SESSION_LIFETIME,
|
||||
'csrf_token' => $_SESSION['CSRF']['TOKEN'],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
function acl($_action, $_scope = null, $_data = null) {
|
||||
function acl($_action, $_scope = null, $_data = null, $_extra = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
@@ -24,7 +24,7 @@ function acl($_action, $_scope = null, $_data = null) {
|
||||
}
|
||||
// Users cannot change their own ACL
|
||||
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $username)
|
||||
|| ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin')) {
|
||||
|| ($_SESSION['mailcow_cc_role'] != 'admin' && $_SESSION['mailcow_cc_role'] != 'domainadmin' && $_SESSION['access_all_exception'] != '1')) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
|
||||
@@ -34,7 +34,7 @@ function acl($_action, $_scope = null, $_data = null) {
|
||||
}
|
||||
// Read all available acl options by calling acl(get)
|
||||
// Set all available acl options we cannot find in the post data to 0, else 1
|
||||
$is_now = acl('get', 'user', $username);
|
||||
$is_now = acl('get', 'user', $username, $_extra);
|
||||
if (!empty($is_now)) {
|
||||
foreach ($is_now as $acl_now_name => $acl_now_val) {
|
||||
$set_acls[$acl_now_name] = (isset($acl_post[$acl_now_name])) ? 1 : 0;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
function app_passwd($_action, $_data = null) {
|
||||
global $pdo;
|
||||
global $lang;
|
||||
global $pdo;
|
||||
global $lang;
|
||||
$_data_log = $_data;
|
||||
!isset($_data_log['app_passwd']) ?: $_data_log['app_passwd'] = '*';
|
||||
!isset($_data_log['app_passwd2']) ?: $_data_log['app_passwd2'] = '*';
|
||||
@@ -43,20 +43,7 @@ function app_passwd($_action, $_data = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'password_complexity'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($password != $password2) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'password_mismatch'
|
||||
);
|
||||
if (password_check($password, $password2) !== true) {
|
||||
return false;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
@@ -88,15 +75,15 @@ function app_passwd($_action, $_data = null) {
|
||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||
'msg' => 'app_passwd_added'
|
||||
);
|
||||
break;
|
||||
break;
|
||||
case 'edit':
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
$is_now = app_passwd('details', $id);
|
||||
if (!empty($is_now)) {
|
||||
$app_name = (!empty($_data['app_name'])) ? $_data['app_name'] : $is_now['name'];
|
||||
$password = (!empty($_data['password'])) ? $_data['password'] : null;
|
||||
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
|
||||
$password = (!empty($_data['app_passwd'])) ? $_data['app_passwd'] : null;
|
||||
$password2 = (!empty($_data['app_passwd2'])) ? $_data['app_passwd2'] : null;
|
||||
if (isset($_data['protocols'])) {
|
||||
$protocols = (array)$_data['protocols'];
|
||||
$imap_access = (in_array('imap_access', $protocols)) ? 1 : 0;
|
||||
@@ -126,20 +113,7 @@ function app_passwd($_action, $_data = null) {
|
||||
}
|
||||
$app_name = htmlspecialchars(trim($app_name));
|
||||
if (!empty($password) && !empty($password2)) {
|
||||
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'password_complexity'
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if ($password != $password2) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||
'msg' => 'password_mismatch'
|
||||
);
|
||||
if (password_check($password, $password2) !== true) {
|
||||
continue;
|
||||
}
|
||||
$password_hashed = hash_password($password);
|
||||
@@ -182,7 +156,7 @@ function app_passwd($_action, $_data = null) {
|
||||
'msg' => array('object_modified', htmlspecialchars(implode(', ', $ids)))
|
||||
);
|
||||
}
|
||||
break;
|
||||
break;
|
||||
case 'delete':
|
||||
$ids = (array)$_data['id'];
|
||||
foreach ($ids as $id) {
|
||||
@@ -213,19 +187,17 @@ function app_passwd($_action, $_data = null) {
|
||||
'msg' => array('app_passwd_removed', htmlspecialchars($id))
|
||||
);
|
||||
}
|
||||
break;
|
||||
break;
|
||||
case 'get':
|
||||
$app_passwds = array();
|
||||
$stmt = $pdo->prepare("SELECT `id`, `name` FROM `app_passwd` WHERE `mailbox` = :username");
|
||||
$stmt->execute(array(':username' => $username));
|
||||
$app_passwds = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $app_passwds;
|
||||
break;
|
||||
break;
|
||||
case 'details':
|
||||
$app_passwd_data = array();
|
||||
$stmt = $pdo->prepare("SELECT *
|
||||
FROM `app_passwd`
|
||||
WHERE `id` = :id");
|
||||
$stmt = $pdo->prepare("SELECT * FROM `app_passwd` WHERE `id` = :id");
|
||||
$stmt->execute(array(':id' => $_data));
|
||||
$app_passwd_data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($app_passwd_data)) {
|
||||
@@ -237,6 +209,6 @@ function app_passwd($_action, $_data = null) {
|
||||
}
|
||||
$app_passwd_data['name'] = htmlspecialchars(trim($app_passwd_data['name']));
|
||||
return $app_passwd_data;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
740
data/web/inc/functions.auth.inc.php
Normal file
740
data/web/inc/functions.auth.inc.php
Normal file
@@ -0,0 +1,740 @@
|
||||
<?php
|
||||
function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
|
||||
global $pdo;
|
||||
global $valkey;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
$role = $extra['role'];
|
||||
|
||||
// Try validate admin
|
||||
if (!isset($role) || $role == "admin") {
|
||||
$result = admin_login($user, $pass);
|
||||
if ($result !== false){
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Try validate domain admin
|
||||
if (!isset($role) || $role == "domain_admin") {
|
||||
$result = domainadmin_login($user, $pass);
|
||||
if ($result !== false) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Try validate app password
|
||||
if (!isset($role) || $role == "app") {
|
||||
$result = apppass_login($user, $pass, $app_passwd_data);
|
||||
if ($result !== false) {
|
||||
if ($app_passwd_data['eas'] === true) {
|
||||
$service = 'EAS';
|
||||
} elseif ($app_passwd_data['dav'] === true) {
|
||||
$service = 'DAV';
|
||||
} else {
|
||||
$service = 'NONE';
|
||||
}
|
||||
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
|
||||
set_sasl_log($user, $real_rip, $service, $pass);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// Try validate user
|
||||
if (!isset($role) || $role == "user") {
|
||||
$result = user_login($user, $pass);
|
||||
if ($result !== false) {
|
||||
if ($app_passwd_data['eas'] === true) {
|
||||
$service = 'EAS';
|
||||
} elseif ($app_passwd_data['dav'] === true) {
|
||||
$service = 'DAV';
|
||||
} else {
|
||||
$service = 'MAILCOWUI';
|
||||
}
|
||||
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
|
||||
set_sasl_log($user, $real_rip, $service);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
// skip log and only return false if it's an internal request
|
||||
if ($is_internal == true) return false;
|
||||
|
||||
if (!isset($_SESSION['ldelay'])) {
|
||||
$_SESSION['ldelay'] = "0";
|
||||
$valkey->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
||||
}
|
||||
elseif (!isset($_SESSION['mailcow_cc_username'])) {
|
||||
$_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
|
||||
$valkey->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
||||
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => 'login_failed'
|
||||
);
|
||||
|
||||
sleep($_SESSION['ldelay']);
|
||||
return false;
|
||||
}
|
||||
|
||||
function admin_login($user, $pass){
|
||||
global $pdo;
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => 'malformed_username'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = strtolower(trim($user));
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
WHERE `superadmin` = '1'
|
||||
AND `active` = '1'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass)) {
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
|
||||
// active tfa authenticators found, set pending user login
|
||||
$_SESSION['pending_mailcow_cc_username'] = $user;
|
||||
$_SESSION['pending_mailcow_cc_role'] = "admin";
|
||||
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
|
||||
unset($_SESSION['ldelay']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'info',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => 'awaiting_tfa_confirmation'
|
||||
);
|
||||
return "pending";
|
||||
} else {
|
||||
unset($_SESSION['ldelay']);
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => array('logged_in_as', $user)
|
||||
);
|
||||
return "admin";
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
function domainadmin_login($user, $pass){
|
||||
global $pdo;
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => 'malformed_username'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||
WHERE `superadmin` = '0'
|
||||
AND `active`='1'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass) !== false) {
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
|
||||
$_SESSION['pending_mailcow_cc_username'] = $user;
|
||||
$_SESSION['pending_mailcow_cc_role'] = "domainadmin";
|
||||
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
|
||||
unset($_SESSION['ldelay']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'info',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => 'awaiting_tfa_confirmation'
|
||||
);
|
||||
return "pending";
|
||||
}
|
||||
else {
|
||||
unset($_SESSION['ldelay']);
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => array('logged_in_as', $user)
|
||||
);
|
||||
return "domainadmin";
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
function user_login($user, $pass, $extra = null){
|
||||
global $pdo;
|
||||
global $iam_provider;
|
||||
global $iam_settings;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
$service = $extra['service'];
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => 'malformed_username'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare("SELECT
|
||||
mailbox.*,
|
||||
domain.active AS d_active
|
||||
FROM `mailbox`
|
||||
INNER JOIN domain on mailbox.domain = domain.domain
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// user does not exist, try call idp login and create user if possible via rest flow
|
||||
if (!$row){
|
||||
$result = false;
|
||||
if ($iam_settings['authsource'] == 'keycloak' && intval($iam_settings['mailpassword_flow']) == 1){
|
||||
$result = keycloak_mbox_login_rest($user, $pass, array('is_internal' => $is_internal, 'create' => true));
|
||||
} else if ($iam_settings['authsource'] == 'ldap') {
|
||||
$result = ldap_mbox_login($user, $pass, array('is_internal' => $is_internal, 'create' => true));
|
||||
}
|
||||
if ($result !== false){
|
||||
// double check if mailbox is active
|
||||
$stmt = $pdo->prepare("SELECT * FROM `mailbox`
|
||||
INNER JOIN domain on mailbox.domain = domain.domain
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `mailbox`.`active`='1'
|
||||
AND `domain`.`active`='1'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!empty($row)) {
|
||||
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
|
||||
$row['attributes'] = json_decode($row['attributes'], true);
|
||||
if (isset($service)) {
|
||||
$key = strtolower($service) . "_access";
|
||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
clear_session();
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
|
||||
$row['attributes'] = json_decode($row['attributes'], true);
|
||||
if (isset($service)) {
|
||||
$key = strtolower($service) . "_access";
|
||||
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
switch ($row['authsource']) {
|
||||
case 'keycloak':
|
||||
// user authsource is keycloak, try using via rest flow
|
||||
if (intval($iam_settings['mailpassword_flow']) == 1){
|
||||
$result = keycloak_mbox_login_rest($user, $pass, array('is_internal' => $is_internal));
|
||||
if ($result !== false) {
|
||||
// double check if mailbox and domain is active
|
||||
$stmt = $pdo->prepare("SELECT * FROM `mailbox`
|
||||
INNER JOIN domain on mailbox.domain = domain.domain
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `mailbox`.`active`='1'
|
||||
AND `domain`.`active`='1'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($row)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
|
||||
// authenticators found, init TFA flow
|
||||
$_SESSION['pending_mailcow_cc_username'] = $user;
|
||||
$_SESSION['pending_mailcow_cc_role'] = "user";
|
||||
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
|
||||
unset($_SESSION['ldelay']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: Keycloak'),
|
||||
'msg' => array('logged_in_as', $user)
|
||||
);
|
||||
return "pending";
|
||||
} else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
|
||||
// no authenticators found, login successfull
|
||||
if (!$is_internal){
|
||||
unset($_SESSION['ldelay']);
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: Keycloak'),
|
||||
'msg' => array('logged_in_as', $user)
|
||||
);
|
||||
}
|
||||
return "user";
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'ldap':
|
||||
// user authsource is ldap
|
||||
$result = ldap_mbox_login($user, $pass, array('is_internal' => $is_internal));
|
||||
if ($result !== false) {
|
||||
// double check if mailbox and domain is active
|
||||
$stmt = $pdo->prepare("SELECT * FROM `mailbox`
|
||||
INNER JOIN domain on mailbox.domain = domain.domain
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `mailbox`.`active`='1'
|
||||
AND `domain`.`active`='1'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (empty($row)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
|
||||
// authenticators found, init TFA flow
|
||||
$_SESSION['pending_mailcow_cc_username'] = $user;
|
||||
$_SESSION['pending_mailcow_cc_role'] = "user";
|
||||
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
|
||||
unset($_SESSION['ldelay']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: LDAP'),
|
||||
'msg' => array('logged_in_as', $user)
|
||||
);
|
||||
return "pending";
|
||||
} else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
|
||||
// no authenticators found, login successfull
|
||||
if (!$is_internal){
|
||||
unset($_SESSION['ldelay']);
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: LDAP'),
|
||||
'msg' => array('logged_in_as', $user)
|
||||
);
|
||||
}
|
||||
return "user";
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
break;
|
||||
case 'mailcow':
|
||||
if ($row['active'] != 1 || $row['d_active'] != 1) {
|
||||
return false;
|
||||
}
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass) !== false) {
|
||||
|
||||
if (intval($row['attributes']['force_pw_update']) == 1) {
|
||||
$_SESSION['pending_pw_update'] = true;
|
||||
}
|
||||
|
||||
// check for tfa authenticators
|
||||
$authenticators = get_tfa($user);
|
||||
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) {
|
||||
// authenticators found, init TFA flow
|
||||
$_SESSION['pending_mailcow_cc_username'] = $user;
|
||||
$_SESSION['pending_mailcow_cc_role'] = "user";
|
||||
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
|
||||
unset($_SESSION['ldelay']);
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: mailcow'),
|
||||
'msg' => array('logged_in_as', $user)
|
||||
);
|
||||
return "pending";
|
||||
} else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
|
||||
// no authenticators found, login successfull
|
||||
if (!$is_internal){
|
||||
unset($_SESSION['ldelay']);
|
||||
// Reactivate TFA if it was set to "deactivate TFA for next login"
|
||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
|
||||
$stmt->execute(array(':user' => $user));
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Provider: mailcow'),
|
||||
'msg' => array('logged_in_as', $user)
|
||||
);
|
||||
}
|
||||
return "user";
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
function apppass_login($user, $pass, $app_passwd_data, $extra = null){
|
||||
global $pdo;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => 'malformed_username'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$protocol = false;
|
||||
if ($app_passwd_data['eas']){
|
||||
$protocol = 'eas';
|
||||
} else if ($app_passwd_data['dav']){
|
||||
$protocol = 'dav';
|
||||
} else if ($app_passwd_data['smtp']){
|
||||
$protocol = 'smtp';
|
||||
} else if ($app_passwd_data['imap']){
|
||||
$protocol = 'imap';
|
||||
} else if ($app_passwd_data['sieve']){
|
||||
$protocol = 'sieve';
|
||||
} else if ($app_passwd_data['pop3']){
|
||||
$protocol = 'pop3';
|
||||
} else if (!$is_internal) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// fetch app password data
|
||||
$stmt = $pdo->prepare("SELECT `app_passwd`.*, `app_passwd`.`password` as `password`, `app_passwd`.`id` as `app_passwd_id` FROM `app_passwd`
|
||||
INNER JOIN `mailbox` ON `mailbox`.`username` = `app_passwd`.`mailbox`
|
||||
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
|
||||
WHERE `mailbox`.`kind` NOT REGEXP 'location|thing|group'
|
||||
AND `mailbox`.`active` = '1'
|
||||
AND `domain`.`active` = '1'
|
||||
AND `app_passwd`.`active` = '1'
|
||||
AND `app_passwd`.`mailbox` = :user"
|
||||
);
|
||||
// fetch password data
|
||||
$stmt->execute(array(
|
||||
':user' => $user,
|
||||
));
|
||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if ($protocol && $row[$protocol . '_access'] != '1'){
|
||||
continue;
|
||||
}
|
||||
|
||||
// verify password
|
||||
if (verify_hash($row['password'], $pass) !== false) {
|
||||
$_SESSION['app_passwd_id'] = $row['app_passwd_id'];
|
||||
unset($_SESSION['ldelay']);
|
||||
return "user";
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
// Keycloak REST Api Flow - auth user by mailcow_password attribute
|
||||
// This password will be used for direct UI, IMAP and SMTP Auth
|
||||
// To use direct user credentials, only Authorization Code Flow is valid
|
||||
function keycloak_mbox_login_rest($user, $pass, $extra = null){
|
||||
global $pdo;
|
||||
global $iam_provider;
|
||||
global $iam_settings;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
$create = $extra['create'];
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => 'malformed_username'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!$iam_provider) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// get access_token for service account of mailcow client
|
||||
$admin_token = identity_provider("get-keycloak-admin-token");
|
||||
|
||||
// get the mailcow_password attribute from keycloak user
|
||||
$url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users";
|
||||
$queryParams = array('email' => $user, 'exact' => true);
|
||||
$queryString = http_build_query($queryParams);
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_TIMEOUT, 7);
|
||||
curl_setopt($curl, CURLOPT_URL, $url . '?' . $queryString);
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER, array(
|
||||
'Authorization: Bearer ' . $admin_token,
|
||||
'Content-Type: application/json'
|
||||
));
|
||||
$user_res = json_decode(curl_exec($curl), true)[0];
|
||||
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
curl_close($curl);
|
||||
if ($code != 200) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Identity Provider returned HTTP ' . $code),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!isset($user_res['attributes']['mailcow_password']) || !is_array($user_res['attributes']['mailcow_password'])){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'User has no mailcow_password attribute'),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (empty($user_res['attributes']['mailcow_password'][0])){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', "User's mailcow_password attribute is empty"),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// validate mailcow_password
|
||||
$mailcow_password = $user_res['attributes']['mailcow_password'][0];
|
||||
if (!verify_hash($mailcow_password, $pass)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// get mapped template
|
||||
$user_template = $user_res['attributes']['mailcow_template'][0];
|
||||
$mapper_key = array_search($user_template, $iam_settings['mappers']);
|
||||
|
||||
if (!$create) {
|
||||
// login success
|
||||
if ($mapper_key !== false) {
|
||||
// update user
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
mailbox('edit', 'mailbox_from_template', array(
|
||||
'username' => $user,
|
||||
'name' => $user_res['name'],
|
||||
'template' => $iam_settings['templates'][$mapper_key]
|
||||
));
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
}
|
||||
return 'user';
|
||||
}
|
||||
|
||||
// check if login provisioning is enabled before creating user
|
||||
if (!$iam_settings['login_provisioning']){
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, "Auto-create users on login is deactivated"),
|
||||
'msg' => 'login_failed'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// check if matching attribute exist
|
||||
if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) {
|
||||
if (!empty($iam_settings['default_template'])) {
|
||||
$mbox_template = $iam_settings['default_template'];
|
||||
} else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'No matching attribute mapping was found'),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
}
|
||||
|
||||
// create mailbox
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
$create_res = mailbox('add', 'mailbox_from_template', array(
|
||||
'domain' => explode('@', $user)[1],
|
||||
'local_part' => explode('@', $user)[0],
|
||||
'name' => $user_res['name'],
|
||||
'authsource' => 'keycloak',
|
||||
'template' => $mbox_template
|
||||
));
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
if (!$create_res){
|
||||
clear_session();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Could not create mailbox on login'),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return 'user';
|
||||
}
|
||||
function ldap_mbox_login($user, $pass, $extra = null){
|
||||
global $pdo;
|
||||
global $iam_provider;
|
||||
global $iam_settings;
|
||||
|
||||
$is_internal = $extra['is_internal'];
|
||||
$create = $extra['create'];
|
||||
|
||||
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*'),
|
||||
'msg' => 'malformed_username'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!$iam_provider) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$ldap_query = $iam_provider->query();
|
||||
if (!empty($iam_settings['filter'])) {
|
||||
$ldap_query = $ldap_query->rawFilter($iam_settings['filter']);
|
||||
}
|
||||
$ldap_query = $ldap_query->where($iam_settings['username_field'], '=', $user)
|
||||
->select([$iam_settings['username_field'], $iam_settings['attribute_field'], 'displayname', 'distinguishedname', 'dn']);
|
||||
|
||||
$user_res = $ldap_query->firstOrFail();
|
||||
} catch (Exception $e) {
|
||||
// clear $_SESSION['return'] to not leak data
|
||||
$_SESSION['return'] = array();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (!$iam_provider->auth()->attempt($user_res['dn'], $pass)) {
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// clear $_SESSION['return'] to not leak data
|
||||
$_SESSION['return'] = array();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// get mapped template
|
||||
$user_template = $user_res[$iam_settings['attribute_field']][0];
|
||||
$mapper_key = array_search($user_template, $iam_settings['mappers']);
|
||||
|
||||
if (!$create) {
|
||||
// login success
|
||||
if ($mapper_key !== false) {
|
||||
// update user
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
mailbox('edit', 'mailbox_from_template', array(
|
||||
'username' => $user,
|
||||
'name' => $user_res['displayname'][0],
|
||||
'template' => $iam_settings['templates'][$mapper_key]
|
||||
));
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
}
|
||||
return 'user';
|
||||
}
|
||||
|
||||
// check if login provisioning is enabled before creating user
|
||||
if (!$iam_settings['login_provisioning']){
|
||||
if (!$is_internal){
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, "Auto-create users on login is deactivated"),
|
||||
'msg' => 'login_failed'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// check if matching attribute exist
|
||||
if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) {
|
||||
if (!empty($iam_settings['default_template'])) {
|
||||
$mbox_template = $iam_settings['default_template'];
|
||||
} else {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'No matching attribute mapping was found'),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$mbox_template = $iam_settings['templates'][$mapper_key];
|
||||
}
|
||||
|
||||
// create mailbox
|
||||
$_SESSION['access_all_exception'] = '1';
|
||||
$create_res = mailbox('add', 'mailbox_from_template', array(
|
||||
'domain' => explode('@', $user)[1],
|
||||
'local_part' => explode('@', $user)[0],
|
||||
'name' => $user_res['displayname'][0],
|
||||
'authsource' => 'ldap',
|
||||
'template' => $mbox_template
|
||||
));
|
||||
$_SESSION['access_all_exception'] = '0';
|
||||
if (!$create_res){
|
||||
clear_session();
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $user, '*', 'Could not create mailbox on login'),
|
||||
'msg' => 'generic_server_error'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return 'user';
|
||||
}
|
||||
@@ -122,10 +122,16 @@ function customize($_action, $_item, $_data = null) {
|
||||
case 'app_links':
|
||||
$apps = (array)$_data['app'];
|
||||
$links = (array)$_data['href'];
|
||||
$user_links = (array)$_data['user_href'];
|
||||
$hide = (array)$_data['hide'];
|
||||
$out = array();
|
||||
if (count($apps) == count($links)) {
|
||||
if (count($apps) == count($links) && count($apps) == count($user_links) && count($apps) == count($hide)) {
|
||||
for ($i = 0; $i < count($apps); $i++) {
|
||||
$out[] = array($apps[$i] => $links[$i]);
|
||||
$out[] = array($apps[$i] => array(
|
||||
'link' => $links[$i],
|
||||
'user_link' => $user_links[$i],
|
||||
'hide' => ($hide[$i] === '0' || $hide[$i] === 0) ? false : true
|
||||
));
|
||||
}
|
||||
try {
|
||||
$valkey->set('APP_LINKS', json_encode($out));
|
||||
@@ -198,6 +204,35 @@ function customize($_action, $_item, $_data = null) {
|
||||
'msg' => 'ip_check_opt_in_modified'
|
||||
);
|
||||
break;
|
||||
case 'custom_login':
|
||||
$hide_user_quicklink = ($_data['hide_user_quicklink'] == "1") ? 1 : 0;
|
||||
$hide_domainadmin_quicklink = ($_data['hide_domainadmin_quicklink'] == "1") ? 1 : 0;
|
||||
$hide_admin_quicklink = ($_data['hide_admin_quicklink'] == "1") ? 1 : 0;
|
||||
$force_sso = ($_data['force_sso'] == "1") ? 1 : 0;
|
||||
|
||||
$custom_login = array(
|
||||
"hide_user_quicklink" => $hide_user_quicklink,
|
||||
"hide_domainadmin_quicklink" => $hide_domainadmin_quicklink,
|
||||
"hide_admin_quicklink" => $hide_admin_quicklink,
|
||||
"force_sso" => $force_sso,
|
||||
);
|
||||
try {
|
||||
$valkey->set('CUSTOM_LOGIN', json_encode($custom_login));
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'success',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => 'custom_login_modified'
|
||||
);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'delete':
|
||||
@@ -256,7 +291,23 @@ function customize($_action, $_item, $_data = null) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return ($app_links) ? $app_links : false;
|
||||
|
||||
if (empty($app_links)){
|
||||
return [];
|
||||
}
|
||||
|
||||
// convert from old style
|
||||
foreach($app_links as $i => $entry){
|
||||
foreach($entry as $app => $link){
|
||||
if (empty($link['link']) && empty($link['user_link'])){
|
||||
$app_links[$i][$app] = array();
|
||||
$app_links[$i][$app]['link'] = $link;
|
||||
$app_links[$i][$app]['user_link'] = $link;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $app_links;
|
||||
break;
|
||||
case 'main_logo':
|
||||
case 'main_logo_dark':
|
||||
@@ -274,8 +325,10 @@ function customize($_action, $_item, $_data = null) {
|
||||
break;
|
||||
case 'ui_texts':
|
||||
try {
|
||||
$data['title_name'] = ($title_name = $valkey->get('TITLE_NAME')) ? $title_name : 'mailcow UI';
|
||||
$data['main_name'] = ($main_name = $valkey->get('MAIN_NAME')) ? $main_name : 'mailcow UI';
|
||||
$mailcow_hostname = strtolower(getenv("MAILCOW_HOSTNAME"));
|
||||
|
||||
$data['title_name'] = ($title_name = $valkey->get('TITLE_NAME')) ? $title_name : "$mailcow_hostname - mail UI";
|
||||
$data['main_name'] = ($main_name = $valkey->get('MAIN_NAME')) ? $main_name : "$mailcow_hostname - mail UI";
|
||||
$data['apps_name'] = ($apps_name = $valkey->get('APPS_NAME')) ? $apps_name : $lang['header']['apps'];
|
||||
$data['help_text'] = ($help_text = $valkey->get('HELP_TEXT')) ? $help_text : false;
|
||||
if (!empty($valkey->get('UI_IMPRESS'))) {
|
||||
@@ -335,6 +388,20 @@ function customize($_action, $_item, $_data = null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 'custom_login':
|
||||
try {
|
||||
$custom_login = $valkey->get('CUSTOM_LOGIN');
|
||||
return $custom_login ? json_decode($custom_login, true) : array();
|
||||
}
|
||||
catch (RedisException $e) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||
'msg' => array('redis_error', $e)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user