1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2024-12-23 02:04:46 +02:00

Merge pull request #6012 from mailcow/staging

2024-08
This commit is contained in:
Niklas Meyer 2024-08-15 14:46:50 +02:00 committed by GitHub
commit 9a729d89bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 2085 additions and 329 deletions

View File

@ -1,13 +1,3 @@
## :memo: Brief description
<!-- Diff summary - START -->
<!-- Diff summary - END -->
## :computer: Commits
<!-- Diff commits - START -->
<!-- Diff commits - END -->
## :file_folder: Modified files ## :file_folder: Modified files
<!-- Diff files - START --> <!-- Diff files - START -->
<!-- Diff files - END --> <!-- Diff files - END -->

38
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,38 @@
<!-- _Please make sure to review and check all of these items, otherwise we might refuse your PR:_ -->
## Contribution Guidelines
* [ ] I've read the [contribution guidelines](https://github.com/mailcow/mailcow-dockerized/blob/master/CONTRIBUTING.md) and wholeheartedly agree them
<!-- _NOTE: this tickbox is needed to fullfil on order to get your PR reviewed._ -->
## What does this PR include?
### Short Description
<!-- Please write a short description, what your PR does here. -->
### Affected Containers
<!-- Please list all affected Docker containers here, which you commited changes to -->
<!--
Please list them like this:
- container1
- container2
- container3
etc.
-->
## Did you run tests?
### What did you tested?
<!-- Please write shortly, what you've tested (which components etc.). -->
### What were the final results? (Awaited, got)
<!-- Please write shortly, what your final tests results were. What did you awaited? Was the outcome the awaited one? -->

View File

@ -1,25 +1,42 @@
# Contribution Guidelines (Last modified on 27th June 2024) # Contribution Guidelines
**_Last modified on 15th August 2024_**
First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow! First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow!
## Pull Requests (Last modified on 27th June 2024) As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly.
**PLEASE NOTE, THAT WE MIGHT CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULLFIL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
## Topics
- [Pull Requests](#pull-requests)
- [Issue Reporting](#issue-reporting)
- [Guidelines](#issue-reporting-guidelines)
- [Issue Report Guide](#issue-report-guide)
## Pull Requests
**_Last modified on 15th August 2024_**
However, please note the following regarding pull requests: However, please note the following regarding pull requests:
1. **ALWAYS** create your PR using the staging branch of your locally cloned mailcow instance, as the pull request will end up in said staging branch of mailcow once approved. Ideally, you should simply create a new branch for your pull request that is named after the type of your PR (e.g. `feat/` for function updates or `fix/` for bug fixes) and the actual content (e.g. `sogo-6.0.0` for an update from SOGo to version 6 or `html-escape` for a fix that includes escaping HTML in mailcow). 1. **ALWAYS** create your PR using the staging branch of your locally cloned mailcow instance, as the pull request will end up in said staging branch of mailcow once approved. Ideally, you should simply create a new branch for your pull request that is named after the type of your PR (e.g. `feat/` for function updates or `fix/` for bug fixes) and the actual content (e.g. `sogo-6.0.0` for an update from SOGo to version 6 or `html-escape` for a fix that includes escaping HTML in mailcow).
2. **ALWAYS** report/request issues/features in the english language, even though mailcow is a german based company. This is done to allow other GitHub users to reply to your issues/requests too which did not speak german or other languages besides english. 2. **ALWAYS** report/request issues/features in the english language, even though mailcow is a german based company. This is done to allow other GitHub users to reply to your issues/requests too which did not speak german or other languages besides english.
3. Please **keep** this pull request branch **clean** and free of commits that have nothing to do with the changes you have made (e.g. commits from other users from other branches). *If you make changes to the `update.sh` script or other scripts that trigger a commit, there is usually a developer mode for clean working in this case. 3. Please **keep** this pull request branch **clean** and free of commits that have nothing to do with the changes you have made (e.g. commits from other users from other branches). *If you make changes to the `update.sh` script or other scripts that trigger a commit, there is usually a developer mode for clean working in this case.*
4. **Test your changes before you commit them as a pull request.** <ins>If possible</ins>, write a small **test log** or demonstrate the functionality with a **screenshot or GIF**. *We will of course also test your pull request ourselves, but proof from you will save us the question of whether you have tested your own changes yourself.* 4. **Test your changes before you commit them as a pull request.** <ins>If possible</ins>, write a small **test log** or demonstrate the functionality with a **screenshot or GIF**. *We will of course also test your pull request ourselves, but proof from you will save us the question of whether you have tested your own changes yourself.*
5. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.* 5. **Please use** the pull request template we provide once creating a pull request. *HINT: During editing you encounter comments which looks like: `<!-- CONTENT -->`. These can be removed or kept, as they will not rendered later on GitHub! Please only create actual content without the said comments.*
6. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project. 6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.*
7. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort! 7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project.
8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort!
--- ---
## Issue Reporting (Last modified on 27th June 2024) ## Issue Reporting
**_Last modified on 15th August 2024_**
If you plan to report a issue within mailcow please read and understand the following rules: If you plan to report a issue within mailcow please read and understand the following rules:
### Issue Reporting Guidelines
1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support). 1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support).
2. **ONLY** report an error if you have the **necessary know-how (at least the basics)** for the administration of an e-mail server and the usage of Docker. mailcow is a complex and fully-fledged e-mail server including groupware components on a Docker basement and it requires a bit of technical know-how for debugging and operating. 2. **ONLY** report an error if you have the **necessary know-how (at least the basics)** for the administration of an e-mail server and the usage of Docker. mailcow is a complex and fully-fledged e-mail server including groupware components on a Docker basement and it requires a bit of technical know-how for debugging and operating.
3. **ALWAYS** report/request issues/features in the english language, even though mailcow is a german based company. This is done to allow other GitHub users to reply to your issues/requests too which did not speak german or other languages besides english. 3. **ALWAYS** report/request issues/features in the english language, even though mailcow is a german based company. This is done to allow other GitHub users to reply to your issues/requests too which did not speak german or other languages besides english.
@ -29,7 +46,7 @@ If you plan to report a issue within mailcow please read and understand the foll
7. When you create a issue/feature request: Please note that the creation does <ins>**not guarantee an instant implementation or fix by the mailcow team or the community**</ins>. 7. When you create a issue/feature request: Please note that the creation does <ins>**not guarantee an instant implementation or fix by the mailcow team or the community**</ins>.
8. Please **ALWAYS** anonymize any sensitive information in your bug report or feature request before submitting it. 8. Please **ALWAYS** anonymize any sensitive information in your bug report or feature request before submitting it.
### Quick guide to reporting problems: ### Issue Report Guide
1. Read your logs; follow them to see what the reason for your problem is. 1. Read your logs; follow them to see what the reason for your problem is.
2. Follow the leads given to you in your logfiles and start investigating. 2. Follow the leads given to you in your logfiles and start investigating.
3. Restarting the troubled service or the whole stack to see if the problem persists. 3. Restarting the troubled service or the whole stack to see if the problem persists.

View File

@ -1,6 +1,6 @@
FROM alpine:3.20 FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
RUN apk upgrade --no-cache \ RUN apk upgrade --no-cache \

View File

@ -123,7 +123,7 @@ done
log_f "Database OK" log_f "Database OK"
log_f "Waiting for Nginx..." log_f "Waiting for Nginx..."
until $(curl --output /dev/null --silent --head --fail http://nginx:8081); do until $(curl --output /dev/null --silent --head --fail http://nginx.${COMPOSE_PROJECT_NAME}_mailcow-network:8081); do
sleep 2 sleep 2
done done
log_f "Nginx OK" log_f "Nginx OK"
@ -137,7 +137,7 @@ log_f "Resolver OK"
# Waiting for domain table # Waiting for domain table
log_f "Waiting for domain table..." log_f "Waiting for domain table..."
while [[ -z ${DOMAIN_TABLE} ]]; do while [[ -z ${DOMAIN_TABLE} ]]; do
curl --silent http://nginx/ >/dev/null 2>&1 curl --silent http://nginx.${COMPOSE_PROJECT_NAME}_mailcow-network/ >/dev/null 2>&1
DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs) DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs)
[[ -z ${DOMAIN_TABLE} ]] && sleep 10 [[ -z ${DOMAIN_TABLE} ]] && sleep 10
done done

View File

@ -2,32 +2,32 @@
# Reading container IDs # Reading container IDs
# Wrapping as array to ensure trimmed content when calling $NGINX etc. # Wrapping as array to ensure trimmed content when calling $NGINX etc.
NGINX=($(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"nginx-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) NGINX=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"nginx-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
DOVECOT=($(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"dovecot-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) DOVECOT=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"dovecot-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
POSTFIX=($(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) POSTFIX=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
reload_nginx(){ reload_nginx(){
echo "Reloading Nginx..." echo "Reloading Nginx..."
NGINX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi/containers/${NGINX}/exec -d '{"cmd":"reload", "task":"nginx"}' --silent -H 'Content-type: application/json' | jq -r .type) NGINX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${NGINX}/exec -d '{"cmd":"reload", "task":"nginx"}' --silent -H 'Content-type: application/json' | jq -r .type)
[[ ${NGINX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Nginx, restarting container..."; restart_container ${NGINX} ; } [[ ${NGINX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Nginx, restarting container..."; restart_container ${NGINX} ; }
} }
reload_dovecot(){ reload_dovecot(){
echo "Reloading Dovecot..." echo "Reloading Dovecot..."
DOVECOT_RELOAD_RET=$(curl -X POST --insecure https://dockerapi/containers/${DOVECOT}/exec -d '{"cmd":"reload", "task":"dovecot"}' --silent -H 'Content-type: application/json' | jq -r .type) DOVECOT_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${DOVECOT}/exec -d '{"cmd":"reload", "task":"dovecot"}' --silent -H 'Content-type: application/json' | jq -r .type)
[[ ${DOVECOT_RELOAD_RET} != 'success' ]] && { echo "Could not reload Dovecot, restarting container..."; restart_container ${DOVECOT} ; } [[ ${DOVECOT_RELOAD_RET} != 'success' ]] && { echo "Could not reload Dovecot, restarting container..."; restart_container ${DOVECOT} ; }
} }
reload_postfix(){ reload_postfix(){
echo "Reloading Postfix..." echo "Reloading Postfix..."
POSTFIX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi/containers/${POSTFIX}/exec -d '{"cmd":"reload", "task":"postfix"}' --silent -H 'Content-type: application/json' | jq -r .type) POSTFIX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/exec -d '{"cmd":"reload", "task":"postfix"}' --silent -H 'Content-type: application/json' | jq -r .type)
[[ ${POSTFIX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Postfix, restarting container..."; restart_container ${POSTFIX} ; } [[ ${POSTFIX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Postfix, restarting container..."; restart_container ${POSTFIX} ; }
} }
restart_container(){ restart_container(){
for container in $*; do for container in $*; do
echo "Restarting ${container}..." echo "Restarting ${container}..."
C_REST_OUT=$(curl -X POST --insecure https://dockerapi/containers/${container}/restart --silent | jq -r '.msg') C_REST_OUT=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${container}/restart --silent | jq -r '.msg')
echo "${C_REST_OUT}" echo "${C_REST_OUT}"
done done
} }

View File

@ -1,6 +1,6 @@
FROM alpine:3.20 FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
RUN apk upgrade --no-cache \ RUN apk upgrade --no-cache \
&& apk add --update --no-cache \ && apk add --update --no-cache \

View File

@ -1,6 +1,6 @@
FROM alpine:3.20 FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
ARG PIP_BREAK_SYSTEM_PACKAGES=1 ARG PIP_BREAK_SYSTEM_PACKAGES=1
WORKDIR /app WORKDIR /app

View File

@ -1,11 +1,12 @@
FROM alpine:3.20 FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$ # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.16 ARG GOSU_VERSION=1.16
ENV LANG C.UTF-8 ENV LANG=C.UTF-8
ENV LC_ALL C.UTF-8 ENV LC_ALL=C.UTF-8
# Add groups and users before installing Dovecot to not break compatibility # Add groups and users before installing Dovecot to not break compatibility
RUN addgroup -g 5000 vmail \ RUN addgroup -g 5000 vmail \
@ -132,4 +133,4 @@ COPY repl_health.sh /usr/local/bin/repl_health.sh
COPY optimize-fts.sh /usr/local/bin/optimize-fts.sh COPY optimize-fts.sh /usr/local/bin/optimize-fts.sh
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]

View File

@ -257,10 +257,14 @@ plugin {
fts_autoindex_exclude2 = \Trash fts_autoindex_exclude2 = \Trash
fts = flatcurve fts = flatcurve
# Maximum term length can be set via the 'maxlen' argument (maxlen is
# specified in bytes, not number of UTF-8 characters)
fts_tokenizer_email_address = maxlen=100
fts_tokenizer_generic = algorithm=simple maxlen=30
# These are not flatcurve settings, but required for Dovecot FTS. See # These are not flatcurve settings, but required for Dovecot FTS. See
# Dovecot FTS Configuration link above for further information. # Dovecot FTS Configuration link above for further information.
fts_languages = en es de fts_languages = en es de
fts_tokenizer_generic = algorithm=simple
fts_tokenizers = generic email-address fts_tokenizers = generic email-address
# OPTIONAL: Recommended default FTS core configuration # OPTIONAL: Recommended default FTS core configuration
@ -407,14 +411,6 @@ sievec /var/vmail/sieve/global_sieve_after.sieve
sievec /usr/lib/dovecot/sieve/report-spam.sieve sievec /usr/lib/dovecot/sieve/report-spam.sieve
sievec /usr/lib/dovecot/sieve/report-ham.sieve sievec /usr/lib/dovecot/sieve/report-ham.sieve
for file in /var/vmail/*/*/sieve/*.sieve ; do
if [[ "$file" == "/var/vmail/*/*/sieve/*.sieve" ]]; then
continue
fi
sievec "$file" "$(dirname "$file")/../.dovecot.svbin"
chown vmail:vmail "$(dirname "$file")/../.dovecot.svbin"
done
# Fix permissions # Fix permissions
chown root:root /etc/dovecot/sql/*.conf chown root:root /etc/dovecot/sql/*.conf
chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua

View File

@ -3,8 +3,8 @@ FILE=/tmp/mail$$
cat > $FILE cat > $FILE
trap "/bin/rm -f $FILE" 0 1 2 3 13 15 trap "/bin/rm -f $FILE" 0 1 2 3 13 15
cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzydel cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzydel
cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnham cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/learnham
cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzyadd
exit 0 exit 0

View File

@ -3,8 +3,8 @@ FILE=/tmp/mail$$
cat > $FILE cat > $FILE
trap "/bin/rm -f $FILE" 0 1 2 3 13 15 trap "/bin/rm -f $FILE" 0 1 2 3 13 15
cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzydel cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzydel
cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnspam cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/learnspam
cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzyadd
exit 0 exit 0

View File

@ -21,11 +21,11 @@ sed -i -e 's/\([^\\]\)\$\([^\/]\)/\1\\$\2/g' /etc/rspamd/custom/sa-rules
if [[ "$(cat /etc/rspamd/custom/sa-rules | md5sum | cut -d' ' -f1)" != "${HASH_SA_RULES}" ]]; then if [[ "$(cat /etc/rspamd/custom/sa-rules | md5sum | cut -d' ' -f1)" != "${HASH_SA_RULES}" ]]; then
CONTAINER_NAME=rspamd-mailcow CONTAINER_NAME=rspamd-mailcow
CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | \ CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | \
jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | \ jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | \
jq -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id") jq -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")
if [[ ! -z ${CONTAINER_ID} ]]; then if [[ ! -z ${CONTAINER_ID} ]]; then
curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://dockerapi/containers/${CONTAINER_ID}/restart curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
fi fi
fi fi

View File

@ -1,5 +1,6 @@
FROM alpine:3.20 FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
WORKDIR /app WORKDIR /app

View File

@ -1,5 +1,6 @@
FROM alpine:3.20 FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
ARG PIP_BREAK_SYSTEM_PACKAGES=1 ARG PIP_BREAK_SYSTEM_PACKAGES=1
WORKDIR /app WORKDIR /app

View File

@ -1,5 +1,6 @@
FROM php:8.2-fpm-alpine3.18 FROM php:8.2-fpm-alpine3.18
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG APCU_PECL_VERSION=5.1.23 ARG APCU_PECL_VERSION=5.1.23

View File

@ -23,7 +23,7 @@ done
# Check mysql_upgrade (master and slave) # Check mysql_upgrade (master and slave)
CONTAINER_ID= CONTAINER_ID=
until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do
CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null) CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
echo "Could not get mysql-mailcow container id... trying again" echo "Could not get mysql-mailcow container id... trying again"
sleep 2 sleep 2
done done
@ -35,7 +35,7 @@ until [[ ${SQL_UPGRADE_STATUS} == 'success' ]]; do
echo "Tried to upgrade MySQL and failed, giving up after ${SQL_LOOP_C} retries and starting container (oops, not good)" echo "Tried to upgrade MySQL and failed, giving up after ${SQL_LOOP_C} retries and starting container (oops, not good)"
break break
fi fi
SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json') SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json')
SQL_UPGRADE_STATUS=$(echo ${SQL_FULL_UPGRADE_RETURN} | jq -r .type) SQL_UPGRADE_STATUS=$(echo ${SQL_FULL_UPGRADE_RETURN} | jq -r .type)
SQL_LOOP_C=$((SQL_LOOP_C+1)) SQL_LOOP_C=$((SQL_LOOP_C+1))
echo "SQL upgrade iteration #${SQL_LOOP_C}" echo "SQL upgrade iteration #${SQL_LOOP_C}"
@ -60,12 +60,12 @@ done
# doing post-installation stuff, if SQL was upgraded (master and slave) # doing post-installation stuff, if SQL was upgraded (master and slave)
if [ ${SQL_CHANGED} -eq 1 ]; then if [ ${SQL_CHANGED} -eq 1 ]; then
POSTFIX=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null) POSTFIX=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
if [[ -z "${POSTFIX}" ]] || ! [[ "${POSTFIX}" =~ ^[[:alnum:]]*$ ]]; then if [[ -z "${POSTFIX}" ]] || ! [[ "${POSTFIX}" =~ ^[[:alnum:]]*$ ]]; then
echo "Could not determine Postfix container ID, skipping Postfix restart." echo "Could not determine Postfix container ID, skipping Postfix restart."
else else
echo "Restarting Postfix" echo "Restarting Postfix"
curl -X POST --silent --insecure https://dockerapi/containers/${POSTFIX}/restart | jq -r '.msg' curl -X POST --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/restart | jq -r '.msg'
echo "Sleeping 5 seconds..." echo "Sleeping 5 seconds..."
sleep 5 sleep 5
fi fi
@ -74,7 +74,7 @@ fi
# Check mysql tz import (master and slave) # Check mysql tz import (master and slave)
TZ_CHECK=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null) TZ_CHECK=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null)
if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then
SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json') SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json')
echo "MySQL mysql_tzinfo_to_sql - debug output:" echo "MySQL mysql_tzinfo_to_sql - debug output:"
echo ${SQL_FULL_TZINFO_IMPORT_RETURN} echo ${SQL_FULL_TZINFO_IMPORT_RETURN}
fi fi

View File

@ -1,5 +1,6 @@
FROM debian:bookworm-slim FROM debian:bookworm-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ENV LC_ALL C ENV LC_ALL C

View File

@ -1,10 +1,10 @@
FROM debian:bullseye-slim FROM debian:bookworm-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG RSPAMD_VER=rspamd_3.7.5-2~8c86c1676 ARG RSPAMD_VER=rspamd_3.9.1-1~82f43560f
ARG CODENAME=bullseye ARG CODENAME=bookworm
ENV LC_ALL C ENV LC_ALL=C
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
tzdata \ tzdata \
@ -12,11 +12,12 @@ RUN apt-get update && apt-get install -y \
gnupg2 \ gnupg2 \
apt-transport-https \ apt-transport-https \
dnsutils \ dnsutils \
netcat \ netcat-traditional \
wget \ wget \
redis-tools \ redis-tools \
procps \ procps \
nano \ nano \
lua-cjson \
&& arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \ && arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
&& wget -P /tmp https://rspamd.com/apt-stable/pool/main/r/rspamd/${RSPAMD_VER}~${CODENAME}_${arch}.deb\ && wget -P /tmp https://rspamd.com/apt-stable/pool/main/r/rspamd/${RSPAMD_VER}~${CODENAME}_${arch}.deb\
&& apt install -y /tmp/${RSPAMD_VER}~${CODENAME}_${arch}.deb \ && apt install -y /tmp/${RSPAMD_VER}~${CODENAME}_${arch}.deb \

View File

@ -124,4 +124,190 @@ for file in /hooks/*; do
fi fi
done done
# If DQS KEY is set in mailcow.conf add Spamhaus DQS RBLs
if [[ ! -z ${SPAMHAUS_DQS_KEY} ]]; then
cat <<EOF > /etc/rspamd/custom/dqs-rbl.conf
# Autogenerated by mailcow. DO NOT TOUCH!
spamhaus {
rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net";
from = false;
}
spamhaus_from {
from = true;
received = false;
rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net";
returncodes {
SPAMHAUS_ZEN = [ "127.0.0.2", "127.0.0.3", "127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.0.9", "127.0.0.10", "127.0.0.11" ];
}
}
spamhaus_authbl_received {
# Check if the sender client is listed in AuthBL (AuthBL is *not* part of ZEN)
rbl = "${SPAMHAUS_DQS_KEY}.authbl.dq.spamhaus.net";
from = false;
received = true;
ipv6 = true;
returncodes {
SH_AUTHBL_RECEIVED = "127.0.0.20"
}
}
spamhaus_dbl {
# Add checks on the HELO string
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
helo = true;
rdns = true;
dkim = true;
disable_monitoring = true;
returncodes {
RBL_DBL_SPAM = "127.0.1.2";
RBL_DBL_PHISH = "127.0.1.4";
RBL_DBL_MALWARE = "127.0.1.5";
RBL_DBL_BOTNET = "127.0.1.6";
RBL_DBL_ABUSED_SPAM = "127.0.1.102";
RBL_DBL_ABUSED_PHISH = "127.0.1.104";
RBL_DBL_ABUSED_MALWARE = "127.0.1.105";
RBL_DBL_ABUSED_BOTNET = "127.0.1.106";
RBL_DBL_DONT_QUERY_IPS = "127.0.1.255";
}
}
spamhaus_dbl_fullurls {
ignore_defaults = true;
no_ip = true;
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
selector = 'urls:get_host'
disable_monitoring = true;
returncodes {
DBLABUSED_SPAM_FULLURLS = "127.0.1.102";
DBLABUSED_PHISH_FULLURLS = "127.0.1.104";
DBLABUSED_MALWARE_FULLURLS = "127.0.1.105";
DBLABUSED_BOTNET_FULLURLS = "127.0.1.106";
}
}
spamhaus_zrd {
# Add checks on the HELO string also for DQS
rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net";
helo = true;
rdns = true;
dkim = true;
disable_monitoring = true;
returncodes {
RBL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"];
RBL_ZRD_FRESH_DOMAIN = [
"127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"
];
RBL_ZRD_DONT_QUERY_IPS = "127.0.2.255";
}
}
"SPAMHAUS_ZEN_URIBL" {
enabled = true;
rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net";
resolve_ip = true;
checks = ['urls'];
replyto = true;
emails = true;
ipv4 = true;
ipv6 = true;
emails_domainonly = true;
returncodes {
URIBL_SBL = "127.0.0.2";
URIBL_SBL_CSS = "127.0.0.3";
URIBL_XBL = ["127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7"];
URIBL_PBL = ["127.0.0.10", "127.0.0.11"];
URIBL_DROP = "127.0.0.9";
}
}
SH_EMAIL_DBL {
ignore_defaults = true;
replyto = true;
emails_domainonly = true;
disable_monitoring = true;
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
returncodes = {
SH_EMAIL_DBL = [
"127.0.1.2",
"127.0.1.4",
"127.0.1.5",
"127.0.1.6"
];
SH_EMAIL_DBL_ABUSED = [
"127.0.1.102",
"127.0.1.104",
"127.0.1.105",
"127.0.1.106"
];
SH_EMAIL_DBL_DONT_QUERY_IPS = [ "127.0.1.255" ];
}
}
SH_EMAIL_ZRD {
ignore_defaults = true;
replyto = true;
emails_domainonly = true;
disable_monitoring = true;
rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net";
returncodes = {
SH_EMAIL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"];
SH_EMAIL_ZRD_FRESH_DOMAIN = [
"127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"
];
SH_EMAIL_ZRD_DONT_QUERY_IPS = [ "127.0.2.255" ];
}
}
"DBL" {
# override the defaults for DBL defined in modules.d/rbl.conf
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
disable_monitoring = true;
}
"ZRD" {
ignore_defaults = true;
rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net";
no_ip = true;
dkim = true;
emails = true;
emails_domainonly = true;
urls = true;
returncodes = {
ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"];
ZRD_FRESH_DOMAIN = ["127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"];
}
}
spamhaus_sbl_url {
ignore_defaults = true
rbl = "${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net";
checks = ['urls'];
disable_monitoring = true;
returncodes {
SPAMHAUS_SBL_URL = "127.0.0.2";
}
}
SH_HBL_EMAIL {
ignore_defaults = true;
rbl = "_email.${SPAMHAUS_DQS_KEY}.hbl.dq.spamhaus.net";
emails_domainonly = false;
selector = "from('smtp').lower;from('mime').lower";
ignore_whitelist = true;
checks = ['emails', 'replyto'];
hash = "sha1";
returncodes = {
SH_HBL_EMAIL = [
"127.0.3.2"
];
}
}
spamhaus_dqs_hbl {
symbol = "HBL_FILE_UNKNOWN";
rbl = "_file.${SPAMHAUS_DQS_KEY}.hbl.dq.spamhaus.net.";
selector = "attachments('rbase32', 'sha256')";
ignore_whitelist = true;
ignore_defaults = true;
returncodes {
SH_HBL_FILE_MALICIOUS = "127.0.3.10";
SH_HBL_FILE_SUSPICIOUS = "127.0.3.15";
}
}
EOF
else
rm -rf /etc/rspamd/custom/dqs-rbl.conf
fi
exec "$@" exec "$@"

View File

@ -1,12 +1,13 @@
FROM debian:bullseye-slim FROM debian:bookworm-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG DEBIAN_VERSION=bullseye ARG DEBIAN_VERSION=bookworm
ARG SOGO_DEBIAN_REPOSITORY=http://www.axis.cz/linux/debian ARG SOGO_DEBIAN_REPOSITORY=http://www.axis.cz/linux/debian
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$ # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.17 ARG GOSU_VERSION=1.17
ENV LC_ALL C ENV LC_ALL=C
# Prerequisites # Prerequisites
RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \

View File

@ -1,4 +1,4 @@
@version: 3.28 @version: 3.38
@include "scl.conf" @include "scl.conf"
options { options {
chain_hostnames(off); chain_hostnames(off);

View File

@ -1,4 +1,4 @@
@version: 3.28 @version: 3.38
@include "scl.conf" @include "scl.conf"
options { options {
chain_hostnames(off); chain_hostnames(off);

View File

@ -1,18 +1,21 @@
FROM alpine:3.20 FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
RUN apk add --update --no-cache \ RUN apk add --update --no-cache \
curl \ curl \
bind-tools \ bind-tools \
coreutils \
unbound \ unbound \
bash \ bash \
openssl \ openssl \
drill \ drill \
tzdata \ tzdata \
syslog-ng \
supervisor \
&& curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache \ && curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache \
&& chown root:unbound /etc/unbound \ && chown root:unbound /etc/unbound \
&& adduser unbound tty \ && adduser unbound tty \
&& chmod 775 /etc/unbound && chmod 775 /etc/unbound
EXPOSE 53/udp 53/tcp EXPOSE 53/udp 53/tcp
@ -21,9 +24,13 @@ COPY docker-entrypoint.sh /docker-entrypoint.sh
# healthcheck (dig, ping) # healthcheck (dig, ping)
COPY healthcheck.sh /healthcheck.sh COPY healthcheck.sh /healthcheck.sh
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
COPY supervisord.conf /etc/supervisor/supervisord.conf
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
RUN chmod +x /healthcheck.sh RUN chmod +x /healthcheck.sh
HEALTHCHECK --interval=30s --timeout=30s CMD [ "/healthcheck.sh" ] HEALTHCHECK --interval=30s --timeout=10s \
CMD sh -c '[ -f /tmp/healthcheck_status ] && [ "$(cat /tmp/healthcheck_status)" -eq 0 ] || exit 1'
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
CMD ["/usr/sbin/unbound"]

View File

@ -1,76 +1,102 @@
#!/bin/bash #!/bin/bash
# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) STATUS_FILE="/tmp/healthcheck_status"
if [[ "${SKIP_UNBOUND_HEALTHCHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then RUNS=0
SKIP_UNBOUND_HEALTHCHECK=y
fi
# Reset logfile # Declare log function for logfile to stdout
echo "$(date +"%Y-%m-%d %H:%M:%S"): Starting health check - logs can be found in /var/log/healthcheck.log" function log_to_stdout() {
echo "$(date +"%Y-%m-%d %H:%M:%S"): Starting health check" > /var/log/healthcheck.log echo "$(date +"%Y-%m-%d %H:%M:%S"): $1"
# Declare log function for logfile inside container
function log_to_file() {
echo "$(date +"%Y-%m-%d %H:%M:%S"): $1" >> /var/log/healthcheck.log
} }
# General Ping function to check general pingability # General Ping function to check general pingability
function check_ping() { function check_ping() {
declare -a ipstoping=("1.1.1.1" "8.8.8.8" "9.9.9.9") declare -a ipstoping=("1.1.1.1" "8.8.8.8" "9.9.9.9")
local fail_tolerance=1
local failures=0
for ip in "${ipstoping[@]}" ; do for ip in "${ipstoping[@]}" ; do
ping -q -c 3 -w 5 "$ip" success=false
if [ $? -ne 0 ]; then for ((i=1; i<=3; i++)); do
log_to_file "Healthcheck: Couldn't ping $ip for 5 seconds... Gave up!" ping -q -c 3 -w 5 "$ip" > /dev/null
log_to_file "Please check your internet connection or firewall rules to fix this error, because a simple ping test should always go through from the unbound container!" if [ $? -eq 0 ]; then
return 1 success=true
fi break
else
log_to_stdout "Healthcheck: Failed to ping $ip on attempt $i. Trying again..."
fi
done done
log_to_file "Healthcheck: Ping Checks WORKING properly!" if [ "$success" = false ]; then
return 0 log_to_stdout "Healthcheck: Couldn't ping $ip after 3 attempts. Marking this IP as failed."
((failures++))
fi
done
if [ $failures -gt $fail_tolerance ]; then
log_to_stdout "Healthcheck: Too many ping failures ($fail_tolerance failures allowed, you got $failures failures), marking Healthcheck as unhealthy..."
return 1
fi
return 0
} }
# General DNS Resolve Check against Unbound Resolver himself # General DNS Resolve Check against Unbound Resolver himself
function check_dns() { function check_dns() {
declare -a domains=("mailcow.email" "github.com" "hub.docker.com") declare -a domains=("fuzzy.mailcow.email" "github.com" "hub.docker.com")
local fail_tolerance=1
local failures=0
for domain in "${domains[@]}" ; do for domain in "${domains[@]}" ; do
for ((i=1; i<=3; i++)); do success=false
dig +short +timeout=2 +tries=1 "$domain" @127.0.0.1 > /dev/null for ((i=1; i<=3; i++)); do
if [ $? -ne 0 ]; then dig_output=$(dig +short +timeout=2 +tries=1 "$domain" @127.0.0.1 2>/dev/null)
log_to_file "Healthcheck: DNS Resolution Failed on $i attempt! Trying again..." dig_rc=$?
if [ $i -eq 3 ]; then
log_to_file "Healthcheck: DNS Resolution not possible after $i attempts... Gave up!" if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
log_to_file "Maybe check your outbound firewall, as it needs to resolve DNS over TCP AND UDP!" log_to_stdout "Healthcheck: DNS Resolution Failed on attempt $i for $domain! Trying again..."
return 1 else
fi success=true
break
fi fi
done
done done
log_to_file "Healthcheck: DNS Resolver WORKING properly!" if [ "$success" = false ]; then
return 0 log_to_stdout "Healthcheck: DNS Resolution not possible after 3 attempts for $domain... Gave up!"
((failures++))
fi
done
if [ $failures -gt $fail_tolerance ]; then
log_to_stdout "Healthcheck: Too many DNS failures ($fail_tolerance failures allowed, you got $failures failures), marking Healthcheck as unhealthy..."
return 1
fi
return 0
} }
if [[ ${SKIP_UNBOUND_HEALTHCHECK} == "y" ]]; then while true; do
log_to_file "Healthcheck: ALL CHECKS WERE SKIPPED! Unbound is healthy!"
exit 0
fi
# run checks, if check is not returning 0 (return value if check is ok), healthcheck will exit with 1 (marked in docker as unhealthy) if [[ ${SKIP_UNBOUND_HEALTHCHECK} == "y" ]]; then
check_ping log_to_stdout "Healthcheck: ALL CHECKS WERE SKIPPED! Unbound is healthy!"
echo "0" > $STATUS_FILE
sleep 365d
fi
if [ $? -ne 0 ]; then # run checks, if check is not returning 0 (return value if check is ok), healthcheck will exit with 1 (marked in docker as unhealthy)
exit 1 check_ping
fi PING_STATUS=$?
check_dns check_dns
DNS_STATUS=$?
if [ $? -ne 0 ]; then if [ $PING_STATUS -ne 0 ] || [ $DNS_STATUS -ne 0 ]; then
exit 1 echo "1" > $STATUS_FILE
fi
log_to_file "Healthcheck: ALL CHECKS WERE SUCCESSFUL! Unbound is healthy!" else
exit 0 echo "0" > $STATUS_FILE
fi
sleep 30
done

View File

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

View File

@ -0,0 +1,32 @@
[supervisord]
nodaemon=true
user=root
pidfile=/var/run/supervisord.pid
[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:unbound]
command=/usr/sbin/unbound
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
[program:unbound-healthcheck]
command=/bin/bash /healthcheck.sh
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
[eventlistener:processes]
command=/usr/local/sbin/stop-supervisor.sh
events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL

View File

@ -0,0 +1,21 @@
@version: 4.5
@include "scl.conf"
options {
chain_hostnames(off);
flush_lines(0);
use_dns(no);
use_fqdn(no);
owner("root"); group("adm"); perm(0640);
stats(freq(0));
keep_timestamp(no);
bad_hostname("^gconfd$");
};
source s_dgram {
unix-dgram("/dev/log");
internal();
};
destination d_stdout { pipe("/dev/stdout"); };
log {
source(s_dgram);
destination(d_stdout);
};

View File

@ -1,5 +1,6 @@
FROM alpine:3.20 FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
# Installation # Installation
RUN apk add --update \ RUN apk add --update \

View File

@ -169,8 +169,12 @@ function notify_error() {
return 1 return 1
fi fi
# Escape subject and body (https://stackoverflow.com/a/2705678)
ESCAPED_SUBJECT=$(echo ${SUBJECT} | sed -e 's/[\/&]/\\&/g')
ESCAPED_BODY=$(echo ${BODY} | sed -e 's/[\/&]/\\&/g')
# Replace subject and body placeholders # Replace subject and body placeholders
WEBHOOK_BODY=$(echo ${WATCHDOG_NOTIFY_WEBHOOK_BODY} | sed "s/\$SUBJECT\|\${SUBJECT}/$SUBJECT/g" | sed "s/\$BODY\|\${BODY}/$BODY/g") WEBHOOK_BODY=$(echo ${WATCHDOG_NOTIFY_WEBHOOK_BODY} | sed -e "s/\$SUBJECT\|\${SUBJECT}/$ESCAPED_SUBJECT/g" -e "s/\$BODY\|\${BODY}/$ESCAPED_BODY/g")
# POST to webhook # POST to webhook
curl -X POST -H "Content-Type: application/json" ${CURL_VERBOSE} -d "${WEBHOOK_BODY}" ${WATCHDOG_NOTIFY_WEBHOOK} curl -X POST -H "Content-Type: application/json" ${CURL_VERBOSE} -d "${WEBHOOK_BODY}" ${WATCHDOG_NOTIFY_WEBHOOK}
@ -191,12 +195,12 @@ get_container_ip() {
else else
sleep 0.5 sleep 0.5
# get long container id for exact match # get long container id for exact match
CONTAINER_ID=($(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")) CONTAINER_ID=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id"))
# returned id can have multiple elements (if scaled), shuffle for random test # returned id can have multiple elements (if scaled), shuffle for random test
CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf)) CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf))
if [[ ! -z ${CONTAINER_ID} ]]; then if [[ ! -z ${CONTAINER_ID} ]]; then
for matched_container in "${CONTAINER_ID[@]}"; do for matched_container in "${CONTAINER_ID[@]}"; do
CONTAINER_IPS=($(curl --silent --insecure https://dockerapi/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress')) CONTAINER_IPS=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress'))
for ip_match in "${CONTAINER_IPS[@]}"; do for ip_match in "${CONTAINER_IPS[@]}"; do
# grep will do nothing if one of these vars is empty # grep will do nothing if one of these vars is empty
[[ -z ${ip_match} ]] && continue [[ -z ${ip_match} ]] && continue
@ -716,7 +720,7 @@ rspamd_checks() {
From: watchdog@localhost From: watchdog@localhost
Empty Empty
' | usr/bin/curl --max-time 10 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan | jq -rc .default.required_score | sed 's/\..*//' ) ' | usr/bin/curl --max-time 10 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/scan | jq -rc .default.required_score | sed 's/\..*//' )
if [[ ${SCORE} -ne 9999 ]]; then if [[ ${SCORE} -ne 9999 ]]; then
echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2 echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2
err_count=$(( ${err_count} + 1)) err_count=$(( ${err_count} + 1))
@ -1095,12 +1099,12 @@ while true; do
elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
kill -STOP ${BACKGROUND_TASKS[*]} kill -STOP ${BACKGROUND_TASKS[*]}
sleep 10 sleep 10
CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id") CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")
if [[ ! -z ${CONTAINER_ID} ]]; then if [[ ! -z ${CONTAINER_ID} ]]; then
if [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then if [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then
HAS_INITDB=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true) HAS_INITDB=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true)
fi fi
S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://dockerapi/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d))) S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d)))
if [ ${S_RUNNING} -lt 360 ]; then if [ ${S_RUNNING} -lt 360 ]; then
log_msg "Container is running for less than 360 seconds, skipping action..." log_msg "Container is running for less than 360 seconds, skipping action..."
elif [[ ! -z ${HAS_INITDB} ]]; then elif [[ ! -z ${HAS_INITDB} ]]; then
@ -1108,7 +1112,7 @@ while true; do
sleep 60 sleep 60
else else
log_msg "Sending restart command to ${CONTAINER_ID}..." log_msg "Sending restart command to ${CONTAINER_ID}..."
curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/restart curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
notify_error "${com_pipe_answer}" notify_error "${com_pipe_answer}"
log_msg "Wait for restarted container to settle and continue watching..." log_msg "Wait for restarted container to settle and continue watching..."
sleep 35 sleep 35

View File

@ -0,0 +1,29 @@
<html>
<head>
<meta name="x-apple-disable-message-reformatting" />
<style>
body {
font-family: Helvetica, Arial, Sans-Serif;
}
/* mobile devices */
@media all and (max-width: 480px) {
.mob {
display: none;
}
}
</style>
</head>
<body>
Hello {{username2}},<br><br>
Somebody requested a new password for the {{hostname}} account associated with {{username}}.<br>
<small>Date of the password reset request: {{date}}</small><br><br>
You can reset your password by clicking the link below:<br>
<a href="{{link}}">{{link}}</a><br><br>
The link will be valid for the next {{token_lifetime}} minutes.<br><br>
If you did not request a new password, please ignore this email.<br>
</body>
</html>

View File

@ -0,0 +1,11 @@
Hello {{username2}},
Somebody requested a new password for the {{hostname}} account associated with {{username}}.
Date of the password reset request: {{date}}
You can reset your password by clicking the link below:
{{link}}
The link will be valid for the next {{token_lifetime}} minutes.
If you did not request a new password, please ignore this email.

View File

@ -289,5 +289,20 @@ namespace inbox {
mailbox "Kladde" { mailbox "Kladde" {
special_use = \Drafts special_use = \Drafts
} }
mailbox "Πρόχειρα" {
special_use = \Drafts
}
mailbox "Απεσταλμένα" {
special_use = \Sent
}
mailbox "Κάδος απορριμάτων" {
special_use = \Trash
}
mailbox "Ανεπιθύμητα" {
special_use = \Junk
}
mailbox "Αρχειοθετημένα" {
special_use = \Archive
}
prefix = prefix =
} }

View File

@ -2,6 +2,7 @@ rbls {
interserver_ip { interserver_ip {
symbol = "RBL_INTERSERVER_IP"; symbol = "RBL_INTERSERVER_IP";
rbl = "rbl.interserver.net"; rbl = "rbl.interserver.net";
from = true;
ipv6 = false; ipv6 = false;
returncodes { returncodes {
RBL_INTERSERVER_BAD_IP = "127.0.0.2"; RBL_INTERSERVER_BAD_IP = "127.0.0.2";
@ -19,4 +20,7 @@ rbls {
RBL_INTERSERVER_BAD_URI = "127.0.0.2"; RBL_INTERSERVER_BAD_URI = "127.0.0.2";
} }
} }
.include(try=true,override=true,priority=5) "$LOCAL_CONFDIR/custom/dqs-rbl.conf"
} }

View File

@ -17,4 +17,261 @@ symbols = {
score = 4.0; score = 4.0;
description = "Listed on Interserver RBL"; description = "Listed on Interserver RBL";
} }
"SPAMHAUS_ZEN" {
weight = 7.0;
}
"SH_AUTHBL_RECEIVED" {
weight = 4.0;
}
"RBL_DBL_SPAM" {
weight = 7.0;
}
"RBL_DBL_PHISH" {
weight = 7.0;
}
"RBL_DBL_MALWARE" {
weight = 7.0;
}
"RBL_DBL_BOTNET" {
weight = 7.0;
}
"RBL_DBL_ABUSED_SPAM" {
weight = 3.0;
}
"RBL_DBL_ABUSED_PHISH" {
weight = 3.0;
}
"RBL_DBL_ABUSED_MALWARE" {
weight = 3.0;
}
"RBL_DBL_ABUSED_BOTNET" {
weight = 3.0;
}
"RBL_ZRD_VERY_FRESH_DOMAIN" {
weight = 7.0;
}
"RBL_ZRD_FRESH_DOMAIN" {
weight = 4.0;
}
"ZRD_VERY_FRESH_DOMAIN" {
weight = 7.0;
}
"ZRD_FRESH_DOMAIN" {
weight = 4.0;
}
"SH_EMAIL_DBL" {
weight = 7.0;
}
"SH_EMAIL_DBL_ABUSED" {
weight = 7.0;
}
"SH_EMAIL_ZRD_VERY_FRESH_DOMAIN" {
weight = 7.0;
}
"SH_EMAIL_ZRD_FRESH_DOMAIN" {
weight = 4.0;
}
"RBL_DBL_DONT_QUERY_IPS" {
weight = 0.0;
}
"RBL_ZRD_DONT_QUERY_IPS" {
weight = 0.0;
}
"SH_EMAIL_ZRD_DONT_QUERY_IPS" {
weight = 0.0;
}
"SH_EMAIL_DBL_DONT_QUERY_IPS" {
weight = 0.0;
}
"DBL" {
weight = 0.0;
description = "DBL unknown result";
groups = ["spamhaus"];
}
"DBL_SPAM" {
weight = 7;
description = "DBL uribl spam";
groups = ["spamhaus"];
}
"DBL_PHISH" {
weight = 7;
description = "DBL uribl phishing";
groups = ["spamhaus"];
}
"DBL_MALWARE" {
weight = 7;
description = "DBL uribl malware";
groups = ["spamhaus"];
}
"DBL_BOTNET" {
weight = 7;
description = "DBL uribl botnet C&C domain";
groups = ["spamhaus"];
}
"DBLABUSED_SPAM_FULLURLS" {
weight = 5.5;
description = "DBL uribl abused legit spam";
groups = ["spamhaus"];
}
"DBLABUSED_PHISH_FULLURLS" {
weight = 5.5;
description = "DBL uribl abused legit phish";
groups = ["spamhaus"];
}
"DBLABUSED_MALWARE_FULLURLS" {
weight = 5.5;
description = "DBL uribl abused legit malware";
groups = ["spamhaus"];
}
"DBLABUSED_BOTNET_FULLURLS" {
weight = 5.5;
description = "DBL uribl abused legit botnet";
groups = ["spamhaus"];
}
"DBL_ABUSE" {
weight = 5.5;
description = "DBL uribl abused legit spam";
groups = ["spamhaus"];
}
"DBL_ABUSE_REDIR" {
weight = 1.5;
description = "DBL uribl abused spammed redirector domain";
groups = ["spamhaus"];
}
"DBL_ABUSE_PHISH" {
weight = 5.5;
description = "DBL uribl abused legit phish";
groups = ["spamhaus"];
}
"DBL_ABUSE_MALWARE" {
weight = 5.5;
description = "DBL uribl abused legit malware";
groups = ["spamhaus"];
}
"DBL_ABUSE_BOTNET" {
weight = 5.5;
description = "DBL uribl abused legit botnet C&C";
groups = ["spamhaus"];
}
"DBL_PROHIBIT" {
weight = 0.0;
description = "DBL uribl IP queries prohibited!";
groups = ["spamhaus"];
}
"DBL_BLOCKED_OPENRESOLVER" {
weight = 0.0;
description = "You are querying Spamhaus from an open resolver, please see https://www.spamhaus.org/returnc/pub/";
groups = ["spamhaus"];
}
"DBL_BLOCKED" {
weight = 0.0;
description = "You are exceeding the query limit, please see https://www.spamhaus.org/returnc/vol/";
groups = ["spamhaus"];
}
"SPAMHAUS_ZEN_URIBL" {
weight = 0.0;
description = "Spamhaus ZEN URIBL: Filtered result";
groups = ["spamhaus"];
}
"URIBL_SBL" {
weight = 6.5;
description = "A domain in the message body resolves to an IP listed in Spamhaus SBL";
one_shot = true;
groups = ["spamhaus"];
}
"URIBL_SBL_CSS" {
weight = 6.5;
description = "A domain in the message body resolves to an IP listed in Spamhaus SBL CSS";
one_shot = true;
groups = ["spamhaus"];
}
"URIBL_PBL" {
weight = 0.01;
description = "A domain in the message body resolves to an IP listed in Spamhaus PBL";
one_shot = true;
groups = ["spamhaus"];
}
"URIBL_DROP" {
weight = 6.5;
description = "A domain in the message body resolves to an IP listed in Spamhaus DROP";
one_shot = true;
groups = ["spamhaus"];
}
"URIBL_XBL" {
weight = 5.0;
description = "A domain in the message body resolves to an IP listed in Spamhaus XBL";
one_shot = true;
groups = ["spamhaus"];
}
"SPAMHAUS_SBL_URL" {
weight = 6.5;
description = "A numeric URL in the message body is listed in Spamhaus SBL";
one_shot = true;
groups = ["spamhaus"];
}
"SH_HBL_EMAIL" {
weight = 7;
description = "Email listed in HBL";
groups = ["spamhaus"];
}
"SH_HBL_FILE_MALICIOUS" {
weight = 7;
description = "An attachment hash is listed in Spamhaus HBL as malicious";
groups = ["spamhaus"];
}
"SH_HBL_FILE_SUSPICIOUS" {
weight = 5;
description = "An attachment hash is listed in Spamhaus HBL as suspicious";
groups = ["spamhaus"];
}
"RBL_SPAMHAUS_CW_BTC" {
score = 7;
description = "Bitcoin found in Spamhaus cryptowallet list";
groups = ["spamhaus"];
}
"RBL_SPAMHAUS_CW_ETH" {
score = 7;
description = "Ethereum found in Spamhaus cryptowallet list";
groups = ["spamhaus"];
}
"RBL_SPAMHAUS_CW_BCH" {
score = 7;
description = "Bitcoinhash found in Spamhaus cryptowallet list";
groups = ["spamhaus"];
}
"RBL_SPAMHAUS_CW_XMR" {
score = 7;
description = "Monero found in Spamhaus cryptowallet list";
groups = ["spamhaus"];
}
"RBL_SPAMHAUS_CW_LTC" {
score = 7;
description = "Litecoin found in Spamhaus cryptowallet list";
groups = ["spamhaus"];
}
"RBL_SPAMHAUS_CW_XRP" {
score = 7;
description = "Ripple found in Spamhaus cryptowallet list";
groups = ["spamhaus"];
}
"RBL_SPAMHAUS_HBL_URL" {
score = 7;
description = "URL found in spamhaus HBL blocklist";
groups = ["spamhaus"];
}
} }

View File

@ -1,12 +1,14 @@
classifier "bayes" { classifier "bayes" {
# name = "custom"; # 'name' parameter must be set if multiple classifiers are defined
learn_condition = 'return require("lua_bayes_learn").can_learn';
new_schema = true;
tokenizer { tokenizer {
name = "osb"; name = "osb";
} }
backend = "redis"; backend = "redis";
min_tokens = 11; min_tokens = 11;
min_learns = 5; min_learns = 5;
new_schema = true; expire = 7776000;
expire = 2592000;
statfile { statfile {
symbol = "BAYES_HAM"; symbol = "BAYES_HAM";
spam = false; spam = false;

View File

@ -107,6 +107,7 @@ $template_data = [
'f2b_banlist_url' => getBaseUrl() . "/api/v1/get/fail2ban/banlist/" . $f2b_data['banlist_id'], 'f2b_banlist_url' => getBaseUrl() . "/api/v1/get/fail2ban/banlist/" . $f2b_data['banlist_id'],
'q_data' => quarantine('settings'), 'q_data' => quarantine('settings'),
'qn_data' => quota_notification('get'), 'qn_data' => quota_notification('get'),
'pw_reset_data' => reset_password('get_notification'),
'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'), 'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'),
'rsettings' => $rsettings, 'rsettings' => $rsettings,
'rspamd_regex_maps' => $rspamd_regex_maps, 'rspamd_regex_maps' => $rspamd_regex_maps,

View File

@ -1073,13 +1073,17 @@ function update_sogo_static_view($mailbox = null) {
function edit_user_account($_data) { function edit_user_account($_data) {
global $lang; global $lang;
global $pdo; global $pdo;
$_data_log = $_data; $_data_log = $_data;
!isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*'; !isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
!isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*'; !isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
!isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*'; !isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
$username = $_SESSION['mailcow_cc_username']; $username = $_SESSION['mailcow_cc_username'];
$role = $_SESSION['mailcow_cc_role']; $role = $_SESSION['mailcow_cc_role'];
$password_old = $_data['user_old_pass']; $password_old = $_data['user_old_pass'];
$pw_recovery_email = $_data['pw_recovery_email'];
if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') { if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@ -1088,20 +1092,24 @@ function edit_user_account($_data) {
); );
return false; return false;
} }
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
WHERE `kind` NOT REGEXP 'location|thing|group' // edit password
AND `username` = :user"); if (!empty($password_old) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
$stmt->execute(array(':user' => $username)); $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
$row = $stmt->fetch(PDO::FETCH_ASSOC); WHERE `kind` NOT REGEXP 'location|thing|group'
if (!verify_hash($row['password'], $password_old)) { AND `username` = :user");
$_SESSION['return'][] = array( $stmt->execute(array(':user' => $username));
'type' => 'danger', $row = $stmt->fetch(PDO::FETCH_ASSOC);
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied' if (!verify_hash($row['password'], $password_old)) {
); $_SESSION['return'][] = array(
return false; 'type' => 'danger',
} 'log' => array(__FUNCTION__, $_data_log),
if (!empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) { 'msg' => 'access_denied'
);
return false;
}
$password_new = $_data['user_new_pass']; $password_new = $_data['user_new_pass'];
$password_new2 = $_data['user_new_pass2']; $password_new2 = $_data['user_new_pass2'];
if (password_check($password_new, $password_new2) !== true) { if (password_check($password_new, $password_new2) !== true) {
@ -1116,8 +1124,29 @@ function edit_user_account($_data) {
':password_hashed' => $password_hashed, ':password_hashed' => $password_hashed,
':username' => $username ':username' => $username
)); ));
update_sogo_static_view();
} }
update_sogo_static_view(); // edit password recovery email
elseif (isset($pw_recovery_email)) {
if (!isset($_SESSION['acl']['pw_reset']) || $_SESSION['acl']['pw_reset'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'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");
$stmt->execute(array(
':recovery_email' => $pw_recovery_email,
':username' => $username
));
}
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_data_log), 'log' => array(__FUNCTION__, $_data_log),
@ -2261,6 +2290,386 @@ function uuid4() {
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
} }
function reset_password($action, $data = null) {
global $pdo;
global $redis;
global $mailcow_hostname;
global $PW_RESET_TOKEN_LIMIT;
global $PW_RESET_TOKEN_LIFETIME;
$_data_log = $data;
if (isset($_data_log['new_password'])) $_data_log['new_password'] = '*';
if (isset($_data_log['new_password2'])) $_data_log['new_password2'] = '*';
switch ($action) {
case 'check':
$token = $data;
$stmt = $pdo->prepare("SELECT `t1`.`username` FROM `reset_password` AS `t1` JOIN `mailbox` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL :lifetime MINUTE) AND `t2`.`active` = 1;");
$stmt->execute(array(
':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token),
':lifetime' => $PW_RESET_TOKEN_LIFETIME
));
$return = $stmt->fetch(PDO::FETCH_ASSOC);
return empty($return['username']) ? false : $return['username'];
break;
case 'issue':
$username = $data;
// perform cleanup
$stmt = $pdo->prepare("DELETE FROM `reset_password` WHERE created < DATE_SUB(NOW(), INTERVAL :lifetime MINUTE);");
$stmt->execute(array(':lifetime' => $PW_RESET_TOKEN_LIFETIME));
if (filter_var($username, FILTER_VALIDATE_EMAIL) === false) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$pw_reset_notification = reset_password('get_notification', 'raw');
if (!$pw_reset_notification) return false;
if (empty($pw_reset_notification['from']) || empty($pw_reset_notification['subject'])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'password_reset_na'
);
return false;
}
$stmt = $pdo->prepare("SELECT * FROM `mailbox`
WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$mailbox_data = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($mailbox_data)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'password_reset_invalid_user'
);
return false;
}
$mailbox_attr = json_decode($mailbox_data['attributes'], true);
if (empty($mailbox_attr['recovery_email']) || filter_var($mailbox_attr['recovery_email'], FILTER_VALIDATE_EMAIL) === false) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => "password_reset_invalid_user"
);
return false;
}
$stmt = $pdo->prepare("SELECT * FROM `reset_password`
WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$generated_token_count = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($generated_token_count >= $PW_RESET_TOKEN_LIMIT) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => "reset_token_limit_exceeded"
);
return false;
}
$token = implode('-', array(
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3)))
));
$stmt = $pdo->prepare("INSERT INTO `reset_password` (`username`, `token`)
VALUES (:username, :token)");
$stmt->execute(array(
':username' => $username,
':token' => $token
));
$reset_link = getBaseURL() . "/reset-password?token=" . $token;
$request_date = new DateTime();
$locale_date = locale_get_default();
$date_formatter = new IntlDateFormatter(
$locale_date,
IntlDateFormatter::FULL,
IntlDateFormatter::FULL
);
$formatted_request_date = $date_formatter->format($request_date);
// set template vars
// subject
$pw_reset_notification['subject'] = str_replace('{{hostname}}', $mailcow_hostname, $pw_reset_notification['subject']);
$pw_reset_notification['subject'] = str_replace('{{link}}', $reset_link, $pw_reset_notification['subject']);
$pw_reset_notification['subject'] = str_replace('{{username}}', $username, $pw_reset_notification['subject']);
$pw_reset_notification['subject'] = str_replace('{{username2}}', $mailbox_attr['recovery_email'], $pw_reset_notification['subject']);
$pw_reset_notification['subject'] = str_replace('{{date}}', $formatted_request_date, $pw_reset_notification['subject']);
$pw_reset_notification['subject'] = str_replace('{{token_lifetime}}', $PW_RESET_TOKEN_LIFETIME, $pw_reset_notification['subject']);
// text
$pw_reset_notification['text_tmpl'] = str_replace('{{hostname}}', $mailcow_hostname, $pw_reset_notification['text_tmpl']);
$pw_reset_notification['text_tmpl'] = str_replace('{{link}}', $reset_link, $pw_reset_notification['text_tmpl']);
$pw_reset_notification['text_tmpl'] = str_replace('{{username}}', $username, $pw_reset_notification['text_tmpl']);
$pw_reset_notification['text_tmpl'] = str_replace('{{username2}}', $mailbox_attr['recovery_email'], $pw_reset_notification['text_tmpl']);
$pw_reset_notification['text_tmpl'] = str_replace('{{date}}', $formatted_request_date, $pw_reset_notification['text_tmpl']);
$pw_reset_notification['text_tmpl'] = str_replace('{{token_lifetime}}', $PW_RESET_TOKEN_LIFETIME, $pw_reset_notification['text_tmpl']);
// html
$pw_reset_notification['html_tmpl'] = str_replace('{{hostname}}', $mailcow_hostname, $pw_reset_notification['html_tmpl']);
$pw_reset_notification['html_tmpl'] = str_replace('{{link}}', $reset_link, $pw_reset_notification['html_tmpl']);
$pw_reset_notification['html_tmpl'] = str_replace('{{username}}', $username, $pw_reset_notification['html_tmpl']);
$pw_reset_notification['html_tmpl'] = str_replace('{{username2}}', $mailbox_attr['recovery_email'], $pw_reset_notification['html_tmpl']);
$pw_reset_notification['html_tmpl'] = str_replace('{{date}}', $formatted_request_date, $pw_reset_notification['html_tmpl']);
$pw_reset_notification['html_tmpl'] = str_replace('{{token_lifetime}}', $PW_RESET_TOKEN_LIFETIME, $pw_reset_notification['html_tmpl']);
$email_sent = reset_password('send_mail', array(
"from" => $pw_reset_notification['from'],
"to" => $mailbox_attr['recovery_email'],
"subject" => $pw_reset_notification['subject'],
"text" => $pw_reset_notification['text_tmpl'],
"html" => $pw_reset_notification['html_tmpl']
));
if (!$email_sent){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => "recovery_email_failed"
);
return false;
}
list($localPart, $domainPart) = explode('@', $mailbox_attr['recovery_email']);
if (strlen($localPart) > 1) {
$maskedLocalPart = $localPart[0] . str_repeat('*', strlen($localPart) - 1);
} else {
$maskedLocalPart = "*";
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => array("recovery_email_sent", $maskedLocalPart . '@' . $domainPart)
);
return array(
"username" => $username,
"issue" => "success"
);
break;
case 'reset':
$token = $data['token'];
$new_password = $data['new_password'];
$new_password2 = $data['new_password2'];
$username = $data['username'];
$check_tfa = $data['check_tfa'];
if (!$username || !$token) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'invalid_reset_token'
);
return false;
}
# check new password
if (!password_check($new_password, $new_password2)) {
return false;
}
if ($check_tfa){
// check for tfa authenticators
$authenticators = get_tfa($username);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
$_SESSION['pending_mailcow_cc_username'] = $username;
$_SESSION['pending_pw_reset_token'] = $token;
$_SESSION['pending_pw_new_password'] = $new_password;
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
$_SESSION['return'][] = array(
'type' => 'info',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => 'awaiting_tfa_confirmation'
);
return false;
}
}
# set new password
$password_hashed = hash_password($new_password);
$stmt = $pdo->prepare("UPDATE `mailbox` SET
`password` = :password_hashed,
`attributes` = JSON_SET(`attributes`, '$.passwd_update', NOW())
WHERE `username` = :username");
$stmt->execute(array(
':password_hashed' => $password_hashed,
':username' => $username
));
// perform cleanup
$stmt = $pdo->prepare("DELETE FROM `reset_password` WHERE `username` = :username;");
$stmt->execute(array(
':username' => $username
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'password_changed_success'
);
return true;
break;
case 'get_notification':
$type = $data;
try {
$settings['from'] = $redis->Get('PW_RESET_FROM');
$settings['subject'] = $redis->Get('PW_RESET_SUBJ');
$settings['html_tmpl'] = $redis->Get('PW_RESET_HTML');
$settings['text_tmpl'] = $redis->Get('PW_RESET_TEXT');
if (empty($settings['html_tmpl']) && empty($settings['text_tmpl'])) {
$settings['html_tmpl'] = file_get_contents("/tpls/pw_reset_html.tpl");
$settings['text_tmpl'] = file_get_contents("/tpls/pw_reset_text.tpl");
}
if ($type != "raw") {
$settings['html_tmpl'] = htmlspecialchars($settings['html_tmpl']);
$settings['text_tmpl'] = htmlspecialchars($settings['text_tmpl']);
}
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => array('redis_error', $e)
);
return false;
}
return $settings;
break;
case 'send_mail':
$from = $data['from'];
$to = $data['to'];
$text = $data['text'];
$html = $data['html'];
$subject = $data['subject'];
if (!filter_var($from, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'from_invalid'
);
return false;
}
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'to_invalid'
);
return false;
}
if (empty($subject)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'subject_empty'
);
return false;
}
if (empty($text)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'text_empty'
);
return false;
}
ini_set('max_execution_time', 0);
ini_set('max_input_time', 0);
$mail = new PHPMailer;
$mail->Timeout = 10;
$mail->SMTPOptions = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true
)
);
$mail->isSMTP();
$mail->Host = 'postfix-mailcow';
$mail->SMTPAuth = false;
$mail->Port = 25;
$mail->setFrom($from);
$mail->Subject = $subject;
$mail->CharSet ="UTF-8";
if (!empty($html)) {
$mail->Body = $html;
$mail->AltBody = $text;
}
else {
$mail->Body = $text;
}
$mail->XMailer = 'MooMail';
$mail->AddAddress($to);
if (!$mail->send()) {
return false;
}
$mail->ClearAllRecipients();
return true;
break;
}
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
switch ($action) {
case 'edit_notification':
$subject = $data['subject'];
$from = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $data['from']);
$from = (!filter_var($from, FILTER_VALIDATE_EMAIL)) ? "" : $from;
$subject = (empty($subject)) ? "" : $subject;
$text = (empty($data['text_tmpl'])) ? "" : $data['text_tmpl'];
$html = (empty($data['html_tmpl'])) ? "" : $data['html_tmpl'];
try {
$redis->Set('PW_RESET_FROM', $from);
$redis->Set('PW_RESET_SUBJ', $subject);
$redis->Set('PW_RESET_HTML', $html);
$redis->Set('PW_RESET_TEXT', $text);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => array('redis_error', $e)
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'saved_settings'
);
break;
}
}
function get_logs($application, $lines = false) { function get_logs($application, $lines = false) {
if ($lines === false) { if ($lines === false) {

View File

@ -184,6 +184,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'msg' => 'global_filter_written' 'msg' => 'global_filter_written'
); );
return true; return true;
break;
case 'filter': case 'filter':
$sieve = new Sieve\SieveParser(); $sieve = new Sieve\SieveParser();
if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) { if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
@ -1249,6 +1250,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0; $_data['quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
$_data['quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0; $_data['quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
$_data['app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0; $_data['app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
$_data['pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0;
} else { } else {
$_data['spam_alias'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_alias']); $_data['spam_alias'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_alias']);
$_data['tls_policy'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_tls_policy']); $_data['tls_policy'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_tls_policy']);
@ -1264,14 +1266,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['quarantine_notification'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_notification']); $_data['quarantine_notification'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_notification']);
$_data['quarantine_category'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_category']); $_data['quarantine_category'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_category']);
$_data['app_passwds'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_app_passwds']); $_data['app_passwds'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_app_passwds']);
$_data['pw_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pw_reset']);
} }
try { try {
$stmt = $pdo->prepare("INSERT INTO `user_acl` $stmt = $pdo->prepare("INSERT INTO `user_acl`
(`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`, (`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`,
`pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`) `pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`, `pw_reset`)
VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset, VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset,
:pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds) "); :pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds, :pw_reset) ");
$stmt->execute(array( $stmt->execute(array(
':username' => $username, ':username' => $username,
':spam_alias' => $_data['spam_alias'], ':spam_alias' => $_data['spam_alias'],
@ -1287,7 +1290,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':quarantine_attachments' => $_data['quarantine_attachments'], ':quarantine_attachments' => $_data['quarantine_attachments'],
':quarantine_notification' => $_data['quarantine_notification'], ':quarantine_notification' => $_data['quarantine_notification'],
':quarantine_category' => $_data['quarantine_category'], ':quarantine_category' => $_data['quarantine_category'],
':app_passwds' => $_data['app_passwds'] ':app_passwds' => $_data['app_passwds'],
':pw_reset' => $_data['pw_reset']
)); ));
} }
catch (PDOException $e) { catch (PDOException $e) {
@ -1576,6 +1580,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['acl_quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0; $attr['acl_quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
$attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0; $attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
$attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0; $attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
$attr['acl_pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0;
} else { } else {
$_data['acl'] = (array)$_data['acl']; $_data['acl'] = (array)$_data['acl'];
$attr['acl_spam_alias'] = 0; $attr['acl_spam_alias'] = 0;
@ -2865,21 +2870,22 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
} }
if (!empty($is_now)) { if (!empty($is_now)) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
(int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']); (int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
(int)$sogo_access = (isset($_data['sogo_access']) && isset($_SESSION['acl']['sogo_access']) && $_SESSION['acl']['sogo_access'] == "1") ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']); (int)$sogo_access = (isset($_data['sogo_access']) && isset($_SESSION['acl']['sogo_access']) && $_SESSION['acl']['sogo_access'] == "1") ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
(int)$imap_access = (isset($_data['imap_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']); (int)$imap_access = (isset($_data['imap_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
(int)$pop3_access = (isset($_data['pop3_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']); (int)$pop3_access = (isset($_data['pop3_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
(int)$smtp_access = (isset($_data['smtp_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']); (int)$smtp_access = (isset($_data['smtp_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
(int)$sieve_access = (isset($_data['sieve_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']); (int)$sieve_access = (isset($_data['sieve_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
(int)$relayhost = (isset($_data['relayhost']) && isset($_SESSION['acl']['mailbox_relayhost']) && $_SESSION['acl']['mailbox_relayhost'] == "1") ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']); (int)$relayhost = (isset($_data['relayhost']) && isset($_SESSION['acl']['mailbox_relayhost']) && $_SESSION['acl']['mailbox_relayhost'] == "1") ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
(int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576); (int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
$name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name']; $name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
$domain = $is_now['domain']; $domain = $is_now['domain'];
$quota_b = $quota_m * 1048576; $quota_b = $quota_m * 1048576;
$password = (!empty($_data['password'])) ? $_data['password'] : null; $password = (!empty($_data['password'])) ? $_data['password'] : null;
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null; $password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
$tags = (is_array($_data['tags']) ? $_data['tags'] : array()); $pw_recovery_email = (isset($_data['pw_recovery_email'])) ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
$tags = (is_array($_data['tags']) ? $_data['tags'] : array());
} }
else { else {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@ -3132,31 +3138,43 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':address' => $username, ':address' => $username,
':active' => $active ':active' => $active
)); ));
$stmt = $pdo->prepare("UPDATE `mailbox` SET try {
`active` = :active, $stmt = $pdo->prepare("UPDATE `mailbox` SET
`name`= :name, `active` = :active,
`quota` = :quota_b, `name`= :name,
`attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update), `quota` = :quota_b,
`attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access), `attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update),
`attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access), `attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
`attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access), `attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
`attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access), `attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access),
`attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost), `attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access) `attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
WHERE `username` = :username"); `attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
$stmt->execute(array( `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email)
':active' => $active, WHERE `username` = :username");
':name' => $name, $stmt->execute(array(
':quota_b' => $quota_b, ':active' => $active,
':force_pw_update' => $force_pw_update, ':name' => $name,
':sogo_access' => $sogo_access, ':quota_b' => $quota_b,
':imap_access' => $imap_access, ':force_pw_update' => $force_pw_update,
':pop3_access' => $pop3_access, ':sogo_access' => $sogo_access,
':sieve_access' => $sieve_access, ':imap_access' => $imap_access,
':smtp_access' => $smtp_access, ':pop3_access' => $pop3_access,
':relayhost' => $relayhost, ':sieve_access' => $sieve_access,
':username' => $username ':smtp_access' => $smtp_access,
)); ':recovery_email' => $pw_recovery_email,
':relayhost' => $relayhost,
':username' => $username
));
}
catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => $e->getMessage()
);
return false;
}
// save tags // save tags
foreach($tags as $index => $tag){ foreach($tags as $index => $tag){
if (empty($tag)) continue; if (empty($tag)) continue;
@ -3263,6 +3281,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['acl_quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0; $attr['acl_quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
$attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0; $attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
$attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0; $attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
$attr['acl_pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0;
} else { } else {
foreach ($is_now as $key => $value){ foreach ($is_now as $key => $value){
$attr[$key] = $is_now[$key]; $attr[$key] = $is_now[$key];
@ -5212,7 +5231,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'msg' => 'Could not move maildir to garbage collector: variables local_part and/or domain empty' 'msg' => 'Could not move maildir to garbage collector: variables local_part and/or domain empty'
); );
} }
if (strtolower(getenv('SKIP_SOLR')) == 'n') { if (strtolower(getenv('SKIP_SOLR')) == 'n' && strtolower(getenv('FLATCURVE_EXPERIMENTAL')) != 'y') {
$curl = curl_init(); $curl = curl_init();
curl_setopt($curl, CURLOPT_URL, 'http://solr:8983/solr/dovecot-fts/update?commit=true'); curl_setopt($curl, CURLOPT_URL, 'http://solr:8983/solr/dovecot-fts/update?commit=true');
curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: text/xml')); curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: text/xml'));

View File

@ -3,7 +3,7 @@ function init_db_schema() {
try { try {
global $pdo; global $pdo;
$db_version = "26022024_1433"; $db_version = "29072024_1000";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@ -483,6 +483,7 @@ function init_db_schema() {
"quarantine_notification" => "TINYINT(1) NOT NULL DEFAULT '1'", "quarantine_notification" => "TINYINT(1) NOT NULL DEFAULT '1'",
"quarantine_category" => "TINYINT(1) NOT NULL DEFAULT '1'", "quarantine_category" => "TINYINT(1) NOT NULL DEFAULT '1'",
"app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'", "app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'",
"pw_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
), ),
"keys" => array( "keys" => array(
"primary" => array( "primary" => array(
@ -694,6 +695,19 @@ function init_db_schema() {
), ),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
), ),
"reset_password" => array(
"cols" => array(
"username" => "VARCHAR(255) NOT NULL",
"token" => "VARCHAR(255) NOT NULL",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
),
"keys" => array(
"primary" => array(
"" => array("token", "created")
),
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"imapsync" => array( "imapsync" => array(
"cols" => array( "cols" => array(
"id" => "INT NOT NULL AUTO_INCREMENT", "id" => "INT NOT NULL AUTO_INCREMENT",

View File

@ -0,0 +1,33 @@
<?xml version='1.0' standalone='yes'?>
<extension name="enotify">
<command name="notify">
<parameter type="tag" name="from" occurrence="optional">
<parameter type="string" name="from-address" />
</parameter>
<parameter type="tag" name="importance" regex="(1|2|3)" occurrence="optional" />
<parameter type="tag" name="options" occurrence="optional">
<parameter type="stringlist" name="option-strings" />
</parameter>
<parameter type="tag" name="message" occurrence="optional">
<parameter type="string" name="message-text" />
</parameter>
<parameter type="string" name="method" />
</command>
<test name="valid_notify_method">
<parameter type="stringlist" name="notification-uris" />
</test>
<test name="notify_method_capability">
<parameter type="string" name="notification-uri" />
<parameter type="string" name="notification-capability" />
<parameter type="stringlist" name="key-list" />
</test>
<modifier name="encodeurl" />
</extension>

View File

@ -0,0 +1,58 @@
<?xml version='1.0' standalone='yes'?>
<extension name="mime">
<command name="foreverypart">
<parameter type="string" name="name" occurrence="optional" />
<block />
</command>
<command name="break">
<parameter type="string" name="name" occurrence="optional" />
</command>
<tagged-argument extends="(header|address|exists)">
<parameter type="tag" name="mime" regex="mime" occurrence="optional" />
</tagged-argument>
<tagged-argument extends="(header|address|exists)">
<parameter type="tag" name="anychild" regex="anychild" occurrence="optional" />
</tagged-argument>
<tagged-argument extends="(header)">
<parameter type="tag" name="type" occurrence="optional" />
</tagged-argument>
<tagged-argument extends="(header)">
<parameter type="tag" name="subtype" occurrence="optional" />
</tagged-argument>
<tagged-argument extends="(header)">
<parameter type="tag" name="contenttype" occurrence="optional" />
</tagged-argument>
<tagged-argument extends="(header)">
<parameter type="tag" name="param" regex="param" occurrence="optional">
<parameter type="stringlist" name="param-list" />
</parameter>
</tagged-argument>
<tagged-argument extends="(header|address|exists)">
<parameter type="stringlist" name="header-names" />
</tagged-argument>
<tagged-argument extends="(header)">
<parameter type="stringlist" name="key-list" />
</tagged-argument>
<action name="replace">
<parameter type="tag" name="mime" regex="mime" occurrence="optional" />
<parameter type="string" name="subject" occurrence="optional" />
<parameter type="string" name="from" occurrence="optional" />
<parameter type="string" name="replacement" />
</action>
<action name="enclose">
<parameter type="string" name="subject" occurrence="optional" />
<parameter type="stringlist" name="headers" occurrence="optional" />
<parameter type="string" name="text" />
</action>
<action name="extracttext">
<parameter type="tag" name="first" regex="first" occurrence="optional" />
<parameter type="number" name="number" occurrence="optional" />
<parameter type="string" name="varname" />
</action>
</extension>

View File

@ -10,16 +10,54 @@ if (!empty($_GET['sso_token'])) {
} }
} }
if (isset($_POST["pw_reset_request"]) && !empty($_POST['username'])) {
reset_password("issue", $_POST['username']);
header("Location: /");
exit;
}
if (isset($_POST["pw_reset"])) {
$username = reset_password("check", $_POST['token']);
$reset_result = reset_password("reset", array(
'new_password' => $_POST['new_password'],
'new_password2' => $_POST['new_password2'],
'token' => $_POST['token'],
'username' => $username,
'check_tfa' => True
));
if ($reset_result){
header("Location: /");
exit;
}
}
if (isset($_POST["verify_tfa_login"])) { if (isset($_POST["verify_tfa_login"])) {
if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) { if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) {
$_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username']; if (isset($_SESSION['pending_mailcow_cc_username']) && isset($_SESSION['pending_pw_reset_token']) && isset($_SESSION['pending_pw_new_password'])) {
$_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role']; reset_password("reset", array(
unset($_SESSION['pending_mailcow_cc_username']); 'new_password' => $_SESSION['pending_pw_new_password'],
unset($_SESSION['pending_mailcow_cc_role']); 'new_password2' => $_SESSION['pending_pw_new_password'],
unset($_SESSION['pending_tfa_methods']); 'token' => $_SESSION['pending_pw_reset_token'],
'username' => $_SESSION['pending_mailcow_cc_username']
));
unset($_SESSION['pending_pw_reset_token']);
unset($_SESSION['pending_pw_new_password']);
unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_tfa_methods']);
header("Location: /user"); header("Location: /");
exit;
} else {
$_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];
$_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role'];
unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_methods']);
header("Location: /user");
}
} else { } else {
unset($_SESSION['pending_pw_reset_token']);
unset($_SESSION['pending_pw_new_password']);
unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_methods']); unset($_SESSION['pending_tfa_methods']);
@ -27,11 +65,13 @@ if (isset($_POST["verify_tfa_login"])) {
} }
if (isset($_GET["cancel_tfa_login"])) { if (isset($_GET["cancel_tfa_login"])) {
unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_pw_reset_token']);
unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_pw_new_password']);
unset($_SESSION['pending_tfa_methods']); unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_methods']);
header("Location: /"); header("Location: /");
} }
if (isset($_POST["quick_release"])) { if (isset($_POST["quick_release"])) {

View File

@ -210,6 +210,12 @@ $MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format'] = 'maildir:';
// Show last IMAP and POP3 logins // Show last IMAP and POP3 logins
$SHOW_LAST_LOGIN = true; $SHOW_LAST_LOGIN = true;
// Maximum number of password reset tokens that can be generated at once per user
$PW_RESET_TOKEN_LIMIT = 3;
// Maximum time in minutes a password reset token is valid
$PW_RESET_TOKEN_LIFETIME = 15;
// UV flag handling in FIDO2/WebAuthn - defaults to false to allow iOS logins // UV flag handling in FIDO2/WebAuthn - defaults to false to allow iOS logins
// true = required // true = required
// false = preferred // false = preferred

View File

@ -380,6 +380,9 @@ $(document).ready(function() {
if (template.acl_app_passwds == 1){ if (template.acl_app_passwds == 1){
acl.push("app_passwds"); acl.push("app_passwds");
} }
if (template.acl_pw_reset == 1){
acl.push("pw_reset");
}
$('#user_acl').selectpicker('val', acl); $('#user_acl').selectpicker('val', acl);
$('#rl_value').val(template.rl_value); $('#rl_value').val(template.rl_value);

View File

@ -1973,7 +1973,6 @@ if (isset($_GET['query'])) {
case "quota_notification_bcc": case "quota_notification_bcc":
process_edit_return(quota_notification_bcc('edit', $attr)); process_edit_return(quota_notification_bcc('edit', $attr));
break; break;
break;
case "mailq": case "mailq":
process_edit_return(mailq('edit', array_merge(array('qid' => $items), $attr))); process_edit_return(mailq('edit', array_merge(array('qid' => $items), $attr)));
break; break;
@ -2069,6 +2068,9 @@ if (isset($_GET['query'])) {
case "cors": case "cors":
process_edit_return(cors('edit', $attr)); process_edit_return(cors('edit', $attr));
break; break;
case "reset-password-notification":
process_edit_return(reset_password('edit_notification', $attr));
break;
// return no route found if no case is matched // return no route found if no case is matched
default: default:
http_response_code(404); http_response_code(404);

View File

@ -14,6 +14,7 @@
"prohibited": "Untersagt durch Richtlinie", "prohibited": "Untersagt durch Richtlinie",
"protocol_access": "Ändern der erlaubten Protokolle", "protocol_access": "Ändern der erlaubten Protokolle",
"pushover": "Pushover", "pushover": "Pushover",
"pw_reset": "Verwalten der E-Mail zur Passwortwiederherstellung erlauben",
"quarantine": "Quarantäne-Aktionen", "quarantine": "Quarantäne-Aktionen",
"quarantine_attachments": "Anhänge aus Quarantäne", "quarantine_attachments": "Anhänge aus Quarantäne",
"quarantine_category": "Ändern der Quarantäne-Benachrichtigungskategorie", "quarantine_category": "Ändern der Quarantäne-Benachrichtigungskategorie",
@ -248,6 +249,11 @@
"password_policy_numbers": "Muss eine Ziffer enthalten", "password_policy_numbers": "Muss eine Ziffer enthalten",
"password_policy_special_chars": "Muss Sonderzeichen enthalten", "password_policy_special_chars": "Muss Sonderzeichen enthalten",
"password_repeat": "Passwort wiederholen", "password_repeat": "Passwort wiederholen",
"password_reset_info": "Wenn keine E-Mail zur Passwortwiederherstellung hinterlegt ist, kann diese Funktion nicht genutzt werden.",
"password_reset_settings": "Einstellungen zur Passwortwiederherstellung",
"password_reset_tmpl_html": "HTML Vorlage",
"password_reset_tmpl_text": "Text Vorlage",
"password_settings": "Passwort Einstellungen",
"priority": "Gewichtung", "priority": "Gewichtung",
"private_key": "Private Key", "private_key": "Private Key",
"quarantine": "Quarantäne", "quarantine": "Quarantäne",
@ -287,6 +293,8 @@
"remove_row": "Entfernen", "remove_row": "Entfernen",
"reset_default": "Zurücksetzen auf Standard", "reset_default": "Zurücksetzen auf Standard",
"reset_limit": "Hash entfernen", "reset_limit": "Hash entfernen",
"reset_password_vars": "<code>{{link}}</code> Der generierte Passwort-Reset-Link<br><code>{{username}}</code> Die E-Mail-Adresse des Benutzers, der die Passwortzurücksetzung angefordert hat<br><code>{{username2}}</code> Die E-Mail-Adresse zur Wiederherstellung<br><code>{{date}}</code> Das Datum, an dem die Passwort-Reset-Anfrage gestellt wurde<br><code>{{token_lifetime}}</code> Die Gültigkeitsdauer des Tokens in Minuten<br><code>{{hostname}}</code> Der mailcow Hostname",
"restore_template": "Leer lassen, um Standard-Template wiederherzustellen.",
"routing": "Routing", "routing": "Routing",
"rsetting_add_rule": "Regel hinzufügen", "rsetting_add_rule": "Regel hinzufügen",
"rsetting_content": "Regelinhalt", "rsetting_content": "Regelinhalt",
@ -407,6 +415,7 @@
"invalid_nexthop_authenticated": "Dieser Next Hop existiert bereits mit abweichenden Authentifizierungsdaten. Die bestehenden Authentifizierungsdaten dieses \"Next Hops\" müssen vorab angepasst werden.", "invalid_nexthop_authenticated": "Dieser Next Hop existiert bereits mit abweichenden Authentifizierungsdaten. Die bestehenden Authentifizierungsdaten dieses \"Next Hops\" müssen vorab angepasst werden.",
"invalid_recipient_map_new": "Neuer Empfänger \"%s\" ist ungültig", "invalid_recipient_map_new": "Neuer Empfänger \"%s\" ist ungültig",
"invalid_recipient_map_old": "Originaler Empfänger \"%s\" ist ungültig", "invalid_recipient_map_old": "Originaler Empfänger \"%s\" ist ungültig",
"invalid_reset_token": "Ungültiger Rücksetz-Token",
"ip_list_empty": "Liste erlaubter IPs darf nicht leer sein", "ip_list_empty": "Liste erlaubter IPs darf nicht leer sein",
"is_alias": "%s lautet bereits eine Alias-Adresse", "is_alias": "%s lautet bereits eine Alias-Adresse",
"is_alias_or_mailbox": "Eine Mailbox, ein Alias oder eine sich aus einer Alias-Domain ergebende Adresse mit dem Namen %s ist bereits vorhanden", "is_alias_or_mailbox": "Eine Mailbox, ein Alias oder eine sich aus einer Alias-Domain ergebende Adresse mit dem Namen %s ist bereits vorhanden",
@ -436,6 +445,8 @@
"password_complexity": "Passwort entspricht nicht den Richtlinien", "password_complexity": "Passwort entspricht nicht den Richtlinien",
"password_empty": "Passwort darf nicht leer sein", "password_empty": "Passwort darf nicht leer sein",
"password_mismatch": "Passwort-Wiederholung stimmt nicht überein", "password_mismatch": "Passwort-Wiederholung stimmt nicht überein",
"password_reset_invalid_user": "Benutzer nicht gefunden oder keine E-Mail-Adresse zur Wiederherstellung eingerichtet",
"password_reset_na": "Die Passwortwiederherstellung ist momentan nicht verfügbar. Bitte wenden Sie sich an Ihren Administrator.",
"policy_list_from_exists": "Ein Eintrag mit diesem Wert existiert bereits", "policy_list_from_exists": "Ein Eintrag mit diesem Wert existiert bereits",
"policy_list_from_invalid": "Eintrag hat ein ungültiges Format", "policy_list_from_invalid": "Eintrag hat ein ungültiges Format",
"private_key_error": "Schlüsselfehler: %s", "private_key_error": "Schlüsselfehler: %s",
@ -444,10 +455,12 @@
"pushover_token": "Pushover Token hat das falsche Format", "pushover_token": "Pushover Token hat das falsche Format",
"quota_not_0_not_numeric": "Speicherplatz muss numerisch und >= 0 sein", "quota_not_0_not_numeric": "Speicherplatz muss numerisch und >= 0 sein",
"recipient_map_entry_exists": "Eine Empfängerumschreibung für Objekt \"%s\" existiert bereits", "recipient_map_entry_exists": "Eine Empfängerumschreibung für Objekt \"%s\" existiert bereits",
"recovery_email_failed": "E-Mail zur Wiederherstellung konnte nicht gesendet werden. Bitte wenden Sie sich an Ihren Administrator.",
"redis_error": "Redis Fehler: %s", "redis_error": "Redis Fehler: %s",
"relayhost_invalid": "Map-Eintrag %s ist ungültig", "relayhost_invalid": "Map-Eintrag %s ist ungültig",
"release_send_failed": "Die Nachricht konnte nicht versendet werden: %s", "release_send_failed": "Die Nachricht konnte nicht versendet werden: %s",
"reset_f2b_regex": "Regex-Filter konnten nicht in vorgegebener Zeit zurückgesetzt werden, bitte erneut versuchen oder die Webseite neu laden.", "reset_f2b_regex": "Regex-Filter konnten nicht in vorgegebener Zeit zurückgesetzt werden, bitte erneut versuchen oder die Webseite neu laden.",
"reset_token_limit_exceeded": "Das Limit für Rücksetz-Tokens wurde überschritten. Bitte versuchen Sie es später erneut.",
"resource_invalid": "Ressourcenname %s ist ungültig", "resource_invalid": "Ressourcenname %s ist ungültig",
"rl_timeframe": "Ratelimit-Zeitraum ist inkorrekt", "rl_timeframe": "Ratelimit-Zeitraum ist inkorrekt",
"rspamd_ui_pw_length": "Rspamd UI-Passwort muss mindestens 6 Zeichen lang sein", "rspamd_ui_pw_length": "Rspamd UI-Passwort muss mindestens 6 Zeichen lang sein",
@ -467,6 +480,7 @@
"tls_policy_map_dest_invalid": "Ziel ist ungültig", "tls_policy_map_dest_invalid": "Ziel ist ungültig",
"tls_policy_map_entry_exists": "Eine TLS-Richtlinie \"%s\" existiert bereits", "tls_policy_map_entry_exists": "Eine TLS-Richtlinie \"%s\" existiert bereits",
"tls_policy_map_parameter_invalid": "Parameter ist ungültig", "tls_policy_map_parameter_invalid": "Parameter ist ungültig",
"to_invalid": "Empfänger darf nicht leer sein",
"totp_verification_failed": "TOTP-Verifizierung fehlgeschlagen", "totp_verification_failed": "TOTP-Verifizierung fehlgeschlagen",
"transport_dest_exists": "Transport-Maps-Ziel \"%s\" existiert bereits", "transport_dest_exists": "Transport-Maps-Ziel \"%s\" existiert bereits",
"webauthn_verification_failed": "WebAuthn-Verifizierung fehlgeschlagen: %s", "webauthn_verification_failed": "WebAuthn-Verifizierung fehlgeschlagen: %s",
@ -638,6 +652,7 @@
"nexthop": "Next Hop", "nexthop": "Next Hop",
"none_inherit": "Keine Auswahl / Erben", "none_inherit": "Keine Auswahl / Erben",
"password": "Passwort", "password": "Passwort",
"password_recovery_email": "E-Mail zur Passwortwiederherstellung",
"password_repeat": "Passwort wiederholen", "password_repeat": "Passwort wiederholen",
"previous": "Vorherige Seite", "previous": "Vorherige Seite",
"private_comment": "Privater Kommentar", "private_comment": "Privater Kommentar",
@ -741,12 +756,19 @@
"session_expires": "Die Sitzung wird in etwa 15 Sekunden beendet." "session_expires": "Die Sitzung wird in etwa 15 Sekunden beendet."
}, },
"login": { "login": {
"back_to_mailcow": "Zurück zu mailcow",
"delayed": "Login wurde zur Sicherheit um %s Sekunde/n verzögert.", "delayed": "Login wurde zur Sicherheit um %s Sekunde/n verzögert.",
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn Login",
"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": "Anmelden",
"mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.", "mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.",
"new_password": "Neues Passwort",
"new_password_confirm": "Neues Passwort bestätigen",
"other_logins": "Key Login", "other_logins": "Key Login",
"password": "Passwort", "password": "Passwort",
"reset_password": "Passwort zurücksetzen",
"request_reset_password": "Passwortänderung anfordern",
"username": "Benutzername" "username": "Benutzername"
}, },
"mailbox": { "mailbox": {
@ -1065,11 +1087,13 @@
"nginx_reloaded": "Nginx wurde neu geladen", "nginx_reloaded": "Nginx wurde neu geladen",
"object_modified": "Änderungen an Objekt %s wurden gespeichert", "object_modified": "Änderungen an Objekt %s wurden gespeichert",
"password_policy_saved": "Passwortrichtlinie wurde erfolgreich gespeichert", "password_policy_saved": "Passwortrichtlinie wurde erfolgreich gespeichert",
"password_changed_success": "Das Passwort wurde erfolgreich geändert",
"pushover_settings_edited": "Pushover-Konfiguration gespeichert, bitte den Zugang im Anschluss verifizieren.", "pushover_settings_edited": "Pushover-Konfiguration gespeichert, bitte den Zugang im Anschluss verifizieren.",
"qlearn_spam": "Nachricht-ID %s wurde als Spam gelernt und gelöscht", "qlearn_spam": "Nachricht-ID %s wurde als Spam gelernt und gelöscht",
"queue_command_success": "Queue-Aufgabe erfolgreich ausgeführt", "queue_command_success": "Queue-Aufgabe erfolgreich ausgeführt",
"recipient_map_entry_deleted": "Empfängerumschreibung mit der ID %s wurde gelöscht", "recipient_map_entry_deleted": "Empfängerumschreibung mit der ID %s wurde gelöscht",
"recipient_map_entry_saved": "Empfängerumschreibung für Objekt \"%s\" wurde gespeichert", "recipient_map_entry_saved": "Empfängerumschreibung für Objekt \"%s\" wurde gespeichert",
"recovery_email_sent": "Wiederherstellungs-E-Mail an %s gesendet",
"relayhost_added": "Map-Eintrag %s wurde hinzugefügt", "relayhost_added": "Map-Eintrag %s wurde hinzugefügt",
"relayhost_removed": "Map-Eintrag %s wurde entfernt", "relayhost_removed": "Map-Eintrag %s wurde entfernt",
"reset_main_logo": "Standardgrafik wurde wiederhergestellt", "reset_main_logo": "Standardgrafik wurde wiederhergestellt",
@ -1202,6 +1226,7 @@
"password": "Passwort", "password": "Passwort",
"password_now": "Aktuelles Passwort (Änderungen bestätigen)", "password_now": "Aktuelles Passwort (Änderungen bestätigen)",
"password_repeat": "Passwort (Wiederholung)", "password_repeat": "Passwort (Wiederholung)",
"password_reset_info": "Wenn keine E-Mail zur Passwortwiederherstellung hinterlegt ist, kann diese Funktion nicht genutzt werden.",
"pushover_evaluate_x_prio": "Hohe Priorität eskalieren [<code>X-Priority: 1</code>]", "pushover_evaluate_x_prio": "Hohe Priorität eskalieren [<code>X-Priority: 1</code>]",
"pushover_info": "Push-Benachrichtungen werden angewendet auf alle nicht-Spam Nachrichten zugestellt an <b>%s</b>, einschließlich Alias-Adressen (shared, non-shared, tagged).", "pushover_info": "Push-Benachrichtungen werden angewendet auf alle nicht-Spam Nachrichten zugestellt an <b>%s</b>, einschließlich Alias-Adressen (shared, non-shared, tagged).",
"pushover_only_x_prio": "Nur Mail mit hoher Priorität berücksichtigen [<code>X-Priority: 1</code>]", "pushover_only_x_prio": "Nur Mail mit hoher Priorität berücksichtigen [<code>X-Priority: 1</code>]",
@ -1211,6 +1236,7 @@
"pushover_title": "Notification Titel", "pushover_title": "Notification Titel",
"pushover_vars": "Wenn kein Sender-Filter definiert ist, werden alle E-Mails berücksichtigt.<br>Die direkte Absenderprüfung und reguläre Ausdrücke werden unabhängig voneinander geprüft, sie <b>hängen nicht voneinander ab</b> und werden der Reihe nach ausgeführt. <br>Verwendbare Variablen für Titel und Text (Datenschutzrichtlinien beachten)", "pushover_vars": "Wenn kein Sender-Filter definiert ist, werden alle E-Mails berücksichtigt.<br>Die direkte Absenderprüfung und reguläre Ausdrücke werden unabhängig voneinander geprüft, sie <b>hängen nicht voneinander ab</b> und werden der Reihe nach ausgeführt. <br>Verwendbare Variablen für Titel und Text (Datenschutzrichtlinien beachten)",
"pushover_verify": "Verbindung verifizieren", "pushover_verify": "Verbindung verifizieren",
"pw_recovery_email": "E-Mail zur Passwortwiederherstellung",
"q_add_header": "Junk-Ordner", "q_add_header": "Junk-Ordner",
"q_all": "Alle Kategorien", "q_all": "Alle Kategorien",
"q_reject": "Abgelehnt", "q_reject": "Abgelehnt",

View File

@ -14,6 +14,7 @@
"prohibited": "Prohibited by ACL", "prohibited": "Prohibited by ACL",
"protocol_access": "Change protocol access", "protocol_access": "Change protocol access",
"pushover": "Pushover", "pushover": "Pushover",
"pw_reset": "Allow to reset mailcow user password",
"quarantine": "Quarantine actions", "quarantine": "Quarantine actions",
"quarantine_attachments": "Quarantine attachments", "quarantine_attachments": "Quarantine attachments",
"quarantine_category": "Change quarantine notification category", "quarantine_category": "Change quarantine notification category",
@ -256,6 +257,11 @@
"password_policy_numbers": "Must contain at least one number", "password_policy_numbers": "Must contain at least one number",
"password_policy_special_chars": "Must contain special characters", "password_policy_special_chars": "Must contain special characters",
"password_repeat": "Confirmation password (repeat)", "password_repeat": "Confirmation password (repeat)",
"password_reset_info": "If no recovery email is provided, this function cannot be used.",
"password_reset_settings": "Password Recovery Settings",
"password_reset_tmpl_html": "HTML Template",
"password_reset_tmpl_text": "Text Template",
"password_settings": "Password Settings",
"priority": "Priority", "priority": "Priority",
"private_key": "Private key", "private_key": "Private key",
"quarantine": "Quarantine", "quarantine": "Quarantine",
@ -296,6 +302,8 @@
"remove_row": "Remove row", "remove_row": "Remove row",
"reset_default": "Reset to default", "reset_default": "Reset to default",
"reset_limit": "Remove hash", "reset_limit": "Remove hash",
"reset_password_vars": "<code>{{link}}</code> The generated password reset link<br><code>{{username}}</code> The mailbox name of the user who requested the password reset<br><code>{{username2}}</code> The recovery mailbox name<br><code>{{date}}</code> The date the password reset request was made<br><code>{{token_lifetime}}</code> The token lifetime in minutes<br><code>{{hostname}}</code> The mailcow hostname",
"restore_template": "Leave empty to restore default template.",
"routing": "Routing", "routing": "Routing",
"rsetting_add_rule": "Add rule", "rsetting_add_rule": "Add rule",
"rsetting_content": "Rule content", "rsetting_content": "Rule content",
@ -407,6 +415,7 @@
"invalid_nexthop_authenticated": "Next hop exists with different credentials, please update the existing credentials for this next hop first.", "invalid_nexthop_authenticated": "Next hop exists with different credentials, please update the existing credentials for this next hop first.",
"invalid_recipient_map_new": "Invalid new recipient specified: %s", "invalid_recipient_map_new": "Invalid new recipient specified: %s",
"invalid_recipient_map_old": "Invalid original recipient specified: %s", "invalid_recipient_map_old": "Invalid original recipient specified: %s",
"invalid_reset_token": "Invalid reset token",
"ip_list_empty": "List of allowed IPs cannot be empty", "ip_list_empty": "List of allowed IPs cannot be empty",
"is_alias": "%s is already known as an alias address", "is_alias": "%s is already known as an alias address",
"is_alias_or_mailbox": "%s is already known as an alias, a mailbox or an alias address expanded from an alias domain.", "is_alias_or_mailbox": "%s is already known as an alias, a mailbox or an alias address expanded from an alias domain.",
@ -436,6 +445,8 @@
"password_complexity": "Password does not meet the policy", "password_complexity": "Password does not meet the policy",
"password_empty": "Password must not be empty", "password_empty": "Password must not be empty",
"password_mismatch": "Confirmation password does not match", "password_mismatch": "Confirmation password does not match",
"password_reset_invalid_user": "Mailbox not found or no recovery email is set",
"password_reset_na": "The password recovery is currently unavailable. Please contact your administrator.",
"policy_list_from_exists": "A record with given name exists", "policy_list_from_exists": "A record with given name exists",
"policy_list_from_invalid": "Record has invalid format", "policy_list_from_invalid": "Record has invalid format",
"private_key_error": "Private key error: %s", "private_key_error": "Private key error: %s",
@ -444,10 +455,12 @@
"pushover_token": "Pushover token has a wrong format", "pushover_token": "Pushover token has a wrong format",
"quota_not_0_not_numeric": "Quota must be numeric and >= 0", "quota_not_0_not_numeric": "Quota must be numeric and >= 0",
"recipient_map_entry_exists": "A Recipient map entry \"%s\" exists", "recipient_map_entry_exists": "A Recipient map entry \"%s\" exists",
"recovery_email_failed": "Could not send a recovery email. Please contact your administrator.",
"redis_error": "Redis error: %s", "redis_error": "Redis error: %s",
"relayhost_invalid": "Map entry %s is invalid", "relayhost_invalid": "Map entry %s is invalid",
"release_send_failed": "Message could not be released: %s", "release_send_failed": "Message could not be released: %s",
"reset_f2b_regex": "Regex filter could not be reset in time, please try again or wait a few more seconds and reload the website.", "reset_f2b_regex": "Regex filter could not be reset in time, please try again or wait a few more seconds and reload the website.",
"reset_token_limit_exceeded": "Reset token limit has been exceeded. Please try again later.",
"resource_invalid": "Resource name %s is invalid", "resource_invalid": "Resource name %s is invalid",
"rl_timeframe": "Rate limit time frame is incorrect", "rl_timeframe": "Rate limit time frame is incorrect",
"rspamd_ui_pw_length": "Rspamd UI password should be at least 6 chars long", "rspamd_ui_pw_length": "Rspamd UI password should be at least 6 chars long",
@ -470,6 +483,7 @@
"tls_policy_map_dest_invalid": "Policy destination is invalid", "tls_policy_map_dest_invalid": "Policy destination is invalid",
"tls_policy_map_entry_exists": "A TLS policy map entry \"%s\" exists", "tls_policy_map_entry_exists": "A TLS policy map entry \"%s\" exists",
"tls_policy_map_parameter_invalid": "Policy parameter is invalid", "tls_policy_map_parameter_invalid": "Policy parameter is invalid",
"to_invalid": "Recipient must not be empty",
"totp_verification_failed": "TOTP verification failed", "totp_verification_failed": "TOTP verification failed",
"transport_dest_exists": "Transport destination \"%s\" exists", "transport_dest_exists": "Transport destination \"%s\" exists",
"webauthn_verification_failed": "WebAuthn verification failed: %s", "webauthn_verification_failed": "WebAuthn verification failed: %s",
@ -638,6 +652,7 @@
"none_inherit": "None / Inherit", "none_inherit": "None / Inherit",
"nexthop": "Next hop", "nexthop": "Next hop",
"password": "Password", "password": "Password",
"password_recovery_email": "Password recovery email",
"password_repeat": "Confirmation password (repeat)", "password_repeat": "Confirmation password (repeat)",
"previous": "Previous page", "previous": "Previous page",
"private_comment": "Private comment", "private_comment": "Private comment",
@ -741,12 +756,19 @@
"session_expires": "Your session will expire in about 15 seconds" "session_expires": "Your session will expire in about 15 seconds"
}, },
"login": { "login": {
"back_to_mailcow": "Back to mailcow",
"delayed": "Login was delayed by %s seconds.", "delayed": "Login was delayed by %s seconds.",
"fido2_webauthn": "FIDO2/WebAuthn Login", "fido2_webauthn": "FIDO2/WebAuthn Login",
"forgot_password": "> Forgot Password?",
"invalid_pass_reset_token": "The reset password token is invalid or has expired.<br>Please request a new password reset link.",
"login": "Login", "login": "Login",
"mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.", "mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.",
"new_password": "New Password",
"new_password_confirm": "Confirm new password",
"other_logins": "Key login", "other_logins": "Key login",
"password": "Password", "password": "Password",
"reset_password": "Reset Password",
"request_reset_password": "Request password change",
"username": "Username" "username": "Username"
}, },
"mailbox": { "mailbox": {
@ -1072,11 +1094,13 @@
"nginx_reloaded": "Nginx was reloaded", "nginx_reloaded": "Nginx was reloaded",
"object_modified": "Changes to object %s have been saved", "object_modified": "Changes to object %s have been saved",
"password_policy_saved": "Password policy was saved successfully", "password_policy_saved": "Password policy was saved successfully",
"password_changed_success": "Password was successfully changed",
"pushover_settings_edited": "Pushover settings successfully set, please verify credentials.", "pushover_settings_edited": "Pushover settings successfully set, please verify credentials.",
"qlearn_spam": "Message ID %s was learned as spam and deleted", "qlearn_spam": "Message ID %s was learned as spam and deleted",
"queue_command_success": "Queue command completed successfully", "queue_command_success": "Queue command completed successfully",
"recipient_map_entry_deleted": "Recipient map ID %s has been deleted", "recipient_map_entry_deleted": "Recipient map ID %s has been deleted",
"recipient_map_entry_saved": "Recipient map entry \"%s\" has been saved", "recipient_map_entry_saved": "Recipient map entry \"%s\" has been saved",
"recovery_email_sent": "Recovery email sent to %s",
"relayhost_added": "Map entry %s has been added", "relayhost_added": "Map entry %s has been added",
"relayhost_removed": "Map entry %s has been removed", "relayhost_removed": "Map entry %s has been removed",
"reset_main_logo": "Reset to default logo", "reset_main_logo": "Reset to default logo",
@ -1210,6 +1234,7 @@
"password": "Password", "password": "Password",
"password_now": "Current password (confirm changes)", "password_now": "Current password (confirm changes)",
"password_repeat": "Password (repeat)", "password_repeat": "Password (repeat)",
"password_reset_info": "If no email for password recovery is provided, this function cannot be used.",
"pushover_evaluate_x_prio": "Escalate high priority mail [<code>X-Priority: 1</code>]", "pushover_evaluate_x_prio": "Escalate high priority mail [<code>X-Priority: 1</code>]",
"pushover_info": "Push notification settings will apply to all clean (non-spam) mail delivered to <b>%s</b> including aliases (shared, non-shared, tagged).", "pushover_info": "Push notification settings will apply to all clean (non-spam) mail delivered to <b>%s</b> including aliases (shared, non-shared, tagged).",
"pushover_only_x_prio": "Only consider high priority mail [<code>X-Priority: 1</code>]", "pushover_only_x_prio": "Only consider high priority mail [<code>X-Priority: 1</code>]",
@ -1220,6 +1245,7 @@
"pushover_sound": "Sound", "pushover_sound": "Sound",
"pushover_vars": "When no sender filter is defined, all mails will be considered.<br>Regex filters as well as exact sender checks can be defined individually and will be considered sequentially. They do not depend on each other.<br>Useable variables for text and title (please take note of data protection policies)", "pushover_vars": "When no sender filter is defined, all mails will be considered.<br>Regex filters as well as exact sender checks can be defined individually and will be considered sequentially. They do not depend on each other.<br>Useable variables for text and title (please take note of data protection policies)",
"pushover_verify": "Verify credentials", "pushover_verify": "Verify credentials",
"pw_recovery_email": "Password recovery email",
"q_add_header": "Junk folder", "q_add_header": "Junk folder",
"q_all": "All categories", "q_all": "All categories",
"q_reject": "Rejected", "q_reject": "Rejected",

View File

@ -322,5 +322,148 @@
"invalid_nexthop": "\"Next hop\"-format er ugyldig", "invalid_nexthop": "\"Next hop\"-format er ugyldig",
"img_dimensions_exceeded": "Bildet overskriver maksimal bildestørrelse", "img_dimensions_exceeded": "Bildet overskriver maksimal bildestørrelse",
"img_size_exceeded": "Bildet overskrider maksimal filstørrelse" "img_size_exceeded": "Bildet overskrider maksimal filstørrelse"
},
"debug": {
"logs": "Logger",
"update_available": "En oppdatering er tilgjengelig",
"service": "Tjeneste",
"show_ip": "Vis offentlig IP",
"solr_dead": "Solr starter, er deaktivert eller døde",
"memory": "Minne",
"online_users": "Tilkoblede brukere",
"restart_container": "Omstart",
"size": "Størrelse",
"solr_status": "Solr-status",
"started_at": "Startet ved",
"started_on": "Startet den",
"static_logs": "Statiske logger",
"success": "Suksess",
"system_containers": "System og kontainere",
"timezone": "Tidssone",
"uptime": "Oppetid",
"no_update_available": "Systemet kjører siste versjon",
"update_failed": "Kunne ikke se etter oppdateringer",
"username": "Brukernavn",
"wip": "Foreløpig under utvikling"
},
"diagnostics": {
"dns_records_24hours": "Vennligst vær oppmerksom på at endringer gjort i DNS kan ta opp til 24 timer før riktig status vises på denne siden. Den er ment som en måte for deg å enkelt se hvordan du kan sette opp DNS-oppføringene dine og se at alle oppføringer er korrekt lagret i DNS.",
"cname_from_a": "Verdi hentet fra A/AAAA-oppføring. Dette er støttet så lenge oppføringen peker til riktig ressurs.",
"dns_records_docs": "Vennligst også se <a target=\"_blank\" href=\"https://docs.mailcow.email/getstarted/prerequisite-dns\">dokumentasjonen</a>.",
"dns_records": "DNS-oppføringer",
"dns_records_data": "Korrekte data",
"dns_records_name": "Navn",
"dns_records_status": "Nåværende status",
"dns_records_type": "Type",
"optional": "Denne oppføringen er valgfri."
},
"edit": {
"bcc_dest_format": "BCC-destinasjon må være en enkelt, gyldig epostadresse.<br>Hvis du trenger å sende en kopi til flere adresser, opprett et alias og bruk det her.",
"pushover_info": "Innstillinger for pushvarslinger vil gjelde alle rene (ikke-spam) eposter levert til <b>%s</b>, inkludert aliaser (delte, ikke-delte, taggede).",
"relay_transport_info": "<div class=\"badge fs-6 bg-info\">Info</div> Du kan definere transportmappinger for et spesifikt mål for dette domenet. Hvis dette ikke er definert, blir det gjort et MX-oppslag.",
"delete2duplicates": "Slett duplikater på målvert",
"description": "Beskrivelse",
"disable_login": "Ikke tillat innlogging (innkommende epost blir likevel mottatt)",
"domain_admin": "Endre domeneadministrator",
"max_mailboxes": "Maks antall mailbokser",
"quota_warning_bcc_info": "Advarsler blir sendt som separate kopier til de følgende mottakerne. Emnet vil få lagt til det korresponderende brukernavnet i parantes, for eksempel: <code>Kvotevarsel (user@example.com)</code>.",
"domain_footer_skip_replies": "Ignorer bunntekst ved svar på epost",
"extended_sender_acl": "Eksterne avsenderadresser",
"extended_sender_acl_info": "En DKIM-domenenøkkel bør importeres, hvis tilgjengelig.<br>\n Husk å legge denne serveren til den korresponderende SPF TXT-oppføringen.<br>\n Når et domene eller aliasdomene legges til på denne serveren, og det overlapper med en ekstern adresse, blir den eksterne adressen fjernet.<br>\n Bruk @domain.tld for å tillate sending som *@domain.tld.",
"password_repeat": "Bekreft passord (gjenta)",
"pushover_title": "Varslingstittel",
"pushover_vars": "Når et avsenderfilter ikke er definert, vil alle epostere bli vurdert.<br>Regex-filtre såvel så eksakte avsendersjekker kan bli individuelt definert og vil bli vurdert sekvensielt. De er ikke avhengige av hverandre.<br>Tilgjengelige variabler for tekst og tittel (vennligst observer policyer for databeskyttelse)",
"admin": "Endre administrator",
"domain_footer_info": "Bunntekst for hele domenet leggese til alle utgående eposter assosiert med en adresse innenfor dette domenet. <br> De følgende variablene kan brukes for bunnteksten:",
"domain_footer_info_vars": {
"custom": "{= foo =} - Hvis mailboksen har en spesialattributt \"foo\" med verdi \"bar\", vil den vise \"bar\"",
"auth_user": "{= auth_user =} - Autentisert brukernavn spesifisert av en MTA",
"from_user": "{= from_user =} - Fra brukerdelen av konvolutten, f.eks. for \"moo@mailcow.tld\" vil den returnere \"moo\"",
"from_name": "{= from_name =} - Fra-navn fra konvolutten, f.eks. for \"Mailcow &lt;moo@mailcow.tld&gt;\" vil den vise \"Mailcow\"",
"from_addr": "{= from_addr =} - Fra adressedelen av konvolutten",
"from_domain": "{= from_domain =} - Fra domene-delen av konvolutten"
},
"mailbox_relayhost_info": "Aktiveres kun for mailboksen og direkte aliaser, overstyrer domene-videresendingsvert.",
"mbox_rl_info": "Denne begrensningen gjelder for SASL-innloggingsnavnet, dersom det er likt noen \"from\"-adresser benyttet av den innloggede brukeren. En mailboks-begrensning overstyrer en domene-begrensning.",
"allow_from_smtp_info": "La stå tom for å tillate alle avsendere.<br>IPv4/IPv6-adresser og -nettverk.",
"domain": "Endre domene",
"encryption": "Kryptering",
"exclude": "Ekskluder objekter (regex)",
"footer_exclude": "Ekskluder fra bunntekst",
"gal_info": "GAL inneholder alle objeker i et domene og kan ikke redigeres av noen brukere. Ledig/opptatt-informasjon i SOGo mangler dersom den deaktiveres! <b>Start SOGo på nytt for å aktivere endringene.</b>",
"grant_types": "Grant-typer",
"hostname": "Vertsnavn",
"inactive": "Inaktiv",
"kind": "Type",
"last_modified": "Sist endret",
"lookup_mx": "Målet er et regex-uttrykk for å matche mot MX_navnet (<code>*\\.google\\.com</code> for å route all epost som skal til en MX-server som slutter på google.com, via dette målet)",
"mailbox": "Endre mailboks",
"mailbox_quota_def": "Standardkvote for mailboks",
"max_quota": "Maks kvote pr. mailboks (MiB)",
"maxage": "Maksimal alder for meldinger, i dager, som vil hentes fra ekstern<br><small>(0 = ignorer alder)</small>",
"maxbytespersecond": "Maks bytes pr. sekund <br><small>(0 = ubegrenset)</small>",
"pushover_sender_array": "Bare vurder de følgende avsenderadressene <small>(komma-separert)</small>",
"pushover_sender_regex": "Vurder følgende avsender-regex",
"pushover_text": "Varslingstekst",
"pushover_sound": "Lyd",
"pushover_verify": "Bekreft identifikasjon",
"quota_mb": "Kvote (MiB)",
"quota_warning_bcc": "Kvotevarsling BCC",
"ratelimit": "Mengdebegrensning",
"domain_footer": "Bunntekst for hele domenet",
"private_comment": "Privat kommentar",
"public_comment": "Offentlig kommentar",
"client_id": "Klient-ID",
"full_name": "Fullt navn",
"gal": "Global adresseliste",
"max_aliases": "Maks. antall aliaser",
"mins_interval": "Intervall (min)",
"multiple_bookings": "Flere bookinger",
"none_inherit": "Ingen / arve",
"acl": "ACL (rettighet)",
"active": "Aktiv",
"advanced_settings": "Avanserte innstillinger",
"alias": "Endre alias",
"allow_from_smtp": "Tillat kun disse IPene å bruke <b>SMTP</b>",
"allowed_protocols": "Tillatte protokoller",
"app_name": "Appnavn",
"app_passwd": "App-passord",
"app_passwd_protocols": "Tillatte protokoller for app-passord",
"automap": "Prøv å automatisk mappe opp mapper (\"Sent items\", \"Sent\" => \"Sendt\" etc.)",
"backup_mx_options": "Videresendingsalternativer",
"client_secret": "Klient-hemmelighet",
"comment_info": "En privat kommentar er ikke synlig for brukeren, mens en offentlig kommentar vises som et tooltip når man holder muspekeren over det",
"created_on": "Opprettet den",
"custom_attributes": "Valgfrie attributter",
"delete1": "Slett fra kilde når fullført",
"delete2": "Slett meldinger på målvert som ikke finnes på kildeverten",
"delete_ays": "Vennligst bekreft slettingen.",
"domain_footer_html": "HTML-bunntekst",
"domain_footer_plain": "PLAIN-bunntekst",
"domain_quota": "Domenekvote",
"domains": "Domener",
"dont_check_sender_acl": "Deaktivere sendersjekk for domene %s (+ aliasdomener)",
"edit_alias_domain": "Endre aliasdomene",
"force_pw_update": "Tving endring av passord ved neste innlogging",
"force_pw_update_info": "Denne brukeren vil bare kunne logge inn på %s. App-passord kan fremdeles brukes.",
"generate": "generer",
"nexthop": "Neste hopp",
"password": "Passord",
"previous": "Forrige side",
"pushover": "Pushover",
"pushover_evaluate_x_prio": "Eskaler mail med høy prioritet [<code>X-Priority: 1</code>]",
"pushover_only_x_prio": "Bare vurder epost med høy prioritet [<code>X-Priority: 1</code>]",
"redirect_uri": "Omdirigerings-/tilbakekallings-URL",
"relay_all": "Videresend alle mottakere",
"relay_all_info": "↪ Hvis du velger å <b>ikke</b> videresende alle mottakkere, så må du legge til en (\"blind\") mailboks for hver eneste mottaker det skal videresendes for.",
"relay_domain": "Videresend dette domenet",
"relay_unknown_only": "Videresend kun ikke-eksisterende mailbokser. Eksisterende mailbokser vil bli levert lokalt.",
"relayhost": "Avsender-avhengige transportmetoder",
"remove": "Fjern",
"resource": "Ressurs",
"save": "Lagre endringer",
"scope": "Omfang",
"sender_acl": "Tillat å sende som",
"sender_acl_disabled": "<span class=\"badge fs-6 bg-danger\">Avsender-sjekk er deaktivert</span>"
} }
} }

View File

@ -2,7 +2,7 @@
"acl": { "acl": {
"alias_domains": "Создание псевдонимов домена", "alias_domains": "Создание псевдонимов домена",
"app_passwds": "Пароли приложений", "app_passwds": "Пароли приложений",
"bcc_maps": "Правила BBC", "bcc_maps": "Правила BCC",
"delimiter_action": "Обработка тегированной почты", "delimiter_action": "Обработка тегированной почты",
"domain_desc": "Изменение описания домена", "domain_desc": "Изменение описания домена",
"domain_relayhost": "Изменение промежуточных узлов для домена", "domain_relayhost": "Изменение промежуточных узлов для домена",
@ -389,7 +389,7 @@
"imagick_exception": "Ошибка в Imagick при чтении изображения", "imagick_exception": "Ошибка в Imagick при чтении изображения",
"img_invalid": "Невозможно проверить файл изображения", "img_invalid": "Невозможно проверить файл изображения",
"img_tmp_missing": "Невозможно проверить файл изображения: временный файл не найден", "img_tmp_missing": "Невозможно проверить файл изображения: временный файл не найден",
"invalid_bcc_map_type": "Неверный тип правила BBC", "invalid_bcc_map_type": "Неверный тип правила BCC",
"invalid_destination": "Назначение \"%s\" указано неверно", "invalid_destination": "Назначение \"%s\" указано неверно",
"invalid_filter_type": "Неверный тип фильтра", "invalid_filter_type": "Неверный тип фильтра",
"invalid_host": "Хост %s указан неверно", "invalid_host": "Хост %s указан неверно",
@ -523,7 +523,7 @@
"app_passwd": "Пароль приложения", "app_passwd": "Пароль приложения",
"automap": "Автоматическое слияние папок (\"Sent items\", \"Sent\" => \"Sent\" etc.)", "automap": "Автоматическое слияние папок (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
"backup_mx_options": "Параметры резервного копирования MX", "backup_mx_options": "Параметры резервного копирования MX",
"bcc_dest_format": "Назначением для правила BBC должен быть единственный действительный адрес электронной почты.", "bcc_dest_format": "Назначением для правила BCC должен быть единственный действительный адрес электронной почты.",
"client_id": "ID клиента", "client_id": "ID клиента",
"client_secret": "Секретный ключ пользователя", "client_secret": "Секретный ключ пользователя",
"comment_info": "Приватный комментарий не виден пользователям, а публичный - отображается рядом с псевдонимом в личном кабинете пользователя", "comment_info": "Приватный комментарий не виден пользователям, а публичный - отображается рядом с псевдонимом в личном кабинете пользователя",
@ -700,7 +700,7 @@
"add": "Добавить", "add": "Добавить",
"add_alias": "Добавить псевдоним", "add_alias": "Добавить псевдоним",
"add_alias_expand": "Скопировать псевдонимы на псевдонимы домена", "add_alias_expand": "Скопировать псевдонимы на псевдонимы домена",
"add_bcc_entry": "Добавить правило BBC", "add_bcc_entry": "Добавить правило BCC",
"add_domain": "Добавить домен", "add_domain": "Добавить домен",
"add_domain_alias": "Добавить псевдоним домена", "add_domain_alias": "Добавить псевдоним домена",
"add_domain_record_first": "Пожалуйста, сначала добавьте домен", "add_domain_record_first": "Пожалуйста, сначала добавьте домен",
@ -719,14 +719,14 @@
"allow_from_smtp_info": "Укажите IPv4/IPv6 адреса и/или подсети.<br>Оставьте поле пустым, чтобы разрешить отправку с любых адресов.", "allow_from_smtp_info": "Укажите IPv4/IPv6 адреса и/или подсети.<br>Оставьте поле пустым, чтобы разрешить отправку с любых адресов.",
"allowed_protocols": "Разрешенные протоколы", "allowed_protocols": "Разрешенные протоколы",
"backup_mx": "Резервный MX", "backup_mx": "Резервный MX",
"bcc": "Правила BBC", "bcc": "Правила BCC",
"bcc_destination": "Назначение BCC", "bcc_destination": "Назначение BCC",
"bcc_destinations": "Назначение BCC", "bcc_destinations": "Назначение BCC",
"bcc_info": "Правила BCC используются для скрытой пересылки копий всех сообщений на другой адрес. Правило типа \"получатель\" используется, когда локальный получатель выступает в качестве получателя почты. Правило типа \"отправитель\" соответствуют тому же принципу. Локальный домен не будут проинформированы о неудачной доставке.", "bcc_info": "Правила BCC используются для скрытой пересылки копий всех сообщений на другой адрес. Правило типа \"получатель\" используется, когда локальный получатель выступает в качестве получателя почты. Правило типа \"отправитель\" соответствуют тому же принципу. Локальный домен не будут проинформированы о неудачной доставке.",
"bcc_local_dest": "Локальный домен", "bcc_local_dest": "Локальный домен",
"bcc_map": "Правила BBC", "bcc_map": "Правила BCC",
"bcc_map_type": "Тип BCC", "bcc_map_type": "Тип BCC",
"bcc_maps": "Правила BBC", "bcc_maps": "Правила BCC",
"bcc_rcpt_map": "Получатель", "bcc_rcpt_map": "Получатель",
"bcc_sender_map": "Отправитель", "bcc_sender_map": "Отправитель",
"bcc_to_rcpt": "Переключиться на тип \"получатель\"", "bcc_to_rcpt": "Переключиться на тип \"получатель\"",

View File

@ -107,7 +107,8 @@
"post_domain_add": "SOGo container \"sogo-mailcow\" mora biti ponovno zagnan po dodajanju nove domene!<br><br>Dodatno se mora preveriti DNS konfiguracija domene. Ko je DNS konfiguracija domene odobrena, ponovno zaženite \"acme-mailcow\" za samodejno generiranje certifikatov za novo domeno (autoconfig.&lt;domain&gt;, autodiscover.&lt;domain&gt;).<br>Ta korak je opcijski in se ponovno poskuša vsakih 24 ur.", "post_domain_add": "SOGo container \"sogo-mailcow\" mora biti ponovno zagnan po dodajanju nove domene!<br><br>Dodatno se mora preveriti DNS konfiguracija domene. Ko je DNS konfiguracija domene odobrena, ponovno zaženite \"acme-mailcow\" za samodejno generiranje certifikatov za novo domeno (autoconfig.&lt;domain&gt;, autodiscover.&lt;domain&gt;).<br>Ta korak je opcijski in se ponovno poskuša vsakih 24 ur.",
"relay_transport_info": "<div class=\"badge fs-6 bg-info\">Info</div> Definirate lahko preslikave transportov za cilj po meri za to domeno. Če ni nastavljena, se ustvari MX poizvedba.", "relay_transport_info": "<div class=\"badge fs-6 bg-info\">Info</div> Definirate lahko preslikave transportov za cilj po meri za to domeno. Če ni nastavljena, se ustvari MX poizvedba.",
"syncjob_hint": "Pozor! Gesla se morajo shraniti v plain-text!", "syncjob_hint": "Pozor! Gesla se morajo shraniti v plain-text!",
"timeout2": "Časovna omejitev za povezavo do lokalnega gostitelja" "timeout2": "Časovna omejitev za povezavo do lokalnega gostitelja",
"dry": "Simuliraj sinhronizacijo"
}, },
"admin": { "admin": {
"access": "Dostop", "access": "Dostop",
@ -347,7 +348,10 @@
"logo_dark_label": "Za temni način", "logo_dark_label": "Za temni način",
"cors_settings": "Nastavitve CORS", "cors_settings": "Nastavitve CORS",
"allowed_methods": "Dovoljene metode za upravljanje dostopa", "allowed_methods": "Dovoljene metode za upravljanje dostopa",
"allowed_origins": "Upravljanje-dostopa-Dovoljeni-Viri" "allowed_origins": "Upravljanje-dostopa-Dovoljeni-Viri",
"copy_to_clipboard": "Besedilo kopirano v odložišče!",
"f2b_manage_external": "Zunanje upravljanje Fail2Ban",
"f2b_manage_external_info": "Fail2ban bo še vedno vzdrževal seznam prepovedi, vendar ne bo aktivno nastavil pravil za blokiranje prometa. Uporabite spodnji ustvarjeni seznam prepovedi za zunanje blokiranje prometa."
}, },
"danger": { "danger": {
"alias_goto_identical": "Alias in goto naslov morata biti identična", "alias_goto_identical": "Alias in goto naslov morata biti identična",
@ -476,7 +480,9 @@
"temp_error": "Začasna napaka", "temp_error": "Začasna napaka",
"cors_invalid_method": "Navedena neveljavna Allow metoda", "cors_invalid_method": "Navedena neveljavna Allow metoda",
"cors_invalid_origin": "Naveden neveljaven Allow-Origin", "cors_invalid_origin": "Naveden neveljaven Allow-Origin",
"invalid_recipient_map_new": "Naveden neveljaven nov prejemnik: %s" "invalid_recipient_map_new": "Naveden neveljaven nov prejemnik: %s",
"img_dimensions_exceeded": "Slika presega največje dovoljene dimenzije",
"img_size_exceeded": "Slika presega največjo dovoljeno velikost datoteke"
}, },
"debug": { "debug": {
"containers_info": "Informacije o vsebniku (containerju)", "containers_info": "Informacije o vsebniku (containerju)",
@ -511,7 +517,13 @@
"no_update_available": "Sistem je na najnovejši verziji", "no_update_available": "Sistem je na najnovejši verziji",
"update_failed": "Ni mogoče preveriti za posodobitve", "update_failed": "Ni mogoče preveriti za posodobitve",
"username": "Uporabniško ime", "username": "Uporabniško ime",
"wip": "Trenutno v delu" "wip": "Trenutno v delu",
"log_info": "<p>mailcow <b>in-memory dnevniki</b> se zbirajo v Redis seznamih in se vsako minuto omejijo na LOG_LINES (%d) da se zmanjša obremenitev.\n <br>In-memory dnevniki niso namenjeni trajnemu shranjevanju. Vse aplikacije, ki beležijo dnevnike in-memory, tudi beležijo v Docker daemon in posledično v privzeti gonilnik za dnevnik.\n <br>In-memory dnevniki se naj uporabljajo za odpravljanje manjših napak s containerji.</p>\n <p><b>Eksterni dnevniki</b> se zbirajo preko API-ja posamezne aplikacije.</p>\n <p><b>Statični dnevniki</b> so večinoma dnevniki aktivnosti, ki se ne beležijo v Dockerd, a jih je vseeno treba hraniti (razen API dnevnikov).</p>",
"login_time": "Čas",
"logs": "Dnevniki",
"memory": "Spomin",
"online_users": "Prijavljeni uporabniki",
"restart_container": "Ponovno zaženi"
}, },
"datatables": { "datatables": {
"infoFiltered": "(filtrirano od _MAX_ skupaj zapisov)", "infoFiltered": "(filtrirano od _MAX_ skupaj zapisov)",
@ -551,6 +563,98 @@
}, },
"edit": { "edit": {
"acl": "ACL (Dovoljenje)", "acl": "ACL (Dovoljenje)",
"active": "Aktivno" "active": "Aktivno",
"allow_from_smtp": "Dovoli samo tem IP naslovom da uporabijo <b>SMTP</b>",
"bcc_dest_format": "Cilj BCC mora biti en veljaven email naslov.<br>Če morate poslati kopijo na več naslovov, ustvarite alias in ga uporabite tukaj.",
"automap": "Poskušaj samodejno preslikati mape (\"Sent items\", \"Sent\" => \"Poslano\" ipd.)",
"admin": "Uredi skrbnika",
"domain_footer_info_vars": {
"custom": "{= foo =} - Če ima poštni predal atribut po meri \"foo\" z vrednostjo \"bar\", spremenljivka vrne \"bar\"",
"auth_user": "{= auth_user =} - Prijavljeno uporabniško ime, ki ga določi MTA",
"from_user": "{= from_user =} - leva stran email naslova uporabnika, npr. za \"moo@mailcow.tld\" vrne \"moo\"",
"from_name": "{= from_name =} - Prikazno ime, npr. za \"Mailcow &lt;moo@mailcow.tld&gt;\" vrne \"Mailcow\"",
"from_addr": "{= from_addr =} - e-poštni naslov \"Od\"",
"from_domain": "{= from_domain =} - domena e-poštnega naslova \"Od\""
},
"dont_check_sender_acl": "Onemogoči kontrolo pošiljatelja za domeno %s (+ alias domene)",
"pushover_title": "Naslov obvestila",
"domains": "Domene",
"extended_sender_acl_info": "Če je DKIM domenski ključ na voljo, ga uvozite.<br>\n Ne pozabite dodati ta strežnik k ustreznemu SPF TXT zapisu.<br>\n Kadar koli je domena ali alias domena dodana k tem strežniku, ki se prekriva z zunanjim naslovom, je zunanji naslov odstranjen.<br>\n uporabite @domain.tld da dovolite pošiljanje kot *@domain.tld.",
"lookup_mx": "Cilj je regular expression za ujemanje MX zapisov (<code>.*\\.google\\.com</code> za usmeritev vse pošte na MX, ki se konča z google.com, preko tega skoka)",
"maxbytespersecond": "Največ bytov na sekundo <br><small>(0 = neomejeno)</small>",
"pushover_sender_array": "Upoštevaj samo sledeče e-poštne naslove pošiljateljev <small>(ločeni z vejico)</small>",
"mbox_rl_info": "Ta omejitev velja za SASL uporabniško ime, preverja se ujemanje s katerim koli \"from\" naslovom, ki ga uporablja prijavljeni uporabnik. Omejitev pošiljanja za poštni predal preglasi pravilo omejitve za domeno.",
"kind": "Tip",
"client_secret": "Client secret",
"comment_info": "Zasebni komentar ni viden uporabniku, javni komentar pa je viden kot tooltip v uporabnikovem pregledu.",
"created_on": "Ustvarjeno",
"custom_attributes": "Atributi po meri",
"delete1": "Izbriši na viru, ko je končano",
"delete2": "Izbriši sporočila na cilju, ki ne obstajajo na viru",
"delete2duplicates": "Izbriši dvojnike na cilju",
"delete_ays": "Prosim potrdite proces izbrisa.",
"description": "Opis",
"disable_login": "Onemogoči prijavo (dohodna pošta je še vedno sprejeta)",
"domain": "Uredi domeno",
"domain_admin": "Uredi domenskega skrbnika",
"domain_footer": "Noga za celo domeno",
"domain_footer_html": "HTML noga",
"pushover_vars": "Če ni definiran noben filter pošiljatelja, bodo upoštevana vsa sporočila.<br>Regex filtre in natančna preverjanja pošiljateljev je mogoče definirati posamezno in bodo obravnavani v nadaljevanju. Niso odvisni drug od drugega.<br>Uporabne spremenljivke za besedilo in naslov (prosimo, upoštevajte politike varstva podatkov)",
"pushover_verify": "Preveri poverilnice",
"quota_mb": "Omejitev (MiB)",
"quota_warning_bcc": "BCC za sporočilo z opozorilom omejitve",
"quota_warning_bcc_info": "Opozorila bodo poslana kot ločene kopije sledečim prejemnikom. K naslovu sporočila bo dodano uporabniško ime v oklepajih, npr. <code>Opozorilo omejitve (user@example.com)</code>",
"ratelimit": "Omejitev pošiljanja",
"advanced_settings": "Napredne nastavitve",
"allow_from_smtp_info": "Pustite prazno da dovolite vse pošiljatelje.<br>IPv4/IPv6 naslovi in omrežja.",
"allowed_protocols": "Dovoljeni protokoli",
"app_name": "Ime aplikacije",
"app_passwd": "Geslo aplikacije",
"app_passwd_protocols": "Dovoljeni protokoli za geslo aplikacije",
"backup_mx_options": "Možnosti posredovanja (relay)",
"client_id": "Client ID",
"domain_footer_info": "Noge za celo domeno so dodane k vsem izhodnim e-poštnim sporočilom v tej domeni.<br> V nogi se lahko uporabijo sledeče spremenljivke:",
"domain_footer_plain": "PLAIN noga",
"domain_footer_skip_replies": "Ne dodajaj noge v odgovorih na e-poštna sporočila",
"domain_quota": "Omejitev (kvota) domene",
"edit_alias_domain": "Uredi alias domeno",
"exclude": "Izključi objekte (regex)",
"extended_sender_acl": "Naslovi zunanjih pošiljateljev",
"force_pw_update": "Obvezna zamenjava gesla ob naslednji prijavi",
"force_pw_update_info": "Ta uporabnik se bo lahko prijavil samo v %s. Gesla aplikacij ostajajo v rabi.",
"footer_exclude": "Izključi iz noge",
"full_name": "Polno ime",
"gal": "Globalni seznam naslovov (GAL)",
"gal_info": "GAL vsebuje vse objekte v domeni in jih uporabniki ne morejo urejati. Če je onemogočeno, ni podatkov o o zasedenosti objekta! <b>Ponovno zaženite SOGo za uveljavitev sprememb.</b>",
"generate": "generiraj",
"grant_types": "Vrste dovoljenj",
"hostname": "Ime gostitelja",
"inactive": "Neaktivno",
"last_modified": "Nazadnje spremenjeno",
"mailbox": "Uredi poštni predal",
"mailbox_quota_def": "Privzeta omejitev/kvota za poštni predal",
"mailbox_relayhost_info": "Velja samo za poštni predal in neposredne aliase. Ne prepiše domenskega relay gostitelja.",
"max_aliases": "Največ aliasov",
"max_mailboxes": "Največ možnih poštnih predalov",
"max_quota": "Največja omejitev/kvota na poštni predal (MiB)",
"maxage": "Največja starost sporočil (v dnevih), po katerih bo poizvedeno iz oddaljenega vira <br><small>(0 = ne omejuj)</small>",
"mins_interval": "Interval (min)",
"multiple_bookings": "Več rezervacij",
"none_inherit": "Brez / podeduj",
"nexthop": "Naslednji skok",
"password": "Geslo",
"password_repeat": "Potrditev gesla (ponovite)",
"previous": "Prejšnja stran",
"private_comment": "Zasebni komentar",
"public_comment": "Javni komentar",
"pushover": "Pushover",
"pushover_evaluate_x_prio": "Eskaliraj visoko prednostno pošto [<code>X-Priority: 1</code>]",
"pushover_info": "Nastavitve potisnih obvestil bodo veljala za vsa čisto (ne spam) elektronsko pošto dostavljeno v <b>%s</b> vključno z aliasi (deljeni, nedeljeni, označeni)",
"pushover_only_x_prio": "Upoštevaj samo pošto z visoko prioriteto [<code>X-Priority: 1</code>]",
"pushover_sender_regex": "Upoštevaj sledeči regex za pošiljatelja",
"pushover_text": "Besedilo obvestila",
"pushover_sound": "Zvok",
"encryption": "Šifriranje",
"alias": "Uredi alias"
} }
} }

View File

@ -391,7 +391,7 @@
"last_key": "Останній ключ не можна видалити, натомість вимкніть TFA.", "last_key": "Останній ключ не можна видалити, натомість вимкніть TFA.",
"login_failed": "Введено неправильний логін або пароль", "login_failed": "Введено неправильний логін або пароль",
"mailbox_invalid": "Неприпустима адреса поштового акаунту", "mailbox_invalid": "Неприпустима адреса поштового акаунту",
"mailbox_quota_left_exceeded": "Недостатньо вільного місця (місця залишилося: %d МіБ)", "mailbox_quota_left_exceeded": "Недостатньо вільного місця (залишилося: %d МіБ)",
"malformed_username": "Некоректне ім'я користувача", "malformed_username": "Некоректне ім'я користувача",
"map_content_empty": "Зміст правила не може бути порожнім", "map_content_empty": "Зміст правила не може бути порожнім",
"max_alias_exceeded": "Перевищено максимальну кількість псевдонімів", "max_alias_exceeded": "Перевищено максимальну кількість псевдонімів",
@ -478,7 +478,8 @@
"extended_sender_acl_denied": "відсутній ACL для встановлення зовнішніх адрес відправників", "extended_sender_acl_denied": "відсутній ACL для встановлення зовнішніх адрес відправників",
"template_exists": "Шаблон %s вже існує", "template_exists": "Шаблон %s вже існує",
"template_id_invalid": "Ідентифікатор шаблону %s недійсний", "template_id_invalid": "Ідентифікатор шаблону %s недійсний",
"template_name_invalid": "Ім'я шаблону невірне" "template_name_invalid": "Ім'я шаблону невірне",
"img_size_exceeded": "Зображення перевищує максимальний розмір файлу"
}, },
"debug": { "debug": {
"chart_this_server": "Діаграма (цей сервер)", "chart_this_server": "Діаграма (цей сервер)",
@ -626,7 +627,7 @@
"admin": "Редагувати адміністратора", "admin": "Редагувати адміністратора",
"allow_from_smtp": "Дозволити <b>SMTP</b> тільки для цих IP", "allow_from_smtp": "Дозволити <b>SMTP</b> тільки для цих IP",
"allow_from_smtp_info": "Вкажіть IPv4/IPv6 адреси та/або підмережі.<br>Залиште поле порожнім, щоб дозволити відправлення з будь-яких адрес.", "allow_from_smtp_info": "Вкажіть IPv4/IPv6 адреси та/або підмережі.<br>Залиште поле порожнім, щоб дозволити відправлення з будь-яких адрес.",
"bcc_dest_format": "Призначенням правила BBC має бути єдина дійсна адреса електронної пошти.<br>Якщо вам потрібно надіслати копію на кілька адрес, створіть псевдонім і використовуйте його тут.", "bcc_dest_format": "Призначенням правила BCC має бути єдина дійсна адреса електронної пошти.<br>Якщо вам потрібно надіслати копію на кілька адрес, створіть псевдонім і використовуйте його тут.",
"comment_info": "Приватний коментар не видно користувачам, а публічний - відображається поряд із псевдонімом в особистому кабінеті користувача", "comment_info": "Приватний коментар не видно користувачам, а публічний - відображається поряд із псевдонімом в особистому кабінеті користувача",
"domain_quota": "Квота домену", "domain_quota": "Квота домену",
"dont_check_sender_acl": "Вимкнути перевірку відправника для домену %s та псевдонімів домену", "dont_check_sender_acl": "Вимкнути перевірку відправника для домену %s та псевдонімів домену",
@ -725,7 +726,7 @@
"add": "Додати", "add": "Додати",
"add_alias": "Додати псевдонім", "add_alias": "Додати псевдонім",
"add_alias_expand": "Копіювати псевдоніми на псевдоніми домену", "add_alias_expand": "Копіювати псевдоніми на псевдоніми домену",
"add_bcc_entry": "Додати правило BBC", "add_bcc_entry": "Додати правило BCC",
"add_domain": "Додати домен", "add_domain": "Додати домен",
"add_domain_alias": "Додати псевдонім домену", "add_domain_alias": "Додати псевдонім домену",
"add_filter": "Додати фільтр", "add_filter": "Додати фільтр",
@ -745,7 +746,7 @@
"bcc_local_dest": "Локальний домен", "bcc_local_dest": "Локальний домен",
"bcc_map": "Правила ВВС", "bcc_map": "Правила ВВС",
"bcc_map_type": "Тип BCC", "bcc_map_type": "Тип BCC",
"bcc_maps": "Правила BBC", "bcc_maps": "Правила BCC",
"bcc_rcpt_map": "Одержувач", "bcc_rcpt_map": "Одержувач",
"bcc_sender_map": "Відправник", "bcc_sender_map": "Відправник",
"bcc_to_rcpt": "Перейти на тип \"одержувач\"", "bcc_to_rcpt": "Перейти на тип \"одержувач\"",

View File

@ -0,0 +1,31 @@
<?php
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
header('Location: /debug');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
header('Location: /mailbox');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
$_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
if (isset($_GET['token'])) $is_reset_token_valid = reset_password("check", $_GET['token']);
else $is_reset_token_valid = False;
$template = 'reset-password.twig';
$template_data = [
'is_mobileconfig' => str_contains($_SESSION['index_query_string'], 'mobileconfig'),
'is_reset_token_valid' => $is_reset_token_valid,
'reset_token' => $_GET['token']
];
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';

View File

@ -22,7 +22,7 @@
<li><button class="dropdown-item" data-bs-target="#tab-config-quarantine" aria-selected="false" aria-controls="tab-config-quarantine" role="tab" data-bs-toggle="tab">{{ lang.admin.quarantine }}</button></li> <li><button class="dropdown-item" data-bs-target="#tab-config-quarantine" aria-selected="false" aria-controls="tab-config-quarantine" role="tab" data-bs-toggle="tab">{{ lang.admin.quarantine }}</button></li>
<li><button class="dropdown-item" data-bs-target="#tab-config-quota" aria-selected="false" aria-controls="tab-config-quota" role="tab" data-bs-toggle="tab">{{ lang.admin.quota_notifications }}</button></li> <li><button class="dropdown-item" data-bs-target="#tab-config-quota" aria-selected="false" aria-controls="tab-config-quota" role="tab" data-bs-toggle="tab">{{ lang.admin.quota_notifications }}</button></li>
<li><button class="dropdown-item" data-bs-target="#tab-config-rsettings" aria-selected="false" aria-controls="tab-config-rsettings" role="tab" data-bs-toggle="tab">{{ lang.admin.rspamd_settings_map }}</button></li> <li><button class="dropdown-item" data-bs-target="#tab-config-rsettings" aria-selected="false" aria-controls="tab-config-rsettings" role="tab" data-bs-toggle="tab">{{ lang.admin.rspamd_settings_map }}</button></li>
<li><button class="dropdown-item" data-bs-target="#tab-config-password-policy" aria-selected="false" aria-controls="tab-config-password-policy" role="tab" data-bs-toggle="tab">{{ lang.admin.password_policy }}</button></li> <li><button class="dropdown-item" data-bs-target="#tab-config-password-settings" aria-selected="false" aria-controls="tab-config-password-settings" role="tab" data-bs-toggle="tab">{{ lang.admin.password_settings }}</button></li>
<li><button class="dropdown-item" data-bs-target="#tab-config-customize" aria-selected="false" aria-controls="tab-config-customize" role="tab" data-bs-toggle="tab">{{ lang.admin.customize }}</button></li> <li><button class="dropdown-item" data-bs-target="#tab-config-customize" aria-selected="false" aria-controls="tab-config-customize" role="tab" data-bs-toggle="tab">{{ lang.admin.customize }}</button></li>
</ul> </ul>
</li> </li>
@ -51,7 +51,7 @@
{% include 'admin/tab-config-quota.twig' %} {% include 'admin/tab-config-quota.twig' %}
{% include 'admin/tab-config-rsettings.twig' %} {% include 'admin/tab-config-rsettings.twig' %}
{% include 'admin/tab-config-customize.twig' %} {% include 'admin/tab-config-customize.twig' %}
{% include 'admin/tab-config-password-policy.twig' %} {% include 'admin/tab-config-password-settings.twig' %}
{% include 'admin/tab-sys-mails.twig' %} {% include 'admin/tab-sys-mails.twig' %}
{% include 'admin/tab-globalfilter-regex.twig' %} {% include 'admin/tab-globalfilter-regex.twig' %}
</div> </div>

View File

@ -1,40 +0,0 @@
<div class="tab-pane fade" id="tab-config-password-policy" role="tabpanel" aria-labelledby="tab-config-password-policy">
<div class="card mb-4">
<div class="card-header d-flex fs-5">
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-password-policy" data-bs-toggle="collapse" aria-controls="collapse-tab-config-password-policy">
{{ lang.admin.password_policy }}
</button>
<span class="d-none d-md-block">{{ lang.admin.password_policy }}</span>
</div>
<div id="collapse-tab-config-password-policy" class="card-body collapse" data-bs-parent="#admin-content">
<form class="form-horizontal" data-id="passwordpolicy" role="form" method="post">
{% for name, value in password_complexity %}
{% if name == 'length' %}
<div class="row mb-4">
<label class="control-label col-sm-3 text-sm-end" for="length">{{ lang.admin.password_length }}:</label>
<div class="col-sm-2">
<input type="number" class="form-control" min="3" max="64" name="length" id="length" value="{{ value }}" required>
</div>
</div>
{% else %}
<input type="hidden" name="{{ name }}" value="0">
<div class="row mb-2">
<div class="offset-sm-3 col-sm-9">
<label>
<input type="checkbox" class="form-check-input" name="{{ name }}" id="{{ name }}" value="1" {% if value == 1 %}checked{% endif %}> {{ lang.admin['password_policy_'~name] }}
</label>
</div>
</div>
{% endif %}
{% endfor %}
<div class="row mt-4 mb-2">
<div class="offset-sm-3 col-sm-9">
<div class="btn-group">
<button class="btn btn-sm d-block d-sm-inline btn-success" data-item="passwordpolicy" data-action="edit_selected" data-id="passwordpolicy" data-api-url='edit/passwordpolicy' data-api-attr='{}' href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,102 @@
<div class="tab-pane fade" id="tab-config-password-settings" role="tabpanel" aria-labelledby="tab-config-password-settings">
<div class="card mb-4">
<div class="card-header d-flex fs-5">
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-password-settings" data-bs-toggle="collapse" aria-controls="collapse-tab-config-password-settings">
{{ lang.admin.password_settings }}
</button>
<span class="d-none d-md-block">{{ lang.admin.password_settings }}</span>
</div>
<div id="collapse-tab-config-password-settings" class="card-body collapse" data-bs-parent="#admin-content">
<form class="form-horizontal" data-id="passwordpolicy" role="form" method="post">
<div class="row">
<div class="col-sm-12">
<legend>
{{ lang.admin.password_policy }}
</legend>
<hr />
</div>
</div>
{% for name, value in password_complexity %}
{% if name == 'length' %}
<div class="row mb-4">
<label class="control-label col-sm-3 text-sm-end" for="length">{{ lang.admin.password_length }}:</label>
<div class="col-sm-2">
<input type="number" class="form-control" min="3" max="64" name="length" id="length" value="{{ value }}" required>
</div>
</div>
{% else %}
<input type="hidden" name="{{ name }}" value="0">
<div class="row mb-2">
<div class="offset-sm-3 col-sm-9">
<label>
<input type="checkbox" class="form-check-input" name="{{ name }}" id="{{ name }}" value="1" {% if value == 1 %}checked{% endif %}> {{ lang.admin['password_policy_'~name] }}
</label>
</div>
</div>
{% endif %}
{% endfor %}
<div class="row mt-4 mb-2">
<div class="offset-sm-3 col-sm-9">
<div class="btn-group">
<button class="btn btn-sm d-block d-sm-inline btn-success" data-item="passwordpolicy" data-action="edit_selected" data-id="passwordpolicy" data-api-url='edit/passwordpolicy' data-api-attr='{}' href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
</div>
</div>
</div>
</form>
<form class="form" role="form" data-id="pw_reset_notification" method="post" style="margin-top: 50px;">
<div class="row">
<div class="col-sm-12">
<legend>
{{ lang.admin.password_reset_settings }}
</legend>
<hr />
<small>{{ lang.admin.reset_password_vars|raw }}</small><br><br>
</div>
</div>
<div class="row mb-4">
<div class="col-sm-6">
<div>
<label for="pw_reset_from">{{ lang.admin.quota_notification_sender }}:</label>
<input type="email" class="form-control" id="pw_reset_from" name="from" value="{{ pw_reset_data.from }}">
</div>
</div>
<div class="col-sm-6">
<div>
<label for="pw_reset_subject">{{ lang.admin.quota_notification_subject }}:</label>
<input type="text" class="form-control" id="pw_reset_subject" name="subject" value="{{ pw_reset_data.subject }}">
</div>
</div>
</div>
<div class="row">
<div class="col-12" data-bs-target="#text_template" style="cursor:pointer" unselectable="on" data-bs-toggle="collapse">
<span class="d-block"><i style="font-size:10pt;" class="bi bi-plus-square"></i> {{ lang.admin.password_reset_tmpl_text }}</span>
<small>{{ lang.admin.restore_template }}</small>
</div>
<div id="text_template" class="col-12 collapse">
<textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code mb-2" rows="20" name="text_tmpl">{{ pw_reset_data.text_tmpl|raw }}</textarea>
</div>
<div class="col-12 mt-3" data-bs-target="#html_template" style="cursor:pointer" unselectable="on" data-bs-toggle="collapse">
<span class="d-block"><i style="font-size:10pt;" class="bi bi-plus-square"></i> {{ lang.admin.password_reset_tmpl_html }}</span>
<small>{{ lang.admin.restore_template }}</small>
</div>
<div id="html_template" class="col-12 collapse">
<textarea autocorrect="off" spellcheck="false" autocapitalize="none" class="form-control textarea-code" rows="20" name="html_tmpl">{{ pw_reset_data.html_tmpl|raw }}</textarea>
</div>
</div>
<div class="row">
<div class="col-sm-10">
<div>
<br>
<a type="button" class="btn btn-sm d-block d-sm-inline btn-success" data-action="edit_selected"
data-item="pw_reset_notification"
data-id="pw_reset_notification"
data-api-url='edit/reset-password-notification'
data-api-attr='{}'><i class="bi bi-check-lg"></i> {{ lang.user.save_changes }}</a>
</div>
</div>
</div>
</form>
</div>
</div>
</div>

View File

@ -112,6 +112,7 @@
<option value="quarantine_notification" {% if template.attributes.acl_quarantine_notification == '1' %} selected{% endif %}>{{ lang.acl["quarantine_notification"] }}</option> <option value="quarantine_notification" {% if template.attributes.acl_quarantine_notification == '1' %} selected{% endif %}>{{ lang.acl["quarantine_notification"] }}</option>
<option value="quarantine_category" {% if template.attributes.acl_quarantine_category == '1' %} selected{% endif %}>{{ lang.acl["quarantine_category"] }}</option> <option value="quarantine_category" {% if template.attributes.acl_quarantine_category == '1' %} selected{% endif %}>{{ lang.acl["quarantine_category"] }}</option>
<option value="app_passwds" {% if template.attributes.acl_app_passwds == '1' %} selected{% endif %}>{{ lang.acl["app_passwds"] }}</option> <option value="app_passwds" {% if template.attributes.acl_app_passwds == '1' %} selected{% endif %}>{{ lang.acl["app_passwds"] }}</option>
<option value="pw_reset" {% if template.attributes.acl_pw_reset == '1' %} selected{% endif %}>{{ lang.acl["pw_reset"] }}</option>
</select> </select>
</div> </div>
</div> </div>

View File

@ -203,6 +203,13 @@
<input type="password" data-pwgen-field="true" class="form-control" name="password2" autocomplete="new-password"> <input type="password" data-pwgen-field="true" class="form-control" name="password2" autocomplete="new-password">
</div> </div>
</div> </div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="pw_recovery_email">{{ lang.edit.password_recovery_email }}</label>
<div class="col-sm-10">
<input type="email" class="form-control" name="pw_recovery_email" value="{{ result.attributes.recovery_email }}">
<small class="text-muted">{{ lang.admin.password_reset_info }}</small>
</div>
</div>
<div data-acl="{{ acl.extend_sender_acl }}" class="row mb-4"> <div data-acl="{{ acl.extend_sender_acl }}" class="row mb-4">
<label class="control-label col-sm-2" for="extended_sender_acl">{{ lang.edit.extended_sender_acl }}</label> <label class="control-label col-sm-2" for="extended_sender_acl">{{ lang.edit.extended_sender_acl }}</label>
<div class="col-sm-10"> <div class="col-sm-10">

View File

@ -63,6 +63,9 @@
{% endif %} {% endif %}
</div> </div>
</form> </form>
<div class="mt-3 mb-4">
<a href="/reset-password">{{ lang.login.forgot_password }}</a>
</div>
{% if login_delay %} {% if login_delay %}
<p><div class="my-4 alert alert-info">{{ lang.login.delayed|format(login_delay) }}</b></div></p> <p><div class="my-4 alert alert-info">{{ lang.login.delayed|format(login_delay) }}</b></div></p>
{% endif %} {% endif %}

View File

@ -149,6 +149,7 @@
<option value="quarantine_notification" selected>{{ lang.acl["quarantine_notification"] }}</option> <option value="quarantine_notification" selected>{{ lang.acl["quarantine_notification"] }}</option>
<option value="quarantine_category" selected>{{ lang.acl["quarantine_category"] }}</option> <option value="quarantine_category" selected>{{ lang.acl["quarantine_category"] }}</option>
<option value="app_passwds" selected>{{ lang.acl["app_passwds"] }}</option> <option value="app_passwds" selected>{{ lang.acl["app_passwds"] }}</option>
<option value="pw_reset" selected>{{ lang.acl["pw_reset"] }}</option>
</select> </select>
</div> </div>
</div> </div>
@ -318,6 +319,7 @@
<option value="quarantine_notification" selected>{{ lang.acl["quarantine_notification"] }}</option> <option value="quarantine_notification" selected>{{ lang.acl["quarantine_notification"] }}</option>
<option value="quarantine_category" selected>{{ lang.acl["quarantine_category"] }}</option> <option value="quarantine_category" selected>{{ lang.acl["quarantine_category"] }}</option>
<option value="app_passwds" selected>{{ lang.acl["app_passwds"] }}</option> <option value="app_passwds" selected>{{ lang.acl["app_passwds"] }}</option>
<option value="pw_reset" selected>{{ lang.acl["pw_reset"] }}</option>
</select> </select>
</div> </div>
</div> </div>

View File

@ -309,6 +309,33 @@
</div> </div>
</div> </div>
</div><!-- pw change modal --> </div><!-- pw change modal -->
<!-- pw recovery email modal -->
<div class="modal fade" id="pwRecoveryEmailModal" tabindex="-1" role="dialog" aria-labelledby="pwRecoveryEmailModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">{{ lang.user.pw_recovery_email }}</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form class="form-horizontal" data-cached-form="false" data-id="pw_recovery_change" role="form" method="post" autocomplete="off">
<div class="row mb-4">
<label class="control-label col-sm-3" for="pw_recovery_email">{{ lang.user.email }}</label>
<div class="col-sm-9">
<input type="email" class="form-control" name="pw_recovery_email" value="{{ mailboxdata.attributes.recovery_email }}">
<small class="text-muted">{{ lang.user.password_reset_info }}</small>
</div>
</div>
<div class="row">
<div class="offset-sm-3 col-sm-9">
<button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="edit_selected" data-id="pw_recovery_change" data-item="null" data-api-url='edit/self' data-api-attr='{}' href="#">{{ lang.user.save }}</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div><!-- pw recovery email modal -->
<!-- temp alias modal --> <!-- temp alias modal -->
<div class="modal fade" id="tempAliasModal" tabindex="-1" role="dialog" aria-labelledby="tempAliasModalLabel"> <div class="modal fade" id="tempAliasModal" tabindex="-1" role="dialog" aria-labelledby="tempAliasModalLabel">
<div class="modal-dialog modal-lg" role="document"> <div class="modal-dialog modal-lg" role="document">

View File

@ -0,0 +1,57 @@
{% extends 'base.twig' %}
{% block navbar %}{% endblock %}
{% block content %}
<div class="row mb-4" style="margin-top: 60px">
<div class="col-12 col-md-7 col-lg-6 col-xl-5 ms-auto me-auto">
<div class="card">
<div class="card-header d-flex align-items-center">
<i class="bi bi-person-fill me-2"></i> {{ lang.login.reset_password }}
<div class="ms-auto form-check form-switch my-auto d-flex align-items-center">
<label class="form-check-label"><i class="bi bi-moon-fill"></i></label>
<input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle">
</div>
</div>
<div class="card-body">
<div class="text-center mailcow-logo mb-4">
<img class="main-logo" src="{{ logo|default('/img/cow_mailcow.svg') }}" alt="mailcow">
<img class="main-logo-dark" src="{{ logo_dark|default('/img/cow_mailcow.svg') }}" alt="mailcow-logo-dark">
</div>
<legend>{{ ui_texts.main_name|raw }}</legend><hr />
{% if is_reset_token_valid %}
<form method="post" autofill="off">
<input type="hidden" name="token" value="{{ reset_token }}" />
<input type="password" autocorrect="off" autocapitalize="none" class="form-control mb-2" name="new_password" placeholder="{{ lang.login.new_password }}" />
<input type="password" autocorrect="off" autocapitalize="none" class="form-control mb-2" name="new_password2" placeholder="{{ lang.login.new_password_confirm }}" />
<small id="mismatch_alert" class="text-danger d-none">{{ lang.login.password_mismatch }}</small>
<div class="d-flex justify-content-end mt-4" style="position: relative">
<button type="submit" class="btn btn-xs-lg d-block d-sm-inline btn-success" name="pw_reset">{{ lang.login.reset_password }}</button>
</div>
</form>
{% elseif reset_token is null %}
<form method="post" autofill="off">
<input type="text" autocorrect="off" autocapitalize="none" class="form-control mb-2" name="username" placeholder="{{ lang.login.username }}" />
<div class="d-flex justify-content-end mt-4" style="position: relative">
<button type="submit" class="btn btn-xs-lg d-block d-sm-inline btn-success" name="pw_reset_request">{{ lang.login.request_reset_password }}</button>
</div>
</form>
{% else %}
<p class="text-center">{{ lang.login.invalid_pass_reset_token|raw }}</p>
<a href="/">{{ lang.login.back_to_mailcow }}</a>
{% endif %}
</div>
</div>
</div>
</div>
<script type='text/javascript'>
var csrf_token = '{{ csrf_token }}';
var mailcow_cc_username = '{{ mailcow_cc_username }}';
</script>
{% endblock %}

View File

@ -50,6 +50,7 @@
<p>{{ mailboxdata.quota_used|formatBytes(2) }} / {% if mailboxdata.quota == 0 %}{% else %}{{ mailboxdata.quota|formatBytes(2) }}{% endif %}<br>{{ mailboxdata.messages }} {{ lang.user.messages }}</p> <p>{{ mailboxdata.quota_used|formatBytes(2) }} / {% if mailboxdata.quota == 0 %}{% else %}{{ mailboxdata.quota|formatBytes(2) }}{% endif %}<br>{{ mailboxdata.messages }} {{ lang.user.messages }}</p>
<hr> <hr>
<p><a href="#pwChangeModal" data-bs-toggle="modal"><i class="bi bi-pencil-fill"></i> {{ lang.user.change_password }}</a></p> <p><a href="#pwChangeModal" data-bs-toggle="modal"><i class="bi bi-pencil-fill"></i> {{ lang.user.change_password }}</a></p>
{% if acl.pw_reset == 1 %}<p><a href="#pwRecoveryEmailModal" data-bs-toggle="modal"><i class="bi bi-pencil-fill"></i> {{ lang.user.pw_recovery_email }}</a></p>{% endif %}
</div> </div>
</div> </div>
<hr> <hr>

View File

@ -1,7 +1,7 @@
services: services:
unbound-mailcow: unbound-mailcow:
image: mailcow/unbound:1.22 image: mailcow/unbound:1.23
environment: environment:
- TZ=${TZ} - TZ=${TZ}
- SKIP_UNBOUND_HEALTHCHECK=${SKIP_UNBOUND_HEALTHCHECK:-n} - SKIP_UNBOUND_HEALTHCHECK=${SKIP_UNBOUND_HEALTHCHECK:-n}
@ -80,7 +80,7 @@ services:
- clamd - clamd
rspamd-mailcow: rspamd-mailcow:
image: mailcow/rspamd:1.96 image: mailcow/rspamd:1.97
stop_grace_period: 30s stop_grace_period: 30s
depends_on: depends_on:
- dovecot-mailcow - dovecot-mailcow
@ -90,6 +90,7 @@ services:
- IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64} - IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
- SPAMHAUS_DQS_KEY=${SPAMHAUS_DQS_KEY:-}
volumes: volumes:
- ./data/hooks/rspamd:/hooks:Z - ./data/hooks/rspamd:/hooks:Z
- ./data/conf/rspamd/custom/:/etc/rspamd/custom:z - ./data/conf/rspamd/custom/:/etc/rspamd/custom:z
@ -110,7 +111,7 @@ services:
- rspamd - rspamd
php-fpm-mailcow: php-fpm-mailcow:
image: mailcow/phpfpm:1.87 image: mailcow/phpfpm:1.88
command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
depends_on: depends_on:
- redis-mailcow - redis-mailcow
@ -167,6 +168,7 @@ services:
- DEMO_MODE=${DEMO_MODE:-n} - DEMO_MODE=${DEMO_MODE:-n}
- WEBAUTHN_ONLY_TRUSTED_VENDORS=${WEBAUTHN_ONLY_TRUSTED_VENDORS:-n} - WEBAUTHN_ONLY_TRUSTED_VENDORS=${WEBAUTHN_ONLY_TRUSTED_VENDORS:-n}
- CLUSTERMODE=${CLUSTERMODE:-} - CLUSTERMODE=${CLUSTERMODE:-}
- FLATCURVE_EXPERIMENTAL=${FLATCURVE_EXPERIMENTAL:-}
restart: always restart: always
networks: networks:
mailcow-network: mailcow-network:
@ -174,7 +176,7 @@ services:
- phpfpm - phpfpm
sogo-mailcow: sogo-mailcow:
image: mailcow/sogo:1.123 image: mailcow/sogo:1.124
environment: environment:
- DBNAME=${DBNAME} - DBNAME=${DBNAME}
- DBUSER=${DBUSER} - DBUSER=${DBUSER}
@ -221,7 +223,7 @@ services:
- sogo - sogo
dovecot-mailcow: dovecot-mailcow:
image: mailcow/dovecot:1.30 image: mailcow/dovecot:2.0
depends_on: depends_on:
- mysql-mailcow - mysql-mailcow
- netfilter-mailcow - netfilter-mailcow
@ -405,7 +407,7 @@ services:
condition: service_started condition: service_started
unbound-mailcow: unbound-mailcow:
condition: service_healthy condition: service_healthy
image: mailcow/acme:1.88 image: mailcow/acme:1.89
dns: dns:
- ${IPV4_NETWORK:-172.22.1}.254 - ${IPV4_NETWORK:-172.22.1}.254
environment: environment:
@ -461,7 +463,7 @@ services:
- /lib/modules:/lib/modules:ro - /lib/modules:/lib/modules:ro
watchdog-mailcow: watchdog-mailcow:
image: mailcow/watchdog:2.03 image: mailcow/watchdog:2.04
dns: dns:
- ${IPV4_NETWORK:-172.22.1}.254 - ${IPV4_NETWORK:-172.22.1}.254
tmpfs: tmpfs:
@ -478,7 +480,6 @@ services:
- mysql-mailcow - mysql-mailcow
- acme-mailcow - acme-mailcow
- redis-mailcow - redis-mailcow
environment: environment:
- IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64} - IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
- LOG_LINES=${LOG_LINES:-9999} - LOG_LINES=${LOG_LINES:-9999}
@ -594,9 +595,10 @@ services:
ofelia-mailcow: ofelia-mailcow:
image: mcuadros/ofelia:latest image: mcuadros/ofelia:latest
restart: always restart: always
command: daemon --docker command: daemon --docker -f label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}
environment: environment:
- TZ=${TZ} - TZ=${TZ}
- COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}
depends_on: depends_on:
- sogo-mailcow - sogo-mailcow
- dovecot-mailcow - dovecot-mailcow

View File

@ -25,6 +25,16 @@ for bin in openssl curl docker git awk sha1sum grep cut; do
if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
done done
# Check Docker Version (need at least 24.X)
docker_version=$(docker -v | grep -oP '\d+\.\d+\.\d+' | cut -d '.' -f 1)
if [[ $docker_version -lt 24 ]]; then
echo -e "\e[31mCannot find Docker with a Version higher or equals 24.0.0\e[0m"
echo -e "\e[33mmailcow needs a newer Docker version to work properly...\e[0m"
echo -e "\e[31mPlease update your Docker installation... exiting\e[0m"
exit 1
fi
if docker compose > /dev/null 2>&1; then if docker compose > /dev/null 2>&1; then
if docker compose version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then if docker compose version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then
COMPOSE_VERSION=native COMPOSE_VERSION=native
@ -147,40 +157,44 @@ done
MEM_TOTAL=$(awk '/MemTotal/ {print $2}' /proc/meminfo) MEM_TOTAL=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
if [ ${MEM_TOTAL} -le "2621440" ]; then if [ -z "${SKIP_CLAMD}" ]; then
echo "Installed memory is <= 2.5 GiB. It is recommended to disable ClamAV to prevent out-of-memory situations." if [ ${MEM_TOTAL} -le "2621440" ]; then
echo "ClamAV can be re-enabled by setting SKIP_CLAMD=n in mailcow.conf." echo "Installed memory is <= 2.5 GiB. It is recommended to disable ClamAV to prevent out-of-memory situations."
read -r -p "Do you want to disable ClamAV now? [Y/n] " response echo "ClamAV can be re-enabled by setting SKIP_CLAMD=n in mailcow.conf."
case $response in read -r -p "Do you want to disable ClamAV now? [Y/n] " response
[nN][oO]|[nN]) case $response in
SKIP_CLAMD=n [nN][oO]|[nN])
SKIP_CLAMD=n
;;
*)
SKIP_CLAMD=y
;; ;;
*) esac
SKIP_CLAMD=y else
;; SKIP_CLAMD=n
esac fi
else
SKIP_CLAMD=n
fi fi
if [ ${MEM_TOTAL} -le "2097152" ]; then if [ -z "${SKIP_SOLR}" ]; then
echo "Disabling Solr on low-memory system." if [ ${MEM_TOTAL} -le "2097152" ]; then
SKIP_SOLR=y echo "Disabling Solr on low-memory system."
elif [ ${MEM_TOTAL} -le "3670016" ]; then SKIP_SOLR=y
echo "Installed memory is <= 3.5 GiB. It is recommended to disable Solr to prevent out-of-memory situations." elif [ ${MEM_TOTAL} -le "3670016" ]; then
echo "Solr is a prone to run OOM and should be monitored. The default Solr heap size is 1024 MiB and should be set in mailcow.conf according to your expected load." echo "Installed memory is <= 3.5 GiB. It is recommended to disable Solr to prevent out-of-memory situations."
echo "Solr can be re-enabled by setting SKIP_SOLR=n in mailcow.conf but will refuse to start with less than 2 GB total memory." echo "Solr is a prone to run OOM and should be monitored. The default Solr heap size is 1024 MiB and should be set in mailcow.conf according to your expected load."
read -r -p "Do you want to disable Solr now? [Y/n] " response echo "Solr can be re-enabled by setting SKIP_SOLR=n in mailcow.conf but will refuse to start with less than 2 GB total memory."
case $response in read -r -p "Do you want to disable Solr now? [Y/n] " response
[nN][oO]|[nN]) case $response in
SKIP_SOLR=n [nN][oO]|[nN])
SKIP_SOLR=n
;;
*)
SKIP_SOLR=y
;; ;;
*) esac
SKIP_SOLR=y else
;; SKIP_SOLR=n
esac fi
else
SKIP_SOLR=n
fi fi
if [[ ${SKIP_BRANCH} != y ]]; then if [[ ${SKIP_BRANCH} != y ]]; then

View File

@ -199,7 +199,7 @@ function restore() {
case "$1" in case "$1" in
vmail) vmail)
docker stop $(docker ps -qf name=dovecot-mailcow) docker stop $(docker ps -qf name=dovecot-mailcow)
docker run -it --name mailcow-backup --rm \ docker run -i --name mailcow-backup --rm \
-v ${RESTORE_LOCATION}:/backup:z \ -v ${RESTORE_LOCATION}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_vmail.tar.gz ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_vmail.tar.gz
@ -218,7 +218,7 @@ function restore() {
;; ;;
redis) redis)
docker stop $(docker ps -qf name=redis-mailcow) docker stop $(docker ps -qf name=redis-mailcow)
docker run -it --name mailcow-backup --rm \ docker run -i --name mailcow-backup --rm \
-v ${RESTORE_LOCATION}:/backup:z \ -v ${RESTORE_LOCATION}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_redis.tar.gz ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_redis.tar.gz
@ -226,7 +226,7 @@ function restore() {
;; ;;
crypt) crypt)
docker stop $(docker ps -qf name=dovecot-mailcow) docker stop $(docker ps -qf name=dovecot-mailcow)
docker run -it --name mailcow-backup --rm \ docker run -i --name mailcow-backup --rm \
-v ${RESTORE_LOCATION}:/backup:z \ -v ${RESTORE_LOCATION}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_crypt.tar.gz ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_crypt.tar.gz
@ -239,7 +239,7 @@ function restore() {
echo -e "Continuing anyhow. If rspamd is crashing opon boot try remove the rspamd volume with docker volume rm ${CMPS_PRJ}_rspamd-vol-1 after you've stopped the stack.\e[0m" echo -e "Continuing anyhow. If rspamd is crashing opon boot try remove the rspamd volume with docker volume rm ${CMPS_PRJ}_rspamd-vol-1 after you've stopped the stack.\e[0m"
sleep 2 sleep 2
docker stop $(docker ps -qf name=rspamd-mailcow) docker stop $(docker ps -qf name=rspamd-mailcow)
docker run -it --name mailcow-backup --rm \ docker run -i --name mailcow-backup --rm \
-v ${RESTORE_LOCATION}:/backup:z \ -v ${RESTORE_LOCATION}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz
@ -250,7 +250,7 @@ function restore() {
echo -e "Skipping rspamd due to compatibility issues!\e[0m" echo -e "Skipping rspamd due to compatibility issues!\e[0m"
else else
docker stop $(docker ps -qf name=rspamd-mailcow) docker stop $(docker ps -qf name=rspamd-mailcow)
docker run -it --name mailcow-backup --rm \ docker run -i --name mailcow-backup --rm \
-v ${RESTORE_LOCATION}:/backup:z \ -v ${RESTORE_LOCATION}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz
@ -259,7 +259,7 @@ function restore() {
;; ;;
postfix) postfix)
docker stop $(docker ps -qf name=postfix-mailcow) docker stop $(docker ps -qf name=postfix-mailcow)
docker run -it --name mailcow-backup --rm \ docker run -i --name mailcow-backup --rm \
-v ${RESTORE_LOCATION}:/backup:z \ -v ${RESTORE_LOCATION}:/backup:z \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:z \
${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_postfix.tar.gz ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_postfix.tar.gz
@ -295,7 +295,7 @@ function restore() {
${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; /bin/rm -rf /var/lib/mysql/* ; rsync -avh --usermap=root:mysql --groupmap=root:mysql /backup/ /var/lib/mysql/" ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; /bin/rm -rf /var/lib/mysql/* ; rsync -avh --usermap=root:mysql --groupmap=root:mysql /backup/ /var/lib/mysql/"
elif [[ -f "${RESTORE_LOCATION}/backup_mysql.gz" ]]; then elif [[ -f "${RESTORE_LOCATION}/backup_mysql.gz" ]]; then
docker run \ docker run \
-it --name mailcow-backup --rm \ -i --name mailcow-backup --rm \
-v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/var/lib/mysql/:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/var/lib/mysql/:z \
--entrypoint= \ --entrypoint= \
-u mysql \ -u mysql \

View File

@ -328,6 +328,16 @@ for bin in curl docker git awk sha1sum grep cut; do
fi fi
done done
# Check Docker Version (need at least 24.X)
docker_version=$(docker -v | grep -oP '\d+\.\d+\.\d+' | cut -d '.' -f 1)
if [[ $docker_version -lt 24 ]]; then
echo -e "\e[31mCannot find Docker with a Version higher or equals 24.0.0\e[0m"
echo -e "\e[33mmailcow needs a newer Docker version to work properly... continuing on your own risk!\e[0m"
echo -e "\e[31mPlease update your Docker installation... sleeping 10s\e[0m"
sleep 10
fi
export LC_ALL=C export LC_ALL=C
DATE=$(date +%Y-%m-%d_%H_%M_%S) DATE=$(date +%Y-%m-%d_%H_%M_%S)
BRANCH=$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD) BRANCH=$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD)
@ -399,17 +409,18 @@ while (($#)); do
-f|--force - Force update, do not ask questions -f|--force - Force update, do not ask questions
-d|--dev - Enables Developer Mode (No Checkout of update.sh for tests) -d|--dev - Enables Developer Mode (No Checkout of update.sh for tests)
' '
exit 1 exit 0
esac esac
shift shift
done done
[[ ! -f mailcow.conf ]] && { echo -e "\e[31mmailcow.conf is missing! Is mailcow installed?\e[0m"; exit 1;}
chmod 600 mailcow.conf chmod 600 mailcow.conf
source mailcow.conf source mailcow.conf
detect_docker_compose_command detect_docker_compose_command
[[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing! Is mailcow installed?"; exit 1;}
DOTS=${MAILCOW_HOSTNAME//[^.]}; DOTS=${MAILCOW_HOSTNAME//[^.]};
if [ ${#DOTS} -lt 1 ]; then if [ ${#DOTS} -lt 1 ]; then
echo -e "\e[31mMAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) is not a FQDN!\e[0m" echo -e "\e[31mMAILCOW_HOSTNAME (${MAILCOW_HOSTNAME}) is not a FQDN!\e[0m"
@ -424,7 +435,7 @@ elif [ ${#DOTS} -eq 1 ]; then
echo "Find more information about why this message exists here: https://github.com/mailcow/mailcow-dockerized/issues/1572" echo "Find more information about why this message exists here: https://github.com/mailcow/mailcow-dockerized/issues/1572"
read -r -p "Do you want to proceed anyway? [y/N] " response read -r -p "Do you want to proceed anyway? [y/N] " response
if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "OK. Procceding." echo "OK. Proceeding."
else else
echo "OK. Exiting." echo "OK. Exiting."
exit 1 exit 1
@ -807,7 +818,7 @@ if ! [ $NEW_BRANCH ]; then
echo -e "\e[33mTo change that run the update.sh Script one time with the --stable parameter to switch to stable builds.\e[0m" echo -e "\e[33mTo change that run the update.sh Script one time with the --stable parameter to switch to stable builds.\e[0m"
else else
echo -e "\e[33mYou are receiving updates from a unsupported branch.\e[0m" echo -e "\e[33mYou are receiving updates from an unsupported branch.\e[0m"
sleep 1 sleep 1
echo -e "\e[33mThe mailcow stack might still work but it is recommended to switch to the master branch (stable builds).\e[0m" echo -e "\e[33mThe mailcow stack might still work but it is recommended to switch to the master branch (stable builds).\e[0m"
echo -e "\e[33mTo change that run the update.sh Script one time with the --stable parameter to switch to stable builds.\e[0m" echo -e "\e[33mTo change that run the update.sh Script one time with the --stable parameter to switch to stable builds.\e[0m"
@ -818,14 +829,14 @@ elif [ $FORCE ]; then
echo -e "\e[31mPlease rerun the update.sh Script without the --force/-f parameter.\e[0m" echo -e "\e[31mPlease rerun the update.sh Script without the --force/-f parameter.\e[0m"
sleep 1 sleep 1
elif [ $NEW_BRANCH == "master" ] && [ $CURRENT_BRANCH != "master" ]; then elif [ $NEW_BRANCH == "master" ] && [ $CURRENT_BRANCH != "master" ]; then
echo -e "\e[33mYou are about to switch your mailcow Updates to the stable (master) branch.\e[0m" echo -e "\e[33mYou are about to switch your mailcow updates to the stable (master) branch.\e[0m"
sleep 1 sleep 1
echo -e "\e[33mBefore you do: Please take a backup of all components to ensure that no Data is lost...\e[0m" echo -e "\e[33mBefore you do: Please take a backup of all components to ensure that no data is lost...\e[0m"
sleep 1 sleep 1
echo -e "\e[31mWARNING: Please see on GitHub or ask in the communitys if a switch to master is stable or not. echo -e "\e[31mWARNING: Please see on GitHub or ask in the community if a switch to master is stable or not.
In some rear cases a Update back to master can destroy your mailcow configuration in case of Database Upgrades etc. In some rear cases an update back to master can destroy your mailcow configuration such as database upgrade, etc.
Normally a upgrade back to master should be safe during each full release. Normally an upgrade back to master should be safe during each full release.
Check GitHub for Database Changes and Update only if there similar to the full release!\e[0m" Check GitHub for Database changes and update only if there similar to the full release!\e[0m"
read -r -p "Are you sure you that want to continue upgrading to the stable (master) branch? [y/N] " response read -r -p "Are you sure you that want to continue upgrading to the stable (master) branch? [y/N] " response
if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then if [[ ! "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "OK. If you prepared yourself for that please run the update.sh Script with the --stable parameter again to trigger this process here." echo "OK. If you prepared yourself for that please run the update.sh Script with the --stable parameter again to trigger this process here."
@ -972,7 +983,7 @@ if [ ! $DEV ]; then
echo -e "\e[31m\nOh no, what happened?\n=> You most likely added files to your local mailcow instance that were now added to the official mailcow repository. Please move them to another location before updating mailcow.\e[0m" echo -e "\e[31m\nOh no, what happened?\n=> You most likely added files to your local mailcow instance that were now added to the official mailcow repository. Please move them to another location before updating mailcow.\e[0m"
exit 1 exit 1
elif [[ ${MERGE_RETURN} == 1 ]]; then elif [[ ${MERGE_RETURN} == 1 ]]; then
echo -e "\e[93mPotenial conflict, trying to fix...\e[0m" echo -e "\e[93mPotential conflict, trying to fix...\e[0m"
git status --porcelain | grep -E "UD|DU" | awk '{print $2}' | xargs rm -v git status --porcelain | grep -E "UD|DU" | awk '{print $2}' | xargs rm -v
git add -A git add -A
git commit -m "After update on ${DATE}" > /dev/null git commit -m "After update on ${DATE}" > /dev/null