mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2024-12-23 02:04:46 +02:00
commit
9a729d89bf
10
.github/ISSUE_TEMPLATE/pr_to_nighty_template.yml
vendored
10
.github/ISSUE_TEMPLATE/pr_to_nighty_template.yml
vendored
@ -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
|
||||
<!-- Diff files - START -->
|
||||
<!-- Diff files - END -->
|
38
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
38
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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? -->
|
@ -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!
|
||||
|
||||
## 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:
|
||||
|
||||
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.
|
||||
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.*
|
||||
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.*
|
||||
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.
|
||||
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!
|
||||
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. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.*
|
||||
7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project.
|
||||
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:
|
||||
|
||||
### 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).
|
||||
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.
|
||||
@ -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>.
|
||||
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.
|
||||
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.
|
||||
|
@ -1,6 +1,6 @@
|
||||
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 \
|
||||
|
@ -123,7 +123,7 @@ done
|
||||
log_f "Database OK"
|
||||
|
||||
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
|
||||
done
|
||||
log_f "Nginx OK"
|
||||
@ -137,7 +137,7 @@ log_f "Resolver OK"
|
||||
# Waiting for domain table
|
||||
log_f "Waiting for domain table..."
|
||||
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)
|
||||
[[ -z ${DOMAIN_TABLE} ]] && sleep 10
|
||||
done
|
||||
|
@ -2,32 +2,32 @@
|
||||
|
||||
# Reading container IDs
|
||||
# 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" " "))
|
||||
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" " "))
|
||||
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" " "))
|
||||
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.${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.${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(){
|
||||
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} ; }
|
||||
}
|
||||
|
||||
reload_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} ; }
|
||||
}
|
||||
|
||||
reload_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} ; }
|
||||
}
|
||||
|
||||
restart_container(){
|
||||
for container in $*; do
|
||||
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}"
|
||||
done
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
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 \
|
||||
&& apk add --update --no-cache \
|
||||
|
@ -1,6 +1,6 @@
|
||||
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
|
||||
WORKDIR /app
|
||||
|
@ -1,11 +1,12 @@
|
||||
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>.*)$
|
||||
ARG GOSU_VERSION=1.16
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
ENV LC_ALL C.UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
|
||||
# Add groups and users before installing Dovecot to not break compatibility
|
||||
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
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
|
@ -257,10 +257,14 @@ plugin {
|
||||
fts_autoindex_exclude2 = \Trash
|
||||
fts = flatcurve
|
||||
|
||||
# Maximum term length can be set via the 'maxlen' argument (maxlen is
|
||||
# specified in bytes, not number of UTF-8 characters)
|
||||
fts_tokenizer_email_address = maxlen=100
|
||||
fts_tokenizer_generic = algorithm=simple maxlen=30
|
||||
|
||||
# These are not flatcurve settings, but required for Dovecot FTS. See
|
||||
# Dovecot FTS Configuration link above for further information.
|
||||
fts_languages = en es de
|
||||
fts_tokenizer_generic = algorithm=simple
|
||||
fts_tokenizers = generic email-address
|
||||
|
||||
# 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-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
|
||||
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
|
||||
|
@ -3,8 +3,8 @@ FILE=/tmp/mail$$
|
||||
cat > $FILE
|
||||
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 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/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: 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.${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.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzyadd
|
||||
|
||||
exit 0
|
||||
|
@ -3,8 +3,8 @@ FILE=/tmp/mail$$
|
||||
cat > $FILE
|
||||
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 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/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: 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.${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.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzyadd
|
||||
|
||||
exit 0
|
||||
|
@ -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
|
||||
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 -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")
|
||||
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
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
FROM alpine:3.20
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
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
|
||||
WORKDIR /app
|
||||
|
@ -1,5 +1,6 @@
|
||||
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>.*)$
|
||||
ARG APCU_PECL_VERSION=5.1.23
|
||||
|
@ -23,7 +23,7 @@ done
|
||||
# Check mysql_upgrade (master and slave)
|
||||
CONTAINER_ID=
|
||||
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"
|
||||
sleep 2
|
||||
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)"
|
||||
break
|
||||
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_LOOP_C=$((SQL_LOOP_C+1))
|
||||
echo "SQL upgrade iteration #${SQL_LOOP_C}"
|
||||
@ -60,12 +60,12 @@ done
|
||||
|
||||
# doing post-installation stuff, if SQL was upgraded (master and slave)
|
||||
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
|
||||
echo "Could not determine Postfix container ID, skipping Postfix restart."
|
||||
else
|
||||
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..."
|
||||
sleep 5
|
||||
fi
|
||||
@ -74,7 +74,7 @@ fi
|
||||
# Check mysql tz import (master and slave)
|
||||
TZ_CHECK=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null)
|
||||
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 ${SQL_FULL_TZINFO_IMPORT_RETURN}
|
||||
fi
|
||||
|
@ -1,5 +1,6 @@
|
||||
FROM debian:bookworm-slim
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ENV LC_ALL C
|
||||
|
@ -1,10 +1,10 @@
|
||||
FROM debian:bullseye-slim
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
FROM debian:bookworm-slim
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG RSPAMD_VER=rspamd_3.7.5-2~8c86c1676
|
||||
ARG CODENAME=bullseye
|
||||
ENV LC_ALL C
|
||||
ARG RSPAMD_VER=rspamd_3.9.1-1~82f43560f
|
||||
ARG CODENAME=bookworm
|
||||
ENV LC_ALL=C
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
tzdata \
|
||||
@ -12,11 +12,12 @@ RUN apt-get update && apt-get install -y \
|
||||
gnupg2 \
|
||||
apt-transport-https \
|
||||
dnsutils \
|
||||
netcat \
|
||||
netcat-traditional \
|
||||
wget \
|
||||
redis-tools \
|
||||
procps \
|
||||
nano \
|
||||
lua-cjson \
|
||||
&& 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\
|
||||
&& apt install -y /tmp/${RSPAMD_VER}~${CODENAME}_${arch}.deb \
|
||||
|
@ -124,4 +124,190 @@ for file in /hooks/*; do
|
||||
fi
|
||||
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 "$@"
|
||||
|
@ -1,12 +1,13 @@
|
||||
FROM debian:bullseye-slim
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG DEBIAN_VERSION=bullseye
|
||||
ARG DEBIAN_VERSION=bookworm
|
||||
ARG SOGO_DEBIAN_REPOSITORY=http://www.axis.cz/linux/debian
|
||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||
ARG GOSU_VERSION=1.17
|
||||
ENV LC_ALL C
|
||||
ENV LC_ALL=C
|
||||
|
||||
# Prerequisites
|
||||
RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
||||
|
@ -1,4 +1,4 @@
|
||||
@version: 3.28
|
||||
@version: 3.38
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
|
@ -1,4 +1,4 @@
|
||||
@version: 3.28
|
||||
@version: 3.38
|
||||
@include "scl.conf"
|
||||
options {
|
||||
chain_hostnames(off);
|
||||
|
@ -1,15 +1,18 @@
|
||||
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 \
|
||||
curl \
|
||||
bind-tools \
|
||||
coreutils \
|
||||
unbound \
|
||||
bash \
|
||||
openssl \
|
||||
drill \
|
||||
tzdata \
|
||||
syslog-ng \
|
||||
supervisor \
|
||||
&& curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache \
|
||||
&& chown root:unbound /etc/unbound \
|
||||
&& adduser unbound tty \
|
||||
@ -21,9 +24,13 @@ COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
# healthcheck (dig, ping)
|
||||
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
|
||||
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"]
|
||||
|
||||
CMD ["/usr/sbin/unbound"]
|
||||
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
|
||||
|
@ -1,76 +1,102 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!)
|
||||
if [[ "${SKIP_UNBOUND_HEALTHCHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
SKIP_UNBOUND_HEALTHCHECK=y
|
||||
fi
|
||||
STATUS_FILE="/tmp/healthcheck_status"
|
||||
RUNS=0
|
||||
|
||||
# Reset logfile
|
||||
echo "$(date +"%Y-%m-%d %H:%M:%S"): Starting health check - logs can be found in /var/log/healthcheck.log"
|
||||
echo "$(date +"%Y-%m-%d %H:%M:%S"): Starting health check" > /var/log/healthcheck.log
|
||||
|
||||
# Declare log function for logfile inside container
|
||||
function log_to_file() {
|
||||
echo "$(date +"%Y-%m-%d %H:%M:%S"): $1" >> /var/log/healthcheck.log
|
||||
# Declare log function for logfile to stdout
|
||||
function log_to_stdout() {
|
||||
echo "$(date +"%Y-%m-%d %H:%M:%S"): $1"
|
||||
}
|
||||
|
||||
# General Ping function to check general pingability
|
||||
function check_ping() {
|
||||
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
|
||||
ping -q -c 3 -w 5 "$ip"
|
||||
if [ $? -ne 0 ]; then
|
||||
log_to_file "Healthcheck: Couldn't ping $ip for 5 seconds... Gave up!"
|
||||
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!"
|
||||
return 1
|
||||
success=false
|
||||
for ((i=1; i<=3; i++)); do
|
||||
ping -q -c 3 -w 5 "$ip" > /dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
success=true
|
||||
break
|
||||
else
|
||||
log_to_stdout "Healthcheck: Failed to ping $ip on attempt $i. Trying again..."
|
||||
fi
|
||||
done
|
||||
|
||||
log_to_file "Healthcheck: Ping Checks WORKING properly!"
|
||||
if [ "$success" = false ]; then
|
||||
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
|
||||
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
|
||||
success=false
|
||||
for ((i=1; i<=3; i++)); do
|
||||
dig +short +timeout=2 +tries=1 "$domain" @127.0.0.1 > /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
log_to_file "Healthcheck: DNS Resolution Failed on $i attempt! Trying again..."
|
||||
if [ $i -eq 3 ]; then
|
||||
log_to_file "Healthcheck: DNS Resolution not possible after $i attempts... Gave up!"
|
||||
log_to_file "Maybe check your outbound firewall, as it needs to resolve DNS over TCP AND UDP!"
|
||||
dig_output=$(dig +short +timeout=2 +tries=1 "$domain" @127.0.0.1 2>/dev/null)
|
||||
dig_rc=$?
|
||||
|
||||
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
|
||||
log_to_stdout "Healthcheck: DNS Resolution Failed on attempt $i for $domain! Trying again..."
|
||||
else
|
||||
success=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$success" = false ]; then
|
||||
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
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
log_to_file "Healthcheck: DNS Resolver WORKING properly!"
|
||||
return 0
|
||||
|
||||
}
|
||||
|
||||
while true; do
|
||||
|
||||
if [[ ${SKIP_UNBOUND_HEALTHCHECK} == "y" ]]; then
|
||||
log_to_file "Healthcheck: ALL CHECKS WERE SKIPPED! Unbound is healthy!"
|
||||
exit 0
|
||||
log_to_stdout "Healthcheck: ALL CHECKS WERE SKIPPED! Unbound is healthy!"
|
||||
echo "0" > $STATUS_FILE
|
||||
sleep 365d
|
||||
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)
|
||||
check_ping
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
PING_STATUS=$?
|
||||
|
||||
check_dns
|
||||
DNS_STATUS=$?
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
exit 1
|
||||
if [ $PING_STATUS -ne 0 ] || [ $DNS_STATUS -ne 0 ]; then
|
||||
echo "1" > $STATUS_FILE
|
||||
|
||||
else
|
||||
echo "0" > $STATUS_FILE
|
||||
fi
|
||||
|
||||
log_to_file "Healthcheck: ALL CHECKS WERE SUCCESSFUL! Unbound is healthy!"
|
||||
exit 0
|
||||
sleep 30
|
||||
|
||||
done
|
10
data/Dockerfiles/unbound/stop-supervisor.sh
Executable file
10
data/Dockerfiles/unbound/stop-supervisor.sh
Executable 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
|
32
data/Dockerfiles/unbound/supervisord.conf
Normal file
32
data/Dockerfiles/unbound/supervisord.conf
Normal 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
|
21
data/Dockerfiles/unbound/syslog-ng.conf
Normal file
21
data/Dockerfiles/unbound/syslog-ng.conf
Normal 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);
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
FROM alpine:3.20
|
||||
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
|
||||
|
||||
# Installation
|
||||
RUN apk add --update \
|
||||
|
@ -169,8 +169,12 @@ function notify_error() {
|
||||
return 1
|
||||
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
|
||||
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
|
||||
curl -X POST -H "Content-Type: application/json" ${CURL_VERBOSE} -d "${WEBHOOK_BODY}" ${WATCHDOG_NOTIFY_WEBHOOK}
|
||||
@ -191,12 +195,12 @@ get_container_ip() {
|
||||
else
|
||||
sleep 0.5
|
||||
# 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
|
||||
CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf))
|
||||
if [[ ! -z ${CONTAINER_ID} ]]; then
|
||||
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
|
||||
# grep will do nothing if one of these vars is empty
|
||||
[[ -z ${ip_match} ]] && continue
|
||||
@ -716,7 +720,7 @@ rspamd_checks() {
|
||||
From: watchdog@localhost
|
||||
|
||||
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
|
||||
echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2
|
||||
err_count=$(( ${err_count} + 1))
|
||||
@ -1095,12 +1099,12 @@ while true; do
|
||||
elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
|
||||
kill -STOP ${BACKGROUND_TASKS[*]}
|
||||
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 [[ "${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
|
||||
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
|
||||
log_msg "Container is running for less than 360 seconds, skipping action..."
|
||||
elif [[ ! -z ${HAS_INITDB} ]]; then
|
||||
@ -1108,7 +1112,7 @@ while true; do
|
||||
sleep 60
|
||||
else
|
||||
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}"
|
||||
log_msg "Wait for restarted container to settle and continue watching..."
|
||||
sleep 35
|
||||
|
29
data/assets/templates/pw_reset_html.tpl
Normal file
29
data/assets/templates/pw_reset_html.tpl
Normal 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>
|
11
data/assets/templates/pw_reset_text.tpl
Normal file
11
data/assets/templates/pw_reset_text.tpl
Normal 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.
|
@ -289,5 +289,20 @@ namespace inbox {
|
||||
mailbox "Kladde" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Πρόχειρα" {
|
||||
special_use = \Drafts
|
||||
}
|
||||
mailbox "Απεσταλμένα" {
|
||||
special_use = \Sent
|
||||
}
|
||||
mailbox "Κάδος απορριμάτων" {
|
||||
special_use = \Trash
|
||||
}
|
||||
mailbox "Ανεπιθύμητα" {
|
||||
special_use = \Junk
|
||||
}
|
||||
mailbox "Αρχειοθετημένα" {
|
||||
special_use = \Archive
|
||||
}
|
||||
prefix =
|
||||
}
|
@ -2,6 +2,7 @@ rbls {
|
||||
interserver_ip {
|
||||
symbol = "RBL_INTERSERVER_IP";
|
||||
rbl = "rbl.interserver.net";
|
||||
from = true;
|
||||
ipv6 = false;
|
||||
returncodes {
|
||||
RBL_INTERSERVER_BAD_IP = "127.0.0.2";
|
||||
@ -19,4 +20,7 @@ rbls {
|
||||
RBL_INTERSERVER_BAD_URI = "127.0.0.2";
|
||||
}
|
||||
}
|
||||
|
||||
.include(try=true,override=true,priority=5) "$LOCAL_CONFDIR/custom/dqs-rbl.conf"
|
||||
|
||||
}
|
@ -17,4 +17,261 @@ symbols = {
|
||||
score = 4.0;
|
||||
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"];
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
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 {
|
||||
name = "osb";
|
||||
}
|
||||
backend = "redis";
|
||||
min_tokens = 11;
|
||||
min_learns = 5;
|
||||
new_schema = true;
|
||||
expire = 2592000;
|
||||
expire = 7776000;
|
||||
statfile {
|
||||
symbol = "BAYES_HAM";
|
||||
spam = false;
|
||||
|
@ -107,6 +107,7 @@ $template_data = [
|
||||
'f2b_banlist_url' => getBaseUrl() . "/api/v1/get/fail2ban/banlist/" . $f2b_data['banlist_id'],
|
||||
'q_data' => quarantine('settings'),
|
||||
'qn_data' => quota_notification('get'),
|
||||
'pw_reset_data' => reset_password('get_notification'),
|
||||
'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'),
|
||||
'rsettings' => $rsettings,
|
||||
'rspamd_regex_maps' => $rspamd_regex_maps,
|
||||
|
@ -1073,13 +1073,17 @@ function update_sogo_static_view($mailbox = null) {
|
||||
function edit_user_account($_data) {
|
||||
global $lang;
|
||||
global $pdo;
|
||||
|
||||
$_data_log = $_data;
|
||||
!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_old_pass']) ?: $_data_log['user_old_pass'] = '*';
|
||||
|
||||
$username = $_SESSION['mailcow_cc_username'];
|
||||
$role = $_SESSION['mailcow_cc_role'];
|
||||
$password_old = $_data['user_old_pass'];
|
||||
$pw_recovery_email = $_data['pw_recovery_email'];
|
||||
|
||||
if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
@ -1088,11 +1092,15 @@ function edit_user_account($_data) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// edit password
|
||||
if (!empty($password_old) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
|
||||
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
|
||||
WHERE `kind` NOT REGEXP 'location|thing|group'
|
||||
AND `username` = :user");
|
||||
$stmt->execute(array(':user' => $username));
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!verify_hash($row['password'], $password_old)) {
|
||||
$_SESSION['return'][] = array(
|
||||
'type' => 'danger',
|
||||
@ -1101,7 +1109,7 @@ function edit_user_account($_data) {
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
|
||||
|
||||
$password_new = $_data['user_new_pass'];
|
||||
$password_new2 = $_data['user_new_pass2'];
|
||||
if (password_check($password_new, $password_new2) !== true) {
|
||||
@ -1116,8 +1124,29 @@ function edit_user_account($_data) {
|
||||
':password_hashed' => $password_hashed,
|
||||
':username' => $username
|
||||
));
|
||||
}
|
||||
|
||||
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(
|
||||
'type' => 'success',
|
||||
'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));
|
||||
}
|
||||
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) {
|
||||
if ($lines === false) {
|
||||
|
@ -184,6 +184,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
'msg' => 'global_filter_written'
|
||||
);
|
||||
return true;
|
||||
break;
|
||||
case 'filter':
|
||||
$sieve = new Sieve\SieveParser();
|
||||
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_category'] = (in_array('quarantine_category', $_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 {
|
||||
$_data['spam_alias'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_alias']);
|
||||
$_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_category'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_category']);
|
||||
$_data['app_passwds'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_app_passwds']);
|
||||
$_data['pw_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pw_reset']);
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $pdo->prepare("INSERT INTO `user_acl`
|
||||
(`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,
|
||||
: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(
|
||||
':username' => $username,
|
||||
':spam_alias' => $_data['spam_alias'],
|
||||
@ -1287,7 +1290,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':quarantine_attachments' => $_data['quarantine_attachments'],
|
||||
':quarantine_notification' => $_data['quarantine_notification'],
|
||||
':quarantine_category' => $_data['quarantine_category'],
|
||||
':app_passwds' => $_data['app_passwds']
|
||||
':app_passwds' => $_data['app_passwds'],
|
||||
':pw_reset' => $_data['pw_reset']
|
||||
));
|
||||
}
|
||||
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_category'] = (in_array('quarantine_category', $_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 {
|
||||
$_data['acl'] = (array)$_data['acl'];
|
||||
$attr['acl_spam_alias'] = 0;
|
||||
@ -2879,6 +2884,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
$quota_b = $quota_m * 1048576;
|
||||
$password = (!empty($_data['password'])) ? $_data['password'] : null;
|
||||
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
|
||||
$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 {
|
||||
@ -3132,6 +3138,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':address' => $username,
|
||||
':active' => $active
|
||||
));
|
||||
try {
|
||||
$stmt = $pdo->prepare("UPDATE `mailbox` SET
|
||||
`active` = :active,
|
||||
`name`= :name,
|
||||
@ -3142,7 +3149,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
`attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
|
||||
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access)
|
||||
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
|
||||
`attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email)
|
||||
WHERE `username` = :username");
|
||||
$stmt->execute(array(
|
||||
':active' => $active,
|
||||
@ -3154,9 +3162,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
||||
':pop3_access' => $pop3_access,
|
||||
':sieve_access' => $sieve_access,
|
||||
':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
|
||||
foreach($tags as $index => $tag){
|
||||
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_category'] = (in_array('quarantine_category', $_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 {
|
||||
foreach ($is_now as $key => $value){
|
||||
$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'
|
||||
);
|
||||
}
|
||||
if (strtolower(getenv('SKIP_SOLR')) == 'n') {
|
||||
if (strtolower(getenv('SKIP_SOLR')) == 'n' && strtolower(getenv('FLATCURVE_EXPERIMENTAL')) != 'y') {
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_URL, 'http://solr:8983/solr/dovecot-fts/update?commit=true');
|
||||
curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: text/xml'));
|
||||
|
@ -3,7 +3,7 @@ function init_db_schema() {
|
||||
try {
|
||||
global $pdo;
|
||||
|
||||
$db_version = "26022024_1433";
|
||||
$db_version = "29072024_1000";
|
||||
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
|
||||
$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_category" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
"pw_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
|
||||
),
|
||||
"keys" => array(
|
||||
"primary" => array(
|
||||
@ -694,6 +695,19 @@ function init_db_schema() {
|
||||
),
|
||||
"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(
|
||||
"cols" => array(
|
||||
"id" => "INT NOT NULL AUTO_INCREMENT",
|
||||
|
33
data/web/inc/lib/sieve/extensions/enotify.xml
Normal file
33
data/web/inc/lib/sieve/extensions/enotify.xml
Normal 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>
|
58
data/web/inc/lib/sieve/extensions/mime.xml
Normal file
58
data/web/inc/lib/sieve/extensions/mime.xml
Normal 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>
|
@ -10,8 +10,43 @@ 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 (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) {
|
||||
if (isset($_SESSION['pending_mailcow_cc_username']) && isset($_SESSION['pending_pw_reset_token']) && isset($_SESSION['pending_pw_new_password'])) {
|
||||
reset_password("reset", array(
|
||||
'new_password' => $_SESSION['pending_pw_new_password'],
|
||||
'new_password2' => $_SESSION['pending_pw_new_password'],
|
||||
'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: /");
|
||||
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']);
|
||||
@ -19,7 +54,10 @@ if (isset($_POST["verify_tfa_login"])) {
|
||||
unset($_SESSION['pending_tfa_methods']);
|
||||
|
||||
header("Location: /user");
|
||||
}
|
||||
} else {
|
||||
unset($_SESSION['pending_pw_reset_token']);
|
||||
unset($_SESSION['pending_pw_new_password']);
|
||||
unset($_SESSION['pending_mailcow_cc_username']);
|
||||
unset($_SESSION['pending_mailcow_cc_role']);
|
||||
unset($_SESSION['pending_tfa_methods']);
|
||||
@ -27,6 +65,8 @@ if (isset($_POST["verify_tfa_login"])) {
|
||||
}
|
||||
|
||||
if (isset($_GET["cancel_tfa_login"])) {
|
||||
unset($_SESSION['pending_pw_reset_token']);
|
||||
unset($_SESSION['pending_pw_new_password']);
|
||||
unset($_SESSION['pending_mailcow_cc_username']);
|
||||
unset($_SESSION['pending_mailcow_cc_role']);
|
||||
unset($_SESSION['pending_tfa_methods']);
|
||||
|
@ -210,6 +210,12 @@ $MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format'] = 'maildir:';
|
||||
// Show last IMAP and POP3 logins
|
||||
$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
|
||||
// true = required
|
||||
// false = preferred
|
||||
|
@ -380,6 +380,9 @@ $(document).ready(function() {
|
||||
if (template.acl_app_passwds == 1){
|
||||
acl.push("app_passwds");
|
||||
}
|
||||
if (template.acl_pw_reset == 1){
|
||||
acl.push("pw_reset");
|
||||
}
|
||||
$('#user_acl').selectpicker('val', acl);
|
||||
|
||||
$('#rl_value').val(template.rl_value);
|
||||
|
@ -1973,7 +1973,6 @@ if (isset($_GET['query'])) {
|
||||
case "quota_notification_bcc":
|
||||
process_edit_return(quota_notification_bcc('edit', $attr));
|
||||
break;
|
||||
break;
|
||||
case "mailq":
|
||||
process_edit_return(mailq('edit', array_merge(array('qid' => $items), $attr)));
|
||||
break;
|
||||
@ -2069,6 +2068,9 @@ if (isset($_GET['query'])) {
|
||||
case "cors":
|
||||
process_edit_return(cors('edit', $attr));
|
||||
break;
|
||||
case "reset-password-notification":
|
||||
process_edit_return(reset_password('edit_notification', $attr));
|
||||
break;
|
||||
// return no route found if no case is matched
|
||||
default:
|
||||
http_response_code(404);
|
||||
|
@ -14,6 +14,7 @@
|
||||
"prohibited": "Untersagt durch Richtlinie",
|
||||
"protocol_access": "Ändern der erlaubten Protokolle",
|
||||
"pushover": "Pushover",
|
||||
"pw_reset": "Verwalten der E-Mail zur Passwortwiederherstellung erlauben",
|
||||
"quarantine": "Quarantäne-Aktionen",
|
||||
"quarantine_attachments": "Anhänge aus Quarantäne",
|
||||
"quarantine_category": "Ändern der Quarantäne-Benachrichtigungskategorie",
|
||||
@ -248,6 +249,11 @@
|
||||
"password_policy_numbers": "Muss eine Ziffer enthalten",
|
||||
"password_policy_special_chars": "Muss Sonderzeichen enthalten",
|
||||
"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",
|
||||
"private_key": "Private Key",
|
||||
"quarantine": "Quarantäne",
|
||||
@ -287,6 +293,8 @@
|
||||
"remove_row": "Entfernen",
|
||||
"reset_default": "Zurücksetzen auf Standard",
|
||||
"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",
|
||||
"rsetting_add_rule": "Regel hinzufügen",
|
||||
"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_recipient_map_new": "Neuer 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",
|
||||
"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",
|
||||
@ -436,6 +445,8 @@
|
||||
"password_complexity": "Passwort entspricht nicht den Richtlinien",
|
||||
"password_empty": "Passwort darf nicht leer sein",
|
||||
"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_invalid": "Eintrag hat ein ungültiges Format",
|
||||
"private_key_error": "Schlüsselfehler: %s",
|
||||
@ -444,10 +455,12 @@
|
||||
"pushover_token": "Pushover Token hat das falsche Format",
|
||||
"quota_not_0_not_numeric": "Speicherplatz muss numerisch und >= 0 sein",
|
||||
"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",
|
||||
"relayhost_invalid": "Map-Eintrag %s ist ungültig",
|
||||
"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_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",
|
||||
"rl_timeframe": "Ratelimit-Zeitraum ist inkorrekt",
|
||||
"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_entry_exists": "Eine TLS-Richtlinie \"%s\" existiert bereits",
|
||||
"tls_policy_map_parameter_invalid": "Parameter ist ungültig",
|
||||
"to_invalid": "Empfänger darf nicht leer sein",
|
||||
"totp_verification_failed": "TOTP-Verifizierung fehlgeschlagen",
|
||||
"transport_dest_exists": "Transport-Maps-Ziel \"%s\" existiert bereits",
|
||||
"webauthn_verification_failed": "WebAuthn-Verifizierung fehlgeschlagen: %s",
|
||||
@ -638,6 +652,7 @@
|
||||
"nexthop": "Next Hop",
|
||||
"none_inherit": "Keine Auswahl / Erben",
|
||||
"password": "Passwort",
|
||||
"password_recovery_email": "E-Mail zur Passwortwiederherstellung",
|
||||
"password_repeat": "Passwort wiederholen",
|
||||
"previous": "Vorherige Seite",
|
||||
"private_comment": "Privater Kommentar",
|
||||
@ -741,12 +756,19 @@
|
||||
"session_expires": "Die Sitzung wird in etwa 15 Sekunden beendet."
|
||||
},
|
||||
"login": {
|
||||
"back_to_mailcow": "Zurück zu mailcow",
|
||||
"delayed": "Login wurde zur Sicherheit um %s Sekunde/n verzögert.",
|
||||
"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",
|
||||
"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",
|
||||
"password": "Passwort",
|
||||
"reset_password": "Passwort zurücksetzen",
|
||||
"request_reset_password": "Passwortänderung anfordern",
|
||||
"username": "Benutzername"
|
||||
},
|
||||
"mailbox": {
|
||||
@ -1065,11 +1087,13 @@
|
||||
"nginx_reloaded": "Nginx wurde neu geladen",
|
||||
"object_modified": "Änderungen an Objekt %s wurden 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.",
|
||||
"qlearn_spam": "Nachricht-ID %s wurde als Spam gelernt und gelöscht",
|
||||
"queue_command_success": "Queue-Aufgabe erfolgreich ausgeführt",
|
||||
"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",
|
||||
"recovery_email_sent": "Wiederherstellungs-E-Mail an %s gesendet",
|
||||
"relayhost_added": "Map-Eintrag %s wurde hinzugefügt",
|
||||
"relayhost_removed": "Map-Eintrag %s wurde entfernt",
|
||||
"reset_main_logo": "Standardgrafik wurde wiederhergestellt",
|
||||
@ -1202,6 +1226,7 @@
|
||||
"password": "Passwort",
|
||||
"password_now": "Aktuelles Passwort (Änderungen bestätigen)",
|
||||
"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_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>]",
|
||||
@ -1211,6 +1236,7 @@
|
||||
"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_verify": "Verbindung verifizieren",
|
||||
"pw_recovery_email": "E-Mail zur Passwortwiederherstellung",
|
||||
"q_add_header": "Junk-Ordner",
|
||||
"q_all": "Alle Kategorien",
|
||||
"q_reject": "Abgelehnt",
|
||||
|
@ -14,6 +14,7 @@
|
||||
"prohibited": "Prohibited by ACL",
|
||||
"protocol_access": "Change protocol access",
|
||||
"pushover": "Pushover",
|
||||
"pw_reset": "Allow to reset mailcow user password",
|
||||
"quarantine": "Quarantine actions",
|
||||
"quarantine_attachments": "Quarantine attachments",
|
||||
"quarantine_category": "Change quarantine notification category",
|
||||
@ -256,6 +257,11 @@
|
||||
"password_policy_numbers": "Must contain at least one number",
|
||||
"password_policy_special_chars": "Must contain special characters",
|
||||
"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",
|
||||
"private_key": "Private key",
|
||||
"quarantine": "Quarantine",
|
||||
@ -296,6 +302,8 @@
|
||||
"remove_row": "Remove row",
|
||||
"reset_default": "Reset to default",
|
||||
"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",
|
||||
"rsetting_add_rule": "Add rule",
|
||||
"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_recipient_map_new": "Invalid new 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",
|
||||
"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.",
|
||||
@ -436,6 +445,8 @@
|
||||
"password_complexity": "Password does not meet the policy",
|
||||
"password_empty": "Password must not be empty",
|
||||
"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_invalid": "Record has invalid format",
|
||||
"private_key_error": "Private key error: %s",
|
||||
@ -444,10 +455,12 @@
|
||||
"pushover_token": "Pushover token has a wrong format",
|
||||
"quota_not_0_not_numeric": "Quota must be numeric and >= 0",
|
||||
"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",
|
||||
"relayhost_invalid": "Map entry %s is invalid",
|
||||
"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_token_limit_exceeded": "Reset token limit has been exceeded. Please try again later.",
|
||||
"resource_invalid": "Resource name %s is invalid",
|
||||
"rl_timeframe": "Rate limit time frame is incorrect",
|
||||
"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_entry_exists": "A TLS policy map entry \"%s\" exists",
|
||||
"tls_policy_map_parameter_invalid": "Policy parameter is invalid",
|
||||
"to_invalid": "Recipient must not be empty",
|
||||
"totp_verification_failed": "TOTP verification failed",
|
||||
"transport_dest_exists": "Transport destination \"%s\" exists",
|
||||
"webauthn_verification_failed": "WebAuthn verification failed: %s",
|
||||
@ -638,6 +652,7 @@
|
||||
"none_inherit": "None / Inherit",
|
||||
"nexthop": "Next hop",
|
||||
"password": "Password",
|
||||
"password_recovery_email": "Password recovery email",
|
||||
"password_repeat": "Confirmation password (repeat)",
|
||||
"previous": "Previous page",
|
||||
"private_comment": "Private comment",
|
||||
@ -741,12 +756,19 @@
|
||||
"session_expires": "Your session will expire in about 15 seconds"
|
||||
},
|
||||
"login": {
|
||||
"back_to_mailcow": "Back to mailcow",
|
||||
"delayed": "Login was delayed by %s seconds.",
|
||||
"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",
|
||||
"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",
|
||||
"password": "Password",
|
||||
"reset_password": "Reset Password",
|
||||
"request_reset_password": "Request password change",
|
||||
"username": "Username"
|
||||
},
|
||||
"mailbox": {
|
||||
@ -1072,11 +1094,13 @@
|
||||
"nginx_reloaded": "Nginx was reloaded",
|
||||
"object_modified": "Changes to object %s have been saved",
|
||||
"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.",
|
||||
"qlearn_spam": "Message ID %s was learned as spam and deleted",
|
||||
"queue_command_success": "Queue command completed successfully",
|
||||
"recipient_map_entry_deleted": "Recipient map ID %s has been deleted",
|
||||
"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_removed": "Map entry %s has been removed",
|
||||
"reset_main_logo": "Reset to default logo",
|
||||
@ -1210,6 +1234,7 @@
|
||||
"password": "Password",
|
||||
"password_now": "Current password (confirm changes)",
|
||||
"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_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>]",
|
||||
@ -1220,6 +1245,7 @@
|
||||
"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_verify": "Verify credentials",
|
||||
"pw_recovery_email": "Password recovery email",
|
||||
"q_add_header": "Junk folder",
|
||||
"q_all": "All categories",
|
||||
"q_reject": "Rejected",
|
||||
|
@ -322,5 +322,148 @@
|
||||
"invalid_nexthop": "\"Next hop\"-format er ugyldig",
|
||||
"img_dimensions_exceeded": "Bildet overskriver maksimal bildestø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 <moo@mailcow.tld>\" 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>"
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"acl": {
|
||||
"alias_domains": "Создание псевдонимов домена",
|
||||
"app_passwds": "Пароли приложений",
|
||||
"bcc_maps": "Правила BBC",
|
||||
"bcc_maps": "Правила BCC",
|
||||
"delimiter_action": "Обработка тегированной почты",
|
||||
"domain_desc": "Изменение описания домена",
|
||||
"domain_relayhost": "Изменение промежуточных узлов для домена",
|
||||
@ -389,7 +389,7 @@
|
||||
"imagick_exception": "Ошибка в Imagick при чтении изображения",
|
||||
"img_invalid": "Невозможно проверить файл изображения",
|
||||
"img_tmp_missing": "Невозможно проверить файл изображения: временный файл не найден",
|
||||
"invalid_bcc_map_type": "Неверный тип правила BBC",
|
||||
"invalid_bcc_map_type": "Неверный тип правила BCC",
|
||||
"invalid_destination": "Назначение \"%s\" указано неверно",
|
||||
"invalid_filter_type": "Неверный тип фильтра",
|
||||
"invalid_host": "Хост %s указан неверно",
|
||||
@ -523,7 +523,7 @@
|
||||
"app_passwd": "Пароль приложения",
|
||||
"automap": "Автоматическое слияние папок (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
|
||||
"backup_mx_options": "Параметры резервного копирования MX",
|
||||
"bcc_dest_format": "Назначением для правила BBC должен быть единственный действительный адрес электронной почты.",
|
||||
"bcc_dest_format": "Назначением для правила BCC должен быть единственный действительный адрес электронной почты.",
|
||||
"client_id": "ID клиента",
|
||||
"client_secret": "Секретный ключ пользователя",
|
||||
"comment_info": "Приватный комментарий не виден пользователям, а публичный - отображается рядом с псевдонимом в личном кабинете пользователя",
|
||||
@ -700,7 +700,7 @@
|
||||
"add": "Добавить",
|
||||
"add_alias": "Добавить псевдоним",
|
||||
"add_alias_expand": "Скопировать псевдонимы на псевдонимы домена",
|
||||
"add_bcc_entry": "Добавить правило BBC",
|
||||
"add_bcc_entry": "Добавить правило BCC",
|
||||
"add_domain": "Добавить домен",
|
||||
"add_domain_alias": "Добавить псевдоним домена",
|
||||
"add_domain_record_first": "Пожалуйста, сначала добавьте домен",
|
||||
@ -719,14 +719,14 @@
|
||||
"allow_from_smtp_info": "Укажите IPv4/IPv6 адреса и/или подсети.<br>Оставьте поле пустым, чтобы разрешить отправку с любых адресов.",
|
||||
"allowed_protocols": "Разрешенные протоколы",
|
||||
"backup_mx": "Резервный MX",
|
||||
"bcc": "Правила BBC",
|
||||
"bcc": "Правила BCC",
|
||||
"bcc_destination": "Назначение BCC",
|
||||
"bcc_destinations": "Назначение BCC",
|
||||
"bcc_info": "Правила BCC используются для скрытой пересылки копий всех сообщений на другой адрес. Правило типа \"получатель\" используется, когда локальный получатель выступает в качестве получателя почты. Правило типа \"отправитель\" соответствуют тому же принципу. Локальный домен не будут проинформированы о неудачной доставке.",
|
||||
"bcc_local_dest": "Локальный домен",
|
||||
"bcc_map": "Правила BBC",
|
||||
"bcc_map": "Правила BCC",
|
||||
"bcc_map_type": "Тип BCC",
|
||||
"bcc_maps": "Правила BBC",
|
||||
"bcc_maps": "Правила BCC",
|
||||
"bcc_rcpt_map": "Получатель",
|
||||
"bcc_sender_map": "Отправитель",
|
||||
"bcc_to_rcpt": "Переключиться на тип \"получатель\"",
|
||||
|
@ -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.<domain>, autodiscover.<domain>).<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.",
|
||||
"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": {
|
||||
"access": "Dostop",
|
||||
@ -347,7 +348,10 @@
|
||||
"logo_dark_label": "Za temni način",
|
||||
"cors_settings": "Nastavitve CORS",
|
||||
"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": {
|
||||
"alias_goto_identical": "Alias in goto naslov morata biti identična",
|
||||
@ -476,7 +480,9 @@
|
||||
"temp_error": "Začasna napaka",
|
||||
"cors_invalid_method": "Navedena neveljavna Allow metoda",
|
||||
"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": {
|
||||
"containers_info": "Informacije o vsebniku (containerju)",
|
||||
@ -511,7 +517,13 @@
|
||||
"no_update_available": "Sistem je na najnovejši verziji",
|
||||
"update_failed": "Ni mogoče preveriti za posodobitve",
|
||||
"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": {
|
||||
"infoFiltered": "(filtrirano od _MAX_ skupaj zapisov)",
|
||||
@ -551,6 +563,98 @@
|
||||
},
|
||||
"edit": {
|
||||
"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 <moo@mailcow.tld>\" 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"
|
||||
}
|
||||
}
|
||||
|
@ -391,7 +391,7 @@
|
||||
"last_key": "Останній ключ не можна видалити, натомість вимкніть TFA.",
|
||||
"login_failed": "Введено неправильний логін або пароль",
|
||||
"mailbox_invalid": "Неприпустима адреса поштового акаунту",
|
||||
"mailbox_quota_left_exceeded": "Недостатньо вільного місця (місця залишилося: %d МіБ)",
|
||||
"mailbox_quota_left_exceeded": "Недостатньо вільного місця (залишилося: %d МіБ)",
|
||||
"malformed_username": "Некоректне ім'я користувача",
|
||||
"map_content_empty": "Зміст правила не може бути порожнім",
|
||||
"max_alias_exceeded": "Перевищено максимальну кількість псевдонімів",
|
||||
@ -478,7 +478,8 @@
|
||||
"extended_sender_acl_denied": "відсутній ACL для встановлення зовнішніх адрес відправників",
|
||||
"template_exists": "Шаблон %s вже існує",
|
||||
"template_id_invalid": "Ідентифікатор шаблону %s недійсний",
|
||||
"template_name_invalid": "Ім'я шаблону невірне"
|
||||
"template_name_invalid": "Ім'я шаблону невірне",
|
||||
"img_size_exceeded": "Зображення перевищує максимальний розмір файлу"
|
||||
},
|
||||
"debug": {
|
||||
"chart_this_server": "Діаграма (цей сервер)",
|
||||
@ -626,7 +627,7 @@
|
||||
"admin": "Редагувати адміністратора",
|
||||
"allow_from_smtp": "Дозволити <b>SMTP</b> тільки для цих IP",
|
||||
"allow_from_smtp_info": "Вкажіть IPv4/IPv6 адреси та/або підмережі.<br>Залиште поле порожнім, щоб дозволити відправлення з будь-яких адрес.",
|
||||
"bcc_dest_format": "Призначенням правила BBC має бути єдина дійсна адреса електронної пошти.<br>Якщо вам потрібно надіслати копію на кілька адрес, створіть псевдонім і використовуйте його тут.",
|
||||
"bcc_dest_format": "Призначенням правила BCC має бути єдина дійсна адреса електронної пошти.<br>Якщо вам потрібно надіслати копію на кілька адрес, створіть псевдонім і використовуйте його тут.",
|
||||
"comment_info": "Приватний коментар не видно користувачам, а публічний - відображається поряд із псевдонімом в особистому кабінеті користувача",
|
||||
"domain_quota": "Квота домену",
|
||||
"dont_check_sender_acl": "Вимкнути перевірку відправника для домену %s та псевдонімів домену",
|
||||
@ -725,7 +726,7 @@
|
||||
"add": "Додати",
|
||||
"add_alias": "Додати псевдонім",
|
||||
"add_alias_expand": "Копіювати псевдоніми на псевдоніми домену",
|
||||
"add_bcc_entry": "Додати правило BBC",
|
||||
"add_bcc_entry": "Додати правило BCC",
|
||||
"add_domain": "Додати домен",
|
||||
"add_domain_alias": "Додати псевдонім домену",
|
||||
"add_filter": "Додати фільтр",
|
||||
@ -745,7 +746,7 @@
|
||||
"bcc_local_dest": "Локальний домен",
|
||||
"bcc_map": "Правила ВВС",
|
||||
"bcc_map_type": "Тип BCC",
|
||||
"bcc_maps": "Правила BBC",
|
||||
"bcc_maps": "Правила BCC",
|
||||
"bcc_rcpt_map": "Одержувач",
|
||||
"bcc_sender_map": "Відправник",
|
||||
"bcc_to_rcpt": "Перейти на тип \"одержувач\"",
|
||||
|
31
data/web/reset-password.php
Normal file
31
data/web/reset-password.php
Normal 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';
|
@ -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-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-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>
|
||||
</ul>
|
||||
</li>
|
||||
@ -51,7 +51,7 @@
|
||||
{% include 'admin/tab-config-quota.twig' %}
|
||||
{% include 'admin/tab-config-rsettings.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-globalfilter-regex.twig' %}
|
||||
</div>
|
||||
|
@ -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>
|
102
data/web/templates/admin/tab-config-password-settings.twig
Normal file
102
data/web/templates/admin/tab-config-password-settings.twig
Normal 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>
|
@ -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_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="pw_reset" {% if template.attributes.acl_pw_reset == '1' %} selected{% endif %}>{{ lang.acl["pw_reset"] }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -203,6 +203,13 @@
|
||||
<input type="password" data-pwgen-field="true" class="form-control" name="password2" autocomplete="new-password">
|
||||
</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">
|
||||
<label class="control-label col-sm-2" for="extended_sender_acl">{{ lang.edit.extended_sender_acl }}</label>
|
||||
<div class="col-sm-10">
|
||||
|
@ -63,6 +63,9 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
<div class="mt-3 mb-4">
|
||||
<a href="/reset-password">{{ lang.login.forgot_password }}</a>
|
||||
</div>
|
||||
{% if login_delay %}
|
||||
<p><div class="my-4 alert alert-info">{{ lang.login.delayed|format(login_delay) }}</b></div></p>
|
||||
{% endif %}
|
||||
|
@ -149,6 +149,7 @@
|
||||
<option value="quarantine_notification" selected>{{ lang.acl["quarantine_notification"] }}</option>
|
||||
<option value="quarantine_category" selected>{{ lang.acl["quarantine_category"] }}</option>
|
||||
<option value="app_passwds" selected>{{ lang.acl["app_passwds"] }}</option>
|
||||
<option value="pw_reset" selected>{{ lang.acl["pw_reset"] }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@ -318,6 +319,7 @@
|
||||
<option value="quarantine_notification" selected>{{ lang.acl["quarantine_notification"] }}</option>
|
||||
<option value="quarantine_category" selected>{{ lang.acl["quarantine_category"] }}</option>
|
||||
<option value="app_passwds" selected>{{ lang.acl["app_passwds"] }}</option>
|
||||
<option value="pw_reset" selected>{{ lang.acl["pw_reset"] }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -309,6 +309,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</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 -->
|
||||
<div class="modal fade" id="tempAliasModal" tabindex="-1" role="dialog" aria-labelledby="tempAliasModalLabel">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
|
57
data/web/templates/reset-password.twig
Normal file
57
data/web/templates/reset-password.twig
Normal 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 %}
|
@ -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>
|
||||
<hr>
|
||||
<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>
|
||||
<hr>
|
||||
|
@ -1,7 +1,7 @@
|
||||
services:
|
||||
|
||||
unbound-mailcow:
|
||||
image: mailcow/unbound:1.22
|
||||
image: mailcow/unbound:1.23
|
||||
environment:
|
||||
- TZ=${TZ}
|
||||
- SKIP_UNBOUND_HEALTHCHECK=${SKIP_UNBOUND_HEALTHCHECK:-n}
|
||||
@ -80,7 +80,7 @@ services:
|
||||
- clamd
|
||||
|
||||
rspamd-mailcow:
|
||||
image: mailcow/rspamd:1.96
|
||||
image: mailcow/rspamd:1.97
|
||||
stop_grace_period: 30s
|
||||
depends_on:
|
||||
- dovecot-mailcow
|
||||
@ -90,6 +90,7 @@ services:
|
||||
- IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
|
||||
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
|
||||
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
|
||||
- SPAMHAUS_DQS_KEY=${SPAMHAUS_DQS_KEY:-}
|
||||
volumes:
|
||||
- ./data/hooks/rspamd:/hooks:Z
|
||||
- ./data/conf/rspamd/custom/:/etc/rspamd/custom:z
|
||||
@ -110,7 +111,7 @@ services:
|
||||
- rspamd
|
||||
|
||||
php-fpm-mailcow:
|
||||
image: mailcow/phpfpm:1.87
|
||||
image: mailcow/phpfpm:1.88
|
||||
command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
|
||||
depends_on:
|
||||
- redis-mailcow
|
||||
@ -167,6 +168,7 @@ services:
|
||||
- DEMO_MODE=${DEMO_MODE:-n}
|
||||
- WEBAUTHN_ONLY_TRUSTED_VENDORS=${WEBAUTHN_ONLY_TRUSTED_VENDORS:-n}
|
||||
- CLUSTERMODE=${CLUSTERMODE:-}
|
||||
- FLATCURVE_EXPERIMENTAL=${FLATCURVE_EXPERIMENTAL:-}
|
||||
restart: always
|
||||
networks:
|
||||
mailcow-network:
|
||||
@ -174,7 +176,7 @@ services:
|
||||
- phpfpm
|
||||
|
||||
sogo-mailcow:
|
||||
image: mailcow/sogo:1.123
|
||||
image: mailcow/sogo:1.124
|
||||
environment:
|
||||
- DBNAME=${DBNAME}
|
||||
- DBUSER=${DBUSER}
|
||||
@ -221,7 +223,7 @@ services:
|
||||
- sogo
|
||||
|
||||
dovecot-mailcow:
|
||||
image: mailcow/dovecot:1.30
|
||||
image: mailcow/dovecot:2.0
|
||||
depends_on:
|
||||
- mysql-mailcow
|
||||
- netfilter-mailcow
|
||||
@ -405,7 +407,7 @@ services:
|
||||
condition: service_started
|
||||
unbound-mailcow:
|
||||
condition: service_healthy
|
||||
image: mailcow/acme:1.88
|
||||
image: mailcow/acme:1.89
|
||||
dns:
|
||||
- ${IPV4_NETWORK:-172.22.1}.254
|
||||
environment:
|
||||
@ -461,7 +463,7 @@ services:
|
||||
- /lib/modules:/lib/modules:ro
|
||||
|
||||
watchdog-mailcow:
|
||||
image: mailcow/watchdog:2.03
|
||||
image: mailcow/watchdog:2.04
|
||||
dns:
|
||||
- ${IPV4_NETWORK:-172.22.1}.254
|
||||
tmpfs:
|
||||
@ -478,7 +480,6 @@ services:
|
||||
- mysql-mailcow
|
||||
- acme-mailcow
|
||||
- redis-mailcow
|
||||
|
||||
environment:
|
||||
- IPV6_NETWORK=${IPV6_NETWORK:-fd4d:6169:6c63:6f77::/64}
|
||||
- LOG_LINES=${LOG_LINES:-9999}
|
||||
@ -594,9 +595,10 @@ services:
|
||||
ofelia-mailcow:
|
||||
image: mcuadros/ofelia:latest
|
||||
restart: always
|
||||
command: daemon --docker
|
||||
command: daemon --docker -f label=com.docker.compose.project=${COMPOSE_PROJECT_NAME}
|
||||
environment:
|
||||
- TZ=${TZ}
|
||||
- COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME}
|
||||
depends_on:
|
||||
- sogo-mailcow
|
||||
- dovecot-mailcow
|
||||
|
@ -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
|
||||
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 version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then
|
||||
COMPOSE_VERSION=native
|
||||
@ -147,6 +157,7 @@ done
|
||||
|
||||
MEM_TOTAL=$(awk '/MemTotal/ {print $2}' /proc/meminfo)
|
||||
|
||||
if [ -z "${SKIP_CLAMD}" ]; then
|
||||
if [ ${MEM_TOTAL} -le "2621440" ]; then
|
||||
echo "Installed memory is <= 2.5 GiB. It is recommended to disable ClamAV to prevent out-of-memory situations."
|
||||
echo "ClamAV can be re-enabled by setting SKIP_CLAMD=n in mailcow.conf."
|
||||
@ -162,7 +173,9 @@ if [ ${MEM_TOTAL} -le "2621440" ]; then
|
||||
else
|
||||
SKIP_CLAMD=n
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "${SKIP_SOLR}" ]; then
|
||||
if [ ${MEM_TOTAL} -le "2097152" ]; then
|
||||
echo "Disabling Solr on low-memory system."
|
||||
SKIP_SOLR=y
|
||||
@ -182,6 +195,7 @@ elif [ ${MEM_TOTAL} -le "3670016" ]; then
|
||||
else
|
||||
SKIP_SOLR=n
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ${SKIP_BRANCH} != y ]]; then
|
||||
echo "Which branch of mailcow do you want to use?"
|
||||
|
@ -199,7 +199,7 @@ function restore() {
|
||||
case "$1" in
|
||||
vmail)
|
||||
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 $(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
|
||||
@ -218,7 +218,7 @@ function restore() {
|
||||
;;
|
||||
redis)
|
||||
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 $(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
|
||||
@ -226,7 +226,7 @@ function restore() {
|
||||
;;
|
||||
crypt)
|
||||
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 $(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
|
||||
@ -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"
|
||||
sleep 2
|
||||
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 $(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
|
||||
@ -250,7 +250,7 @@ function restore() {
|
||||
echo -e "Skipping rspamd due to compatibility issues!\e[0m"
|
||||
else
|
||||
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 $(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
|
||||
@ -259,7 +259,7 @@ function restore() {
|
||||
;;
|
||||
postfix)
|
||||
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 $(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
|
||||
@ -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/"
|
||||
elif [[ -f "${RESTORE_LOCATION}/backup_mysql.gz" ]]; then
|
||||
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 \
|
||||
--entrypoint= \
|
||||
-u mysql \
|
||||
|
33
update.sh
33
update.sh
@ -328,6 +328,16 @@ for bin in curl docker git awk sha1sum grep cut; do
|
||||
fi
|
||||
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
|
||||
DATE=$(date +%Y-%m-%d_%H_%M_%S)
|
||||
BRANCH=$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD)
|
||||
@ -399,17 +409,18 @@ while (($#)); do
|
||||
-f|--force - Force update, do not ask questions
|
||||
-d|--dev - Enables Developer Mode (No Checkout of update.sh for tests)
|
||||
'
|
||||
exit 1
|
||||
exit 0
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
[[ ! -f mailcow.conf ]] && { echo -e "\e[31mmailcow.conf is missing! Is mailcow installed?\e[0m"; exit 1;}
|
||||
|
||||
chmod 600 mailcow.conf
|
||||
source mailcow.conf
|
||||
|
||||
detect_docker_compose_command
|
||||
|
||||
[[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing! Is mailcow installed?"; exit 1;}
|
||||
DOTS=${MAILCOW_HOSTNAME//[^.]};
|
||||
if [ ${#DOTS} -lt 1 ]; then
|
||||
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"
|
||||
read -r -p "Do you want to proceed anyway? [y/N] " response
|
||||
if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||
echo "OK. Procceding."
|
||||
echo "OK. Proceeding."
|
||||
else
|
||||
echo "OK. Exiting."
|
||||
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"
|
||||
|
||||
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
|
||||
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"
|
||||
@ -818,14 +829,14 @@ elif [ $FORCE ]; then
|
||||
echo -e "\e[31mPlease rerun the update.sh Script without the --force/-f parameter.\e[0m"
|
||||
sleep 1
|
||||
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
|
||||
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
|
||||
echo -e "\e[31mWARNING: Please see on GitHub or ask in the communitys 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.
|
||||
Normally a 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"
|
||||
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 an update back to master can destroy your mailcow configuration such as database upgrade, etc.
|
||||
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"
|
||||
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
|
||||
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"
|
||||
exit 1
|
||||
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 add -A
|
||||
git commit -m "After update on ${DATE}" > /dev/null
|
||||
|
Loading…
Reference in New Issue
Block a user