Compare commits

..

8 Commits

Author SHA1 Message Date
FreddleSpl0it
c3c68360dc Merge pull request #6391 from mailcow/staging
Update 2025-03
2025-03-25 08:10:50 +01:00
FreddleSpl0it
a632980871 Merge pull request #6336 from mailcow/staging
Update 2025-02
2025-02-27 11:48:57 +01:00
FreddleSpl0it
2d1ef41d32 Merge pull request #6335 from mailcow/staging
Update 2025-02
2025-02-27 11:05:55 +01:00
FreddleSpl0it
120366fec7 Merge pull request #6291 from mailcow/staging
Update 2025-01a
2025-02-04 13:55:30 +01:00
DerLinkman
244d4b8c4c compose: rollback clamd version until next major... accidentally pushed 2025-01-29 13:46:53 +01:00
DerLinkman
f92ddd86c5 clamd: update to 1.4.2 + build from source instead using alpine packages (#6273)
* clamd: update to 1.4.2 + build from source instead using alpine packages

* clamd: remove exposed ports from buildfile

* clamd: cleanup dockerfile
2025-01-29 09:49:04 +01:00
FreddleSpl0it
ba0349a911 Merge pull request #6256 from mailcow/staging
[Nginx] move conf.d include to end of nginx.conf
2025-01-23 14:55:38 +01:00
FreddleSpl0it
8caf09cd80 Merge pull request #6253 from mailcow/staging
2025-01
2025-01-23 12:01:38 +01:00
141 changed files with 2478 additions and 7760 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
.gitignore vendored
View File

@@ -75,4 +75,3 @@ refresh_images.sh
update_diffs/
create_cold_standby.sh
!data/conf/nginx/mailcow_auth.conf
data/conf/postfix/postfix-tlspol

View File

@@ -13,22 +13,6 @@ 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. 🐄

View File

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

View File

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

View File

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

View File

@@ -1,300 +0,0 @@
#!/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 AZ, az, 09)' >> mailcow.conf
echo '# This key is used to encrypt email addresses within SOGo URLs' >> mailcow.conf
echo "SOGO_URL_ENCRYPTION_KEY=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 16)" >> mailcow.conf
;;
*)
echo "${option}=" >> mailcow.conf
;;
esac
done
}

View File

@@ -159,6 +159,18 @@ 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"
@@ -206,7 +218,7 @@ while true; do
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
# Fetch certs for autoconfig and autodiscover subdomains
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig' 'mta-sts')
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
fi
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
@@ -287,7 +299,7 @@ while true; do
VALIDATED_CERTIFICATES+=("${CERT_NAME}")
# obtain server certificate if required
DOMAINS=${SERVER_SAN_VALIDATED[@]} /srv/obtain-certificate.sh rsa
ACME_CONTACT_PARAMETER=${ACME_CONTACT_PARAMETER} DOMAINS=${SERVER_SAN_VALIDATED[@]} /srv/obtain-certificate.sh rsa
RETURN="$?"
if [[ "$RETURN" == "0" ]]; then # 0 = cert created successfully
CERT_AMOUNT_CHANGED=1

View File

@@ -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} --account-key ${ACME_BASE}/acme/account.pem --disable-check --csr ${CSR} --acme-dir /var/www/acme/"
ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} \
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} \
--account-key ${ACME_BASE}/acme/account.pem \
--disable-check \
--csr ${CSR} \

View File

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

View File

@@ -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("Unknown PubSub received - %s" % json.dumps(data_json))
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
else:
dockerapi.logger.error("Unknown PubSub received - %s" % json.dumps(data_json))
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
await asyncio.sleep(0.0)
except asyncio.TimeoutError:

View File

@@ -3,7 +3,7 @@ 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.17
ARG GOSU_VERSION=1.16
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8

View File

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

View File

@@ -8,8 +8,7 @@ from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import COMMASPACE, formatdate
import jinja2
from jinja2 import TemplateError
from jinja2.sandbox import SandboxedEnvironment
from jinja2 import Template
import json
import redis
import time
@@ -76,27 +75,22 @@ try:
def notify_rcpt(rcpt, msg_count, quarantine_acl, category):
if category == "add_header": category = "add header"
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))
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))
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 = 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:
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 = 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
template = Template(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)
text = html2text.html2text(html)
count = 0
while count < 15:

View File

@@ -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.sandbox import SandboxedEnvironment
from jinja2 import Template
import redis
import time
import json
@@ -33,24 +33,16 @@ while True:
if r.get('QW_HTML'):
try:
env = SandboxedEnvironment()
template = env.from_string(r.get('QW_HTML'))
except Exception:
print("Error: Cannot parse quota template, falling back to default template.")
template = Template(r.get('QW_HTML'))
except:
print("Error: Cannot parse quarantine template, falling back to default template.")
with open('/templates/quota.tpl') as file_:
env = SandboxedEnvironment()
template = env.from_string(file_.read())
template = Template(file_.read())
else:
with open('/templates/quota.tpl') as file_:
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)
template = Template(file_.read())
html = template.render(username=username, percent=percent)
text = html2text.html2text(html)
try:

View File

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

View File

@@ -1,7 +1,5 @@
#!/usr/bin/env python3
DEBUG = False
import re
import os
import sys
@@ -22,13 +20,10 @@ 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
@@ -38,10 +33,12 @@ r = None
pubsub = None
clear_before_quit = False
def refreshF2boptions():
global f2boptions
global quit_now
global exit_code
f2boptions = {}
if not r.get('F2B_OPTIONS'):
@@ -55,9 +52,8 @@ def refreshF2boptions():
else:
try:
f2boptions = json.loads(r.get('F2B_OPTIONS'))
except ValueError as e:
logger.logCrit(
'Error loading F2B options: F2B_OPTIONS is not json. Exception: %s' % e)
except ValueError:
logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json')
quit_now = True
exit_code = 2
@@ -65,15 +61,15 @@ def refreshF2boptions():
r.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
@@ -115,7 +111,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'])
@@ -123,43 +119,31 @@ def ban(address):
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
ip = get_ip(address)
if not ip:
logdebug("No valid IP -- skipping ban()")
return
if not ip: return
address = str(ip)
self_network = ipaddress.ip_network(address)
with lock:
temp_whitelist = set(WHITELIST)
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
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
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()))
@@ -167,41 +151,34 @@ 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))
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
else:
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (
MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
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)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
return
logger.logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
r.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)
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
if net in bans:
logdebug("Unban for %s, setting attempts=0, ban_counter+=1" % net)
bans[net]['attempts'] = 0
bans[net]['ban_counter'] += 1
@@ -227,19 +204,17 @@ def permBan(net, unban=False):
if is_unbanned:
r.hdel('F2B_PERM_BANS', '%s' % net)
logger.logCrit('Removed host/network %s from denylist' % net)
logger.logCrit('Removed host/network %s from blacklist' % net)
elif is_banned:
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
logger.logCrit('Added host/network %s to denylist' % net)
logger.logCrit('Added host/network %s to blacklist' % 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:
@@ -300,35 +275,21 @@ 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 = r.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))
# 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)
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)
def mailcowChainOrder():
global lock
@@ -398,7 +359,7 @@ def whitelistUpdate():
with lock:
if Counter(new_whitelist) != Counter(WHITELIST):
WHITELIST = new_whitelist
logger.logInfo('Allowlist was changed, it has %s entries' % len(WHITELIST))
logger.logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
def blacklistUpdate():
@@ -414,7 +375,7 @@ def blacklistUpdate():
addban = set(new_blacklist).difference(BLACKLIST)
delban = set(BLACKLIST).difference(new_blacklist)
BLACKLIST = new_blacklist
logger.logInfo('Denylist was changed, it has %s entries' % len(BLACKLIST))
logger.logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
if addban:
for net in addban:
permBan(net=net)
@@ -425,43 +386,42 @@ 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 before_quit():
logdebug("before_quit called, clear_before_quit=%s" % clear_before_quit)
def berfore_quit():
if clear_before_quit:
clear()
if pubsub is not None:
pubsub.unsubscribe()
if __name__ == '__main__':
logger = Logger()
logdebug("Sys.argv: %s" % sys.argv)
atexit.register(before_quit)
atexit.register(berfore_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")
@@ -472,28 +432,23 @@ if __name__ == '__main__':
try:
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
logdebug(
"Connecting redis (SLAVEOF_IP:%s, PORT:%s)" % (redis_slaveof_ip, redis_slaveof_port))
if "".__eq__(redis_slaveof_ip):
r = redis.StrictRedis(
host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS'])
r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS'])
else:
r = redis.StrictRedis(
host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS'])
r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS'])
r.ping()
pubsub = r.pubsub()
except Exception as ex:
logdebug(
'Redis connection failed: %s - trying again in 3 seconds' % (ex))
print('%s - trying again in 3 seconds' % (ex))
time.sleep(3)
else:
break
logger.set_redis(r)
logdebug("Redis connection established, setting up F2B keys")
# rename fail2ban to netfilter
if r.exists('F2B_LOG'):
logdebug("Renaming F2B_LOG to NETFILTER_LOG")
r.rename('F2B_LOG', 'NETFILTER_LOG')
# clear bans in redis
r.delete('F2B_ACTIVE_BANS')
r.delete('F2B_PERM_BANS')
@@ -508,7 +463,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:
@@ -544,5 +499,4 @@ if __name__ == '__main__':
while not quit_now:
time.sleep(0.5)
logdebug("Exiting with code %s" % exit_code)
sys.exit(exit_code)
sys.exit(exit_code)

View File

@@ -1,6 +1,5 @@
import time
import json
import datetime
class Logger:
def __init__(self):
@@ -9,28 +8,17 @@ class Logger:
def set_redis(self, redis):
self.r = redis
def _format_timestamp(self):
# Local time with milliseconds
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def log(self, priority, message):
# build redis-friendly dict
tolog = {
'time': int(round(time.time())), # keep raw timestamp for Redis
'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
tolog = {}
tolog['time'] = int(round(time.time()))
tolog['priority'] = priority
tolog['message'] = message
print(message)
if self.r is not None:
try:
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
except Exception as ex:
print(f'{ts} WARN: Failed logging to redis: {ex}', flush=True)
print('Failed logging to redis: %s' % (ex))
def logWarn(self, message):
self.log('warn', message)
@@ -39,4 +27,4 @@ class Logger:
self.log('crit', message)
def logInfo(self, message):
self.log('info', message)
self.log('info', message)

View File

@@ -10,7 +10,7 @@ def includes_conf(env, template_vars):
server_name_config = f"server_name {template_vars['MAILCOW_HOSTNAME']} autodiscover.* autoconfig.* {' '.join(template_vars['ADDITIONAL_SERVER_NAMES'])};"
listen_plain_config = f"listen {template_vars['HTTP_PORT']};"
listen_ssl_config = f"listen {template_vars['HTTPS_PORT']};"
if template_vars['ENABLE_IPV6']:
if not template_vars['DISABLE_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"),
'ENABLE_IPV6': os.getenv("ENABLE_IPV6", "true").lower() != "false",
'DISABLE_IPv6': os.getenv("DISABLE_IPv6", "n").lower() in ("y", "yes"),
'HTTP_REDIRECT': os.getenv("HTTP_REDIRECT", "n").lower() in ("y", "yes"),
}

View File

@@ -32,13 +32,6 @@ 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'))
@@ -120,7 +113,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

View File

@@ -1,25 +1,21 @@
FROM php:8.4-fpm-alpine3.22
FROM php:8.2-fpm-alpine3.21
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG APCU_PECL_VERSION=5.1.27
ARG APCU_PECL_VERSION=5.1.24
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
ARG IMAGICK_PECL_VERSION=3.8.0
ARG IMAGICK_PECL_VERSION=3.7.0
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MAILPARSE_PECL_VERSION=3.1.9
ARG MAILPARSE_PECL_VERSION=3.1.8
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MEMCACHED_PECL_VERSION=3.3.0
ARG MEMCACHED_PECL_VERSION=3.2.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
ARG REDIS_PECL_VERSION=6.2.0
ARG REDIS_PECL_VERSION=6.1.0
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
ARG COMPOSER_VERSION=2.8.6
# renovate: datasource=github-tags depName=php/pecl-text-pspell versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG PSPELL_PECL_VERSION=1.0.1
# renovate: datasource=github-tags depName=php/pecl-mail-imap versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG IMAP_PECL_VERSION=1.0.3
RUN apk update && apk add -U --no-cache autoconf \
RUN apk add -U --no-cache autoconf \
aspell-dev \
aspell-libs \
bash \
@@ -72,18 +68,18 @@ RUN apk update && apk add -U --no-cache autoconf \
&& pecl install mailparse-${MAILPARSE_PECL_VERSION} \
&& pecl install memcached-${MEMCACHED_PECL_VERSION} \
&& pecl install redis-${REDIS_PECL_VERSION} \
&& pecl install pspell-${PSPELL_PECL_VERSION} \
&& pecl install --configureoptions='with-kerberos="no" with-imap="yes" with-imap-ssl="yes"' imap-${IMAP_PECL_VERSION} \
&& docker-php-ext-enable apcu imagick memcached mailparse redis pspell imap \
&& docker-php-ext-enable apcu imagick memcached mailparse redis \
&& pecl clear-cache \
&& docker-php-ext-configure intl \
&& docker-php-ext-configure exif \
&& docker-php-ext-configure gd --with-freetype=/usr/include/ \
&& docker-php-ext-configure gd --with-freetype=/usr/include/ \
--with-jpeg=/usr/include/ \
--with-webp \
--with-xpm \
--with-avif \
&& docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql soap sockets zip bcmath gmp \
&& docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql pspell soap sockets zip bcmath gmp \
&& docker-php-ext-configure imap --with-imap --with-imap-ssl \
&& docker-php-ext-install -j 4 imap \
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \
&& mv composer.phar /usr/local/bin/composer \
&& chmod +x /usr/local/bin/composer \

View File

@@ -1,50 +0,0 @@
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-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
COPY postfix-tlspol.sh /opt/postfix-tlspol.sh
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
COPY docker-entrypoint.sh /docker-entrypoint.sh
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"]

View File

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

View File

@@ -1,52 +0,0 @@
#!/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 ${REDIS_SLAVEOF_IP} ]]; then
export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
else
export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
fi
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..."
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

View File

@@ -1,8 +0,0 @@
#!/bin/bash
printf "READY\n";
while read line; do
echo "Processing Event: $line" >&2;
kill -3 $(cat "/var/run/supervisord.pid")
done < /dev/stdin

View File

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

View File

@@ -1,45 +0,0 @@
@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_redis_ui_log {
redis(
host("`REDIS_SLAVEOF_IP`")
persist-name("redis1")
port(`REDIS_SLAVEOF_PORT`)
auth("`REDISPASS`")
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
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_redis_ui_log);
};

View File

@@ -1,45 +0,0 @@
@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_redis_ui_log {
redis(
host("redis-mailcow")
persist-name("redis1")
port(6379)
auth("`REDISPASS`")
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_redis_ui_log);
};

View File

@@ -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 \

View File

@@ -524,4 +524,4 @@ if [[ $? != 0 ]]; then
else
postfix -c /opt/postfix/conf start
sleep 126144000
fi
fi

View File

@@ -2,7 +2,7 @@ FROM debian:bookworm-slim
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG RSPAMD_VER=rspamd_3.13.2-1~8bf602278
ARG RSPAMD_VER=rspamd_3.11.1-1~ab0b44951
ARG CODENAME=bookworm
ENV LC_ALL=C
@@ -14,8 +14,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
dnsutils \
netcat-traditional \
wget \
redis-tools \
procps \
redis-tools \
procps \
nano \
lua-cjson \
&& arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \

View File

@@ -81,29 +81,6 @@ EOF
redis-cli -h redis-mailcow -a ${REDISPASS} --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

View File

@@ -24,10 +24,6 @@ while [[ "${DBV_NOW}" != "${DBV_NEW}" ]]; do
done
echo "DB schema is ${DBV_NOW}"
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TRIGGER IF EXISTS sogo_update_password"
fi
# cat /dev/urandom seems to hang here occasionally and is not recommended anyway, better use openssl
RAND_PASS=$(openssl rand -base64 16 | tr -dc _A-Z-a-z-0-9)
@@ -50,10 +46,6 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
<string>YES</string>
<key>SOGoEncryptionKey</key>
<string>${RAND_PASS}</string>
<key>SOGoURLEncryptionEnabled</key>
<string>YES</string>
<key>SOGoURLEncryptionPassphrase</key>
<string>${SOGO_URL_ENCRYPTION_KEY}</string>
<key>OCSAdminURL</key>
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
<key>OCSCacheFolderURL</key>

View File

@@ -1,15 +1,20 @@
60,65d58
59,65d58
< ng-show="::!activeUser.isSuperUser"
< var:ng-click="navButtonClick"
< ng-href="/user">
< <md-icon>build</md-icon>
< <md-tooltip>mailcow <var:string label:value="Preferences"/></md-tooltip>
< <md-tooltip><var:string label:value="mailcow"/></md-tooltip>
< </md-button>
< <md-button class="md-icon-button"
83c76
< onclick="mc_logout();"
< onclick="document.getElementById('mc_logout').setAttribute('action', '/'); document.getElementById('mc_logout').submit();"
---
> ng-show="::activeUser.path.logoff.length"
85c78
< ng-href="#">
---
> ng-href="{{::activeUser.path.logoff}}">
89,91d81
< <form method="POST" id="mc_logout" action="user">
< <input type="hidden" name="logout" value="1">
< </form>

View File

@@ -16,6 +16,7 @@ RUN apk add --update \
fcgi \
openssl \
nagios-plugins-mysql \
nagios-plugins-dns \
nagios-plugins-disk \
bind-tools \
redis \
@@ -31,11 +32,9 @@ RUN apk add --update \
tzdata \
whois \
&& curl https://raw.githubusercontent.com/mludvig/smtp-cli/v3.10/smtp-cli -o /smtp-cli \
&& chmod +x smtp-cli \
&& mkdir /usr/lib/mailcow
&& chmod +x smtp-cli
COPY watchdog.sh /watchdog.sh
COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
COPY check_dns.sh /usr/lib/mailcow/check_dns.sh
CMD ["/watchdog.sh"]

View File

@@ -1,39 +0,0 @@
#!/bin/sh
while getopts "H:s:" opt; do
case "$opt" in
H) HOST="$OPTARG" ;;
s) SERVER="$OPTARG" ;;
*) echo "Usage: $0 -H host -s server"; exit 3 ;;
esac
done
if [ -z "$SERVER" ]; then
echo "No DNS Server provided"
exit 3
fi
if [ -z "$HOST" ]; then
echo "No host to test provided"
exit 3
fi
# run dig and measure the time it takes to run
START_TIME=$(date +%s%3N)
dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null)
dig_rc=$?
dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -)
END_TIME=$(date +%s%3N)
ELAPSED_TIME=$((END_TIME - START_TIME))
# validate and perform nagios like output and exit codes
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
echo "Domain $HOST was not found by the server"
exit 2
elif [ $dig_rc -eq 0 ]; then
echo "DNS OK: $ELAPSED_TIME ms response time. $HOST returns $dig_output_ips"
exit 0
else
echo "Unknown error"
exit 3
fi

View File

@@ -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 UNKNOWN situations now #
# 2013101701 Minor changes in output, handling UNKWNON 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,7 +131,7 @@ elif [[ -n "${socket}" && (-z "${user}" || -z "${password}") ]]; then
fi
# Connect to the DB server and store output in vars
if [[ -n $socket ]]; then
if [[ -n $socket ]]; then
ConnectionResult=$(mariadb --skip-ssl ${optfile} ${socket} ${user} -e "show slave ${connection} status\G" 2>&1)
else
ConnectionResult=$(mariadb --skip-ssl ${optfile} ${host} ${port} ${user} -e "show slave ${connection} status\G" 2>&1)
@@ -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

View File

@@ -1,10 +1,5 @@
#!/bin/bash
if [ "${DEV_MODE}" != "n" ]; then
echo -e "\e[31mEnabled Debug Mode\e[0m"
set -x
fi
trap "exit" INT TERM
trap "kill 0" EXIT
@@ -302,7 +297,7 @@ unbound_checks() {
touch /tmp/unbound-mailcow; echo "$(tail -50 /tmp/unbound-mailcow)" > /tmp/unbound-mailcow
host_ip=$(get_container_ip unbound-mailcow)
err_c_cur=${err_count}
/usr/lib/mailcow/check_dns.sh -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
/usr/lib/nagios/plugins/check_dns -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
DNSSEC=$(dig com +dnssec | egrep 'flags:.+ad')
if [[ -z ${DNSSEC} ]]; then
echo "DNSSEC failure" 2>> /tmp/unbound-mailcow 1>&2
@@ -450,31 +445,6 @@ postfix_checks() {
return 1
}
postfix-tlspol_checks() {
err_count=0
diff_c=0
THRESHOLD=${POSTFIX_TLSPOL_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/postfix-tlspol-mailcow; echo "$(tail -50 /tmp/postfix-tlspol-mailcow)" > /tmp/postfix-tlspol-mailcow
host_ip=$(get_container_ip postfix-tlspol-mailcow)
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 8642 2>> /tmp/postfix-tlspol-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Postfix TLS Policy companion" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
clamd_checks() {
err_count=0
diff_c=0
@@ -952,18 +922,6 @@ PID=$!
echo "Spawned mailq_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! postfix-tlspol_checks; then
log_msg "Postfix TLS Policy hit error limit"
echo postfix-tlspol-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned postfix-tlspol_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! dovecot_checks; then
@@ -1036,7 +994,6 @@ 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
@@ -1048,7 +1005,6 @@ done
PID=$!
echo "Spawned olefy_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
fi
(
while true; do

View File

@@ -69,43 +69,36 @@ require_once 'functions.acl.inc.php';
$isSOGoRequest = $post['real_rip'] == getenv('IPV4_NETWORK') . '.248';
$result = false;
$protocol = $post['protocol'];
if ($isSOGoRequest) {
$protocol = null;
// 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(
$result = apppass_login($post['username'], $post['password'], $protocol, 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) error_log('MAILCOWAUTH: App auth for user ' . $post['username']);
}
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']);
}
$result = user_login($post['username'], $post['password'], array('is_internal' => true));
if ($result) error_log('MAILCOWAUTH: User auth for user ' . $post['username']);
}
if ($result) {
http_response_code(200); // OK
$return['success'] = true;
} else {
error_log("MAILCOWAUTH: Login failed for user " . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
error_log("MAILCOWAUTH: Login failed for user " . $post['username']);
http_response_code(401); // Unauthorized
}

View File

@@ -3,20 +3,21 @@ function auth_password_verify(request, password)
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
json = require "cjson"
ltn12 = require "ltn12"
https = require "ssl.https"
https.TIMEOUT = 5
local req = {
username = request.user,
password = password,
real_rip = request.real_rip,
service = request.service
protocol = {}
}
req.protocol[request.service] = true
local req_json = json.encode(req)
local res = {}
local res = {}
local b, c = https.request {
method = "POST",
url = "https://nginx:9082",
@@ -28,27 +29,11 @@ function auth_password_verify(request, password)
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
local api_response = json.decode(table.concat(res))
if api_response.success == true then
return dovecot.auth.PASSDB_RESULT_OK, ""
end
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
end

View File

@@ -53,7 +53,7 @@ mail_shared_explicit_inbox = yes
mail_prefetch_count = 30
passdb {
driver = lua
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes cache_key=%s:%u:%w
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes cache_key=%u:%w
result_success = return-ok
result_failure = continue
result_internalfail = continue

View File

@@ -48,21 +48,13 @@ http {
listen {{ HTTP_PORT }} default_server;
listen [::]:{{ HTTP_PORT }} default_server;
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* {{ 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;
}
@@ -78,7 +70,7 @@ http {
{%endif%}
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
{% if ENABLE_IPV6 %}
{% if not DISABLE_IPv6 %}
{% if not HTTP_REDIRECT %}
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
{%endif%}
@@ -90,7 +82,7 @@ http {
ssl_certificate /etc/ssl/mail/cert.pem;
ssl_certificate_key /etc/ssl/mail/key.pem;
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.*;
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.*;
include /etc/nginx/includes/sites-default.conf;
}
@@ -105,7 +97,7 @@ http {
{%endif%}
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
{% if ENABLE_IPV6 %}
{% if not DISABLE_IPv6 %}
{% if not HTTP_REDIRECT %}
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
{%endif%}
@@ -126,7 +118,7 @@ http {
# rspamd dynmaps:
server {
listen 8081;
{% if ENABLE_IPV6 %}
{% if not DISABLE_IPv6 %}
listen [::]:8081;
{%endif%}
index index.php index.html;
@@ -190,8 +182,6 @@ http {
}
}
include /etc/nginx/conf.d/*.conf;
{% for cert in valid_cert_dirs %}
server {
{% if not HTTP_REDIRECT %}
@@ -199,7 +189,7 @@ http {
{%endif%}
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
{% if ENABLE_IPV6 %}
{% if not DISABLE_IPv6 %}
{% if not HTTP_REDIRECT %}
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
{%endif%}
@@ -216,4 +206,6 @@ http {
include /etc/nginx/includes/sites-default.conf;
}
{% endfor %}
include /etc/nginx/conf.d/*.conf;
}

View File

@@ -76,14 +76,6 @@ 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;

View File

@@ -130,7 +130,7 @@ while (true) {
curl_close($ch);
if ($code != 200){
logMsg("err", "Received HTTP {$code}");
logMsg("err", "Recieved HTTP {$code}");
session_destroy();
exit;
}
@@ -141,7 +141,7 @@ while (true) {
break;
}
if (!is_array($response)){
logMsg("err", "Received malformed response from keycloak api");
logMsg("err", "Recieved malformed response from keycloak api");
break;
}
if (count($response) == 0) {
@@ -196,7 +196,7 @@ while (true) {
logMsg("err", "Could not create user " . $user['email']);
continue;
}
} else if ($row && intval($iam_settings['periodic_sync']) == 1 && $row['authsource'] == "keycloak") {
} else if ($row && intval($iam_settings['periodic_sync']) == 1) {
if ($mapper_key === false){
logMsg("warning", "No matching attribute mapping found for user " . $user['email']);
continue;

View File

@@ -168,7 +168,7 @@ foreach ($response as $user) {
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") {
} else if ($row && intval($iam_settings['periodic_sync']) == 1) {
if ($mapper_key === false){
logMsg("warning", "No matching attribute mapping found for user " . $user[$iam_settings['username_field']][0]);
continue;

View File

@@ -1,15 +1,7 @@
; NOTE: Restart phpfpm on ANY manual changes to PHP files!
; opcache
opcache.enable=1
opcache.enable_cli=1
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000
opcache.memory_consumption=128
opcache.save_comments=1
opcache.revalidate_freq=120
opcache.validate_timestamps=0
; JIT
opcache.jit=1255
opcache.jit_buffer_size=8M
opcache.revalidate_freq=1

View File

@@ -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 socketmap:inet:postfix-tlspol:8642:QUERY
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf
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 "*"

View File

@@ -1,26 +1,13 @@
# Whitelist generated by Postwhite v3.4 on Wed Oct 1 00:21:33 UTC 2025
# Whitelist generated by Postwhite v3.4 on Sat Mar 1 00:19:29 UTC 2025
# https://github.com/stevejenkins/postwhite/
# 2216 total rules
# 2000 total rules
2a00:1450:4000::/36 permit
2a01:111:f400::/48 permit
2a01:111:f403:2800::/53 permit
2a01:111:f403:8000::/50 permit
2a01:111:f403:8000::/51 permit
2a01:111:f403::/49 permit
2a01:111:f403:c000::/51 permit
2a01:111:f403:d000::/53 permit
2a01:111:f403:f000::/52 permit
2a01:238:20a:202:5370::1 permit
2a01:238:20a:202:5372::1 permit
2a01:238:20a:202:5373::1 permit
2a01:238:400:101:53::1 permit
2a01:238:400:102:53::1 permit
2a01:238:400:103:53::1 permit
2a01:238:400:301:53::1 permit
2a01:238:400:302:53::1 permit
2a01:238:400:303:53::1 permit
2a01:238:400:470:53::1 permit
2a01:238:400:471:53::1 permit
2a01:238:400:472:53::1 permit
2a01:b747:3000:200::/56 permit
2a01:b747:3001:200::/56 permit
2a01:b747:3002:200::/56 permit
@@ -30,42 +17,22 @@
2a01:b747:3006:200::/56 permit
2a02:a60:0:5::/64 permit
2c0f:fb50:4000::/36 permit
2.207.217.30 permit
3.64.237.68 permit
3.65.3.180 permit
2.207.151.53 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
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
@@ -78,26 +45,23 @@
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
@@ -106,11 +70,14 @@
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
@@ -122,7 +89,6 @@
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
@@ -135,51 +101,19 @@
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.115.239 permit
34.218.116.3 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
@@ -193,21 +127,12 @@
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
45.143.132.0/24 permit
45.143.133.0/24 permit
45.143.134.0/24 permit
45.143.135.0/24 permit
46.19.170.16 permit
46.226.48.0/21 permit
46.228.36.37 permit
46.228.36.38/31 permit
@@ -258,7 +183,6 @@
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
@@ -272,24 +196,14 @@
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
@@ -332,24 +246,23 @@
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/18 permit
54.240.64.0/19 permit
54.240.96.0/19 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
@@ -357,9 +270,6 @@
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
@@ -372,7 +282,6 @@
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
@@ -382,6 +291,9 @@
64.207.219.13 permit
64.207.219.14 permit
64.207.219.15 permit
64.207.219.24 permit
64.207.219.25 permit
64.207.219.26 permit
64.207.219.71 permit
64.207.219.72 permit
64.207.219.73 permit
@@ -391,6 +303,9 @@
64.207.219.77 permit
64.207.219.78 permit
64.207.219.79 permit
64.207.219.88 permit
64.207.219.89 permit
64.207.219.90 permit
64.207.219.135 permit
64.207.219.136 permit
64.207.219.137 permit
@@ -426,10 +341,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
@@ -635,6 +550,7 @@
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
@@ -663,11 +579,6 @@
77.238.189.142 permit
77.238.189.146/31 permit
77.238.189.148/30 permit
79.135.106.0/24 permit
79.135.107.0/24 permit
81.169.146.243 permit
81.169.146.245 permit
81.169.146.246 permit
81.223.46.0/27 permit
82.165.159.2 permit
82.165.159.3 permit
@@ -683,17 +594,10 @@
82.165.159.45 permit
82.165.159.130 permit
82.165.159.131 permit
85.9.206.169 permit
85.9.210.45 permit
84.116.6.0/23 permit
84.116.36.0/24 permit
84.116.50.0/23 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
@@ -733,13 +637,12 @@
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
@@ -1233,14 +1136,15 @@
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
@@ -1366,7 +1270,6 @@
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
@@ -1375,8 +1278,9 @@
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
@@ -1400,9 +1304,6 @@
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
@@ -1451,6 +1352,7 @@
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
@@ -1459,28 +1361,12 @@
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
@@ -1495,7 +1381,6 @@
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
@@ -1530,7 +1415,6 @@
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
@@ -1540,7 +1424,7 @@
148.105.0.0/16 permit
148.105.8.0/21 permit
149.72.0.0/16 permit
149.72.234.184 permit
149.72.223.204 permit
149.72.248.236 permit
149.97.173.180 permit
150.230.98.160 permit
@@ -1596,7 +1480,6 @@
159.183.0.0/16 permit
159.183.68.71 permit
159.183.79.38 permit
159.183.129.172 permit
160.1.62.192 permit
161.38.192.0/20 permit
161.38.204.0/22 permit
@@ -1613,14 +1496,8 @@
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
@@ -1645,28 +1522,20 @@
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
@@ -1696,14 +1565,9 @@
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
@@ -1711,8 +1575,6 @@
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
@@ -1765,7 +1627,6 @@
188.125.85.234/31 permit
188.125.85.236/31 permit
188.125.85.238 permit
188.165.51.139 permit
188.172.128.0/20 permit
192.0.64.0/18 permit
192.18.139.154 permit
@@ -1788,28 +1649,8 @@
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
@@ -1826,14 +1667,6 @@
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
@@ -1846,16 +1679,7 @@
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
@@ -1912,13 +1736,9 @@
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
@@ -1993,6 +1813,8 @@
208.71.42.212/31 permit
208.71.42.214 permit
208.72.249.240/29 permit
208.74.204.5 permit
208.74.204.9 permit
208.75.120.0/22 permit
208.76.62.0/24 permit
208.76.63.0/24 permit
@@ -2056,8 +1878,6 @@
212.227.15.4 permit
212.227.15.5 permit
212.227.15.6 permit
212.227.15.7 permit
212.227.15.8 permit
212.227.15.14 permit
212.227.15.15 permit
212.227.15.18 permit
@@ -2074,36 +1894,21 @@
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.95.19.64/27 permit
213.95.135.4 permit
213.46.255.0/24 permit
213.199.128.139 permit
213.199.128.145 permit
213.199.138.181 permit
@@ -2113,7 +1918,6 @@
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
@@ -2151,8 +1955,6 @@
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
@@ -2180,30 +1982,12 @@
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
@@ -2217,5 +2001,4 @@
2620:119:50c0:207::/64 permit
2620:119:50c0:207::215 permit
2800:3f0:4000::/36 permit
49.12.4.251 permit # checks.mailcow.email
2a01:4f8:c17:7906::10 permit # checks.mailcow.email
194.25.134.0/24 permit # t-online.de

View File

@@ -24,6 +24,7 @@
/.+\.guru$/i
/.+\.icu$/i
/.+\.id$/i
/.+\.info$/i
/.+\.in.net$/i
/.+\.ir$/i
/.+\.jetzt$/i

View File

@@ -133,7 +133,7 @@ try {
error_log("ALIAS EXPANDER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = explode(',', $goto_branch);
} else {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
$stmt->execute(array(':domain' => $parsed_goto['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) {

View File

@@ -56,7 +56,7 @@ function normalize_email($email) {
$email = explode('@', $email);
$email[0] = str_replace('.', '', $email[0]);
$email = implode('@', $email);
}
}
$gm_alt = "@googlemail.com";
if (substr_compare($email, $gm_alt, -strlen($gm_alt)) == 0) {
$email = explode('@', $email);
@@ -114,7 +114,7 @@ function ucl_rcpts($object, $type) {
$rcpt[] = str_replace('/', '\/', $row['address']);
}
// Aliases by alias domains
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox`
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox`
LEFT OUTER JOIN `alias_domain` ON `mailbox`.`domain` = `alias_domain`.`target_domain`
WHERE `mailbox`.`username` = :object");
$stmt->execute(array(
@@ -184,7 +184,7 @@ while ($row = array_shift($rows)) {
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
<?php
}
$stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf`
$stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf`
WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel')
AND `object`= :object");
$stmt->execute(array(':object' => $row['object']));
@@ -468,36 +468,4 @@ while ($row = array_shift($rows)) {
<?php
}
?>
<?php
// Start internal aliases
$stmt = $pdo->query("SELECT `id`, `address`, `domain` FROM `alias` WHERE `active` = '1' AND `internal` = '1'");
$aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($alias = array_shift($aliases)) {
// build allowed_domains regex and add target domain and alias domains
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `active` = '1' AND `target_domain` = :target_domain");
$stmt->execute(array(':target_domain' => $alias['domain']));
$allowed_domains = $stmt->fetchAll(PDO::FETCH_ASSOC);
$allowed_domains = array_map(function($item) {
return str_replace('.', '\.', $item['alias_domain']);
}, $allowed_domains);
$allowed_domains[] = str_replace('.', '\.', $alias['domain']);
$allowed_domains = implode('|', $allowed_domains);
?>
internal_alias_<?=$alias['id'];?> {
priority = 10;
rcpt = "<?=$alias['address'];?>";
from = "/^((?!.*@(<?=$allowed_domains;?>)).)*$/";
apply "default" {
MAILCOW_INTERNAL_ALIAS = 9999.0;
}
symbols [
"MAILCOW_INTERNAL_ALIAS"
]
}
<?php
}
?>
}

View File

@@ -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

View File

@@ -0,0 +1,12 @@
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;
}

View File

@@ -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', 'postmaster')
task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt')
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', 'keep_spam')
task:set_pre_result('accept', 'ip matched with forward hosts')
end
end
end
@@ -454,18 +454,12 @@ rspamd_config:register_symbol({
local redis_params = rspamd_parse_redis_server('dyn_rl')
local rspamd_logger = require "rspamd_logger"
local envfrom = task:get_from(1)
local envrcpt = task:get_recipients(1) or {}
local uname = task:get_user()
if not envfrom or not uname then
return false
end
local uname = uname:lower()
if #envrcpt == 1 and envrcpt[1].addr:lower() == uname then
return false
end
local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case
local function redis_cb_user(err, data)
@@ -550,13 +544,13 @@ rspamd_config:register_symbol({
-- determine newline type
local function newline(task)
local t = task:get_newlines_type()
if t == 'cr' then
return '\r'
elseif t == 'lf' then
return '\n'
end
return '\r\n'
end
-- retrieve footer
@@ -564,7 +558,7 @@ rspamd_config:register_symbol({
if err or type(data) ~= 'string' then
rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
else
-- parse json string
local footer = cjson.decode(data)
if not footer then
@@ -613,30 +607,26 @@ rspamd_config:register_symbol({
if footer.plain and footer.plain ~= "" then
footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
end
-- add footer
local out = {}
local rewrite = lua_mime.add_text_footer(task, footer.html, footer.plain) or {}
local seen_cte
local newline_s = newline(task)
local function rewrite_ct_cb(name, hdr)
if rewrite.need_rewrite_ct then
if name:lower() == 'content-type' then
-- include boundary if present
local boundary_part = rewrite.new_ct.boundary and
string.format('; boundary="%s"', rewrite.new_ct.boundary) or ''
local nct = string.format('%s: %s/%s; charset=utf-8%s',
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)
local nct = string.format('%s: %s/%s; charset=utf-8',
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
out[#out + 1] = nct
-- update Content-Type header (include boundary if present)
-- update Content-Type header
task:set_milter_reply({
remove_headers = {['Content-Type'] = 0},
})
task:set_milter_reply({
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8%s',
rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)}
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8', rewrite.new_ct.type, rewrite.new_ct.subtype)}
})
return
elseif name:lower() == 'content-transfer-encoding' then
@@ -655,16 +645,16 @@ rspamd_config:register_symbol({
end
out[#out + 1] = hdr.raw:gsub('\r?\n?$', '')
end
task:headers_foreach(rewrite_ct_cb, {full = true})
if not seen_cte and rewrite.need_rewrite_ct then
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable')
end
-- End of headers
out[#out + 1] = newline_s
if rewrite.out then
for _,o in ipairs(rewrite.out) do
out[#out + 1] = o

View File

@@ -182,7 +182,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = explode(',', $goto_branch);
} else {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
$stmt->execute(array(':domain' => $parsed_goto['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) {
@@ -236,9 +236,6 @@ 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 (

View File

@@ -167,7 +167,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = explode(',', $goto_branch);
} else {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'");
$stmt->execute(array(':domain' => $parsed_goto['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) {

View File

@@ -5,16 +5,6 @@ document.addEventListener('DOMContentLoaded', function () {
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

View File

@@ -16,9 +16,6 @@
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;
@@ -94,7 +91,7 @@
//SoDebugBaseURL = YES;
//ImapDebugEnabled = YES;
//SOGoEASDebugEnabled = YES;
SOGoEASSearchInBody = YES;
SOGoEASSearchInBody = YES; // Experimental. Enabled since 2023-10
//LDAPDebugEnabled = YES;
//PGDebugEnabled = YES;
//MySQL4DebugEnabled = YES;

View File

@@ -18,7 +18,6 @@ elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] !=
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 = $redis->Get('LICENSE_STATUS_CACHE')) {
@@ -34,7 +33,6 @@ $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) {
@@ -79,7 +77,6 @@ $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']),

View File

@@ -22,8 +22,7 @@ $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
$template = 'admin_index.twig';
$template_data = [
'login_delay' => @$_SESSION['ldelay'],
'custom_login' => customize('get', 'custom_login'),
'login_delay' => @$_SESSION['ldelay']
];
$js_minifier->add('/web/js/site/index.js');

View File

@@ -125,7 +125,6 @@ $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,

View File

@@ -346,8 +346,7 @@ paths:
description: the domain which emails should be forwarded
type: string
type:
description: the type of bcc map can be `sender` or `rcpt`
enum: [sender, rcpt]
description: the type of bcc map can be `sender` or `recipient`
type: string
type: object
summary: Create BCC Map
@@ -5847,7 +5846,6 @@ paths:
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:
@@ -5901,9 +5899,6 @@ paths:
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
@@ -5994,7 +5989,6 @@ paths:
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"]
@@ -6039,7 +6033,6 @@ paths:
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"]

View File

@@ -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">
<enable visiturl="https://<?=$mailcow_hostname; ?><?php if ($port != 443) echo ':'.$port; ?>/admin.php">
<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>

View File

@@ -7,8 +7,6 @@ if(file_exists('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';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.inc.php';
$default_autodiscover_config = $autodiscover_config;
$autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);

File diff suppressed because one or more lines are too long

View File

@@ -22,7 +22,6 @@ $_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');

View File

@@ -48,12 +48,6 @@ 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'],
@@ -64,7 +58,6 @@ 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"])
@@ -132,7 +125,6 @@ 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,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -71,7 +71,6 @@ 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();
@@ -129,27 +128,6 @@ 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',
@@ -363,25 +341,15 @@ 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)) {
@@ -390,8 +358,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]) {
@@ -399,7 +367,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;

View File

@@ -26,25 +26,23 @@ if (is_array($alertbox_log_parser)) {
// map tfa details for twig
$pending_tfa_authmechs = [];
if (array_key_exists('pending_tfa_methods', $_SESSION)) {
foreach($_SESSION['pending_tfa_methods'] as $authdata){
$pending_tfa_authmechs[$authdata['authmech']] = false;
}
if (isset($pending_tfa_authmechs['webauthn'])) {
$pending_tfa_authmechs['webauthn'] = true;
}
if (!isset($pending_tfa_authmechs['webauthn'])
&& isset($pending_tfa_authmechs['yubi_otp'])) {
$pending_tfa_authmechs['yubi_otp'] = true;
}
if (!isset($pending_tfa_authmechs['webauthn'])
&& !isset($pending_tfa_authmechs['yubi_otp'])
&& isset($pending_tfa_authmechs['totp'])) {
$pending_tfa_authmechs['totp'] = true;
}
if (isset($pending_tfa_authmechs['u2f'])) {
$pending_tfa_authmechs['u2f'] = true;
}
foreach($_SESSION['pending_tfa_methods'] as $authdata){
$pending_tfa_authmechs[$authdata['authmech']] = false;
}
if (isset($pending_tfa_authmechs['webauthn'])) {
$pending_tfa_authmechs['webauthn'] = true;
}
if (!isset($pending_tfa_authmechs['webauthn'])
&& isset($pending_tfa_authmechs['yubi_otp'])) {
$pending_tfa_authmechs['yubi_otp'] = true;
}
if (!isset($pending_tfa_authmechs['webauthn'])
&& !isset($pending_tfa_authmechs['yubi_otp'])
&& isset($pending_tfa_authmechs['totp'])) {
$pending_tfa_authmechs['totp'] = true;
}
if (isset($pending_tfa_authmechs['u2f'])) {
$pending_tfa_authmechs['u2f'] = true;
}
// globals

View File

@@ -1,7 +1,7 @@
<?php
function app_passwd($_action, $_data = null) {
global $pdo;
global $lang;
global $pdo;
global $lang;
$_data_log = $_data;
!isset($_data_log['app_passwd']) ?: $_data_log['app_passwd'] = '*';
!isset($_data_log['app_passwd2']) ?: $_data_log['app_passwd2'] = '*';
@@ -43,7 +43,20 @@ function app_passwd($_action, $_data = null) {
);
return false;
}
if (password_check($password, $password2) !== true) {
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_complexity'
);
return false;
}
if ($password != $password2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_mismatch'
);
return false;
}
$password_hashed = hash_password($password);
@@ -75,15 +88,15 @@ function app_passwd($_action, $_data = null) {
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'app_passwd_added'
);
break;
break;
case 'edit':
$ids = (array)$_data['id'];
foreach ($ids as $id) {
$is_now = app_passwd('details', $id);
if (!empty($is_now)) {
$app_name = (!empty($_data['app_name'])) ? $_data['app_name'] : $is_now['name'];
$password = (!empty($_data['app_passwd'])) ? $_data['app_passwd'] : null;
$password2 = (!empty($_data['app_passwd2'])) ? $_data['app_passwd2'] : null;
$password = (!empty($_data['password'])) ? $_data['password'] : null;
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
if (isset($_data['protocols'])) {
$protocols = (array)$_data['protocols'];
$imap_access = (in_array('imap_access', $protocols)) ? 1 : 0;
@@ -113,7 +126,20 @@ function app_passwd($_action, $_data = null) {
}
$app_name = htmlspecialchars(trim($app_name));
if (!empty($password) && !empty($password2)) {
if (password_check($password, $password2) !== true) {
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_complexity'
);
continue;
}
if ($password != $password2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_mismatch'
);
continue;
}
$password_hashed = hash_password($password);
@@ -156,7 +182,7 @@ function app_passwd($_action, $_data = null) {
'msg' => array('object_modified', htmlspecialchars(implode(', ', $ids)))
);
}
break;
break;
case 'delete':
$ids = (array)$_data['id'];
foreach ($ids as $id) {
@@ -187,17 +213,19 @@ function app_passwd($_action, $_data = null) {
'msg' => array('app_passwd_removed', htmlspecialchars($id))
);
}
break;
break;
case 'get':
$app_passwds = array();
$stmt = $pdo->prepare("SELECT `id`, `name` FROM `app_passwd` WHERE `mailbox` = :username");
$stmt->execute(array(':username' => $username));
$app_passwds = $stmt->fetchAll(PDO::FETCH_ASSOC);
return $app_passwds;
break;
break;
case 'details':
$app_passwd_data = array();
$stmt = $pdo->prepare("SELECT * FROM `app_passwd` WHERE `id` = :id");
$stmt = $pdo->prepare("SELECT *
FROM `app_passwd`
WHERE `id` = :id");
$stmt->execute(array(':id' => $_data));
$app_passwd_data = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($app_passwd_data)) {
@@ -209,6 +237,6 @@ function app_passwd($_action, $_data = null) {
}
$app_passwd_data['name'] = htmlspecialchars(trim($app_passwd_data['name']));
return $app_passwd_data;
break;
break;
}
}

View File

@@ -9,52 +9,25 @@ function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
// Try validate admin
if (!isset($role) || $role == "admin") {
$result = admin_login($user, $pass);
if ($result !== false){
return $result;
}
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;
}
if ($result !== false) 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;
}
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) return $result;
}
// skip log and only return false if it's an internal request
@@ -193,7 +166,6 @@ function user_login($user, $pass, $extra = null){
global $iam_settings;
$is_internal = $extra['is_internal'];
$service = $extra['service'];
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
if (!$is_internal){
@@ -236,14 +208,6 @@ function user_login($user, $pass, $extra = null){
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!empty($row)) {
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
$row['attributes'] = json_decode($row['attributes'], true);
if (isset($service)) {
$key = strtolower($service) . "_access";
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
return false;
}
}
return true;
}
}
@@ -251,14 +215,6 @@ function user_login($user, $pass, $extra = null){
return false;
}
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
$row['attributes'] = json_decode($row['attributes'], true);
if (isset($service)) {
$key = strtolower($service) . "_access";
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
return false;
}
}
switch ($row['authsource']) {
case 'keycloak':
// user authsource is keycloak, try using via rest flow
@@ -368,11 +324,6 @@ function user_login($user, $pass, $extra = null){
}
// 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) {
@@ -464,7 +415,21 @@ function apppass_login($user, $pass, $app_passwd_data, $extra = null){
// verify password
if (verify_hash($row['password'], $pass) !== false) {
$_SESSION['app_passwd_id'] = $row['app_passwd_id'];
if ($is_internal){
$remote_addr = $extra['remote_addr'];
} else {
$remote_addr = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
}
$service = strtoupper($is_app_passwd);
$stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)");
$stmt->execute(array(
':service' => $service,
':app_id' => $row['app_passwd_id'],
':username' => $user,
':remote_addr' => $remote_addr
));
unset($_SESSION['ldelay']);
return "user";
}
@@ -493,9 +458,6 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
}
return false;
}
if (!$iam_provider) {
return false;
}
// get access_token for service account of mailcow client
$admin_token = identity_provider("get-keycloak-admin-token");
@@ -565,17 +527,6 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
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'])) {
@@ -689,21 +640,10 @@ function ldap_mbox_login($user, $pass, $extra = null){
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'];
if (!empty($iam_settings['default_tempalte'])) {
$mbox_template = $iam_settings['default_tempalte'];
} else {
$_SESSION['return'][] = array(
'type' => 'danger',

View File

@@ -204,35 +204,6 @@ 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 {
$redis->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':
@@ -293,7 +264,7 @@ function customize($_action, $_item, $_data = null) {
}
if (empty($app_links)){
return [];
return false;
}
// convert from old style
@@ -325,10 +296,8 @@ function customize($_action, $_item, $_data = null) {
break;
case 'ui_texts':
try {
$mailcow_hostname = strtolower(getenv("MAILCOW_HOSTNAME"));
$data['title_name'] = ($title_name = $redis->get('TITLE_NAME')) ? $title_name : "$mailcow_hostname - mail UI";
$data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : "$mailcow_hostname - mail UI";
$data['title_name'] = ($title_name = $redis->get('TITLE_NAME')) ? $title_name : 'mailcow UI';
$data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : 'mailcow UI';
$data['apps_name'] = ($apps_name = $redis->get('APPS_NAME')) ? $apps_name : $lang['header']['apps'];
$data['help_text'] = ($help_text = $redis->get('HELP_TEXT')) ? $help_text : false;
if (!empty($redis->get('UI_IMPRESS'))) {
@@ -388,20 +357,6 @@ function customize($_action, $_item, $_data = null) {
return false;
}
break;
case 'custom_login':
try {
$custom_login = $redis->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;
}

View File

@@ -350,34 +350,6 @@ function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {
}
}
function set_sasl_log($username, $real_rip, $service){
global $pdo;
try {
if (!empty($_SESSION['app_passwd_id'])) {
$app_password = $_SESSION['app_passwd_id'];
} else {
$app_password = 0;
}
$stmt = $pdo->prepare('REPLACE INTO `sasl_log` (`username`, `real_rip`, `service`, `app_password`) VALUES (:username, :real_rip, :service, :app_password)');
$stmt->execute(array(
':username' => $username,
':real_rip' => $real_rip,
':service' => $service,
':app_password' => $app_password
));
} catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => array('mysql_error', $e)
);
return false;
}
return true;
}
function flush_memcached() {
try {
$m = new Memcached();
@@ -1001,12 +973,11 @@ function edit_user_account($_data) {
':password_hashed' => $password_hashed,
':username' => $username
));
$_SESSION['pending_pw_update'] = false;
update_sogo_static_view();
}
// edit password recovery email
elseif (!empty($password_old) && isset($pw_recovery_email)) {
elseif (isset($pw_recovery_email)) {
if (!isset($_SESSION['acl']['pw_reset']) || $_SESSION['acl']['pw_reset'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
@@ -1016,21 +987,6 @@ function edit_user_account($_data) {
return false;
}
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
WHERE `kind` NOT REGEXP 'location|thing|group'
AND `username` = :user AND authsource = 'mailcow'");
$stmt->execute(array(':user' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!verify_hash($row['password'], $password_old)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$pw_recovery_email = (!filter_var($pw_recovery_email, FILTER_VALIDATE_EMAIL)) ? '' : $pw_recovery_email;
$stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email)
WHERE `username` = :username AND authsource = 'mailcow'");
@@ -1122,21 +1078,11 @@ function user_get_alias_details($username) {
}
return $data;
}
function is_valid_domain_name($domain_name, $options = array()) {
function is_valid_domain_name($domain_name) {
if (empty($domain_name)) {
return false;
}
// Convert domain name to ASCII for validation
$domain_name = idn_to_ascii($domain_name, 0, INTL_IDNA_VARIANT_UTS46);
if (isset($options['allow_wildcard']) && $options['allow_wildcard'] == true) {
// Remove '*.' if wildcard subdomains are allowed
if (strpos($domain_name, '*.') === 0) {
$domain_name = substr($domain_name, 2);
}
}
return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name)
&& preg_match("/^.{1,253}$/", $domain_name)
&& preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name));
@@ -1439,7 +1385,6 @@ function fido2($_data) {
);
break;
case "verify":
$role = "";
$tokenData = json_decode($_data['token']);
$clientDataJSON = base64_decode($tokenData->clientDataJSON);
$authenticatorData = base64_decode($tokenData->authenticatorData);
@@ -1473,17 +1418,17 @@ function fido2($_data) {
$stmt->execute(array(':username' => $process_fido2['username']));
$obj_props = $stmt->fetch(PDO::FETCH_ASSOC);
if ($obj_props['superadmin'] === 1 && (!$_data['user'] || $_data['user'] == "admin")) {
$role = "admin";
$_SESSION["mailcow_cc_role"] = "admin";
}
elseif ($obj_props['superadmin'] === 0 && (!$_data['user'] || $_data['user'] == "domainadmin")) {
$role = "domainadmin";
$_SESSION["mailcow_cc_role"] = "domainadmin";
}
elseif (!isset($obj_props['superadmin']) && (!$_data['user'] || $_data['user'] == "user")) {
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username");
$stmt->execute(array(':username' => $process_fido2['username']));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row['username'] == $process_fido2['username']) {
$role = "user";
$_SESSION["mailcow_cc_role"] = "user";
}
}
else {
@@ -1494,7 +1439,7 @@ function fido2($_data) {
);
return false;
}
if (empty($role)) {
if (empty($_SESSION["mailcow_cc_role"])) {
session_unset();
session_destroy();
$_SESSION['return'][] = array(
@@ -1504,17 +1449,15 @@ function fido2($_data) {
);
return false;
}
$_SESSION["mailcow_cc_username"] = $process_fido2['username'];
$_SESSION["fido2_cid"] = $process_fido2['cid'];
unset($_SESSION["challenge"]);
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array("fido2_login", $_data['user'], $process_fido2['username']),
'msg' => array('logged_in_as', $process_fido2['username'])
);
return array(
"role" => $role,
"username" => $process_fido2['username'],
"cid" => $process_fido2['cid']
);
return true;
break;
}
}
@@ -2236,7 +2179,7 @@ function cors($action, $data = null) {
$cors_settings['allowed_origins'] = $allowed_origins[0];
if (in_array('*', $allowed_origins)){
$cors_settings['allowed_origins'] = '*';
} else if (array_key_exists('HTTP_ORIGIN', $_SERVER) && in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
} else if (in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
$cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN'];
}
// always allow OPTIONS for preflight request
@@ -2312,14 +2255,12 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach($rows as $row){
switch ($row["key"]) {
case "redirect_url_extra":
case "mappers":
case "templates":
$settings[$row["key"]] = json_decode($row["value"]);
break;
case "use_ssl":
case "use_tls":
case "login_provisioning":
case "ignore_ssl_errors":
$settings[$row["key"]] = boolval($row["value"]);
break;
@@ -2328,10 +2269,6 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
break;
}
}
// set login_provisioning if not exists
if (!array_key_exists('login_provisioning', $settings)) {
$settings['login_provisioning'] = 1;
}
// return default client_scopes for generic-oidc if none is set
if ($settings["authsource"] == "generic-oidc" && empty($settings["client_scopes"])){
$settings["client_scopes"] = "openid profile email mailcow_template";
@@ -2396,8 +2333,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
return false;
}
$_data['ignore_ssl_error'] = isset($_data['ignore_ssl_error']) ? boolval($_data['ignore_ssl_error']) : false;
$_data['login_provisioning'] = isset($_data['login_provisioning']) ? boolval($_data['login_provisioning']) : false;
$_data['ignore_ssl_error'] = isset($_data['ignore_ssl_error']) ? boolval($_data['ignore_ssl_error']) : false;
switch ($_data['authsource']) {
case "keycloak":
$_data['server_url'] = (!empty($_data['server_url'])) ? rtrim($_data['server_url'], '/') : null;
@@ -2406,14 +2342,14 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
$_data['import_users'] = isset($_data['import_users']) ? intval($_data['import_users']) : 0;
$_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15;
$_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
$required_settings = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval', 'ignore_ssl_error', 'login_provisioning');
$required_settings = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval', 'ignore_ssl_error');
break;
case "generic-oidc":
$_data['authorize_url'] = (!empty($_data['authorize_url'])) ? $_data['authorize_url'] : null;
$_data['token_url'] = (!empty($_data['token_url'])) ? $_data['token_url'] : null;
$_data['userinfo_url'] = (!empty($_data['userinfo_url'])) ? $_data['userinfo_url'] : null;
$_data['client_scopes'] = (!empty($_data['client_scopes'])) ? $_data['client_scopes'] : "openid profile email mailcow_template";
$required_settings = array('authsource', 'authorize_url', 'token_url', 'client_id', 'client_secret', 'redirect_url', 'userinfo_url', 'client_scopes', 'ignore_ssl_error', 'login_provisioning');
$required_settings = array('authsource', 'authorize_url', 'token_url', 'client_id', 'client_secret', 'redirect_url', 'userinfo_url', 'client_scopes', 'ignore_ssl_error');
break;
case "ldap":
$_data['host'] = (!empty($_data['host'])) ? str_replace(" ", "", $_data['host']) : "";
@@ -2427,7 +2363,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
$_data['use_tls'] = isset($_data['use_tls']) && !$_data['use_ssl'] ? boolval($_data['use_tls']) : false;
$_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15;
$_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval'];
$required_settings = array('authsource', 'host', 'port', 'basedn', 'username_field', 'filter', 'attribute_field', 'binddn', 'bindpass', 'periodic_sync', 'import_users', 'sync_interval', 'use_ssl', 'use_tls', 'ignore_ssl_error', 'login_provisioning');
$required_settings = array('authsource', 'host', 'port', 'basedn', 'username_field', 'filter', 'attribute_field', 'binddn', 'bindpass', 'periodic_sync', 'import_users', 'sync_interval', 'use_ssl', 'use_tls', 'ignore_ssl_error');
break;
}
@@ -2451,18 +2387,6 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
}
$pdo->commit();
// add redirect_url_extra
if (isset($_data['redirect_url_extra'])){
$_data['redirect_url_extra'] = (!is_array($_data['redirect_url_extra'])) ? array($_data['redirect_url_extra']) : $_data['redirect_url_extra'];
$redirect_url_extra = array_filter($_data['redirect_url_extra']);
$redirect_url_extra = json_encode($redirect_url_extra);
$stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES ('redirect_url_extra', :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);");
$stmt->bindParam(':value', $redirect_url_extra);
$stmt->execute();
}
// add default template
if (isset($_data['default_template'])) {
$_data['default_template'] = (empty($_data['default_template'])) ? "" : $_data['default_template'];
@@ -2797,16 +2721,6 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
return true;
}
// user doesn't exist, check if login provisioning is enabled
if (!$iam_settings['login_provisioning']){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, "Auto-create users on login is deactivated"),
'msg' => 'login_failed'
);
return false;
}
if (empty($iam_settings['mappers']) || empty($user_template) || $mapper_key === false){
if (!empty($iam_settings['default_template'])) {
$mbox_template = $iam_settings['default_template'];
@@ -2906,19 +2820,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
case "get-redirect":
if ($iam_settings['authsource'] != 'keycloak' && $iam_settings['authsource'] != 'generic-oidc')
return false;
$options = [];
if (isset($iam_settings['redirect_url_extra'])) {
// check if the current domain is used in an extra redirect URL
$targetDomain = strtolower($_SERVER['HTTP_HOST']);
foreach ($iam_settings['redirect_url_extra'] as $testUrl) {
$testUrlParsed = parse_url($testUrl);
if (isset($testUrlParsed['host']) && strtolower($testUrlParsed['host']) == $targetDomain) {
$options['redirect_uri'] = $testUrl;
break;
}
}
}
$authUrl = $iam_provider->getAuthorizationUrl($options);
$authUrl = $iam_provider->getAuthorizationUrl();
$_SESSION['oauth2state'] = $iam_provider->getState();
return $authUrl;
break;

View File

@@ -684,16 +684,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return true;
break;
case 'alias':
$addresses = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['address']));
$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto']));
$internal = intval($_data['internal']);
$active = intval($_data['active']);
$sogo_visible = intval($_data['sogo_visible']);
$goto_null = intval($_data['goto_null']);
$goto_spam = intval($_data['goto_spam']);
$goto_ham = intval($_data['goto_ham']);
$addresses = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['address']));
$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto']));
$active = intval($_data['active']);
$sogo_visible = intval($_data['sogo_visible']);
$goto_null = intval($_data['goto_null']);
$goto_spam = intval($_data['goto_spam']);
$goto_ham = intval($_data['goto_ham']);
$private_comment = $_data['private_comment'];
$public_comment = $_data['public_comment'];
$public_comment = $_data['public_comment'];
if (strlen($private_comment) > 160 | strlen($public_comment) > 160){
$_SESSION['return'][] = array(
'type' => 'danger',
@@ -843,8 +842,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
continue;
}
$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `active`)
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :active)");
$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `active`)
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :active)");
if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
$stmt->execute(array(
':address' => '@'.$domain,
@@ -854,7 +853,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':goto' => $goto,
':domain' => $domain,
':sogo_visible' => $sogo_visible,
':internal' => $internal,
':active' => $active
));
}
@@ -866,7 +864,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':goto' => $goto,
':domain' => $domain,
':sogo_visible' => $sogo_visible,
':internal' => $internal,
':active' => $active
));
}
@@ -1226,14 +1223,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(
':username' => $username
));
// save delimiter_action
if (isset($_data['tagged_mail_handler'])) {
mailbox('edit', 'delimiter_action', array(
'username' => $username,
'tagged_mail_handler' => $_data['tagged_mail_handler']
));
}
// save tags
foreach($tags as $index => $tag){
if (empty($tag)) continue;
@@ -1403,80 +1392,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return mailbox('add', 'mailbox', $mailbox_attributes);
break;
case 'mta_sts':
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
$version = strtolower($_data['version']);
$mode = strtolower($_data['mode']);
$mx = explode(",", preg_replace('/\s+/', '', $_data['mx']));
$max_age = intval($_data['max_age']);
$active = (intval($_data['active']) == 1) ? 1 : 0;
$id = date('YmdHis');
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => 'access_denied'
);
return false;
}
if (empty($version) || !in_array($version, array('stsv1'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('version_invalid', htmlspecialchars($domain))
);
return false;
}
if (empty($mode) || !in_array($mode, array('enforce', 'testing', 'none'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('mode_invalid', htmlspecialchars($domain))
);
return false;
}
if (empty($max_age) || $max_age < 0 || $max_age > 31536000) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('max_age_invalid', htmlspecialchars($domain))
);
return false;
}
foreach ($mx as $index => $mx_domain) {
$mx_domain = idn_to_ascii(strtolower(trim($mx_domain)), 0, INTL_IDNA_VARIANT_UTS46);
if (!is_valid_domain_name($mx_domain, array('allow_wildcard' => true))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('mx_invalid', htmlspecialchars($mx_domain))
);
return false;
}
}
try {
$stmt = $pdo->prepare("INSERT INTO `mta_sts` (`id`, `domain`, `version`, `mode`, `mx`, `max_age`, `active`)
VALUES (:id, :domain, :version, :mode, :mx, :max_age, :active)");
$stmt->execute(array(
':id' => $id,
':domain' => $domain,
':version' => $version,
':mode' => $mode,
':mx' => implode(",", $mx),
':max_age' => $max_age,
':active' => $active
));
} catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data),
'msg' => $e->getMessage()
);
return false;
}
break;
case 'resource':
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
$description = $_data['description'];
@@ -1698,7 +1613,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr = array();
$attr["quota"] = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0;
$attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array();
$attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['tagged_mail_handler']);
$attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
$attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
@@ -2484,7 +2398,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
foreach ($ids as $id) {
$is_now = mailbox('get', 'alias_details', $id);
if (!empty($is_now)) {
$internal = (isset($_data['internal'])) ? intval($_data['internal']) : $is_now['internal'];
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$sogo_visible = (isset($_data['sogo_visible'])) ? intval($_data['sogo_visible']) : $is_now['sogo_visible'];
$goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0;
@@ -2670,7 +2583,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`domain` = :domain,
`goto` = :goto,
`sogo_visible`= :sogo_visible,
`internal`= :internal,
`active`= :active
WHERE `id` = :id");
$stmt->execute(array(
@@ -2680,7 +2592,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':domain' => $domain,
':goto' => $goto,
':sogo_visible' => $sogo_visible,
':internal' => $internal,
':active' => $active,
':id' => $is_now['id']
));
@@ -3348,13 +3259,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
// save delimiter_action
if (isset($_data['tagged_mail_handler'])) {
mailbox('edit', 'delimiter_action', array(
'username' => $username,
'tagged_mail_handler' => $_data['tagged_mail_handler']
));
}
// save tags
foreach($tags as $index => $tag){
if (empty($tag)) continue;
@@ -3420,7 +3324,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
}
$is_now = mailbox('get', 'mailbox_details', $old_username);
if (empty($is_now) || ($is_now['active'] != '1' && $is_now['active'] != '2')) {
if (empty($is_now)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -3700,7 +3604,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr = array();
$attr["quota"] = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0;
$attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : $is_now['tags'];
$attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : $is_now['tagged_mail_handler'];
$attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification'];
$attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category'];
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : $is_now['rl_frame'];
@@ -3821,125 +3724,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return true;
break;
case 'mta_sts':
if (!is_array($_data['domains'])) {
$domains = array();
$domains[] = $_data['domains'];
}
else {
$domains = $_data['domains'];
}
foreach ($domains as $domain) {
$domain = idn_to_ascii(strtolower(trim($domain)), 0, INTL_IDNA_VARIANT_UTS46);
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => 'access_denied'
);
continue;
}
$is_now = mailbox('get', 'mta_sts', $domain);
if (!empty($is_now)) {
$version = (isset($_data['version'])) ? strtolower($_data['version']) : $is_now['version'];
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$active = ($active == 1) ? 1 : 0;
$mode = (isset($_data['mode'])) ? strtolower($_data['mode']) : $is_now['mode'];
$mx = (isset($_data['mx'])) ? explode(",", preg_replace('/\s+/', '', $_data['mx'])) : $is_now['mx'];
$max_age = (isset($_data['max_age'])) ? intval($_data['max_age']) : $is_now['max_age'];
// Update ID if neccesary
if ($version != strtolower($is_now['version']) ||
$mode != strtolower($is_now['mode']) ||
$mx != $is_now['mx'] ||
$max_age != $is_now['max_age']) {
$id = date('YmdHis');
} else {
$id = $is_now['id'];
}
} else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (empty($version) || !in_array($version, array('stsv1'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('version_invalid', htmlspecialchars($version))
);
continue;
}
if (empty($mode) || !in_array($mode, array('enforce', 'testing', 'none'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('mode_invalid', htmlspecialchars($domain))
);
continue;
}
if (empty($max_age) || $max_age < 0 || $max_age > 31557600) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('max_age_invalid', htmlspecialchars($domain))
);
continue;
}
foreach ($mx as $index => $mx_domain) {
$mx_domain = idn_to_ascii(strtolower(trim($mx_domain)), 0, INTL_IDNA_VARIANT_UTS46);
$invalid_mx = false;
if (!is_valid_domain_name($mx_domain, array('allow_wildcard' => true))) {
$invalid_mx = $mx_domain;
break;
}
}
if ($invalid_mx) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('mx_invalid', htmlspecialchars($invalid_mx))
);
continue;
}
try {
$stmt = $pdo->prepare("UPDATE `mta_sts` SET `id` = :id, `version` = :version, `mode` = :mode, `mx` = :mx, `max_age` = :max_age, `active` = :active WHERE `domain` = :domain");
$stmt->execute(array(
':id' => $id,
':domain' => $domain,
':version' => $version,
':mode' => $mode,
':mx' => implode(",", $mx),
':max_age' => $max_age,
':active' => $active
));
} catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data),
'msg' => $e->getMessage()
);
continue;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('object_modified', $domain)
);
}
return true;
break;
case 'resource':
if (!is_array($_data['name'])) {
$names = array();
@@ -4706,7 +4490,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`address`,
`public_comment`,
`private_comment`,
`internal`,
`active`,
`sogo_visible`,
`created`,
@@ -4737,7 +4520,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$aliasdata['goto'] = $row['goto'];
$aliasdata['address'] = $row['address'];
(!filter_var($aliasdata['address'], FILTER_VALIDATE_EMAIL)) ? $aliasdata['is_catch_all'] = 1 : $aliasdata['is_catch_all'] = 0;
$aliasdata['internal'] = $row['internal'];
$aliasdata['active'] = $row['active'];
$aliasdata['active_int'] = $row['active'];
$aliasdata['sogo_visible'] = $row['sogo_visible'];
@@ -5230,20 +5012,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return $rows;
}
break;
case 'mta_sts':
$stmt = $pdo->prepare("SELECT * FROM `mta_sts` WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $_data,
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($row)){
return [];
}
$row['mx'] = explode(',', $row['mx']);
$row['version'] = strtoupper(substr($row['version'], 0, 3)) . substr($row['version'], 3);
return $row;
break;
case 'resource_details':
$resourcedata = array();
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
@@ -5629,10 +5397,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(
':domain' => $domain,
));
$stmt = $pdo->prepare("DELETE FROM `mta_sts` WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain,
));
$stmt = $pdo->query("DELETE FROM `admin` WHERE `superadmin` = 0 AND `username` NOT IN (SELECT `username`FROM `domain_admins`);");
$stmt = $pdo->query("DELETE FROM `da_acl` WHERE `username` NOT IN (SELECT `username`FROM `domain_admins`);");
try {

View File

@@ -22,7 +22,7 @@ function quarantine($_action, $_data = null) {
return false;
}
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
WHERE `qhash` = :hash
WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash
AND user_acl.quarantine = 1
AND rcpt IN (SELECT username FROM mailbox)');
$stmt->execute(array(':hash' => $hash));
@@ -65,7 +65,7 @@ function quarantine($_action, $_data = null) {
return false;
}
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
WHERE `qhash` = :hash
WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash
AND `user_acl`.`quarantine` = 1
AND `username` IN (SELECT `username` FROM `mailbox`)');
$stmt->execute(array(':hash' => $hash));
@@ -169,7 +169,7 @@ function quarantine($_action, $_data = null) {
}
}
elseif ($release_format == 'raw') {
$detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $detail_row['msg']);
$detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/', 'X-Pre-Release-Spam-Flag $1', $detail_row['msg']);
$postfix_talk = array(
array('220', 'HELO quarantine' . chr(10)),
array('250', 'MAIL FROM: ' . $sender . chr(10)),
@@ -464,7 +464,7 @@ function quarantine($_action, $_data = null) {
}
}
elseif ($release_format == 'raw') {
$row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $row['msg']);
$row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/', 'X-Pre-Release-Spam-Flag $1', $row['msg']);
$postfix_talk = array(
array('220', 'HELO quarantine' . chr(10)),
array('250', 'MAIL FROM: ' . $sender . chr(10)),
@@ -833,7 +833,7 @@ function quarantine($_action, $_data = null) {
)));
return false;
}
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash');
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash');
$stmt->execute(array(':hash' => $hash));
return $stmt->fetch(PDO::FETCH_ASSOC);
break;

View File

@@ -62,11 +62,7 @@ if ($app_links_processed){
}
}
// Workaround to get text with <br> straight to twig.
// Using "nl2br" doesn't work with Twig as it would escape everything by default.
if (isset($UI_TEXTS["ui_footer"])) {
$UI_TEXTS["ui_footer"] = nl2br($UI_TEXTS["ui_footer"]);
}
$globalVariables = [
'mailcow_hostname' => getenv('MAILCOW_HOSTNAME'),

View File

@@ -4,7 +4,7 @@ function init_db_schema()
try {
global $pdo;
$db_version = "07102025_1015";
$db_version = "27012025_1555";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -184,7 +184,6 @@ function init_db_schema()
"private_comment" => "TEXT",
"public_comment" => "TEXT",
"sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'",
"internal" => "TINYINT(1) NOT NULL DEFAULT '0'",
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
),
"keys" => array(
@@ -346,14 +345,10 @@ function init_db_schema()
"notified" => "TINYINT(1) NOT NULL DEFAULT '0'",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
"user" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'",
"qhash" => "VARCHAR(64)",
),
"keys" => array(
"primary" => array(
"" => array("id")
),
"key" => array(
"qhash" => array("qhash")
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
@@ -476,23 +471,6 @@ function init_db_schema()
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"mta_sts" => array(
"cols" => array(
"id" => "BIGINT NOT NULL",
"domain" => "VARCHAR(255) NOT NULL",
"version" => "VARCHAR(255) NOT NULL",
"mode" => "VARCHAR(255) NOT NULL",
"mx" => "VARCHAR(255) NOT NULL",
"max_age" => "VARCHAR(255) NOT NULL",
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
),
"keys" => array(
"primary" => array(
"" => array("domain")
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"user_acl" => array(
"cols" => array(
"username" => "VARCHAR(255) NOT NULL",
@@ -1337,14 +1315,6 @@ function init_db_schema()
$pdo->query($create);
}
// Clear old app_passwd log entries
$pdo->exec("DELETE FROM logs
WHERE role != 'unauthenticated'
AND JSON_EXTRACT(`call`, '$[0]') = 'app_passwd'
AND JSON_EXTRACT(`call`, '$[1]') = 'edit'
AND (JSON_CONTAINS_PATH(`call`, 'one', '$[2].password')
OR JSON_CONTAINS_PATH(`call`, 'one', '$[2].password2'));");
// Mitigate imapsync argument injection issue
$pdo->query("UPDATE `imapsync` SET `custom_params` = ''
WHERE `custom_params` LIKE '%pipemess%'
@@ -1512,10 +1482,6 @@ function init_db_schema()
'msg' => 'db_init_complete'
);
}
// fill quarantine.qhash
$pdo->query("UPDATE `quarantine` SET `qhash` = SHA2(CONCAT(`id`, `qid`), 256) WHERE ISNULL(`qhash`)");
} catch (PDOException $e) {
if (php_sapi_name() == "cli") {
echo "DB initialization failed: " . print_r($e, true) . PHP_EOL;

View File

@@ -1,9 +1,7 @@
<?php
// Start session
if (session_status() !== PHP_SESSION_ACTIVE) {
session_name($SESSION_NAME);
ini_set("session.cookie_httponly", 1);
ini_set("session.cookie_samesite", $SESSION_SAMESITE_POLICY);
ini_set('session.gc_maxlifetime', $SESSION_LIFETIME);
}

View File

@@ -19,16 +19,11 @@ if (isset($_POST["verify_tfa_login"])) {
unset($_SESSION['pending_tfa_methods']);
}
if (isset($_POST["verify_fido2_login"])) {
$res = fido2(array(
fido2(array(
"action" => "verify",
"token" => $_POST["token"],
"user" => "admin"
));
if (is_array($res) && $res['role'] == "admin" && !empty($res['username'])){
$_SESSION["mailcow_cc_username"] = $res['username'];
$_SESSION["mailcow_cc_role"] = $res['role'];
$_SESSION["fido2_cid"] = $res['cid'];
}
exit;
}

View File

@@ -30,16 +30,11 @@ if (isset($_POST["verify_tfa_login"])) {
unset($_SESSION['pending_tfa_methods']);
}
if (isset($_POST["verify_fido2_login"])) {
$res = fido2(array(
fido2(array(
"action" => "verify",
"token" => $_POST["token"],
"user" => "domainadmin"
));
if (is_array($res) && $res['role'] == "domainadmin" && !empty($res['username'])){
$_SESSION["mailcow_cc_username"] = $res['username'];
$_SESSION["mailcow_cc_role"] = $res['role'];
$_SESSION["fido2_cid"] = $res['cid'];
}
exit;
}

View File

@@ -21,7 +21,7 @@ if ($iam_provider){
}
} elseif ($_GET['code'] && $_GET['state'] === $_SESSION['oauth2state']) {
// Check given state against previously stored one to mitigate CSRF attack
// Received access token in $_GET['code']
// Recieved access token in $_GET['code']
// extract info and verify user
identity_provider('verify-sso');
}
@@ -66,21 +66,10 @@ if (isset($_POST["verify_tfa_login"])) {
die();
} else {
set_user_loggedin_session($_SESSION['pending_mailcow_cc_username']);
if (isset($_SESSION['oauth2_request'])) {
$oauth2_request = $_SESSION['oauth2_request'];
unset($_SESSION['oauth2_request']);
header('Location: ' . $oauth2_request);
die();
}
$user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_access']) == 1 &&
intval($user_details['attributes']['force_pw_update']) != 1 &&
getenv('SKIP_SOGO') != "y" &&
!$is_dual) {
header("Location: /SOGo/so/");
if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) {
header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}");
die();
} else {
header("Location: /user");
@@ -95,15 +84,11 @@ if (isset($_POST["verify_tfa_login"])) {
unset($_SESSION['pending_tfa_methods']);
}
if (isset($_POST["verify_fido2_login"])) {
$res = fido2(array(
fido2(array(
"action" => "verify",
"token" => $_POST["token"],
"user" => "user"
));
if (is_array($res) && $res['role'] == "user" && !empty($res['username'])){
set_user_loggedin_session($res['username']);
$_SESSION["fido2_cid"] = $res['cid'];
}
exit;
}
@@ -133,20 +118,11 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
header("Location: /mobileconfig.php");
die();
}
if (isset($_SESSION['oauth2_request'])) {
$oauth2_request = $_SESSION['oauth2_request'];
unset($_SESSION['oauth2_request']);
header('Location: ' . $oauth2_request);
die();
}
$user_details = mailbox("get", "mailbox_details", $login_user);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_access']) == 1 &&
intval($user_details['attributes']['force_pw_update']) != 1 &&
getenv('SKIP_SOGO') != "y" &&
!$is_dual) {
header("Location: /SOGo/so/");
if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) {
header("Location: /SOGo/so/{$login_user}");
die();
} else {
header("Location: /user");

View File

@@ -83,7 +83,6 @@ $DEFAULT_LANG = 'en-gb';
// https://en.wikipedia.org/wiki/IETF_language_tag
$AVAILABLE_LANGUAGES = array(
// 'ca-es' => 'Català (Catalan)',
'bg-bg' => 'Български (Bulgarian)',
'cs-cz' => 'Čeština (Czech)',
'da-dk' => 'Danish (Dansk)',
'de-de' => 'Deutsch (German)',
@@ -153,13 +152,6 @@ $LOG_PAGINATION_SIZE = 50;
// Session lifetime in seconds
$SESSION_LIFETIME = 10800;
// Session SameSite Policy
// Use "None", "Lax" or "Strict"
$SESSION_SAMESITE_POLICY = "Lax";
// Name of the session cookie
$SESSION_NAME = "MCSESSID";
// Label for OTP devices
$OTP_LABEL = "mailcow UI";
@@ -193,12 +185,6 @@ $MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'] = false;
// Enable SOGo access - Users will be redirected to SOGo after login (set to false to disable redirect by default)
$MAILBOX_DEFAULT_ATTRIBUTES['sogo_access'] = true;
// How to handle tagged emails
// none - No special handling
// subfolder - Create subfolder under INBOX (e.g. "INBOX/Facebook")
// subject - Add tag to subject (e.g. "[Facebook] Subject")
$MAILBOX_DEFAULT_ATTRIBUTES['tagged_mail_handler'] = "none";
// Send notification when quarantine is not empty (never, hourly, daily, weekly)
$MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification'] = 'hourly';
@@ -251,12 +237,12 @@ $FIDO2_FORMATS = array('apple', 'android-key', 'android-safetynet', 'fido-u2f',
// Set visible Rspamd maps in mailcow UI, do not change unless you know what you are doing
$RSPAMD_MAPS = array(
'regex' => array(
'Header-From: Denylist' => 'global_mime_from_blacklist.map',
'Header-From: Allowlist' => 'global_mime_from_whitelist.map',
'Envelope Sender Denylist' => 'global_smtp_from_blacklist.map',
'Envelope Sender Allowlist' => 'global_smtp_from_whitelist.map',
'Recipient Denylist' => 'global_rcpt_blacklist.map',
'Recipient Allowlist' => 'global_rcpt_whitelist.map',
'Header-From: Blacklist' => 'global_mime_from_blacklist.map',
'Header-From: Whitelist' => 'global_mime_from_whitelist.map',
'Envelope Sender Blacklist' => 'global_smtp_from_blacklist.map',
'Envelope Sender Whitelist' => 'global_smtp_from_whitelist.map',
'Recipient Blacklist' => 'global_rcpt_blacklist.map',
'Recipient Whitelist' => 'global_rcpt_whitelist.map',
'Fishy TLDS (only fired in combination with bad words)' => 'fishy_tlds.map',
'Bad Words (only fired in combination with fishy TLDs)' => 'bad_words.map',
'Bad Words DE (only fired in combination with fishy TLDs)' => 'bad_words_de.map',
@@ -270,57 +256,57 @@ $RSPAMD_MAPS = array(
$IMAPSYNC_OPTIONS = array(
'whitelist' => array(
'abort',
'authmd51',
'authmd52',
'abort',
'authmd51',
'authmd52',
'authmech1',
'authmech2',
'authuser1',
'authuser2',
'debug',
'debugcontent',
'debugcrossduplicates',
'debugflags',
'debugfolders',
'debugimap',
'debugimap1',
'debugimap2',
'debugmemory',
'debugssl',
'authuser1',
'authuser2',
'debug',
'debugcontent',
'debugcrossduplicates',
'debugflags',
'debugfolders',
'debugimap',
'debugimap1',
'debugimap2',
'debugmemory',
'debugssl',
'delete1emptyfolders',
'delete2folders',
'disarmreadreceipts',
'delete2folders',
'disarmreadreceipts',
'domain1',
'domain2',
'domino1',
'domino2',
'domino1',
'domino2',
'dry',
'errorsmax',
'exchange1',
'exchange2',
'exchange1',
'exchange2',
'exitwhenover',
'expunge1',
'f1f2',
'filterbuggyflags',
'f1f2',
'filterbuggyflags',
'folder',
'folderfirst',
'folderlast',
'folderrec',
'gmail1',
'gmail2',
'idatefromheader',
'gmail1',
'gmail2',
'idatefromheader',
'include',
'inet4',
'inet6',
'justconnect',
'justfolders',
'justfoldersizes',
'justlogin',
'keepalive1',
'keepalive2',
'justconnect',
'justfolders',
'justfoldersizes',
'justlogin',
'keepalive1',
'keepalive2',
'log',
'logdir',
'logfile',
'logfile',
'maxbytesafter',
'maxlinelength',
'maxmessagespersecond',
@@ -328,62 +314,62 @@ $IMAPSYNC_OPTIONS = array(
'maxsleep',
'minage',
'minsize',
'noabletosearch',
'noabletosearch1',
'noabletosearch2',
'noexpunge1',
'noexpunge2',
'noabletosearch',
'noabletosearch1',
'noabletosearch2',
'noexpunge1',
'noexpunge2',
'nofoldersizesatend',
'noid',
'nolog',
'nomixfolders',
'noresyncflags',
'nossl1',
'nossl2',
'nosyncacls',
'notls1',
'notls2',
'nouidexpunge2',
'nousecache',
'noid',
'nolog',
'nomixfolders',
'noresyncflags',
'nossl1',
'nossl2',
'nosyncacls',
'notls1',
'notls2',
'nouidexpunge2',
'nousecache',
'oauthaccesstoken1',
'oauthaccesstoken2',
'oauthdirect1',
'oauthdirect2',
'office1',
'office2',
'pidfile',
'pidfilelocking',
'office1',
'office2',
'pidfile',
'pidfilelocking',
'prefix1',
'prefix2',
'proxyauth1',
'proxyauth2',
'resyncflags',
'resynclabels',
'search',
'proxyauth1',
'proxyauth2',
'resyncflags',
'resynclabels',
'search',
'search1',
'search2',
'search2',
'sep1',
'sep2',
'showpasswords',
'skipemptyfolders',
'ssl2',
'ssl2',
'sslargs1',
'sslargs2',
'sslargs2',
'subfolder1',
'subscribe',
'subscribe',
'subscribed',
'syncacls',
'syncduplicates',
'syncinternaldates',
'synclabels',
'tests',
'testslive',
'testslive6',
'tls2',
'truncmess',
'usecache',
'useheader',
'useuid'
'synclabels',
'tests',
'testslive',
'testslive6',
'tls2',
'truncmess',
'usecache',
'useheader',
'useuid'
),
'blacklist' => array(
'skipmess',

View File

@@ -11,8 +11,8 @@ if (isset($_SESSION['mailcow_cc_role']) && isset($_SESSION['oauth2_request'])) {
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
$user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']);
$is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false;
if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual && getenv('SKIP_SOGO') != "y") {
header("Location: /SOGo/so/");
if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual) {
header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}");
} else {
header("Location: /user");
}
@@ -33,18 +33,16 @@ $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
$has_iam_sso = false;
if ($iam_provider){
$iam_redirect_url = identity_provider("get-redirect");
$has_iam_sso = $iam_redirect_url ? true : false;
$has_iam_sso = identity_provider("get-redirect") ? true : false;
}
$custom_login = customize('get', 'custom_login');
$template = 'user_index.twig';
$template_data = [
'oauth2_request' => @$_SESSION['oauth2_request'],
'is_mobileconfig' => str_contains($_SESSION['index_query_string'], 'mobileconfig'),
'login_delay' => @$_SESSION['ldelay'],
'has_iam_sso' => $has_iam_sso,
'custom_login' => $custom_login,
'has_iam_sso' => $has_iam_sso
];
$js_minifier->add('/web/js/site/index.js');

View File

@@ -22,8 +22,8 @@ $(document).ready(function() {
$.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
}
$(".generate_password").click(async function( event ) {
try {
$(".generate_password").click(async function( event ) {
try {
var password_policy = await window.fetch("/api/v1/get/passwordpolicy", { method:'GET', cache:'no-cache' });
var password_policy = await password_policy.json();
random_passwd_length = password_policy.length;
@@ -48,11 +48,7 @@ $(document).ready(function() {
})
}
$(".rot-enc").html(function(){
footer_html = $(this).html();
footer_html = footer_html.replace(/&lt;/g, '<').replace(/&gt;/g, '>')
.replace(/&amp;/g, '&').replace(/&nzc;/g, '&')
.replace(/&quot;/g, '"').replace(/&#x27;/g, "'");
return str_rot13(footer_html)
return str_rot13($(this).html())
});
// https://stackoverflow.com/questions/4399005/implementing-jquerys-shake-effect-with-animate
function shake(div,interval,distance,times) {
@@ -129,7 +125,7 @@ $(document).ready(function() {
}
});
})();
// responsive tabs, scroll to opened tab
$(document).on("shown.bs.collapse shown.bs.tab", function (e) {
var target = $(e.target);
@@ -413,4 +409,4 @@ function copyToClipboard(id) {
// only works with https connections
navigator.clipboard.writeText(copyText.value);
mailcow_alert_box(lang.copy_to_clipboard, "success");
}
}

View File

@@ -51,7 +51,7 @@ jQuery(function($){
$('.submit_rspamd_regex').attr({"disabled": true});
});
$("#show_rspamd_global_filters").click(function() {
$.get("/inc/ajax/show_rspamd_global_filters.php");
$.get("inc/ajax/show_rspamd_global_filters.php");
$("#confirm_show_rspamd_global_filters").hide();
$("#rspamd_global_filters").removeClass("d-none");
});
@@ -558,7 +558,7 @@ jQuery(function($){
} else if (table == 'oauth2clientstable') {
$.each(data, function (i, item) {
item.action = '<div class="btn-group">' +
'<a href="/edit/oauth2client/' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-lg btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
'<a href="/edit.php?oauth2client=' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-lg btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
'<a href="#" data-action="delete_selected" data-id="single-oauth2-client" data-api-url="delete/oauth2-client" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-lg btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
'</div>';
item.scope = "profile";
@@ -573,7 +573,7 @@ jQuery(function($){
item.action = '<div class="btn-group">' +
'<a href="/edit/domainadmin/' + encodeURI(item.username) + '" class="btn btn-xs btn-xs-lg btn-xs-third btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
'<a href="#" data-action="delete_selected" data-id="single-domain-admin" data-api-url="delete/domain-admin" data-item="' + encodeURI(item.username) + '" class="btn btn-xs btn-xs-lg btn-xs-third btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
'<a href="/domainadmin/?duallogin=' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-xs-lg btn-xs-third btn-success"><i class="bi bi-person-fill"></i> Login</a>' +
'<a href="/index.php?duallogin=' + encodeURIComponent(item.username) + '" class="btn btn-xs btn-xs-lg btn-xs-third btn-success"><i class="bi bi-person-fill"></i> Login</a>' +
'</div>';
});
} else if (table == 'adminstable') {
@@ -655,7 +655,7 @@ jQuery(function($){
$(this).html('<i class="bi bi-arrow-repeat icon-spin"></i> ');
$.ajax({
type: 'GET',
url: '/inc/ajax/relay_check.php',
url: 'inc/ajax/relay_check.php',
dataType: 'text',
data: $('#test_relayhost_form').serialize(),
complete: function (data) {
@@ -681,7 +681,7 @@ jQuery(function($){
$(this).html('<div class="spinner-border" role="status"><span class="visually-hidden">Loading...</span></div> ');
$.ajax({
type: 'GET',
url: '/inc/ajax/transport_check.php',
url: 'inc/ajax/transport_check.php',
dataType: 'text',
data: $('#test_transport_form').serialize(),
complete: function (data) {
@@ -715,6 +715,7 @@ jQuery(function($){
$('.app_hide').off('change');
$('.app_hide').on('change', function (e) {
var value = $(this).is(':checked') ? '1' : '0';
console.log(value)
$(this).parent().children(':first-child').val(value);
})
}
@@ -788,18 +789,6 @@ jQuery(function($){
$('.iam_ldap_rolemap_del').click(async function(e){
deleteAttributeMappingRow(this, e);
});
$('.iam_redirect_add_keycloak').click(async function(e){
addRedirectUrlRow('#iam_keycloak_redirect_list', '.iam_keycloak_redirect_del', e);
});
$('.iam_redirect_add_generic').click(async function(e){
addRedirectUrlRow('#iam_generic_redirect_list', '.iam_generic_redirect_del', e);
});
$('.iam_keycloak_redirect_del').click(async function(e){
deleteRedirectUrlRow(this, e);
});
$('.iam_generic_redirect_del').click(async function(e){
deleteRedirectUrlRow(this, e);
});
// selecting identity provider
$('#iam_provider').on('change', function(){
// toggle password fields
@@ -844,22 +833,4 @@ jQuery(function($){
if ($(elem).parent().parent().parent().parent().children().length > 1)
$(elem).parent().parent().parent().remove();
}
function addRedirectUrlRow(list_id, del_class, e) {
e.preventDefault();
var parent = $(list_id)
$(parent).children().last().clone().appendTo(parent);
var newChild = $(parent).children().last();
$(newChild).find('input').val('');
$(del_class).off('click');
$(del_class).click(async function(e){
deleteRedirectUrlRow(this, e);
});
}
function deleteRedirectUrlRow(elem, e) {
e.preventDefault();
if ($(elem).parent().parent().parent().parent().children().length > 2)
$(elem).parent().parent().parent().remove();
}
});

View File

@@ -47,6 +47,8 @@ $(document).ready(function() {
window.fetch("/api/v1/get/status/host/ip", { method:'GET', cache:'no-cache' }).then(function(response) {
return response.json();
}).then(function(data) {
console.log(data);
// display host ips
if (data.ipv4)
$("#host_ipv4").text(data.ipv4);
@@ -1005,7 +1007,7 @@ jQuery(function($){
"data-order": cellData.sortBy,
"data-sort": cellData.sortBy
});
},
},
render: function (data) {
return data.value;
}
@@ -1030,7 +1032,7 @@ jQuery(function($){
"data-order": cellData.sortBy,
"data-sort": cellData.sortBy
});
},
},
render: function (data) {
return data.value;
}
@@ -1346,6 +1348,8 @@ function update_stats(timeout=5){
window.fetch("/api/v1/get/status/host", {method:'GET',cache:'no-cache'}).then(function(response) {
return response.json();
}).then(function(data) {
console.log(data);
if (data){
// display table data
$("#host_date").text(data.system_time);
@@ -1395,6 +1399,8 @@ function update_container_stats(timeout=5){
var diskIOCtx = Chart.getChart(container + "_DiskIOChart");
var netIOCtx = Chart.getChart(container + "_NetIOChart");
console.log(container);
console.log(data);
prev_stats = null;
if (data.length >= 2){
prev_stats = data[data.length -2];

View File

@@ -66,6 +66,7 @@ $(document).ready(function() {
// load tags
if ($('#tags').length){
var tagsEl = $('#tags').parent().find('.tag-values')[0];
console.log($(tagsEl).val())
var tags = JSON.parse($(tagsEl).val());
$(tagsEl).val("");

View File

@@ -269,24 +269,6 @@ $(document).ready(function() {
function setMailboxTemplateData(template){
$("#addInputQuota").val(template.quota / 1048576);
if (template.tagged_mail_handler === "subfolder"){
$('#tagged_mail_handler_subfolder').prop('checked', true);
$('#tagged_mail_handler_subject').prop('checked', false);
$('#tagged_mail_handler_none').prop('checked', false);
} else if(template.tagged_mail_handler === "subject"){
$('#tagged_mail_handler_subfolder').prop('checked', false);
$('#tagged_mail_handler_subject').prop('checked', true);
$('#tagged_mail_handler_none').prop('checked', false);
} else if(template.tagged_mail_handler === "none"){
$('#tagged_mail_handler_subfolder').prop('checked', false);
$('#tagged_mail_handler_subject').prop('checked', false);
$('#tagged_mail_handler_none').prop('checked', true);
} else {
$('#tagged_mail_handler_subfolder').prop('checked', false);
$('#tagged_mail_handler_subject').prop('checked', false);
$('#tagged_mail_handler_none').prop('checked', true);
}
if (template.quarantine_notification === "never"){
$('#quarantine_notification_never').prop('checked', true);
$('#quarantine_notification_hourly').prop('checked', false);
@@ -1949,6 +1931,11 @@ jQuery(function($){
defaultContent: '',
responsivePriority: 5,
},
{
title: lang.bcc_destinations,
data: 'bcc_dest',
defaultContent: ''
},
{
title: lang.sogo_visible,
data: 'sogo_visible',
@@ -1967,15 +1954,6 @@ jQuery(function($){
data: 'private_comment',
defaultContent: ''
},
{
title: lang.internal,
data: 'internal',
defaultContent: '',
responsivePriority: 6,
render: function (data, type) {
return 1==data?'<i class="bi bi-check-lg"><span class="sorting-value">1</span></i>':0==data&&'<i class="bi bi-x-lg"><span class="sorting-value">0</span></i>';
}
},
{
title: lang.active,
data: 'active',

View File

@@ -90,24 +90,31 @@ jQuery(function($){
console.log('error reading last logins');
},
success: function (data) {
$('.last-ui-login').html('');
$('.last-sasl-login').html('');
if (data.ui.time) {
$('.last-ui-login').html('<i class="bi bi-person-fill"></i> ' + lang.last_ui_login + ': ' + unix_time_format(data.ui.time));
} else {
$('.last-ui-login').text(lang.no_last_login);
}
if (data.sasl) {
$('.last-sasl-login').append('<ul class="list-group">');
$.each(data.sasl, function (i, item) {
var datetime = new Date(item.datetime.replace(/-/g, "/"));
var local_datetime = datetime.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
var service = '<div class="badge bg-secondary">' + item.service.toUpperCase() + '</div>';
var app_password = item.app_password ? ' <a href="/edit/app-passwd/' + item.app_password + '"><i class="bi bi-key-fill"></i><span class="ms-1">' + escapeHtml(item.app_password_name || "App") + '</span></a>' : '';
var real_rip = item.real_rip.startsWith("Web") ? item.real_rip : '<a href="https://bgp.tools/prefix/' + item.real_rip + '" target="_blank">' + item.real_rip + "</a>";
var app_password = item.app_password ? ' <a href="/edit/app-passwd/' + item.app_password + '"><i class="bi bi-app-indicator"></i> ' + escapeHtml(item.app_password_name || "App") + '</a>' : '';
var real_rip = item.real_rip.startsWith("Web") ? item.real_rip : '<a href="https://bgp.he.net/ip/' + item.real_rip + '" target="_blank">' + item.real_rip + "</a>";
var ip_location = item.location ? ' <span class="flag-icon flag-icon-' + item.location.toLowerCase() + '"></span>' : '';
var ip_data = real_rip + ip_location + app_password;
$(".last-sasl-login").append(`
<li class="list-group-item d-flex justify-content-between align-items-start">
<div class="ms-2 me-auto d-flex flex-column">
<div class="fw-bold">` + ip_location + real_rip + `</div>
<small class="fst-italic mt-2">` + service + ` ` + local_datetime + `</small>` + app_password + `
<div class="fw-bold">` + real_rip + `</div>
<small class="fst-italic mt-2">` + service + ` ` + local_datetime + `</small>
</div>
<span>` + ip_location + `</span>
</li>
`);
})
@@ -168,6 +175,7 @@ jQuery(function($){
type: "GET",
url: "/api/v1/get/time_limited_aliases",
dataSrc: function(data){
console.log(data);
$.each(data, function (i, item) {
if (acl_data.spam_alias === 1) {
item.action = '<div class="btn-group">' +
@@ -260,6 +268,7 @@ jQuery(function($){
type: "GET",
url: '/api/v1/get/syncjobs/' + encodeURIComponent(mailcow_cc_username) + '/no_log',
dataSrc: function(data){
console.log(data);
$.each(data, function (i, item) {
item.user1 = escapeHtml(item.user1);
item.log = '<a href="#syncjobLogModal" data-bs-toggle="modal" data-syncjob-id="' + item.id + '">' + lang.open_logs + '</a>'
@@ -415,6 +424,7 @@ jQuery(function($){
type: "GET",
url: '/api/v1/get/app-passwd/all',
dataSrc: function(data){
console.log(data);
$.each(data, function (i, item) {
item.name = escapeHtml(item.name)
item.protocols = []
@@ -510,6 +520,7 @@ jQuery(function($){
type: "GET",
url: '/api/v1/get/policy_wl_mailbox',
dataSrc: function(data){
console.log(data);
$.each(data, function (i, item) {
if (validateEmail(item.object)) {
item.chkbox = '<input type="checkbox" class="form-check-input" data-id="policy_wl_mailbox" name="multi_select" value="' + item.prefid + '" />';
@@ -580,6 +591,7 @@ jQuery(function($){
type: "GET",
url: '/api/v1/get/policy_bl_mailbox',
dataSrc: function(data){
console.log(data);
$.each(data, function (i, item) {
if (validateEmail(item.object)) {
item.chkbox = '<input type="checkbox" class="form-check-input" data-id="policy_bl_mailbox" name="multi_select" value="' + item.prefid + '" />';

View File

@@ -324,9 +324,6 @@ if (isset($_GET['query'])) {
case "app-passwd":
process_add_return(app_passwd('add', $attr));
break;
case "mta-sts":
process_add_return(mailbox('add', 'mta_sts', $attr));
break;
// return no route found if no case is matched
default:
http_response_code(404);
@@ -1979,9 +1976,6 @@ if (isset($_GET['query'])) {
case "ip_check":
process_edit_return(customize('edit', 'ip_check', $attr));
break;
case "custom_login":
process_edit_return(customize('edit', 'custom_login', $attr));
break;
case "self":
if ($_SESSION['mailcow_cc_role'] == "domainadmin") {
process_edit_return(domain_admin('edit', $attr));
@@ -2004,9 +1998,6 @@ if (isset($_GET['query'])) {
case "reset-password-notification":
process_edit_return(reset_password('edit_notification', $attr));
break;
case "mta-sts":
process_edit_return(mailbox('edit', 'mta_sts', array_merge(array('domains' => $items), $attr)));
break;
// return no route found if no case is matched
default:
http_response_code(404);

File diff suppressed because it is too large Load Diff

View File

@@ -557,4 +557,4 @@
"week": "Setmana",
"weeks": "Setmanes"
}
}
}

View File

@@ -1,7 +1,7 @@
{
"acl": {
"alias_domains": "Doménové aliasy",
"app_passwds": "Správa hesel aplikací",
"app_passwds": "Hesla aplikací",
"bcc_maps": "BCC mapy",
"delimiter_action": "Zacházení s označkovanou poštou",
"domain_desc": "Změnit popis domény",
@@ -24,7 +24,7 @@
"sogo_access": "Správa přístupu do SOGo",
"sogo_profile_reset": "Resetování profilu SOGo",
"spam_alias": "Dočasné aliasy",
"spam_policy": "Denylist/Allowlist",
"spam_policy": "Blacklist/Whitelist",
"spam_score": "Skóre spamu",
"syncjobs": "Synchronizační úlohy",
"tls_policy": "Pravidla TLS",
@@ -82,12 +82,12 @@
"password": "Heslo",
"password_repeat": "Potvrzení nového hesla (opakujte)",
"port": "Port",
"post_domain_add": "Po přidání nové domény se musí restartovat kontejner SOGo!<br><br>Je také třeba ověřit nastavení DNS nové domény. Po ověření restartujte kontejner \"acme-mailcow\", aby se vygenerovaly certifikáty domény (autoconfig.&lt;domain&gt;, autodiscover.&lt;domain&gt;).<br>Tento krok je volitelný, a provede se automaticky každých 24 hodin.",
"post_domain_add": "Po přidání nové domény je nutné restartovat SOGo kontejner!",
"private_comment": "Soukromý komentář",
"public_comment": "Veřejný komentář",
"quota_mb": "Kvóta (MiB)",
"relay_all": "Předávání všech příjemců",
"relay_all_info": "Pokud se rozhodnete <b>nepředávat</b> všechny příjemce, musíte přidat prázdnou mailovou schránku pro každého příjemce, který se má předávat.",
"relay_all_info": "<small>Pokud se rozhodnete <b>nepředávat</b> všechny příjemce, musíte přidat prázdnou mailovou schránku pro každého příjemce, který se má předávat.</small>",
"relay_domain": "Předávání domény",
"relay_transport_info": "<div class=\"badge fs-6 bg-info\">Info</div> U této domény lze pro konkrétní cíl nastavit transportní mapu. Není-li nastavena, použije se MX záznam.",
"relay_unknown_only": "Předávat jen neexistující schránky. Doručení do existujících proběhne lokálně.",
@@ -109,9 +109,7 @@
"validate": "Ověřit",
"validation_success": "Úspěšně ověřeno",
"tags": "Štítky",
"dry": "Simulovat synchronizaci",
"internal": "Interní",
"internal_info": "Interní aliasy jsou přístupné jen z vlastních domén nebo jejich aliasů."
"dry": "Simulovat synchronizaci"
},
"admin": {
"access": "Přístupy",
@@ -185,7 +183,7 @@
"f2b_ban_time": "Doba blokování (s)",
"f2b_blacklist": "Sítě/hostitelé na blacklistu",
"f2b_filter": "Regex filtre",
"f2b_list_info": "Síť nebo hostitelé na blacklistu mají vždy větší váhu než položky na whitelistu. <b>Každá úprava seznamů trvá pár sekund.</b>",
"f2b_list_info": "Síť nebo hostitelé na blacklistu mají vždy větší váhu než položky na whitelistu. Blacklist se sestavuje vždy při startu kontejneru.",
"f2b_max_attempts": "Max. pokusů",
"f2b_netban_ipv4": "Rozsah IPv4 podsítě k zablokování (8-32)",
"f2b_netban_ipv6": "Rozsah IPv6 podsítě k zablokování (8-128)",
@@ -258,7 +256,7 @@
"quarantine_exclude_domains": "Vyloučené domény a doménové aliasy",
"quarantine_max_age": "Maximální stáří ve dnech<br><small>Hodnota musí být rovna nebo větší než 1 den.</small>",
"quarantine_max_score": "Neposílat notifikace pokud je spam skóre větší než hodnota:<br><small>Výchozí je 9999.0</small>",
"quarantine_max_size": "Maximální velikost v MiB (větší prvky budou smazány)<br /><small>0 <b>neznamená</b> neomezeno.</small>",
"quarantine_max_size": "Maximální velikost v MiB (větší prvky budou smazány)<br />0 <b>neznamená</b> neomezeno.",
"quarantine_notification_html": "Šablona upozornění:<br><small>Ponechte prázdné, aby se obnovila výchozí šablona.</small>",
"quarantine_notification_sender": "Odesílatel upozornění",
"quarantine_notification_subject": "Předmět upozornění",
@@ -266,7 +264,7 @@
"quarantine_release_format": "Formát propuštěných položek",
"quarantine_release_format_att": "Jako příloha",
"quarantine_release_format_raw": "Nezměněný originál",
"quarantine_retention_size": "Počet zadržených zpráv na mailovou schránku<br /><small>0 znamená <b>neaktivní</b>.</small>",
"quarantine_retention_size": "Počet zadržených zpráv na mailovou schránku<br />0 znamená <b>neaktivní</b>.",
"quota_notification_html": "Šablona upozornění:<br><small>Ponechte prázdné, aby se obnovila výchozí šablona.</small>",
"quota_notification_sender": "Odesílatel upozornění",
"quota_notification_subject": "Předmět upozornění",
@@ -285,7 +283,7 @@
"relay_rcpt": "\"Komu:\" adresa",
"relay_run": "Provést test",
"relayhosts": "Transporty podle odesílatele",
"relayhosts_hint": "Zde definujte transporty podle odesílatele, jež pak můžete použít v nastavení domény.<br>\nProtokol transportu je vždy \"smtp:\" a použije se TLS, je-li nabídnuto. Zabalené TLS (SMTPS) se nepodporuje. Bere se v potaz uživatelské nastavení odchozího TLS.<br>\nTýká se vybraných domén včetně doménových aliasů.",
"relayhosts_hint": "Zde definujte transporty podle odesílatele, jež pak můžete použít v nastavení domény.<br>\r\nProtokol transportu je vždy \"smtp:\". Bere se v potaz uživatelské nastavení odchozího TLS.",
"remove": "Smazat",
"remove_row": "Smazat řádek",
"reset_default": "Obnovit výchozí nastavení",
@@ -301,11 +299,11 @@
"rsettings_preset_2": "Postmasteři chtějí dostávat spam",
"rsettings_preset_3": "Povolit jen určité odesílatele pro schránku (např. jen interní schránka)",
"rsettings_preset_4": "Deaktivujte Rspamd pro doménu",
"rspamd_com_settings": "Název nastavení se vygeneruje automaticky, viz ukázky nastavení níže. Více informací viz <a href=\"https://rspamd.com/doc/configuration/settings.html#settings-structure\" target=\"_blank\">Rspamd dokumentace</a>",
"rspamd_com_settings": "<a href=\"https://rspamd.com/doc/configuration/settings.html#settings-structure\" target=\"_blank\">Rspamd dokumentace</a>\r\n - Název nastavení bude automaticky vygenerován, viz níže uvedené předvolby.",
"rspamd_global_filters": "Mapa globálních filtrů",
"rspamd_global_filters_agree": "Budu opatrný!",
"rspamd_global_filters_info": "Mapa globálních filtrů obsahuje jiné globální black- a whitelisty.",
"rspamd_global_filters_regex": "Názvy stačí k vysvětlení. Položky musejí obsahovat jen platné regulární výrazy ve tvaru \"/vyraz/parametry\" (e.g. <code>/.+@domena\\.tld/i</code>).<br>\n Každý výraz bude podroben základní kontrole, přesto je možné Rspamd 'rozbít', nebude-li syntax zcela korektní.<br>\n Rspamd se pokusí po každé změně načíst mapu znovu. V případě potíží <a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">restartujte Rspamd</a>, aby se konfigurace načetla explicitně.",
"rspamd_global_filters_regex": "Názvy jsou dostatečným vysvětlením. Musí obsahovat jen platné regulární výrazy ve formátu \"/vyraz/parametry\" (e.g. <code>/.+@domena\\.tld/i</code>).<br>\r\n Každý výraz bude podroben základní kontrole, přesto je možné Rspamd 'rozbít', nebude-li syntax zcela korektní.<br>\r\n Rspamd se pokusí načíst mapu po každé změně. V případě potíží, <a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">restartujte Rspamd</a>, aby se konfigurace načetla explicitně.",
"rspamd_settings_map": "Nastavení Rspamd",
"sal_level": "Úroveň 'Moo'",
"save": "Uložit změny",
@@ -326,8 +324,8 @@
"to_top": "Zpět na začátek",
"transport_dest_format": "Formát: example.org, .example.org, *, box@example.org (vícero položek lze oddělit čárkou)",
"transport_maps": "Transportní mapy",
"transport_test_rcpt_info": "&#8226; K otestování předávání pošty ven použijte null@hosted.mailcow.de.",
"transports_hint": "&#8226; Položka transportní mapy <b>přebíjí</b> transportní mapu podle odesílatele</b>.<br>\n&#8226; Transporty založené na MX mají přednost.<br>\n&#8226; Uživatelské nastavení odchozího TLS se ignoruje a lze je vynutit výhradně mapou TLS pravidel.<br>\n&#8226; Transportní služnou pro tyto transporty je vždy \"smtp:\" a použije se TLS, je-li nabídnuto. Zabalené TLS (SMTPS) se nepodporuje.<br>\n&#8226; Adresy, jež odpovídají výrazu \"/localhost$/\", se vždy předají přes \"local:\", takže nejsou zahrnuty do definice cíle \"*\".<br>\n&#8226; Pro stanovení přihlašovacích údajů dalšího skoku, např. \"[host]:25\", bude Postfix <b>vždy</b> hledat nejdříve \"host\" a teprve pak \"[host]:25\". Kvůli tomu nelze použít současně \"host\" a \"[host]:25\".",
"transport_test_rcpt_info": "&#8226; Na otestování odchozí pošty je možné použít null@hosted.mailcow.de jako adresáta",
"transports_hint": " Položka transportní mapy <b>přebíjí</b> transportní mapu podle odesílatele</b>.<br>\r\n→ Uživatelské nastavení odchozího TLS se ignoruje a lze je výhradně vynutit mapováním TLS pravidel.<br>\r\n→ Protokol transportu je vždy \"smtp:\".<br>\r\n→ Adresy, jež odpovídají výrazu \"/localhost$/\", se vždy předají přes \"local:\", takže nejsou zahrnuty do definice cíle \"*\".<br>\r\n→ Pro stanovení přihlašovacích údajů dalšího skoku, např. \"[host]:25\", bude Postfix <b>vždy</b> hledat nejdříve \"host\" a teprve pak \"[host]:25\". Kvůli tomu nelze použít současně \"host\" a \"[host]:25\"",
"ui_footer": "Pata stránka (HTML povoleno)",
"ui_header_announcement": "Oznámení",
"ui_header_announcement_active": "Nastavit jako aktivní",
@@ -346,72 +344,17 @@
"validate_license_now": "Ověřit GUID na licenčním serveru",
"verify": "Ověřit",
"yes": "&#10003;",
"f2b_ban_time_increment": "Délka bloku se prodlužuje s každým dalším zablokováním",
"f2b_max_ban_time": "Maximální délka bloku (s)",
"f2b_ban_time_increment": "Délka banu je prodlužována s každým dalším banem",
"f2b_max_ban_time": "Maximální délka banu (s)",
"cors_settings": "Nastavení CORS",
"queue_unban": "odblokovat",
"queue_unban": "zrušit ban",
"password_reset_info": "Pokud není zadán žádný e-mail pro obnovení, nelze tuto funkci použít.",
"password_reset_settings": "Nastavení obnovení hesla",
"password_settings": "Nastavení hesel",
"password_reset_tmpl_html": "HTML šablona",
"password_reset_tmpl_text": "Textová šablona",
"reset_password_vars": "<code>{{link}}</code> Vygenerovaný odkaz pro obnovení hesla<br><code>{{username}}</code> Název mailboxu uživatele, který požádal o resetování hesla.<br><code>{{username2}}</code> Název schránky pro obnovení<br><code>{{date}}</code> Datum podání žádosti o obnovení hesla<br><code>{{token_lifetime}}</code> Délka životnosti tokenu v minutách<br><code>{{hostname}}</code> Název serveru mailcow",
"restore_template": "Ponechte prázdné pro obnovení výchozí šablony.",
"copy_to_clipboard": "Text zkopírován do schránky!",
"iam_login_provisioning": "Automaticky vytvořit uživatele při přihlášení",
"user_quicklink": "Skrýt zkratku na přihlášení uživatele",
"domainadmin_quicklink": "Skrýt zkratku na přihlášení správce domény",
"iam_auth_flow_info": "Kromě metody autorizačního kódu (Authorization Code Flow, výchozího v Keycloaku), jež se používá pro SSO, podporuje mailcow také metody autentizaci přímo pomocí přihlašovacích údajů. The metoda Mailpassword Flow se pokusí ověřit přihlašovací údaje uživatele přímo v Admin REST API Keycloaku. mailcow získá hash hesla z atributu <code>mailcow_password</code> , namapovaného v Keycloaku.",
"iam_default_template_description": "Nemá-li uživatel přiřazenu šablonu, použije se výchozí šablona k vytvoření schránky, ale ne k její úpravě či aktualizaci.",
"iam_userinfo_url": "Koncový bod pro informace o uživatelích",
"iam_redirect_url": "URL přesměrování",
"user_link": "Odkaz pro uživatele",
"force_sso_text": "Je-li nastaven externí poskytovatel OIDC, zapnutím této volby skryjete výchozí přihlašovací formulář. Zůstane vidět jen tlačítko pro SSO",
"iam_mapping": "Mapování atributů",
"iam_bindpass": "Heslo pro bind",
"iam_periodic_full_sync": "Pravidelná úplná synchronizace",
"iam_port": "Port",
"iam_realm": "Realm",
"iam_rest_flow": "Mailpassword Flow",
"iam_server_url": "URL serveru",
"iam_sso": "Single Sign-On",
"iam_sync_interval": "Interval synchronizace/importu (min)",
"iam_test_connection": "Test spojení",
"iam_token_url": "Koncový bod pro tokeny",
"iam_username_field": "Pole uživatelského jména",
"iam_binddn": "Doména pro bind",
"iam_use_ssl": "Používat SSL",
"iam_use_tls": "Používat StartTLS",
"iam_version": "Verze",
"quicklink_text": "Zobrazení zkratek k dalším přihlašovacím stránkám",
"iam_use_tls_info": "Je-li zapnuto TLS, musí se používat standardní port pro LDAP (389). Port SSL nelze použít.",
"ignore_ssl_error": "Ignorovat chyby SSL",
"iam_use_ssl_info": "Je-li zapnuto SSL a nastaven port 389, použije se automaticky port 636.",
"task": "Úloha",
"app_hide": "Skrýt při přihlášení",
"admin_quicklink": "Skrýt zkratku na přihlášení správce",
"allowed_methods": "Access-Control-Allow-Methods",
"allowed_origins": "Access-Control-Allow-Origin",
"login_page": "Přihlašovací stránka",
"f2b_manage_external": "Spravovat Fail2Ban externě",
"f2b_manage_external_info": "Fail2ban bude udržovat seznam zakázaných adres, ale nebude aktivně nastavovat pravidla blokování. Pro blokování použijte seznam adres níže.",
"filter": "Filtr",
"force_sso": "Vypnout přihlášení mailcow a ponechat jen SSO",
"iam": "Poskytovatel identity",
"iam_attribute_field": "Pole atributu",
"iam_authorize_url": "Autorizační koncový bod",
"iam_basedn": "Doména (Base DN)",
"iam_client_id": "ID klienta",
"iam_client_secret": "Tajný kód klienta",
"iam_client_scopes": "Scopes klienta",
"iam_default_template": "Výchozí šablona",
"iam_description": "Nastavení externího poskytovatele ověření<br>Schránky uživatele se vytvoří po prvním přihlášení automaticky, pokud je tedy nastaveno mapování atributů.",
"iam_extra_permission": "Aby vše fungovalo, musí mít mailcow klient v Keycloaku nastavený <code>servisní účet</code> a povolení <code>view-users</code>.",
"iam_host": "Hostitel",
"iam_host_info": "Zadejte jeden či více hostitelů, oddělte čárkou.",
"iam_import_users": "Importovat uživatele",
"iam_auth_flow": "Proces autentizace",
"needs_restart": "potřebuje restart"
"restore_template": "Ponechte prázdné pro obnovení výchozí šablony."
},
"danger": {
"access_denied": "Přístup odepřen nebo jsou neplatná data ve formuláři",
@@ -465,7 +408,7 @@
"is_alias": "%s je již známa jako adresa aliasu",
"is_alias_or_mailbox": "%s je již známa jako adresa aliasu, mailové schránky nebo aliasu rozvedeného z aliasu domény.",
"is_spam_alias": "%s je již známa jako adresa spamového aliasu",
"last_key": "Nelze smazat poslední klíč, vypněte tedy celé TFA.",
"last_key": "Nelze smazat poslední klíč",
"login_failed": "Přihlášení selhalo",
"mailbox_defquota_exceeds_mailbox_maxquota": "Výchozí kvóta překračuje maximální kvótu schránky\"",
"mailbox_invalid": "Název mailové schránky je neplatný",
@@ -543,20 +486,7 @@
"demo_mode_enabled": "Demo režim je zapnutý",
"recovery_email_failed": "Nepodařilo se odeslat e-mail pro obnovení. Obraťte se prosím na svého správce.",
"password_reset_invalid_user": "Mailbox nebyl nalezen nebo není nastaven žádný e-mail pro obnovu",
"password_reset_na": "Obnovení hesla není v současné době k dispozici. Obraťte se prosím na svého správce.",
"generic_server_error": "Došlo k nečekané chybě. Obraťte se na vašeho správce.",
"to_invalid": "Adresát nemůže být prázdný",
"authsource_in_use": "Poskytovatele identity nelze změnit nebo odstranit, neboť se právě používá pro jednoho či více uživatelů.",
"iam_test_connection": "Spojení selhalo",
"img_dimensions_exceeded": "Obrázek je větší než povolené rozměry",
"img_size_exceeded": "Obrázek má větší než povolenou velikost souboru",
"invalid_reset_token": "Neplatný resetovací token",
"required_data_missing": "Chybí potřebný údaj %s",
"reset_token_limit_exceeded": "Byl překročen limit na reset tokeny. Zkuste to později.",
"max_age_invalid": "Maximální životnost %s není platná",
"mode_invalid": "Mód %s není platný",
"mx_invalid": "Záznam MX %s není platný",
"version_invalid": "Verze %s není platná"
"password_reset_na": "Obnovení hesla není v současné době k dispozici. Obraťte se prosím na svého správce."
},
"datatables": {
"emptyTable": "Tabulka neobsahuje žádná data",
@@ -618,8 +548,7 @@
"update_failed": "Nepodařilo se zkontrolovat aktualizace",
"wip": "Nedokončená vývojová verze",
"memory": "Paměť",
"container_disabled": "Kontejner je zastaven nebo zakázán",
"cores": "jádra"
"container_disabled": "Kontejner je zastaven nebo zakázán"
},
"diagnostics": {
"cname_from_a": "Hodnota odvozena z A/AAAA záznamu. Lze použít, pokud záznam ukazuje na správný zdroj.",
@@ -640,7 +569,7 @@
"alias": "Upravit alias",
"allow_from_smtp": "Umožnit pouze těmto IP adresám používat <b>SMTP</b>",
"allow_from_smtp_info": "Nechte prázdné pro povolení všech odesílatelů.<br>IPv4/IPv6 adresy a sítě.",
"allowed_protocols": "Povolené protokoly pro přímá spojení (netýká se protokolů na změnu hesla)",
"allowed_protocols": "Povolené protokoly",
"app_name": "Název aplikace",
"app_passwd": "Heslo aplikace",
"app_passwd_protocols": "Povolené protokoly pro hesla aplikací",
@@ -678,7 +607,7 @@
"inactive": "Neaktivní",
"kind": "Druh",
"last_modified": "Naposledy změněn",
"lookup_mx": "Cíl je regulární výraz, jenž se porovná s MX záznamem (např. <code>.*\\.google\\.com</code> na tento skok nasměruje veškerou poštu s MX, jež končí na *.google.com)",
"lookup_mx": "Cíl je regulární výraz který se shoduje s MX záznamem (<code>.*\\.google\\.com</code> směřuje veškerou poštu na MX které jsou cílem pro google.com přes tento skok)",
"mailbox": "Úprava mailové schránky",
"mailbox_quota_def": "Výchozí kvóta schránky",
"mailbox_relayhost_info": "Aplikované jen na uživatelskou schránku a přímé aliasy, přepisuje předávající server domény.",
@@ -712,7 +641,7 @@
"ratelimit": "Omezení přenosu",
"redirect_uri": "URL přesměrování/odvolání",
"relay_all": "Předávání všech příjemců",
"relay_all_info": "Pokud se rozhodnete <b>nepředávat</b> všechny příjemce, musíte přidat prázdnou mailovou schránku pro každého příjemce, který se má předávat.",
"relay_all_info": "<small>Pokud se rozhodnete <b>nepředávat</b> všechny příjemce, musíte přidat prázdnou mailovou schránku pro každého příjemce, který se má předávat.</small>",
"relay_domain": "Předávání domény",
"relay_transport_info": "<div class=\"badge fs-6 bg-info\">Info</div> U této domény lze pro konkrétní cíl nastavit transportní mapu. Není-li nastavena, použije se MX záznam.",
"relay_unknown_only": "Předávat jen neexistující schránky. Doručení do existujících proběhne lokálně.",
@@ -737,7 +666,7 @@
"spam_score": "Nastavte vlastní skóre spamu",
"subfolder2": "Synchronizace do podsložky v cílovém umístění<br><small>(prázdné = nepoužívat podsložku)</small>",
"syncjob": "Upravit synchronizační úlohu",
"target_address": "Cílová adresa/y <small>(oddělte čárkou)</small>",
"target_address": "Cílová adresa/y<br /> <small>(oddělte čárkou)</small>",
"target_domain": "Cílová doména",
"timeout1": "Časový limit pro připojení ke vzdálenému serveru",
"timeout2": "Časový limit pro připojení k lokálnímu serveru",
@@ -761,26 +690,7 @@
"custom_attributes": "Vlastní atributy",
"footer_exclude": "Vyloučit ze zápatí",
"domain_footer_skip_replies": "Ignorovat patičku u odpovědí na e-maily",
"password_recovery_email": "E-mail pro obnovu hesla",
"mailbox_rename": "Přejmenovat schránku",
"mailbox_rename_agree": "Mám vytvořenou zálohu.",
"mailbox_rename_warning": "DŮLEŽITÉ! Vytvořte si zálohu schránky, než ji přejmenujete.",
"mailbox_rename_alias": "Automaticky vytvořit alias",
"mailbox_rename_title": "Nový název zdejší schránky",
"pushover": "Pushover",
"internal": "Interní",
"internal_info": "Interní aliasy jsou přístupné jen z vlastních domén nebo jejich aliasů.",
"mta_sts": "MTA-STS",
"mta_sts_info": "<a href='https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security' target='_blank'>MTA-STS</a> je standard, jenž říká poštovním serverům, aby komunikovaly pomocí TLS s platnými certifikáty. <br>Používá se, pokud není k dispozici <a target='_blank' href='https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities'>DANE</a>, např. chybí-li či není podporováno DNSSEC.<br><b>Pozn.</b>: Podporuje-li přijímající doména DANE a DNSSEC, bude <b>vždy</b> použito DANE; MTA-STS zůstane jako plán B.",
"mta_sts_version": "Verze",
"mta_sts_version_info": "Určuje verzi standardu MTA-STS zatím je podporována jen <code>STSv1</code>.",
"mta_sts_mode": "Mód",
"mta_sts_mode_info": "K dispozici jsou tři módy:<ul><li><em>testing</em> pravidlo se jen sleduje, porušení je bez následků.</li><li><em>enforce</em> pravidlo je důsledně dodržováno, spojení bez platného TLS jsou odmítána.</li><li><em>none</em> pravidlo je zveřejněno, ale neuplatňuje se.</li></ul>",
"mta_sts_max_age": "Maximální životnost",
"mta_sts_max_age_info": "Doba v sekundách, po niž poštovní servery mohou toho pravidlo držet v mezipaměti bez nutnosti obnovení.",
"mta_sts_mx": "Server MX",
"mta_sts_mx_info": "Dovoluje odesílání jen výslovně vypsaným poštovním serverům; odesílající server kontroluje, že server MX určený v DNS odpovídá pravidlu, a povolí doručení jen s platným certifikátem TLS (chrání přes útokem typu MITM).",
"mta_sts_mx_notice": "Lze zadat více serverů MX (oddělte čárkou)."
"password_recovery_email": "E-mail pro obnovu hesla"
},
"fido2": {
"confirm": "Potvrdit",
@@ -801,7 +711,7 @@
"cancel": "Zrušit",
"confirm_delete": "Potvdit smazání",
"delete_now": "Smazat",
"delete_these_items": "Prosím potvrďte změny objektu id",
"delete_these_items": "Prosím potvrďte změny objektu id:",
"hibp_check": "Ověřit heslo v databázi hacknutých hesel haveibeenpwned.com",
"hibp_nok": "Nalezeno! Toto je potenciálně nebezpečné heslo!",
"hibp_ok": "Nebyla nalezena žádná shoda.",
@@ -810,12 +720,12 @@
"restart_container": "Restartovat kontejner",
"restart_container_info": "<b>Důležité:</b> Šetrný restart může chvíli trvat, prosím čekejte...",
"restart_now": "Restartovat nyní",
"restarting_container": "Restartuje se kontejner, může to chvilku trvat"
"restarting_container": "Restartuje se kontejner, může to chvilku trvat..."
},
"header": {
"administration": "Hlavní nastavení",
"apps": "Aplikace",
"debug": "Informace",
"debug": "Systémové informace",
"email": "E-Mail",
"mailcow_system": "Systém",
"mailcow_config": "Nastavení",
@@ -843,15 +753,7 @@
"new_password": "Nové heslo",
"new_password_confirm": "Ověření nového hesla",
"reset_password": "Obnovit heslo",
"request_reset_password": "Požádat o změnu hesla",
"login_domainadmintext": "Přihlášení správce domény",
"login_linkstext": "Hledáte jinou přihlašovací stránku?",
"login_usertext": "Přihlášení uživatele",
"login_admintext": "Přihlášení správce",
"login_user": "Přihlášení uživatele",
"login_dadmin": "Přihlášení správce domény",
"login_admin": "Přihlášení správce",
"email": "Mailová adresa"
"request_reset_password": "Požádat o změnu hesla"
},
"mailbox": {
"action": "Akce",
@@ -883,7 +785,7 @@
"bcc": "BCC",
"bcc_destination": "Cíl kopie",
"bcc_destinations": "Cíl kopií",
"bcc_info": "<br/>Skrytá kopie (mapa BCC) se používá pro tiché předávání kopií všech zpráv na jinou adresu. Mapa příjemců se použije, funguje-li je místní cíl jako adresát zprávy. Totéž platí pro mapy odesílatelů.<br/>\n Místní cíl se nedozví, selže-li doručení na cíl BCC.",
"bcc_info": "Skryté kopie (Mapa BCC) se používá pro tiché předávání kopií všech zpráv na jinou adresu. Při použití skryté kopie typu <i>Přijatý e-mail</i> budou přeposlány všechny maily směřující na dotyčnou adresu nebo doménu.\nU typu <i>Odeslaný e-mail</i> budou přeposlány všechny maily odeslané z dotyčné adresy nebo domény.\nPokud selže přeposlání na cílovou adresu, tak odesílatel o tom nebude informován.",
"bcc_local_dest": "Týká se",
"bcc_map": "Skrytá kopie",
"bcc_map_type": "Typ skryté kopie",
@@ -938,7 +840,7 @@
"last_run_reset": "Znovu naplánovat",
"mailbox": "Mailová schránka",
"mailbox_defaults": "Výchozí nastavení",
"mailbox_defaults_info": "Definuje výchozí nastavení pro nové schránky.",
"mailbox_defaults_info": "Definuje výchozí nastavení pro nové schránky",
"mailbox_defquota": "Výchozí velikost schránky",
"mailbox_templates": "Šablony schránek",
"mailbox_quota": "Max. velikost schránky",
@@ -958,7 +860,7 @@
"private_comment": "Soukromý komentář",
"public_comment": "Veřejný komentář",
"q_add_header": "Složka nevyžádaná pošta",
"q_all": " Nevyžádaná pošta a Odmítnuta",
"q_all": "Všechny kategorie",
"q_reject": "Odmítnuta",
"quarantine_category": "Kategorie oznámení karantény",
"quarantine_notification": "Upozornění z karantény",
@@ -967,7 +869,7 @@
"recipient_map": "Mapa příjemce",
"recipient_map_info": "Mapy příjemců slouží k nahrazení cílové adresy zprávy před doručením.",
"recipient_map_new": "Nový přijemce",
"recipient_map_new_info": "Cílovou adresou mapy příjemců musí být emailová adresa nebo název domény.",
"recipient_map_new_info": "Cílová adresa mapy příjemce musí být emailová adresa nebo název domény.",
"recipient_map_old": "Původní příjemce",
"recipient_map_old_info": "Původní příjemce musí být platná emailová adresa nebo název domény.",
"recipient_maps": "Mapy příjemců",
@@ -986,7 +888,7 @@
"sieve_preset_5": "Automatický odpovídač (dovolená)",
"sieve_preset_6": "Odmítnout zprávu s odpovědí",
"sieve_preset_7": "Přesměrovat a ponechat/zahodit",
"sieve_preset_8": "Zprávu od určitého odesílatele přesměrovat, označit jako přečtenou a uložit do složky",
"sieve_preset_8": "Zahodit zprávu poslanou na alias, do něhož patří i odesílatel",
"sieve_preset_header": "Vizte následující ukázková pravidla. Více informací na <a href=\"https://en.wikipedia.org/wiki/Sieve_(mail_filtering_language)\" target=\"_blank\">Wikipedii</a>.",
"sogo_visible": "Alias dostupný v SOGo",
"sogo_visible_n": "Skrýt alias v SOGo",
@@ -1026,9 +928,7 @@
"waiting": "Čekání",
"weekly": "Každý týden",
"yes": "&#10003;",
"relay_unknown": "Předávání neexistujících schránek",
"iam": "Poskytovatel identity",
"internal": "Interní"
"relay_unknown": "Předávání neexistujících schránek"
},
"oauth2": {
"access_denied": "K udělení přístupu se přihlašte jako vlastník mailové schránky.",
@@ -1036,7 +936,7 @@
"deny": "Zamítnout",
"permit": "Ověřit aplikaci",
"profile": "Profil",
"profile_desc": "Zobrazit osobní údaje: uživ. jméno, celé jméno, datum vytvoření a úpravy, stav",
"profile_desc": "Zobrazit osobní údaje: uživ. jméno, jméno, datum vytvoření a úpravy, stav",
"scope_ask_permission": "Aplikace požádala o následující oprávnění"
},
"quarantine": {
@@ -1105,8 +1005,7 @@
"hold_mail_legend": "Podrží vybrané e-maily. (Zabrání dalším pokusům o doručení)",
"show_message": "Zobrazit zprávu",
"unhold_mail": "Uvolnit",
"unhold_mail_legend": "Uvolnit vybrané e-maily k doručení. (Pouze v případě předchozího podržení)",
"unban": "odblokovat"
"unhold_mail_legend": "Uvolnit vybrané e-maily k doručení. (Pouze v případě předchozího podržení)"
},
"ratelimit": {
"disabled": "Vypnuto",
@@ -1167,7 +1066,7 @@
"logged_in_as": "Přihlášen jako %s",
"mailbox_added": "Mailová schránka %s přidána",
"mailbox_modified": "Změny mailové schránky %s uloženy",
"mailbox_removed": "Mailová schránka %s odstraněna",
"mailbox_removed": "Mailová schránka %s odebrána",
"nginx_reloaded": "Nginx reload byl úspěšný",
"object_modified": "Změny objektu %s uloženy",
"password_policy_saved": "Politika hesel byla úspěšně uložena",
@@ -1201,14 +1100,7 @@
"verified_yotp_login": "Yubico OTP přihlášení ověřeno",
"cors_headers_edited": "Nastavení CORS byla uložena",
"domain_footer_modified": "Změny patičky domény %s byly uloženy",
"recovery_email_sent": "E-mail k obnovení byl odeslán na adresu %s",
"custom_login_modified": "Úpravy přihlašování úspěšně uloženy",
"domain_add_dkim_available": "Klíč DKIM už existoval",
"f2b_banlist_refreshed": "Seznam zákazů úspěšně obnoven.",
"iam_test_connection": "Spojení úspěšně navázano",
"ip_check_opt_in_modified": "Kontrola IP adresy úspěšně uložena",
"mailbox_renamed": "Schránka přejmenována z %s na %s",
"password_changed_success": "Heslo úspěšně změněno"
"recovery_email_sent": "E-mail k obnovení byl odeslán na adresu %s"
},
"tfa": {
"api_register": "%s používá Yubico Cloud API. Prosím získejte API klíč pro své Yubico <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">ZDE</a>",
@@ -1224,7 +1116,7 @@
"none": "Deaktivovat",
"reload_retry": "- (znovu načtěte stránku, opakuje-li se chyba)",
"scan_qr_code": "Prosím načtěte následující kód svou aplikací na ověření nebo zadejte kód ručně.",
"select": "Vyberte prosím",
"select": "Prosím vyberte...",
"set_tfa": "Nastavení způsobu dvoufaktorového ověření",
"start_webauthn_validation": "Zahájit inicializaci",
"tfa": "Dvoufaktorové ověření (TFA)",
@@ -1233,10 +1125,7 @@
"webauthn": "WebAuthn ověření",
"waiting_usb_auth": "<i>Čeká se na USB zařízení...</i><br><br>Prosím stiskněte tlačítko na svém WebAuthn USB zařízení.",
"waiting_usb_register": "<i>Čeká se na USB zařízení...</i><br><br>Prosím zadejte své heslo výše a potvrďte WebAuthn registraci stiskem tlačítka na svém WebAuthn USB zařízení.",
"yubi_otp": "Yubico OTP ověření",
"u2f_deprecated": "Zdá se, že váš klíč byl registrován zastaralou metodou U2F. Dojde k deaktivaci dvoufaktorové autentifikace a smazání klíče.",
"authenticators": "Autentifikátory",
"u2f_deprecated_important": "Registrujte svůj klíč novou metodou WebAuthn ve správě správců."
"yubi_otp": "Yubico OTP ověření"
},
"user": {
"action": "Akce",
@@ -1252,7 +1141,7 @@
"alias_time_left": "Zbývající čas",
"alias_valid_until": "Platný do",
"aliases_also_send_as": "Smí odesílat také jako uživatel",
"aliases_send_as_all": "Nekontrolovat přístup odesílatele pro následující doménu(y) a jejich aliasy",
"aliases_send_as_all": "Nekontrolovat přístup odesílatele pro následující doménu(y) a jejich aliasy domény:",
"allowed_protocols": "Povolené protokoly",
"app_hint": "Hesla aplikací jsou alternativní heslo pro přihlášení k IMAP, SMTP, CalDAV, CardDAV a EAS. Uživatelské jméno zůstává stejné.<br>SOGo však nelze s heslem aplikace použít.",
"app_name": "Název aplikace",
@@ -1273,8 +1162,8 @@
"description": "Popis",
"delete_ays": "Potvrďte odstranění.",
"direct_aliases": "Přímé aliasy",
"direct_aliases_desc": "Na přímé aliasy se uplatňuje filtr spamu a nastavení pravidel TLS.",
"direct_protocol_access": "Tento uživatel mailové schránky má <b>přímý externí přístup</b> k následujícím protokolům a aplikacím. Toto nastavení je řízeno správcem. Pro udělení přístupu k jednotlivým protokolům a aplikacím lze vytvořit hesla aplikací.<br>Tlačítko \"Webmail\" zajišťuje jednotné přihlášení k SOGo a je vždy k dispozici.",
"direct_aliases_desc": "Na přímé aliasy se uplatňuje filtr spamu a nastavení pravidel TLS",
"direct_protocol_access": "Tento uživatel mailové schránky má <b>přímý externí přístup</b> k následujícím protokolům a aplikacím. Toto nastavení je řízeno správcem. Pro udělení přístupu k jednotlivým protokolům a aplikacím lze vytvořit hesla aplikací.<br>Tlačítko \"Webmailu\" zajišťuje jednotné přihlášení k SOGo a je vždy k dispozici.",
"eas_reset": "Smazat mezipaměť zařízení ActiveSync",
"eas_reset_help": "Obnovení mezipaměti zařízení pomůže zpravidla obnovit poškozený profil služby ActiveSync.<br><b>Upozornění:</b> Všechna data budou opětovně stažena!",
"eas_reset_now": "Smazat",
@@ -1315,7 +1204,7 @@
"no_last_login": "Žádný záznam o přihlášení",
"no_record": "Žádný záznam",
"open_logs": "Otevřít záznam",
"open_webmail_sso": "Webmail",
"open_webmail_sso": "Webmailu",
"password": "Heslo",
"password_now": "Současné heslo (pro potvrzení změny)",
"password_repeat": "Heslo (znovu)",
@@ -1352,8 +1241,8 @@
"spamfilter": "Filtr spamu",
"spamfilter_behavior": "Hodnocení",
"spamfilter_bl": "Seznam zakázaných adres (blacklist)",
"spamfilter_bl_desc": "Zakázané emailové adresy budou <b>vždy</b> klasifikovány jako spam a odmítnuty. Odmítnutá pošta <b>nebude</b> uložena do karantény. Lze použít zástupné znaky (*). Filtr se použije pouze na přímé aliasy (s jednou cílovou poštovní schránkou), s výjimkou doménových košů a samotné poštovní schránky.",
"spamfilter_default_score": "Výchozí hodnoty",
"spamfilter_bl_desc": "Zakázané emailové adresy <b>budou vždy klasifikovány jako spam a odmítnuty</b>. Lze použít zástupné znaky (*). Filtr se použije pouze na přímé aliasy (s jednou cílovou mailovou schránkou), s výjimkou doménových košů a samotné mailové schránky.",
"spamfilter_default_score": "Výchozí hodnoty:",
"spamfilter_green": "Zelená: tato zpráva není spam",
"spamfilter_hint": "První hodnota představuje \"nízké spam skóre\" a druhá \"vysoké spam skóre\".",
"spamfilter_red": "Červená: Tato zpráva je spam a server ji odmítne",
@@ -1385,7 +1274,7 @@
"tag_in_subject": "V předmětu",
"text": "Text",
"title": "Předmět",
"tls_enforce_in": "Vynutit TLS pro příchozí poštu",
"tls_enforce_in": "Vynutit TLS pro příchozí poštu ",
"tls_enforce_out": "Vynutit TLS pro odchozí poštu",
"tls_policy": "Politika šifrování",
"tls_policy_warning": "<strong>Varování:</strong> Pokud se rozhodnete vynutit šifrovaný přenos pošty, může dojít ke ztrátě e-mailů.<br>Zprávy, které nesplňují tuto politiku, budou mailovým systémem odmítnuty.<br>Tato volba ovlivňuje primární e-mailovou adresu (přihlašovací jméno), všechny adresy odvozené z doménových aliasů i aliasy, jež mají tuto mailovou schránku jako cíl.",
@@ -1401,13 +1290,7 @@
"years": "let",
"pushover_sound": "Zvukové upozornění",
"password_reset_info": "Pokud není zadán e-mail pro obnovení hesla, nelze tuto funkci použít.",
"pw_recovery_email": "E-mail pro obnovení hesla",
"tfa_info": "Dvoufaktorová autentizace vám pomáhá chránit svůj účet. Je-li zapnuta, musíte si vytvořit aplikační hesla pro aplikace, jež dvoufaktorovou autentizaci nepodporují (např. poštovní klienti).",
"attribute": "Atribut",
"authentication": "Autentifikace",
"overview": "Přehled",
"protocols": "Protokoly",
"value": "Hodnota"
"pw_recovery_email": "E-mail pro obnovení hesla"
},
"warning": {
"cannot_delete_self": "Nelze smazat právě přihlášeného uživatele",

View File

@@ -25,7 +25,7 @@
"sogo_access": "Verwalten des SOGo-Zugriffsrechts erlauben",
"sogo_profile_reset": "SOGo-Profil zurücksetzen",
"spam_alias": "Temporäre E-Mail-Aliasse",
"spam_policy": "Deny/Allowlist",
"spam_policy": "Blacklist/Whitelist",
"spam_score": "Spam-Bewertung",
"syncjobs": "Sync Jobs",
"tls_policy": "Verschlüsselungsrichtlinie",
@@ -71,8 +71,6 @@
"goto_spam": "Nachrichten als <span class=\"text-danger\"><b>Spam</b></span> lernen",
"hostname": "Host",
"inactive": "Inaktiv",
"internal": "Intern",
"internal_info": "Interne Aliasse sind nur von der eigenen Domäne oder Alias-Domänen erreichbar.",
"kind": "Art",
"mailbox_quota_def": "Standard-Quota einer Mailbox",
"mailbox_quota_m": "Max. Speicherplatz pro Mailbox (MiB)",
@@ -136,7 +134,6 @@
"admin_domains": "Domain-Zuweisungen",
"admins": "Administratoren",
"admins_ldap": "LDAP-Administratoren",
"admin_quicklink": "Quicklink zur Admin-Loginseite ausblenden",
"advanced_settings": "Erweiterte Einstellungen",
"api_allow_from": "IP-Adressen oder Netzwerke (CIDR Notation) für Zugriff auf API",
"api_info": "Die API befindet sich noch in Entwicklung, die Dokumentation kann unter <a href=\"/api\">/api</a> abgerufen werden.",
@@ -149,7 +146,7 @@
"arrival_time": "Ankunftszeit (Serverzeit)",
"authed_user": "Auth. Benutzer",
"ays": "Soll der Vorgang wirklich ausgeführt werden?",
"ban_list_info": "Übersicht ausgesperrter Netzwerke: <b>Netzwerk (verbleibende Bannzeit) - [Aktionen]</b>.<br />IPs, die zum Entsperren eingereiht werden, verlassen die Liste aktiver Banns nach wenigen Sekunden.<br />Rote Labels sind Indikatoren für aktive Allowlist-Einträge.",
"ban_list_info": "Übersicht ausgesperrter Netzwerke: <b>Netzwerk (verbleibende Bannzeit) - [Aktionen]</b>.<br />IPs, die zum Entsperren eingereiht werden, verlassen die Liste aktiver Banns nach wenigen Sekunden.<br />Rote Labels sind Indikatoren für aktive Blacklist-Einträge.",
"change_logo": "Logo ändern",
"configuration": "Konfiguration",
"convert_html_to_text": "Konvertiere HTML zu reinem Text",
@@ -158,7 +155,6 @@
"credentials_transport_warning": "<b>Warnung</b>: Das Hinzufügen einer neuen Regel bewirkt die Aktualisierung der Authentifizierungsdaten aller vorhandenen Einträge mit identischem Next Hop.",
"customer_id": "Kunde",
"customize": "UI-Anpassung",
"login_page": "Login-Seite",
"destination": "Ziel",
"dkim_add_key": "ARC/DKIM-Key hinzufügen",
"dkim_domains_selector": "Selector",
@@ -177,7 +173,6 @@
"domain": "Domain",
"domain_admin": "Administrator hinzufügen",
"domain_admins": "Domain-Administratoren",
"domainadmin_quicklink": "Quicklink zur Domainadmin-Loginseite ausblenden",
"domain_s": "Domain(s)",
"duplicate": "Duplizieren",
"duplicate_dkim": "DKIM duplizieren",
@@ -186,9 +181,9 @@
"excludes": "Diese Empfänger ausschließen",
"f2b_ban_time": "Bannzeit in Sekunden",
"f2b_ban_time_increment": "Bannzeit erhöht sich mit jedem Bann",
"f2b_blacklist": "Denyliste für Netzwerke und Hosts",
"f2b_blacklist": "Blacklist für Netzwerke und Hosts",
"f2b_filter": "Regex-Filter",
"f2b_list_info": "Ein Host oder Netzwerk auf der Denyliste wird immer eine Allowlist-Einheit überwiegen. <b>Die Aktualisierung der Liste dauert einige Sekunden.</b>",
"f2b_list_info": "Ein Host oder Netzwerk auf der Blacklist wird immer eine Whitelist-Einheit überwiegen. <b>Die Aktualisierung der Liste dauert einige Sekunden.</b>",
"f2b_manage_external": "Fail2Ban extern verwalten",
"f2b_manage_external_info": "Fail2ban wird die Banlist weiterhin pflegen, jedoch werden keine aktiven Regeln zum blockieren gesetzt. Die unten generierte Banlist, kann verwendet werden, um den Datenverkehr extern zu blockieren.",
"f2b_max_attempts": "Max. Versuche",
@@ -198,10 +193,8 @@
"f2b_parameters": "Fail2ban-Parameter",
"f2b_regex_info": "Berücksichtigte Logs: SOGo, Postfix, Dovecot, PHP-FPM.",
"f2b_retry_window": "Wiederholungen im Zeitraum von (s)",
"f2b_whitelist": "Allowliste für Netzwerke und Hosts",
"f2b_whitelist": "Whitelist für Netzwerke und Hosts",
"filter_table": "Tabelle filtern",
"force_sso_text": "Wenn ein externer OIDC-Provider konfiguriert ist, blendet diese Option die mailcow-Loginform aus und zeigt nur den Single-Sign-On-Button an.",
"force_sso": "mailcow-Login deaktivieren und nur Single Sign-On anzeigen",
"forwarding_hosts": "Weiterleitungs-Hosts",
"forwarding_hosts_add_hint": "Sie können entweder IPv4-/IPv6-Adressen, Netzwerke in CIDR-Notation, Hostnamen (die zu IP-Adressen aufgelöst werden), oder Domainnamen (die zu IP-Adressen aufgelöst werden, indem ihr SPF-Record abgefragt wird oder, in dessen Abwesenheit, ihre MX-Records) angeben.",
"forwarding_hosts_hint": "Eingehende Nachrichten werden von den hier gelisteten Hosts bedingungslos akzeptiert. Diese Hosts werden dann nicht mit DNSBLs abgeglichen oder Greylisting unterworfen. Von ihnen empfangener Spam wird nie abgelehnt, optional kann er aber in den Spam-Ordner einsortiert werden. Die übliche Verwendung für diese Funktion ist, um Mailserver anzugeben, auf denen eine Weiterleitung zu Ihrem mailcow-Server eingerichtet wurde.",
@@ -229,7 +222,6 @@
"iam_host": "Host",
"iam_host_info": "Gib einen oder mehrere LDAP-Hosts ein, getrennt durch Kommas.",
"iam_import_users": "Importiere Benutzer",
"iam_login_provisioning": "Benutzer beim Login erstellen",
"iam_mapping": "Attribut Mapping",
"iam_bindpass": "Bind Passwort",
"iam_periodic_full_sync": "Vollsynchronisation",
@@ -246,9 +238,7 @@
"iam_username_field": "Username Feld",
"iam_binddn": "Bind DN",
"iam_use_ssl": "Benutze SSL",
"iam_use_ssl_info": "Wenn SSL aktiviert ist und der Port auf 389 gesetzt wurde, wird dieser automatisch auf 636 geändert.",
"iam_use_tls": "Benutze StartTLS",
"iam_use_tls_info": "Wenn TLS aktiviert wird, muss der Standardport deines LDAP-Servers (389) verwendet werden. SSL-Ports können dabei nicht verwendet werden.",
"iam_use_tls": "Benutze TLS",
"iam_version": "Version",
"ignore_ssl_error": "Ignoriere SSL Fehler",
"import": "Importieren",
@@ -274,7 +264,6 @@
"message": "Nachricht",
"message_size": "Nachrichtengröße",
"nexthop": "Next Hop",
"needs_restart": "benötigt Neustart",
"no": "&#10005;",
"no_active_bans": "Keine aktiven Banns",
"no_new_rows": "Keine weiteren Zeilen vorhanden",
@@ -317,7 +306,6 @@
"quarantine_release_format_att": "Als Anhang",
"quarantine_release_format_raw": "Unverändertes Original",
"quarantine_retention_size": "Rückhaltungen pro Mailbox:<br><small>0 bedeutet <b>inaktiv</b>.</small>",
"quicklink_text": "Quicklinks zu anderen Login-Seiten unter der Loginform ein- oder ausblenden",
"quota_notification_html": "Benachrichtigungs-E-Mail Inhalt:<br><small>Leer lassen, um Standard-Template wiederherzustellen.</small>",
"quota_notification_sender": "Benachrichtigungs-E-Mail Absender",
"quota_notification_subject": "Benachrichtigungs-E-Mail Betreff",
@@ -357,8 +345,8 @@
"rspamd_com_settings": "Ein Name wird automatisch generiert. Beispielinhalte zur Einsicht stehen nachstehend bereit. Siehe auch <a href=\"https://rspamd.com/doc/configuration/settings.html#settings-structure\" target=\"_blank\">Rspamd docs</a>",
"rspamd_global_filters": "Globale Filter-Maps",
"rspamd_global_filters_agree": "Ich werde vorsichtig sein!",
"rspamd_global_filters_info": "Globale Filter-Maps steuern globales Allow- und Denylisting dieses Servers.",
"rspamd_global_filters_regex": "Die akzeptierte Form für Einträge sind <b>ausschließlich</b> Regular Expressions.\r\n Trotz rudimentärer Überprüfung der Map, kann es zu fehlerhaften Einträgen kommen, die Rspamd im schlechtesten Fall mit unvorhersehbarer Funktionalität bestraft.<br>\r\n Das korrekte Format lautet \"/pattern/options\" (Beispiel: <code>/.+@domain\\.tld/i</code>).<br>\r\n Der Name der Map beschreibt die jeweilige Funktion.<br>\r\n Rspamd versucht die Maps umgehend aufzulösen. Bei Problemen sollte <a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">Rspamd manuell neugestartet werden</a>.<br>Elemente auf Denylisten sind von der Quarantäne ausgeschlossen.",
"rspamd_global_filters_info": "Globale Filter-Maps steuern globales White- und Blacklisting dieses Servers.",
"rspamd_global_filters_regex": "Die akzeptierte Form für Einträge sind <b>ausschließlich</b> Regular Expressions.\r\n Trotz rudimentärer Überprüfung der Map, kann es zu fehlerhaften Einträgen kommen, die Rspamd im schlechtesten Fall mit unvorhersehbarer Funktionalität bestraft.<br>\r\n Das korrekte Format lautet \"/pattern/options\" (Beispiel: <code>/.+@domain\\.tld/i</code>).<br>\r\n Der Name der Map beschreibt die jeweilige Funktion.<br>\r\n Rspamd versucht die Maps umgehend aufzulösen. Bei Problemen sollte <a href=\"\" data-toggle=\"modal\" data-container=\"rspamd-mailcow\" data-target=\"#RestartContainer\">Rspamd manuell neugestartet werden</a>.<br>Elemente auf Blacklists sind von der Quarantäne ausgeschlossen.",
"rspamd_settings_map": "Rspamd-Settings-Map",
"sal_level": "Moo-Level",
"save": "Änderungen speichern",
@@ -397,7 +385,6 @@
"unchanged_if_empty": "Unverändert, wenn leer",
"upload": "Hochladen",
"username": "Benutzername",
"user_quicklink": "Quicklink zur Benutzer-Loginseite ausblenden",
"validate_license_now": "GUID erneut verifizieren",
"verify": "Verifizieren",
"yes": "&#10003;",
@@ -409,9 +396,7 @@
"allowed_methods": "Access-Control-Allow-Methods",
"allowed_origins": "Access-Control-Allow-Origin",
"logo_dark_label": "Invertiert für den Darkmode",
"logo_normal_label": "Normal",
"user_link": "Nutzer-Link",
"filter": "Filter"
"logo_normal_label": "Normal"
},
"danger": {
"access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten",
@@ -485,13 +470,10 @@
"mailboxes_in_use": "Maximale Anzahl an Mailboxen muss größer oder gleich %d sein",
"malformed_username": "Benutzername hat ein falsches Format",
"map_content_empty": "Inhalt darf nicht leer sein",
"max_age_invalid": "Maximales Alter %s ist ungültig",
"max_alias_exceeded": "Anzahl an Alias-Adressen überschritten",
"max_mailbox_exceeded": "Anzahl an Mailboxen überschritten (%d von %d)",
"max_quota_in_use": "Mailbox-Speicherplatzlimit muss größer oder gleich %d MiB sein",
"maxquota_empty": "Max. Speicherplatz pro Mailbox darf nicht 0 sein.",
"mode_invalid": "Modus %s ist ungültig",
"mx_invalid": "MX-Eintrag %s ist ungültig",
"mysql_error": "MySQL-Fehler: %s",
"network_host_invalid": "Netzwerk oder Host ungültig: %s",
"next_hop_interferes": "%s verhindert das Hinzufügen von Next Hop %s",
@@ -551,12 +533,10 @@
"username_invalid": "Benutzername %s kann nicht verwendet werden",
"validity_missing": "Bitte geben Sie eine Gültigkeitsdauer an",
"value_missing": "Bitte alle Felder ausfüllen",
"version_invalid": "Version %s ist ungültig",
"yotp_verification_failed": "Yubico OTP-Verifizierung fehlgeschlagen: %s",
"template_exists": "Vorlage %s existiert bereits",
"template_id_invalid": "Vorlagen-ID %s ungültig",
"template_name_invalid": "Name der Vorlage ungültig",
"required_data_missing": "Die benötigten Daten: %s fehlen"
"template_name_invalid": "Name der Vorlage ungültig"
},
"datatables": {
"collapse_all": "Alle Einklappen",
@@ -692,8 +672,6 @@
"grant_types": "Grant-types",
"hostname": "Servername",
"inactive": "Inaktiv",
"internal": "Intern",
"internal_info": "Interne Aliasse sind nur von der eigenen Domäne oder Alias-Domänen erreichbar.",
"kind": "Art",
"last_modified": "Zuletzt geändert",
"lookup_mx": "Ziel mit MX vergleichen (Regex, etwa <code>.*\\.google\\.com</code>, um alle Ziele mit MX *google.com zu routen)",
@@ -712,17 +690,6 @@
"maxbytespersecond": "Max. Übertragungsrate in Bytes/s (0 für unlimitiert)",
"mbox_rl_info": "Dieses Limit wird auf den SASL Loginnamen angewendet und betrifft daher alle Absenderadressen, die der eingeloggte Benutzer verwendet. Bei Mailbox Ratelimit überwiegt ein Domain-weites Ratelimit.",
"mins_interval": "Intervall (min)",
"mta_sts": "MTA-STS",
"mta_sts_info": "<a href='https://de.wikipedia.org/wiki/STARTTLS#MTA-STS' target='_blank'>MTA-STS</a> ist ein Standard, der den E-Mail-Versand zwischen Mailservern zwingt, TLS mit gültigen Zertifikaten zu verwenden. <br>Er wird verwendet, wenn <a target='_blank' href='https://de.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities'>DANE</a> aufgrund fehlender oder nicht unterstützter DNSSEC nicht möglich ist.<br><b>Hinweis</b>: Wenn die empfangende Domain DANE mit DNSSEC unterstützt, wird DANE <b>immer</b> bevorzugt MTA-STS fungiert nur als Fallback.",
"mta_sts_version": "Version",
"mta_sts_version_info": "Definiert die Version des MTA-STS-Standards derzeit ist nur <code>STSv1</code> gültig.",
"mta_sts_mode": "Modus",
"mta_sts_mode_info": "Es gibt drei Modi zur Auswahl:<ul><li><em>testing</em> Die Richtlinie wird nur überwacht, Verstöße haben keine Auswirkungen.</li><li><em>enforce</em> Die Richtlinie wird strikt durchgesetzt, Verbindungen ohne gültiges TLS werden abgelehnt.</li><li><em>none</em> Die Richtlinie wird veröffentlicht, aber nicht angewendet.</li></ul>",
"mta_sts_max_age": "Maximales Alter",
"mta_sts_max_age_info": "Zeit in Sekunden, die empfangende Mailserver diese Richtlinie zwischenspeichern dürfen, bevor sie erneut abgerufen wird.",
"mta_sts_mx": "MX-Server",
"mta_sts_mx_info": "Erlaubt das Senden nur an explizit aufgeführte Mailserver-Hostnamen; der sendende MTA überprüft, ob der DNS-MX-Hostname mit der Richtlinienliste übereinstimmt, und erlaubt die Zustellung nur mit einem gültigen TLS-Zertifikat (schützt vor MITM).",
"mta_sts_mx_notice": "Es können mehrere MX-Server angegeben werden (durch Kommas getrennt).",
"multiple_bookings": "Mehrfaches Buchen",
"nexthop": "Next Hop",
"none_inherit": "Keine Auswahl / Erben",
@@ -769,7 +736,7 @@
"sogo_visible_info": "Diese Option hat lediglich Einfluss auf Objekte, die in SOGo darstellbar sind (geteilte oder nicht-geteilte Alias-Adressen mit dem Ziel mindestens einer lokalen Mailbox).",
"spam_alias": "Anpassen temporärer Alias-Adressen",
"spam_filter": "Spamfilter",
"spam_policy": "Hinzufügen und Entfernen von Einträgen in Allow- und Denylisten",
"spam_policy": "Hinzufügen und Entfernen von Einträgen in White- und Blacklists",
"spam_score": "Einen benutzerdefiniterten Spam-Score festlegen",
"subfolder2": "Ziel-Ordner<br><small>(leer = kein Unterordner)</small>",
"syncjob": "Sync-Job bearbeiten",
@@ -837,13 +804,9 @@
"forgot_password": "> Passwort vergessen?",
"invalid_pass_reset_token": "Der Rücksetz-Token für das Passwort ist ungültig oder abgelaufen.<br>Bitte fordern Sie einen neuen Link zur Passwortwiederherstellung an.",
"login": "Anmelden",
"login_linkstext": "Nicht der richtige Login?",
"login_usertext": "Als Benutzer anmelden",
"login_domainadmintext": "Als Domainadmin anmelden",
"login_admintext": "Als Admin anmelden",
"login_user": "Anmeldung als Benutzer",
"login_dadmin": "Anmeldung als Domain-Administrator",
"login_admin": "Anmeldung als Administrator",
"login_user": "Benutzer Anmelden",
"login_dadmin": "Domain-Administrator Anmelden",
"login_admin": "Administrator Anmelden",
"mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.",
"new_password": "Neues Passwort",
"new_password_confirm": "Neues Passwort bestätigen",
@@ -851,8 +814,7 @@
"password": "Passwort",
"reset_password": "Passwort zurücksetzen",
"request_reset_password": "Passwortänderung anfordern",
"username": "Benutzername",
"email": "E-Mail-Adresse"
"username": "Benutzername"
},
"mailbox": {
"action": "Aktion",
@@ -873,7 +835,7 @@
"add_tls_policy_map": "TLS-Richtlinieneintrag hinzufügen",
"address_rewriting": "Adressumschreibung",
"alias": "Alias",
"alias_domain_alias_hint": "Alias-Adressen werden <b>nicht</b> automatisch auch auf Domain-Alias Adressen angewendet. Eine Alias-Adresse <code>mein-alias@domain</code> bildet demnach <b>nicht</b> die Adresse <code>mein-alias@alias-domain</code> ab.<br>E-Mail-Weiterleitungen an externe Postfächer sollten über Sieve (SOGo Weiterleitung oder im Reiter Filter) angelegt werden. Der Button Alias über Alias-Domains expandieren erstellt fehlende Alias-Adressen in Alias-Domains.",
"alias_domain_alias_hint": "Alias-Adressen werden <b>nicht</b> automatisch auch auf Domain-Alias Adressen angewendet. Eine Alias-Adresse <code>mein-alias@domain</code> bildet demnach <b>nicht</b> die Adresse <code>mein-alias@alias-domain</code> ab.<br>E-Mail-Weiterleitungen an externe Postfächer sollten über Sieve (SOGo Weiterleitung oder im Reiter \"Filter\") angelegt werden. Der Button \"Alias über Alias-Domains expandieren\" erstellt fehlende Alias-Adressen in Alias-Domains.",
"alias_domain_backupmx": "Alias-Domain für Relay-Domain inaktiv",
"aliases": "Aliasse",
"allow_from_smtp": "Nur folgende IPs für <b>SMTP</b> erlauben",
@@ -927,7 +889,6 @@
"in_use": "Prozentualer Gebrauch",
"inactive": "Inaktiv",
"insert_preset": "Beispiel \"%s\" laden",
"internal": "Intern",
"kind": "Art",
"last_mail_login": "Letzter Mail-Login",
"last_modified": "Zuletzt geändert",
@@ -965,7 +926,7 @@
"recipient_map_new": "Neuer Empfänger",
"recipient_map_new_info": "Der neue Empfänger muss eine E-Mail-Adresse oder ein Domainname sein.",
"recipient_map_old": "Original-Empfänger",
"recipient_map_old_info": "Der originäre Empfänger muss eine E-Mail-Adresse oder ein Domainname sein.",
"recipient_map_old_info": "Der originale Empfänger muss eine E-Mail-Adresse oder ein Domainname sein.",
"recipient_maps": "Empfängerumschreibungen",
"relay_all": "Alle Empfänger-Adressen relayen",
"relay_unknown": "Unbekannte Mailboxen relayen",
@@ -1028,8 +989,7 @@
"syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung",
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem",
"syncjob_EXIT_OVERQUOTA": "Ziel Mailbox ist über dem Limit",
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kann keine Verbindung zum Zielserver herstellen",
"iam": "Identitätsanbieter"
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kann keine Verbindung zum Zielserver herstellen"
},
"oauth2": {
"access_denied": "Bitte als Mailbox-Nutzer einloggen, um den Zugriff via OAuth2 zu erlauben.",
@@ -1061,7 +1021,7 @@
"notified": "Benachrichtigt",
"qhandler_success": "Aktion wurde an das System übergeben. Sie dürfen dieses Fenster nun schließen.",
"qid": "Rspamd QID",
"qinfo": "Das Quarantänesystem speichert abgelehnte Nachrichten in der Datenbank (dem Sender wird <em>nicht</em> signalisiert, dass seine E-Mail zugestellt wurde) als auch diese, die als Kopie in den Junk-Ordner der jeweiligen Mailbox zugestellt wurden.\r\n <br>\"Als Spam lernen und löschen\" lernt Nachrichten nach bayesscher Statistik als Spam und erstellt Fuzzy Hashes ausgehend von der jeweiligen Nachricht, um ähnliche Inhalte zukünftig zu unterbinden.\r\n <br>Der Prozess des Lernens kann abhängig vom System zeitintensiv sein.<br>Auf Denylisten vorkommende Elemente sind von der Quarantäne ausgeschlossen.",
"qinfo": "Das Quarantänesystem speichert abgelehnte Nachrichten in der Datenbank (dem Sender wird <em>nicht</em> signalisiert, dass seine E-Mail zugestellt wurde) als auch diese, die als Kopie in den Junk-Ordner der jeweiligen Mailbox zugestellt wurden.\r\n <br>\"Als Spam lernen und löschen\" lernt Nachrichten nach bayesscher Statistik als Spam und erstellt Fuzzy Hashes ausgehend von der jeweiligen Nachricht, um ähnliche Inhalte zukünftig zu unterbinden.\r\n <br>Der Prozess des Lernens kann abhängig vom System zeitintensiv sein.<br>Auf Blacklists vorkommende Elemente sind von der Quarantäne ausgeschlossen.",
"qitem": "Quarantäneeintrag",
"quarantine": "Quarantäne",
"quick_actions": "Aktionen",
@@ -1100,7 +1060,7 @@
"legend": "Funktionen der Mailqueue Aktionen:",
"ays": "Soll die derzeitige Queue wirklich komplett bereinigt werden?",
"deliver_mail": "Ausliefern",
"deliver_mail_legend": "Versucht eine erneute Zustellung der ausgewählten Mails.",
"deliver_mail_legend": "Versucht eine erneute Zustellung der ausgwählten Mails.",
"hold_mail": "Zurückhalten",
"hold_mail_legend": "Hält die ausgewählten Mails zurück. (Verhindert weitere Zustellversuche)",
"queue_manager": "Queue Manager",
@@ -1132,7 +1092,6 @@
"bcc_edited": "BCC-Map-Eintrag %s wurde geändert",
"bcc_saved": "BCC- Map-Eintrag wurde gespeichert",
"cors_headers_edited": "CORS Einstellungen wurden erfolgreich gespeichert",
"custom_login_modified": "Login Anpassung wurde erfolgreich gespeichert",
"db_init_complete": "Datenbankinitialisierung abgeschlossen",
"delete_filter": "Filter-ID %s wurde gelöscht",
"delete_filters": "Filter gelöscht: %s",
@@ -1270,7 +1229,7 @@
"delete_ays": "Soll der Löschvorgang wirklich ausgeführt werden?",
"direct_aliases": "Direkte Alias-Adressen",
"direct_aliases_desc": "Nur direkte Alias-Adressen werden für benutzerdefinierte Einstellungen berücksichtigt.",
"direct_protocol_access": "Der Hauptbenutzer hat <b>direkten, externen Zugriff</b> auf folgende Protokolle und Anwendungen. Diese Einstellung wird vom Administrator gesteuert. App-Passwörter können verwendet werden, um individuelle Zugänge für Protokolle und Anwendungen zu erstellen.<br>Der Button Webmail kann unabhängig der Einstellung immer verwendet werden.",
"direct_protocol_access": "Der Hauptbenutzer hat <b>direkten, externen Zugriff</b> auf folgende Protokolle und Anwendungen. Diese Einstellung wird vom Administrator gesteuert. App-Passwörter können verwendet werden, um individuelle Zugänge für Protokolle und Anwendungen zu erstellen.<br>Der Button \"Webmail\" kann unabhängig der Einstellung immer verwendet werden.",
"eas_reset": "ActiveSync-Geräte-Cache zurücksetzen",
"eas_reset_help": "In vielen Fällen kann ein ActiveSync-Profil durch das Zurücksetzen des Caches repariert werden.<br><b>Vorsicht:</b> Alle Elemente werden erneut heruntergeladen!",
"eas_reset_now": "Jetzt zurücksetzen",
@@ -1350,8 +1309,8 @@
"spam_score_reset": "Auf Server-Standard zurücksetzen",
"spamfilter": "Spamfilter",
"spamfilter_behavior": "Bewertung",
"spamfilter_bl": "Denyliste",
"spamfilter_bl_desc": "Für E-Mail-Adressen, die vom Spamfilter <b>immer</b> als Spam erfasst und abgelehnt werden. Die Quarantäne-Funktion ist für diese Nachrichten <b>deaktiviert</b>. Die Verwendung von Wildcards ist gestattet. Ein Filter funktioniert lediglich für direkte Nicht-Catch-All“-Alias-Adressen (Alias-Adressen mit lediglich einer Mailbox als Ziel-Adresse) sowie die Mailbox-Adresse selbst.",
"spamfilter_bl": "Blacklist",
"spamfilter_bl_desc": "Für E-Mail-Adressen, die vom Spamfilter <b>immer</b> als Spam erfasst und abgelehnt werden. Die Quarantäne-Funktion ist für diese Nachrichten deaktiviert. Die Verwendung von Wildcards ist gestattet. Ein Filter funktioniert lediglich für direkte nicht-\"Catch All\" Alias-Adressen (Alias-Adressen mit lediglich einer Mailbox als Ziel-Adresse) sowie die Mailbox-Adresse selbst.",
"spamfilter_default_score": "Standardwert",
"spamfilter_green": "Grün: Die Nachricht ist kein Spam",
"spamfilter_hint": "Der erste Wert beschreibt den \"low spam score\", der zweite Wert den \"high spam score\".",
@@ -1362,8 +1321,8 @@
"spamfilter_table_empty": "Keine Einträge vorhanden",
"spamfilter_table_remove": "Entfernen",
"spamfilter_table_rule": "Regel",
"spamfilter_wl": "Allowliste",
"spamfilter_wl_desc": "Für E-Mail-Adressen, die vom Spamfilter <b>nicht</b> erfasst werden sollen. Die Verwendung von Wildcards ist gestattet. Ein Filter funktioniert lediglich für direkte Nicht-Catch-All“-Alias-Adressen (Alias-Adressen mit lediglich einer Mailbox als Ziel-Adresse) sowie die Mailbox-Adresse selbst.",
"spamfilter_wl": "Whitelist",
"spamfilter_wl_desc": "Für E-Mail-Adressen, die vom Spamfilter <b>nicht</b> erfasst werden sollen. Die Verwendung von Wildcards ist gestattet. Ein Filter funktioniert lediglich für direkte nicht-\"Catch All\" Alias-Adressen (Alias-Adressen mit lediglich einer Mailbox als Ziel-Adresse) sowie die Mailbox-Adresse selbst.",
"spamfilter_yellow": "Gelb: Die Nachricht ist vielleicht Spam, wird als Spam markiert und in den Junk-Ordner verschoben",
"status": "Status",
"sync_jobs": "Sync Jobs",
@@ -1374,7 +1333,7 @@
"tag_in_subfolder": "In Unterordner",
"tag_in_subject": "In Betreff",
"text": "Text",
"tfa_info": "Zwei-Faktor-Authentifizierung hilft dabei, Ihr Konto zu schützen. Wenn Sie sie aktivieren, benötigen Sie App-Passwörter, um sich bei Apps oder Diensten anzumelden, die die Zwei-Faktor-Authentifizierung nicht unterstützen (z.B. Mailclients).",
"tfa_info": "Zwei-Faktor-Authentifizierung hilft dabei, Ihr Konto zu schützen. Wenn Sie sie aktivieren, benötigen Sie möglicherweise App-Passwörter, um sich bei Apps oder Diensten anzumelden, die die Zwei-Faktor-Authentifizierung nicht unterstützen (z.B. Mailclients).",
"title": "Title",
"tls_enforce_in": "TLS eingehend erzwingen",
"tls_enforce_out": "TLS ausgehend erzwingen",

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