Compare commits

...

329 Commits

Author SHA1 Message Date
Niklas Meyer
9c814cc182 Merge pull request #5922 from mailcow/staging
2024-06
2024-06-27 11:15:53 +02:00
Niklas Meyer
cf6594220c dovecot: add Flatcurve FTS Engine as EXPERIMENTAL (#5920)
* dovecot: experimental added flatcurve backend + switch

* dovecot: bump docker image
2024-06-26 11:28:18 +02:00
Niklas Meyer
2cf952eb36 [Postfix] Upgrade to Deb12 + PF to 3.7.10 & Drop TLS 1.0/1.1 per default (#5635)
* postfix: removed TLS1.0/1.1 support (natively)

* postfix: upgrade to deb12 + pf to 3.7.9

* compose: increased postfix tag

* postfix: shortened TLS syntax with new format of 3.6+
2024-06-26 10:44:07 +02:00
DerLinkman
6fc86dd7d3 acme: corrected acme-tiny download path 2024-06-24 10:00:30 +02:00
DerLinkman
bf13af9691 increased rspamd image tag 2024-06-24 10:00:16 +02:00
milkmaker
1af9c21a50 Translations update from Weblate (#5912)
* [Web] Updated lang.ca-es.json

[Web] Updated lang.ca-es.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.cs-cz.json

[Web] Updated lang.cs-cz.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.da-dk.json

[Web] Updated lang.da-dk.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.es-es.json

[Web] Updated lang.es-es.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.fi-fi.json

[Web] Updated lang.fi-fi.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.fr-fr.json

[Web] Updated lang.fr-fr.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.hu-hu.json

[Web] Updated lang.hu-hu.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.ro-ro.json

[Web] Updated lang.ro-ro.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.it-it.json

[Web] Updated lang.it-it.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.lv-lv.json

[Web] Updated lang.lv-lv.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.ko-kr.json

[Web] Updated lang.ko-kr.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.nl-nl.json

[Web] Updated lang.nl-nl.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.ru-ru.json

[Web] Updated lang.ru-ru.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.sk-sk.json

[Web] Updated lang.sk-sk.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.sv-se.json

[Web] Updated lang.sv-se.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.zh-cn.json

[Web] Updated lang.zh-cn.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.uk-ua.json

Co-authored-by: Anonymous <noreply@weblate.org>

* [Web] Updated lang.zh-tw.json

[Web] Updated lang.zh-tw.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.pt-pt.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.tr-tr.json

[Web] Updated lang.tr-tr.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.si-si.json

[Web] Updated lang.si-si.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.gr-gr.json

[Web] Updated lang.gr-gr.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.nb-no.json

Co-authored-by: Anonymous <noreply@weblate.org>

* [Web] Updated lang.lt-lt.json

Co-authored-by: Anonymous <noreply@weblate.org>

---------

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>
2024-06-24 09:07:46 +02:00
realizelol
443941e687 [Rspamd] Delete overriding obsolete rspamd plugin (#5900)
* [Dockerfiles] rspamd: Delete COPY of metadata_exporter.lua plugin

* [Dockerfiles] rspamd: Delete metadata_exporter.lua plugin file

* Dockerfile: changed way of installing rspamd (granular version)

---------

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
2024-06-24 09:07:12 +02:00
renovate[bot]
527577b438 chore(deps): update docker/build-push-action action to v6 (#5910)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-17 17:38:21 +02:00
milkmaker
9daf2d80c0 Translations update from Weblate (#5908)
* [Web] Updated lang.fr-fr.json

Co-authored-by: Paul FERA <paulfera17@gmail.com>

* [Web] Updated lang.lv-lv.json

[Web] Updated lang.lv-lv.json

Co-authored-by: Deniss <mailcow@rigaden.me>
Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>

---------

Co-authored-by: Paul FERA <paulfera17@gmail.com>
Co-authored-by: Deniss <mailcow@rigaden.me>
Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
2024-06-16 19:21:46 +02:00
Daniel
38b0641742 Remove unnecessary log lines in Postfix's log (#5817)
* Update main.cf

In order to avoid unnecessary log lines, changed:

smtpd_discard_ehlo_keywords = chunking
to this one:

# The non-logging alternative:
smtpd_discard_ehlo_keywords = chunking, silent-discard

Update main.cf to remove unnecessary log lines in Postfix log
2024-06-10 14:51:55 +02:00
Niklas Meyer
f675af5bb0 Merge pull request #5902 from mailcow/feat/nextcloud-deprecation
nextcloud: add deprecation notice once script start
2024-06-10 14:33:53 +02:00
DerLinkman
533c4e7956 nextcloud: add deprecation notice once script start 2024-06-10 14:21:13 +02:00
Niklas Meyer
1b2c2c0037 Merge pull request #5690 from mailcow:renovate/nextcloud-server-28.x
chore(deps): update dependency nextcloud/server to v28.0.6
2024-06-10 13:57:09 +02:00
Niklas Meyer
97768494e1 Merge pull request #5880 from PierrePlt:fix/blocking-last-logins
Fix blocking last logins fetching
2024-06-10 12:40:43 +02:00
Lasagne
4a052da289 Add switch to skip fetching certificates auto{config,discover} subdomains (#5838)
* Add ACME_DONT_FETCH_CERTS_FOR_HTTP_SUBDOMAINS to acme.sh

* Add ACME_DONT_FETCH_CERTS_FOR_HTTP_SUBDOMAINS to docker-compose.yml

* Add ACME_DONT_FETCH_CERTS_FOR_HTTP_SUBDOMAINS to generate_config.sh

* Add ACME_DONT_FETCH_CERTS_FOR_HTTP_SUBDOMAINS to update.sh

* AUTODISCOVER_SAN instead of long string

default on,
default is fetching certs for auto{discover,conf}

* AUTODISCOVER_SAN instead of long string

also flipped

* AUTODISCOVER_SAN instead of long string

flipped default meaning

* fix explanation for AUTODISCOVER_SAN

* AUTODISCOVER_SAN instead of long string

and flipped meaning of the bool

* fix AUTODISCOVER_SAN explanation

* Merge branch 'mailcow:staging' into staging

* update.sh: corrected syntax for mailcow.conf insertion
2024-06-10 12:33:02 +02:00
Niklas Meyer
18d7a55b15 Merge pull request #5901 from mailcow:sorbs
Remove discontinued SORBS DNSBL
2024-06-10 12:18:43 +02:00
Michael Kuron
9ca2fb7ccf Remove discontinued SORBS DNSBL 2024-06-08 12:29:08 +02:00
Niklas Meyer
b4e8355827 Merge pull request #5845 from iamspido:patch-1
remove version from docker-compose.yml
2024-06-06 15:30:15 +02:00
DerLinkman
e0bde1c459 compose: removed all versions declarations (DEPRECATED) 2024-06-06 15:29:34 +02:00
Niklas Meyer
27c007ebd3 Merge pull request #5750 from DocFraggle:staging
Fix unbound healthcheck.sh to log all messages to logfile
2024-06-06 15:27:40 +02:00
Niklas Meyer
8f3ea09732 Merge pull request #5893 from mailcow/feat/base-os
os: updated all Alpine containers to 3.20
2024-06-05 13:10:10 +02:00
DerLinkman
af626d98d3 dovecot: fixed sa-rules download 2024-06-05 13:07:12 +02:00
Niklas Meyer
34b0574e56 Merge pull request #5886 from Thomas2500:patch-1
Switch IP2Country lookup backend to shortened version
2024-06-05 12:37:31 +02:00
Niklas Meyer
49d738809b Merge pull request #5863 from mailcow:update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2024-06-05 12:32:56 +02:00
Niklas Meyer
2fa3a22eca Merge pull request #5875 from CallMeLeon167:staging_cml
Update debug.twig to include a link to the git project URL for the mailcow version tag
2024-06-05 12:31:33 +02:00
Niklas Meyer
dc5eb6f92e Merge pull request #5883 from mailcow:renovate/alpine-3.x
chore(deps): update alpine docker tag to v3.20
2024-06-05 12:27:28 +02:00
DerLinkman
ba8902f0b1 os: updated all Alpine containers to 3.20 2024-06-05 11:52:48 +02:00
milkmaker
11e9a77840 update postscreen_access.cidr 2024-06-01 00:15:03 +00:00
Thomas Bella
64cd7e74c5 Switch IP2Country lookup backend to shortened version
Improves performance of #5880
2024-05-28 20:29:05 +02:00
renovate[bot]
cac65d081e chore(deps): update dependency nextcloud/server to v28.0.6
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2024-05-28 12:54:13 +00:00
renovate[bot]
e5ada994be Update alpine Docker tag to v3.20
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2024-05-22 18:17:51 +00:00
Pierre Pelletier
6ba2459645 Fixed blocking last connection fetching 2024-05-18 11:38:41 +00:00
DerLinkman
58f63aad08 [UI] Corrected Sieve Preset 1 (Fixed Regex) 2024-05-13 15:02:41 +02:00
milkmaker
8a8687a63c [Web] Updated lang.zh-cn.json (#5876)
Co-authored-by: Koala Ng <tonghoil@hotmail.com>
2024-05-10 22:38:08 +02:00
Leon Schmidt
f7f93c360d fix formatting of the mailcow version tag link 2024-05-10 19:56:31 +02:00
Leon
c160e1f68e Update debug.twig 2024-05-10 15:57:36 +02:00
Leon Schmidt
47c08ab8d2 Update debug.twig target="_blank" attribute for the mailcow version tag link 2024-05-10 15:17:49 +02:00
Leon Schmidt
cd83ffbaa2 Update debug.twig to include a link to the git project URL for the mailcow version tag 2024-05-10 15:09:27 +02:00
milkmaker
e12981a821 [Web] Updated lang.zh-cn.json (#5873)
Co-authored-by: Koala Ng <tonghoil@hotmail.com>
2024-05-07 17:44:37 +02:00
Ramis
47fd1bb894 Update lang.ru-ru.json (#5865)
Update lang

Co-authored-by: Patrick Schult <75116288+FreddleSpl0it@users.noreply.github.com>
2024-05-03 14:05:43 +02:00
milkmaker
20582b6353 [Web] Updated lang.lv-lv.json (#5862)
Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
2024-04-30 18:28:09 +02:00
IamSpido
c8ff5387c0 remove version from docker-compose.yml
With docker version 25.05 the version 2.1 in docker-compose.yml will be obsolete.
docker-compose.yml: `version` is obsolete
2024-04-16 14:10:44 +02:00
Mitchell van Bijleveld
7cb138d515 Improve Dutch translation (#5840) 2024-04-11 21:36:45 +02:00
milkmaker
3dd4c45fab Translations update from Weblate (#5839)
* [Web] Updated lang.hu-hu.json

Co-authored-by: David Csillag <csillag.david.istvan@gmail.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.lv-lv.json

Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

---------

Co-authored-by: David Csillag <csillag.david.istvan@gmail.com>
Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
2024-04-10 21:55:31 +02:00
polido
549539bec9 Update lang.pt-pt.json (#5832)
Co-authored-by: Patrick Schult <75116288+FreddleSpl0it@users.noreply.github.com>
2024-04-08 17:48:41 +02:00
milkmaker
e449cac464 Translations update from Weblate (#5835)
* [Web] Updated lang.fr-fr.json

Co-authored-by: Quiwy <github@quiwy.ninja>

* [Web] Updated lang.sv-se.json

Co-authored-by: André J <aj@nadox.se>

---------

Co-authored-by: Quiwy <github@quiwy.ninja>
Co-authored-by: André J <aj@nadox.se>
2024-04-08 17:47:43 +02:00
milkmaker
62e458f39b [Web] Updated lang.fr-fr.json (#5824)
Co-authored-by: Quiwy <github@quiwy.ninja>
2024-04-04 19:23:02 +02:00
FreddleSpl0it
b37caaf9e5 [Web] secure container_ctrl.php 2024-04-04 16:30:35 +02:00
FreddleSpl0it
7660ca89ae [Web] break loop if rspamd_map is valid 2024-04-04 16:29:58 +02:00
Patrick Schult
36b5cccd18 Merge pull request #5819 from mailcow/staging
2024-04
2024-04-04 08:50:58 +02:00
Patrick Schult
9decfa9c31 Merge pull request #5818 from mailcow/fix/web
[Web] fix exception handler and rspamd_maps function
2024-04-04 08:19:58 +02:00
FreddleSpl0it
3aee2b6cf5 [Web] use SEC_FETCH_DEST header instead of Referer to block api requests 2024-04-03 11:43:48 +02:00
Patrick Schult
17d797cee4 Merge pull request #5751 from mailcow/fix/rspamd-rewrite-ct
[Rspamd] milter update Content-Type and Content-Transfer-Encoding header
2024-04-03 10:49:21 +02:00
Patrick Schult
75550eeea3 Merge pull request #5812 from mailcow/limit-local-addrs
[Rspamd] Set local_addrs lo mailcow networks
2024-04-03 10:48:46 +02:00
FreddleSpl0it
0d09c86c12 [Web] fix invalid rspamd map check 2024-04-03 10:08:18 +02:00
FreddleSpl0it
2db8f482db [Web] escape html of alert messages 2024-04-03 10:07:36 +02:00
FreddleSpl0it
00d4b32a1b [Web] deny api calls from sogo 2024-04-03 10:06:43 +02:00
milkmaker
8a82bab1f3 [Web] Updated lang.tr-tr.json (#5815)
Co-authored-by: Uğurcan Albayrak <canalbayrakugur@gmail.com>
2024-04-02 18:04:30 +02:00
milkmaker
237a25e6b0 update postscreen_access.cidr (#5811) 2024-04-02 02:20:31 +02:00
milkmaker
5dc836671d [Web] Updated lang.tr-tr.json (#5813)
[Web] Updated lang.tr-tr.json

Co-authored-by: Uğurcan Albayrak <canalbayrakugur@gmail.com>
Co-authored-by: evrenkoksal <evrenkoksal@gmail.com>
2024-04-01 21:57:15 +02:00
Dmitriy Alekseev
26be1cb602 Set local_addrs in Rspamd 2024-04-01 11:28:06 +03:00
yvan-algoo
dc7a48cbf9 Update French translation (#5805)
* Fix some typo in French translation

* Fix typo error introduced in last commit

* Fixed another typo introduced in my first commit
2024-03-30 01:10:12 +01:00
milkmaker
52455be815 Translations update from Weblate (#5810)
* [Web] Updated lang.lt-lt.json

[Web] Updated lang.lt-lt.json

[Web] Updated lang.lt-lt.json

[Web] Updated lang.lt-lt.json

[Web] Updated lang.lt-lt.json

[Web] Updated lang.lt-lt.json

[Web] Added lang.lt-lt.json

Co-authored-by: Ari Archer <ari@ari.lt>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.lv-lv.json

[Web] Updated lang.lv-lv.json

[Web] Updated lang.lv-lv.json

[Web] Updated lang.lv-lv.json

Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
Co-authored-by: Edgars Počs <edgars.pocs@dna.lv>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.tr-tr.json

[Web] Updated lang.tr-tr.json

[Web] Updated lang.tr-tr.json

[Web] Updated lang.tr-tr.json

[Web] Updated lang.tr-tr.json

[Web] Updated lang.tr-tr.json

Co-authored-by: evrenkoksal <evrenkoksal@gmail.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* Add lt-lt in vars.inc.php

---------

Co-authored-by: Ari Archer <ari@ari.lt>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
Co-authored-by: Edgars Počs <edgars.pocs@dna.lv>
Co-authored-by: evrenkoksal <evrenkoksal@gmail.com>
2024-03-30 01:09:22 +01:00
Niklas Meyer
bbbdcfb625 Merge pull request #5743 from mailcow/fix-5742
Remove one GmbH in Dockerfiles
2024-03-20 09:37:45 +01:00
Niklas Meyer
b054a57e16 Merge pull request #5770 from mailcow/update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2024-03-19 10:19:54 +01:00
aaadddfgh
fd73b3ad88 Update lang.zh-cn.json (#5789)
Change a better translation
2024-03-13 15:53:37 +01:00
milkmaker
8c0637b556 [Web] Updated lang.lv-lv.json (#5777)
Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
2024-03-05 17:57:55 +01:00
DerLinkman
d92ffe8fc7 helper: remove old SOGo repo to not break builds on ARM64 2024-03-01 11:41:11 +01:00
milkmaker
e0eb3a4f13 update postscreen_access.cidr 2024-03-01 00:14:54 +00:00
Niklas Meyer
1fb0060a73 Merge pull request #5765 from mailcow/feat/sogo-5.10
sogo: upgrade to 5.10.0
2024-02-27 08:22:19 +01:00
DerLinkman
d7430bf516 sogo: add new options to sogo.conf for update 5.10.0 2024-02-26 17:17:34 +01:00
DerLinkman
35f039a119 sogo: update to 5.10.0 2024-02-26 16:55:13 +01:00
milkmaker
79432a40d7 Translations update from Weblate (#5762)
* [Web] Updated lang.es-es.json

Co-authored-by: Fernando Dilland <fernandodilland@gmail.com>

* [Web] Updated lang.nb-no.json

Co-authored-by: Christer Solstrand Johannessen <csjoh@users.noreply.translate.mailcow.email>

---------

Co-authored-by: Fernando Dilland <fernandodilland@gmail.com>
Co-authored-by: Christer Solstrand Johannessen <csjoh@users.noreply.translate.mailcow.email>
2024-02-25 19:51:57 +01:00
FreddleSpl0it
98cdb95bc0 [Rspamd] milter update Content-Type and Content-Transfer-Encoding header after need_rewrite_ct 2024-02-19 11:20:19 +01:00
Hailer, Christian
02a55ce9db Fix unbound healthcheck.sh to log all messages to logfile 2024-02-19 09:26:29 +01:00
renovate[bot]
6f4720e1ea chore(deps): update thollander/actions-comment-pull-request action to v2.5.0 (#5747) 2024-02-17 11:42:30 +01:00
Peter
6a807b7799 Remove one GmbH 2024-02-15 17:43:01 +01:00
Patrick Schult
8d4ef147d2 Merge pull request #5741 from mailcow/staging
2024-02
2024-02-15 11:27:09 +01:00
milkmaker
8ed6217d1c Translations update from Weblate (#5740)
* [Web] Language file updated by 'Cleanup translation files' addon

[Web] Updated lang.it-it.json

ui: fixed broken Links to docs

ui: fix wrong docs links

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
Co-authored-by: Michele Caputo <michele@caputoweb.xyz>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Language file updated by 'Cleanup translation files' addon

Co-authored-by: milkmaker <milkmaker@mailcow.de>

---------

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
Co-authored-by: Michele Caputo <michele@caputoweb.xyz>
2024-02-14 20:11:51 +01:00
milkmaker
7dae4a976d Translations update from Weblate (#5732)
* ui: fix wrong docs links

* ui: fixed broken Links to docs

* [Web] Updated lang.nb-no.json

[Web] Updated lang.nb-no.json

[Web] Updated lang.nb-no.json

[Web] Added lang.nb-no.json

Co-authored-by: Christer Solstrand Johannessen <csjoh@users.noreply.translate.mailcow.email>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.fr-fr.json

[Web] Updated lang.fr-fr.json

Co-authored-by: Alix ANNERAUD <alix.anneraud@outlook.fr>
Co-authored-by: William Blondel <contact@williamblondel.fr>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.it-it.json

Co-authored-by: Michele Caputo <michele@caputoweb.xyz>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.zh-tw.json

Co-authored-by: BallBill <BallBill@users.noreply.translate.mailcow.email>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

[Web] Updated lang.pt-br.json

[Web] Updated lang.pt-br.json

[Web] Updated lang.pt-br.json

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

* Add Norwegian in vars.inc.php

---------

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
Co-authored-by: Christer Solstrand Johannessen <csjoh@users.noreply.translate.mailcow.email>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: Alix ANNERAUD <alix.anneraud@outlook.fr>
Co-authored-by: William Blondel <contact@williamblondel.fr>
Co-authored-by: Michele Caputo <michele@caputoweb.xyz>
Co-authored-by: BallBill <BallBill@users.noreply.translate.mailcow.email>
Co-authored-by: Abner Santana <abnerss@outlook.com>
2024-02-14 20:09:10 +01:00
FreddleSpl0it
3b83949ba3 [Netfilter] Update to 1.58 2024-02-14 13:58:07 +01:00
Patrick Schult
d8baadb991 Merge pull request #5679 from Habetdin/staging
[Netfilter] respect ban time limits
2024-02-14 11:37:24 +01:00
Patrick Schult
7d3f9fa407 Merge pull request #5727 from mailcow/fix/domain-wide-footer
[Rspamd] apply domain wide footer to alias domains
2024-02-14 09:43:04 +01:00
Patrick Schult
705d144a85 Merge pull request #5729 from mailcow/feat/readable-domainnames
[Web] display human readable domainnames instead of punycode
2024-02-14 09:36:20 +01:00
Patrick Schult
ff05cff36c Merge pull request #5730 from mailcow/fix/add-domain-gal
[Web] fix setting unchecked checkboxes on domain adding
2024-02-14 09:34:21 +01:00
Patrick Schult
861fa7b145 Merge pull request #5728 from mailcow/fix/debug-tz
[Web] fix blank /debug page with invalid timezone
2024-02-14 09:32:17 +01:00
FreddleSpl0it
d65a0bba44 [ClamAV] Update to 1.2.2 2024-02-13 09:16:38 +01:00
FreddleSpl0it
dac1bd88dc [Web] fix setting unchecked checkboxes 2024-02-09 15:17:02 +01:00
FreddleSpl0it
288dbfa37c [Web] display human readable domainnames instead of punycode 2024-02-09 15:13:45 +01:00
FreddleSpl0it
a0e55cb9b1 [Web] fix blank /debug page with invalid timezone 2024-02-09 15:08:21 +01:00
FreddleSpl0it
86ba019ca0 [Rspamd] apply domain wide footer to alias domains 2024-02-09 14:59:14 +01:00
DerLinkman
3cb9c2ece5 ui: fix wrong docs links
ui: fixed broken Links to docs
2024-02-09 08:11:20 +01:00
Habetdin
1787c53d98 [Netfilter] respect ban time limits 2024-02-09 01:57:09 +03:00
Niklas Meyer
8ae762a8c8 Merge pull request #5717 from mailcow/staging
2024-01e
2024-02-08 15:58:47 +01:00
DerLinkman
63426c3cd0 unbound: remove netcat check & package 2024-02-08 15:55:26 +01:00
DerLinkman
e184713c67 added action for support label in issues 2024-02-08 13:06:02 +01:00
Niklas Meyer
1926625297 Merge pull request #5711 from amorfo77/master
[Netfilter] set IP check more relaxed on NFTables.py
2024-02-08 12:36:03 +01:00
DerLinkman
63bb8e8cef unbound: increase check interval to 30s 2024-02-08 12:23:46 +01:00
DerLinkman
583c5b48a0 dovecot: bump to docker image 1.28.1 2024-02-07 17:29:36 +01:00
DerLinkman
d08ccbce78 dovecot: fix wrong timestamps inside logs 2024-02-07 17:28:49 +01:00
DerLinkman
5a9702771c [SOGo] Fixed SOGo crash on older kernels < 5.10.0-X 2024-02-07 17:18:20 +01:00
eb91d9905b fix typpo in chain order message 2024-02-07 15:48:49 +01:00
38cc85fa4c set strict=False 2024-02-07 15:36:04 +01:00
FreddleSpl0it
77e6ef218c [Netfilter] Update to 1.57 2024-02-05 09:54:16 +01:00
FreddleSpl0it
464b6f2e93 [Netfilter] fix redis logs 2024-02-05 09:47:19 +01:00
Niklas Meyer
20c90642f9 Merge pull request #5700 from mailcow/staging
[Netfilter] fix mailcow isolation rule for iptables
2024-02-02 17:49:49 +01:00
FreddleSpl0it
57e67ea8f7 [Netfilter] fix mailcow isolation rule for iptables 2024-02-02 17:40:44 +01:00
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
Patrick Schult
121f0120f0 Merge pull request #5604 from mailcow/staging
🛷 🐄 Moocember 2023 Update | Netfilter NFTables Support and Banlist Endpoint
2023-12-19 10:59:37 +01:00
Niklas Meyer
515b85bb2f Merge pull request #5603 from mailcow/renovate/alpine-3.x
chore(deps): update alpine docker tag to v3.19
2023-12-19 10:06:21 +01:00
renovate[bot]
f27e41d19c chore(deps): update alpine docker tag to v3.19
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-12-19 08:48:40 +00:00
Niklas Meyer
603d451fc9 Merge pull request #5602 from mailcow/feat/bug-reporting-changes
Guideline Improvement + Issue Template adjusting
2023-12-19 09:48:21 +01:00
DerLinkman
89adaabb64 contributing.md: Updated guidelines 2023-12-19 09:47:12 +01:00
DerLinkman
987ca68ca6 issue_templates: corrected links + added premium support link 2023-12-18 16:02:59 +01:00
FreddleSpl0it
71defbf2f9 escapeHtml in qhandler.js 2023-12-18 14:02:05 +01:00
FreddleSpl0it
5c35b42844 Update Netfilter and Watchdog Image 2023-12-18 11:53:30 +01:00
milkmaker
904b37c4be [Web] Updated lang.pt-br.json (#5598)
Co-authored-by: Abner Santana <abnerss@outlook.com>
2023-12-16 19:23:27 +01:00
milkmaker
4e252f8243 [Web] Updated lang.pt-br.json (#5591)
Co-authored-by: Abner Santana <abnerss@outlook.com>
2023-12-13 17:50:13 +01:00
Niklas Meyer
dc3e52a900 Merge pull request #5589 from mailcow/renovate/nextcloud-server-28.x
Update dependency nextcloud/server to v28
2023-12-13 10:56:05 +01:00
milkmaker
06ad5f6652 Translations update from Weblate (#5590)
* [Web] Updated lang.ru-ru.json

Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>

* [Web] Updated lang.uk-ua.json

Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>

---------

Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>
2023-12-12 17:49:29 +01:00
renovate[bot]
c3b5474cbf Update dependency nextcloud/server to v28
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-12-12 13:30:18 +00:00
Patrick Schult
69e3b830ed Merge pull request #5453 from smarsching/watchdog-no-notify-on-startup
Allow suppressing watchdog start notification
2023-12-12 11:16:37 +01:00
Patrick Schult
96a5891ce7 Merge branch 'staging' into watchdog-no-notify-on-startup 2023-12-12 11:14:29 +01:00
FreddleSpl0it
66b9245b28 fix WATCHDOG_NOTIFY_WEBHOOK env vars 2023-12-12 11:10:10 +01:00
DerLinkman
f38ec68695 [SOGo] Update to 5.9.1 2023-12-12 11:00:16 +01:00
Patrick Schult
996772a27d Merge pull request #4968 from felixoi/staging
Watchdog: Allow sending notifications via webhooks
2023-12-11 16:29:52 +01:00
Patrick Schult
7f4e9c1ad4 Merge branch 'staging' into staging 2023-12-11 16:28:05 +01:00
FreddleSpl0it
218ba69501 [Watchdog] add curl verbose & use | as sed delimiter 2023-12-11 15:44:11 +01:00
Patrick Schult
c2e5dfd933 Merge pull request #5313 from mailcow/feat/f2b-banlist
[Web] add f2b_banlist endpoint
2023-12-11 12:36:06 +01:00
FreddleSpl0it
3e40bbc603 Merge remote-tracking branch 'origin/staging' into feat/f2b-banlist 2023-12-11 12:27:14 +01:00
Patrick Schult
3498d4b9c5 Merge pull request #5585 from mailcow/feat/nftables
[Netfilter] add nftables support
2023-12-11 11:54:01 +01:00
FreddleSpl0it
f4b838cad8 [Netfilter] update image & delete old server.py 2023-12-11 11:51:28 +01:00
FreddleSpl0it
86fa8634ee [Netfilter] do not ignore RETRY_WINDOW 2023-12-11 11:38:48 +01:00
milkmaker
8882006700 Translations update from Weblate (#5583)
* [Web] Updated lang.cs-cz.json

Co-authored-by: Kristian Feldsam <feldsam@gmail.com>

* [Web] Updated lang.de-de.json

Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.sk-sk.json

Co-authored-by: Kristian Feldsam <feldsam@gmail.com>

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

[Web] Updated lang.pt-br.json

Co-authored-by: Abner Santana <abnerss@outlook.com>
Co-authored-by: xmacaba <lixo@macaba.com.br>

---------

Co-authored-by: Kristian Feldsam <feldsam@gmail.com>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: Abner Santana <abnerss@outlook.com>
Co-authored-by: xmacaba <lixo@macaba.com.br>
2023-12-10 18:07:28 +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
renovate[bot]
0257736c64 Update actions/stale action to v9 (#5579)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-12-07 15:57:53 +01:00
Niklas Meyer
2024cda560 Merge pull request #5578 from mailcow/staging
2023-11a
2023-12-07 12:52:32 +01:00
DerLinkman
03aaf4ad76 Update Rspamd Image to 1.94 2023-12-07 12:50:10 +01:00
DerLinkman
550b88861f [UI] Fixed showing of "disabled" placeholder for ratelimits in domains 2023-12-07 12:10:04 +01:00
Niklas Meyer
02ae5fa007 Merge pull request #5577 from mailcow/fix/rspamd-ratelimiting
[Rspamd] Fixed Ratelimit forced by global ratelimits
2023-12-07 12:07:58 +01:00
DerLinkman
d81f105ed7 [Rspamd] Added customizable global ratelimit file (disabled by default) 2023-12-07 12:04:45 +01:00
DerLinkman
d3ed225675 [Rspamd] Removed global ratelimit override 2023-12-07 12:04:06 +01: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
Niklas Meyer
9ffc83f0f6 Merge pull request #5570 from mailcow/update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2023-12-04 10:50:23 +01:00
milkmaker
981c7d5974 [Web] Updated lang.pt-br.json (#5573)
Co-authored-by: Abner Santana <abnerss@outlook.com>
2023-12-02 15:22:45 +01:00
milkmaker
5da089ccd7 update postscreen_access.cidr 2023-12-01 00:15:24 +00:00
milkmaker
91e00f7d97 Translations update from Weblate (#5569)
* [Web] Updated lang.ru-ru.json

Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>

* [Web] Updated lang.uk-ua.json

Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>

---------

Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>
2023-11-30 21:14:42 +01:00
milkmaker
3a675fb541 [Web] Updated lang.fi-fi.json (#5567)
Co-authored-by: Mika Ruohomäki <mika.ruohomaki@ix1.fi>
2023-11-28 21:00:59 +01:00
Niklas Meyer
9a5d8d2d22 Merge pull request #5562 from startnow65/master
Detect docker compose version of form v2.x
2023-11-28 08:30:35 +01:00
DerLinkman
de812221ef Implemented improved check in update.sh as well. 2023-11-28 08:29:54 +01:00
FreddleSpl0it
340980bdd0 [Netfilter] set image back to mailcow/netfilter:1.52 2023-11-27 17:32:41 +01:00
Patrick Schult
f68a28fa2b Merge pull request #5555 from mailcow/feat/custom-footer-vars
[Web][Rspamd] domain wide footer improvements and custom mailbox attributes
2023-11-27 17:06:06 +01:00
FreddleSpl0it
7b7798e8c4 [Web] check if mbox exists before excluding it from domain wide footer 2023-11-27 17:04:29 +01:00
FreddleSpl0it
b3ac94115e [Rspamd] fix excluding alias from domain wide footer 2023-11-27 16:20:44 +01:00
DerLinkman
b1a172cad9 Use full mastodon name instead 2023-11-27 14:35:09 +01:00
DerLinkman
f2e21c68d0 Add Mastodon Links 2023-11-27 14:34:56 +01:00
DerLinkman
8b784c0eb1 Use full mastodon name instead 2023-11-27 14:34:15 +01:00
DerLinkman
bc59f32b96 Add Mastodon Links 2023-11-27 14:32:51 +01:00
Josiah Adenegan
a4fa8a4fae Detect docker compose version of form v2.x 2023-11-25 20:36:40 +00:00
Niklas Meyer
f730192c98 Merge pull request #5559 from mailcow/renovate/nextcloud-server-27.x
Update dependency nextcloud/server to v27.1.4
2023-11-24 11:16:00 +01:00
Patrick Schult
f994501296 Merge pull request #5482 from mailcow/feat/get-spam-score
[Web] add /api/v1/get/spam-score endpoint
2023-11-24 09:39:43 +01:00
renovate[bot]
9c3e73606c Update dependency nextcloud/server to v27.1.4
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-11-23 21:31:24 +00:00
milkmaker
5619e16b70 [Web] Updated lang.cs-cz.json (#5557)
Co-authored-by: Peter <magic@kthx.at>
2023-11-23 19:12:11 +01:00
FreddleSpl0it
d2e3867893 [Web][Rspamd] implement custom mailbox attributes and improve domain wide footer 2023-11-23 16:12:43 +01:00
Niklas Meyer
979f5475c3 Merge pull request #5552 from mailcow/staging
[Update.sh] Fix repo change when running in forced mode
2023-11-21 15:42:25 +01:00
DerLinkman
5a10f2dd7c Fix repo change when running in forced mode 2023-11-21 15:37:53 +01:00
Niklas Meyer
a80b5b7dd0 Merge pull request #5551 from mailcow/staging
2023-11
2023-11-21 10:39:05 +01:00
FreddleSpl0it
392967d664 [Rspamd] domain wide footer check for empty strings 2023-11-21 10:19:00 +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
Niklas Meyer
3b3c2b7141 Merge pull request #5546 from mailcow/fix/domain-wide-footer
Fix: Domain Wide Disclaimer breaks attachments visualization on Gmail and Outlook #5529
2023-11-21 10:01:38 +01:00
Niklas Meyer
f55c3c0887 Merge pull request #5548 from mailcow/fix-5547
[Web] escape quarantine html
2023-11-21 10:01:04 +01:00
FreddleSpl0it
f423ad77f3 [Web] escape quarantine html 2023-11-21 08:49:18 +01:00
FreddleSpl0it
8ba1e1ba9e [Rspamd] workaround - remove "--\x0D\x0A" prefix from rewritten cts 2023-11-20 12:38:37 +01:00
Niklas Meyer
55576084fc Merge pull request #5544 from mailcow/feat/update-renovate 2023-11-18 12:33:12 +01:00
Peter
03311b06c9 Ignore everything in vendor subdirs 2023-11-18 11:40:57 +01:00
milkmaker
b5c3d01834 Translations update from Weblate (#5538)
* [Web] Updated lang.cs-cz.json

Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.fr-fr.json

Co-authored-by: Quiwy <github@quiwy.ninja>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

---------

Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: Quiwy <github@quiwy.ninja>
2023-11-16 17:07:33 +01:00
Niklas Meyer
f398ecbe39 Merge pull request #5487 from artemislena/master
Add a helper script for generating CAA records
2023-11-16 11:42:11 +01:00
Niklas Meyer
8f1ae0f099 Merge pull request #5530 from Quiwy/staging
fix: support utf-8 in password synchronization
2023-11-16 11:21:27 +01:00
Niklas Meyer
c8bee57732 Merge pull request #5521 from raph-topo/fix/impasync-options
Add `--dry` IMAPsync Parameter as Button to select for SyncJobs
2023-11-16 11:19:47 +01:00
DerLinkman
85641794c3 Added f1f2 + sorted whitelist for imapsync 2023-11-16 11:18:50 +01:00
Niklas Meyer
849decaa59 Merge pull request #5532 from mailcow/renovate/actions-cache-3.x
Update actions/cache action to v3
2023-11-16 10:46:28 +01:00
Niklas Meyer
6e88550f92 Merge pull request #5533 from mailcow/renovate/actions-checkout-4.x
Update actions/checkout action to v4
2023-11-16 10:46:03 +01:00
renovate[bot]
7c52483887 Update actions/checkout action to v4
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-11-16 09:29:22 +00:00
renovate[bot]
0aa520c030 Update actions/cache action to v3
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-11-16 09:29:15 +00:00
Niklas Meyer
548999f163 Merge pull request #5498 from mailcow:feat/fix-5497
Update nextcloud.conf when updating nextcloud
2023-11-16 10:28:54 +01:00
DerLinkman
63df547306 Tweaked German Translation 2023-11-15 16:45:27 +01:00
DerLinkman
547d2ca308 Add Dry Mode Option for ImapSyncs (Button) 2023-11-15 16:18:18 +01:00
Quiwy
46b995f9e3 fix: support utf-8 in password synchronization 2023-11-14 10:11:25 +01: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
Niklas Meyer
1fdf704cb4 Merge pull request #5524 from mailcow/feat/fix-renovate 2023-11-12 18:28:42 +01:00
Peter
5ec9c4c750 Fix renovate regex 2023-11-12 18:00:20 +01: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
Raphael
afed94cc0e Allow --dry IMAPsync 2023-11-09 15:24:16 +01:00
Niklas Meyer
6f48c5ace0 Merge pull request #5513 from mailcow/feat/new-sieve-template
[UI] Added a new Sieve Rule as Template
2023-11-02 17:17:19 +01:00
DerLinkman
9a7e1c2b5a Added new Sieve Template. Thanks to @EricThi 2023-11-02 17:15:10 +01:00
Niklas Meyer
2ef7539d55 Merge pull request #5509 from mailcow/update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2023-11-02 17:08:04 +01:00
Burak Buylu
4e52542e33 Update lang.tr-tr.json (#5510)
Every day I will translate :)
2023-11-01 09:26:05 +01:00
milkmaker
a1895ad924 update postscreen_access.cidr 2023-11-01 00:14:31 +00:00
Niklas Meyer
d5a2c96887 Merge pull request #5459 from SecT0uch/patch-1 2023-10-30 21:55:58 +01:00
Niklas Meyer
3f30fe3113 Merge pull request #5508 from BandhiyaHardik/staging 2023-10-30 21:54:29 +01:00
HardikBandhiya
d89f24a1a3 Merge branch 'mailcow:staging' into staging 2023-10-31 02:18:14 +05:30
HardikBandhiya
413354ff29 Update README.md
changed the name of Twitter to 𝕏
2023-10-31 02:07:46 +05:30
FreddleSpl0it
a28ba5bebb [Web] fix broken github links in changelog 2023-10-30 16:07:10 +01:00
milkmaker
b93375b671 [Web] Updated lang.hu-hu.json (#5505)
Co-authored-by: Bence Kócsi <ttcrafttt@gmail.com>
2023-10-30 12:05:10 +01:00
FreddleSpl0it
f39005b72d [Netfilter] add nftables support 2023-10-30 11:54:14 +01:00
Kristian Feldsam
b568a33581 [web] sk and cz translations (#5502)
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-10-30 10:09:22 +01:00
Niklas Meyer
b05ef8edac Merge pull request #5500 from mailcow/renovate/nextcloud-server-27.x 2023-10-28 20:37:13 +02:00
renovate[bot]
015f9b663f Update dependency nextcloud/server to v27.1.3
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-10-28 14:40:08 +00:00
Niklas Meyer
b6167257c9 Merge pull request #5455 from mailcow/feat/rspamd-3.7.1 2023-10-28 16:39:53 +02:00
milkmaker
687fe044b2 [Web] Updated lang.si-si.json (#5499)
Co-authored-by: gomiunik <boris@gomiunik.net>
2023-10-28 15:10:30 +02:00
Peter
cfa47eb873 Update nextcloud.conf 2023-10-27 22:59:46 +02:00
Peter
7079000ee0 Update nextcloud.conf when updating nextcloud 2023-10-27 22:56:51 +02:00
milkmaker
f60c4f39ee [Web] Updated lang.si-si.json (#5494)
Co-authored-by: gomiunik <boris@gomiunik.net>
2023-10-25 19:46:19 +02:00
yvan-algoo
473713219f Update lang.fr-fr.json (#5492)
- Fix typos
- Replace "..." by "…"
2023-10-25 18:38:01 +02:00
artemislena
03ed81dc3f T.: Added a script for generating CAA records 2023-10-23 19:44:28 +02:00
renovate[bot]
53543ccf26 Update thollander/actions-comment-pull-request action to v2.4.3 (#5484)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-10-21 12:10:34 +02:00
FreddleSpl0it
3b183933e3 [Web] add api get spam-score endpoint 2023-10-20 10:48:04 +02:00
DerLinkman
6c6fde8e2e Improved docker image pruning 2023-10-19 12:31:13 +02:00
DerLinkman
61e23b6b81 Added Dev Mode option for git diff creation 2023-10-19 12:14:27 +02:00
DerLinkman
6c649debc9 Update DockerAPI to implement CPU load fix 2023-10-18 10:31:49 +02:00
milkmaker
87b0683f77 Translations update from Weblate (#5472)
* [Web] Updated lang.cs-cz.json

Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.sk-sk.json

Co-authored-by: Peter <magic@kthx.at>

---------

Co-authored-by: Peter <magic@kthx.at>
2023-10-14 22:58:28 +02:00
milkmaker
59c1e7a18a [Web] Updated lang.pt-br.json (#5471)
Co-authored-by: Peter <magic@kthx.at>
2023-10-14 14:26:06 +02:00
Pedro Lucca S.C
4f9dad5dd3 pt-br translation (#5470) 2023-10-14 14:16:07 +02:00
DerLinkman
adc6a0054c Updated compose version info color from red to yellow 2023-10-13 15:37:37 +02:00
Sebastian Marsching
5425cca47e Allow suppressing watchdog start notification.
The default behavior is still the old one (send a notifcation when the
watchdog is started), but this notification can now be suppressed by
setting WATCHDOG_NOTIFY_START=n.
2023-10-12 18:34:55 +02:00
milkmaker
8a70cdb48b Translations update from Weblate (#5460)
* [Web] Added lang.pt-br.json

Co-authored-by: Peter <magic@kthx.at>

* Add pt-br in vars.inc.php

---------

Co-authored-by: Peter <magic@kthx.at>
2023-10-12 18:27:04 +02:00
Jordan ERNST
bb4bc11383 Fix for git < v1.7.5
This change should be compatible with all git version.
(get-url available from v1.7.5)
2023-10-12 15:55:53 +02:00
Niklas Meyer
a366494c34 Merge pull request #5458 from mailcow/staging
2023-10a
2023-10-12 15:45:40 +02:00
DerLinkman
99de302ec9 Reverted restart action removal in docker-compose.yml for older 2.X compatibility 2023-10-12 15:38:58 +02:00
DerLinkman
907912046f Fix Clamd Version image in compose 2023-10-12 15:18:19 +02:00
DerLinkman
2c0d379dc5 [Rspamd] Update to 3.7.1 2023-10-12 13:05:27 +02:00
FreddleSpl0it
db2759b7d1 [Web] fix wrong content type + add more http 500 responses 2023-07-12 16:46:32 +02:00
DerLinkman
3c3b9575a2 [Netfilter] Update Compose File to 1.53 2023-07-12 09:42:17 +02:00
FreddleSpl0it
987cfd5dae [Web] f2b banlist - add http status codes 2023-07-11 10:31:25 +02:00
FreddleSpl0it
1537fb39c0 [Web] add manage f2b external option 2023-07-11 10:19:32 +02:00
FreddleSpl0it
65cbc478b8 [Web] add manage f2b external option 2023-07-11 10:13:00 +02:00
FreddleSpl0it
e2e8fbe313 [Web] add f2b_banlist endpoint 2023-07-10 13:54:23 +02:00
Felix Kleinekathöfer
a3c5f785e9 Added new env vars to docker compose 2023-02-20 22:34:53 +01:00
Felix Kleinekathöfer
7877215d59 mailcow should be lowercase 2023-01-08 20:02:46 +01:00
Felix Kleinekathöfer
e4347792b8 mailcow should be llow 2023-01-08 20:02:18 +01:00
Felix Kleinekathöfer
50fde60899 Added webhook variables to update script 2023-01-07 16:29:43 +01:00
Felix Kleinekathöfer
38f5e293b0 Webhook variables in config generation 2023-01-07 16:21:11 +01:00
Felix Kleinekathöfer
b6b399a590 Fixed POST to webhook 2023-01-07 16:00:17 +01:00
Felix Kleinekathöfer
b83841d253 Replace placeholders with sed 2023-01-07 15:44:29 +01:00
Felix Kleinekathöfer
3e69304f0f Send webhook 2023-01-06 16:25:18 +01:00
Felix Kleinekathöfer
fe8131f743 Only sent mail if enabled 2023-01-06 15:52:36 +01:00
Felix Kleinekathöfer
9ef14a20d1 Centralized checking of enabled notifications 2023-01-06 15:43:43 +01:00
Felix Kleinekathöfer
5897b97065 Renamed mail notification method for watchdog to be more general 2023-01-06 15:35:06 +01:00
152 changed files with 19539 additions and 2838 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

@@ -1,8 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: ❓ Community-driven support
- name: ❓ Community-driven support (Free)
url: https://docs.mailcow.email/#get-support
about: Please use the community forum for questions or assistance
- name: 🔥 Premium Support (Paid)
url: https://www.servercow.de/mailcow?lang=en#support
about: Buy a support subscription for any critical issues and get assisted by the mailcow Team. See conditions!
- name: 🚨 Report a security vulnerability
url: https://www.servercow.de/anfrage?lang=en
url: "mailto:info@servercow.de?subject=mailcow: dockerized Security Vulnerability"
about: Please give us appropriate time to verify, respond and fix before disclosure.

View File

@@ -12,7 +12,7 @@
"baseBranches": ["staging"],
"enabledManagers": ["github-actions", "regex", "docker-compose"],
"ignorePaths": [
"data\/web\/inc\/lib\/vendor\/matthiasmullie\/minify\/**"
"data\/web\/inc\/lib\/vendor\/**"
],
"regexManagers": [
{

View File

@@ -0,0 +1,37 @@
name: Check if labeled support, if so send message and close issue
on:
issues:
types:
- labeled
jobs:
add-comment:
if: github.event.label.name == 'support'
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Add comment
run: gh issue comment "$NUMBER" --body "$BODY"
env:
GH_TOKEN: ${{ secrets.SUPPORTISSUES_ACTION_PAT }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
BODY: |
**THIS IS A AUTOMATED MESSAGE!**
It seems your issue is not a bug.
Therefore we highly advise you to get support!
You can get support either by:
- ordering a paid [support contract at Servercow](https://www.servercow.de/mailcow?lang=en#support/) (Directly from the developers) or
- using the [community forum](https://community.mailcow.email) (**Based on volunteers! NO guaranteed answer**) or
- using the [Telegram support channel](https://t.me/mailcow) (**Based on volunteers! NO guaranteed answer**)
This issue will be closed. If you think your reported issue is not a support case feel free to comment above and if so the issue will reopened.
- name: Close issue
env:
GH_TOKEN: ${{ secrets.SUPPORTISSUES_ACTION_PAT }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
run: gh issue close "$NUMBER" -r "not planned"

View File

@@ -10,7 +10,7 @@ jobs:
if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging
steps:
- name: Send message
uses: thollander/actions-comment-pull-request@v2.4.2
uses: thollander/actions-comment-pull-request@v2.5.0
with:
GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }}
message: |

View File

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

View File

@@ -26,7 +26,7 @@ jobs:
password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64

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,5 +1,33 @@
When a problem occurs, then always for a reason! What you want to do in such a case is:
# Contribution Guidelines (Last modified on 18th December 2023)
First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow!
## Pull Requests (Last modified on 18th December 2023)
However, please note the following regarding pull requests:
1. **ALWAYS** create your PR using the staging branch of your locally cloned mailcow instance, as the pull request will end up in said staging branch of mailcow once approved. Ideally, you should simply create a new branch for your pull request that is named after the type of your PR (e.g. `feat/` for function updates or `fix/` for bug fixes) and the actual content (e.g. `sogo-6.0.0` for an update from SOGo to version 6 or `html-escape` for a fix that includes escaping HTML in mailcow).
2. Please **keep** this pull request branch **clean** and free of commits that have nothing to do with the changes you have made (e.g. commits from other users from other branches). *If you make changes to the `update.sh` script or other scripts that trigger a commit, there is usually a developer mode for clean working in this case.
3. **Test your changes before you commit them as a pull request.** <ins>If possible</ins>, write a small **test log** or demonstrate the functionality with a **screenshot or GIF**. *We will of course also test your pull request ourselves, but proof from you will save us the question of whether you have tested your own changes yourself.*
4. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.*
5. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project.
6. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort!
---
## Issue Reporting (Last modified on 18th December 2023)
If you plan to report a issue within mailcow please read and understand the following rules:
1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support).
2. **ONLY** report an error if you have the **necessary know-how (at least the basics)** for the administration of an e-mail server and the usage of Docker. mailcow is a complex and fully-fledged e-mail server including groupware components on a Docker basement and it requires a bit of technical know-how for debugging and operating.
3. **ONLY** report bugs that are contained in the latest mailcow release series. *The definition of the latest release series includes the last major patch (e.g. 2023-12) and all minor patches (revisions) below it (e.g. 2023-12a, b, c etc.).* New issue reports published starting from January 1, 2024 must meet this criterion, as versions below the latest releases are no longer supported by us.
4. When reporting a problem, please be as detailed as possible and include even the smallest changes to your mailcow installation. Simply fill out the corresponding bug report form in detail and accurately to minimize possible questions.
5. **Before you open an issue/feature request**, please first check whether a similar request already exists in the mailcow tracker on GitHub. If so, please include yourself in this request.
6. When you create a issue/feature request: Please note that the creation does <ins>**not guarantee an instant implementation or fix by the mailcow team or the community**</ins>.
7. Please **ALWAYS** anonymize any sensitive information in your bug report or feature request before submitting it.
### Quick guide to reporting problems:
1. Read your logs; follow them to see what the reason for your problem is.
2. Follow the leads given to you in your logfiles and start investigating.
3. Restarting the troubled service or the whole stack to see if the problem persists.
@@ -7,3 +35,5 @@ When a problem occurs, then always for a reason! What you want to do in such a c
5. Search our [issues](https://github.com/mailcow/mailcow-dockerized/issues) for your problem.
6. [Create an issue](https://github.com/mailcow/mailcow-dockerized/issues/new/choose) over at our GitHub repository if you think your problem might be a bug or a missing feature you badly need. But please make sure, that you include **all the logs** and a full description to your problem.
7. Ask your questions in our community-driven [support channels](https://docs.mailcow.email/#community-support-and-chat).
## When creating an issue/feature request or a pull request, you will be asked to confirm these guidelines.

View File

@@ -2,6 +2,8 @@
[![Translation status](https://translate.mailcow.email/widgets/mailcow-dockerized/-/translation/svg-badge.svg)](https://translate.mailcow.email/engage/mailcow-dockerized/)
[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/mailcow_email.svg?style=social&label=Follow%20%40mailcow_email)](https://twitter.com/mailcow_email)
![Mastodon Follow](https://img.shields.io/mastodon/follow/109388212176073348?domain=https%3A%2F%2Fmailcow.social&label=Follow%20%40doncow%40mailcow.social&link=https%3A%2F%2Fmailcow.social%2F%40doncow)
## Want to support mailcow?
@@ -25,7 +27,9 @@ Please see [the official documentation](https://docs.mailcow.email/) for install
[Telegram mailcow Off-Topic channel](https://t.me/mailcowOfftopic)
[Official Twitter Account](https://twitter.com/mailcow_email)
[Official 𝕏 (Twitter) Account](https://twitter.com/mailcow_email)
[Official Mastodon Account](https://mailcow.social/@doncow)
Telegram desktop clients are available for [multiple platforms](https://desktop.telegram.org). You can search the groups history for keywords.
@@ -38,4 +42,4 @@ mailcow is a registered word mark of The Infrastructure Company GmbH, Parkstr. 4
The project is managed and maintained by The Infrastructure Company GmbH.
Originated from @andryyy (André)
Originated from @andryyy (André)

View File

@@ -1,7 +1,8 @@
FROM alpine:3.17
FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
RUN apk upgrade --no-cache \
&& apk add --update --no-cache \
bash \
@@ -14,9 +15,7 @@ RUN apk upgrade --no-cache \
tini \
tzdata \
python3 \
py3-pip \
&& pip3 install --upgrade pip \
&& pip3 install acme-tiny
acme-tiny --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/
COPY acme.sh /srv/acme.sh
COPY functions.sh /srv/functions.sh

View File

@@ -33,6 +33,10 @@ if [[ "${ONLY_MAILCOW_HOSTNAME}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
ONLY_MAILCOW_HOSTNAME=y
fi
if [[ "${AUTODISCOVER_SAN}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
AUTODISCOVER_SAN=y
fi
# Request individual certificate for every domain
if [[ "${ENABLE_SSL_SNI}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
ENABLE_SSL_SNI=y
@@ -211,7 +215,11 @@ while true; do
ADDITIONAL_SAN_ARR+=($i)
fi
done
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
# Fetch certs for autoconfig and autodiscover subdomains
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
fi
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
# Start IP detection

View File

@@ -1,3 +1,3 @@
FROM debian:bullseye-slim
FROM debian:bookworm-slim
RUN apt update && apt install pigz

View File

@@ -1,12 +1,14 @@
FROM clamav/clamav:1.0.3_base
FROM alpine:3.20
LABEL maintainer "André Peters <andre.peters@servercow.de>"
LABEL maintainer "The Infrastructure Company 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.20
LABEL maintainer "The Infrastructure Company 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

@@ -358,8 +358,8 @@ class DockerApi:
for line in cmd_response.split("\n"):
if '$2$' in line:
hash = line.strip()
hash_out = re.search('\$2\$.+$', hash).group(0)
rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
hash_out = re.search(r'\$2\$.+$', hash).group(0)
rspamd_passphrase_hash = re.sub(r'[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc"
cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")

View File

@@ -1,119 +1,115 @@
FROM debian:bullseye-slim
FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
# renovate: datasource=github-tags depName=dovecot/core versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG DOVECOT=2.3.21
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^v(?<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 \
coreutils \
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 \
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 \
dovecot-fts-flatcurve \
&& 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
COPY trim_logs.sh /usr/local/bin/trim_logs.sh
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
@@ -133,6 +129,7 @@ COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
COPY quarantine_notify.py /usr/local/bin/quarantine_notify.py
COPY quota_notify.py /usr/local/bin/quota_notify.py
COPY repl_health.sh /usr/local/bin/repl_health.sh
COPY optimize-fts.sh /usr/local/bin/optimize-fts.sh
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf

View File

@@ -29,6 +29,7 @@ ${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
# Create missing directories
[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
[[ ! -d /etc/dovecot/lua/ ]] && mkdir -p /etc/dovecot/lua/
[[ ! -d /etc/dovecot/conf.d/ ]] && mkdir -p /etc/dovecot/conf.d/
[[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage
[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo
@@ -109,7 +110,14 @@ EOF
echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
if [[ "${FLATCURVE_EXPERIMENTAL}" =~ ^([yY][eE][sS]|[yY]) ]]; then
echo -e "\e[33mActivating Flatcurve as FTS Backend...\e[0m"
echo -e "\e[33mDepending on your previous setup a full reindex might be needed... \e[0m"
echo -e "\e[34mVisit https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-fts/#fts-related-dovecot-commands to learn how to reindex\e[0m"
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins_imap
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_flatcurve notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
elif [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication' > /etc/dovecot/mail_plugins
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify listescape replication mail_log' > /etc/dovecot/mail_plugins_imap
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
@@ -239,6 +247,47 @@ function script_deinit()
end
EOF
# Temporarily set FTS depending on user choice inside mailcow.conf. Will be removed as soon as Solr is dropped
if [[ "${FLATCURVE_EXPERIMENTAL}" =~ ^([yY][eE][sS]|[yY])$ ]]; then
cat <<EOF > /etc/dovecot/conf.d/fts.conf
# Autogenerated by mailcow
plugin {
fts_autoindex = yes
fts_autoindex_exclude = \Junk
fts_autoindex_exclude2 = \Trash
fts = flatcurve
# These are not flatcurve settings, but required for Dovecot FTS. See
# Dovecot FTS Configuration link above for further information.
fts_languages = en es de
fts_tokenizer_generic = algorithm=simple
fts_tokenizers = generic email-address
# OPTIONAL: Recommended default FTS core configuration
fts_filters = normalizer-icu snowball stopwords
fts_filters_en = lowercase snowball english-possessive stopwords
}
EOF
elif [[ ! "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])$ ]]; then
cat <<EOF > /etc/dovecot/conf.d/fts.conf
# Autogenerated by mailcow
plugin {
fts = solr
fts_autoindex = yes
fts_autoindex_exclude = \Junk
fts_autoindex_exclude2 = \Trash
fts_solr = url=http://solr:8983/solr/dovecot-fts/
fts_tokenizers = generic email-address
fts_tokenizer_generic = algorithm=simple
fts_filters = normalizer-icu snowball stopwords
fts_filters_en = lowercase snowball english-possessive stopwords
}
EOF
fi
# Replace patterns in app-passdb.lua
sed -i "s/__DBUSER__/${DBUSER}/g" /etc/dovecot/lua/passwd-verify.lua
sed -i "s/__DBPASS__/${DBPASS}/g" /etc/dovecot/lua/passwd-verify.lua
@@ -335,6 +384,14 @@ 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
@@ -378,7 +435,8 @@ chmod +x /usr/lib/dovecot/sieve/rspamd-pipe-ham \
/usr/local/bin/maildir_gc.sh \
/usr/local/sbin/stop-supervisor.sh \
/usr/local/bin/quota_notify.py \
/usr/local/bin/repl_health.sh
/usr/local/bin/repl_health.sh \
/usr/local/bin/optimize-fts.sh
# Prepare environment file for cronjobs
printenv | sed 's/^\(.*\)$/export \1/g' > /source_env.sh
@@ -432,4 +490,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

@@ -75,7 +75,8 @@ my $sth = $dbh->prepare("SELECT id,
custom_params,
subscribeall,
timeout1,
timeout2
timeout2,
dry
FROM imapsync
WHERE active = 1
AND is_running = 0
@@ -111,13 +112,16 @@ while ($row = $sth->fetchrow_arrayref()) {
$subscribeall = @$row[18];
$timeout1 = @$row[19];
$timeout2 = @$row[20];
$dry = @$row[21];
if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
my $template = $run_dir . '/imapsync.XXXXXXX';
my $passfile1 = File::Temp->new(TEMPLATE => $template);
my $passfile2 = File::Temp->new(TEMPLATE => $template);
binmode( $passfile1, ":utf8" );
print $passfile1 "$password1\n";
print $passfile2 trim($master_pass) . "\n";
@@ -148,6 +152,7 @@ while ($row = $sth->fetchrow_arrayref()) {
"--host2", "localhost",
"--user2", $user2 . '*' . trim($master_user),
"--passfile2", $passfile2->filename,
($dry eq "1" ? ('--dry') : ()),
'--no-modulesversion',
'--noreleasecheck'];

View File

@@ -0,0 +1,7 @@
#!/bin/bash
if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ && ! "${FLATCURVE_EXPERIMENTAL}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
exit 0
else
doveadm fts optimize -A
fi

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

@@ -11,7 +11,7 @@ else
fi
# Deploy
curl --connect-timeout 15 --retry 10 --max-time 30 http://www.spamassassin.heinlein-support.de/$(dig txt 1.4.3.spamassassin.heinlein-support.de +short | tr -d '"' | tr -dc '0-9').tar.gz --output /tmp/sa-rules-heinlein.tar.gz
curl --connect-timeout 15 --retry 10 --max-time 30 https://www.spamassassin.heinlein-support.de/$(dig txt 1.4.3.spamassassin.heinlein-support.de +short | tr -d '"' | tr -dc '0-9').tar.gz --output /tmp/sa-rules-heinlein.tar.gz
if gzip -t /tmp/sa-rules-heinlein.tar.gz; then
tar xfvz /tmp/sa-rules-heinlein.tar.gz -C /tmp/sa-rules-heinlein
cat /tmp/sa-rules-heinlein/*cf > /etc/rspamd/custom/sa-rules

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,12 @@ options {
use_dns(no);
use_fqdn(no);
owner("root"); group("adm"); perm(0640);
stats_freq(0);
stats(freq(0));
keep_timestamp(no);
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 +37,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,12 @@ options {
use_dns(no);
use_fqdn(no);
owner("root"); group("adm"); perm(0640);
stats_freq(0);
stats(freq(0));
keep_timestamp(no);
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 +37,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,6 +1,9 @@
FROM alpine:3.17
FROM alpine:3.20
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
@@ -12,12 +15,16 @@ RUN apk add --virtual .build-deps \
openssl-dev \
&& apk add -U python3 \
iptables \
iptables-dev \
ip6tables \
xtables-addons \
nftables \
tzdata \
py3-pip \
py3-nftables \
musl-dev \
&& pip3 install --ignore-installed --upgrade pip \
jsonschema \
python-iptables \
redis \
ipaddress \
@@ -26,5 +33,10 @@ RUN apk add --virtual .build-deps \
# && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \
COPY server.py /
CMD ["python3", "-u", "/server.py"]
COPY modules /app/modules
COPY main.py /app/
COPY ./docker-entrypoint.sh /app/
RUN chmod +x /app/docker-entrypoint.sh
CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"]

View File

@@ -0,0 +1,29 @@
#!/bin/sh
backend=iptables
nft list table ip filter &>/dev/null
nftables_found=$?
iptables -L -n &>/dev/null
iptables_found=$?
if [ $nftables_found -lt $iptables_found ]; then
backend=nftables
fi
if [ $nftables_found -gt $iptables_found ]; then
backend=iptables
fi
if [ $nftables_found -eq 0 ] && [ $nftables_found -eq $iptables_found ]; then
nftables_lines=$(nft list ruleset | wc -l)
iptables_lines=$(iptables-save | wc -l)
if [ $nftables_lines -gt $iptables_lines ]; then
backend=nftables
else
backend=iptables
fi
fi
exec python -u /app/main.py $backend

View File

@@ -0,0 +1,503 @@
#!/usr/bin/env python3
import re
import os
import sys
import time
import atexit
import signal
import ipaddress
from collections import Counter
from random import randint
from threading import Thread
from threading import Lock
import redis
import json
import dns.resolver
import dns.exception
import uuid
from modules.Logger import Logger
from modules.IPTables import IPTables
from modules.NFTables import NFTables
# globals
WHITELIST = []
BLACKLIST= []
bans = {}
quit_now = False
exit_code = 0
lock = Lock()
chain_name = "MAILCOW"
r = None
pubsub = None
clear_before_quit = False
def refreshF2boptions():
global f2boptions
global quit_now
global exit_code
f2boptions = {}
if not r.get('F2B_OPTIONS'):
f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
else:
try:
f2boptions = json.loads(r.get('F2B_OPTIONS'))
except ValueError:
logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json')
quit_now = True
exit_code = 2
verifyF2boptions(f2boptions)
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
def verifyF2boptions(f2boptions):
verifyF2boption(f2boptions,'ban_time', 1800)
verifyF2boption(f2boptions,'max_ban_time', 10000)
verifyF2boption(f2boptions,'ban_time_increment', True)
verifyF2boption(f2boptions,'max_attempts', 10)
verifyF2boption(f2boptions,'retry_window', 600)
verifyF2boption(f2boptions,'netban_ipv4', 32)
verifyF2boption(f2boptions,'netban_ipv6', 128)
verifyF2boption(f2boptions,'banlist_id', str(uuid.uuid4()))
verifyF2boption(f2boptions,'manage_external', 0)
def verifyF2boption(f2boptions, f2boption, f2bdefault):
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
def refreshF2bregex():
global f2bregex
global quit_now
global exit_code
if not r.get('F2B_REGEX'):
f2bregex = {}
f2bregex[1] = r'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
f2bregex[2] = r'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
f2bregex[3] = r'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
f2bregex[4] = r'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
f2bregex[5] = r'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
f2bregex[6] = r'-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
f2bregex[7] = r'-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
f2bregex[8] = r'-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
f2bregex[9] = r'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
f2bregex[10] = r'([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
else:
try:
f2bregex = {}
f2bregex = json.loads(r.get('F2B_REGEX'))
except ValueError:
logger.logCrit('Error loading F2B options: F2B_REGEX is not json')
quit_now = True
exit_code = 2
def get_ip(address):
ip = ipaddress.ip_address(address)
if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
ip = ip.ipv4_mapped
if ip.is_private or ip.is_loopback:
return False
return ip
def ban(address):
global f2boptions
global lock
refreshF2boptions()
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
RETRY_WINDOW = int(f2boptions['retry_window'])
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
ip = get_ip(address)
if not ip: return
address = str(ip)
self_network = ipaddress.ip_network(address)
with lock:
temp_whitelist = set(WHITELIST)
if temp_whitelist:
for wl_key in temp_whitelist:
wl_net = ipaddress.ip_network(wl_key, False)
if wl_net.overlaps(self_network):
logger.logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
return
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
net = str(net)
if not net in bans:
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
current_attempt = time.time()
if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW:
bans[net]['attempts'] = 0
bans[net]['attempts'] += 1
bans[net]['last_attempt'] = current_attempt
if bans[net]['attempts'] >= MAX_ATTEMPTS:
cur_time = int(round(time.time()))
NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter'])
logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1:
with lock:
tables.banIPv4(net)
elif int(f2boptions['manage_external']) != 1:
with lock:
tables.banIPv6(net)
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
else:
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
def unban(net):
global lock
if not net in bans:
logger.logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
return
logger.logInfo('Unbanning %s' % net)
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
with lock:
tables.unbanIPv4(net)
else:
with lock:
tables.unbanIPv6(net)
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
if net in bans:
bans[net]['attempts'] = 0
bans[net]['ban_counter'] += 1
def permBan(net, unban=False):
global f2boptions
global lock
is_unbanned = False
is_banned = False
if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
with lock:
if unban:
is_unbanned = tables.unbanIPv4(net)
elif int(f2boptions['manage_external']) != 1:
is_banned = tables.banIPv4(net)
else:
with lock:
if unban:
is_unbanned = tables.unbanIPv6(net)
elif int(f2boptions['manage_external']) != 1:
is_banned = tables.banIPv6(net)
if is_unbanned:
r.hdel('F2B_PERM_BANS', '%s' % net)
logger.logCrit('Removed host/network %s from blacklist' % net)
elif is_banned:
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
logger.logCrit('Added host/network %s to blacklist' % net)
def clear():
global lock
logger.logInfo('Clearing all bans')
for net in bans.copy():
unban(net)
with lock:
tables.clearIPv4Table()
tables.clearIPv6Table()
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():
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():
refreshF2bregex()
for rule_id, rule_regex in f2bregex.items():
if item['data'] and item['type'] == 'message':
try:
result = re.search(rule_regex, item['data'])
except re.error:
result = False
if result:
addr = result.group(1)
ip = ipaddress.ip_address(addr)
if ip.is_private or ip.is_loopback:
continue
logger.logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
ban(addr)
except Exception as ex:
logger.logWarn('Error reading log line from pubsub: %s' % ex)
pubsub = None
quit_now = True
exit_code = 2
def snat4(snat_target):
global lock
global quit_now
while not quit_now:
time.sleep(10)
with lock:
tables.snat4(snat_target, os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24')
def snat6(snat_target):
global lock
global quit_now
while not quit_now:
time.sleep(10)
with lock:
tables.snat6(snat_target, os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64'))
def autopurge():
global f2boptions
while not quit_now:
time.sleep(10)
refreshF2boptions()
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
if QUEUE_UNBAN:
for net in QUEUE_UNBAN:
unban(str(net))
for net in bans.copy():
if bans[net]['attempts'] >= MAX_ATTEMPTS:
NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter'])
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME:
unban(net)
def mailcowChainOrder():
global lock
global quit_now
global exit_code
while not quit_now:
time.sleep(10)
with lock:
quit_now, exit_code = tables.checkIPv4ChainOrder()
if quit_now: return
quit_now, exit_code = tables.checkIPv6ChainOrder()
def calcNetBanTime(ban_counter):
global f2boptions
BAN_TIME = int(f2boptions['ban_time'])
MAX_BAN_TIME = int(f2boptions['max_ban_time'])
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** ban_counter
NET_BAN_TIME = max([BAN_TIME, min([NET_BAN_TIME, MAX_BAN_TIME])])
return NET_BAN_TIME
def isIpNetwork(address):
try:
ipaddress.ip_network(address, False)
except ValueError:
return False
return True
def genNetworkList(list):
resolver = dns.resolver.Resolver()
hostnames = []
networks = []
for key in list:
if isIpNetwork(key):
networks.append(key)
else:
hostnames.append(key)
for hostname in hostnames:
hostname_ips = []
for rdtype in ['A', 'AAAA']:
try:
answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
except dns.exception.Timeout:
logger.logInfo('Hostname %s timedout on resolve' % hostname)
break
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
continue
except dns.exception.DNSException as dnsexception:
logger.logInfo('%s' % dnsexception)
continue
for rdata in answer:
hostname_ips.append(rdata.to_text())
networks.extend(hostname_ips)
return set(networks)
def whitelistUpdate():
global lock
global quit_now
global WHITELIST
while not quit_now:
start_time = time.time()
list = r.hgetall('F2B_WHITELIST')
new_whitelist = []
if list:
new_whitelist = genNetworkList(list)
with lock:
if Counter(new_whitelist) != Counter(WHITELIST):
WHITELIST = new_whitelist
logger.logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
def blacklistUpdate():
global quit_now
global BLACKLIST
while not quit_now:
start_time = time.time()
list = r.hgetall('F2B_BLACKLIST')
new_blacklist = []
if list:
new_blacklist = genNetworkList(list)
if Counter(new_blacklist) != Counter(BLACKLIST):
addban = set(new_blacklist).difference(BLACKLIST)
delban = set(BLACKLIST).difference(new_blacklist)
BLACKLIST = new_blacklist
logger.logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
if addban:
for net in addban:
permBan(net=net)
if delban:
for net in delban:
permBan(net=net, unban=True)
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
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__':
atexit.register(berfore_quit)
signal.signal(signal.SIGTERM, sigterm_quit)
# init Logger
logger = Logger()
# 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.set_redis(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()
if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':
try:
snat_ip = os.getenv('SNAT_TO_SOURCE')
snat_ipo = ipaddress.ip_address(snat_ip)
if type(snat_ipo) is ipaddress.IPv4Address:
snat4_thread = Thread(target=snat4,args=(snat_ip,))
snat4_thread.daemon = True
snat4_thread.start()
except ValueError:
print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':
try:
snat_ip = os.getenv('SNAT6_TO_SOURCE')
snat_ipo = ipaddress.ip_address(snat_ip)
if type(snat_ipo) is ipaddress.IPv6Address:
snat6_thread = Thread(target=snat6,args=(snat_ip,))
snat6_thread.daemon = True
snat6_thread.start()
except ValueError:
print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')
autopurge_thread = Thread(target=autopurge)
autopurge_thread.daemon = True
autopurge_thread.start()
mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
mailcowchainwatch_thread.daemon = True
mailcowchainwatch_thread.start()
blacklistupdate_thread = Thread(target=blacklistUpdate)
blacklistupdate_thread.daemon = True
blacklistupdate_thread.start()
whitelistupdate_thread = Thread(target=whitelistUpdate)
whitelistupdate_thread.daemon = True
whitelistupdate_thread.start()
while not quit_now:
time.sleep(0.5)
sys.exit(exit_code)

View File

@@ -0,0 +1,252 @@
import iptc
import time
import os
class IPTables:
def __init__(self, chain_name, logger):
self.chain_name = chain_name
self.logger = logger
def initChainIPv4(self):
if not iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name) in iptc.Table(iptc.Table.FILTER).chains:
iptc.Table(iptc.Table.FILTER).create_chain(self.chain_name)
for c in ['FORWARD', 'INPUT']:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
rule = iptc.Rule()
rule.src = '0.0.0.0/0'
rule.dst = '0.0.0.0/0'
target = iptc.Target(rule, self.chain_name)
rule.target = target
if rule not in chain.rules:
chain.insert_rule(rule)
def initChainIPv6(self):
if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name) in iptc.Table6(iptc.Table6.FILTER).chains:
iptc.Table6(iptc.Table6.FILTER).create_chain(self.chain_name)
for c in ['FORWARD', 'INPUT']:
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
rule = iptc.Rule6()
rule.src = '::/0'
rule.dst = '::/0'
target = iptc.Target(rule, self.chain_name)
rule.target = target
if rule not in chain.rules:
chain.insert_rule(rule)
def checkIPv4ChainOrder(self):
filter_table = iptc.Table(iptc.Table.FILTER)
filter_table.refresh()
return self.checkChainOrder(filter_table)
def checkIPv6ChainOrder(self):
filter_table = iptc.Table6(iptc.Table6.FILTER)
filter_table.refresh()
return self.checkChainOrder(filter_table)
def checkChainOrder(self, filter_table):
err = False
exit_code = None
forward_chain = iptc.Chain(filter_table, 'FORWARD')
input_chain = iptc.Chain(filter_table, 'INPUT')
for chain in [forward_chain, input_chain]:
target_found = False
for position, item in enumerate(chain.rules):
if item.target.name == self.chain_name:
target_found = True
if position > 2:
self.logger.logCrit('Error in %s chain: %s target not found, restarting container' % (chain.name, self.chain_name))
err = True
exit_code = 2
if not target_found:
self.logger.logCrit('Error in %s chain: %s target not found, restarting container' % (chain.name, self.chain_name))
err = True
exit_code = 2
return err, exit_code
def clearIPv4Table(self):
self.clearTable(iptc.Table(iptc.Table.FILTER))
def clearIPv6Table(self):
self.clearTable(iptc.Table6(iptc.Table6.FILTER))
def clearTable(self, filter_table):
filter_table.autocommit = False
forward_chain = iptc.Chain(filter_table, "FORWARD")
input_chain = iptc.Chain(filter_table, "INPUT")
mailcow_chain = iptc.Chain(filter_table, self.chain_name)
if mailcow_chain in filter_table.chains:
for rule in mailcow_chain.rules:
mailcow_chain.delete_rule(rule)
for rule in forward_chain.rules:
if rule.target.name == self.chain_name:
forward_chain.delete_rule(rule)
for rule in input_chain.rules:
if rule.target.name == self.chain_name:
input_chain.delete_rule(rule)
filter_table.delete_chain(self.chain_name)
filter_table.commit()
filter_table.refresh()
filter_table.autocommit = True
def banIPv4(self, source):
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name)
rule = iptc.Rule()
rule.src = source
target = iptc.Target(rule, "REJECT")
rule.target = target
if rule in chain.rules:
return False
chain.insert_rule(rule)
return True
def banIPv6(self, source):
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name)
rule = iptc.Rule6()
rule.src = source
target = iptc.Target(rule, "REJECT")
rule.target = target
if rule in chain.rules:
return False
chain.insert_rule(rule)
return True
def unbanIPv4(self, source):
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name)
rule = iptc.Rule()
rule.src = source
target = iptc.Target(rule, "REJECT")
rule.target = target
if rule not in chain.rules:
return False
chain.delete_rule(rule)
return True
def unbanIPv6(self, source):
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name)
rule = iptc.Rule6()
rule.src = source
target = iptc.Target(rule, "REJECT")
rule.target = target
if rule not in chain.rules:
return False
chain.delete_rule(rule)
return True
def snat4(self, snat_target, source):
try:
table = iptc.Table('nat')
table.refresh()
chain = iptc.Chain(table, 'POSTROUTING')
table.autocommit = False
new_rule = self.getSnat4Rule(snat_target, source)
if not chain.rules:
# if there are no rules in the chain, insert the new rule directly
self.logger.logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
chain.insert_rule(new_rule)
else:
for position, rule in enumerate(chain.rules):
if not hasattr(rule.target, 'parameter'):
continue
match = all((
new_rule.get_src() == rule.get_src(),
new_rule.get_dst() == rule.get_dst(),
new_rule.target.parameters == rule.target.parameters,
new_rule.target.name == rule.target.name
))
if position == 0:
if not match:
self.logger.logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
chain.insert_rule(new_rule)
else:
if match:
self.logger.logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
chain.delete_rule(rule)
table.commit()
table.autocommit = True
return True
except:
self.logger.logCrit('Error running SNAT4, retrying...')
return False
def snat6(self, snat_target, source):
try:
table = iptc.Table6('nat')
table.refresh()
chain = iptc.Chain(table, 'POSTROUTING')
table.autocommit = False
new_rule = self.getSnat6Rule(snat_target, source)
if new_rule not in chain.rules:
self.logger.logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (new_rule.src, snat_target))
chain.insert_rule(new_rule)
else:
for position, item in enumerate(chain.rules):
if item == new_rule:
if position != 0:
chain.delete_rule(new_rule)
table.commit()
table.autocommit = True
except:
self.logger.logCrit('Error running SNAT6, retrying...')
def getSnat4Rule(self, snat_target, source):
rule = iptc.Rule()
rule.src = source
rule.dst = '!' + rule.src
target = rule.create_target("SNAT")
target.to_source = snat_target
match = rule.create_match("comment")
match.comment = f'{int(round(time.time()))}'
return rule
def getSnat6Rule(self, snat_target, source):
rule = iptc.Rule6()
rule.src = source
rule.dst = '!' + rule.src
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

@@ -0,0 +1,30 @@
import time
import json
class Logger:
def __init__(self):
self.r = None
def set_redis(self, redis):
self.r = redis
def log(self, priority, message):
tolog = {}
tolog['time'] = int(round(time.time()))
tolog['priority'] = priority
tolog['message'] = message
print(message)
if self.r is not None:
try:
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
except Exception as ex:
print('Failed logging to redis: %s' % (ex))
def logWarn(self, message):
self.log('warn', message)
def logCrit(self, message):
self.log('crit', message)
def logInfo(self, message):
self.log('info', message)

View File

@@ -0,0 +1,659 @@
import nftables
import ipaddress
import os
class NFTables:
def __init__(self, chain_name, logger):
self.chain_name = chain_name
self.logger = logger
self.nft = nftables.Nftables()
self.nft.set_json_output(True)
self.nft.set_handle_output(True)
self.nft_chain_names = {'ip': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} },
'ip6': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} } }
self.search_current_chains()
def initChainIPv4(self):
self.insert_mailcow_chains("ip")
def initChainIPv6(self):
self.insert_mailcow_chains("ip6")
def checkIPv4ChainOrder(self):
return self.checkChainOrder("ip")
def checkIPv6ChainOrder(self):
return self.checkChainOrder("ip6")
def checkChainOrder(self, filter_table):
err = False
exit_code = None
for chain in ['input', 'forward']:
chain_position = self.check_mailcow_chains(filter_table, chain)
if chain_position is None: continue
if chain_position is False:
self.logger.logCrit(f'MAILCOW target not found in {filter_table} {chain} table, restarting container to fix it...')
err = True
exit_code = 2
if chain_position > 0:
chain_position += 1
self.logger.logCrit(f'MAILCOW target is in position {chain_position} in the {filter_table} {chain} table, restarting container to fix it...')
err = True
exit_code = 2
return err, exit_code
def clearIPv4Table(self):
self.clearTable("ip")
def clearIPv6Table(self):
self.clearTable("ip6")
def clearTable(self, _family):
is_empty_dict = True
json_command = self.get_base_dict()
chain_handle = self.get_chain_handle(_family, "filter", self.chain_name)
# if no handle, the chain doesn't exists
if chain_handle is not None:
is_empty_dict = False
# flush chain
mailcow_chain = {'family': _family, 'table': 'filter', 'name': self.chain_name}
flush_chain = {'flush': {'chain': mailcow_chain}}
json_command["nftables"].append(flush_chain)
# remove rule in forward chain
# remove rule in input chain
chains_family = [self.nft_chain_names[_family]['filter']['input'],
self.nft_chain_names[_family]['filter']['forward'] ]
for chain_base in chains_family:
if not chain_base: continue
rules_handle = self.get_rules_handle(_family, "filter", chain_base)
if rules_handle is not None:
for r_handle in rules_handle:
is_empty_dict = False
mailcow_rule = {'family':_family,
'table': 'filter',
'chain': chain_base,
'handle': r_handle }
delete_rules = {'delete': {'rule': mailcow_rule} }
json_command["nftables"].append(delete_rules)
# remove chain
# after delete all rules referencing this chain
if chain_handle is not None:
mc_chain_handle = {'family':_family,
'table': 'filter',
'name': self.chain_name,
'handle': chain_handle }
delete_chain = {'delete': {'chain': mc_chain_handle} }
json_command["nftables"].append(delete_chain)
if is_empty_dict == False:
if self.nft_exec_dict(json_command):
self.logger.logInfo(f"Clear completed: {_family}")
def banIPv4(self, source):
ban_dict = self.get_ban_ip_dict(source, "ip")
return self.nft_exec_dict(ban_dict)
def banIPv6(self, source):
ban_dict = self.get_ban_ip_dict(source, "ip6")
return self.nft_exec_dict(ban_dict)
def unbanIPv4(self, source):
unban_dict = self.get_unban_ip_dict(source, "ip")
if not unban_dict:
return False
return self.nft_exec_dict(unban_dict)
def unbanIPv6(self, source):
unban_dict = self.get_unban_ip_dict(source, "ip6")
if not unban_dict:
return False
return self.nft_exec_dict(unban_dict)
def snat4(self, snat_target, source):
self.snat_rule("ip", snat_target, source)
def snat6(self, snat_target, source):
self.snat_rule("ip6", snat_target, source)
def nft_exec_dict(self, query: dict):
if not query: return False
rc, output, error = self.nft.json_cmd(query)
if rc != 0:
#self.logger.logCrit(f"Nftables Error: {error}")
return False
# Prevent returning False or empty string on commands that do not produce output
if rc == 0 and len(output) == 0:
return True
return output
def get_base_dict(self):
return {'nftables': [{ 'metainfo': { 'json_schema_version': 1} } ] }
def search_current_chains(self):
nft_chain_priority = {'ip': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} },
'ip6': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} } }
# Command: 'nft list chains'
_list = {'list' : {'chains': 'null'} }
command = self.get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = self.nft_exec_dict(command)
if kernel_ruleset:
for _object in kernel_ruleset['nftables']:
chain = _object.get("chain")
if not chain: continue
_family = chain['family']
_table = chain['table']
_hook = chain.get("hook")
_priority = chain.get("prio")
_name = chain['name']
if _family not in self.nft_chain_names: continue
if _table not in self.nft_chain_names[_family]: continue
if _hook not in self.nft_chain_names[_family][_table]: continue
if _priority is None: continue
_saved_priority = nft_chain_priority[_family][_table][_hook]
if _saved_priority is None or _priority < _saved_priority:
# at this point, we know the chain has:
# hook and priority set
# and it has the lowest priority
nft_chain_priority[_family][_table][_hook] = _priority
self.nft_chain_names[_family][_table][_hook] = _name
def search_for_chain(self, kernel_ruleset: dict, chain_name: str):
found = False
for _object in kernel_ruleset["nftables"]:
chain = _object.get("chain")
if not chain:
continue
ch_name = chain.get("name")
if ch_name == chain_name:
found = True
break
return found
def get_chain_dict(self, _family: str, _name: str):
# nft (add | create) chain [<family>] <table> <name>
_chain_opts = {'family': _family, 'table': 'filter', 'name': _name }
_add = {'add': {'chain': _chain_opts} }
final_chain = self.get_base_dict()
final_chain["nftables"].append(_add)
return final_chain
def get_mailcow_jump_rule_dict(self, _family: str, _chain: str):
_jump_rule = self.get_base_dict()
_expr_opt=[]
_expr_counter = {'family': _family, 'table': 'filter', 'packets': 0, 'bytes': 0}
_counter_dict = {'counter': _expr_counter}
_expr_opt.append(_counter_dict)
_jump_opts = {'jump': {'target': self.chain_name} }
_expr_opt.append(_jump_opts)
_rule_params = {'family': _family,
'table': 'filter',
'chain': _chain,
'expr': _expr_opt,
'comment': "mailcow" }
_add_rule = {'insert': {'rule': _rule_params} }
_jump_rule["nftables"].append(_add_rule)
return _jump_rule
def insert_mailcow_chains(self, _family: str):
nft_input_chain = self.nft_chain_names[_family]['filter']['input']
nft_forward_chain = self.nft_chain_names[_family]['filter']['forward']
# Command: 'nft list table <family> filter'
_table_opts = {'family': _family, 'name': 'filter'}
_list = {'list': {'table': _table_opts} }
command = self.get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = self.nft_exec_dict(command)
if kernel_ruleset:
# chain
if not self.search_for_chain(kernel_ruleset, self.chain_name):
cadena = self.get_chain_dict(_family, self.chain_name)
if self.nft_exec_dict(cadena):
self.logger.logInfo(f"MAILCOW {_family} chain created successfully.")
input_jump_found, forward_jump_found = False, False
for _object in kernel_ruleset["nftables"]:
if not _object.get("rule"):
continue
rule = _object["rule"]
if nft_input_chain and rule["chain"] == nft_input_chain:
if rule.get("comment") and rule["comment"] == "mailcow":
input_jump_found = True
if nft_forward_chain and rule["chain"] == nft_forward_chain:
if rule.get("comment") and rule["comment"] == "mailcow":
forward_jump_found = True
if not input_jump_found:
command = self.get_mailcow_jump_rule_dict(_family, nft_input_chain)
self.nft_exec_dict(command)
if not forward_jump_found:
command = self.get_mailcow_jump_rule_dict(_family, nft_forward_chain)
self.nft_exec_dict(command)
def delete_nat_rule(self, _family:str, _chain: str, _handle:str):
delete_command = self.get_base_dict()
_rule_opts = {'family': _family,
'table': 'nat',
'chain': _chain,
'handle': _handle }
_delete = {'delete': {'rule': _rule_opts} }
delete_command["nftables"].append(_delete)
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']
# no postrouting chain, may occur if docker has ipv6 disabled.
if not chain_name: return
# Command: nft list chain <family> nat <chain_name>
_chain_opts = {'family': _family, 'table': 'nat', 'name': chain_name}
_list = {'list':{'chain': _chain_opts} }
command = self.get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = self.nft_exec_dict(command)
if not kernel_ruleset:
return
rule_position = 0
rule_handle = None
rule_found = False
for _object in kernel_ruleset["nftables"]:
if not _object.get("rule"):
continue
rule = _object["rule"]
if not rule.get("comment") or not rule["comment"] == "mailcow":
rule_position +=1
continue
rule_found = True
rule_handle = rule["handle"]
break
dest_net = ipaddress.ip_network(source_address, strict=False)
target_net = ipaddress.ip_network(snat_target, strict=False)
if rule_found:
saddr_ip = rule["expr"][0]["match"]["right"]["prefix"]["addr"]
saddr_len = int(rule["expr"][0]["match"]["right"]["prefix"]["len"])
daddr_ip = rule["expr"][1]["match"]["right"]["prefix"]["addr"]
daddr_len = int(rule["expr"][1]["match"]["right"]["prefix"]["len"])
target_ip = rule["expr"][3]["snat"]["addr"]
saddr_net = ipaddress.ip_network(saddr_ip + '/' + str(saddr_len), strict=False)
daddr_net = ipaddress.ip_network(daddr_ip + '/' + str(daddr_len), strict=False)
current_target_net = ipaddress.ip_network(target_ip, strict=False)
match = all((
dest_net == saddr_net,
dest_net == daddr_net,
target_net == current_target_net
))
try:
if rule_position == 0:
if not match:
# Position 0 , it is a mailcow rule , but it does not have the same parameters
if self.delete_nat_rule(_family, chain_name, rule_handle):
self.logger.logInfo(f'Remove rule for source network {saddr_net} to SNAT target {target_net} from {_family} nat {chain_name} chain, rule does not match configured parameters')
else:
# Position > 0 and is mailcow rule
if self.delete_nat_rule(_family, chain_name, rule_handle):
self.logger.logInfo(f'Remove rule for source network {saddr_net} to SNAT target {target_net} from {_family} nat {chain_name} chain, rule is at position {rule_position}')
except:
self.logger.logCrit(f"Error running SNAT on {_family}, retrying..." )
else:
# rule not found
json_command = self.get_base_dict()
try:
snat_dict = {'snat': {'addr': str(target_net.network_address)} }
expr_counter = {'family': _family, 'table': 'nat', 'packets': 0, 'bytes': 0}
counter_dict = {'counter': expr_counter}
prefix_dict = {'prefix': {'addr': str(dest_net.network_address), 'len': int(dest_net.prefixlen)} }
payload_dict = {'payload': {'protocol': _family, 'field': "saddr"} }
match_dict1 = {'match': {'op': '==', 'left': payload_dict, 'right': prefix_dict} }
payload_dict2 = {'payload': {'protocol': _family, 'field': "daddr"} }
match_dict2 = {'match': {'op': '!=', 'left': payload_dict2, 'right': prefix_dict } }
expr_list = [
match_dict1,
match_dict2,
counter_dict,
snat_dict
]
rule_fields = {'family': _family,
'table': 'nat',
'chain': chain_name,
'comment': "mailcow",
'expr': expr_list }
insert_dict = {'insert': {'rule': rule_fields} }
json_command["nftables"].append(insert_dict)
if self.nft_exec_dict(json_command):
self.logger.logInfo(f'Added {_family} nat {chain_name} rule for source network {dest_net} to {target_net}')
except:
self.logger.logCrit(f"Error running SNAT on {_family}, retrying...")
def get_chain_handle(self, _family: str, _table: str, chain_name: str):
chain_handle = None
# Command: 'nft list chains {family}'
_list = {'list': {'chains': {'family': _family} } }
command = self.get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = self.nft_exec_dict(command)
if kernel_ruleset:
for _object in kernel_ruleset["nftables"]:
if not _object.get("chain"):
continue
chain = _object["chain"]
if chain["family"] == _family and chain["table"] == _table and chain["name"] == chain_name:
chain_handle = chain["handle"]
break
return chain_handle
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}
_list = {'list': {'chain': _chain_opts} }
command = self.get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = self.nft_exec_dict(command)
if kernel_ruleset:
for _object in kernel_ruleset["nftables"]:
if not _object.get("rule"):
continue
rule = _object["rule"]
if rule["family"] == _family and rule["table"] == _table and rule["chain"] == chain_name:
if rule.get("comment") and rule["comment"] == _comment_filter:
rule_handle.append(rule["handle"])
return rule_handle
def get_ban_ip_dict(self, ipaddr: str, _family: str):
json_command = self.get_base_dict()
expr_opt = []
ipaddr_net = ipaddress.ip_network(ipaddr, strict=False)
right_dict = {'prefix': {'addr': str(ipaddr_net.network_address), 'len': int(ipaddr_net.prefixlen) } }
left_dict = {'payload': {'protocol': _family, 'field': 'saddr'} }
match_dict = {'op': '==', 'left': left_dict, 'right': right_dict }
expr_opt.append({'match': match_dict})
counter_dict = {'counter': {'family': _family, 'table': "filter", 'packets': 0, 'bytes': 0} }
expr_opt.append(counter_dict)
expr_opt.append({'drop': "null"})
rule_dict = {'family': _family, 'table': "filter", 'chain': self.chain_name, 'expr': expr_opt}
base_dict = {'insert': {'rule': rule_dict} }
json_command["nftables"].append(base_dict)
return json_command
def get_unban_ip_dict(self, ipaddr:str, _family: str):
json_command = self.get_base_dict()
# Command: 'nft list chain {s_family} filter MAILCOW'
_chain_opts = {'family': _family, 'table': 'filter', 'name': self.chain_name}
_list = {'list': {'chain': _chain_opts} }
command = self.get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = self.nft_exec_dict(command)
rule_handle = None
if kernel_ruleset:
for _object in kernel_ruleset["nftables"]:
if not _object.get("rule"):
continue
rule = _object["rule"]["expr"][0]["match"]
if not "payload" in rule["left"]:
continue
left_opt = rule["left"]["payload"]
if not left_opt["protocol"] == _family:
continue
if not left_opt["field"] =="saddr":
continue
# ip currently banned
rule_right = rule["right"]
if isinstance(rule_right, dict):
current_rule_ip = rule_right["prefix"]["addr"] + '/' + str(rule_right["prefix"]["len"])
else:
current_rule_ip = rule_right
current_rule_net = ipaddress.ip_network(current_rule_ip)
# ip to ban
candidate_net = ipaddress.ip_network(ipaddr, strict=False)
if current_rule_net == candidate_net:
rule_handle = _object["rule"]["handle"]
break
if rule_handle is not None:
mailcow_rule = {'family': _family, 'table': 'filter', 'chain': self.chain_name, 'handle': rule_handle}
delete_rule = {'delete': {'rule': mailcow_rule} }
json_command["nftables"].append(delete_rule)
else:
return False
return json_command
def check_mailcow_chains(self, family: str, chain: str):
position = 0
rule_found = False
chain_name = self.nft_chain_names[family]['filter'][chain]
if not chain_name: return None
_chain_opts = {'family': family, 'table': 'filter', 'name': chain_name}
_list = {'list': {'chain': _chain_opts}}
command = self.get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = self.nft_exec_dict(command)
if kernel_ruleset:
for _object in kernel_ruleset["nftables"]:
if not _object.get("rule"):
continue
rule = _object["rule"]
if rule.get("comment") and rule["comment"] == "mailcow":
rule_found = True
break
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,610 +0,0 @@
#!/usr/bin/env python3
import re
import os
import sys
import time
import atexit
import signal
import ipaddress
from collections import Counter
from random import randint
from threading import Thread
from threading import Lock
import redis
import json
import iptc
import dns.resolver
import dns.exception
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()
WHITELIST = []
BLACKLIST= []
bans = {}
quit_now = False
exit_code = 0
lock = Lock()
def log(priority, message):
tolog = {}
tolog['time'] = int(round(time.time()))
tolog['priority'] = priority
tolog['message'] = message
r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
print(message)
def logWarn(message):
log('warn', message)
def logCrit(message):
log('crit', message)
def logInfo(message):
log('info', message)
def refreshF2boptions():
global f2boptions
global quit_now
global exit_code
f2boptions = {}
if not r.get('F2B_OPTIONS'):
f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
else:
try:
f2boptions = json.loads(r.get('F2B_OPTIONS'))
except ValueError:
print('Error loading F2B options: F2B_OPTIONS is not json')
quit_now = True
exit_code = 2
verifyF2boptions(f2boptions)
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
def verifyF2boptions(f2boptions):
verifyF2boption(f2boptions,'ban_time', 1800)
verifyF2boption(f2boptions,'max_ban_time', 10000)
verifyF2boption(f2boptions,'ban_time_increment', True)
verifyF2boption(f2boptions,'max_attempts', 10)
verifyF2boption(f2boptions,'retry_window', 600)
verifyF2boption(f2boptions,'netban_ipv4', 32)
verifyF2boption(f2boptions,'netban_ipv6', 128)
def verifyF2boption(f2boptions, f2boption, f2bdefault):
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
def refreshF2bregex():
global f2bregex
global quit_now
global exit_code
if not r.get('F2B_REGEX'):
f2bregex = {}
f2bregex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
f2bregex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
f2bregex[7] = '-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
f2bregex[8] = '-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
else:
try:
f2bregex = {}
f2bregex = json.loads(r.get('F2B_REGEX'))
except ValueError:
print('Error loading F2B options: F2B_REGEX is not json')
quit_now = True
exit_code = 2
if r.exists('F2B_LOG'):
r.rename('F2B_LOG', 'NETFILTER_LOG')
def mailcowChainOrder():
global lock
global quit_now
global exit_code
while not quit_now:
time.sleep(10)
with lock:
filter4_table = iptc.Table(iptc.Table.FILTER)
filter6_table = iptc.Table6(iptc.Table6.FILTER)
filter4_table.refresh()
filter6_table.refresh()
for f in [filter4_table, filter6_table]:
forward_chain = iptc.Chain(f, 'FORWARD')
input_chain = iptc.Chain(f, 'INPUT')
for chain in [forward_chain, input_chain]:
target_found = False
for position, item in enumerate(chain.rules):
if item.target.name == 'MAILCOW':
target_found = True
if position > 2:
logCrit('Error in %s chain order: MAILCOW on position %d, restarting container' % (chain.name, position))
quit_now = True
exit_code = 2
if not target_found:
logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name))
quit_now = True
exit_code = 2
def ban(address):
global lock
refreshF2boptions()
BAN_TIME = int(f2boptions['ban_time'])
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
RETRY_WINDOW = int(f2boptions['retry_window'])
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
ip = ipaddress.ip_address(address)
if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
ip = ip.ipv4_mapped
address = str(ip)
if ip.is_private or ip.is_loopback:
return
self_network = ipaddress.ip_network(address)
with lock:
temp_whitelist = set(WHITELIST)
if temp_whitelist:
for wl_key in temp_whitelist:
wl_net = ipaddress.ip_network(wl_key, False)
if wl_net.overlaps(self_network):
logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
return
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
net = str(net)
if not net in bans:
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
bans[net]['attempts'] += 1
bans[net]['last_attempt'] = time.time()
if bans[net]['attempts'] >= MAX_ATTEMPTS:
cur_time = int(round(time.time()))
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
if type(ip) is ipaddress.IPv4Address:
with lock:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
rule = iptc.Rule()
rule.src = net
target = iptc.Target(rule, "REJECT")
rule.target = target
if rule not in chain.rules:
chain.insert_rule(rule)
else:
with lock:
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
rule = iptc.Rule6()
rule.src = net
target = iptc.Target(rule, "REJECT")
rule.target = target
if rule not in chain.rules:
chain.insert_rule(rule)
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
else:
logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
def unban(net):
global lock
if not net in bans:
logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
return
logInfo('Unbanning %s' % net)
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
with lock:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
rule = iptc.Rule()
rule.src = net
target = iptc.Target(rule, "REJECT")
rule.target = target
if rule in chain.rules:
chain.delete_rule(rule)
else:
with lock:
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
rule = iptc.Rule6()
rule.src = net
target = iptc.Target(rule, "REJECT")
rule.target = target
if rule in chain.rules:
chain.delete_rule(rule)
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
if net in bans:
bans[net]['attempts'] = 0
bans[net]['ban_counter'] += 1
def permBan(net, unban=False):
global lock
if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
with lock:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
rule = iptc.Rule()
rule.src = net
target = iptc.Target(rule, "REJECT")
rule.target = target
if rule not in chain.rules and not unban:
logCrit('Add host/network %s to blacklist' % net)
chain.insert_rule(rule)
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
elif rule in chain.rules and unban:
logCrit('Remove host/network %s from blacklist' % net)
chain.delete_rule(rule)
r.hdel('F2B_PERM_BANS', '%s' % net)
else:
with lock:
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
rule = iptc.Rule6()
rule.src = net
target = iptc.Target(rule, "REJECT")
rule.target = target
if rule not in chain.rules and not unban:
logCrit('Add host/network %s to blacklist' % net)
chain.insert_rule(rule)
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
elif rule in chain.rules and unban:
logCrit('Remove host/network %s from blacklist' % net)
chain.delete_rule(rule)
r.hdel('F2B_PERM_BANS', '%s' % net)
def quit(signum, frame):
global quit_now
quit_now = True
def clear():
global lock
logInfo('Clearing all bans')
for net in bans.copy():
unban(net)
with lock:
filter4_table = iptc.Table(iptc.Table.FILTER)
filter6_table = iptc.Table6(iptc.Table6.FILTER)
for filter_table in [filter4_table, filter6_table]:
filter_table.autocommit = False
forward_chain = iptc.Chain(filter_table, "FORWARD")
input_chain = iptc.Chain(filter_table, "INPUT")
mailcow_chain = iptc.Chain(filter_table, "MAILCOW")
if mailcow_chain in filter_table.chains:
for rule in mailcow_chain.rules:
mailcow_chain.delete_rule(rule)
for rule in forward_chain.rules:
if rule.target.name == 'MAILCOW':
forward_chain.delete_rule(rule)
for rule in input_chain.rules:
if rule.target.name == 'MAILCOW':
input_chain.delete_rule(rule)
filter_table.delete_chain("MAILCOW")
filter_table.commit()
filter_table.refresh()
filter_table.autocommit = True
r.delete('F2B_ACTIVE_BANS')
r.delete('F2B_PERM_BANS')
pubsub.unsubscribe()
def watch():
logInfo('Watching Redis channel F2B_CHANNEL')
pubsub.subscribe('F2B_CHANNEL')
global quit_now
global exit_code
while not quit_now:
try:
for item in pubsub.listen():
refreshF2bregex()
for rule_id, rule_regex in f2bregex.items():
if item['data'] and item['type'] == 'message':
try:
result = re.search(rule_regex, item['data'])
except re.error:
result = False
if result:
addr = result.group(1)
ip = ipaddress.ip_address(addr)
if ip.is_private or ip.is_loopback:
continue
logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
ban(addr)
except Exception as ex:
logWarn('Error reading log line from pubsub: %s' % ex)
quit_now = True
exit_code = 2
def snat4(snat_target):
global lock
global quit_now
def get_snat4_rule():
rule = iptc.Rule()
rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'
rule.dst = '!' + rule.src
target = rule.create_target("SNAT")
target.to_source = snat_target
match = rule.create_match("comment")
match.comment = f'{int(round(time.time()))}'
return rule
while not quit_now:
time.sleep(10)
with lock:
try:
table = iptc.Table('nat')
table.refresh()
chain = iptc.Chain(table, 'POSTROUTING')
table.autocommit = False
new_rule = get_snat4_rule()
if not chain.rules:
# if there are no rules in the chain, insert the new rule directly
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
chain.insert_rule(new_rule)
else:
for position, rule in enumerate(chain.rules):
if not hasattr(rule.target, 'parameter'):
continue
match = all((
new_rule.get_src() == rule.get_src(),
new_rule.get_dst() == rule.get_dst(),
new_rule.target.parameters == rule.target.parameters,
new_rule.target.name == rule.target.name
))
if position == 0:
if not match:
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
chain.insert_rule(new_rule)
else:
if match:
logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
chain.delete_rule(rule)
table.commit()
table.autocommit = True
except:
print('Error running SNAT4, retrying...')
def snat6(snat_target):
global lock
global quit_now
def get_snat6_rule():
rule = iptc.Rule6()
rule.src = os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64')
rule.dst = '!' + rule.src
target = rule.create_target("SNAT")
target.to_source = snat_target
return rule
while not quit_now:
time.sleep(10)
with lock:
try:
table = iptc.Table6('nat')
table.refresh()
chain = iptc.Chain(table, 'POSTROUTING')
table.autocommit = False
if get_snat6_rule() not in chain.rules:
logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat6_rule().src, snat_target))
chain.insert_rule(get_snat6_rule())
table.commit()
else:
for position, item in enumerate(chain.rules):
if item == get_snat6_rule():
if position != 0:
chain.delete_rule(get_snat6_rule())
table.commit()
table.autocommit = True
except:
print('Error running SNAT6, retrying...')
def autopurge():
while not quit_now:
time.sleep(10)
refreshF2boptions()
BAN_TIME = int(f2boptions['ban_time'])
MAX_BAN_TIME = int(f2boptions['max_ban_time'])
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
if QUEUE_UNBAN:
for net in QUEUE_UNBAN:
unban(str(net))
for net in bans.copy():
if bans[net]['attempts'] >= MAX_ATTEMPTS:
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME or TIME_SINCE_LAST_ATTEMPT > MAX_BAN_TIME:
unban(net)
def isIpNetwork(address):
try:
ipaddress.ip_network(address, False)
except ValueError:
return False
return True
def genNetworkList(list):
resolver = dns.resolver.Resolver()
hostnames = []
networks = []
for key in list:
if isIpNetwork(key):
networks.append(key)
else:
hostnames.append(key)
for hostname in hostnames:
hostname_ips = []
for rdtype in ['A', 'AAAA']:
try:
answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
except dns.exception.Timeout:
logInfo('Hostname %s timedout on resolve' % hostname)
break
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
continue
except dns.exception.DNSException as dnsexception:
logInfo('%s' % dnsexception)
continue
for rdata in answer:
hostname_ips.append(rdata.to_text())
networks.extend(hostname_ips)
return set(networks)
def whitelistUpdate():
global lock
global quit_now
global WHITELIST
while not quit_now:
start_time = time.time()
list = r.hgetall('F2B_WHITELIST')
new_whitelist = []
if list:
new_whitelist = genNetworkList(list)
with lock:
if Counter(new_whitelist) != Counter(WHITELIST):
WHITELIST = new_whitelist
logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
def blacklistUpdate():
global quit_now
global BLACKLIST
while not quit_now:
start_time = time.time()
list = r.hgetall('F2B_BLACKLIST')
new_blacklist = []
if list:
new_blacklist = genNetworkList(list)
if Counter(new_blacklist) != Counter(BLACKLIST):
addban = set(new_blacklist).difference(BLACKLIST)
delban = set(BLACKLIST).difference(new_blacklist)
BLACKLIST = new_blacklist
logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
if addban:
for net in addban:
permBan(net=net)
if delban:
for net in delban:
permBan(net=net, unban=True)
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
def initChain():
# Is called before threads start, no locking
print("Initializing mailcow netfilter chain")
# IPv4
if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:
iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW")
for c in ['FORWARD', 'INPUT']:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
rule = iptc.Rule()
rule.src = '0.0.0.0/0'
rule.dst = '0.0.0.0/0'
target = iptc.Target(rule, "MAILCOW")
rule.target = target
if rule not in chain.rules:
chain.insert_rule(rule)
# IPv6
if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), "MAILCOW") in iptc.Table6(iptc.Table6.FILTER).chains:
iptc.Table6(iptc.Table6.FILTER).create_chain("MAILCOW")
for c in ['FORWARD', 'INPUT']:
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
rule = iptc.Rule6()
rule.src = '::/0'
rule.dst = '::/0'
target = iptc.Target(rule, "MAILCOW")
rule.target = target
if rule not in chain.rules:
chain.insert_rule(rule)
if __name__ == '__main__':
# In case a previous session was killed without cleanup
clear()
# Reinit MAILCOW chain
initChain()
watch_thread = Thread(target=watch)
watch_thread.daemon = True
watch_thread.start()
if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':
try:
snat_ip = os.getenv('SNAT_TO_SOURCE')
snat_ipo = ipaddress.ip_address(snat_ip)
if type(snat_ipo) is ipaddress.IPv4Address:
snat4_thread = Thread(target=snat4,args=(snat_ip,))
snat4_thread.daemon = True
snat4_thread.start()
except ValueError:
print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':
try:
snat_ip = os.getenv('SNAT6_TO_SOURCE')
snat_ipo = ipaddress.ip_address(snat_ip)
if type(snat_ipo) is ipaddress.IPv6Address:
snat6_thread = Thread(target=snat6,args=(snat_ip,))
snat6_thread.daemon = True
snat6_thread.start()
except ValueError:
print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')
autopurge_thread = Thread(target=autopurge)
autopurge_thread.daemon = True
autopurge_thread.start()
mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
mailcowchainwatch_thread.daemon = True
mailcowchainwatch_thread.start()
blacklistupdate_thread = Thread(target=blacklistUpdate)
blacklistupdate_thread.daemon = True
blacklistupdate_thread.start()
whitelistupdate_thread = Thread(target=whitelistUpdate)
whitelistupdate_thread.daemon = True
whitelistupdate_thread.start()
signal.signal(signal.SIGTERM, quit)
atexit.register(clear)
while not quit_now:
time.sleep(0.5)
sys.exit(exit_code)

View File

@@ -1,6 +1,7 @@
FROM alpine:3.17
FROM alpine:3.20
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,18 +1,18 @@
FROM php:8.2-fpm-alpine3.17
FROM php:8.2-fpm-alpine3.20
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
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=^v(?<version>.*)$
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>.*)$
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=^v(?<version>.*)$
ARG REDIS_PECL_VERSION=6.0.1
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG COMPOSER_VERSION=2.6.5
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
ARG REDIS_PECL_VERSION=6.0.2
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
ARG COMPOSER_VERSION=2.6.6
RUN apk add -U --no-cache autoconf \
aspell-dev \

View File

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

View File

@@ -415,12 +415,6 @@ postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
b.barracudacentral.org=127.0.0.2*7
bl.mailspike.net=127.0.0.2*5
bl.mailspike.net=127.0.0.[10;11;12]*4
dnsbl.sorbs.net=127.0.0.10*8
dnsbl.sorbs.net=127.0.0.5*6
dnsbl.sorbs.net=127.0.0.7*3
dnsbl.sorbs.net=127.0.0.8*2
dnsbl.sorbs.net=127.0.0.6*2
dnsbl.sorbs.net=127.0.0.9*2
EOF
fi
DNSBL_CONFIG=$(grep -v '^#' /opt/postfix/conf/dns_blocklists.cf | grep '\S')

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ FROM debian:bullseye-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG RSPAMD_VER=rspamd_3.7.5-2~8c86c1676
ARG CODENAME=bullseye
ENV LC_ALL C
@@ -12,11 +13,14 @@ RUN apt-get update && apt-get install -y \
apt-transport-https \
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 \
&& apt-get update \
&& apt-get --no-install-recommends -y install rspamd redis-tools procps nano \
&& rm -rf /var/lib/apt/lists/* \
wget \
redis-tools \
procps \
nano \
&& arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
&& wget -P /tmp https://rspamd.com/apt-stable/pool/main/r/rspamd/${RSPAMD_VER}~${CODENAME}_${arch}.deb\
&& apt install -y /tmp/${RSPAMD_VER}~${CODENAME}_${arch}.deb \
&& rm -rf /var/lib/apt/lists/* /tmp/*\
&& apt-get autoremove --purge \
&& apt-get clean \
&& mkdir -p /run/rspamd \
@@ -25,7 +29,6 @@ RUN apt-get update && apt-get install -y \
&& sed -i 's/#analysis_keyword_table > 0/analysis_cat_table.macro_exist == "M"/g' /usr/share/rspamd/lualib/lua_scanners/oletools.lua
COPY settings.conf /etc/rspamd/settings.conf
COPY metadata_exporter.lua /usr/share/rspamd/plugins/metadata_exporter.lua
COPY set_worker_password.sh /set_worker_password.sh
COPY docker-entrypoint.sh /docker-entrypoint.sh

View File

@@ -79,6 +79,9 @@ EOF
redis-cli -h redis-mailcow SLAVEOF NO ONE
fi
# Provide additional lua modules
ln -s /usr/lib/$(uname -m)-linux-gnu/liblua5.1-cjson.so.0.0.0 /usr/lib/rspamd/cjson.so
chown -R _rspamd:_rspamd /var/lib/rspamd \
/etc/rspamd/local.d \
/etc/rspamd/override.d \

View File

@@ -1,632 +0,0 @@
--[[
Copyright (c) 2016, Andrew Lewis <nerf@judo.za.org>
Copyright (c) 2016, Vsevolod Stakhov <vsevolod@highsecure.ru>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]--
if confighelp then
return
end
-- A plugin that pushes metadata (or whole messages) to external services
local redis_params
local lua_util = require "lua_util"
local rspamd_http = require "rspamd_http"
local rspamd_util = require "rspamd_util"
local rspamd_logger = require "rspamd_logger"
local ucl = require "ucl"
local E = {}
local N = 'metadata_exporter'
local settings = {
pusher_enabled = {},
pusher_format = {},
pusher_select = {},
mime_type = 'text/plain',
defer = false,
mail_from = '',
mail_to = 'postmaster@localhost',
helo = 'rspamd',
email_template = [[From: "Rspamd" <$mail_from>
To: $mail_to
Subject: Spam alert
Date: $date
MIME-Version: 1.0
Message-ID: <$our_message_id>
Content-type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Authenticated username: $user
IP: $ip
Queue ID: $qid
SMTP FROM: $from
SMTP RCPT: $rcpt
MIME From: $header_from
MIME To: $header_to
MIME Date: $header_date
Subject: $header_subject
Message-ID: $message_id
Action: $action
Score: $score
Symbols: $symbols]],
}
local function get_general_metadata(task, flatten, no_content)
local r = {}
local ip = task:get_from_ip()
if ip and ip:is_valid() then
r.ip = tostring(ip)
else
r.ip = 'unknown'
end
r.user = task:get_user() or 'unknown'
r.qid = task:get_queue_id() or 'unknown'
r.subject = task:get_subject() or 'unknown'
r.action = task:get_metric_action('default')
local s = task:get_metric_score('default')[1]
r.score = flatten and string.format('%.2f', s) or s
local fuzzy = task:get_mempool():get_variable("fuzzy_hashes", "fstrings")
if fuzzy and #fuzzy > 0 then
local fz = {}
for _,h in ipairs(fuzzy) do
table.insert(fz, h)
end
if not flatten then
r.fuzzy = fz
else
r.fuzzy = table.concat(fz, ', ')
end
else
r.fuzzy = 'unknown'
end
local rcpt = task:get_recipients('smtp')
if rcpt then
local l = {}
for _, a in ipairs(rcpt) do
table.insert(l, a['addr'])
end
if not flatten then
r.rcpt = l
else
r.rcpt = table.concat(l, ', ')
end
else
r.rcpt = 'unknown'
end
local from = task:get_from('smtp')
if ((from or E)[1] or E).addr then
r.from = from[1].addr
else
r.from = 'unknown'
end
local syminf = task:get_symbols_all()
if flatten then
local l = {}
for _, sym in ipairs(syminf) do
local txt
if sym.options then
local topt = table.concat(sym.options, ', ')
txt = sym.name .. '(' .. string.format('%.2f', sym.score) .. ')' .. ' [' .. topt .. ']'
else
txt = sym.name .. '(' .. string.format('%.2f', sym.score) .. ')'
end
table.insert(l, txt)
end
r.symbols = table.concat(l, '\n\t')
else
r.symbols = syminf
end
local function process_header(name)
local hdr = task:get_header_full(name)
if hdr then
local l = {}
for _, h in ipairs(hdr) do
table.insert(l, h.decoded)
end
if not flatten then
return l
else
return table.concat(l, '\n')
end
else
return 'unknown'
end
end
if not no_content then
r.header_from = process_header('from')
r.header_to = process_header('to')
r.header_subject = process_header('subject')
r.header_date = process_header('date')
r.message_id = task:get_message_id()
end
return r
end
local formatters = {
default = function(task)
return task:get_content(), {}
end,
email_alert = function(task, rule, extra)
local meta = get_general_metadata(task, true)
local display_emails = {}
local mail_targets = {}
meta.mail_from = rule.mail_from or settings.mail_from
local mail_rcpt = rule.mail_to or settings.mail_to
if type(mail_rcpt) ~= 'table' then
table.insert(display_emails, string.format('<%s>', mail_rcpt))
table.insert(mail_targets, mail_rcpt)
else
for _, e in ipairs(mail_rcpt) do
table.insert(display_emails, string.format('<%s>', e))
table.insert(mail_targets, mail_rcpt)
end
end
if rule.email_alert_sender then
local x = task:get_from('smtp')
if x and string.len(x[1].addr) > 0 then
table.insert(mail_targets, x)
table.insert(display_emails, string.format('<%s>', x[1].addr))
end
end
if rule.email_alert_user then
local x = task:get_user()
if x then
table.insert(mail_targets, x)
table.insert(display_emails, string.format('<%s>', x))
end
end
if rule.email_alert_recipients then
local x = task:get_recipients('smtp')
if x then
for _, e in ipairs(x) do
if string.len(e.addr) > 0 then
table.insert(mail_targets, e.addr)
table.insert(display_emails, string.format('<%s>', e.addr))
end
end
end
end
meta.mail_to = table.concat(display_emails, ', ')
meta.our_message_id = rspamd_util.random_hex(12) .. '@rspamd'
meta.date = rspamd_util.time_to_string(rspamd_util.get_time())
return lua_util.template(rule.email_template or settings.email_template, meta), { mail_targets = mail_targets}
end,
json = function(task)
return ucl.to_format(get_general_metadata(task), 'json-compact')
end
}
local function is_spam(action)
return (action == 'reject' or action == 'add header' or action == 'rewrite subject')
end
local selectors = {
default = function(task)
return true
end,
is_spam = function(task)
local action = task:get_metric_action('default')
return is_spam(action)
end,
is_spam_authed = function(task)
if not task:get_user() then
return false
end
local action = task:get_metric_action('default')
return is_spam(action)
end,
is_reject = function(task)
local action = task:get_metric_action('default')
return (action == 'reject')
end,
is_reject_authed = function(task)
if not task:get_user() then
return false
end
local action = task:get_metric_action('default')
return (action == 'reject')
end,
}
local function maybe_defer(task, rule)
if rule.defer then
rspamd_logger.warnx(task, 'deferring message')
task:set_pre_result('soft reject', 'deferred', N)
end
end
local pushers = {
redis_pubsub = function(task, formatted, rule)
local _,ret,upstream
local function redis_pub_cb(err)
if err then
rspamd_logger.errx(task, 'got error %s when publishing on server %s',
err, upstream:get_addr())
return maybe_defer(task, rule)
end
return true
end
ret,_,upstream = rspamd_redis_make_request(task,
redis_params, -- connect params
nil, -- hash key
true, -- is write
redis_pub_cb, --callback
'PUBLISH', -- command
{rule.channel, formatted} -- arguments
)
if not ret then
rspamd_logger.errx(task, 'error connecting to redis')
maybe_defer(task, rule)
end
end,
http = function(task, formatted, rule)
local function http_callback(err, code)
if err then
rspamd_logger.errx(task, 'got error %s in http callback', err)
return maybe_defer(task, rule)
end
if code ~= 200 then
rspamd_logger.errx(task, 'got unexpected http status: %s', code)
return maybe_defer(task, rule)
end
return true
end
local hdrs = {}
if rule.meta_headers then
local gm = get_general_metadata(task, false, true)
local pfx = rule.meta_header_prefix or 'X-Rspamd-'
for k, v in pairs(gm) do
if type(v) == 'table' then
hdrs[pfx .. k] = ucl.to_format(v, 'json-compact')
else
hdrs[pfx .. k] = v
end
end
end
rspamd_http.request({
task=task,
url=rule.url,
body=formatted,
callback=http_callback,
mime_type=rule.mime_type or settings.mime_type,
headers=hdrs,
})
end,
send_mail = function(task, formatted, rule, extra)
local lua_smtp = require "lua_smtp"
local function sendmail_cb(ret, err)
if not ret then
rspamd_logger.errx(task, 'SMTP export error: %s', err)
maybe_defer(task, rule)
end
end
lua_smtp.sendmail({
task = task,
host = rule.smtp,
port = rule.smtp_port or settings.smtp_port or 25,
from = rule.mail_from or settings.mail_from,
recipients = extra.mail_targets or rule.mail_to or settings.mail_to,
helo = rule.helo or settings.helo,
timeout = rule.timeout or settings.timeout,
}, formatted, sendmail_cb)
end,
}
local opts = rspamd_config:get_all_opt(N)
if not opts then return end
local process_settings = {
select = function(val)
selectors.custom = assert(load(val))()
end,
format = function(val)
formatters.custom = assert(load(val))()
end,
push = function(val)
pushers.custom = assert(load(val))()
end,
custom_push = function(val)
if type(val) == 'table' then
for k, v in pairs(val) do
pushers[k] = assert(load(v))()
end
end
end,
custom_select = function(val)
if type(val) == 'table' then
for k, v in pairs(val) do
selectors[k] = assert(load(v))()
end
end
end,
custom_format = function(val)
if type(val) == 'table' then
for k, v in pairs(val) do
formatters[k] = assert(load(v))()
end
end
end,
pusher_enabled = function(val)
if type(val) == 'string' then
if pushers[val] then
settings.pusher_enabled[val] = true
else
rspamd_logger.errx(rspamd_config, 'Pusher type: %s is invalid', val)
end
elseif type(val) == 'table' then
for _, v in ipairs(val) do
if pushers[v] then
settings.pusher_enabled[v] = true
else
rspamd_logger.errx(rspamd_config, 'Pusher type: %s is invalid', val)
end
end
end
end,
}
for k, v in pairs(opts) do
local f = process_settings[k]
if f then
f(opts[k])
else
settings[k] = v
end
end
if type(settings.rules) ~= 'table' then
-- Legacy config
settings.rules = {}
if not next(settings.pusher_enabled) then
if pushers.custom then
rspamd_logger.infox(rspamd_config, 'Custom pusher implicitly enabled')
settings.pusher_enabled.custom = true
else
-- Check legacy options
if settings.url then
rspamd_logger.warnx(rspamd_config, 'HTTP pusher implicitly enabled')
settings.pusher_enabled.http = true
end
if settings.channel then
rspamd_logger.warnx(rspamd_config, 'Redis Pubsub pusher implicitly enabled')
settings.pusher_enabled.redis_pubsub = true
end
if settings.smtp and settings.mail_to then
rspamd_logger.warnx(rspamd_config, 'SMTP pusher implicitly enabled')
settings.pusher_enabled.send_mail = true
end
end
end
if not next(settings.pusher_enabled) then
rspamd_logger.errx(rspamd_config, 'No push backend enabled')
return
end
if settings.formatter then
settings.format = formatters[settings.formatter]
if not settings.format then
rspamd_logger.errx(rspamd_config, 'No such formatter: %s', settings.formatter)
return
end
end
if settings.selector then
settings.select = selectors[settings.selector]
if not settings.select then
rspamd_logger.errx(rspamd_config, 'No such selector: %s', settings.selector)
return
end
end
for k in pairs(settings.pusher_enabled) do
local formatter = settings.pusher_format[k]
local selector = settings.pusher_select[k]
if not formatter then
settings.pusher_format[k] = settings.formatter or 'default'
rspamd_logger.infox(rspamd_config, 'Using default formatter for %s pusher', k)
else
if not formatters[formatter] then
rspamd_logger.errx(rspamd_config, 'No such formatter: %s - disabling %s', formatter, k)
settings.pusher_enabled.k = nil
end
end
if not selector then
settings.pusher_select[k] = settings.selector or 'default'
rspamd_logger.infox(rspamd_config, 'Using default selector for %s pusher', k)
else
if not selectors[selector] then
rspamd_logger.errx(rspamd_config, 'No such selector: %s - disabling %s', selector, k)
settings.pusher_enabled.k = nil
end
end
end
if settings.pusher_enabled.redis_pubsub then
redis_params = rspamd_parse_redis_server(N)
if not redis_params then
rspamd_logger.errx(rspamd_config, 'No redis servers are specified')
settings.pusher_enabled.redis_pubsub = nil
else
local r = {}
r.backend = 'redis_pubsub'
r.channel = settings.channel
r.defer = settings.defer
r.selector = settings.pusher_select.redis_pubsub
r.formatter = settings.pusher_format.redis_pubsub
settings.rules[r.backend:upper()] = r
end
end
if settings.pusher_enabled.http then
if not settings.url then
rspamd_logger.errx(rspamd_config, 'No URL is specified')
settings.pusher_enabled.http = nil
else
local r = {}
r.backend = 'http'
r.url = settings.url
r.mime_type = settings.mime_type
r.defer = settings.defer
r.selector = settings.pusher_select.http
r.formatter = settings.pusher_format.http
settings.rules[r.backend:upper()] = r
end
end
if settings.pusher_enabled.send_mail then
if not (settings.mail_to and settings.smtp) then
rspamd_logger.errx(rspamd_config, 'No mail_to and/or smtp setting is specified')
settings.pusher_enabled.send_mail = nil
else
local r = {}
r.backend = 'send_mail'
r.mail_to = settings.mail_to
r.mail_from = settings.mail_from
r.helo = settings.hello
r.smtp = settings.smtp
r.smtp_port = settings.smtp_port
r.email_template = settings.email_template
r.defer = settings.defer
r.selector = settings.pusher_select.send_mail
r.formatter = settings.pusher_format.send_mail
settings.rules[r.backend:upper()] = r
end
end
if not next(settings.pusher_enabled) then
rspamd_logger.errx(rspamd_config, 'No push backend enabled')
return
end
elseif not next(settings.rules) then
lua_util.debugm(N, rspamd_config, 'No rules enabled')
return
end
if not settings.rules or not next(settings.rules) then
rspamd_logger.errx(rspamd_config, 'No rules enabled')
return
end
local backend_required_elements = {
http = {
'url',
},
smtp = {
'mail_to',
'smtp',
},
redis_pubsub = {
'channel',
},
}
local check_element = {
selector = function(k, v)
if not selectors[v] then
rspamd_logger.errx(rspamd_config, 'Rule %s has invalid selector %s', k, v)
return false
else
return true
end
end,
formatter = function(k, v)
if not formatters[v] then
rspamd_logger.errx(rspamd_config, 'Rule %s has invalid formatter %s', k, v)
return false
else
return true
end
end,
}
local backend_check = {
default = function(k, rule)
local reqset = backend_required_elements[rule.backend]
if reqset then
for _, e in ipairs(reqset) do
if not rule[e] then
rspamd_logger.errx(rspamd_config, 'Rule %s misses required setting %s', k, e)
settings.rules[k] = nil
end
end
end
for sett, v in pairs(rule) do
local f = check_element[sett]
if f then
if not f(sett, v) then
settings.rules[k] = nil
end
end
end
end,
}
backend_check.redis_pubsub = function(k, rule)
if not redis_params then
redis_params = rspamd_parse_redis_server(N)
end
if not redis_params then
rspamd_logger.errx(rspamd_config, 'No redis servers are specified')
settings.rules[k] = nil
else
backend_check.default(k, rule)
end
end
setmetatable(backend_check, {
__index = function()
return backend_check.default
end,
})
for k, v in pairs(settings.rules) do
if type(v) == 'table' then
local backend = v.backend
if not backend then
rspamd_logger.errx(rspamd_config, 'Rule %s has no backend', k)
settings.rules[k] = nil
elseif not pushers[backend] then
rspamd_logger.errx(rspamd_config, 'Rule %s has invalid backend %s', k, backend)
settings.rules[k] = nil
else
local f = backend_check[backend]
f(k, v)
end
else
rspamd_logger.errx(rspamd_config, 'Rule %s has bad type: %s', k, type(v))
settings.rules[k] = nil
end
end
local function gen_exporter(rule)
return function (task)
if task:has_flag('skip') then return end
local selector = rule.selector or 'default'
local selected = selectors[selector](task)
if selected then
lua_util.debugm(N, task, 'Message selected for processing')
local formatter = rule.formatter or 'default'
local formatted, extra = formatters[formatter](task, rule)
if formatted then
pushers[rule.backend](task, formatted, rule, extra)
else
lua_util.debugm(N, task, 'Formatter [%s] returned non-truthy value [%s]', formatter, formatted)
end
else
lua_util.debugm(N, task, 'Selector [%s] returned non-truthy value [%s]', selector, selected)
end
end
end
if not next(settings.rules) then
rspamd_logger.errx(rspamd_config, 'No rules enabled')
lua_util.disable_module(N, "config")
end
for k, r in pairs(settings.rules) do
rspamd_config:register_symbol({
name = 'EXPORT_METADATA_' .. k,
type = 'idempotent',
callback = gen_exporter(r),
priority = 10,
flags = 'empty,explicit_disable,ignore_passthrough',
})
end

View File

@@ -2,9 +2,10 @@ FROM debian:bullseye-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG SOGO_DEBIAN_REPOSITORY=http://packages.sogo.nu/nightly/5/debian/
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG GOSU_VERSION=1.16
ARG DEBIAN_VERSION=bullseye
ARG SOGO_DEBIAN_REPOSITORY=http://www.axis.cz/linux/debian
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.17
ENV LC_ALL C
# 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

@@ -150,6 +150,8 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
<string>YES</string>
<key>SOGoEncryptionKey</key>
<string>${RAND_PASS}</string>
<key>OCSAdminURL</key>
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
<key>OCSCacheFolderURL</key>
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_cache_folder</string>
<key>OCSEMailAlarmsFolderURL</key>

View File

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

View File

@@ -1,7 +1,15 @@
#!/bin/bash
if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
if [[ "${FLATCURVE_EXPERIMENTAL}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "FLATCURVE_EXPERIMENTAL=y, skipping Solr but enabling Flatcurve as FTS for Dovecot!"
echo "Solr will be removed in the future!"
sleep 365d
exit 0
elif [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "SKIP_SOLR=y, skipping Solr..."
echo "HINT: You could try the newer FTS Backend Flatcurve, which is currently in experimental state..."
echo "Simply set FLATCURVE_EXPERIMENTAL=y inside your mailcow.conf and restart the stack afterwards!"
echo "Solr will be removed in the future!"
sleep 365d
exit 0
fi
@@ -57,5 +65,11 @@ if [[ "${1}" == "--bootstrap" ]]; then
exit 0
fi
echo "Starting up Solr..."
echo -e "\e[31mSolr is deprecated! You can try the new FTS System now by enabling FLATCURVE_EXPERIMENTAL=y inside mailcow.conf and restarting the stack\e[0m"
echo -e "\e[31mSolr will be removed completely soon!\e[0m"
sleep 15
exec gosu solr solr-foreground

View File

@@ -1,9 +1,10 @@
FROM alpine:3.17
FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
RUN apk add --update --no-cache \
curl \
bind-tools \
unbound \
bash \
openssl \
@@ -18,10 +19,10 @@ EXPOSE 53/udp 53/tcp
COPY docker-entrypoint.sh /docker-entrypoint.sh
# healthcheck (nslookup)
# healthcheck (dig, ping)
COPY healthcheck.sh /healthcheck.sh
RUN chmod +x /healthcheck.sh
HEALTHCHECK --interval=30s --timeout=10s CMD [ "/healthcheck.sh" ]
HEALTHCHECK --interval=30s --timeout=30s CMD [ "/healthcheck.sh" ]
ENTRYPOINT ["/docker-entrypoint.sh"]

View File

@@ -1,12 +1,76 @@
#!/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!"
# Reset logfile
echo "$(date +"%Y-%m-%d %H:%M:%S"): Starting health check - logs can be found in /var/log/healthcheck.log"
echo "$(date +"%Y-%m-%d %H:%M:%S"): Starting health check" > /var/log/healthcheck.log
# Declare log function for logfile inside container
function log_to_file() {
echo "$(date +"%Y-%m-%d %H:%M:%S"): $1" >> /var/log/healthcheck.log
}
# 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
}
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
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.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
# Installation
RUN apk add --update \

View File

@@ -19,9 +19,11 @@ fi
if [[ "${WATCHDOG_VERBOSE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
SMTP_VERBOSE="--verbose"
CURL_VERBOSE="--verbose"
set -xv
else
SMTP_VERBOSE=""
CURL_VERBOSE=""
exec 2>/dev/null
fi
@@ -97,7 +99,9 @@ log_msg() {
echo $(date) $(printf '%s\n' "${1}")
}
function mail_error() {
function notify_error() {
# Check if one of the notification options is enabled
[[ -z ${WATCHDOG_NOTIFY_EMAIL} ]] && [[ -z ${WATCHDOG_NOTIFY_WEBHOOK} ]] && return 0
THROTTLE=
[[ -z ${1} ]] && return 1
# If exists, body will be the content of "/tmp/${1}", even if ${2} is set
@@ -122,37 +126,57 @@ function mail_error() {
else
SUBJECT="${WATCHDOG_SUBJECT}: ${1}"
fi
IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}"
for rcpt in "${MAIL_RCPTS[@]}"; do
RCPT_DOMAIN=
RCPT_MX=
RCPT_DOMAIN=$(echo ${rcpt} | awk -F @ {'print $NF'})
CHECK_FOR_VALID_MX=$(dig +short ${RCPT_DOMAIN} mx)
if [[ -z ${CHECK_FOR_VALID_MX} ]]; then
log_msg "Cannot determine MX for ${rcpt}, skipping email notification..."
# Send mail notification if enabled
if [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]]; then
IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}"
for rcpt in "${MAIL_RCPTS[@]}"; do
RCPT_DOMAIN=
RCPT_MX=
RCPT_DOMAIN=$(echo ${rcpt} | awk -F @ {'print $NF'})
CHECK_FOR_VALID_MX=$(dig +short ${RCPT_DOMAIN} mx)
if [[ -z ${CHECK_FOR_VALID_MX} ]]; then
log_msg "Cannot determine MX for ${rcpt}, skipping email notification..."
return 1
fi
[ -f "/tmp/${1}" ] && BODY="/tmp/${1}"
timeout 10s ./smtp-cli --missing-modules-ok \
"${SMTP_VERBOSE}" \
--charset=UTF-8 \
--subject="${SUBJECT}" \
--body-plain="${BODY}" \
--add-header="X-Priority: 1" \
--to=${rcpt} \
--from="watchdog@${MAILCOW_HOSTNAME}" \
--hello-host=${MAILCOW_HOSTNAME} \
--ipv4
if [[ $? -eq 1 ]]; then # exit code 1 is fine
log_msg "Sent notification email to ${rcpt}"
else
if [[ "${SMTP_VERBOSE}" == "" ]]; then
log_msg "Error while sending notification email to ${rcpt}. You can enable verbose logging by setting 'WATCHDOG_VERBOSE=y' in mailcow.conf."
else
log_msg "Error while sending notification email to ${rcpt}."
fi
fi
done
fi
# Send webhook notification if enabled
if [[ ! -z ${WATCHDOG_NOTIFY_WEBHOOK} ]]; then
if [[ -z ${WATCHDOG_NOTIFY_WEBHOOK_BODY} ]]; then
log_msg "No webhook body set, skipping webhook notification..."
return 1
fi
[ -f "/tmp/${1}" ] && BODY="/tmp/${1}"
timeout 10s ./smtp-cli --missing-modules-ok \
"${SMTP_VERBOSE}" \
--charset=UTF-8 \
--subject="${SUBJECT}" \
--body-plain="${BODY}" \
--add-header="X-Priority: 1" \
--to=${rcpt} \
--from="watchdog@${MAILCOW_HOSTNAME}" \
--hello-host=${MAILCOW_HOSTNAME} \
--ipv4
if [[ $? -eq 1 ]]; then # exit code 1 is fine
log_msg "Sent notification email to ${rcpt}"
else
if [[ "${SMTP_VERBOSE}" == "" ]]; then
log_msg "Error while sending notification email to ${rcpt}. You can enable verbose logging by setting 'WATCHDOG_VERBOSE=y' in mailcow.conf."
else
log_msg "Error while sending notification email to ${rcpt}."
fi
fi
done
# Replace subject and body placeholders
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}
log_msg "Sent notification using webhook"
fi
}
get_container_ip() {
@@ -197,7 +221,7 @@ get_container_ip() {
# One-time check
if grep -qi "$(echo ${IPV6_NETWORK} | cut -d: -f1-3)" <<< "$(ip a s)"; then
if [[ -z "$(get_ipv6)" ]]; then
mail_error "ipv6-config" "enable_ipv6 is true in docker-compose.yml, but an IPv6 link could not be established. Please verify your IPv6 connection."
notify_error "ipv6-config" "enable_ipv6 is true in docker-compose.yml, but an IPv6 link could not be established. Please verify your IPv6 connection."
fi
fi
@@ -692,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
@@ -746,8 +770,8 @@ olefy_checks() {
}
# Notify about start
if [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]]; then
mail_error "watchdog-mailcow" "Watchdog started monitoring mailcow."
if [[ ${WATCHDOG_NOTIFY_START} =~ ^([yY][eE][sS]|[yY])+$ ]]; then
notify_error "watchdog-mailcow" "Watchdog started monitoring mailcow."
fi
# Create watchdog agents
@@ -1029,33 +1053,33 @@ while true; do
fi
if [[ ${com_pipe_answer} == "ratelimit" ]]; then
log_msg "At least one ratelimit was applied"
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}"
notify_error "${com_pipe_answer}"
elif [[ ${com_pipe_answer} == "mail_queue_status" ]]; then
log_msg "Mail queue status is critical"
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}"
notify_error "${com_pipe_answer}"
elif [[ ${com_pipe_answer} == "external_checks" ]]; then
log_msg "Your mailcow is an open relay!"
# Define $2 to override message text, else print service was restarted at ...
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please stop mailcow now and check your network configuration!"
notify_error "${com_pipe_answer}" "Please stop mailcow now and check your network configuration!"
elif [[ ${com_pipe_answer} == "mysql_repl_checks" ]]; then
log_msg "MySQL replication is not working properly"
# Define $2 to override message text, else print service was restarted at ...
# Once mail per 10 minutes
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check the SQL replication status" 600
notify_error "${com_pipe_answer}" "Please check the SQL replication status" 600
elif [[ ${com_pipe_answer} == "dovecot_repl_checks" ]]; then
log_msg "Dovecot replication is not working properly"
# Define $2 to override message text, else print service was restarted at ...
# Once mail per 10 minutes
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check the Dovecot replicator status" 600
notify_error "${com_pipe_answer}" "Please check the Dovecot replicator status" 600
elif [[ ${com_pipe_answer} == "certcheck" ]]; then
log_msg "Certificates are about to expire"
# Define $2 to override message text, else print service was restarted at ...
# Only mail once a day
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please renew your certificate" 86400
notify_error "${com_pipe_answer}" "Please renew your certificate" 86400
elif [[ ${com_pipe_answer} == "acme-mailcow" ]]; then
log_msg "acme-mailcow did not complete successfully"
# Define $2 to override message text, else print service was restarted at ...
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check acme-mailcow for further information."
notify_error "${com_pipe_answer}" "Please check acme-mailcow for further information."
elif [[ ${com_pipe_answer} == "fail2ban" ]]; then
F2B_RES=($(timeout 4s ${REDIS_CMDLINE} --raw GET F2B_RES 2> /dev/null))
if [[ ! -z "${F2B_RES}" ]]; then
@@ -1065,7 +1089,7 @@ while true; do
log_msg "Banned ${host}"
rm /tmp/fail2ban 2> /dev/null
timeout 2s whois "${host}" > /tmp/fail2ban
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && [[ ${WATCHDOG_NOTIFY_BAN} =~ ^([yY][eE][sS]|[yY])+$ ]] && mail_error "${com_pipe_answer}" "IP ban: ${host}"
[[ ${WATCHDOG_NOTIFY_BAN} =~ ^([yY][eE][sS]|[yY])+$ ]] && notify_error "${com_pipe_answer}" "IP ban: ${host}"
done
fi
elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
@@ -1085,7 +1109,7 @@ while true; do
else
log_msg "Sending restart command to ${CONTAINER_ID}..."
curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/restart
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}"
notify_error "${com_pipe_answer}"
log_msg "Wait for restarted container to settle and continue watching..."
sleep 35
fi
@@ -1095,3 +1119,4 @@ while true; do
kill -USR1 ${BACKGROUND_TASKS[*]}
fi
done

View File

@@ -86,7 +86,7 @@ server {
deny all;
}
location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+)\.php(?:$|\/) {
location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+)\.php(?:$|\/) {
fastcgi_split_path_info ^(.+?\.php)(\/.*|)$;
set $path_info $fastcgi_path_info;
try_files $fastcgi_script_name =404;
@@ -105,7 +105,7 @@ server {
fastcgi_read_timeout 1200;
}
location ~ ^\/(?:updater|oc[ms]-provider)(?:$|\/) {
location ~ ^\/(?:updater|ocs-provider)(?:$|\/) {
try_files $uri/ =404;
index index.php;
}

View File

@@ -10,6 +10,7 @@
auth_mechanisms = plain login
#mail_debug = yes
#auth_debug = yes
#log_debug = category=fts-flatcurve # Activate Logging for Flatcurve FTS Searchings
log_path = syslog
disable_plaintext_auth = yes
# Uncomment on NFS share
@@ -194,9 +195,6 @@ plugin {
acl_shared_dict = file:/var/vmail/shared-mailboxes.db
acl = vfile
acl_user = %u
fts = solr
fts_autoindex = yes
fts_solr = url=http://solr:8983/solr/dovecot-fts/
quota = dict:Userquota::proxy::sqlquota
quota_rule2 = Trash:storage=+100%%
sieve = /var/vmail/sieve/%u.sieve
@@ -247,6 +245,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
@@ -302,6 +303,7 @@ replication_dsync_parameters = -d -l 30 -U -n INBOX
!include_try /etc/dovecot/extra.conf
!include_try /etc/dovecot/sogo-sso.conf
!include_try /etc/dovecot/shared_namespace.conf
!include_try /etc/dovecot/conf.d/fts.conf
# </Includes>
default_client_limit = 10400
default_vsz_limit = 1024 M

View File

@@ -1,6 +1,6 @@
if /^\s*Received:.*Authenticated sender.*\(Postcow\)/
#/^Received: from .*? \([\w-.]* \[.*?\]\)\s+\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (E?SMTPS?A?) id ([A-F0-9]+).+;.*?/
/^Received: from .*? \([\w-.]* \[.*?\]\)(.*|\n.*)\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (.*)/
/^Received: from .*? \([\w\-.]* \[.*?\]\)(.*|\n.*)\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (.*)/
REPLACE Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with $3
endif
if /^\s*Received: from.* \(.*dovecot-mailcow.*mailcow-network.*\).*\(Postcow\)/
@@ -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
@@ -112,14 +114,14 @@ smtpd_tls_loglevel = 1
# Mandatory protocols and ciphers are used when a connections is enforced to use TLS
# Does _not_ apply to enforced incoming TLS settings per mailbox
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
lmtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_mandatory_protocols = >=TLSv1.2
lmtp_tls_mandatory_protocols = >=TLSv1.2
smtpd_tls_mandatory_protocols = >=TLSv1.2
smtpd_tls_mandatory_ciphers = high
smtp_tls_protocols = !SSLv2, !SSLv3
lmtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_protocols = !SSLv2, !SSLv3
smtp_tls_protocols = >=TLSv1.2
lmtp_tls_protocols = >=TLSv1.2
smtpd_tls_protocols = >=TLSv1.2
smtpd_tls_security_level = may
tls_preempt_cipherlist = yes
@@ -160,12 +162,13 @@ 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
compatibility_level = 2
postscreen_discard_ehlo_keywords = silent-discard, dsn, chunking
smtpd_discard_ehlo_keywords = chunking, silent-discard
compatibility_level = 3.7
smtputf8_enable = no
# Define protocols for SMTPS and submission service
submission_smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtps_smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
submission_smtpd_tls_mandatory_protocols = >=TLSv1.2
smtps_smtpd_tls_mandatory_protocols = >=TLSv1.2
parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients
# DO NOT EDIT ANYTHING BELOW #

View File

@@ -1,18 +1,19 @@
# Whitelist generated by Postwhite v3.4 on Sun Oct 1 00:14:59 UTC 2023
# Whitelist generated by Postwhite v3.4 on Sat Jun 1 00:15:02 UTC 2024
# https://github.com/stevejenkins/postwhite/
# 2019 total rules
# 2000 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
2a02:a60:0:5::/64 permit
2c0f:fb50:4000::/36 permit
2.207.151.53 permit
3.14.230.16 permit
3.70.123.177 permit
3.93.157.0/24 permit
3.94.40.108 permit
3.129.120.190 permit
3.210.190.0/24 permit
8.20.114.31 permit
@@ -20,6 +21,7 @@
8.25.196.0/23 permit
8.39.54.0/23 permit
8.40.222.0/23 permit
10.162.0.0/16 permit
12.130.86.238 permit
13.70.32.43 permit
13.72.50.45 permit
@@ -31,21 +33,25 @@
13.110.216.0/22 permit
13.110.224.0/20 permit
13.111.0.0/16 permit
13.111.191.0/24 permit
15.200.21.50 permit
15.200.44.248 permit
15.200.201.185 permit
17.41.0.0/16 permit
17.57.155.0/24 permit
17.57.156.0/24 permit
17.58.0.0/16 permit
17.142.0.0/15 permit
18.156.89.250 permit
18.157.243.190 permit
18.194.95.56 permit
18.198.96.88 permit
18.208.124.128/25 permit
18.216.232.154 permit
18.234.1.244 permit
18.236.40.242 permit
18.236.56.161 permit
20.51.6.32/30 permit
20.51.98.61 permit
20.52.52.2 permit
20.52.128.133 permit
20.59.80.4/30 permit
@@ -63,10 +69,9 @@
20.107.239.64/30 permit
20.112.250.133 permit
20.118.139.208/30 permit
20.185.213.160/27 permit
20.185.213.224/27 permit
20.141.10.196 permit
20.185.213.0/24 permit
20.185.214.0/27 permit
20.185.214.2 permit
20.185.214.32/27 permit
20.185.214.64/27 permit
20.231.239.246 permit
@@ -90,23 +95,22 @@
27.123.204.172 permit
27.123.204.188/30 permit
27.123.204.192 permit
27.123.206.0/24 permit
27.123.206.50/31 permit
27.123.206.56/29 permit
27.123.206.76/30 permit
27.123.206.80/28 permit
31.25.48.222 permit
34.195.217.107 permit
34.202.239.6 permit
34.212.163.75 permit
34.215.104.144 permit
34.218.116.3 permit
34.225.212.172 permit
34.247.168.44 permit
35.161.32.253 permit
35.167.93.243 permit
35.176.132.251 permit
35.190.247.0/24 permit
35.191.0.0/16 permit
35.242.169.159 permit
37.218.248.47 permit
37.218.249.47 permit
37.218.251.62 permit
@@ -116,13 +120,11 @@
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
44.236.56.93 permit
44.238.220.251 permit
46.19.168.0/23 permit
46.19.170.16 permit
46.226.48.0/21 permit
46.228.36.37 permit
46.228.36.38/31 permit
@@ -162,7 +164,6 @@
46.228.38.144/29 permit
46.228.38.152/31 permit
46.228.38.154 permit
46.228.39.0/24 permit
46.228.39.64/27 permit
46.228.39.96/30 permit
46.228.39.100/30 permit
@@ -183,24 +184,23 @@
50.18.125.237 permit
50.18.126.162 permit
50.31.32.0/19 permit
50.31.156.96/27 permit
50.31.205.0/24 permit
50.56.130.220/30 permit
51.137.58.21 permit
51.140.75.55 permit
51.144.100.179 permit
52.1.14.157 permit
52.5.230.59 permit
52.27.5.72 permit
52.27.28.47 permit
52.28.63.81 permit
52.36.138.31 permit
52.37.142.146 permit
52.50.24.208 permit
52.58.216.183 permit
52.59.143.3 permit
52.60.41.5 permit
52.60.115.116 permit
52.61.91.9 permit
52.71.0.205 permit
52.82.172.0/22 permit
52.94.124.0/28 permit
52.95.48.152/29 permit
52.95.49.88/29 permit
@@ -211,11 +211,11 @@
52.96.222.194 permit
52.96.222.226 permit
52.96.223.2 permit
52.96.228.130 permit
52.96.229.242 permit
52.100.0.0/14 permit
52.103.0.0/17 permit
52.119.213.144/28 permit
52.160.39.140 permit
52.165.175.144 permit
52.185.106.240/28 permit
52.200.59.0/24 permit
@@ -227,37 +227,33 @@
52.222.75.85 permit
52.222.89.228 permit
52.234.172.96/28 permit
52.235.253.128 permit
52.236.28.240/28 permit
52.244.206.214 permit
52.247.53.144 permit
52.250.107.196 permit
52.250.126.174 permit
54.90.148.255 permit
54.165.19.38 permit
54.172.97.247 permit
54.174.52.0/24 permit
54.174.53.128/30 permit
54.174.57.0/24 permit
54.174.59.0/24 permit
54.174.60.0/23 permit
54.174.63.0/24 permit
54.186.193.102 permit
54.191.223.56 permit
54.194.61.95 permit
54.195.113.45 permit
54.213.20.246 permit
54.214.39.184 permit
54.216.77.168 permit
54.221.227.204 permit
54.240.0.0/18 permit
54.240.64.0/19 permit
54.240.96.0/19 permit
54.241.16.209 permit
54.244.54.130 permit
54.244.242.0/24 permit
54.246.232.180 permit
54.255.61.23 permit
62.13.128.0/24 permit
62.13.128.150 permit
62.13.128.196 permit
62.13.129.128/25 permit
62.13.136.0/22 permit
62.13.140.0/22 permit
@@ -265,14 +261,13 @@
62.13.148.0/23 permit
62.13.150.0/23 permit
62.13.152.0/23 permit
62.13.159.196 permit
62.17.146.128/26 permit
62.179.121.0/24 permit
62.201.172.0/27 permit
62.201.172.32/27 permit
62.253.227.114 permit
63.32.13.159 permit
63.80.14.0/23 permit
63.111.28.137 permit
63.128.21.0/24 permit
63.143.57.128/25 permit
63.143.59.128/25 permit
@@ -285,40 +280,34 @@
64.79.155.193 permit
64.79.155.205 permit
64.79.155.206 permit
64.89.44.85 permit
64.89.45.80 permit
64.89.45.194 permit
64.89.45.196 permit
64.127.115.252 permit
64.132.88.0/23 permit
64.132.92.0/24 permit
64.147.123.17 permit
64.147.123.18 permit
64.147.123.19 permit
64.147.123.20 permit
64.147.123.21 permit
64.147.123.24 permit
64.147.123.25 permit
64.147.123.26 permit
64.147.123.27 permit
64.147.123.28 permit
64.147.123.29 permit
64.147.123.128/27 permit
64.207.219.7 permit
64.207.219.8 permit
64.207.219.9 permit
64.207.219.10 permit
64.207.219.11 permit
64.207.219.12 permit
64.207.219.13 permit
64.207.219.14 permit
64.207.219.15 permit
64.207.219.71 permit
64.207.219.72 permit
64.207.219.73 permit
64.207.219.74 permit
64.207.219.75 permit
64.207.219.76 permit
64.207.219.77 permit
64.207.219.78 permit
64.207.219.79 permit
64.207.219.135 permit
64.207.219.136 permit
64.207.219.137 permit
64.207.219.138 permit
64.207.219.139 permit
64.207.219.140 permit
64.207.219.141 permit
64.207.219.142 permit
64.207.219.143 permit
@@ -351,23 +340,8 @@
65.154.166.0/24 permit
65.212.180.36 permit
66.102.0.0/20 permit
66.111.4.25 permit
66.111.4.26 permit
66.111.4.27 permit
66.111.4.28 permit
66.111.4.29 permit
66.111.4.221 permit
66.111.4.222 permit
66.111.4.224 permit
66.111.4.225 permit
66.111.4.229 permit
66.111.4.230 permit
66.119.150.192/26 permit
66.135.202.0/27 permit
66.135.215.0/24 permit
66.135.222.1 permit
66.162.193.226/31 permit
66.163.184.0/21 permit
66.163.184.0/24 permit
66.163.185.0/24 permit
66.163.186.0/24 permit
@@ -380,7 +354,6 @@
66.196.80.112/28 permit
66.196.80.144/29 permit
66.196.80.193 permit
66.196.81.0/24 permit
66.196.81.104/29 permit
66.196.81.112/29 permit
66.196.81.120 permit
@@ -395,8 +368,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.86/31 permit
66.211.170.88/29 permit
66.211.184.0/23 permit
66.218.74.64/30 permit
@@ -425,12 +396,10 @@
66.249.80.0/20 permit
67.23.31.6 permit
67.72.99.26 permit
67.195.22.0/24 permit
67.195.22.113 permit
67.195.22.116/30 permit
67.195.23.144/30 permit
67.195.23.148 permit
67.195.60.0/24 permit
67.195.60.45 permit
67.195.60.46/31 permit
67.195.60.48/31 permit
@@ -438,7 +407,6 @@
67.195.60.146 permit
67.195.60.155 permit
67.195.60.156 permit
67.195.87.0/24 permit
67.195.87.64 permit
67.195.87.81 permit
67.195.87.82/31 permit
@@ -463,7 +431,6 @@
67.228.37.4/30 permit
67.231.145.42 permit
67.231.153.30 permit
68.142.230.0/24 permit
68.142.230.64/31 permit
68.142.230.69 permit
68.142.230.70/31 permit
@@ -491,7 +458,6 @@
70.37.151.128/25 permit
70.42.149.0/24 permit
70.42.149.35 permit
72.3.237.64/28 permit
72.14.192.0/18 permit
72.21.192.0/19 permit
72.21.217.142 permit
@@ -522,7 +488,6 @@
72.30.237.180/30 permit
72.30.237.184/31 permit
72.30.237.204/30 permit
72.30.238.0/23 permit
72.30.238.116/30 permit
72.30.238.120/31 permit
72.30.238.128 permit
@@ -553,12 +518,7 @@
72.30.239.228/31 permit
72.30.239.244/30 permit
72.30.239.248/31 permit
72.34.168.76 permit
72.34.168.80 permit
72.34.168.85 permit
72.34.168.86 permit
72.52.72.32/28 permit
74.6.128.0/21 permit
74.6.128.0/24 permit
74.6.129.0/24 permit
74.6.130.0/24 permit
@@ -585,13 +545,14 @@
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
77.238.176.0/24 permit
77.238.177.0/24 permit
77.238.178.0/24 permit
@@ -614,17 +575,25 @@
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
85.158.136.0/21 permit
86.61.88.25 permit
87.198.219.130 permit
87.198.219.153 permit
87.238.80.0/21 permit
87.248.103.12 permit
87.248.103.21 permit
@@ -664,6 +633,7 @@
87.253.232.0/21 permit
89.22.108.0/24 permit
91.211.240.0/22 permit
94.169.2.0/23 permit
94.245.112.0/27 permit
94.245.112.10/31 permit
95.131.104.0/21 permit
@@ -677,7 +647,6 @@
98.136.44.181 permit
98.136.44.182/31 permit
98.136.44.184 permit
98.136.164.0/24 permit
98.136.164.36/31 permit
98.136.164.64/29 permit
98.136.164.72/30 permit
@@ -685,7 +654,6 @@
98.136.164.78 permit
98.136.172.32/30 permit
98.136.172.36/31 permit
98.136.185.0/24 permit
98.136.185.29 permit
98.136.185.42/31 permit
98.136.185.46 permit
@@ -720,7 +688,6 @@
98.136.215.184 permit
98.136.215.208/30 permit
98.136.215.212/31 permit
98.136.217.0/24 permit
98.136.217.1 permit
98.136.217.2 permit
98.136.217.3 permit
@@ -730,14 +697,12 @@
98.136.217.12/30 permit
98.136.217.16/30 permit
98.136.217.20/30 permit
98.136.218.0/24 permit
98.136.218.39 permit
98.136.218.40/29 permit
98.136.218.48/28 permit
98.136.218.67 permit
98.136.218.68/30 permit
98.136.218.72/30 permit
98.137.12.0/24 permit
98.137.12.48/30 permit
98.137.12.52/31 permit
98.137.12.54 permit
@@ -775,7 +740,6 @@
98.137.13.132 permit
98.137.13.137 permit
98.137.13.138 permit
98.137.64.0/21 permit
98.137.64.0/24 permit
98.137.65.0/24 permit
98.137.66.0/24 permit
@@ -799,7 +763,6 @@
98.138.83.176/31 permit
98.138.83.179 permit
98.138.83.180/31 permit
98.138.84.0/22 permit
98.138.84.37 permit
98.138.84.38/31 permit
98.138.84.40/29 permit
@@ -840,7 +803,6 @@
98.138.87.148/31 permit
98.138.87.192/30 permit
98.138.87.196/31 permit
98.138.88.0/22 permit
98.138.88.105 permit
98.138.88.106 permit
98.138.88.128/30 permit
@@ -880,7 +842,6 @@
98.138.91.2/31 permit
98.138.91.4/31 permit
98.138.91.6 permit
98.138.100.0/23 permit
98.138.100.220/30 permit
98.138.100.224/30 permit
98.138.100.228/31 permit
@@ -890,7 +851,6 @@
98.138.104.100 permit
98.138.104.112/30 permit
98.138.104.116 permit
98.138.120.0/24 permit
98.138.120.36/30 permit
98.138.120.48/28 permit
98.138.197.46/31 permit
@@ -983,12 +943,10 @@
98.138.213.238/31 permit
98.138.213.240/31 permit
98.138.213.242 permit
98.138.215.0/24 permit
98.138.215.12/30 permit
98.138.215.16/28 permit
98.138.217.216/30 permit
98.138.217.220/31 permit
98.138.226.0/24 permit
98.138.226.30/31 permit
98.138.226.56/29 permit
98.138.226.64/30 permit
@@ -1014,21 +972,18 @@
98.138.227.108/31 permit
98.138.227.128/30 permit
98.138.227.132/31 permit
98.138.229.0/24 permit
98.138.229.24/29 permit
98.138.229.32/31 permit
98.138.229.122/31 permit
98.138.229.138/31 permit
98.138.229.154/31 permit
98.138.229.170/31 permit
98.139.164.0/24 permit
98.139.164.96/30 permit
98.139.164.100/30 permit
98.139.164.104/29 permit
98.139.164.112/30 permit
98.139.172.112/30 permit
98.139.172.116/31 permit
98.139.175.0/24 permit
98.139.175.65 permit
98.139.175.66/31 permit
98.139.175.68/30 permit
@@ -1053,10 +1008,8 @@
98.139.210.196/31 permit
98.139.210.202/31 permit
98.139.210.204/30 permit
98.139.211.0/24 permit
98.139.211.160/30 permit
98.139.211.192/28 permit
98.139.212.0/23 permit
98.139.212.160/28 permit
98.139.212.176/29 permit
98.139.212.184/30 permit
@@ -1071,7 +1024,6 @@
98.139.214.155 permit
98.139.214.156/30 permit
98.139.214.221 permit
98.139.215.0/24 permit
98.139.215.228/31 permit
98.139.215.230 permit
98.139.215.248/30 permit
@@ -1130,14 +1082,12 @@
98.139.220.243 permit
98.139.220.245 permit
98.139.220.253 permit
98.139.221.0/24 permit
98.139.221.43 permit
98.139.221.60/30 permit
98.139.221.156/30 permit
98.139.221.232/30 permit
98.139.221.236/31 permit
98.139.221.250 permit
98.139.244.0/24 permit
98.139.244.47 permit
98.139.244.49 permit
98.139.244.50/31 permit
@@ -1177,6 +1127,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
@@ -1192,7 +1143,6 @@
104.130.96.0/28 permit
104.130.122.0/23 permit
104.214.25.77 permit
104.245.209.192/26 permit
106.10.144.64/27 permit
106.10.144.100/31 permit
106.10.144.103 permit
@@ -1218,7 +1168,6 @@
106.10.146.52/31 permit
106.10.146.224/30 permit
106.10.146.228/31 permit
106.10.148.0/24 permit
106.10.148.48/30 permit
106.10.148.52/31 permit
106.10.148.68/30 permit
@@ -1231,7 +1180,6 @@
106.10.149.30 permit
106.10.149.160/30 permit
106.10.149.164/31 permit
106.10.150.0/23 permit
106.10.150.23 permit
106.10.150.24/30 permit
106.10.150.28/31 permit
@@ -1270,7 +1218,6 @@
106.10.151.250/31 permit
106.10.151.252/31 permit
106.10.151.254 permit
106.10.167.0/24 permit
106.10.167.72 permit
106.10.167.128/27 permit
106.10.167.160/28 permit
@@ -1292,7 +1239,6 @@
106.10.174.120/30 permit
106.10.174.154/31 permit
106.10.174.156/30 permit
106.10.176.0/24 permit
106.10.176.32/29 permit
106.10.176.48 permit
106.10.176.112 permit
@@ -1311,7 +1257,6 @@
106.10.196.43 permit
106.10.196.44/30 permit
106.10.196.48 permit
106.10.240.0/22 permit
106.10.240.0/24 permit
106.10.241.0/24 permit
106.10.242.0/24 permit
@@ -1319,6 +1264,7 @@
106.10.244.0/24 permit
106.39.212.64/29 permit
106.50.16.0/28 permit
107.20.210.250 permit
108.174.0.0/24 permit
108.174.0.215 permit
108.174.3.0/24 permit
@@ -1329,6 +1275,7 @@
108.175.30.45 permit
108.177.8.0/21 permit
108.177.96.0/19 permit
108.179.144.0/20 permit
109.237.142.0/24 permit
111.221.23.128/25 permit
111.221.26.0/27 permit
@@ -1337,7 +1284,6 @@
111.221.112.0/21 permit
112.19.199.64/29 permit
112.19.242.64/29 permit
116.214.12.0/24 permit
116.214.12.47 permit
116.214.12.48/31 permit
116.214.12.56/31 permit
@@ -1356,7 +1302,6 @@
121.244.91.48 permit
122.15.156.182 permit
123.126.78.64/29 permit
124.108.96.0/24 permit
124.108.96.24/31 permit
124.108.96.28/31 permit
124.108.96.70/31 permit
@@ -1369,6 +1314,8 @@
128.245.0.0/20 permit
128.245.64.0/20 permit
128.245.176.0/20 permit
128.245.240.0/24 permit
128.245.241.0/24 permit
128.245.242.0/24 permit
128.245.242.16 permit
128.245.242.17 permit
@@ -1378,6 +1325,7 @@
128.245.245.0/24 permit
128.245.246.0/24 permit
128.245.247.0/24 permit
128.245.248.0/21 permit
129.41.77.70 permit
129.41.169.249 permit
129.80.5.164 permit
@@ -1415,9 +1363,11 @@
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
136.143.190.0/23 permit
136.147.128.0/20 permit
136.147.135.0/24 permit
136.147.176.0/20 permit
@@ -1435,6 +1385,12 @@
139.180.17.0/24 permit
141.148.159.229 permit
141.193.32.0/23 permit
141.193.184.32/27 permit
141.193.184.64/26 permit
141.193.184.128/25 permit
141.193.185.32/27 permit
141.193.185.64/26 permit
141.193.185.128/25 permit
143.55.224.0/21 permit
143.55.232.0/22 permit
143.55.236.0/22 permit
@@ -1448,13 +1404,13 @@
144.178.38.0/24 permit
145.253.228.160/29 permit
145.253.239.128/29 permit
146.20.14.104/30 permit
146.20.112.0/26 permit
146.20.113.0/24 permit
146.20.191.0/24 permit
146.20.215.0/24 permit
146.20.215.182 permit
146.88.28.0/24 permit
147.160.158.0/24 permit
147.243.1.47 permit
147.243.1.48 permit
147.243.1.153 permit
@@ -1463,6 +1419,7 @@
148.105.0.0/16 permit
148.105.8.0/21 permit
149.72.0.0/16 permit
149.72.223.204 permit
149.72.248.236 permit
149.97.173.180 permit
150.230.98.160 permit
@@ -1492,6 +1449,8 @@
158.101.211.207 permit
158.120.80.0/21 permit
158.247.16.0/20 permit
159.92.154.0/24 permit
159.92.155.0/24 permit
159.92.157.0/24 permit
159.92.157.16 permit
159.92.157.17 permit
@@ -1501,6 +1460,9 @@
159.92.160.0/24 permit
159.92.161.0/24 permit
159.92.162.0/24 permit
159.92.163.0/24 permit
159.92.164.0/22 permit
159.92.168.0/21 permit
159.112.240.0/20 permit
159.112.242.162 permit
159.135.132.128/25 permit
@@ -1515,9 +1477,11 @@
161.71.64.0/20 permit
162.247.216.0/22 permit
163.47.180.0/22 permit
163.47.180.0/23 permit
163.114.130.16 permit
163.114.132.120 permit
163.114.134.16 permit
163.114.135.16 permit
164.177.132.168/30 permit
165.173.128.0/24 permit
166.78.68.0/22 permit
166.78.68.221 permit
@@ -1538,8 +1502,6 @@
167.89.75.164 permit
167.89.101.2 permit
167.89.101.192/28 permit
167.216.129.210 permit
167.216.131.180 permit
167.220.67.232/29 permit
168.138.5.36 permit
168.138.73.51 permit
@@ -1549,6 +1511,9 @@
168.245.127.231 permit
169.148.129.0/24 permit
169.148.131.0/24 permit
169.148.142.10 permit
169.148.144.0/25 permit
169.148.144.10 permit
170.10.68.0/22 permit
170.10.128.0/24 permit
170.10.129.0/24 permit
@@ -1586,14 +1551,14 @@
182.50.76.0/22 permit
182.50.78.64/28 permit
183.240.219.64/29 permit
185.4.120.0/23 permit
185.4.122.0/24 permit
185.4.120.0/22 permit
185.12.80.0/22 permit
185.58.84.93 permit
185.80.93.204 permit
185.80.93.227 permit
185.80.95.31 permit
185.90.20.0/22 permit
185.138.56.128/25 permit
185.189.236.0/22 permit
185.211.120.0/22 permit
185.250.236.0/22 permit
@@ -1610,7 +1575,6 @@
188.125.68.184 permit
188.125.68.186 permit
188.125.68.192 permit
188.125.69.0/24 permit
188.125.69.105 permit
188.125.69.110 permit
188.125.69.112 permit
@@ -1653,9 +1617,6 @@
192.0.64.0/18 permit
192.18.139.154 permit
192.30.252.0/22 permit
192.64.236.0/24 permit
192.64.237.0/24 permit
192.64.238.0/24 permit
192.161.144.0/20 permit
192.162.87.0/24 permit
192.237.158.0/23 permit
@@ -1671,7 +1632,6 @@
193.122.128.100 permit
193.123.56.63 permit
194.19.134.0/25 permit
194.64.234.128/27 permit
194.64.234.129 permit
194.106.220.0/23 permit
194.113.24.0/22 permit
@@ -1697,16 +1657,23 @@
198.61.254.231 permit
198.178.234.57 permit
198.244.48.0/20 permit
198.244.59.30 permit
198.244.59.33 permit
198.244.59.35 permit
198.244.60.0/22 permit
198.245.80.0/20 permit
198.245.81.0/24 permit
199.15.176.173 permit
199.15.213.187 permit
199.15.226.37 permit
199.16.156.0/22 permit
199.33.145.1 permit
199.33.145.32 permit
199.34.22.36 permit
199.59.148.0/22 permit
199.67.80.2 permit
199.67.80.20 permit
199.67.82.2 permit
199.67.82.20 permit
199.67.84.0/24 permit
199.67.86.0/24 permit
199.67.88.0/24 permit
@@ -1734,7 +1701,6 @@
203.188.194.251 permit
203.188.195.240/30 permit
203.188.195.244/31 permit
203.188.197.0/24 permit
203.188.197.193 permit
203.188.197.194/31 permit
203.188.197.196/30 permit
@@ -1744,7 +1710,6 @@
203.188.197.216/29 permit
203.188.197.232/29 permit
203.188.197.240/29 permit
203.188.200.0/24 permit
203.188.200.56/31 permit
203.188.200.58 permit
203.188.200.60/30 permit
@@ -1760,11 +1725,9 @@
203.209.230.76/31 permit
204.11.168.0/21 permit
204.13.11.48/29 permit
204.13.11.48/30 permit
204.14.232.0/21 permit
204.14.232.64/28 permit
204.14.234.64/28 permit
204.29.186.0/23 permit
204.75.142.0/24 permit
204.79.197.212 permit
204.92.114.187 permit
@@ -1813,6 +1776,7 @@
207.67.98.192/27 permit
207.68.176.0/26 permit
207.68.176.96/27 permit
207.97.204.96/29 permit
207.126.144.0/20 permit
207.171.160.0/19 permit
207.211.30.64/26 permit
@@ -1826,11 +1790,7 @@
208.43.21.28/30 permit
208.43.21.64/29 permit
208.43.21.72/30 permit
208.46.212.80 permit
208.46.212.208/31 permit
208.46.212.210 permit
208.64.132.0/22 permit
208.71.40.0/24 permit
208.71.40.63 permit
208.71.40.64/31 permit
208.71.40.174/31 permit
@@ -1849,18 +1809,15 @@
208.71.41.172/31 permit
208.71.41.188/30 permit
208.71.41.192/31 permit
208.71.42.0/24 permit
208.71.42.190/31 permit
208.71.42.192/28 permit
208.71.42.208/30 permit
208.71.42.212/31 permit
208.71.42.214 permit
208.72.249.240/29 permit
208.74.204.0/22 permit
208.74.204.5 permit
208.74.204.9 permit
208.75.120.0/22 permit
208.75.121.246 permit
208.75.122.246 permit
208.82.237.96/29 permit
208.82.237.104/31 permit
208.82.238.96/29 permit
@@ -1879,10 +1836,8 @@
209.67.98.46 permit
209.67.98.59 permit
209.85.128.0/17 permit
212.82.96.0/24 permit
212.82.96.32/27 permit
212.82.96.64/29 permit
212.82.98.0/24 permit
212.82.98.32/29 permit
212.82.98.64/27 permit
212.82.98.96/30 permit
@@ -1919,12 +1874,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
@@ -1935,7 +1919,6 @@
216.17.150.251 permit
216.22.15.224/27 permit
216.24.224.0/20 permit
216.39.60.0/23 permit
216.39.60.154/31 permit
216.39.60.156/30 permit
216.39.60.160/30 permit
@@ -1953,14 +1936,12 @@
216.39.61.170 permit
216.39.61.175 permit
216.39.61.238/31 permit
216.39.62.0/24 permit
216.39.62.32/28 permit
216.39.62.48/29 permit
216.39.62.56/30 permit
216.39.62.60/31 permit
216.39.62.136/29 permit
216.39.62.144/31 permit
216.46.168.0/24 permit
216.58.192.0/19 permit
216.66.217.240/29 permit
216.71.138.33 permit
@@ -1973,12 +1954,8 @@
216.98.158.0/24 permit
216.99.5.67 permit
216.99.5.68 permit
216.109.114.0/24 permit
216.109.114.32/27 permit
216.109.114.64/29 permit
216.113.160.0/24 permit
216.113.172.0/25 permit
216.113.175.0/24 permit
216.128.126.97 permit
216.136.162.65 permit
216.136.162.120/29 permit
@@ -1988,10 +1965,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
@@ -2009,6 +1986,8 @@
2603:1030:20e:3::23c permit
2603:1030:b:3::152 permit
2603:1030:c02:8::14 permit
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
2607:f8b0:4000::/36 permit
2620:109:c003:104::/64 permit
2620:109:c003:104::215 permit
@@ -2017,6 +1996,8 @@
2620:109:c00d:104::/64 permit
2620:10d:c090:400::8:1 permit
2620:10d:c091:400::8:1 permit
2620:10d:c09b:400::8:1 permit
2620:10d:c09c:400::8:1 permit
2620:119:50c0:207::/64 permit
2620:119:50c0:207::215 permit
2800:3f0:4000::/36 permit

View File

@@ -0,0 +1,113 @@
<?php
// File size is limited by Nginx site to 10M
// To speed things up, we do not include prerequisites
header('Content-Type: text/plain');
require_once "vars.inc.php";
// Do not show errors, we log to using error_log
ini_set('error_reporting', 0);
// Init database
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
$opt = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
}
catch (PDOException $e) {
error_log("FOOTER: " . $e . PHP_EOL);
http_response_code(501);
exit;
}
if (!function_exists('getallheaders')) {
function getallheaders() {
if (!is_array($_SERVER)) {
return array();
}
$headers = array();
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
}
// Read headers
$headers = getallheaders();
// Get Domain
$domain = $headers['Domain'];
// Get Username
$username = $headers['Username'];
// Get From
$from = $headers['From'];
// define empty footer
$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 {
// try get $target_domain if $domain is an alias_domain
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain`
WHERE `alias_domain` = :alias_domain");
$stmt->execute(array(
':alias_domain' => $domain
));
$alias_domain = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$alias_domain) {
$target_domain = $domain;
} else {
$target_domain = $alias_domain['target_domain'];
}
// get footer associated with the domain
$stmt = $pdo->prepare("SELECT `plain`, `html`, `mbox_exclude`, `alias_domain_exclude`, `skip_replies` FROM `domain_wide_footer`
WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $target_domain
));
$footer = $stmt->fetch(PDO::FETCH_ASSOC);
// check if the sender is excluded
if (in_array($from, json_decode($footer['mbox_exclude']))){
$footer = false;
}
if (in_array($domain, json_decode($footer['alias_domain_exclude']))){
$footer = false;
}
if (empty($footer)){
echo $empty_footer;
exit;
}
error_log("FOOTER: " . json_encode($footer) . PHP_EOL);
// footer will be applied
// get custom mailbox attributes to insert into the footer
$stmt = $pdo->prepare("SELECT `custom_attributes` FROM `mailbox` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username
));
$custom_attributes = $stmt->fetch(PDO::FETCH_ASSOC)['custom_attributes'];
if (empty($custom_attributes)){
$custom_attributes = (object)array();
}
}
catch (Exception $e) {
error_log("FOOTER: " . $e->getMessage() . PHP_EOL);
http_response_code(502);
exit;
}
// return footer
$footer["vars"] = $custom_attributes;
echo json_encode($footer);

View File

@@ -6,3 +6,4 @@ disable_monitoring = true;
# In case a task times out (like DNS lookup), soft reject the message
# instead of silently accepting the message without further processing.
soft_reject_on_timeout = true;
local_addrs = /etc/rspamd/custom/mailcow_networks.map;

View File

@@ -0,0 +1,9 @@
# Uncomment below to apply the ratelimits globally. Use Ratelimits inside mailcow UI to overwrite them for a specific domain/mailbox.
# rates {
# # Format: "1 / 1h" or "20 / 1m" etc.
# to = "100 / 1s";
# to_ip = "100 / 1s";
# to_ip_from = "100 / 1s";
# bounce_to = "100 / 1h";
# bounce_to_ip = "7 / 1m";
# }

View File

@@ -1,20 +1,4 @@
rbls {
sorbs {
symbol = "RBL_SORBS";
rbl = "dnsbl.sorbs.net";
returncodes {
# http:// www.sorbs.net/general/using.shtml
RBL_SORBS_HTTP = "127.0.0.2";
RBL_SORBS_SOCKS = "127.0.0.3";
RBL_SORBS_MISC = "127.0.0.4";
RBL_SORBS_SMTP = "127.0.0.5";
RBL_SORBS_RECENT = "127.0.0.6";
RBL_SORBS_WEB = "127.0.0.7";
RBL_SORBS_DUL = "127.0.0.10";
RBL_SORBS_BLOCK = "127.0.0.8";
RBL_SORBS_ZOMBIE = "127.0.0.9";
}
}
interserver_ip {
symbol = "RBL_INTERSERVER_IP";
rbl = "rbl.interserver.net";

View File

@@ -5,46 +5,6 @@ symbols = {
"RBL_UCEPROTECT_LEVEL2" {
score = 1.5;
}
"RBL_SORBS" {
score = 0.0;
description = "Unrecognised result from SORBS RBL";
}
"RBL_SORBS_HTTP" {
score = 2.5;
description = "List of Open HTTP Proxy Servers.";
}
"RBL_SORBS_SOCKS" {
score = 2.5;
description = "List of Open SOCKS Proxy Servers.";
}
"RBL_SORBS_MISC" {
score = 1.0;
description = "List of open Proxy Servers not listed in the SOCKS or HTTP lists.";
}
"RBL_SORBS_SMTP" {
score = 4.0;
description = "List of Open SMTP relay servers.";
}
"RBL_SORBS_RECENT" {
score = 2.0;
description = "List of hosts that have been noted as sending spam/UCE/UBE to the admins of SORBS within the last 28 days (includes new.spam.dnsbl.sorbs.net).";
}
"RBL_SORBS_WEB" {
score = 2.0;
description = "List of web (WWW) servers which have spammer abusable vulnerabilities (e.g. FormMail scripts)";
}
"RBL_SORBS_DUL" {
score = 2.0;
description = "Dynamic IP Address ranges (NOT a Dial Up list!)";
}
"RBL_SORBS_BLOCK" {
score = 0.5;
description = "List of hosts demanding that they never be tested by SORBS.";
}
"RBL_SORBS_ZOMBIE" {
score = 2.0;
description = "List of networks hijacked from their original owners, some of which have already used for spamming.";
}
"RECEIVED_SPAMHAUS_XBL" {
weight = 0.0;
description = "Received address is listed in ZEN XBL";

View File

@@ -527,20 +527,21 @@ rspamd_config:register_symbol({
name = 'MOO_FOOTER',
type = 'prefilter',
callback = function(task)
local cjson = require "cjson"
local lua_mime = require "lua_mime"
local lua_util = require "lua_util"
local rspamd_logger = require "rspamd_logger"
local rspamd_redis = require "rspamd_redis"
local ucl = require "ucl"
local redis_params = rspamd_parse_redis_server('footer')
local rspamd_http = require "rspamd_http"
local envfrom = task:get_from(1)
local uname = task:get_user()
if not envfrom or not uname then
return false
end
local uname = uname:lower()
local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case
local env_from_domain = envfrom[1].domain:lower()
local env_from_addr = envfrom[1].addr:lower()
-- determine newline type
local function newline(task)
local t = task:get_newlines_type()
@@ -552,20 +553,27 @@ rspamd_config:register_symbol({
return '\r\n'
end
local function redis_cb_footer(err, data)
-- retrieve footer
local function footer_cb(err_message, code, data, headers)
if err or type(data) ~= 'string' then
rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
else
-- parse json string
local parser = ucl.parser()
local res,err = parser:parse_string(data)
if not res then
local footer = cjson.decode(data)
if not footer then
rspamd_logger.infox(rspamd_config, "parsing domain wide footer for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
else
local footer = parser:get_object()
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 and type(footer) == "table" and (footer.html or footer.plain) then
rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s", uname, footer.html, footer.plain)
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 = ""
@@ -575,6 +583,7 @@ rspamd_config:register_symbol({
from_name = envfrom[1].name
end
-- default replacements
local replacements = {
auth_user = uname,
from_user = envfrom[1].user,
@@ -582,10 +591,20 @@ rspamd_config:register_symbol({
from_addr = envfrom[1].addr,
from_domain = envfrom[1].domain:lower()
}
if footer.html then
-- add custom mailbox attributes
if footer.vars and type(footer.vars) == "string" then
local footer_vars = cjson.decode(footer.vars)
if type(footer_vars) == "table" then
for key, value in pairs(footer_vars) do
replacements[key] = value
end
end
end
if footer.html and footer.html ~= "" then
footer.html = lua_util.jinja_template(footer.html, replacements, true)
end
if footer.plain then
if footer.plain and footer.plain ~= "" then
footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
end
@@ -602,10 +621,24 @@ rspamd_config:register_symbol({
local nct = string.format('%s: %s/%s; charset=utf-8',
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
out[#out + 1] = nct
-- update Content-Type header
task:set_milter_reply({
remove_headers = {['Content-Type'] = 0},
})
task:set_milter_reply({
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8', rewrite.new_ct.type, rewrite.new_ct.subtype)}
})
return
elseif name:lower() == 'content-transfer-encoding' then
out[#out + 1] = string.format('%s: %s',
'Content-Transfer-Encoding', 'quoted-printable')
-- update Content-Transfer-Encoding header
task:set_milter_reply({
remove_headers = {['Content-Transfer-Encoding'] = 0},
})
task:set_milter_reply({
add_headers = {['Content-Transfer-Encoding'] = 'quoted-printable'}
})
seen_cte = true
return
end
@@ -631,15 +664,19 @@ rspamd_config:register_symbol({
end
local out_parts = {}
for _,o in ipairs(out) do
if type(o) ~= 'table' then
out_parts[#out_parts + 1] = o
out_parts[#out_parts + 1] = newline_s
else
out_parts[#out_parts + 1] = o[1]
if o[2] then
out_parts[#out_parts + 1] = newline_s
end
end
if type(o) ~= 'table' then
out_parts[#out_parts + 1] = o
out_parts[#out_parts + 1] = newline_s
else
local removePrefix = "--\x0D\x0AContent-Type"
if string.lower(string.sub(tostring(o[1]), 1, string.len(removePrefix))) == string.lower(removePrefix) then
o[1] = string.sub(tostring(o[1]), string.len("--\x0D\x0A") + 1)
end
out_parts[#out_parts + 1] = o[1]
if o[2] then
out_parts[#out_parts + 1] = newline_s
end
end
end
task:set_message(out_parts)
else
@@ -649,17 +686,14 @@ rspamd_config:register_symbol({
end
end
local redis_ret_footer = rspamd_redis_make_request(task,
redis_params, -- connect params
env_from_domain, -- hash key
false, -- is write
redis_cb_footer, --callback
'HGET', -- command
{"DOMAIN_WIDE_FOOTER", env_from_domain} -- arguments
)
if not redis_ret_footer then
rspamd_logger.infox(rspamd_config, "cannot make request to load footer for domain")
end
-- fetch footer
rspamd_http.request({
task=task,
url='http://nginx:8081/footer.php',
body='',
callback=footer_cb,
headers={Domain=env_from_domain,Username=uname,From=env_from_addr},
})
return true
end,

View File

@@ -1,11 +1,3 @@
rates {
# Format: "1 / 1h" or "20 / 1m" etc. - global ratelimits are disabled by default
to = "100 / 1s";
to_ip = "100 / 1s";
to_ip_from = "100 / 1s";
bounce_to = "100 / 1h";
bounce_to_ip = "7 / 1m";
}
whitelisted_rcpts = "postmaster,mailer-daemon";
max_rcpt = 25;
custom_keywords = "/etc/rspamd/lua/ratelimit.lua";

View File

@@ -12,9 +12,13 @@
SOGoJunkFolderName= "Junk";
SOGoMailDomain = "sogo.local";
SOGoEnableEMailAlarms = YES;
SOGoMailHideInlineAttachments = YES;
SOGoFoldersSendEMailNotifications = YES;
SOGoForwardEnabled = YES;
// Option to set Users as admin to globally manage calendar permissions etc. Disabled by default
// SOGoSuperUsernames = ("moo@example.com");
SOGoUIAdditionalJSFiles = (
js/theme.js,
js/custom-sogo.js
@@ -37,6 +41,7 @@
SOGoLanguage = English;
SOGoMailAuxiliaryUserAccountsEnabled = YES;
// SOGoCreateIdentitiesDisabled = NO;
SOGoMailCustomFromEnabled = YES;
SOGoMailingMechanism = smtp;
SOGoSMTPAuthenticationType = plain;

View File

@@ -85,6 +85,8 @@ $cors_settings = cors('get');
$cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allowed_origins']);
$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
$f2b_data = fail2ban('get');
$template = 'admin.twig';
$template_data = [
'tfa_data' => $tfa_data,
@@ -101,7 +103,8 @@ $template_data = [
'domains' => $domains,
'all_domains' => $all_domains,
'mailboxes' => $mailboxes,
'f2b_data' => fail2ban('get'),
'f2b_data' => $f2b_data,
'f2b_banlist_url' => getBaseUrl() . "/api/v1/get/fail2ban/banlist/" . $f2b_data['banlist_id'],
'q_data' => quarantine('settings'),
'qn_data' => quota_notification('get'),
'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'),
@@ -113,6 +116,7 @@ $template_data = [
'password_complexity' => password_complexity('get'),
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
'cors_settings' => $cors_settings,
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
'lang_admin' => json_encode($lang['admin']),
'lang_datatables' => json_encode($lang['datatables'])
];

View File

@@ -3137,6 +3137,86 @@ paths:
type: string
type: object
summary: Update domain
/api/v1/edit/domain/footer:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
- log:
- mailbox
- edit
- domain_wide_footer
- domains:
- mailcow.tld
html: "<br>foo {= foo =}"
plain: "<foo {= foo =}"
mbox_exclude:
- moo@mailcow.tld
- null
msg:
- domain_footer_modified
- mailcow.tld
type: success
schema:
properties:
log:
description: contains request object
items: {}
type: array
msg:
items: {}
type: array
type:
enum:
- success
- danger
- error
type: string
type: object
description: OK
headers: {}
tags:
- Domains
description: >-
You can update the footer of one or more domains per request.
operationId: Update domain wide footer
requestBody:
content:
application/json:
schema:
example:
attr:
html: "<br>foo {= foo =}"
plain: "foo {= foo =}"
mbox_exclude:
- moo@mailcow.tld
items: mailcow.tld
properties:
attr:
properties:
html:
description: Footer text in HTML format
type: string
plain:
description: Footer text in PLAIN text format
type: string
mbox_exclude:
description: Array of mailboxes to exclude from domain wide footer
type: object
type: object
items:
description: contains a list of domain names where you want to update the footer
type: array
items:
type: string
type: object
summary: Update domain wide footer
/api/v1/edit/fail2ban:
post:
responses:
@@ -3336,6 +3416,86 @@ paths:
type: object
type: object
summary: Update mailbox
/api/v1/edit/mailbox/custom-attribute:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
- log:
- mailbox
- edit
- mailbox_custom_attribute
- mailboxes:
- moo@mailcow.tld
attribute:
- role
- foo
value:
- cow
- bar
- null
msg:
- mailbox_modified
- moo@mailcow.tld
type: success
schema:
properties:
log:
description: contains request object
items: {}
type: array
msg:
items: {}
type: array
type:
enum:
- success
- danger
- error
type: string
type: object
description: OK
headers: {}
tags:
- Mailboxes
description: >-
You can update custom attributes of one or more mailboxes per request.
operationId: Update mailbox custom attributes
requestBody:
content:
application/json:
schema:
example:
attr:
attribute:
- role
- foo
value:
- cow
- bar
items:
- moo@mailcow.tld
properties:
attr:
properties:
attribute:
description: Array of attribute keys
type: object
value:
description: Array of attribute values
type: object
type: object
items:
description: contains list of mailboxes you want update
type: object
type: object
summary: Update mailbox custom attributes
/api/v1/edit/mailq:
post:
responses:
@@ -5581,6 +5741,7 @@ paths:
sogo_access: "1"
tls_enforce_in: "0"
tls_enforce_out: "0"
custom_attributes: {}
domain: domain3.tld
is_relayed: 0
local_part: info
@@ -5646,6 +5807,40 @@ paths:
items:
type: string
summary: Edit Cross-Origin Resource Sharing (CORS) settings
"/api/v1/get/spam-score/{mailbox}":
get:
parameters:
- description: name of mailbox or empty for current user - admin user will retrieve the global spam filter score
in: path
name: mailbox
required: true
schema:
type: string
- description: e.g. api-key-string
example: api-key-string
in: header
name: X-API-Key
required: false
schema:
type: string
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
spam_score: "8,15"
description: OK
headers: {}
tags:
- Mailboxes
description: >-
Using this endpoint you can get the global spam filter score or the spam filter score of a certain mailbox.
operationId: Get mailbox or global spam filter score
summary: Get mailbox or global spam filter score
tags:
- name: Domains

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

@@ -39,9 +39,13 @@ foreach ($containers as $container => $container_info) {
$StartedAt['month'],
$StartedAt['day'],
$StartedAt['year']));
$user_tz = new DateTimeZone(getenv('TZ'));
$date->setTimezone($user_tz);
$started = $date->format('r');
try {
$user_tz = new DateTimeZone(getenv('TZ'));
$date->setTimezone($user_tz);
$started = $date->format('r');
} catch(Exception $e) {
$started = '?';
}
}
else {
$started = '?';

View File

@@ -58,6 +58,9 @@ if (isset($_SESSION['mailcow_cc_role'])) {
'dkim' => dkim('details', $domain),
'domain_details' => $result,
'domain_footer' => $domain_footer,
'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]),
'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address'),
'alias_domains' => mailbox('get', 'alias_domains', $_GET["domain"])
];
}
}
@@ -218,6 +221,7 @@ $js_minifier->add('/web/js/site/pwgen.js');
$template_data['result'] = $result;
$template_data['return_to'] = $_SESSION['return_to'];
$template_data['lang_user'] = json_encode($lang['user']);
$template_data['lang_admin'] = json_encode($lang['admin']);
$template_data['lang_datatables'] = json_encode($lang['datatables']);
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';

View File

@@ -1,4 +1,11 @@
<?php
// Block requests by checking the 'Sec-Fetch-Dest' header.
if (isset($_SERVER['HTTP_SEC_FETCH_DEST']) && $_SERVER['HTTP_SEC_FETCH_DEST'] !== 'empty') {
header('HTTP/1.1 403 Forbidden');
exit;
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != 'admin') {
exit();

View File

@@ -12,7 +12,8 @@ $alertbox_log_parser = alertbox_log_parser($_SESSION);
$alerts = [];
if (is_array($alertbox_log_parser)) {
foreach ($alertbox_log_parser as $log) {
$message = strtr($log['msg'], ["\n" => '', "\r" => '', "\t" => '<br>']);
$message = htmlspecialchars($log['msg'], ENT_QUOTES);
$message = strtr($message, ["\n" => '', "\r" => '', "\t" => '<br>']);
$alerts[trim($log['type'], '"')][] = trim($message, '"');
}
$alert = array_filter(array_unique($alerts));

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

@@ -1,5 +1,5 @@
<?php
function fail2ban($_action, $_data = null) {
function fail2ban($_action, $_data = null, $_extra = null) {
global $redis;
$_data_log = $_data;
switch ($_action) {
@@ -247,6 +247,7 @@ function fail2ban($_action, $_data = null) {
$netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
$wl = (isset($_data['whitelist'])) ? $_data['whitelist'] : $is_now['whitelist'];
$bl = (isset($_data['blacklist'])) ? $_data['blacklist'] : $is_now['blacklist'];
$manage_external = (isset($_data['manage_external'])) ? intval($_data['manage_external']) : 0;
}
else {
$_SESSION['return'][] = array(
@@ -266,6 +267,8 @@ function fail2ban($_action, $_data = null) {
$f2b_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
$f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
$f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
$f2b_options['banlist_id'] = $is_now['banlist_id'];
$f2b_options['manage_external'] = ($manage_external > 0) ? 1 : 0;
try {
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
$redis->Del('F2B_WHITELIST');
@@ -329,5 +332,71 @@ function fail2ban($_action, $_data = null) {
'msg' => 'f2b_modified'
);
break;
case 'banlist':
try {
$f2b_options = json_decode($redis->Get('F2B_OPTIONS'), true);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
'msg' => array('redis_error', $e)
);
http_response_code(500);
return false;
}
if (is_array($_extra)) {
$_extra = $_extra[0];
}
if ($_extra != $f2b_options['banlist_id']){
http_response_code(404);
return false;
}
switch ($_data) {
case 'get':
try {
$bl = $redis->hKeys('F2B_BLACKLIST');
$active_bans = $redis->hKeys('F2B_ACTIVE_BANS');
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
'msg' => array('redis_error', $e)
);
http_response_code(500);
return false;
}
$banlist = implode("\n", array_merge($bl, $active_bans));
return $banlist;
break;
case 'refresh':
if ($_SESSION['mailcow_cc_role'] != "admin") {
return false;
}
$f2b_options['banlist_id'] = uuid4();
try {
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
'msg' => array('redis_error', $e)
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
'msg' => 'f2b_banlist_refreshed'
);
return true;
break;
}
break;
}
}

View File

@@ -284,17 +284,17 @@ function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {
}
if (!$sasl[$k]['location']) {
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL,"https://dfdata.bella.network/lookup/" . $sasl[$k]['real_rip']);
curl_setopt($curl, CURLOPT_URL,"https://dfdata.bella.network/country/" . $sasl[$k]['real_rip']);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_USERAGENT, 'Moocow');
curl_setopt($curl, CURLOPT_TIMEOUT, 5);
$ip_data = curl_exec($curl);
if (!curl_errno($curl)) {
$ip_data_array = json_decode($ip_data, true);
if ($ip_data_array !== false and !empty($ip_data_array['location']['shortcountry'])) {
$sasl[$k]['location'] = $ip_data_array['location']['shortcountry'];
if ($ip_data_array !== false and !empty($ip_data_array['shortcountry'])) {
$sasl[$k]['location'] = $ip_data_array['shortcountry'];
try {
$redis->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['location']['shortcountry']);
$redis->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['shortcountry']);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
@@ -2246,6 +2246,21 @@ function cors($action, $data = null) {
break;
}
}
function getBaseURL() {
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'];
$base_url = $protocol . '://' . $host;
return $base_url;
}
function uuid4() {
$data = openssl_random_pseudo_bytes(16);
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
function get_logs($application, $lines = false) {
if ($lines === false) {

View File

@@ -325,6 +325,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$timeout2 = intval($_data['timeout2']);
$skipcrossduplicates = intval($_data['skipcrossduplicates']);
$automap = intval($_data['automap']);
$dry = intval($_data['dry']);
$port1 = $_data['port1'];
$host1 = strtolower($_data['host1']);
$password1 = $_data['password1'];
@@ -435,8 +436,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return false;
}
$stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `timeout1`, `timeout2`, `automap`, `skipcrossduplicates`, `maxbytespersecond`, `subscribeall`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `custom_params`, `active`)
VALUES (:user2, :exclude, :delete1, :delete2, :timeout1, :timeout2, :automap, :skipcrossduplicates, :maxbytespersecond, :subscribeall, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :custom_params, :active)");
$stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `timeout1`, `timeout2`, `automap`, `skipcrossduplicates`, `maxbytespersecond`, `subscribeall`, `dry`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `custom_params`, `active`)
VALUES (:user2, :exclude, :delete1, :delete2, :timeout1, :timeout2, :automap, :skipcrossduplicates, :maxbytespersecond, :subscribeall, :dry, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :custom_params, :active)");
$stmt->execute(array(
':user2' => $username,
':custom_params' => $custom_params,
@@ -450,6 +451,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':skipcrossduplicates' => $skipcrossduplicates,
':maxbytespersecond' => $maxbytespersecond,
':subscribeall' => $subscribeall,
':dry' => $dry,
':subfolder2' => $subfolder2,
':host1' => $host1,
':authmech1' => 'PLAIN',
@@ -476,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',
@@ -518,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';
}
@@ -623,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(
@@ -1004,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',
@@ -1017,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;
@@ -1027,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']);
@@ -1225,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'],
@@ -1249,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,
@@ -1522,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;
@@ -2031,6 +2058,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$success = (isset($_data['success'])) ? NULL : $is_now['success'];
$delete2duplicates = (isset($_data['delete2duplicates'])) ? intval($_data['delete2duplicates']) : $is_now['delete2duplicates'];
$subscribeall = (isset($_data['subscribeall'])) ? intval($_data['subscribeall']) : $is_now['subscribeall'];
$dry = (isset($_data['dry'])) ? intval($_data['dry']) : $is_now['dry'];
$delete1 = (isset($_data['delete1'])) ? intval($_data['delete1']) : $is_now['delete1'];
$delete2 = (isset($_data['delete2'])) ? intval($_data['delete2']) : $is_now['delete2'];
$automap = (isset($_data['automap'])) ? intval($_data['automap']) : $is_now['automap'];
@@ -2164,6 +2192,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`timeout1` = :timeout1,
`timeout2` = :timeout2,
`subscribeall` = :subscribeall,
`dry` = :dry,
`active` = :active
WHERE `id` = :id");
$stmt->execute(array(
@@ -2189,6 +2218,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':timeout1' => $timeout1,
':timeout2' => $timeout2,
':subscribeall' => $subscribeall,
':dry' => $dry,
':active' => $active,
));
$_SESSION['return'][] = array(
@@ -3259,6 +3289,62 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
return true;
break;
case 'mailbox_custom_attribute':
$_data['attribute'] = isset($_data['attribute']) ? $_data['attribute'] : array();
$_data['attribute'] = is_array($_data['attribute']) ? $_data['attribute'] : array($_data['attribute']);
$_data['attribute'] = array_map(function($value) { return str_replace(' ', '', $value); }, $_data['attribute']);
$_data['value'] = isset($_data['value']) ? $_data['value'] : array();
$_data['value'] = is_array($_data['value']) ? $_data['value'] : array($_data['value']);
$attributes = (object)array_combine($_data['attribute'], $_data['value']);
$mailboxes = is_array($_data['mailboxes']) ? $_data['mailboxes'] : array($_data['mailboxes']);
foreach ($mailboxes as $mailbox) {
if (!filter_var($mailbox, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('username_invalid', $mailbox)
);
continue;
}
$is_now = mailbox('get', 'mailbox_details', $mailbox);
if(!empty($is_now)){
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $is_now['domain'])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("UPDATE `mailbox`
SET `custom_attributes` = :custom_attributes
WHERE username = :username");
$stmt->execute(array(
":username" => $mailbox,
":custom_attributes" => json_encode($attributes)
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $mailbox)
);
}
return true;
break;
case 'resource':
if (!is_array($_data['name'])) {
$names = array();
@@ -3338,44 +3424,116 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
}
break;
case 'domain_wide_footer':
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
if (!is_valid_domain_name($domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_invalid'
);
return false;
case 'domain_wide_footer':
if (!is_array($_data['domains'])) {
$domains = array();
$domains[] = $_data['domains'];
}
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
else {
$domains = $_data['domains'];
}
$footers = array();
$footers['html'] = isset($_data['footer_html']) ? $_data['footer_html'] : '';
$footers['plain'] = isset($_data['footer_plain']) ? $_data['footer_plain'] : '';
try {
$redis->hSet('DOMAIN_WIDE_FOOTER', $domain, json_encode($footers));
$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();
$footers['alias_domain_exclude'] = array();
if (isset($_data["exclude"])){
if (!is_array($_data["exclude"])) {
$_data["exclude"] = array($_data["exclude"]);
}
foreach ($_data["exclude"] as $exclude) {
if (filter_var($exclude, FILTER_VALIDATE_EMAIL)) {
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `address` = :address
UNION
SELECT `username` FROM `mailbox` WHERE `username` = :username");
$stmt->execute(array(
':address' => $exclude,
':username' => $exclude,
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if(!$row){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('username_invalid', $exclude)
);
continue;
}
array_push($footers['mbox_exclude'], $exclude);
}
elseif (is_valid_domain_name($exclude)) {
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain` = :alias_domain");
$stmt->execute(array(
':alias_domain' => $exclude,
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if(!$row){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('username_invalid', $exclude)
);
continue;
}
array_push($footers['alias_domain_exclude'], $exclude);
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('username_invalid', $exclude)
);
}
}
}
catch (RedisException $e) {
foreach ($domains as $domain) {
$domain = idn_to_ascii(strtolower(trim($domain)), 0, INTL_IDNA_VARIANT_UTS46);
if (!is_valid_domain_name($domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_invalid'
);
return false;
}
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
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`, `alias_domain_exclude`, `skip_replies`) VALUES (:domain, :html, :plain, :mbox_exclude, :alias_domain_exclude, :skip_replies)");
$stmt->execute(array(
':domain' => $domain,
':html' => $footers['html'],
':plain' => $footers['plain'],
':mbox_exclude' => json_encode($footers['mbox_exclude']),
':alias_domain_exclude' => json_encode($footers['alias_domain_exclude']),
':skip_replies' => $footers['skip_replies'],
));
}
catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => $e->getMessage()
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'danger',
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e)
'msg' => array('domain_footer_modified', htmlspecialchars($domain))
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('domain_footer_modified', htmlspecialchars($domain))
);
break;
}
break;
@@ -3929,13 +4087,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
$stmt = $pdo->prepare("SELECT `id` FROM `alias` WHERE `address` != `goto` AND `domain` = :domain");
$stmt = $pdo->prepare("SELECT `id`, `address` FROM `alias` WHERE `address` != `goto` AND `domain` = :domain");
$stmt->execute(array(
':domain' => $_data,
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$aliases[] = $row['id'];
if ($_extra == "address"){
$aliases[] = $row['address'];
} else {
$aliases[] = $row['id'];
}
}
return $aliases;
break;
@@ -4179,6 +4341,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$domaindata['mboxes_in_domain'] = $MailboxDataDomain['count'];
$domaindata['mboxes_left'] = $row['mailboxes'] - $MailboxDataDomain['count'];
$domaindata['domain_name'] = $row['domain'];
$domaindata['domain_h_name'] = idn_to_utf8($row['domain']);
$domaindata['description'] = $row['description'];
$domaindata['max_num_aliases_for_domain'] = $row['aliases'];
$domaindata['max_num_mboxes_for_domain'] = $row['mailboxes'];
@@ -4287,6 +4450,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`mailbox`.`modified`,
`quota2`.`bytes`,
`attributes`,
`custom_attributes`,
`quota2`.`messages`
FROM `mailbox`, `quota2`, `domain`
WHERE (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)
@@ -4307,6 +4471,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`mailbox`.`modified`,
`quota2replica`.`bytes`,
`attributes`,
`custom_attributes`,
`quota2replica`.`messages`
FROM `mailbox`, `quota2replica`, `domain`
WHERE (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)
@@ -4323,12 +4488,12 @@ 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'];
$mailboxdata['messages'] = $row['messages'];
$mailboxdata['attributes'] = json_decode($row['attributes'], true);
$mailboxdata['custom_attributes'] = json_decode($row['custom_attributes'], true);
$mailboxdata['quota_used'] = intval($row['bytes']);
$mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
$mailboxdata['created'] = $row['created'];
@@ -4509,19 +4674,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
}
try {
$footers = $redis->hGet('DOMAIN_WIDE_FOOTER', $domain);
$footers = json_decode($footers, true);
$stmt = $pdo->prepare("SELECT `html`, `plain`, `mbox_exclude`, `alias_domain_exclude`, `skip_replies` FROM `domain_wide_footer`
WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain
));
$footer = $stmt->fetch(PDO::FETCH_ASSOC);
}
catch (RedisException $e) {
catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e)
'msg' => $e->getMessage()
);
return false;
}
return $footers;
return $footer;
break;
}
break;

View File

@@ -143,17 +143,26 @@ function rspamd_maps($_action, $_data = null) {
return false;
}
$maps = (array)$_data['map'];
$valid_maps = array();
foreach ($maps as $map) {
$is_valid = false;
foreach ($RSPAMD_MAPS as $rspamd_map_type) {
if (!in_array($map, $rspamd_map_type)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, '-'),
'msg' => array('global_map_invalid', $map)
);
continue;
if (in_array($map, $rspamd_map_type)) {
$is_valid = true;
break;
}
}
if ($is_valid) {
array_push($valid_maps, $map);
} else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, '-'),
'msg' => array('global_map_invalid', $map)
);
}
}
foreach ($valid_maps as $map) {
try {
if (file_exists('/rspamd_custom_maps/' . $map)) {
$map_content = trim($_data['rspamd_map_data']);

View File

@@ -49,7 +49,6 @@ $globalVariables = [
'app_links' => customize('get', 'app_links'),
'is_root_uri' => (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) == '/'),
'uri' => $_SERVER['REQUEST_URI'],
'last_login' => last_login('get', $_SESSION['mailcow_cc_username'], 7, 0)['ui']['time']
];
foreach ($globalVariables as $globalVariableName => $globalVariableValue) {

View File

@@ -3,7 +3,7 @@ function init_db_schema() {
try {
global $pdo;
$db_version = "14022023_1000";
$db_version = "26022024_1433";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -267,6 +267,22 @@ function init_db_schema() {
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"domain_wide_footer" => array(
"cols" => array(
"domain" => "VARCHAR(255) NOT NULL",
"html" => "LONGTEXT",
"plain" => "LONGTEXT",
"mbox_exclude" => "JSON NOT NULL DEFAULT ('[]')",
"alias_domain_exclude" => "JSON NOT NULL DEFAULT ('[]')",
"skip_replies" => "TINYINT(1) NOT NULL DEFAULT '0'"
),
"keys" => array(
"primary" => array(
"" => array("domain")
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"tags_domain" => array(
"cols" => array(
"tag_name" => "VARCHAR(255) NOT NULL",
@@ -344,6 +360,7 @@ function init_db_schema() {
"local_part" => "VARCHAR(255) NOT NULL",
"domain" => "VARCHAR(255) NOT NULL",
"attributes" => "JSON",
"custom_attributes" => "JSON NOT NULL DEFAULT ('{}')",
"kind" => "VARCHAR(100) NOT NULL DEFAULT ''",
"multiple_bookings" => "INT NOT NULL DEFAULT -1",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
@@ -704,6 +721,7 @@ function init_db_schema() {
"timeout1" => "SMALLINT NOT NULL DEFAULT '600'",
"timeout2" => "SMALLINT NOT NULL DEFAULT '600'",
"subscribeall" => "TINYINT(1) NOT NULL DEFAULT '1'",
"dry" => "TINYINT(1) NOT NULL DEFAULT '0'",
"is_running" => "TINYINT(1) NOT NULL DEFAULT '0'",
"returned_text" => "LONGTEXT",
"last_run" => "TIMESTAMP NULL DEFAULT NULL",
@@ -961,6 +979,18 @@ function init_db_schema() {
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"sogo_admin" => array(
"cols" => array(
"c_key" => "VARCHAR(255) NOT NULL DEFAULT ''",
"c_content" => "mediumtext NOT NULL",
),
"keys" => array(
"primary" => array(
"" => array("c_key")
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"pushover" => array(
"cols" => array(
"username" => "VARCHAR(255) NOT NULL",

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

@@ -19,10 +19,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Cache dependencies
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/.composer/cache/files
key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
@@ -52,10 +52,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Cache dependencies
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/.composer/cache/files
key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}

View File

@@ -12,7 +12,7 @@ jobs:
dependency-version: [prefer-lowest, prefer-stable]
steps:
- name: Checkout code
uses: actions/checkout@v1
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v1
uses: actions/checkout@v4
- name: Install dependencies
run: composer update --no-progress --ignore-platform-reqs
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v1
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2

View File

@@ -13,7 +13,7 @@ jobs:
php-version: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0']
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:

View File

@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v1
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2

View File

@@ -32,7 +32,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@v2
@@ -86,7 +86,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@v2

View File

@@ -18,7 +18,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: "Set-up PHP"
uses: shivammathur/setup-php@v2
@@ -33,7 +33,7 @@ jobs:
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache dependencies
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ${{ steps.composercache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@@ -54,7 +54,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: "Run DOCtor-RST"
uses: docker://oskarstark/doctor-rst

View File

@@ -70,6 +70,8 @@ try {
}
}
catch (Exception $e) {
// Stop when redis is not available
http_response_code(500);
?>
<center style='font-family:sans-serif;'>Connection to Redis failed.<br /><br />The following error was reported:<br/><?=$e->getMessage();?></center>
<?php
@@ -98,6 +100,7 @@ try {
}
catch (PDOException $e) {
// Stop when SQL connection fails
http_response_code(500);
?>
<center style='font-family:sans-serif;'>Connection to database failed.<br /><br />The following error was reported:<br/> <?=$e->getMessage();?></center>
<?php
@@ -105,6 +108,7 @@ exit;
}
// Stop when dockerapi is not available
if (fsockopen("tcp://dockerapi", 443, $errno, $errstr) === false) {
http_response_code(500);
?>
<center style='font-family:sans-serif;'>Connection to dockerapi container failed.<br /><br />The following error was reported:<br/><?=$errno;?> - <?=$errstr;?></center>
<?php

View File

@@ -1,7 +1,7 @@
headline: lang.sieve_preset_1
content: |
require ["reject","body","regex"];
if anyof (body :raw :regex ["filename=.*\.doc","filename=.*\.exe","filename=.*\.moo"]) {
if anyof (body :raw :regex ["filename=\".*\\.(doc|exe|moo)\""]) {
reject text:
doc, exe and moo are dangerous file extensions.
Why would you do that? I am a sad cow.

View File

@@ -0,0 +1,18 @@
headline: lang.sieve_preset_8
content: |
require "fileinto";
require "mailbox";
require "variables";
require "subaddress";
require "envelope";
require "duplicate";
require "imap4flags";
if header :matches "To" "*mail@domain.tld*" {
redirect "anothermail@anotherdomain.tld";
setflag "\\seen"; /* Mark mail as read */
fileInto "INBOX/SubFolder"; /* Move mail on subfolder after */
} else {
# The rest goes into INBOX
# default is "implicit keep", we do it explicitly here
keep;
}

View File

@@ -95,8 +95,11 @@ $AVAILABLE_LANGUAGES = array(
'it-it' => 'Italiano (Italian)',
'ko-kr' => '한국어 (Korean)',
'lv-lv' => 'latviešu (Latvian)',
'lt-lt' => 'Lietuvių (Lithuanian)',
'nb-no' => 'Norsk (Norwegian)',
'nl-nl' => 'Nederlands (Dutch)',
'pl-pl' => 'Język Polski (Polish)',
'pt-br' => 'Português brasileiro (Brazilian Portuguese)',
'pt-pt' => 'Português (Portuguese)',
'ro-ro' => 'Română (Romanian)',
'ru-ru' => 'Pусский (Russian)',
@@ -125,6 +128,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;
@@ -235,118 +247,120 @@ $RSPAMD_MAPS = array(
$IMAPSYNC_OPTIONS = array(
'whitelist' => array(
'abort',
'authmd51',
'authmd52',
'authmech1',
'authmech2',
'authuser1',
'authuser2',
'debugcontent',
'disarmreadreceipts',
'logdir',
'debugcrossduplicates',
'maxsize',
'minsize',
'minage',
'search',
'noabletosearch',
'pidfile',
'pidfilelocking',
'search1',
'search2',
'sslargs1',
'sslargs2',
'syncduplicates',
'usecache',
'synclabels',
'truncmess',
'domino2',
'expunge1',
'filterbuggyflags',
'justconnect',
'justfolders',
'maxlinelength',
'useheader',
'noabletosearch1',
'nolog',
'prefix1',
'prefix2',
'sep1',
'sep2',
'nofoldersizesatend',
'justfoldersizes',
'proxyauth1',
'skipemptyfolders',
'include',
'subfolder1',
'subscribed',
'subscribe',
'debug',
'debugcontent',
'debugcrossduplicates',
'debugflags',
'debugfolders',
'debugimap',
'debugimap1',
'debugimap2',
'debugmemory',
'debugssl',
'delete1emptyfolders',
'delete2folders',
'disarmreadreceipts',
'domain1',
'domain2',
'domino1',
'domino2',
'dry',
'errorsmax',
'exchange1',
'exchange2',
'exitwhenover',
'expunge1',
'f1f2',
'filterbuggyflags',
'folder',
'folderfirst',
'folderlast',
'folderrec',
'gmail1',
'gmail2',
'idatefromheader',
'include',
'inet4',
'inet6',
'justconnect',
'justfolders',
'justfoldersizes',
'justlogin',
'keepalive1',
'keepalive2',
'log',
'logdir',
'logfile',
'maxbytesafter',
'maxlinelength',
'maxmessagespersecond',
'maxsize',
'maxsleep',
'minage',
'minsize',
'noabletosearch',
'noabletosearch1',
'noabletosearch2',
'noexpunge1',
'noexpunge2',
'nofoldersizesatend',
'noid',
'nolog',
'nomixfolders',
'noresyncflags',
'nossl1',
'nouidexpunge2',
'syncinternaldates',
'idatefromheader',
'useuid',
'debugflags',
'debugimap',
'delete1emptyfolders',
'delete2folders',
'gmail2',
'office1',
'testslive6',
'debugimap1',
'errorsmax',
'tests',
'gmail1',
'maxmessagespersecond',
'maxbytesafter',
'maxsleep',
'abort',
'resyncflags',
'resynclabels',
'syncacls',
'nossl2',
'nosyncacls',
'notls1',
'notls2',
'nouidexpunge2',
'nousecache',
'office2',
'testslive',
'debugmemory',
'exitwhenover',
'noid',
'noexpunge1',
'authmd51',
'logfile',
'proxyauth2',
'domain1',
'domain2',
'oauthaccesstoken1',
'oauthaccesstoken2',
'oauthdirect1',
'oauthdirect2',
'folder',
'folderrec',
'folderfirst',
'folderlast',
'nomixfolders',
'authmd52',
'debugfolders',
'nossl2',
'office1',
'office2',
'pidfile',
'pidfilelocking',
'prefix1',
'prefix2',
'proxyauth1',
'proxyauth2',
'resyncflags',
'resynclabels',
'search',
'search1',
'search2',
'sep1',
'sep2',
'showpasswords',
'skipemptyfolders',
'ssl2',
'sslargs1',
'sslargs2',
'subfolder1',
'subscribe',
'subscribed',
'syncacls',
'syncduplicates',
'syncinternaldates',
'synclabels',
'tests',
'testslive',
'testslive6',
'tls2',
'notls2',
'debugssl',
'notls1',
'inet4',
'inet6',
'log',
'showpasswords'
'truncmess',
'usecache',
'useheader',
'useuid'
),
'blacklist' => array(
'skipmess',

View File

@@ -391,3 +391,11 @@ function addTag(tagAddElem, tag = null){
$(tagValuesElem).val(JSON.stringify(value_tags));
$(tagInputElem).val('');
}
function copyToClipboard(id) {
var copyText = document.getElementById(id);
copyText.select();
copyText.setSelectionRange(0, 99999);
// only works with https connections
navigator.clipboard.writeText(copyText.value);
mailcow_alert_box(lang.copy_to_clipboard, "success");
}

View File

@@ -1684,7 +1684,7 @@ function showVersionModal(title, version){
function parseGithubMarkdownLinks(inputText) {
var replacedText, replacePattern1;
replacePattern1 = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
replacePattern1 = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])(?![^<]*>)/gim;
replacedText = inputText.replace(replacePattern1, (matched, index, original, input_string) => {
if (matched.includes('github.com')){
// return short link if it's github link

View File

@@ -199,6 +199,23 @@ jQuery(function($){
});
}
function add_table_row(table_id, type) {
var row = $('<tr />');
if (type == "mbox_attr") {
cols = '<td><input class="input-sm input-xs-lg form-control" data-id="mbox_attr" type="text" name="attribute" required></td>';
cols += '<td><input class="input-sm input-xs-lg form-control" data-id="mbox_attr" type="text" name="value" required></td>';
cols += '<td><a href="#" role="button" class="btn btn-sm btn-xs-lg btn-secondary h-100 w-100" type="button">' + lang_admin.remove_row + '</a></td>';
}
row.append(cols);
table_id.append(row);
}
$('#mbox_attr_table').on('click', 'tr a', function (e) {
e.preventDefault();
$(this).parents('tr').remove();
});
$('#add_mbox_attr_row').click(function() {
add_table_row($('#mbox_attr_table'), "mbox_attr");
});
// detect element visibility changes
function onVisible(element, callback) {

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,10 +447,14 @@ 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.domain_h_name = escapeHtml(item.domain_h_name);
if (item.domain_name != item.domain_h_name){
item.domain_h_name = item.domain_h_name + '<small class="d-block">' + item.domain_name + '</small>';
}
item.aliases = item.aliases_in_domain + " / " + item.max_num_aliases_for_domain;
item.mailboxes = item.mboxes_in_domain + " / " + item.max_num_mboxes_for_domain;
@@ -489,16 +493,16 @@ jQuery(function($){
if (item.backupmx == 1) {
if (item.relay_unknown_only == 1) {
item.domain_name = '<div class="badge fs-6 bg-info">Relay Non-Local</div> ' + item.domain_name;
item.domain_h_name = '<div class="badge fs-7 bg-info">Relay Non-Local</div> ' + item.domain_h_name;
} else if (item.relay_all_recipients == 1) {
item.domain_name = '<div class="badge fs-6 bg-info">Relay All</div> ' + item.domain_name;
item.domain_h_name = '<div class="badge fs-7 bg-info">Relay All</div> ' + item.domain_h_name;
} else {
item.domain_name = '<div class="badge fs-6 bg-info">Relay</div> ' + item.domain_name;
item.domain_h_name = '<div class="badge fs-7 bg-info">Relay</div> ' + item.domain_h_name;
}
}
});
return json;
return json.data;
}
},
columns: [
@@ -521,24 +525,27 @@ jQuery(function($){
},
{
title: lang.domain,
data: 'domain_name',
data: 'domain_h_name',
responsivePriority: 3,
defaultContent: ''
},
{
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 +555,7 @@ jQuery(function($){
{
title: lang.stats,
data: 'stats',
searchable: false,
defaultContent: '',
render: function (data, type) {
data = data.split("/");
@@ -557,53 +565,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 +635,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 +868,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 +877,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 +968,7 @@ jQuery(function($){
}
});
return json;
return json.data;
}
},
columns: [
@@ -975,13 +998,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 +1018,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 +1092,7 @@ jQuery(function($){
{
title: lang.msg_num,
data: 'messages',
searchable: false,
defaultContent: '',
responsivePriority: 5
},
@@ -1085,12 +1111,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 +1128,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

@@ -40,7 +40,7 @@ jQuery(function($){
if (value.score > 0) highlightClass = 'negative';
else if (value.score < 0) highlightClass = 'positive';
else highlightClass = 'neutral';
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? escapeHtml(value.options.join(', ')) : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
});
$('[data-bs-toggle="tooltip"]').tooltip();
}

View File

@@ -220,7 +220,7 @@ jQuery(function($){
if (value.score > 0) highlightClass = 'negative';
else if (value.score < 0) highlightClass = 'positive';
else highlightClass = 'neutral';
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? escapeHtml(value.options.join(', ')) : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
});
$('[data-bs-toggle="tooltip"]').tooltip();
}
@@ -295,3 +295,7 @@ jQuery(function($){
$(".table_collapse_option").hide();
}
});

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