Compare commits

...

87 Commits

Author SHA1 Message Date
Niklas Meyer
c9e9628383 Merge pull request #5699 from mailcow/staging
2024-01d
2024-02-02 17:08:45 +01:00
DerLinkman
909f07939e dovecot: bump version for repl fix 2024-02-02 17:06:31 +01:00
FreddleSpl0it
a310493485 [Dovecot] fix repl_health.sh 2024-02-02 16:52:41 +01:00
Niklas Meyer
1e09df20b6 Merge pull request #5689 from mailcow/staging
2024-01c
2024-02-02 15:52:33 +01:00
Patrick Schult
087481ac12 Merge pull request #5696 from mailcow/fix/netfilter
[Netfilter] add mailcow isolation rule to MAILCOW chain
2024-02-02 14:33:01 +01:00
FreddleSpl0it
c941e802d4 [Netfilter] only perform cleanup at exit if SIGTERM was recieved 2024-02-02 12:57:21 +01:00
FreddleSpl0it
39589bd441 [Netfilter] only perform cleanup at exit if SIGTERM was recieved 2024-02-02 12:46:50 +01:00
DerLinkman
2e57325dde docker-compose.yml: Bump dovecot + netfilter version 2024-02-02 11:27:46 +01:00
FreddleSpl0it
2072301d89 [Netfilter] only perform cleanup at exit if SIGTERM was recieved 2024-02-02 11:08:44 +01:00
FreddleSpl0it
b236fd3ac6 [Netfilter] add mailcow isolation rule to MAILCOW chain
[Netfilter] add mailcow rule to docker-user chain

[Netfilter] add mailcow isolation rule to MAILCOW chain

[Netfilter] add mailcow isolation rule to MAILCOW chain

[Netfilter] set mailcow isolation rule before redis

[Netfilter] clear bans in redis after connecting

[Netfilter] simplify mailcow isolation rule for compatibility with iptables-nft

[Netfilter] stop container after mariadb, redis, dovecot, solr

[Netfilter] simplify mailcow isolation rule for compatibility with iptables-nft

[Netfilter] add exception for mailcow isolation rule for HA setups

[Netfilter] add exception for mailcow isolation rule for HA setups

[Netfilter] add DISABLE_NETFILTER_ISOLATION_RULE

[Netfilter] fix wrong var name

[Netfilter] add DISABLE_NETFILTER_ISOLATION_RULE to update and generate_config sh
2024-02-02 10:10:11 +01:00
Niklas Meyer
b968695e31 Merge pull request #5686 from mailcow/update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2024-02-01 08:58:35 +01:00
Niklas Meyer
694f1d1623 Merge pull request #5688 from mailcow/fix/sogo-authenticated-users
sogo: fix ACL allow authenticated users + rebuild on Bookworm
2024-02-01 08:42:53 +01:00
DerLinkman
93e4d58606 sogo: fix ACL allow authenticated users + rebuild on Bookworm 2024-02-01 08:41:11 +01:00
milkmaker
cc77caad67 update postscreen_access.cidr 2024-02-01 00:13:56 +00:00
renovate[bot]
f74573f5d0 chore(deps): update peter-evans/create-pull-request action to v6 (#5683)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-31 16:14:42 +01:00
DerLinkman
deb6f0babc issue: added architecture as dropdown 2024-01-23 08:46:06 +01:00
Niklas Meyer
cb978136bd Merge pull request #5663 from mailcow/staging
2024-01b
2024-01-22 11:50:41 +01:00
Niklas Meyer
1159450cc4 Merge pull request #5662 from mailcow/fix/rollback-curl-bug
fix: rollback curl bug
2024-01-22 11:39:27 +01:00
DerLinkman
a0613e4b10 fix: rollback of Alpine 3.19 were possible 2024-01-22 11:26:26 +01:00
Niklas Meyer
68989f0a45 Merge pull request #5647 from Candinya/patch-1
fix: watchdog webhook body variables injector
2024-01-22 10:34:06 +01:00
DerLinkman
7da5e3697e compose: bump watchdog version 2024-01-22 10:32:01 +01:00
Nya Candy
6e7a0eb662 fix: watchdog webhook body variables injector 2024-01-22 10:32:01 +01:00
Niklas Meyer
b25ac855ca Merge pull request #5660 from luminem/openrc-support
Test for openrc configuration file instead of alpine
2024-01-22 10:27:29 +01:00
Niklas Meyer
3e02dcbb95 Merge pull request #5652 from KagurazakaNyaa/master
Allow user skip unbound healthcheck
2024-01-22 10:25:50 +01:00
DerLinkman
53be119e39 compose: bump unbound version 2024-01-22 10:22:24 +01:00
Luca Barbato
25bdc4c9ed Test for openrc configuration file instead of alpine
This way other distro using openrc can be supported.
2024-01-22 09:50:24 +01:00
KagurazakaNyaa
9d4055fc4d add parameter SKIP_UNBOUND_HEALTHCHECK to old installations 2024-01-19 00:07:51 +08:00
KagurazakaNyaa
d2edf359ac update config comment 2024-01-18 23:53:08 +08:00
KagurazakaNyaa
aa1d92dfbb add SKIP_UNBOUND_HEALTHCHECK to docker-compose.yml 2024-01-18 23:50:26 +08:00
KagurazakaNyaa
b89d71e6e4 change variable name 2024-01-18 23:48:59 +08:00
KagurazakaNyaa
ed493f9c3a Allow user skip unbound healthcheck 2024-01-18 23:28:03 +08:00
Niklas Meyer
76f8a5b7de Merge pull request #5650 from mailcow/staging
unbound: increased healthcheck timeout
2024-01-18 11:56:09 +01:00
DerLinkman
cb3bc207b9 unbound: increased healthcheck timeout 2024-01-18 11:55:01 +01:00
Niklas Meyer
b5db5dd0b4 Merge pull request #5642 from mailcow/staging
2024-01
2024-01-17 13:51:40 +01:00
FreddleSpl0it
90a7cff2c9 [Rspamd] check if footer.skip_replies is not 0 2024-01-17 12:05:51 +01:00
FreddleSpl0it
cc3adbe78c [Web] fix datatables ssp queries 2024-01-17 12:04:01 +01:00
Niklas Meyer
bd6a7210b7 Merge pull request #5523 from FELDSAM-INC/feldsam/datatables-ssp
Implemented Server Side processing for domains and mailboxes datatables
2024-01-17 10:23:05 +01:00
Niklas Meyer
905a202873 Merge pull request #5587 from mailcow/feat/arm64
mailcow Multiarch (x86 and ARM64) support
2024-01-17 10:18:06 +01:00
DerLinkman
accedf0280 Updated mailcow Components to be ARM64 compatible 2024-01-17 10:14:36 +01:00
FreddleSpl0it
99d9a2eacd [Web] fix mailbox and domain creation 2024-01-17 09:52:43 +01:00
Kristian Feldsam
ac4f131fa8 Domains and Mailboxes datatable - server side processing - filtering by tags
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2024-01-16 15:03:28 +01:00
FreddleSpl0it
7f6f7e0e9f [Web] limit logo file upload 2024-01-15 16:34:47 +01:00
Niklas Meyer
43bb26f28c Merge pull request #5639 from mailcow/feat/unbound-healthcheck-rewrite
unbound: rewrote of healthcheck
2024-01-15 15:57:18 +01:00
DerLinkman
b29dc37991 unbound: rewrote healthcheck to be more detailed
unbound: added comments to rewritten healthcheck
2024-01-15 15:17:28 +01:00
DerLinkman
cf9f02adbb ui: fix alignment secondary 2024-01-10 14:43:59 +01:00
DerLinkman
b5a1a18b04 lang: fixed totp langs 2024-01-09 12:20:30 +01:00
Niklas Meyer
b4eeb0ffae Merge pull request #5522 from mailcow/renovate/krakjoe-apcu-5.x
chore(deps): update dependency krakjoe/apcu to v5.1.23
2024-01-09 12:06:12 +01:00
Niklas Meyer
48549ead7f Merge pull request #5549 from mailcow/renovate/phpredis-phpredis-6.x
chore(deps): update dependency phpredis/phpredis to v6.0.2
2024-01-09 12:04:41 +01:00
Niklas Meyer
01b0ad0fd9 Merge pull request #5550 from mailcow/renovate/tianon-gosu-1.x
chore(deps): update dependency tianon/gosu to v1.17
2024-01-09 12:04:21 +01:00
Niklas Meyer
2b21501450 Merge pull request #5581 from mailcow/renovate/composer-composer-2.x
chore(deps): update dependency composer/composer to v2.6.6
2024-01-09 12:03:08 +01:00
Niklas Meyer
b491f6af9b Merge pull request #5615 from mailcow/fix/default-values
[Web] use template for default values in mbox and domain creation
2024-01-09 12:01:24 +01:00
Niklas Meyer
942ef7c254 Merge pull request #5592 from mailcow/feat/alpine-3.19
Update Dockerfiles to Alpine 3.19
2024-01-09 11:57:34 +01:00
DerLinkman
1ee3bb42f3 compose: updated image tags 2024-01-09 11:55:32 +01:00
DerLinkman
25007b1963 dockerapi: implemented lifespan function 2024-01-09 11:50:22 +01:00
DerLinkman
f442378377 dockerfiles: updated maintainer 2024-01-09 11:18:55 +01:00
DerLinkman
333b7ebc0c Fix Alpine 3.19 dependencies 2024-01-09 11:17:52 +01:00
Peter
5896766fc3 Update to Alpine 3.19 2024-01-09 11:17:51 +01:00
Niklas Meyer
89540aec28 Merge pull request #5612 from mailcow/feat/domain-wide-footer
[Rspamd] add option to skip domain wide footer on reply e-mails
2024-01-09 11:10:35 +01:00
DerLinkman
b960143045 translation: update de-de.json 2024-01-09 11:09:35 +01:00
DerLinkman
6ab45cf668 db: bumped version to newer timestamp 2024-01-08 14:43:25 +01:00
Niklas Meyer
fd206a7ef6 Merge pull request #5621 from mailcow/align-ehlo-keywords-to-fuctions
[Postfix] Remove pipeling from ehlo keywords as we block it in data
2024-01-08 09:52:28 +01:00
Niklas Meyer
1c7347d38d Merge pull request #5616 from FELDSAM-INC/feldsam/fix-form-dark-mode
Fixed bg color of form elements in dark mode
2024-01-08 09:51:48 +01:00
Niklas Meyer
7f58c422f2 Merge pull request #5625 from mailcow/update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2024-01-08 09:51:27 +01:00
Niklas Meyer
0a0e2b5e93 Merge pull request #5624 from mthld/patch-2
Add new SOGoMailHideInlineAttachments option to sogo.conf
2024-01-08 09:47:50 +01:00
milkmaker
de00c424f4 update postscreen_access.cidr 2024-01-01 00:15:27 +00:00
Mathilde
a249e2028d Add new SOGoMailHideInlineAttachments option to sogo.conf
SOGoMailHideInlineAttachments = YES; will allow to hide inline (body and footer) images being shown as attachments.
2023-12-30 10:16:25 +01:00
Dmitriy Alekseev
68036eeccf Update main.cf 2023-12-29 22:06:18 +02:00
Patrick Schult
cb0b0235f0 Merge pull request #5623 from mailcow/staging
🛷 🐄 Moocember 2023 Update Revision A | Postfix CVE-2023-51764 Security Update
2023-12-29 20:35:20 +01:00
FreddleSpl0it
6ff6f7a28d [Postfix] set smtpd_forbid_bare_newline = yes 2023-12-29 20:19:26 +01:00
milkmaker
0b628fb22d Translations update from Weblate (#5622)
* [Web] Updated lang.zh-tw.json

Co-authored-by: BallBill <xxx@billtang.ddns.net>

* [Web] Updated lang.pt-br.json

Co-authored-by: Abner Santana <abnerss@outlook.com>

---------

Co-authored-by: BallBill <xxx@billtang.ddns.net>
Co-authored-by: Abner Santana <abnerss@outlook.com>
2023-12-29 19:22:19 +01:00
Dmitriy Alekseev
b4bb11320f Update main.cf 2023-12-29 16:04:52 +02:00
Dmitriy Alekseev
c61938db23 [Postfix] Remove pipeling from ehlo keywords as we block it in data restrictions 2023-12-29 15:59:16 +02:00
Patrick Schult
acf9d5480c Merge pull request #5504 from FELDSAM-INC/feldsam/do-not-remove-x-mailer
[Postfix] Do not remove X-Mailer header
2023-12-27 18:40:19 +01:00
milkmaker
a1cb7fd778 [Web] Updated lang.zh-tw.json (#5617)
Co-authored-by: BallBill <xxx@billtang.ddns.net>
2023-12-27 18:03:24 +01:00
Kristian Feldsam
c24543fea0 [Web] Fixed form fields bg color in dark mode
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-12-27 17:33:12 +01:00
Kristian Feldsam
100e8ab00d [Postfix] Do not remove X-Mailer header
some providers, like seznam.cz use X-Mailer in DKIM signatures

Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-12-27 16:32:50 +01:00
FreddleSpl0it
38497b04ac [Web] use template for default values in mbox and domain creation 2023-12-27 14:57:27 +01:00
renovate[bot]
7bd27b920a chore(deps): update dependency nextcloud/server to v28.0.1 (#5614)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-24 18:24:01 +01:00
FreddleSpl0it
efab11720d add option to skip footer on reply e-mails 2023-12-22 10:39:07 +01:00
renovate[bot]
40fdf99a55 Update dependency composer/composer to v2.6.6
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-12-08 20:07:11 +00:00
Kristian Feldsam
efcca61f5a Mailboxes datatable - server side processing ordering
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-12-04 14:52:17 +01:00
Kristian Feldsam
4dad0002cd Domains datatable - server side processing ordering
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-12-04 14:15:57 +01:00
renovate[bot]
d4dd1e37ce Update dependency tianon/gosu to v1.17
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-11-21 09:03:09 +00:00
renovate[bot]
a8dfa95126 Update dependency phpredis/phpredis to v6.0.2
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-11-21 09:03:02 +00:00
renovate[bot]
4f109c1a94 Update dependency krakjoe/apcu to v5.1.23
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-11-12 17:28:57 +00:00
Kristian Feldsam
28cec99699 Mailboxes datatable - server side processing
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-11-12 10:35:26 +01:00
Kristian Feldsam
3e194c7906 Domains datatable - server side processing
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-11-12 10:35:22 +01:00
60 changed files with 1884 additions and 472 deletions

View File

@@ -62,6 +62,16 @@ body:
- nightly
validations:
required: true
- type: dropdown
attributes:
label: "Which architecture are you using?"
description: "#### `uname -m`"
multiple: false
options:
- x86
- ARM64 (aarch64)
validations:
required: true
- type: input
attributes:
label: "Operating System:"

View File

@@ -22,7 +22,7 @@ jobs:
bash helper-scripts/update_postscreen_whitelist.sh
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
commit-message: update postscreen_access.cidr

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@ data/conf/dovecot/acl_anyone
data/conf/dovecot/dovecot-master.passwd
data/conf/dovecot/dovecot-master.userdb
data/conf/dovecot/extra.conf
data/conf/dovecot/mail_replica.conf
data/conf/dovecot/global_sieve_*
data/conf/dovecot/last_login
data/conf/dovecot/lua

View File

@@ -1,7 +1,8 @@
FROM alpine:3.17
FROM alpine:3.18
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
ARG PIP_BREAK_SYSTEM_PACKAGES=1
RUN apk upgrade --no-cache \
&& apk add --update --no-cache \
bash \

View File

@@ -1,12 +1,14 @@
FROM clamav/clamav:1.0.3_base
FROM alpine:3.19
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
RUN apk upgrade --no-cache \
&& apk add --update --no-cache \
rsync \
clamav \
bind-tools \
bash
bash \
tini
# init
COPY clamd.sh /clamd.sh
@@ -14,7 +16,9 @@ RUN chmod +x /sbin/tini
# healthcheck
COPY healthcheck.sh /healthcheck.sh
COPY clamdcheck.sh /usr/local/bin
RUN chmod +x /healthcheck.sh
RUN chmod +x /usr/local/bin/clamdcheck.sh
HEALTHCHECK --start-period=6m CMD "/healthcheck.sh"
ENTRYPOINT []

View File

@@ -0,0 +1,14 @@
#!/bin/sh
set -eu
if [ "${CLAMAV_NO_CLAMD:-}" != "false" ]; then
if [ "$(echo "PING" | nc localhost 3310)" != "PONG" ]; then
echo "ERROR: Unable to contact server"
exit 1
fi
echo "Clamd is up"
fi
exit 0

View File

@@ -1,7 +1,8 @@
FROM alpine:3.17
FROM alpine:3.19
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
ARG PIP_BREAK_SYSTEM_PACKAGES=1
WORKDIR /app
RUN apk add --update --no-cache python3 \
@@ -9,12 +10,13 @@ RUN apk add --update --no-cache python3 \
openssl \
tzdata \
py3-psutil \
py3-redis \
py3-async-timeout \
&& pip3 install --upgrade pip \
fastapi \
uvicorn \
aiodocker \
docker \
aioredis
docker
RUN mkdir /app/modules
COPY docker-entrypoint.sh /app/

View File

@@ -5,16 +5,63 @@ import json
import uuid
import async_timeout
import asyncio
import aioredis
import aiodocker
import docker
import logging
from logging.config import dictConfig
from fastapi import FastAPI, Response, Request
from modules.DockerApi import DockerApi
from redis import asyncio as aioredis
from contextlib import asynccontextmanager
dockerapi = None
app = FastAPI()
@asynccontextmanager
async def lifespan(app: FastAPI):
global dockerapi
# Initialize a custom logger
logger = logging.getLogger("dockerapi")
logger.setLevel(logging.INFO)
# Configure the logger to output logs to the terminal
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
formatter = logging.Formatter("%(levelname)s: %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info("Init APP")
# Init redis client
if os.environ['REDIS_SLAVEOF_IP'] != "":
redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0")
else:
redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0")
# Init docker clients
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
logger.info("Subscribe to redis channel")
# Subscribe to redis channel
dockerapi.pubsub = redis.pubsub()
await dockerapi.pubsub.subscribe("MC_CHANNEL")
asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
yield
# Close docker connections
dockerapi.sync_docker_client.close()
await dockerapi.async_docker_client.close()
# Close redis
await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
await dockerapi.redis_client.close()
app = FastAPI(lifespan=lifespan)
# Define Routes
@app.get("/host/stats")
@@ -144,53 +191,7 @@ async def post_container_update_stats(container_id : str):
stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
# Events
@app.on_event("startup")
async def startup_event():
global dockerapi
# Initialize a custom logger
logger = logging.getLogger("dockerapi")
logger.setLevel(logging.INFO)
# Configure the logger to output logs to the terminal
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
formatter = logging.Formatter("%(levelname)s: %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info("Init APP")
# Init redis client
if os.environ['REDIS_SLAVEOF_IP'] != "":
redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0")
else:
redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0")
# Init docker clients
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
logger.info("Subscribe to redis channel")
# Subscribe to redis channel
dockerapi.pubsub = redis.pubsub()
await dockerapi.pubsub.subscribe("MC_CHANNEL")
asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
@app.on_event("shutdown")
async def shutdown_event():
global dockerapi
# Close docker connections
dockerapi.sync_docker_client.close()
await dockerapi.async_docker_client.close()
# Close redis
await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
await dockerapi.redis_client.close()
# PubSub Handler
async def handle_pubsub_messages(channel: aioredis.client.PubSub):

View File

@@ -1,119 +1,115 @@
FROM debian:bullseye-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
FROM alpine:3.19
LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
# renovate: datasource=github-tags depName=dovecot/core versioning=semver-coerced extractVersion=(?<version>.*)$
ARG DOVECOT=2.3.21
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=(?<version>.*)$
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.16
ENV LC_ALL C
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
# Add groups and users before installing Dovecot to not break compatibility
RUN groupadd -g 5000 vmail \
&& groupadd -g 401 dovecot \
&& groupadd -g 402 dovenull \
&& groupadd -g 999 sogo \
&& usermod -a -G sogo nobody \
&& useradd -g vmail -u 5000 vmail -d /var/vmail \
&& useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \
&& useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull \
&& touch /etc/default/locale \
&& apt-get update \
&& apt-get -y --no-install-recommends install \
build-essential \
apt-transport-https \
RUN addgroup -g 5000 vmail \
&& addgroup -g 401 dovecot \
&& addgroup -g 402 dovenull \
&& sed -i "s/999/99/" /etc/group \
&& addgroup -g 999 sogo \
&& addgroup nobody sogo \
&& adduser -D -u 5000 -G vmail -h /var/vmail vmail \
&& adduser -D -G dovecot -u 401 -h /dev/null -s /sbin/nologin dovecot \
&& adduser -D -G dovenull -u 402 -h /dev/null -s /sbin/nologin dovenull \
&& apk add --no-cache --update \
bash \
bind-tools \
findutils \
envsubst \
ca-certificates \
cpanminus \
curl \
dnsutils \
dirmngr \
gettext \
gnupg2 \
jq \
libauthen-ntlm-perl \
libcgi-pm-perl \
libcrypt-openssl-rsa-perl \
libcrypt-ssleay-perl \
libdata-uniqid-perl \
libdbd-mysql-perl \
libdbi-perl \
libdigest-hmac-perl \
libdist-checkconflicts-perl \
libencode-imaputf7-perl \
libfile-copy-recursive-perl \
libfile-tail-perl \
libhtml-parser-perl \
libio-compress-perl \
libio-socket-inet6-perl \
libio-socket-ssl-perl \
libio-tee-perl \
libipc-run-perl \
libjson-webtoken-perl \
liblockfile-simple-perl \
libmail-imapclient-perl \
libmodule-implementation-perl \
libmodule-scandeps-perl \
libnet-ssleay-perl \
libpackage-stash-perl \
libpackage-stash-xs-perl \
libpar-packer-perl \
libparse-recdescent-perl \
libproc-processtable-perl \
libreadonly-perl \
libregexp-common-perl \
libssl-dev \
libsys-meminfo-perl \
libterm-readkey-perl \
libtest-deep-perl \
libtest-fatal-perl \
libtest-mock-guard-perl \
libtest-mockobject-perl \
libtest-nowarnings-perl \
libtest-pod-perl \
libtest-requires-perl \
libtest-simple-perl \
libtest-warn-perl \
libtry-tiny-perl \
libunicode-string-perl \
liburi-perl \
libwww-perl \
lua-sql-mysql \
lua \
lua-cjson \
lua-socket \
lua-sql-mysql \
lua5.3-sql-mysql \
icu-data-full \
mariadb-connector-c \
gcompat \
mariadb-client \
perl \
perl-ntlm \
perl-cgi \
perl-crypt-openssl-rsa \
perl-utils \
perl-crypt-ssleay \
perl-data-uniqid \
perl-dbd-mysql \
perl-dbi \
perl-digest-hmac \
perl-dist-checkconflicts \
perl-encode-imaputf7 \
perl-file-copy-recursive \
perl-file-tail \
perl-io-socket-inet6 \
perl-io-gzip \
perl-io-socket-ssl \
perl-io-tee \
perl-ipc-run \
perl-json-webtoken \
perl-mail-imapclient \
perl-module-implementation \
perl-module-scandeps \
perl-net-ssleay \
perl-package-stash \
perl-package-stash-xs \
perl-par-packer \
perl-parse-recdescent \
perl-lockfile-simple --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/ \
libproc \
perl-readonly \
perl-regexp-common \
perl-sys-meminfo \
perl-term-readkey \
perl-test-deep \
perl-test-fatal \
perl-test-mockobject \
perl-test-mock-guard \
perl-test-pod \
perl-test-requires \
perl-test-simple \
perl-test-warn \
perl-try-tiny \
perl-unicode-string \
perl-proc-processtable \
perl-app-cpanminus \
procps \
python3-pip \
redis-server \
supervisor \
python3 \
py3-mysqlclient \
py3-html2text \
py3-jinja2 \
py3-redis \
redis \
syslog-ng \
syslog-ng-core \
syslog-ng-mod-redis \
syslog-ng-redis \
syslog-ng-json \
supervisor \
tzdata \
wget \
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
&& chmod +x /usr/local/bin/gosu \
&& gosu nobody true \
&& apt-key adv --fetch-keys https://repo.dovecot.org/DOVECOT-REPO-GPG \
&& echo "deb https://repo.dovecot.org/ce-${DOVECOT}/debian/bullseye bullseye main" > /etc/apt/sources.list.d/dovecot.list \
&& apt-get update \
&& apt-get -y --no-install-recommends install \
dovecot-lua \
dovecot-managesieved \
dovecot-sieve \
dovecot \
dovecot-dev \
dovecot-lmtpd \
dovecot-lua \
dovecot-ldap \
dovecot-mysql \
dovecot-core \
dovecot-sql \
dovecot-submissiond \
dovecot-pigeonhole-plugin \
dovecot-pop3d \
dovecot-imapd \
dovecot-solr \
&& pip3 install mysql-connector-python html2text jinja2 redis \
&& apt-get autoremove --purge -y \
&& apt-get autoclean \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /tmp/* /var/tmp/* /root/.cache/
# imapsync dependencies
RUN cpan Crypt::OpenSSL::PKCS12
dovecot-fts-solr \
&& arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$arch" \
&& chmod +x /usr/local/bin/gosu \
&& gosu nobody true
# RUN cpan LockFile::Simple
COPY trim_logs.sh /usr/local/bin/trim_logs.sh
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh

View File

@@ -335,6 +335,15 @@ sys.exit()
EOF
fi
# Set mail_replica for HA setups
if [[ -n ${MAILCOW_REPLICA_IP} && -n ${DOVEADM_REPLICA_PORT} ]]; then
cat <<EOF > /etc/dovecot/mail_replica.conf
# Autogenerated by mailcow
mail_replica = tcp:${MAILCOW_REPLICA_IP}:${DOVEADM_REPLICA_PORT}
EOF
fi
# 401 is user dovecot
if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then
openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem
@@ -432,4 +441,8 @@ done
# May be related to something inside Docker, I seriously don't know
touch /etc/dovecot/lua/passwd-verify.lua
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
fi
exec "$@"

View File

@@ -3,11 +3,10 @@
import smtplib
import os
import sys
import mysql.connector
import MySQLdb
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import COMMASPACE, formatdate
import cgi
import jinja2
from jinja2 import Template
import json
@@ -50,7 +49,7 @@ try:
def query_mysql(query, headers = True, update = False):
while True:
try:
cnx = mysql.connector.connect(unix_socket = '/var/run/mysqld/mysqld.sock', user=os.environ.get('DBUSER'), passwd=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
cnx = MySQLdb.connect(user=os.environ.get('DBUSER'), password=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
except Exception as ex:
print('%s - trying again...' % (ex))
time.sleep(3)

View File

@@ -55,7 +55,7 @@ try:
msg.attach(text_part)
msg.attach(html_part)
msg['To'] = username
p = Popen(['/usr/lib/dovecot/dovecot-lda', '-d', username, '-o', '"plugin/quota=maildir:User quota:noenforcing"'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)
p = Popen(['/usr/libexec/dovecot/dovecot-lda', '-d', username, '-o', '"plugin/quota=maildir:User quota:noenforcing"'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)
p.communicate(input=bytes(msg.as_string(), 'utf-8'))
domain = username.split("@")[-1]

View File

@@ -11,7 +11,7 @@ fi
# Is replication active?
# grep on file is less expensive than doveconf
if ! grep -qi mail_replica /etc/dovecot/dovecot.conf; then
if [ -n ${MAILCOW_REPLICA_IP} ]; then
${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
exit
fi

View File

@@ -13,6 +13,10 @@ autostart=true
[program:dovecot]
command=/usr/sbin/dovecot -F
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
[eventlistener:processes]

View File

@@ -1,4 +1,4 @@
@version: 3.28
@version: 4.5
@include "scl.conf"
options {
chain_hostnames(off);
@@ -6,11 +6,11 @@ options {
use_dns(no);
use_fqdn(no);
owner("root"); group("adm"); perm(0640);
stats_freq(0);
stats(freq(0));
bad_hostname("^gconfd$");
};
source s_src {
unix-stream("/dev/log");
source s_dgram {
unix-dgram("/dev/log");
internal();
};
destination d_stdout { pipe("/dev/stdout"); };
@@ -36,7 +36,7 @@ filter f_replica {
not match("Error: sync: Unknown user in remote" value("MESSAGE"));
};
log {
source(s_src);
source(s_dgram);
filter(f_replica);
destination(d_stdout);
filter(f_mail);

View File

@@ -1,4 +1,4 @@
@version: 3.28
@version: 4.5
@include "scl.conf"
options {
chain_hostnames(off);
@@ -6,11 +6,11 @@ options {
use_dns(no);
use_fqdn(no);
owner("root"); group("adm"); perm(0640);
stats_freq(0);
stats(freq(0));
bad_hostname("^gconfd$");
};
source s_src {
unix-stream("/dev/log");
source s_dgram {
unix-dgram("/dev/log");
internal();
};
destination d_stdout { pipe("/dev/stdout"); };
@@ -36,7 +36,7 @@ filter f_replica {
not match("Error: sync: Unknown user in remote" value("MESSAGE"));
};
log {
source(s_src);
source(s_dgram);
filter(f_replica);
destination(d_stdout);
filter(f_mail);

View File

@@ -1,8 +1,9 @@
FROM alpine:3.17
FROM alpine:3.19
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
WORKDIR /app
ARG PIP_BREAK_SYSTEM_PACKAGES=1
ENV XTABLES_LIBDIR /usr/lib/xtables
ENV PYTHON_IPTABLES_XTABLES_VERSION 12
ENV IPTABLES_LIBDIR /usr/lib
@@ -14,6 +15,7 @@ RUN apk add --virtual .build-deps \
openssl-dev \
&& apk add -U python3 \
iptables \
iptables-dev \
ip6tables \
xtables-addons \
nftables \

View File

@@ -21,28 +21,6 @@ from modules.IPTables import IPTables
from modules.NFTables import NFTables
# connect to redis
while True:
try:
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
if "".__eq__(redis_slaveof_ip):
r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
else:
r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)
r.ping()
except Exception as ex:
print('%s - trying again in 3 seconds' % (ex))
time.sleep(3)
else:
break
pubsub = r.pubsub()
# rename fail2ban to netfilter
if r.exists('F2B_LOG'):
r.rename('F2B_LOG', 'NETFILTER_LOG')
# globals
WHITELIST = []
BLACKLIST= []
@@ -50,18 +28,10 @@ bans = {}
quit_now = False
exit_code = 0
lock = Lock()
# init Logger
logger = Logger(r)
# init backend
backend = sys.argv[1]
if backend == "nftables":
logger.logInfo('Using NFTables backend')
tables = NFTables("MAILCOW", logger)
else:
logger.logInfo('Using IPTables backend')
tables = IPTables("MAILCOW", logger)
chain_name = "MAILCOW"
r = None
pubsub = None
clear_before_quit = False
def refreshF2boptions():
@@ -250,17 +220,21 @@ def clear():
with lock:
tables.clearIPv4Table()
tables.clearIPv6Table()
r.delete('F2B_ACTIVE_BANS')
r.delete('F2B_PERM_BANS')
pubsub.unsubscribe()
try:
if r is not None:
r.delete('F2B_ACTIVE_BANS')
r.delete('F2B_PERM_BANS')
except Exception as ex:
logger.logWarn('Error clearing redis keys F2B_ACTIVE_BANS and F2B_PERM_BANS: %s' % ex)
def watch():
logger.logInfo('Watching Redis channel F2B_CHANNEL')
pubsub.subscribe('F2B_CHANNEL')
global pubsub
global quit_now
global exit_code
logger.logInfo('Watching Redis channel F2B_CHANNEL')
pubsub.subscribe('F2B_CHANNEL')
while not quit_now:
try:
for item in pubsub.listen():
@@ -280,6 +254,7 @@ def watch():
ban(addr)
except Exception as ex:
logger.logWarn('Error reading log line from pubsub: %s' % ex)
pubsub = None
quit_now = True
exit_code = 2
@@ -403,21 +378,76 @@ def blacklistUpdate():
permBan(net=net, unban=True)
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
def quit(signum, frame):
global quit_now
quit_now = True
def sigterm_quit(signum, frame):
global clear_before_quit
clear_before_quit = True
sys.exit(exit_code)
def berfore_quit():
if clear_before_quit:
clear()
if pubsub is not None:
pubsub.unsubscribe()
if __name__ == '__main__':
refreshF2boptions()
atexit.register(berfore_quit)
signal.signal(signal.SIGTERM, sigterm_quit)
# init Logger
logger = Logger(None)
# init backend
backend = sys.argv[1]
if backend == "nftables":
logger.logInfo('Using NFTables backend')
tables = NFTables(chain_name, logger)
else:
logger.logInfo('Using IPTables backend')
tables = IPTables(chain_name, logger)
# In case a previous session was killed without cleanup
clear()
# Reinit MAILCOW chain
# Is called before threads start, no locking
logger.logInfo("Initializing mailcow netfilter chain")
tables.initChainIPv4()
tables.initChainIPv6()
if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE").lower() in ("y", "yes"):
logger.logInfo(f"Skipping {chain_name} isolation")
else:
logger.logInfo(f"Setting {chain_name} isolation")
tables.create_mailcow_isolation_rule("br-mailcow", [3306, 6379, 8983, 12345], os.getenv("MAILCOW_REPLICA_IP"))
# connect to redis
while True:
try:
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
if "".__eq__(redis_slaveof_ip):
r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
else:
r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)
r.ping()
pubsub = r.pubsub()
except Exception as ex:
print('%s - trying again in 3 seconds' % (ex))
time.sleep(3)
else:
break
Logger.r = r
# rename fail2ban to netfilter
if r.exists('F2B_LOG'):
r.rename('F2B_LOG', 'NETFILTER_LOG')
# clear bans in redis
r.delete('F2B_ACTIVE_BANS')
r.delete('F2B_PERM_BANS')
refreshF2boptions()
watch_thread = Thread(target=watch)
watch_thread.daemon = True
watch_thread.start()
@@ -460,9 +490,6 @@ if __name__ == '__main__':
whitelistupdate_thread.daemon = True
whitelistupdate_thread.start()
signal.signal(signal.SIGTERM, quit)
atexit.register(clear)
while not quit_now:
time.sleep(0.5)

View File

@@ -1,5 +1,6 @@
import iptc
import time
import os
class IPTables:
def __init__(self, chain_name, logger):
@@ -211,3 +212,41 @@ class IPTables:
target = rule.create_target("SNAT")
target.to_source = snat_target
return rule
def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""):
try:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name)
# insert mailcow isolation rule
rule = iptc.Rule()
rule.in_interface = f'! {_interface}'
rule.out_interface = _interface
rule.protocol = 'tcp'
rule.create_target("DROP")
match = rule.create_match("multiport")
match.dports = ','.join(map(str, _dports))
if rule in chain.rules:
chain.delete_rule(rule)
chain.insert_rule(rule, position=0)
# insert mailcow isolation exception rule
if _allow != "":
rule = iptc.Rule()
rule.src = _allow
rule.in_interface = f'! {_interface}'
rule.out_interface = _interface
rule.protocol = 'tcp'
rule.create_target("ACCEPT")
match = rule.create_match("multiport")
match.dports = ','.join(map(str, _dports))
if rule in chain.rules:
chain.delete_rule(rule)
chain.insert_rule(rule, position=0)
return True
except Exception as e:
self.logger.logCrit(f"Error adding {self.chain_name} isolation: {e}")
return False

View File

@@ -10,7 +10,8 @@ class Logger:
tolog['time'] = int(round(time.time()))
tolog['priority'] = priority
tolog['message'] = message
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
if self.r:
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
print(message)
def logWarn(self, message):

View File

@@ -1,5 +1,6 @@
import nftables
import ipaddress
import os
class NFTables:
def __init__(self, chain_name, logger):
@@ -266,6 +267,17 @@ class NFTables:
return self.nft_exec_dict(delete_command)
def delete_filter_rule(self, _family:str, _chain: str, _handle:str):
delete_command = self.get_base_dict()
_rule_opts = {'family': _family,
'table': 'filter',
'chain': _chain,
'handle': _handle }
_delete = {'delete': {'rule': _rule_opts} }
delete_command["nftables"].append(_delete)
return self.nft_exec_dict(delete_command)
def snat_rule(self, _family: str, snat_target: str, source_address: str):
chain_name = self.nft_chain_names[_family]['nat']['postrouting']
@@ -381,7 +393,7 @@ class NFTables:
break
return chain_handle
def get_rules_handle(self, _family: str, _table: str, chain_name: str):
def get_rules_handle(self, _family: str, _table: str, chain_name: str, _comment_filter = "mailcow"):
rule_handle = []
# Command: 'nft list chain {family} {table} {chain_name}'
_chain_opts = {'family': _family, 'table': _table, 'name': chain_name}
@@ -397,7 +409,7 @@ class NFTables:
rule = _object["rule"]
if rule["family"] == _family and rule["table"] == _table and rule["chain"] == chain_name:
if rule.get("comment") and rule["comment"] == "mailcow":
if rule.get("comment") and rule["comment"] == _comment_filter:
rule_handle.append(rule["handle"])
return rule_handle
@@ -493,3 +505,152 @@ class NFTables:
position+=1
return position if rule_found else False
def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""):
family = "ip"
table = "filter"
comment_filter_drop = "mailcow isolation"
comment_filter_allow = "mailcow isolation allow"
json_command = self.get_base_dict()
# Delete old mailcow isolation rules
handles = self.get_rules_handle(family, table, self.chain_name, comment_filter_drop)
for handle in handles:
self.delete_filter_rule(family, self.chain_name, handle)
handles = self.get_rules_handle(family, table, self.chain_name, comment_filter_allow)
for handle in handles:
self.delete_filter_rule(family, self.chain_name, handle)
# insert mailcow isolation rule
_match_dict_drop = [
{
"match": {
"op": "!=",
"left": {
"meta": {
"key": "iifname"
}
},
"right": _interface
}
},
{
"match": {
"op": "==",
"left": {
"meta": {
"key": "oifname"
}
},
"right": _interface
}
},
{
"match": {
"op": "==",
"left": {
"payload": {
"protocol": "tcp",
"field": "dport"
}
},
"right": {
"set": _dports
}
}
},
{
"counter": {
"packets": 0,
"bytes": 0
}
},
{
"drop": None
}
]
rule_drop = { "insert": { "rule": {
"family": family,
"table": table,
"chain": self.chain_name,
"comment": comment_filter_drop,
"expr": _match_dict_drop
}}}
json_command["nftables"].append(rule_drop)
# insert mailcow isolation allow rule
if _allow != "":
_match_dict_allow = [
{
"match": {
"op": "==",
"left": {
"payload": {
"protocol": "ip",
"field": "saddr"
}
},
"right": _allow
}
},
{
"match": {
"op": "!=",
"left": {
"meta": {
"key": "iifname"
}
},
"right": _interface
}
},
{
"match": {
"op": "==",
"left": {
"meta": {
"key": "oifname"
}
},
"right": _interface
}
},
{
"match": {
"op": "==",
"left": {
"payload": {
"protocol": "tcp",
"field": "dport"
}
},
"right": {
"set": _dports
}
}
},
{
"counter": {
"packets": 0,
"bytes": 0
}
},
{
"accept": None
}
]
rule_allow = { "insert": { "rule": {
"family": family,
"table": table,
"chain": self.chain_name,
"comment": comment_filter_allow,
"expr": _match_dict_allow
}}}
json_command["nftables"].append(rule_allow)
success = self.nft_exec_dict(json_command)
if success == False:
self.logger.logCrit(f"Error adding {self.chain_name} isolation")
return False
return True

View File

@@ -1,6 +1,7 @@
FROM alpine:3.17
FROM alpine:3.19
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
ARG PIP_BREAK_SYSTEM_PACKAGES=1
WORKDIR /app
#RUN addgroup -S olefy && adduser -S olefy -G olefy \

View File

@@ -1,8 +1,8 @@
FROM php:8.2-fpm-alpine3.17
FROM php:8.2-fpm-alpine3.18
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.22
ARG APCU_PECL_VERSION=5.1.23
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
ARG IMAGICK_PECL_VERSION=3.7.0
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
@@ -10,9 +10,9 @@ ARG MAILPARSE_PECL_VERSION=3.1.6
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MEMCACHED_PECL_VERSION=3.2.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
ARG REDIS_PECL_VERSION=6.0.1
ARG REDIS_PECL_VERSION=6.0.2
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
ARG COMPOSER_VERSION=2.6.5
ARG COMPOSER_VERSION=2.6.6
RUN apk add -U --no-cache autoconf \
aspell-dev \

View File

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

View File

@@ -1,5 +1,5 @@
FROM debian:bullseye-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG CODENAME=bullseye
@@ -13,7 +13,7 @@ RUN apt-get update && apt-get install -y \
dnsutils \
netcat \
&& apt-key adv --fetch-keys https://rspamd.com/apt-stable/gpg.key \
&& echo "deb [arch=amd64] https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \
&& echo "deb https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \
&& apt-get update \
&& apt-get --no-install-recommends -y install rspamd redis-tools procps nano \
&& rm -rf /var/lib/apt/lists/* \

View File

@@ -1,10 +1,11 @@
FROM debian:bullseye-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
FROM debian:bookworm-slim
LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG SOGO_DEBIAN_REPOSITORY=http://packages.sogo.nu/nightly/5/debian/
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.16
ARG GOSU_VERSION=1.17
ENV LC_ALL C
# Prerequisites
@@ -21,7 +22,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
syslog-ng-core \
syslog-ng-mod-redis \
dirmngr \
netcat \
netcat-traditional \
psmisc \
wget \
patch \
@@ -32,7 +33,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
&& mkdir /usr/share/doc/sogo \
&& touch /usr/share/doc/sogo/empty.sh \
&& apt-key adv --keyserver keys.openpgp.org --recv-key 74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 \
&& echo "deb ${SOGO_DEBIAN_REPOSITORY} bullseye bullseye" > /etc/apt/sources.list.d/sogo.list \
&& echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} sogo-v5" > /etc/apt/sources.list.d/sogo.list \
&& apt-get update && apt-get install -y --no-install-recommends \
sogo \
sogo-activesync \

View File

@@ -3,7 +3,7 @@ FROM solr:7.7-slim
USER root
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=(?<version>.*)$
ARG GOSU_VERSION=1.16
ARG GOSU_VERSION=1.17
COPY solr.sh /
COPY solr-config-7.7.0.xml /

View File

@@ -1,9 +1,11 @@
FROM alpine:3.17
FROM alpine:3.18
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
RUN apk add --update --no-cache \
curl \
bind-tools \
netcat-openbsd \
unbound \
bash \
openssl \
@@ -21,7 +23,7 @@ COPY docker-entrypoint.sh /docker-entrypoint.sh
# healthcheck (nslookup)
COPY healthcheck.sh /healthcheck.sh
RUN chmod +x /healthcheck.sh
HEALTHCHECK --interval=30s --timeout=10s CMD [ "/healthcheck.sh" ]
HEALTHCHECK --interval=5s --timeout=30s CMD [ "/healthcheck.sh" ]
ENTRYPOINT ["/docker-entrypoint.sh"]

View File

@@ -1,12 +1,99 @@
#!/bin/bash
nslookup mailcow.email 127.0.0.1 1> /dev/null
# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!)
if [[ "${SKIP_UNBOUND_HEALTHCHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
SKIP_UNBOUND_HEALTHCHECK=y
fi
if [ $? == 0 ]; then
echo "DNS resolution is working!"
# Declare log function for logfile inside container
function log_to_file() {
echo "$(date +"%Y-%m-%d %H:%M:%S"): $1" > /var/log/healthcheck.log
}
# General Ping function to check general pingability
function check_ping() {
declare -a ipstoping=("1.1.1.1" "8.8.8.8" "9.9.9.9")
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
fi
done
log_to_file "Healthcheck: Ping Checks WORKING properly!"
return 0
}
# General DNS Resolve Check against Unbound Resolver himself
function check_dns() {
declare -a domains=("mailcow.email" "github.com" "hub.docker.com")
for domain in "${domains[@]}" ; do
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!"
return 1
fi
fi
done
done
log_to_file "Healthcheck: DNS Resolver WORKING properly!"
return 0
}
# Simple Netcat Check to connect to common webports
function check_netcat() {
declare -a domains=("mailcow.email" "github.com" "hub.docker.com")
declare -a ports=("80" "443")
for domain in "${domains[@]}" ; do
for port in "${ports[@]}" ; do
nc -z -w 2 $domain $port
if [ $? -ne 0 ]; then
log_to_file "Healthcheck: Could not reach $domain on Port $port... Gave up!"
log_to_file "Please check your internet connection or firewall rules to fix this error."
return 1
fi
done
done
log_to_file "Healthcheck: Netcat Checks WORKING properly!"
return 0
}
if [[ ${SKIP_UNBOUND_HEALTHCHECK} == "y" ]]; then
log_to_file "Healthcheck: ALL CHECKS WERE SKIPPED! Unbound is healthy!"
exit 0
else
echo "DNS resolution is not working correctly..."
echo "Maybe check your outbound firewall, as it needs to resolve DNS over TCP AND UDP!"
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
check_dns
if [ $? -ne 0 ]; then
exit 1
fi
check_netcat
if [ $? -ne 0 ]; then
exit 1
fi
log_to_file "Healthcheck: ALL CHECKS WERE SUCCESSFUL! Unbound is healthy!"
exit 0

View File

@@ -1,5 +1,5 @@
FROM alpine:3.17
LABEL maintainer "André Peters <andre.peters@servercow.de>"
FROM alpine:3.18
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
# Installation
RUN apk add --update \

View File

@@ -170,7 +170,7 @@ function notify_error() {
fi
# Replace subject and body placeholders
WEBHOOK_BODY=$(echo ${WATCHDOG_NOTIFY_WEBHOOK_BODY} | sed "s|\$SUBJECT\|\${SUBJECT}|$SUBJECT|g" | sed "s|\$BODY\|\${BODY}|$BODY|")
WEBHOOK_BODY=$(echo ${WATCHDOG_NOTIFY_WEBHOOK_BODY} | sed "s/\$SUBJECT\|\${SUBJECT}/$SUBJECT/g" | sed "s/\$BODY\|\${BODY}/$BODY/g")
# POST to webhook
curl -X POST -H "Content-Type: application/json" ${CURL_VERBOSE} -d "${WEBHOOK_BODY}" ${WATCHDOG_NOTIFY_WEBHOOK}
@@ -716,8 +716,8 @@ 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)
if [[ ${SCORE} != "9999" ]]; then
' | 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/\..*//' )
if [[ ${SCORE} -ne 9999 ]]; then
echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2
err_count=$(( ${err_count} + 1))
else

View File

@@ -247,6 +247,9 @@ plugin {
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
mail_log_fields = uid box msgid size
mail_log_cached_only = yes
# Try set mail_replica
!include_try /etc/dovecot/mail_replica.conf
}
service quota-warning {
executable = script /usr/local/bin/quota_notify.py

View File

@@ -12,7 +12,8 @@ if /^\s*Received: from.* \(.*rspamd-mailcow.*mailcow-network.*\).*\(Postcow\)/
REPLACE Received: from rspamd (rspamd $3) by $4 (Postcow) with $5
endif
/^\s*X-Enigmail/ IGNORE
/^\s*X-Mailer/ IGNORE
# Not removing Mailer by default, might be signed
#/^\s*X-Mailer/ IGNORE
/^\s*X-Originating-IP/ IGNORE
/^\s*X-Forward/ IGNORE
# Not removing UA by default, might be signed

View File

@@ -11,6 +11,7 @@ smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtpd_relay_restrictions = permit_mynetworks,
permit_sasl_authenticated,
defer_unauth_destination
smtpd_forbid_bare_newline = yes
# alias maps are auto-generated in postfix.sh on startup
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
@@ -84,6 +85,7 @@ smtp_tls_security_level = dane
smtpd_data_restrictions = reject_unauth_pipelining, permit
smtpd_delay_reject = yes
smtpd_error_sleep_time = 10s
smtpd_forbid_bare_newline = yes
smtpd_hard_error_limit = ${stress?1}${stress:5}
smtpd_helo_required = yes
smtpd_proxy_timeout = 600s
@@ -160,7 +162,8 @@ transport_maps = pcre:/opt/postfix/conf/custom_transport.pcre,
proxy:mysql:/opt/postfix/conf/sql/mysql_relay_ne.cf,
proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf
smtp_sasl_auth_soft_bounce = no
postscreen_discard_ehlo_keywords = silent-discard, dsn
postscreen_discard_ehlo_keywords = silent-discard, dsn, chunking
smtpd_discard_ehlo_keywords = chunking
compatibility_level = 2
smtputf8_enable = no
# Define protocols for SMTPS and submission service

View File

@@ -1,9 +1,10 @@
# Whitelist generated by Postwhite v3.4 on Fri Dec 1 00:15:18 UTC 2023
# Whitelist generated by Postwhite v3.4 on Thu Feb 1 00:13:50 UTC 2024
# https://github.com/stevejenkins/postwhite/
# 2038 total rules
# 2089 total rules
2a00:1450:4000::/36 permit
2a01:111:f400::/48 permit
2a01:111:f403:8000::/50 permit
2a01:111:f403:8000::/51 permit
2a01:111:f403::/49 permit
2a01:111:f403:c000::/51 permit
2a01:111:f403:f000::/52 permit
@@ -13,7 +14,7 @@
3.70.123.177 permit
3.93.157.0/24 permit
3.129.120.190 permit
3.137.78.75 permit
3.137.16.58 permit
3.210.190.0/24 permit
8.20.114.31 permit
8.25.194.0/23 permit
@@ -116,7 +117,6 @@
40.92.0.0/16 permit
40.107.0.0/16 permit
40.112.65.63 permit
40.117.80.0/24 permit
43.228.184.0/22 permit
44.206.138.57 permit
44.209.42.157 permit
@@ -183,6 +183,8 @@
50.18.125.237 permit
50.18.126.162 permit
50.31.32.0/19 permit
50.56.130.220 permit
50.56.130.221 permit
51.137.58.21 permit
51.140.75.55 permit
51.144.100.179 permit
@@ -204,7 +206,6 @@
52.95.49.88/29 permit
52.96.91.34 permit
52.96.111.82 permit
52.96.172.98 permit
52.96.214.50 permit
52.96.222.194 permit
52.96.222.226 permit
@@ -403,7 +404,6 @@
66.196.81.228/30 permit
66.196.81.232/31 permit
66.196.81.234 permit
66.211.168.230/31 permit
66.211.170.88/29 permit
66.211.184.0/23 permit
66.218.74.64/30 permit
@@ -592,10 +592,12 @@
74.112.67.243 permit
74.125.0.0/16 permit
74.202.227.40 permit
74.208.4.192/26 permit
74.208.5.64/26 permit
74.208.122.0/26 permit
74.208.4.200 permit
74.208.4.201 permit
74.208.4.220 permit
74.208.4.221 permit
74.209.250.0/24 permit
75.2.70.75 permit
76.223.128.0/19 permit
76.223.176.0/20 permit
77.238.176.0/22 permit
@@ -621,12 +623,20 @@
77.238.189.148/30 permit
81.7.169.128/25 permit
81.223.46.0/27 permit
82.165.159.0/24 permit
82.165.159.0/26 permit
82.165.229.31 permit
82.165.229.130 permit
82.165.230.21 permit
82.165.230.22 permit
82.165.159.2 permit
82.165.159.3 permit
82.165.159.4 permit
82.165.159.12 permit
82.165.159.13 permit
82.165.159.14 permit
82.165.159.34 permit
82.165.159.35 permit
82.165.159.40 permit
82.165.159.41 permit
82.165.159.42 permit
82.165.159.45 permit
82.165.159.130 permit
82.165.159.131 permit
84.116.6.0/23 permit
84.116.36.0/24 permit
84.116.50.0/23 permit
@@ -1186,6 +1196,7 @@
98.139.245.208/30 permit
98.139.245.212/31 permit
99.78.197.208/28 permit
99.83.190.102 permit
103.2.140.0/22 permit
103.9.96.0/22 permit
103.28.42.0/24 permit
@@ -1426,6 +1437,7 @@
135.84.216.0/22 permit
136.143.160.0/24 permit
136.143.161.0/24 permit
136.143.178.49 permit
136.143.182.0/23 permit
136.143.184.0/24 permit
136.143.188.0/24 permit
@@ -1460,6 +1472,8 @@
144.178.38.0/24 permit
145.253.228.160/29 permit
145.253.239.128/29 permit
146.20.14.105 permit
146.20.14.107 permit
146.20.112.0/26 permit
146.20.113.0/24 permit
146.20.191.0/24 permit
@@ -1534,6 +1548,10 @@
163.47.180.0/23 permit
163.114.130.16 permit
163.114.132.120 permit
164.177.132.168 permit
164.177.132.169 permit
164.177.132.170 permit
164.177.132.171 permit
165.173.128.0/24 permit
166.78.68.0/22 permit
166.78.68.221 permit
@@ -1726,6 +1744,7 @@
199.34.22.36 permit
199.59.148.0/22 permit
199.67.80.2 permit
199.67.82.2 permit
199.67.84.0/24 permit
199.67.86.0/24 permit
199.67.88.0/24 permit
@@ -1789,6 +1808,7 @@
204.92.114.187 permit
204.92.114.203 permit
204.92.114.204/31 permit
204.132.224.66 permit
204.141.32.0/23 permit
204.141.42.0/23 permit
204.220.160.0/20 permit
@@ -1832,6 +1852,8 @@
207.67.98.192/27 permit
207.68.176.0/26 permit
207.68.176.96/27 permit
207.97.204.96 permit
207.97.204.97 permit
207.126.144.0/20 permit
207.171.160.0/19 permit
207.211.30.64/26 permit
@@ -1938,12 +1960,41 @@
212.82.111.228/31 permit
212.82.111.230 permit
212.123.28.40 permit
212.227.15.0/24 permit
212.227.15.0/25 permit
212.227.17.0/27 permit
212.227.126.128/25 permit
212.227.15.3 permit
212.227.15.4 permit
212.227.15.5 permit
212.227.15.6 permit
212.227.15.14 permit
212.227.15.15 permit
212.227.15.18 permit
212.227.15.19 permit
212.227.15.25 permit
212.227.15.26 permit
212.227.15.29 permit
212.227.15.44 permit
212.227.15.45 permit
212.227.15.46 permit
212.227.15.47 permit
212.227.15.50 permit
212.227.15.52 permit
212.227.15.53 permit
212.227.15.54 permit
212.227.15.55 permit
212.227.17.11 permit
212.227.17.12 permit
212.227.17.18 permit
212.227.17.19 permit
212.227.17.20 permit
212.227.17.21 permit
212.227.17.22 permit
212.227.17.26 permit
212.227.17.28 permit
212.227.17.29 permit
212.227.126.224 permit
212.227.126.225 permit
212.227.126.226 permit
212.227.126.227 permit
213.46.255.0/24 permit
213.165.64.0/23 permit
213.199.128.139 permit
213.199.128.145 permit
213.199.138.181 permit
@@ -2007,10 +2058,10 @@
216.203.30.55 permit
216.203.33.178/31 permit
216.205.24.0/24 permit
216.221.160.0/19 permit
216.239.32.0/19 permit
217.72.192.64/26 permit
217.72.192.248/29 permit
217.72.207.0/27 permit
217.72.192.77 permit
217.72.192.78 permit
217.77.141.52 permit
217.77.141.59 permit
217.175.194.0/24 permit

View File

@@ -49,13 +49,14 @@ $from = $headers['From'];
$empty_footer = json_encode(array(
'html' => '',
'plain' => '',
'skip_replies' => 0,
'vars' => array()
));
error_log("FOOTER: checking for domain " . $domain . ", user " . $username . " and address " . $from . PHP_EOL);
try {
$stmt = $pdo->prepare("SELECT `plain`, `html`, `mbox_exclude` FROM `domain_wide_footer`
$stmt = $pdo->prepare("SELECT `plain`, `html`, `mbox_exclude`, `skip_replies` FROM `domain_wide_footer`
WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain

View File

@@ -567,6 +567,14 @@ rspamd_config:register_symbol({
if footer and type(footer) == "table" and (footer.html and footer.html ~= "" or footer.plain and footer.plain ~= "") then
rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s, vars=%s", uname, footer.html, footer.plain, footer.vars)
if footer.skip_replies ~= 0 then
in_reply_to = task:get_header_raw('in-reply-to')
if in_reply_to then
rspamd_logger.infox(rspamd_config, "mail is a reply - skip footer")
return
end
end
local envfrom_mime = task:get_from(2)
local from_name = ""
if envfrom_mime and envfrom_mime[1].name then

View File

@@ -12,6 +12,7 @@
SOGoJunkFolderName= "Junk";
SOGoMailDomain = "sogo.local";
SOGoEnableEMailAlarms = YES;
SOGoMailHideInlineAttachments = YES;
SOGoFoldersSendEMailNotifications = YES;
SOGoForwardEnabled = YES;

View File

@@ -228,8 +228,8 @@ legend {
margin-top: 20px;
}
.slave-info {
padding: 15px 0px 15px 15px;
font-weight: bold;
color: orange;
}
.alert-hr {
margin:3px 0px;

View File

@@ -175,6 +175,9 @@ pre {
background-color: #282828;
border: 1px solid #555;
}
.form-control {
background-color: transparent;
}
input.form-control, textarea.form-control {
color: #e2e2e2 !important;
background-color: #424242 !important;

View File

@@ -2,6 +2,7 @@
function customize($_action, $_item, $_data = null) {
global $redis;
global $lang;
global $LOGO_LIMITS;
switch ($_action) {
case 'add':
@@ -35,6 +36,23 @@ function customize($_action, $_item, $_data = null) {
);
return false;
}
if ($_data[$_item]['size'] > $LOGO_LIMITS['max_size']) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'img_size_exceeded'
);
return false;
}
list($width, $height) = getimagesize($_data[$_item]['tmp_name']);
if ($width > $LOGO_LIMITS['max_width'] || $height > $LOGO_LIMITS['max_height']) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'img_dimensions_exceeded'
);
return false;
}
$image = new Imagick($_data[$_item]['tmp_name']);
if ($image->valid() !== true) {
$_SESSION['return'][] = array(

View File

@@ -478,16 +478,24 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
$DOMAIN_DEFAULT_ATTRIBUTES = null;
if ($_data['template']){
$DOMAIN_DEFAULT_ATTRIBUTES = mailbox('get', 'domain_templates', $_data['template'])['attributes'];
}
if (empty($DOMAIN_DEFAULT_ATTRIBUTES)) {
$DOMAIN_DEFAULT_ATTRIBUTES = mailbox('get', 'domain_templates')[0]['attributes'];
}
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
$description = $_data['description'];
if (empty($description)) $description = $domain;
$tags = (array)$_data['tags'];
$aliases = (int)$_data['aliases'];
$mailboxes = (int)$_data['mailboxes'];
$defquota = (int)$_data['defquota'];
$maxquota = (int)$_data['maxquota'];
$tags = (isset($_data['tags'])) ? (array)$_data['tags'] : $DOMAIN_DEFAULT_ATTRIBUTES['tags'];
$aliases = (isset($_data['aliases'])) ? (int)$_data['aliases'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_num_aliases_for_domain'];
$mailboxes = (isset($_data['mailboxes'])) ? (int)$_data['mailboxes'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_num_mboxes_for_domain'];
$defquota = (isset($_data['defquota'])) ? (int)$_data['defquota'] : $DOMAIN_DEFAULT_ATTRIBUTES['def_quota_for_mbox'] / 1024 ** 2;
$maxquota = (isset($_data['maxquota'])) ? (int)$_data['maxquota'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_quota_for_mbox'] / 1024 ** 2;
$restart_sogo = (int)$_data['restart_sogo'];
$quota = (int)$_data['quota'];
$quota = (isset($_data['quota'])) ? (int)$_data['quota'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_quota_for_domain'] / 1024 ** 2;
if ($defquota > $maxquota) {
$_SESSION['return'][] = array(
'type' => 'danger',
@@ -520,11 +528,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
$active = intval($_data['active']);
$relay_all_recipients = intval($_data['relay_all_recipients']);
$relay_unknown_only = intval($_data['relay_unknown_only']);
$backupmx = intval($_data['backupmx']);
$gal = intval($_data['gal']);
$active = (isset($_data['active'])) ? intval($_data['active']) : $DOMAIN_DEFAULT_ATTRIBUTES['active'];
$relay_all_recipients = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_all_recipients'];
$relay_unknown_only = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_unknown_only'];
$backupmx = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : $DOMAIN_DEFAULT_ATTRIBUTES['backupmx'];
$gal = (isset($_data['gal'])) ? intval($_data['gal']) : $DOMAIN_DEFAULT_ATTRIBUTES['gal'];
if ($relay_all_recipients == 1) {
$backupmx = '1';
}
@@ -625,9 +633,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
if (!empty(intval($_data['rl_value']))) {
$_data['rl_value'] = (isset($_data['rl_value'])) ? intval($_data['rl_value']) : $DOMAIN_DEFAULT_ATTRIBUTES['rl_value'];
$_data['rl_frame'] = (isset($_data['rl_frame'])) ? $_data['rl_frame'] : $DOMAIN_DEFAULT_ATTRIBUTES['rl_frame'];
if (!empty($_data['rl_value']) && !empty($_data['rl_frame'])){
ratelimit('edit', 'domain', array('rl_value' => $_data['rl_value'], 'rl_frame' => $_data['rl_frame'], 'object' => $domain));
}
$_data['key_size'] = (isset($_data['key_size'])) ? intval($_data['key_size']) : $DOMAIN_DEFAULT_ATTRIBUTES['key_size'];
$_data['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : $DOMAIN_DEFAULT_ATTRIBUTES['dkim_selector'];
if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) {
if (!empty($redis->hGet('DKIM_SELECTORS', $domain))) {
$_SESSION['return'][] = array(
@@ -1006,11 +1018,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
if (empty($name)) {
$name = $local_part;
}
$template_attr = null;
if ($_data['template']){
$template_attr = mailbox('get', 'mailbox_templates', $_data['template'])['attributes'];
}
if (empty($template_attr)) {
$template_attr = mailbox('get', 'mailbox_templates')[0]['attributes'];
}
$MAILBOX_DEFAULT_ATTRIBUTES = array_merge($MAILBOX_DEFAULT_ATTRIBUTES, $template_attr);
$password = $_data['password'];
$password2 = $_data['password2'];
$name = ltrim(rtrim($_data['name'], '>'), '<');
$tags = $_data['tags'];
$quota_m = intval($_data['quota']);
$tags = (isset($_data['tags'])) ? $_data['tags'] : $MAILBOX_DEFAULT_ATTRIBUTES['tags'];
$quota_m = (isset($_data['quota'])) ? intval($_data['quota']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['quota']) / 1024 ** 2;
if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
@@ -1019,9 +1043,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
if (empty($name)) {
$name = $local_part;
}
if (isset($_data['protocol_access'])) {
$_data['protocol_access'] = (array)$_data['protocol_access'];
$_data['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
@@ -1029,7 +1051,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
}
$active = intval($_data['active']);
$active = (isset($_data['active'])) ? intval($_data['active']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['active']);
$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
$tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
$tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
@@ -1227,12 +1249,29 @@ 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;
} else {
$_data['spam_alias'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_alias']);
$_data['tls_policy'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_tls_policy']);
$_data['spam_score'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_score']);
$_data['spam_policy'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_policy']);
$_data['delimiter_action'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_delimiter_action']);
$_data['syncjobs'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_syncjobs']);
$_data['eas_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_eas_reset']);
$_data['sogo_profile_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_sogo_profile_reset']);
$_data['pushover'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pushover']);
$_data['quarantine'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine']);
$_data['quarantine_attachments'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_attachments']);
$_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']);
}
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`)
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) ");
$stmt->execute(array(
':username' => $username,
':spam_alias' => $_data['spam_alias'],
@@ -1251,31 +1290,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':app_passwds' => $_data['app_passwds']
));
}
else {
$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`)
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) ");
$stmt->execute(array(
':username' => $username,
':spam_alias' => 0,
':tls_policy' => 0,
':spam_score' => 0,
':spam_policy' => 0,
':delimiter_action' => 0,
':syncjobs' => 0,
':eas_reset' => 0,
':sogo_profile_reset' => 0,
':pushover' => 0,
':quarantine' => 0,
':quarantine_attachments' => 0,
':quarantine_notification' => 0,
':quarantine_category' => 0,
':app_passwds' => 0
));
catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => $e->getMessage()
);
return false;
}
$_data['rl_frame'] = (isset($_data['rl_frame'])) ? $_data['rl_frame'] : $MAILBOX_DEFAULT_ATTRIBUTES['rl_frame'];
$_data['rl_value'] = (isset($_data['rl_value'])) ? $_data['rl_value'] : $MAILBOX_DEFAULT_ATTRIBUTES['rl_value'];
if (isset($_data['rl_frame']) && isset($_data['rl_value'])){
ratelimit('edit', 'mailbox', array(
'object' => $username,
@@ -1524,17 +1549,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr["tls_enforce_out"] = isset($_data['tls_enforce_out']) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
if (isset($_data['protocol_access'])) {
$_data['protocol_access'] = (array)$_data['protocol_access'];
$attr['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
$attr['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
}
else {
$attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
$attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
$attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
$attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
}
}
if (isset($_data['acl'])) {
$_data['acl'] = (array)$_data['acl'];
$attr['acl_spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0;
@@ -3411,6 +3436,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$footers = array();
$footers['html'] = isset($_data['html']) ? $_data['html'] : '';
$footers['plain'] = isset($_data['plain']) ? $_data['plain'] : '';
$footers['skip_replies'] = isset($_data['skip_replies']) ? (int)$_data['skip_replies'] : 0;
$footers['mbox_exclude'] = array();
if (isset($_data["mbox_exclude"])){
if (!is_array($_data["mbox_exclude"])) {
@@ -3460,12 +3486,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
try {
$stmt = $pdo->prepare("DELETE FROM `domain_wide_footer` WHERE `domain`= :domain");
$stmt->execute(array(':domain' => $domain));
$stmt = $pdo->prepare("INSERT INTO `domain_wide_footer` (`domain`, `html`, `plain`, `mbox_exclude`) VALUES (:domain, :html, :plain, :mbox_exclude)");
$stmt = $pdo->prepare("INSERT INTO `domain_wide_footer` (`domain`, `html`, `plain`, `mbox_exclude`, `skip_replies`) VALUES (:domain, :html, :plain, :mbox_exclude, :skip_replies)");
$stmt->execute(array(
':domain' => $domain,
':html' => $footers['html'],
':plain' => $footers['plain'],
':mbox_exclude' => json_encode($footers['mbox_exclude']),
':skip_replies' => $footers['skip_replies'],
));
}
catch (PDOException $e) {
@@ -4435,7 +4462,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$mailboxdata['active'] = $row['active'];
$mailboxdata['active_int'] = $row['active'];
$mailboxdata['domain'] = $row['domain'];
$mailboxdata['relayhost'] = $row['relayhost'];
$mailboxdata['name'] = $row['name'];
$mailboxdata['local_part'] = $row['local_part'];
$mailboxdata['quota'] = $row['quota'];
@@ -4622,7 +4648,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
}
try {
$stmt = $pdo->prepare("SELECT `html`, `plain`, `mbox_exclude` FROM `domain_wide_footer`
$stmt = $pdo->prepare("SELECT `html`, `plain`, `mbox_exclude`, `skip_replies` FROM `domain_wide_footer`
WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain

View File

@@ -3,7 +3,7 @@ function init_db_schema() {
try {
global $pdo;
$db_version = "21112023_1644";
$db_version = "08012024_1442";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -273,6 +273,7 @@ function init_db_schema() {
"html" => "LONGTEXT",
"plain" => "LONGTEXT",
"mbox_exclude" => "JSON NOT NULL DEFAULT ('[]')",
"skip_replies" => "TINYINT(1) NOT NULL DEFAULT '0'"
),
"keys" => array(
"primary" => array(

View File

@@ -0,0 +1,622 @@
<?php
/*
* Helper functions for building a DataTables server-side processing SQL query
*
* The static functions in this class are just helper functions to help build
* the SQL used in the DataTables demo server-side processing scripts. These
* functions obviously do not represent all that can be done with server-side
* processing, they are intentionally simple to show how it works. More complex
* server-side processing operations will likely require a custom script.
*
* See https://datatables.net/usage/server-side for full details on the server-
* side processing requirements of DataTables.
*
* @license MIT - https://datatables.net/license_mit
*/
class SSP {
/**
* Create the data output array for the DataTables rows
*
* @param array $columns Column information array
* @param array $data Data from the SQL get
* @return array Formatted data in a row based format
*/
static function data_output ( $columns, $data )
{
$out = array();
for ( $i=0, $ien=count($data) ; $i<$ien ; $i++ ) {
$row = array();
for ( $j=0, $jen=count($columns) ; $j<$jen ; $j++ ) {
$column = $columns[$j];
// Is there a formatter?
if ( isset( $column['formatter'] ) ) {
if(empty($column['db'])){
$row[ $column['dt'] ] = $column['formatter']( $data[$i] );
}
else{
$row[ $column['dt'] ] = $column['formatter']( $data[$i][ $column['db'] ], $data[$i] );
}
}
else {
if(!empty($column['db']) && (!isset($column['dummy']) || $column['dummy'] !== true)){
$row[ $column['dt'] ] = $data[$i][ $columns[$j]['db'] ];
}
else{
$row[ $column['dt'] ] = "";
}
}
}
$out[] = $row;
}
return $out;
}
/**
* Database connection
*
* Obtain an PHP PDO connection from a connection details array
*
* @param array $conn SQL connection details. The array should have
* the following properties
* * host - host name
* * db - database name
* * user - user name
* * pass - user password
* * Optional: `'charset' => 'utf8'` - you might need this depending on your PHP / MySQL config
* @return resource PDO connection
*/
static function db ( $conn )
{
if ( is_array( $conn ) ) {
return self::sql_connect( $conn );
}
return $conn;
}
/**
* Paging
*
* Construct the LIMIT clause for server-side processing SQL query
*
* @param array $request Data sent to server by DataTables
* @param array $columns Column information array
* @return string SQL limit clause
*/
static function limit ( $request, $columns )
{
$limit = '';
if ( isset($request['start']) && $request['length'] != -1 ) {
$limit = "LIMIT ".intval($request['start']).", ".intval($request['length']);
}
return $limit;
}
/**
* Ordering
*
* Construct the ORDER BY clause for server-side processing SQL query
*
* @param array $request Data sent to server by DataTables
* @param array $columns Column information array
* @return string SQL order by clause
*/
static function order ( $tableAS, $request, $columns )
{
$select = '';
$order = '';
if ( isset($request['order']) && count($request['order']) ) {
$selects = [];
$orderBy = [];
$dtColumns = self::pluck( $columns, 'dt' );
for ( $i=0, $ien=count($request['order']) ; $i<$ien ; $i++ ) {
// Convert the column index into the column data property
$columnIdx = intval($request['order'][$i]['column']);
$requestColumn = $request['columns'][$columnIdx];
$columnIdx = array_search( $columnIdx, $dtColumns );
$column = $columns[ $columnIdx ];
if ( $requestColumn['orderable'] == 'true' ) {
$dir = $request['order'][$i]['dir'] === 'asc' ?
'ASC' :
'DESC';
if(isset($column['order_subquery'])) {
$selects[] = '('.$column['order_subquery'].') AS `'.$column['db'].'_count`';
$orderBy[] = '`'.$column['db'].'_count` '.$dir;
} else {
$orderBy[] = '`'.$tableAS.'`.`'.$column['db'].'` '.$dir;
}
}
}
if ( count( $selects ) ) {
$select = ', '.implode(', ', $selects);
}
if ( count( $orderBy ) ) {
$order = 'ORDER BY '.implode(', ', $orderBy);
}
}
return [$select, $order];
}
/**
* Searching / Filtering
*
* Construct the WHERE clause for server-side processing SQL query.
*
* NOTE this does not match the built-in DataTables filtering which does it
* word by word on any field. It's possible to do here performance on large
* databases would be very poor
*
* @param array $request Data sent to server by DataTables
* @param array $columns Column information array
* @param array $bindings Array of values for PDO bindings, used in the
* sql_exec() function
* @return string SQL where clause
*/
static function filter ( $tablesAS, $request, $columns, &$bindings )
{
$globalSearch = array();
$columnSearch = array();
$joins = array();
$dtColumns = self::pluck( $columns, 'dt' );
if ( isset($request['search']) && $request['search']['value'] != '' ) {
$str = $request['search']['value'];
for ( $i=0, $ien=count($request['columns']) ; $i<$ien ; $i++ ) {
$requestColumn = $request['columns'][$i];
$columnIdx = array_search( $i, $dtColumns );
$column = $columns[ $columnIdx ];
if ( $requestColumn['searchable'] == 'true' ) {
if(!empty($column['db'])){
$binding = self::bind( $bindings, '%'.$str.'%', PDO::PARAM_STR );
if(isset($column['search']['join'])) {
$joins[] = $column['search']['join'];
$globalSearch[] = $column['search']['where_column'].' LIKE '.$binding;
} else {
$globalSearch[] = "`".$tablesAS."`.`".$column['db']."` LIKE ".$binding;
}
}
}
}
}
// Individual column filtering
if ( isset( $request['columns'] ) ) {
for ( $i=0, $ien=count($request['columns']) ; $i<$ien ; $i++ ) {
$requestColumn = $request['columns'][$i];
$columnIdx = array_search( $requestColumn['data'], $dtColumns );
$column = $columns[ $columnIdx ];
$str = $requestColumn['search']['value'];
if ( $requestColumn['searchable'] == 'true' &&
$str != '' ) {
if(!empty($column['db'])){
$binding = self::bind( $bindings, '%'.$str.'%', PDO::PARAM_STR );
$columnSearch[] = "`".$tablesAS."`.`".$column['db']."` LIKE ".$binding;
}
}
}
}
// Combine the filters into a single string
$where = '';
if ( count( $globalSearch ) ) {
$where = '('.implode(' OR ', $globalSearch).')';
}
if ( count( $columnSearch ) ) {
$where = $where === '' ?
implode(' AND ', $columnSearch) :
$where .' AND '. implode(' AND ', $columnSearch);
}
$join = '';
if( count($joins) ) {
$join = implode(' ', $joins);
}
if ( $where !== '' ) {
$where = 'WHERE '.$where;
}
return [$join, $where];
}
/**
* Perform the SQL queries needed for an server-side processing requested,
* utilising the helper functions of this class, limit(), order() and
* filter() among others. The returned array is ready to be encoded as JSON
* in response to an SSP request, or can be modified if needed before
* sending back to the client.
*
* @param array $request Data sent to server by DataTables
* @param array|PDO $conn PDO connection resource or connection parameters array
* @param string $table SQL table to query
* @param string $primaryKey Primary key of the table
* @param array $columns Column information array
* @return array Server-side processing response array
*/
static function simple ( $request, $conn, $table, $primaryKey, $columns )
{
$bindings = array();
$db = self::db( $conn );
// Allow for a JSON string to be passed in
if (isset($request['json'])) {
$request = json_decode($request['json'], true);
}
// table AS
$tablesAS = null;
if(is_array($table)) {
$tablesAS = $table[1];
$table = $table[0];
}
// Build the SQL query string from the request
list($select, $order) = self::order( $tablesAS, $request, $columns );
$limit = self::limit( $request, $columns );
list($join, $where) = self::filter( $tablesAS, $request, $columns, $bindings );
// Main query to actually get the data
$data = self::sql_exec( $db, $bindings,
"SELECT `$tablesAS`.`".implode("`, `$tablesAS`.`", self::pluck($columns, 'db'))."`
$select
FROM `$table` AS `$tablesAS`
$join
$where
GROUP BY `{$tablesAS}`.`{$primaryKey}`
$order
$limit"
);
// Data set length after filtering
$resFilterLength = self::sql_exec( $db, $bindings,
"SELECT COUNT(DISTINCT `{$tablesAS}`.`{$primaryKey}`)
FROM `$table` AS `$tablesAS`
$join
$where"
);
$recordsFiltered = $resFilterLength[0][0];
// Total data set length
$resTotalLength = self::sql_exec( $db,
"SELECT COUNT(`{$tablesAS}`.`{$primaryKey}`)
FROM `$table` AS `$tablesAS`"
);
$recordsTotal = $resTotalLength[0][0];
/*
* Output
*/
return array(
"draw" => isset ( $request['draw'] ) ?
intval( $request['draw'] ) :
0,
"recordsTotal" => intval( $recordsTotal ),
"recordsFiltered" => intval( $recordsFiltered ),
"data" => self::data_output( $columns, $data )
);
}
/**
* The difference between this method and the `simple` one, is that you can
* apply additional `where` conditions to the SQL queries. These can be in
* one of two forms:
*
* * 'Result condition' - This is applied to the result set, but not the
* overall paging information query - i.e. it will not effect the number
* of records that a user sees they can have access to. This should be
* used when you want apply a filtering condition that the user has sent.
* * 'All condition' - This is applied to all queries that are made and
* reduces the number of records that the user can access. This should be
* used in conditions where you don't want the user to ever have access to
* particular records (for example, restricting by a login id).
*
* In both cases the extra condition can be added as a simple string, or if
* you are using external values, as an assoc. array with `condition` and
* `bindings` parameters. The `condition` is a string with the SQL WHERE
* condition and `bindings` is an assoc. array of the binding names and
* values.
*
* @param array $request Data sent to server by DataTables
* @param array|PDO $conn PDO connection resource or connection parameters array
* @param string|array $table SQL table to query, if array second key is AS
* @param string $primaryKey Primary key of the table
* @param array $columns Column information array
* @param string $join JOIN sql string
* @param string|array $whereResult WHERE condition to apply to the result set
* @return array Server-side processing response array
*/
static function complex (
$request,
$conn,
$table,
$primaryKey,
$columns,
$join=null,
$whereResult=null
) {
$bindings = array();
$db = self::db( $conn );
// table AS
$tablesAS = null;
if(is_array($table)) {
$tablesAS = $table[1];
$table = $table[0];
}
// Build the SQL query string from the request
list($select, $order) = self::order( $tablesAS, $request, $columns );
$limit = self::limit( $request, $columns );
list($join_filter, $where) = self::filter( $tablesAS, $request, $columns, $bindings );
// whereResult can be a simple string, or an assoc. array with a
// condition and bindings
if ( $whereResult ) {
$str = $whereResult;
if ( is_array($whereResult) ) {
$str = $whereResult['condition'];
if ( isset($whereResult['bindings']) ) {
self::add_bindings($bindings, $whereResult);
}
}
$where = $where ?
$where .' AND '.$str :
'WHERE '.$str;
}
// Main query to actually get the data
$data = self::sql_exec( $db, $bindings,
"SELECT `$tablesAS`.`".implode("`, `$tablesAS`.`", self::pluck($columns, 'db'))."`
$select
FROM `$table` AS `$tablesAS`
$join
$join_filter
$where
GROUP BY `{$tablesAS}`.`{$primaryKey}`
$order
$limit"
);
// Data set length after filtering
$resFilterLength = self::sql_exec( $db, $bindings,
"SELECT COUNT(DISTINCT `{$tablesAS}`.`{$primaryKey}`)
FROM `$table` AS `$tablesAS`
$join
$join_filter
$where"
);
$recordsFiltered = (isset($resFilterLength[0])) ? $resFilterLength[0][0] : 0;
// Total data set length
$resTotalLength = self::sql_exec( $db, $bindings,
"SELECT COUNT(`{$tablesAS}`.`{$primaryKey}`)
FROM `$table` AS `$tablesAS`
$join
$join_filter
$where"
);
$recordsTotal = (isset($resTotalLength[0])) ? $resTotalLength[0][0] : 0;
/*
* Output
*/
return array(
"draw" => isset ( $request['draw'] ) ?
intval( $request['draw'] ) :
0,
"recordsTotal" => intval( $recordsTotal ),
"recordsFiltered" => intval( $recordsFiltered ),
"data" => self::data_output( $columns, $data )
);
}
/**
* Connect to the database
*
* @param array $sql_details SQL server connection details array, with the
* properties:
* * host - host name
* * db - database name
* * user - user name
* * pass - user password
* @return resource Database connection handle
*/
static function sql_connect ( $sql_details )
{
try {
$db = @new PDO(
"mysql:host={$sql_details['host']};dbname={$sql_details['db']}",
$sql_details['user'],
$sql_details['pass'],
array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION )
);
}
catch (PDOException $e) {
self::fatal(
"An error occurred while connecting to the database. ".
"The error reported by the server was: ".$e->getMessage()
);
}
return $db;
}
/**
* Execute an SQL query on the database
*
* @param resource $db Database handler
* @param array $bindings Array of PDO binding values from bind() to be
* used for safely escaping strings. Note that this can be given as the
* SQL query string if no bindings are required.
* @param string $sql SQL query to execute.
* @return array Result from the query (all rows)
*/
static function sql_exec ( $db, $bindings, $sql=null )
{
// Argument shifting
if ( $sql === null ) {
$sql = $bindings;
}
$stmt = $db->prepare( $sql );
// Bind parameters
if ( is_array( $bindings ) ) {
for ( $i=0, $ien=count($bindings) ; $i<$ien ; $i++ ) {
$binding = $bindings[$i];
$stmt->bindValue( $binding['key'], $binding['val'], $binding['type'] );
}
}
// Execute
try {
$stmt->execute();
}
catch (PDOException $e) {
self::fatal( "An SQL error occurred: ".$e->getMessage() );
}
// Return all
return $stmt->fetchAll( PDO::FETCH_BOTH );
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Internal methods
*/
/**
* Throw a fatal error.
*
* This writes out an error message in a JSON string which DataTables will
* see and show to the user in the browser.
*
* @param string $msg Message to send to the client
*/
static function fatal ( $msg )
{
echo json_encode( array(
"error" => $msg
) );
exit(0);
}
/**
* Create a PDO binding key which can be used for escaping variables safely
* when executing a query with sql_exec()
*
* @param array &$a Array of bindings
* @param * $val Value to bind
* @param int $type PDO field type
* @return string Bound key to be used in the SQL where this parameter
* would be used.
*/
static function bind ( &$a, $val, $type )
{
$key = ':binding_'.count( $a );
$a[] = array(
'key' => $key,
'val' => $val,
'type' => $type
);
return $key;
}
static function add_bindings(&$bindings, $vals)
{
foreach($vals['bindings'] as $key => $value) {
$bindings[] = array(
'key' => $key,
'val' => $value,
'type' => PDO::PARAM_STR
);
}
}
/**
* Pull a particular property from each assoc. array in a numeric array,
* returning and array of the property values from each item.
*
* @param array $a Array to get data from
* @param string $prop Property to read
* @return array Array of property values
*/
static function pluck ( $a, $prop )
{
$out = array();
for ( $i=0, $len=count($a) ; $i<$len ; $i++ ) {
if ( empty($a[$i][$prop]) && $a[$i][$prop] !== 0 ) {
continue;
}
if ( $prop == 'db' && isset($a[$i]['dummy']) && $a[$i]['dummy'] === true ) {
continue;
}
//removing the $out array index confuses the filter method in doing proper binding,
//adding it ensures that the array data are mapped correctly
$out[$i] = $a[$i][$prop];
}
return $out;
}
/**
* Return a string from an array or a string
*
* @param array|string $a Array to join
* @param string $join Glue for the concatenation
* @return string Joined string
*/
static function _flatten ( $a, $join = ' AND ' )
{
if ( ! $a ) {
return '';
}
else if ( $a && is_array($a) ) {
return implode( $join, $a );
}
return $a;
}
}

View File

@@ -126,6 +126,15 @@ $MAILCOW_APPS = array(
)
);
// Logo max file size in bytes
$LOGO_LIMITS['max_size'] = 15 * 1024 * 1024; // 15MB
// Logo max width in pixels
$LOGO_LIMITS['max_width'] = 1920;
// Logo max height in pixels
$LOGO_LIMITS['max_height'] = 1920;
// Rows until pagination begins
$PAGINATION_SIZE = 25;

View File

@@ -435,7 +435,7 @@ jQuery(function($){
var table = $('#domain_table').DataTable({
responsive: true,
processing: true,
serverSide: false,
serverSide: true,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
@@ -447,9 +447,9 @@ jQuery(function($){
},
ajax: {
type: "GET",
url: "/api/v1/get/domain/all",
url: "/api/v1/get/domain/datatables",
dataSrc: function(json){
$.each(json, function(i, item) {
$.each(json.data, function(i, item) {
item.domain_name = escapeHtml(item.domain_name);
item.aliases = item.aliases_in_domain + " / " + item.max_num_aliases_for_domain;
@@ -498,7 +498,7 @@ jQuery(function($){
}
});
return json;
return json.data;
}
},
columns: [
@@ -528,17 +528,20 @@ jQuery(function($){
{
title: lang.aliases,
data: 'aliases',
searchable: false,
defaultContent: ''
},
{
title: lang.mailboxes,
data: 'mailboxes',
searchable: false,
responsivePriority: 4,
defaultContent: ''
},
{
title: lang.domain_quota,
data: 'quota',
searchable: false,
defaultContent: '',
render: function (data, type) {
data = data.split("/");
@@ -548,6 +551,7 @@ jQuery(function($){
{
title: lang.stats,
data: 'stats',
searchable: false,
defaultContent: '',
render: function (data, type) {
data = data.split("/");
@@ -557,53 +561,67 @@ jQuery(function($){
{
title: lang.mailbox_defquota,
data: 'def_quota_for_mbox',
searchable: false,
defaultContent: ''
},
{
title: lang.mailbox_quota,
data: 'max_quota_for_mbox',
searchable: false,
defaultContent: ''
},
{
title: 'RL',
data: 'rl',
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: lang.backup_mx,
data: 'backupmx',
searchable: false,
defaultContent: '',
redner: function (data, type){
return 1==value ? '<i class="bi bi-check-lg"></i>' : 0==value && '<i class="bi bi-x-lg"></i>';
render: function (data, type){
return 1==data ? '<i class="bi bi-check-lg"></i>' : 0==data && '<i class="bi bi-x-lg"></i>';
}
},
{
title: lang.domain_admins,
data: 'domain_admins',
searchable: false,
orderable: false,
defaultContent: '',
className: 'none'
},
{
title: lang.created_on,
data: 'created',
searchable: false,
orderable: false,
defaultContent: '',
className: 'none'
},
{
title: lang.last_modified,
data: 'modified',
searchable: false,
orderable: false,
defaultContent: '',
className: 'none'
},
{
title: 'Tags',
data: 'tags',
searchable: true,
orderable: false,
defaultContent: '',
className: 'none'
},
{
title: lang.active,
data: 'active',
searchable: false,
defaultContent: '',
responsivePriority: 6,
render: function (data, type) {
@@ -613,6 +631,8 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
searchable: false,
orderable: false,
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
responsivePriority: 5,
defaultContent: ''
@@ -844,7 +864,7 @@ jQuery(function($){
var table = $('#mailbox_table').DataTable({
responsive: true,
processing: true,
serverSide: false,
serverSide: true,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
@@ -853,13 +873,12 @@ jQuery(function($){
language: lang_datatables,
initComplete: function(settings, json){
hideTableExpandCollapseBtn('#tab-mailboxes', '#mailbox_table');
filterByDomain(json, 8, table);
},
ajax: {
type: "GET",
url: "/api/v1/get/mailbox/reduced",
url: "/api/v1/get/mailbox/datatables",
dataSrc: function(json){
$.each(json, function (i, item) {
$.each(json.data, function (i, item) {
item.quota = {
sortBy: item.quota_used,
value: item.quota
@@ -945,7 +964,7 @@ jQuery(function($){
}
});
return json;
return json.data;
}
},
columns: [
@@ -975,13 +994,14 @@ jQuery(function($){
{
title: lang.domain_quota,
data: 'quota.value',
searchable: false,
responsivePriority: 8,
defaultContent: '',
orderData: 23
defaultContent: ''
},
{
title: lang.last_mail_login,
data: 'last_mail_login',
searchable: false,
defaultContent: '',
responsivePriority: 7,
render: function (data, type) {
@@ -994,15 +1014,16 @@ jQuery(function($){
{
title: lang.last_pw_change,
data: 'last_pw_change',
searchable: false,
defaultContent: ''
},
{
title: lang.in_use,
data: 'in_use.value',
searchable: false,
defaultContent: '',
responsivePriority: 9,
className: 'dt-data-w100',
orderData: 24
className: 'dt-data-w100'
},
{
title: lang.fname,
@@ -1067,6 +1088,7 @@ jQuery(function($){
{
title: lang.msg_num,
data: 'messages',
searchable: false,
defaultContent: '',
responsivePriority: 5
},
@@ -1085,12 +1107,14 @@ jQuery(function($){
{
title: 'Tags',
data: 'tags',
searchable: true,
defaultContent: '',
className: 'none'
},
{
title: lang.active,
data: 'active',
searchable: false,
defaultContent: '',
responsivePriority: 4,
render: function (data, type) {
@@ -1100,22 +1124,12 @@ jQuery(function($){
{
title: lang.action,
data: 'action',
searchable: false,
orderable: false,
className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right',
responsivePriority: 6,
defaultContent: ''
},
{
title: "",
data: 'quota.sortBy',
defaultContent: '',
className: "d-none"
},
{
title: "",
data: 'in_use.sortBy',
defaultContent: '',
className: "d-none"
},
}
]
});

View File

@@ -15,7 +15,7 @@ function api_log($_data) {
continue;
}
$value = json_decode($value, true);
$value = json_decode($value, true);
if ($value) {
if (is_array($value)) unset($value["csrf_token"]);
foreach ($value as $key => &$val) {
@@ -23,7 +23,7 @@ function api_log($_data) {
$val = '*';
}
}
$value = json_encode($value);
$value = json_encode($value);
}
$data_var[] = $data . "='" . $value . "'";
}
@@ -44,7 +44,7 @@ function api_log($_data) {
'msg' => 'Redis: '.$e
);
return false;
}
}
}
if (isset($_GET['query'])) {
@@ -178,12 +178,12 @@ if (isset($_GET['query'])) {
// parse post data
$post = trim(file_get_contents('php://input'));
if ($post) $post = json_decode($post);
// process registration data from authenticator
try {
// decode base64 strings
$clientDataJSON = base64_decode($post->clientDataJSON);
$attestationObject = base64_decode($post->attestationObject);
$attestationObject = base64_decode($post->attestationObject);
// processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true)
$data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $_SESSION['challenge'], false, true);
@@ -250,7 +250,7 @@ if (isset($_GET['query'])) {
default:
process_add_return(mailbox('add', 'domain', $attr));
break;
}
}
break;
case "resource":
process_add_return(mailbox('add', 'resource', $attr));
@@ -470,7 +470,7 @@ if (isset($_GET['query'])) {
// false, if only internal is allowed
// null, if internal and cross-platform is allowed
$createArgs = $WebAuthn->getCreateArgs($_SESSION["mailcow_cc_username"], $_SESSION["mailcow_cc_username"], $_SESSION["mailcow_cc_username"], 30, false, $GLOBALS['WEBAUTHN_UV_FLAG_REGISTER'], null, $excludeCredentialIds);
print(json_encode($createArgs));
$_SESSION['challenge'] = $WebAuthn->getChallenge();
return;
@@ -533,9 +533,50 @@ if (isset($_GET['query'])) {
case "domain":
switch ($object) {
case "datatables":
$table = ['domain', 'd'];
$primaryKey = 'domain';
$columns = [
['db' => 'domain', 'dt' => 2],
['db' => 'aliases', 'dt' => 3, 'order_subquery' => "SELECT COUNT(*) FROM `alias` WHERE (`domain`= `d`.`domain` OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = `d`.`domain`)) AND `address` NOT IN (SELECT `username` FROM `mailbox`)"],
['db' => 'mailboxes', 'dt' => 4, 'order_subquery' => "SELECT COUNT(*) FROM `mailbox` WHERE `mailbox`.`domain` = `d`.`domain` AND (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)"],
['db' => 'quota', 'dt' => 5, 'order_subquery' => "SELECT COALESCE(SUM(`mailbox`.`quota`), 0) FROM `mailbox` WHERE `mailbox`.`domain` = `d`.`domain` AND (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)"],
['db' => 'stats', 'dt' => 6, 'dummy' => true, 'order_subquery' => "SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` IN (SELECT `username` FROM `mailbox` WHERE `domain` = `d`.`domain`)"],
['db' => 'defquota', 'dt' => 7],
['db' => 'maxquota', 'dt' => 8],
['db' => 'backupmx', 'dt' => 10],
['db' => 'tags', 'dt' => 14, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_domain` AS `td` ON `td`.`domain` = `d`.`domain`', 'where_column' => '`td`.`tag_name`']],
['db' => 'active', 'dt' => 15],
];
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php';
global $pdo;
if($_SESSION['mailcow_cc_role'] === 'admin') {
$data = SSP::simple($_GET, $pdo, $table, $primaryKey, $columns);
} elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
$data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns,
'INNER JOIN domain_admins as da ON da.domain = d.domain',
[
'condition' => 'da.active = 1 and da.username = :username',
'bindings' => ['username' => $_SESSION['mailcow_cc_username']]
]);
}
if (!empty($data['data'])) {
$domainsData = [];
foreach ($data['data'] as $domain) {
if ($details = mailbox('get', 'domain_details', $domain[2])) {
$domainsData[] = $details;
}
}
$data['data'] = $domainsData;
}
process_get_return($data);
break;
case "all":
$tags = null;
if (isset($_GET['tags']) && $_GET['tags'] != '')
if (isset($_GET['tags']) && $_GET['tags'] != '')
$tags = explode(',', $_GET['tags']);
$domains = mailbox('get', 'domains', null, $tags);
@@ -1021,10 +1062,49 @@ if (isset($_GET['query'])) {
break;
case "mailbox":
switch ($object) {
case "datatables":
$table = ['mailbox', 'm'];
$primaryKey = 'username';
$columns = [
['db' => 'username', 'dt' => 2],
['db' => 'quota', 'dt' => 3],
['db' => 'last_mail_login', 'dt' => 4, 'dummy' => true, 'order_subquery' => "SELECT MAX(`datetime`) FROM `sasl_log` WHERE `service` != 'SSO' AND `username` = `m`.`username`"],
['db' => 'last_pw_change', 'dt' => 5, 'dummy' => true, 'order_subquery' => "JSON_EXTRACT(attributes, '$.passwd_update')"],
['db' => 'in_use', 'dt' => 6, 'dummy' => true, 'order_subquery' => "(SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`) / `m`.`quota`"],
['db' => 'messages', 'dt' => 17, 'dummy' => true, 'order_subquery' => "SELECT SUM(messages) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`"],
['db' => 'tags', 'dt' => 20, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_mailbox` AS `tm` ON `tm`.`username` = `m`.`username`', 'where_column' => '`tm`.`tag_name`']],
['db' => 'active', 'dt' => 21]
];
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php';
global $pdo;
if($_SESSION['mailcow_cc_role'] === 'admin') {
$data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns, null, "(`m`.`kind` = '' OR `m`.`kind` = NULL)");
} elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
$data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns,
'INNER JOIN domain_admins as da ON da.domain = m.domain',
[
'condition' => "(`m`.`kind` = '' OR `m`.`kind` = NULL) AND `da`.`active` = 1 AND `da`.`username` = :username",
'bindings' => ['username' => $_SESSION['mailcow_cc_username']]
]);
}
if (!empty($data['data'])) {
$mailboxData = [];
foreach ($data['data'] as $mailbox) {
if ($details = mailbox('get', 'mailbox_details', $mailbox[2])) {
$mailboxData[] = $details;
}
}
$data['data'] = $mailboxData;
}
process_get_return($data);
break;
case "all":
case "reduced":
$tags = null;
if (isset($_GET['tags']) && $_GET['tags'] != '')
if (isset($_GET['tags']) && $_GET['tags'] != '')
$tags = explode(',', $_GET['tags']);
if (empty($extra)) $domains = mailbox('get', 'domains');
@@ -1058,7 +1138,7 @@ if (isset($_GET['query'])) {
break;
default:
$tags = null;
if (isset($_GET['tags']) && $_GET['tags'] != '')
if (isset($_GET['tags']) && $_GET['tags'] != '')
$tags = explode(',', $_GET['tags']);
if ($tags === null) {
@@ -1068,7 +1148,7 @@ if (isset($_GET['query'])) {
$mailboxes = mailbox('get', 'mailboxes', $object, $tags);
if (is_array($mailboxes)) {
foreach ($mailboxes as $mailbox) {
if ($details = mailbox('get', 'mailbox_details', $mailbox))
if ($details = mailbox('get', 'mailbox_details', $mailbox))
$data[] = $details;
}
}
@@ -1571,15 +1651,15 @@ if (isset($_GET['query'])) {
'solr_size' => $solr_size,
'solr_documents' => $solr_documents
));
break;
break;
case "host":
if (!$extra){
$stats = docker("host_stats");
echo json_encode($stats);
}
}
else if ($extra == "ip") {
// get public ips
$curl = curl_init();
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 0);
@@ -2003,7 +2083,7 @@ if (isset($_GET['query'])) {
exit();
}
}
if ($_SESSION['mailcow_cc_api'] === true) {
if (array_key_exists('mailcow_cc_api', $_SESSION) && $_SESSION['mailcow_cc_api'] === true) {
if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) {
unset($_SESSION['return']);
}

View File

@@ -394,7 +394,9 @@
"goto_invalid": "Ziel-Adresse %s ist ungültig",
"ham_learn_error": "Ham Lernfehler: %s",
"imagick_exception": "Fataler Bildverarbeitungsfehler",
"img_dimensions_exceeded": "Grafik überschreitet die maximale Bildgröße",
"img_invalid": "Grafik konnte nicht validiert werden",
"img_size_exceeded": "Grafik überschreitet die maximale Dateigröße",
"img_tmp_missing": "Grafik konnte nicht validiert werden: Erstellung temporärer Datei fehlgeschlagen.",
"invalid_bcc_map_type": "Ungültiger BCC-Map-Typ",
"invalid_destination": "Ziel-Format \"%s\" ist ungültig",
@@ -588,10 +590,19 @@
"disable_login": "Login verbieten (Mails werden weiterhin angenommen)",
"domain": "Domain bearbeiten",
"domain_admin": "Domain-Administrator bearbeiten",
"domain_footer": "Domain wide footer",
"domain_footer_html": "HTML footer",
"domain_footer_info": "Domain wide footer werden allen ausgehenden E-Mails hinzugefügt, die einer Adresse innerhalb dieser Domain gehört.<br>Die folgenden Variablen können für den Footer benutzt werden:",
"domain_footer_plain": "PLAIN footer",
"domain_footer": "Domänenweite Fußzeile",
"domain_footer_html": "Fußzeile im HTML Format",
"domain_footer_info": "Domänenweite Footer (Domain wide footer) werden allen ausgehenden E-Mails hinzugefügt, die einer Adresse innerhalb dieser Domain gehört.<br>Die folgenden Variablen können für die Fußzeile benutzt werden:",
"domain_footer_info_vars": {
"auth_user": "{= auth_user =} - Angemeldeter Benutzername vom MTA",
"from_user": "{= from_user =} - Absender Teil der E-Mail z.B. für \"moo@mailcow.tld\" wird \"moo\" zurückgeben.",
"from_name": "{= from_name =} - Namen des Absenders z.B. für \"Mailcow &lt;moo@mailcow.tld&gt;\", wird \"Mailcow\" zurückgegeben.",
"from_addr": "{= from_addr =} - Adresse des Absenders.",
"from_domain": "{= from_domain =} - Domain des Absenders",
"custom": "{= foo =} - Wenn die Mailbox das benutzerdefinierte Attribut \"foo\" mit dem Wert \"bar\" hat, wird \"bar\" zurückgegeben."
},
"domain_footer_plain": "Fußzeile im PLAIN Format",
"domain_footer_skip_replies": "Ignoriere Footer bei Antwort E-Mails",
"domain_quota": "Domain Speicherplatz gesamt (MiB)",
"domains": "Domains",
"dont_check_sender_acl": "Absender für Domain %s u. Alias-Domain nicht prüfen",
@@ -680,11 +691,7 @@
"unchanged_if_empty": "Unverändert, wenn leer",
"username": "Benutzername",
"validate_save": "Validieren und speichern",
"pushover_sound": "Ton",
"domain_footer_info_vars": {
"auth_user": "{= auth_user =} - Angemeldeter Benutzername vom MTA",
"from_user": "{= from_user =} - Von Teil des Benutzers z.B. \"moo@mailcow.tld\" wird \"moo\" zurückgeben."
}
"pushover_sound": "Ton"
},
"fido2": {
"confirm": "Bestätigen",
@@ -1088,6 +1095,7 @@
"verified_yotp_login": "Yubico-OTP-Anmeldung verifiziert"
},
"tfa": {
"authenticators": "Authentikatoren",
"api_register": "%s verwendet die Yubico-Cloud-API. Ein API-Key für den Yubico-Stick kann <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">hier</a> bezogen werden.",
"confirm": "Bestätigen",
"confirm_totp_token": "Bitte bestätigen Sie die Änderung durch Eingabe eines generierten Tokens",

View File

@@ -394,7 +394,9 @@
"goto_invalid": "Goto address %s is invalid",
"ham_learn_error": "Ham learn error: %s",
"imagick_exception": "Error: Imagick exception while reading image",
"img_dimensions_exceeded": "Image exceeds the maximum image size",
"img_invalid": "Cannot validate image file",
"img_size_exceeded": "Image exceeds the maximum file size",
"img_tmp_missing": "Cannot validate image file: Temporary file not found",
"invalid_bcc_map_type": "Invalid BCC map type",
"invalid_destination": "Destination format \"%s\" is invalid",
@@ -600,6 +602,7 @@
"custom": "{= foo =} - If mailbox has the custom attribute \"foo\" with value \"bar\" it returns \"bar\""
},
"domain_footer_plain": "PLAIN footer",
"domain_footer_skip_replies": "Ignore footer on reply e-mails",
"domain_quota": "Domain quota",
"domains": "Domains",
"dont_check_sender_acl": "Disable sender check for domain %s (+ alias domains)",
@@ -1099,6 +1102,7 @@
"verified_yotp_login": "Verified Yubico OTP login"
},
"tfa": {
"authenticators": "Authenticators",
"api_register": "%s uses the Yubico Cloud API. Please get an API key for your key <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">here</a>",
"confirm": "Confirm",
"confirm_totp_token": "Please confirm your changes by entering the generated token",

View File

@@ -9,8 +9,8 @@
"eas_reset": "Redefinir dispositivos EAS",
"extend_sender_acl": "Permitir estender a ACL do remetente por endereços externos",
"filters": "Filtros",
"login_as": "Faça login como usuário da caixa de correio",
"mailbox_relayhost": "Alterar relayhost para uma caixa de correio",
"login_as": "Faça login como usuário da mailbox",
"mailbox_relayhost": "Alterar relayhost para uma mailbox",
"prohibited": "Proibido pela ACL",
"protocol_access": "Alterar o acesso ao protocolo",
"pushover": "Pushover",
@@ -28,7 +28,7 @@
"spam_score": "Pontuação de spam",
"syncjobs": "Trabalhos de sincronização",
"tls_policy": "Política de TLS",
"unlimited_quota": "Cota ilimitada para caixas de correio"
"unlimited_quota": "Cota ilimitada para mailbox"
},
"add": {
"activate_filter_warn": "Todos os outros filtros serão desativados quando a opção ativa estiver marcada.",
@@ -70,11 +70,11 @@
"hostname": "Anfitrião",
"inactive": "Inativo",
"kind": "Gentil",
"mailbox_quota_def": "Cota de caixa de correio padrão",
"mailbox_quota_m": "Cota máxima por caixa de correio (MiB)",
"mailbox_quota_def": "Cota de caixa de mailbox",
"mailbox_quota_m": "Cota máxima por mailbox (MiB)",
"mailbox_username": "Nome de usuário (parte esquerda de um endereço de e-mail)",
"max_aliases": "Máximo de aliases possíveis",
"max_mailboxes": "Número máximo de caixas de correio possíveis",
"max_mailboxes": "Número máximo de mailboxes possíveis",
"mins_interval": "Intervalo de votação (minutos)",
"multiple_bookings": "Várias reservas",
"nexthop": "Próximo salto",
@@ -86,10 +86,10 @@
"public_comment": "Comentário público",
"quota_mb": "Cota (MiB)",
"relay_all": "Retransmita todos os destinatários",
"relay_all_info": "↪ Se você optar por <b>não</b> retransmitir todos os destinatários, precisará adicionar uma caixa de correio (“cega”) para cada destinatário que deve ser retransmitido.",
"relay_all_info": "↪ Se você optar por <b>não</b> retransmitir todos os destinatários, precisará adicionar uma mailbox (“cega”) para cada destinatário que deve ser retransmitido.",
"relay_domain": "Retransmitir este domínio",
"relay_transport_info": "<div class=\"badge fs-6 bg-info\">Informações</div> Você pode definir mapas de transporte para um destino personalizado para esse domínio. Se não for definido, uma pesquisa MX será feita.",
"relay_unknown_only": "Retransmita somente caixas de correio não existentes. As caixas de correio existentes serão entregues localmente.",
"relay_unknown_only": "Retransmita somente mailboxes não existentes. As mailboxes existentes serão entregues localmente.",
"relayhost_wrapped_tls_info": "Por favor, <b>não</b> use portas com cobertura TLS (usadas principalmente na porta 465). <br>\r\nUse qualquer porta não encapsulada e emita STARTTLS. Uma política de TLS para impor o TLS pode ser criada em “mapas de políticas de TLS”.",
"select": "Selecione...",
"select_domain": "Selecione primeiro um domínio",
@@ -212,7 +212,7 @@
"in_use_by": "Em uso por",
"inactive": "Inativo",
"include_exclude": "Incluir/Excluir",
"include_exclude_info": "Por padrão - sem seleção - <b>todas as caixas de correio são endereçadas</b>",
"include_exclude_info": "Por padrão - sem seleção - <b>todas as mailboxes são endereçadas</b>",
"includes": "Inclua esses destinatários",
"ip_check": "Verificação de IP",
"ip_check_disabled": "A verificação de IP está desativada. Você pode ativá-lo em <br><strong>Sistema > Configuração > Opções > Personalizar</strong>",
@@ -268,13 +268,13 @@
"quarantine_release_format": "Formato dos itens lançados",
"quarantine_release_format_att": "Como anexo",
"quarantine_release_format_raw": "Original não modificado",
"quarantine_retention_size": "<b>Retenções por caixa de correio: <small>0</small> indica inativo.</b> <br>",
"quarantine_retention_size": "Retenções por mailbox: <br><small>0 indica <b>inativo</b>.</small>",
"quota_notification_html": "Modelo de e-mail de notificação: <br><small>deixe em branco para restaurar o modelo padrão.</small>",
"quota_notification_sender": "Remetente do e-mail de notificação",
"quota_notification_subject": "Assunto do e-mail de notificação",
"quota_notifications": "Notificações de cotas",
"quota_notifications_info": "As notificações de cota são enviadas aos usuários uma vez ao ultrapassar 80% e uma vez ao ultrapassar 95% de uso.",
"quota_notifications_vars": "{{percent}} é igual à cota atual do usuário <br>{{username}} é o nome da caixa de correio",
"quota_notifications_vars": "{{percent}} é igual à cota atual do usuário <br>{{username}} é o nome da mailbox",
"queue_unban": "não banido",
"r_active": "Restrições ativas",
"r_inactive": "Restrições inativas",
@@ -302,7 +302,7 @@
"rsettings_insert_preset": "Inserir exemplo de predefinição “%s”",
"rsettings_preset_1": "Desative tudo, exceto o DKIM e o limite de taxa para usuários autenticados",
"rsettings_preset_2": "Postmasters querem spam",
"rsettings_preset_3": "Permitir somente remetentes específicos para uma caixa de correio (ou seja, uso somente como caixa de correio interna)",
"rsettings_preset_3": "Permitir somente remetentes específicos para uma mailbox (ou seja, uso somente como mailbox interna)",
"rsettings_preset_4": "Desativar Rspamd para um domínio",
"rspamd_com_settings": "Um nome de configuração será gerado automaticamente, veja os exemplos de predefinições abaixo. Para obter mais detalhes, consulte a documentação <a href=\"https://rspamd.com/doc/configuration/settings.html#settings-structure\" target=\"_blank\">do Rspamd</a>",
"rspamd_global_filters": "Mapas de filtro globais",
@@ -369,7 +369,7 @@
"comment_too_long": "Comentário muito longo, máximo de 160 caracteres permitidos",
"cors_invalid_method": "Método de permissão inválido especificado",
"cors_invalid_origin": "Origem de permissão inválida especificada",
"defquota_empty": "A cota padrão por caixa de correio não deve ser 0.",
"defquota_empty": "A cota padrão por mailbox não deve ser 0.",
"demo_mode_enabled": "O modo de demonstração está ativado",
"description_invalid": "A descrição do recurso para %s é inválida",
"dkim_domain_or_sel_exists": "Existe uma chave DKIM para “%s” e não será substituída",
@@ -407,22 +407,22 @@
"invalid_recipient_map_old": "Destinatário original inválido especificado: %s",
"ip_list_empty": "A lista de IPs permitidos não pode estar vazia",
"is_alias": "%s já é conhecido como endereço de alias",
"is_alias_or_mailbox": "%s já é conhecido como alias, caixa de correio ou endereço de alias expandido a partir de um domínio de alias.",
"is_alias_or_mailbox": "%s já é conhecido como alias, mailbox ou alias de endereço expandido a partir de um domínio de alias.",
"is_spam_alias": "%s já é conhecido como endereço de alias temporário (endereço de alias de spam)",
"last_key": "A última chave não pode ser excluída. Em vez disso, desative o TFA.",
"login_failed": "Falha no login",
"mailbox_defquota_exceeds_mailbox_maxquota": "A cota padrão excede o limite máximo da cota",
"mailbox_invalid": "O nome da caixa de correio é inválido",
"mailbox_invalid": "O nome da mailbox é inválido",
"mailbox_quota_exceeded": "A cota excede o limite do domínio (máx. %d MiB)",
"mailbox_quota_exceeds_domain_quota": "A cota máxima excede o limite da cota do domínio",
"mailbox_quota_left_exceeded": "Não há espaço restante (espaço restante: %d MiB)",
"mailboxes_in_use": "O máximo de caixas de correio deve ser maior ou igual a %d",
"mailboxes_in_use": "O máximo de mailboxes deve ser maior ou igual a %d",
"malformed_username": "Nome de usuário malformado",
"map_content_empty": "O conteúdo do mapa não pode estar vazio",
"max_alias_exceeded": "Número máximo de aliases excedido",
"max_mailbox_exceeded": "Número máximo de caixas de correio excedido (%d de %d)",
"max_quota_in_use": "A cota da caixa de correio deve ser maior ou igual a %d MiB",
"maxquota_empty": "A cota máxima por caixa de correio não deve ser 0.",
"max_mailbox_exceeded": "Número máximo de mailboxes excedido (%d de %d)",
"max_quota_in_use": "A cota da mailbox deve ser maior ou igual a %d MiB",
"maxquota_empty": "A cota máxima por mailbox não deve ser 0.",
"mysql_error": "Erro do MySQL: %s",
"network_host_invalid": "Rede ou host inválidos: %s",
"next_hop_interferes": "%s interfere com o nexthop %s",
@@ -619,11 +619,11 @@
"kind": "Gentil",
"last_modified": "Última modificação",
"lookup_mx": "Destination é uma expressão regular que corresponde ao nome MX (<code>.*\\ .google\\ .com</code> para rotear todos os e-mails direcionados a um MX que termina em google.com nesse salto)",
"mailbox": "Editar caixa de correio",
"mailbox_quota_def": "Cota de caixa de correio padrão",
"mailbox": "Editar mailbox",
"mailbox_quota_def": "Cota mailbox padrão",
"mailbox_relayhost_info": "Aplicado somente à caixa de correio e aos aliases diretos, substitui um host de retransmissão de domínio.",
"max_aliases": "Máximo de aliases",
"max_mailboxes": "Número máximo de caixas de correio possíveis",
"max_mailboxes": "Número máximo de mailboxes possíveis",
"max_quota": "Cota máxima por caixa de correio (MiB)",
"maxage": "Duração máxima das mensagens em dias que serão pesquisadas remotamente <br><small>(0 = ignorar a idade</small>)",
"maxbytespersecond": "Máximo de bytes por segundo <br><small>(0 = ilimitado</small>)",
@@ -657,7 +657,7 @@
"relay_all_info": "↪ Se você optar por <b>não</b> retransmitir todos os destinatários, precisará adicionar uma caixa de correio (“cega”) para cada destinatário que deve ser retransmitido.",
"relay_domain": "Retransmitir este domínio",
"relay_transport_info": "<div class=\"badge fs-6 bg-info\">Informações</div> Você pode definir mapas de transporte para um destino personalizado para esse domínio. Se não for definido, uma pesquisa MX será feita.",
"relay_unknown_only": "Retransmita somente caixas de correio não existentes. As caixas de correio existentes serão entregues localmente.",
"relay_unknown_only": "Retransmita somente mailboxes não existentes. As caixas de mailboxes serão entregues localmente.",
"relayhost": "Transportes dependentes do remetente",
"remove": "Remover",
"resource": "Recurso",
@@ -688,7 +688,7 @@
"username": "Nome de usuário",
"validate_save": "Valide e salve",
"custom_attributes": "Atributos personalizados",
"mbox_exclude": "Excluir caixas de email"
"mbox_exclude": "Excluir mailboxes"
},
"fido2": {
"confirm": "Confirme",
@@ -831,13 +831,13 @@
"last_run_reset": "Programe a seguir",
"mailbox": "Caixa de correio",
"mailbox_defaults": "Configurações padrão",
"mailbox_defaults_info": "Defina as configurações padrão para novas caixas de correio.",
"mailbox_defaults_info": "Defina as configurações padrão para novas mailboxes.",
"mailbox_defquota": "Tamanho padrão da caixa de correio",
"mailbox_templates": "Modelos de caixa de correio",
"mailbox_quota": "Tamanho máximo de uma caixa de correio",
"mailboxes": "Caixas de correio",
"mailboxes": "mailboxes",
"max_aliases": "Máximo de aliases",
"max_mailboxes": "Número máximo de caixas de correio possíveis",
"max_mailboxes": "Número máximo de mailboxes possíveis",
"max_quota": "Cota máxima por caixa de correio",
"mins_interval": "Intervalo (min)",
"msg_num": "Mensagem #",
@@ -865,7 +865,7 @@
"recipient_map_old_info": "O destino original do mapa de um destinatário deve ser um endereço de e-mail válido ou um nome de domínio.",
"recipient_maps": "Mapas de destinatários",
"relay_all": "Retransmita todos os destinatários",
"relay_unknown": "Retransmitir caixas de correio desconhecidas",
"relay_unknown": "Retransmitir mailboxes desconhecidas",
"remove": "Remover",
"resources": "Recursos",
"running": "Executando",
@@ -1010,7 +1010,7 @@
"help": "Mostrar/ocultar painel de ajuda",
"imap_smtp_server_auth_info": "Use seu endereço de e-mail completo e o mecanismo de autenticação PLAIN. <br>\r\nSeus dados de login serão criptografados pela criptografia obrigatória do lado do servidor.",
"mailcow_apps_detail": "Use um aplicativo mailcow para acessar seus e-mails, calendário, contatos e muito mais.",
"mailcow_panel_detail": "<b>Os administradores de domínio</b> criam, modificam ou excluem caixas de correio e aliases, alteram domínios e leem mais informações sobre seus domínios atribuídos. <br>\r\n<b>Os usuários de caixas de correio</b> podem criar aliases com limite de tempo (aliases de spam), alterar suas configurações de senha e filtro de spam."
"mailcow_panel_detail": "<b>Os administradores de domínio</b> criam, modificam ou excluem mailboxes e aliases, alteram domínios e leem mais informações sobre seus domínios atribuídos. <br>\n<b>Os usuários de caixas de correio</b> podem criar aliases com limite de tempo (aliases de spam), alterar suas configurações de senha e filtro de spam."
},
"success": {
"acl_saved": "ACL para o objeto %s salvo",
@@ -1298,7 +1298,7 @@
"ip_invalid": "IP inválido ignorado: %s",
"is_not_primary_alias": "Alias não primário ignorado %s",
"no_active_admin": "Não é possível desativar o último administrador ativo",
"quota_exceeded_scope": "Cota de domínio excedida: somente caixas de correio ilimitadas podem ser criadas nesse escopo de domínio.",
"quota_exceeded_scope": "Cota de domínio excedida: somente mailboxes ilimitadas podem ser criadas nesse escopo de domínio.",
"session_token": "Token de formulário inválido: incompatibilidade de token",
"session_ua": "Token de formulário inválido: erro de validação do agente de usuário"
}

View File

@@ -107,7 +107,8 @@
"timeout2": "本地主機連線逾時時間",
"username": "使用者名稱",
"validate": "驗證",
"validation_success": "驗證成功"
"validation_success": "驗證成功",
"dry": "模擬同步"
},
"admin": {
"access": "存取",
@@ -335,7 +336,22 @@
"username": "使用者名稱",
"validate_license_now": "與證書伺服器驗證 GUID",
"verify": "驗證",
"yes": "&#10003;"
"yes": "&#10003;",
"f2b_manage_external_info": "Fail2ban仍會維護禁令列表但不會主動設定規則來阻止流量。 使用下面產生的禁止清單從外部阻止流量。",
"allowed_origins": "存取控制允許來源",
"logo_dark_label": "深色模式",
"logo_normal_label": "標準",
"f2b_ban_time_increment": "禁令時間會隨著每次禁令增加",
"copy_to_clipboard": "文字已複製到剪貼簿!",
"cors_settings": "CORS 設定",
"f2b_manage_external": "外部管理 Fail2Ban",
"f2b_max_ban_time": "最大限度。 禁止時間(s)",
"allowed_methods": "存取控制允許方法",
"ip_check": "IP檢查",
"ip_check_opt_in": "選擇使用第三方服務 <strong>ipv4.mailcow.email</strong> 和 <strong>ipv6.mailcow.email</strong> 來解析外部 IP 位址。",
"ip_check_disabled": "IP 檢查已停用。 您可以在<br> <strong>系統 > 配置 > 選項 > 自訂</strong>下啟用它",
"options": "選項",
"queue_unban": "解除禁令"
},
"danger": {
"access_denied": "存取拒絕或表單資料有誤",
@@ -454,7 +470,17 @@
"username_invalid": "使用者名稱 %s 無法使用",
"validity_missing": "請設定有效期",
"value_missing": "請填入所有欄位",
"yotp_verification_failed": "Yubico OTP 認證失敗: %s"
"yotp_verification_failed": "Yubico OTP 認證失敗: %s",
"webauthn_authenticator_failed": "找不到所選的驗證器",
"webauthn_publickey_failed": "沒有為選定的身份驗證器儲存公鑰",
"webauthn_username_failed": "所選驗證器屬於另一個帳戶",
"cors_invalid_method": "指定的允許方法無效",
"cors_invalid_origin": "指定的允許來源無效",
"demo_mode_enabled": "演示模式已啟用",
"extended_sender_acl_denied": "缺少設定外部寄件者地址的 ACL",
"template_exists": "模板 %s 已存在",
"template_id_invalid": "範本 ID %s 無效",
"template_name_invalid": "模板名稱無效"
},
"debug": {
"chart_this_server": "圖表 (此伺服器)",
@@ -473,7 +499,7 @@
"restart_container": "重新啟動",
"service": "服務",
"size": "大小",
"solr_dead": "Solr 正在啟動停用或已停止運行",
"solr_dead": "Solr 正在啟動,停用或已停止運行.",
"solr_status": "Solr 狀態",
"started_at": "啟動於",
"started_on": "啟動於",
@@ -481,7 +507,19 @@
"success": "成功",
"system_containers": "系統和容器",
"uptime": "運行時間",
"username": "使用者名稱"
"username": "使用者名稱",
"architecture": "結構",
"current_time": "系統時間",
"container_running": "正在執行",
"memory": "記憶",
"container_disabled": "容器停止或停用",
"container_stopped": "已停止",
"cores": "核心",
"error_show_ip": "無法解析公用IP位址",
"show_ip": "顯示公網IP",
"update_available": "有可用更新",
"no_update_available": "系統已經是最新版本",
"update_failed": "無法檢查更新"
},
"diagnostics": {
"cname_from_a": "由 A/AAAA 紀錄獲取。只要紀錄指向正確的資源,此功能就會持續運作。",
@@ -607,7 +645,11 @@
"title": "編輯物件",
"unchanged_if_empty": "如果不更改則留空",
"username": "使用者名稱",
"validate_save": "驗證並儲存"
"validate_save": "驗證並儲存",
"domain_footer_info": "網域範圍的頁尾將會新增至與該網域內的位址關聯的所有外發電子郵件。 <br> 以下變數可用於頁尾:",
"custom_attributes": "自訂屬性",
"mbox_exclude": "排除信箱",
"pushover_sound": "聲音"
},
"fido2": {
"confirm": "確認",
@@ -646,7 +688,10 @@
"quarantine": "隔離",
"restart_netfilter": "重新啟動 netfilter",
"restart_sogo": "重新啟動 SOGo",
"user_settings": "使用者設定"
"user_settings": "使用者設定",
"email": "電子郵件",
"mailcow_system": "系統",
"mailcow_config": "配置"
},
"info": {
"awaiting_tfa_confirmation": "等待 TFA 確認",
@@ -829,7 +874,13 @@
"username": "使用者名稱",
"waiting": "等待中",
"weekly": "每週",
"yes": "&#10003;"
"yes": "&#10003;",
"templates": "範本",
"domain_templates": "域模板",
"add_template": "新增模板",
"mailbox_templates": "信箱模板",
"relay_unknown": "轉發未知信箱",
"template": "範本"
},
"oauth2": {
"access_denied": "請使用信箱使用者登入來進行 OAuth2 授權",
@@ -929,7 +980,7 @@
"delete_filters": "過濾器 %s 已刪除",
"deleted_syncjob": "同步任務 ID %s 已刪除",
"deleted_syncjobs": "同步任務 %s 已刪除",
"dkim_added": " DKIM 金鑰 %s 已儲存",
"dkim_added": "DKIM 金鑰 %s 已儲存",
"domain_add_dkim_available": "DKIM 金鑰已存在",
"dkim_duplicated": "域名的 DKIM 金鑰 %s 已複製到 %s",
"dkim_removed": " DKIM 金鑰 %s 已刪除",
@@ -976,14 +1027,21 @@
"settings_map_added": "設定規則已新增",
"settings_map_removed": "設定規則 ID %s 已刪除",
"sogo_profile_reset": "使用者 %s 的 SOGo 個人頁面已重設",
"tls_policy_map_entry_deleted": " TLS 規則 ID %s 已刪除",
"tls_policy_map_entry_deleted": "TLS 規則 ID %s 已刪除",
"tls_policy_map_entry_saved": "TLS 規則 \"%s\" 已儲存",
"ui_texts": "UI 內文更改已儲存",
"upload_success": "檔案已成功上傳",
"verified_fido2_login": "FIDO2 登入驗證成功",
"verified_totp_login": "TOTP 登入驗證成功",
"verified_webauthn_login": "WebAuthn 登入驗證成功",
"verified_yotp_login": "Yubico OTP 登入驗證成功"
"verified_yotp_login": "Yubico OTP 登入驗證成功",
"template_removed": "模板 ID %s 已刪除",
"template_added": "新增了模板 %s",
"template_modified": "模板 %s 的變更已儲存",
"cors_headers_edited": "CORS 設定已儲存",
"domain_footer_modified": "網域頁尾 %s 的變更已儲存",
"f2b_banlist_refreshed": "禁止清單 ID 已成功刷新。",
"ip_check_opt_in_modified": "IP檢查已成功儲存"
},
"tfa": {
"api_register": "%s 使用 Yubico Cloud API請在<a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">這裡</a>為這把金鑰獲取 API 金鑰",
@@ -1171,7 +1229,9 @@
"weeks": "週",
"with_app_password": "使用應用程式密碼",
"year": "年",
"years": "年"
"years": "年",
"attribute": "屬性",
"pushover_sound": "聲音"
},
"warning": {
"cannot_delete_self": "不能刪除已登入的使用者",
@@ -1185,5 +1245,43 @@
"quota_exceeded_scope": "域名容量配額已滿: 此域名現在只能創建無限容量的信箱。",
"session_token": "表單驗證失敗: 驗證碼錯誤",
"session_ua": "表單驗證失敗: User-Agent 校驗錯誤"
},
"datatables": {
"infoEmpty": "顯示 0 到 0 個條目,共 0 個條目",
"infoFiltered": "從_MAX_個總條目中過濾",
"lengthMenu": "顯示_選單_條目",
"loadingRecords": "載入中...",
"processing": "請稍等...",
"search": "搜尋:",
"zeroRecords": "未找到符合的記錄",
"paginate": {
"first": "第一的",
"last": "最後的",
"next": "下一個",
"previous": "上一個"
},
"aria": {
"sortAscending": ":啟動以升序對列進行排序",
"sortDescending": ":啟動以降序對列進行排序"
},
"expand_all": "展開全部",
"info": "顯示 _START_ 到 _END_ 條,共 _TOTAL_ 條",
"collapse_all": "全部折疊",
"emptyTable": "表中沒有可用數據",
"thousands": ",",
"decimal": "."
},
"queue": {
"deliver_mail_legend": "嘗試重新投遞選定的郵件。",
"show_message": "顯示訊息",
"unban": "解除禁令隊列",
"legend": "郵件隊列操作功能:",
"delete": "刪除所有",
"flush": "刷新隊列",
"info": "郵件隊列包含所有等待投遞的電子郵件。 如果電子郵件長時間滯留在郵件隊列中,系統會自動將其刪除。<br>對應郵件的錯誤訊息會提供有關郵件無法送達的原因的資訊。",
"ays": "請確認您要刪除目前隊列中的所有項目。",
"deliver_mail": "遞送",
"queue_manager": "隊列管理器",
"unhold_mail_legend": "釋放選定的郵件以供投遞。 (需事先持有)"
}
}

View File

@@ -114,7 +114,9 @@
<li class="logged-in-as nav-item"><a href="#" onclick="logout.submit()" class="nav-link"><b class="username-lia">{{ mailcow_cc_username }} <span class="text-info">({{ dual_login.username }})</span> </b><i class="bi bi-power ms-2"></i></a></li>
{% endif %}
{% if not is_master %}
<li class="text-warning slave-info nav-item">[ slave ]</li>
<div class="nav-link form-check form-switch my-auto d-flex align-items-center">
<li class="slave-info">[ slave ]</li>
</div>
{% endif %}
</ul>
</div><!--/.nav-collapse -->

View File

@@ -305,6 +305,14 @@
</select>
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="domain_footer_skip_replies">{{ lang.edit.domain_footer_skip_replies }}:</label>
<div class="col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" id="domain_footer_skip_replies" name="skip_replies"{% if domain_footer.skip_replies == '1' %} checked{% endif %}></label>
</div>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="domain_footer_html">{{ lang.edit.domain_footer_html }}:</label>
<div class="col-sm-10">

View File

@@ -155,7 +155,7 @@
{% if pending_tfa_authmechs["totp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
<li class="nav-item">
<a class="nav-link {% if pending_tfa_authmechs["totp"] %}active{% endif %}" href="#tfa_tab_totp" data-bs-toggle="tab" id="pending_tfa_tab_totp"><i class="bi bi-clock-history"></i> Time based OTP</a>
<a class="nav-link {% if pending_tfa_authmechs["totp"] %}active{% endif %}" href="#tfa_tab_totp" data-bs-toggle="tab" id="pending_tfa_tab_totp"><i class="bi bi-clock-history"></i> Time-based OTP</a>
</li>
{% endif %}
@@ -173,7 +173,7 @@
<form role="form" method="post" id="webauthn_auth_form">
<legend class="mt-2 mb-2">
<i class="bi bi-shield-fill-check"></i>
Authenticators
{{ lang.tfa.authenticators }}
<hr />
</legend>
<div class="list-group">
@@ -216,7 +216,7 @@
<form role="form" method="post">
<legend class="mt-2 mb-2">
<i class="bi bi-shield-fill-check"></i>
Authenticate
{{ lang.tfa.authenticators }}
<hr />
</legend>
<div class="collapse show pending-tfa-collapse" id="collapseYubiTFA">
@@ -244,7 +244,7 @@
<form role="form" method="post">
<legend class="mt-2 mb-2">
<i class="bi bi-shield-fill-check"></i>
Authenticators
{{ lang.tfa.authenticators }}
<hr />
</legend>
<div class="list-group">

View File

@@ -2,9 +2,10 @@ version: '2.1'
services:
unbound-mailcow:
image: mailcow/unbound:1.18
image: mailcow/unbound:1.20
environment:
- TZ=${TZ}
- SKIP_UNBOUND_HEALTHCHECK=${SKIP_UNBOUND_HEALTHCHECK:-n}
volumes:
- ./data/hooks/unbound:/hooks:Z
- ./data/conf/unbound/unbound.conf:/etc/unbound/unbound.conf:ro,Z
@@ -20,6 +21,7 @@ services:
image: mariadb:10.5
depends_on:
- unbound-mailcow
- netfilter-mailcow
stop_grace_period: 45s
volumes:
- mysql-vol-1:/var/lib/mysql/
@@ -45,6 +47,8 @@ services:
volumes:
- redis-vol-1:/data/
restart: always
depends_on:
- netfilter-mailcow
ports:
- "${REDIS_PORT:-127.0.0.1:7654}:6379"
environment:
@@ -58,7 +62,7 @@ services:
- redis
clamd-mailcow:
image: mailcow/clamd:1.63
image: mailcow/clamd:1.64
restart: always
depends_on:
unbound-mailcow:
@@ -77,7 +81,7 @@ services:
- clamd
rspamd-mailcow:
image: mailcow/rspamd:1.94
image: mailcow/rspamd:1.95
stop_grace_period: 30s
depends_on:
- dovecot-mailcow
@@ -107,7 +111,7 @@ services:
- rspamd
php-fpm-mailcow:
image: mailcow/phpfpm:1.85
image: mailcow/phpfpm:1.87
command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
depends_on:
- redis-mailcow
@@ -171,7 +175,7 @@ services:
- phpfpm
sogo-mailcow:
image: mailcow/sogo:1.120
image: mailcow/sogo:1.122
environment:
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
@@ -203,7 +207,7 @@ services:
labels:
ofelia.enabled: "true"
ofelia.job-exec.sogo_sessions.schedule: "@every 1m"
ofelia.job-exec.sogo_sessions.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool expire-sessions $${SOGO_EXPIRE_SESSION} || exit 0\""
ofelia.job-exec.sogo_sessions.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool -v expire-sessions $${SOGO_EXPIRE_SESSION} || exit 0\""
ofelia.job-exec.sogo_ealarms.schedule: "@every 1m"
ofelia.job-exec.sogo_ealarms.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-ealarms-notify -p /etc/sogo/sieve.creds || exit 0\""
ofelia.job-exec.sogo_eautoreply.schedule: "@every 5m"
@@ -218,9 +222,10 @@ services:
- sogo
dovecot-mailcow:
image: mailcow/dovecot:1.26
image: mailcow/dovecot:1.28.1
depends_on:
- mysql-mailcow
- netfilter-mailcow
dns:
- ${IPV4_NETWORK:-172.22.1}.254
cap_add:
@@ -241,6 +246,8 @@ services:
environment:
- DOVECOT_MASTER_USER=${DOVECOT_MASTER_USER:-}
- DOVECOT_MASTER_PASS=${DOVECOT_MASTER_PASS:-}
- MAILCOW_REPLICA_IP=${MAILCOW_REPLICA_IP:-}
- DOVEADM_REPLICA_PORT=${DOVEADM_REPLICA_PORT:-}
- LOG_LINES=${LOG_LINES:-9999}
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
@@ -298,7 +305,7 @@ services:
- dovecot
postfix-mailcow:
image: mailcow/postfix:1.72
image: mailcow/postfix:1.74
depends_on:
mysql-mailcow:
condition: service_started
@@ -398,7 +405,7 @@ services:
condition: service_started
unbound-mailcow:
condition: service_healthy
image: mailcow/acme:1.85
image: mailcow/acme:1.87
dns:
- ${IPV4_NETWORK:-172.22.1}.254
environment:
@@ -434,14 +441,8 @@ services:
- acme
netfilter-mailcow:
image: mailcow/netfilter:1.54
image: mailcow/netfilter:1.56
stop_grace_period: 30s
depends_on:
- dovecot-mailcow
- postfix-mailcow
- sogo-mailcow
- php-fpm-mailcow
- redis-mailcow
restart: always
privileged: true
environment:
@@ -452,12 +453,14 @@ services:
- SNAT6_TO_SOURCE=${SNAT6_TO_SOURCE:-n}
- REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-}
- REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-}
- MAILCOW_REPLICA_IP=${MAILCOW_REPLICA_IP:-}
- DISABLE_NETFILTER_ISOLATION_RULE=${DISABLE_NETFILTER_ISOLATION_RULE:-n}
network_mode: "host"
volumes:
- /lib/modules:/lib/modules:ro
watchdog-mailcow:
image: mailcow/watchdog:2.00
image: mailcow/watchdog:2.02
dns:
- ${IPV4_NETWORK:-172.22.1}.254
tmpfs:
@@ -529,7 +532,7 @@ services:
- watchdog
dockerapi-mailcow:
image: mailcow/dockerapi:2.06
image: mailcow/dockerapi:2.07
security_opt:
- label=disable
restart: always
@@ -547,9 +550,13 @@ services:
aliases:
- dockerapi
##### Will be removed soon #####
solr-mailcow:
image: mailcow/solr:1.8.1
image: mailcow/solr:1.8.2
restart: always
depends_on:
- netfilter-mailcow
volumes:
- solr-vol-1:/opt/solr/server/solr/dovecot-fts/data
ports:
@@ -562,9 +569,10 @@ services:
mailcow-network:
aliases:
- solr
################################
olefy-mailcow:
image: mailcow/olefy:1.11
image: mailcow/olefy:1.12
restart: always
environment:
- TZ=${TZ}

View File

@@ -34,7 +34,7 @@ if docker compose > /dev/null 2>&1; then
echo -e "\e[33mNotice: You´ll have to update this Compose Version via your Package Manager manually!\e[0m"
else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
elif docker-compose > /dev/null 2>&1; then
@@ -47,14 +47,14 @@ elif docker-compose > /dev/null 2>&1; then
echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
fi
else
echo -e "\e[31mCannot find Docker Compose.\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
@@ -363,6 +363,10 @@ SKIP_IP_CHECK=n
SKIP_HTTP_VERIFICATION=n
# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) - y/n
SKIP_UNBOUND_HEALTHCHECK=n
# Skip ClamAV (clamd-mailcow) anti-virus (Rspamd will auto-detect a missing ClamAV container) - y/n
SKIP_CLAMD=${SKIP_CLAMD}
@@ -490,6 +494,9 @@ WEBAUTHN_ONLY_TRUSTED_VENDORS=n
# Otherwise it will work normally.
SPAMHAUS_DQS_KEY=
# Prevent netfilter from setting an iptables/nftables rule to isolate the mailcow docker network - y/n
# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost
DISABLE_NETFILTER_ISOLATION_RULE=n
EOF
mkdir -p data/assets/ssl

View File

@@ -2,6 +2,7 @@
PATH=${PATH}:/opt/bin
DATE=$(date +%Y-%m-%d_%H_%M_%S)
LOCAL_ARCH=$(uname -m)
export LC_ALL=C
echo
@@ -148,6 +149,9 @@ else
echo -e "\e[31mCannot find any Docker Compose on remote, exiting...\e[0m"
exit 1
fi
REMOTE_ARCH=$(ssh -o StrictHostKeyChecking=no -i "${REMOTE_SSH_KEY}" ${REMOTE_SSH_HOST} -p ${REMOTE_SSH_PORT} "uname -m")
}
SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
@@ -164,6 +168,17 @@ echo -e "\033[1mFound compose project name ${CMPS_PRJ} for ${MAILCOW_HOSTNAME}\0
echo -e "\033[1mFound SQL ${SQLIMAGE}\033[0m"
echo
# Print Message if Local Arch and Remote Arch is not the same
if [[ $LOCAL_ARCH != $REMOTE_ARCH ]]; then
echo
echo -e "\e[1;33m!!!!!!!!!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!!!!!!!!!\e[0m"
echo -e "\e[3;33mDetected Architecture missmatch from source to destination...\e[0m"
echo -e "\e[3;33mYour backup is transferred but some volumes might be skipped!\e[0m"
echo -e "\e[1;33m!!!!!!!!!!!!!!!!!!!!!!!!!! CAUTION !!!!!!!!!!!!!!!!!!!!!!!!!!\e[0m"
echo
sleep 2
fi
# Make sure destination exists, rsync can fail under some circumstances
echo -e "\033[1mPreparing remote...\033[0m"
if ! ssh -o StrictHostKeyChecking=no \
@@ -248,8 +263,21 @@ for vol in $(docker volume ls -qf name="${CMPS_PRJ}"); do
# Cleanup
rm -rf "${SCRIPT_DIR}/../_tmp_mariabackup/"
else
elif [[ "${vol}" =~ "rspamd-vol-1" ]]; then
# Exclude rspamd-vol-1 if the Architectures are not the same on source and destination due to compatibility issues.
if [[ $LOCAL_ARCH == $REMOTE_ARCH ]]; then
echo -e "\033[1mSynchronizing ${vol} from local ${mountpoint}...\033[0m"
rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \
-i \"${REMOTE_SSH_KEY}\" \
-p ${REMOTE_SSH_PORT}" \
"${mountpoint}/" root@${REMOTE_SSH_HOST}:"${mountpoint}"
else
echo -e "\e[1;31mSkipping ${vol} from local maschine due to incompatiblity between different architecture...\e[0m"
sleep 2
continue
fi
else
echo -e "\033[1mSynchronizing ${vol} from local ${mountpoint}...\033[0m"
rsync --delete --info=progress2 -aH -e "ssh -o StrictHostKeyChecking=no \
-i \"${REMOTE_SSH_KEY}\" \

View File

@@ -53,6 +53,7 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
COMPOSE_FILE=${SCRIPT_DIR}/../docker-compose.yml
ENV_FILE=${SCRIPT_DIR}/../.env
THREADS=$(echo ${THREADS:-1})
ARCH=$(uname -m)
if ! [[ "${THREADS}" =~ ^[1-9]+$ ]] ; then
echo "Thread input is not a number!"
@@ -96,6 +97,7 @@ function backup() {
mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}"
chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}"
cp "${SCRIPT_DIR}/../mailcow.conf" "${BACKUP_LOCATION}/mailcow-${DATE}"
touch "${BACKUP_LOCATION}/mailcow-${DATE}/.$ARCH"
for bin in docker; do
if [[ -z $(which ${bin}) ]]; then
>&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
@@ -231,12 +233,29 @@ function restore() {
docker start $(docker ps -aqf name=dovecot-mailcow)
;;
rspamd)
docker stop $(docker ps -qf name=rspamd-mailcow)
docker run -it --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
docker start $(docker ps -aqf name=rspamd-mailcow)
if [[ $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then
echo -e "\e[33mCould not find a architecture signature of the loaded backup... Maybe the backup was done before the multiarch update?"
sleep 2
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 \
-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
docker start $(docker ps -aqf name=rspamd-mailcow)
elif [[ $ARCH != $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then
echo -e "\e[31mThe Architecture of the backed up mailcow OS is different then your restoring mailcow OS..."
sleep 2
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 \
-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
docker start $(docker ps -aqf name=rspamd-mailcow)
fi
;;
postfix)
docker stop $(docker ps -qf name=postfix-mailcow)
@@ -360,9 +379,17 @@ elif [[ ${1} == "restore" ]]; then
FILE_SELECTION[${i}]="redis"
((i++))
elif [[ ${file} =~ rspamd ]]; then
echo "[ ${i} ] - Rspamd data"
FILE_SELECTION[${i}]="rspamd"
((i++))
if [[ $(find "${FOLDER_SELECTION[${input_sel}]}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then
echo "[ ${i} ] - Rspamd data (unkown Arch detected, restore with caution!)"
FILE_SELECTION[${i}]="rspamd"
((i++))
elif [[ $ARCH != $(find "${FOLDER_SELECTION[${input_sel}]}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then
echo -e "\e[31m[ NaN ] - Rspamd data (incompatible Arch, cannot restore it)\e[0m"
else
echo "[ ${i} ] - Rspamd data"
FILE_SELECTION[${i}]="rspamd"
((i++))
fi
elif [[ ${file} =~ postfix ]]; then
echo "[ ${i} ] - Postfix data"
FILE_SELECTION[${i}]="postfix"

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# renovate: datasource=github-releases depName=nextcloud/server versioning=semver extractVersion=^v(?<version>.*)$
NEXTCLOUD_VERSION=28.0.0
NEXTCLOUD_VERSION=28.0.1
echo -ne "Checking prerequisites..."
sleep 1

View File

@@ -116,11 +116,11 @@ migrate_docker_nat() {
echo "Working on IPv6 NAT, please wait..."
echo ${NAT_CONFIG} > /etc/docker/daemon.json
ip6tables -F -t nat
[[ -e /etc/alpine-release ]] && rc-service docker restart || systemctl restart docker.service
[[ -e /etc/rc.conf ]] && rc-service docker restart || systemctl restart docker.service
if [[ $? -ne 0 ]]; then
echo -e "\e[31mError:\e[0m Failed to activate IPv6 NAT! Reverting and exiting."
rm /etc/docker/daemon.json
if [[ -e /etc/alpine-release ]]; then
if [[ -e /etc/rc.conf ]]; then
rc-service docker restart
else
systemctl reset-failed docker.service
@@ -181,7 +181,7 @@ if ! [[ "${DOCKER_COMPOSE_VERSION}" =~ ^(native|standalone)$ ]]; then
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
elif docker-compose > /dev/null 2>&1; then
@@ -196,14 +196,14 @@ if ! [[ "${DOCKER_COMPOSE_VERSION}" =~ ^(native|standalone)$ ]]; then
echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
echo -e "\e[31mPlease update/install regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
fi
else
echo -e "\e[31mCannot find Docker Compose.\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
@@ -216,7 +216,7 @@ elif [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then
if ! $COMPOSE_COMMAND > /dev/null 2>&1 || ! $COMPOSE_COMMAND --version | grep "^2." > /dev/null 2>&1; then
# IF it cannot find Standalone in > 2.X, then script stops
echo -e "\e[31mCannot find Docker Compose or the Version is lower then 2.X.X.\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
# If it finds the standalone Plugin it will use this instead and change the mailcow.conf Variable accordingly
@@ -236,7 +236,7 @@ elif [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then
if ! $COMPOSE_COMMAND > /dev/null 2>&1; then
# IF it cannot find Native in > 2.X, then script stops
echo -e "\e[31mCannot find Docker Compose.\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/i_u_m/i_u_m_install/\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
# If it finds the native Plugin it will use this instead and change the mailcow.conf Variable accordingly
@@ -480,6 +480,8 @@ CONFIG_ARRAY=(
"WATCHDOG_VERBOSE"
"WEBAUTHN_ONLY_TRUSTED_VENDORS"
"SPAMHAUS_DQS_KEY"
"SKIP_UNBOUND_HEALTHCHECK"
"DISABLE_NETFILTER_ISOLATION_RULE"
)
detect_bad_asn
@@ -747,6 +749,19 @@ for option in ${CONFIG_ARRAY[@]}; do
echo '# Enable watchdog verbose logging' >> mailcow.conf
echo 'WATCHDOG_VERBOSE=n' >> mailcow.conf
fi
elif [[ ${option} == "SKIP_UNBOUND_HEALTHCHECK" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) - y/n' >> mailcow.conf
echo 'SKIP_UNBOUND_HEALTHCHECK=n' >> mailcow.conf
fi
elif [[ ${option} == "DISABLE_NETFILTER_ISOLATION_RULE" ]]; then
if ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo '# Prevent netfilter from setting an iptables/nftables rule to isolate the mailcow docker network - y/n' >> mailcow.conf
echo '# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost' >> mailcow.conf
echo 'DISABLE_NETFILTER_ISOLATION_RULE=n' >> mailcow.conf
fi
elif ! grep -q ${option} mailcow.conf; then
echo "Adding new option \"${option}\" to mailcow.conf"
echo "${option}=n" >> mailcow.conf