1
0
mirror of https://github.com/mailcow/mailcow-dockerized.git synced 2026-06-13 10:00:22 +00:00

Compare commits

...

588 Commits

Author SHA1 Message Date
FreddleSpl0it 75eb1c42d5 [Web] add mTLS Authentication 2024-02-13 11:17:49 +01:00
FreddleSpl0it a794c1ba6c [Web] fix idp mailbox login 2024-02-05 17:15:36 +01:00
DerLinkman b001097c54 dovecot: corrected dockerfile inside nightly 2024-02-05 15:57:44 +01:00
FreddleSpl0it 9e0d82e117 fix functions.inc.php, update sogo and dovecot nightly image 2024-02-05 15:22:02 +01:00
Geert Hauwaerts 6d6152a341 Fixed SQL query for retrieving SSO users. 2024-02-05 12:49:51 +01:00
FreddleSpl0it 9521a50dfb [Web] add log messages to verify-sso function 2024-02-05 12:49:50 +01:00
FreddleSpl0it 24c4ea6f9e [Web] add missing file to autodiscover.php 2024-02-05 12:49:50 +01:00
DerLinkman 95ee29dd6d Updated Dovecot Image to use OpenSSL 3.0 fix 2024-02-05 12:49:42 +01:00
FreddleSpl0it ca99280e5a [Web] add configurable client scopes for generic-oidc 2024-02-05 12:49:12 +01:00
FreddleSpl0it 73fdf31144 [Web] dont rtrim generic-oidc urls 2024-02-05 12:49:12 +01:00
DerLinkman a65f55d499 Update Dovecot to reuse lz4 compression 2024-02-05 12:48:57 +01:00
DerLinkman a070a18f81 Use dedicated nightly images on nightly branch + updates of images itself 2024-02-05 12:48:16 +01:00
FreddleSpl0it 423211f317 fix keycloak mailpassword flow 2024-02-05 12:47:38 +01:00
Mirko Ceroni 39a3e58de6 Update sogo-auth.php 2024-02-05 12:47:38 +01:00
Mirko Ceroni c792f6c172 Update sogo-auth.php
initialize db before validating credentials
2024-02-05 12:47:38 +01:00
FreddleSpl0it 3a65da8a87 Fixes #5408 2024-02-05 12:47:37 +01:00
FreddleSpl0it c92f3fea17 [Web] add missing break 2024-02-05 12:47:35 +01:00
FreddleSpl0it 4d9c10e4f7 [Web] fix user protocol badges 2024-02-05 12:47:02 +01:00
FreddleSpl0it 4f79d013d0 [Dovecot] remove passwd-verify.lua generation 2024-02-05 12:46:33 +01:00
DerLinkman 43ba5dfd09 Clamd using Alpine Packages instead self compile 2024-02-05 12:46:06 +01:00
DerLinkman 59ca84d6ff Rebased Dovecot on Alpine + fixed logging 2024-02-05 12:45:15 +01:00
DerLinkman 7664eb6fb9 Small fixes for CLAMD Health Check 2024-02-05 12:42:51 +01:00
DerLinkman be9db39a64 Added missing Labels to Dockerfiles 2024-02-05 12:40:19 +01:00
DerLinkman 2463405dfd Optimized Build Process for Dovecot 2024-02-05 12:40:18 +01:00
DerLinkman ddc0070d3a Changed Dovecot Base to Bullseye again (Self compile) 2024-02-05 12:40:04 +01:00
DerLinkman fbc8fb7ecb Optimized CLAMAV Builds to match exact version instead of Repo 2024-02-05 12:39:14 +01:00
DerLinkman ff8f4c31c5 Switched to Alpine Edge (for IMAPSYNC Deps) 2024-02-05 12:38:27 +01:00
DerLinkman b556c2c9dd Rebased Dovecot on Alpine 3.17 instead Bullseye (ARM64 Support) 2024-02-05 12:37:27 +01:00
DerLinkman 785c36bdf4 Removed Test self compiled SOGo Dockerfile 2024-02-05 12:36:22 +01:00
DerLinkman d91c4de392 Changed Maintainer to tinc within Dockerfiles 2024-02-05 12:36:21 +01:00
DerLinkman 31783b5086 Updated Clamd Building to be x86 and ARM Compatible 2024-02-05 12:35:27 +01:00
DerLinkman 31a33af141 [Rspamd] Update to 3.6 (Ratelimit fix) 2024-02-05 12:35:10 +01:00
FreddleSpl0it 2725423838 [Web] minor fixes 2024-02-05 12:34:26 +01:00
FreddleSpl0it b7324e5c25 [SOGo] deny direct login on external users 2024-02-05 12:34:25 +01:00
FreddleSpl0it da29a7a736 [SOGo] remove sogo_view and triggers 2024-02-05 12:34:25 +01:00
FreddleSpl0it a0e0dc92eb [Web] catch update_sogo exceptions 2024-02-05 12:34:12 +01:00
FreddleSpl0it 016c028ec7 [Web] fix malformed_username check 2024-02-05 12:34:12 +01:00
FreddleSpl0it c744ffd2c8 [Web] remove keycloak sync disabled warning 2024-02-05 12:34:12 +01:00
FreddleSpl0it adc7d89b57 deny changes on identity provider if it's in use 2024-02-05 12:34:11 +01:00
FreddleSpl0it 2befafa8b1 rework auth - move dovecot sasl log to php 2024-02-05 12:34:11 +01:00
FreddleSpl0it ce76b3d75f [Web] allow mailbox authsource to be switchable 2024-02-05 12:34:10 +01:00
FreddleSpl0it dd1a5d7775 [Web] fix identity-provider settings layout 2024-02-05 12:33:21 +01:00
FreddleSpl0it e437e2cc5e [Web] trim CRON_LOG 2024-02-05 12:33:21 +01:00
FreddleSpl0it f093e3a054 [Web] remove unnecessary if block 2024-02-05 12:33:20 +01:00
FreddleSpl0it 5725ddf197 [Web] add curl timeouts to oidc requests 2024-02-05 12:33:20 +01:00
FreddleSpl0it 4293d184bd [Web] update lang files 2024-02-05 12:33:20 +01:00
FreddleSpl0it 51ee8ce1a2 [Dovecot] mailcowauth minor fixes 2024-02-05 12:33:19 +01:00
FreddleSpl0it 6fe17c5d34 [Web] improve identity-provider template 2024-02-05 12:33:19 +01:00
FreddleSpl0it 7abf61478a [Web] improve attribute sync performance & make authsource editable 2024-02-05 12:33:19 +01:00
FreddleSpl0it 4bb02f4bb0 [Web] add crontasks logs 2024-02-05 12:32:01 +01:00
FreddleSpl0it dce3239809 [Web] add keycloak sync crontask 2024-02-05 12:32:01 +01:00
FreddleSpl0it 36c9e91efa [Web] handle fatal errors on getAccessToken 2024-02-05 12:32:01 +01:00
FreddleSpl0it 1258ddcdc6 [Web] fix attribute mapping list 2024-02-05 12:32:00 +01:00
FreddleSpl0it 8c8eae965d [Web] hide auth settings for external users 2024-02-05 12:32:00 +01:00
FreddleSpl0it 1bb9f70b96 [Web] fix bug on mailbox login 2024-02-05 12:32:00 +01:00
FreddleSpl0it 002eef51e1 [Web] update lang.en-gb.json 2024-02-05 12:31:59 +01:00
FreddleSpl0it 5923382831 [Web] update guzzlehttp/psr7 2024-02-05 12:31:59 +01:00
FreddleSpl0it d4add71b33 [Web] update stevenmaguire/oauth2-keycloak and firebase/php-jwt 2024-02-05 12:31:59 +01:00
FreddleSpl0it 105016b1aa [Web] add league/oauth2-client 2024-02-05 12:31:58 +01:00
FreddleSpl0it ae9584ff8b update gitignore 2024-02-05 12:31:58 +01:00
FreddleSpl0it c8e18b0fdb [Web] functions.auth.inc.php corrections 2024-02-05 12:31:58 +01:00
FreddleSpl0it 84c0f1e38b [Web] remove ropc flow 2024-02-05 12:31:57 +01:00
FreddleSpl0it 00d826edf6 [Web] add "add mailbox_from_template" function 2024-02-05 12:31:57 +01:00
FreddleSpl0it d2e656107f [Web] add generic-oidc provider 2024-02-05 12:31:56 +01:00
FreddleSpl0it 821972767c [Web] add "edit mailbox_from_template" function 2024-02-05 12:31:56 +01:00
FreddleSpl0it 35869d2f67 [Web] revert configurable authsource 2024-02-05 12:31:56 +01:00
FreddleSpl0it 8539d55c75 [Web] rename var for tab-config-identity-provider.twig 2024-02-05 12:31:55 +01:00
FreddleSpl0it 61559f3a66 [Web] add generic-oidc provider 2024-02-05 12:30:44 +01:00
FreddleSpl0it 412d8490d1 [Web] remove sso login alertbox 2024-02-05 12:30:05 +01:00
FreddleSpl0it a331813790 [Web] move iam sso functions 2024-02-05 12:30:05 +01:00
FreddleSpl0it 9be79cb08e [Dovecot] group auth files 2024-02-05 12:30:05 +01:00
FreddleSpl0it 73256b49b7 [Web] move /process/login to internal endpoint 2024-02-05 12:30:04 +01:00
FreddleSpl0it 974827cccc [Web] iam - add switch for direct login flow 2024-02-05 12:30:04 +01:00
FreddleSpl0it bb461bc0ad [Dovecot] fix wrong lua syntax 2024-02-05 12:30:04 +01:00
FreddleSpl0it b331baa123 [Web] add IAM delete button & fix add mbox modal 2024-02-05 12:30:03 +01:00
FreddleSpl0it cd6f09fb18 [Web] IAM - add delete option & fix test connection 2024-02-05 12:30:03 +01:00
FreddleSpl0it c7a7f2cd46 [Web] fix iam attribute mapping ui 2024-02-05 12:29:47 +01:00
FreddleSpl0it a4244897c2 replace ropc flow with keycloak rest api flow 2024-02-05 12:29:47 +01:00
FreddleSpl0it fb27b54ae3 [Web] rename role mapping to attribute mapping 2024-02-05 12:29:47 +01:00
FreddleSpl0it b6bf98ed48 new dovecout lua auth - use https 2024-02-05 12:29:46 +01:00
FreddleSpl0it 61960be9c4 [Web] create ratelimit acl on iam mbox creation 2 2024-02-05 12:29:16 +01:00
FreddleSpl0it 590a4e73d4 [Web] create ratelimit acl on iam mbox creation 2024-02-05 12:29:16 +01:00
FreddleSpl0it c7573752ce [Web] fix broken sogo-sso 2024-02-05 12:29:15 +01:00
FreddleSpl0it 93d7610ae7 [Web] fix app_pass ignore_access 2024-02-05 12:29:15 +01:00
FreddleSpl0it edd58e8f98 [Web] keycloak auth functions 2024-02-05 12:28:19 +01:00
FreddleSpl0it 560abc7a94 [Web] add manage identity provider 2024-02-05 12:28:18 +01:00
FreddleSpl0it f3ed3060b0 [Web] remove u2f lib from prerequisites 2024-02-05 12:24:30 +01:00
FreddleSpl0it 04a423ec6a [Web] add oauth2-keycloak lib 2024-02-05 12:24:30 +01:00
FreddleSpl0it f2b78e3232 [Web] remove u2f lib 2024-02-05 12:24:29 +01:00
FreddleSpl0it 0b7e5c9d48 [Web] limit identity_provider function better 2024-02-05 12:24:29 +01:00
FreddleSpl0it 410ff40782 [Web] manage keycloak identity provider 2024-02-05 12:23:02 +01:00
FreddleSpl0it 7218095041 [Web] organize auth functions+api auth w/ dovecot 2024-02-05 12:21:27 +01:00
FreddleSpl0it 4f350d17e5 [Web] update de-de + en-gb lang 2024-02-05 12:06:19 +01:00
FreddleSpl0it b57ec1323d [Web] organize user landing 2024-02-05 12:05:20 +01:00
FreddleSpl0it 528077394e [Web] fix user login history 2024-02-05 11:53:38 +01:00
FreddleSpl0it 7b965a60ed [Web] add app hide option 2024-02-05 11:53:37 +01:00
FreddleSpl0it c5dcae471b [Web] add seperate link for logged in users 2024-02-05 11:53:14 +01:00
FreddleSpl0it 0468af5d79 [Web] few style changes 2024-02-05 11:52:33 +01:00
FreddleSpl0it 768304a32e [Web] redirect to sogo after failed sogo-auth 2024-02-05 11:52:28 +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
FreddleSpl0it 57e67ea8f7 [Netfilter] fix mailcow isolation rule for iptables 2024-02-02 17:40:44 +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
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
Niklas Meyer 5b8efeb2ba Merge pull request #5454 from mailcow/staging
2023-10
2023-10-12 12:55:01 +02:00
Niklas Meyer f1c93fa337 Merge pull request #5253 from mailcow/renovate/composer-composer-2.x
Update dependency composer/composer to v2.6.5
2023-10-12 11:39:22 +02:00
Niklas Meyer a94a29a6ac Merge pull request #5442 from mailcow/renovate/php-pecl-mail-mailparse-3.x
Update dependency php/pecl-mail-mailparse to v3.1.6
2023-10-12 11:38:47 +02:00
Niklas Meyer 7e3d736ee1 Merge pull request #5413 from mailcow/renovate/phpredis-phpredis-6.x
Update dependency phpredis/phpredis to v6
2023-10-12 11:38:34 +02:00
Niklas Meyer 437534556e Merge pull request #5372 from Habetdin:staging
[Postfix] fix extra.cf updating
2023-10-12 11:25:32 +02:00
Niklas Meyer ce4b9c98dc Merge pull request #5402 from cero1988/staging
enable search in bodies from EAS
2023-10-12 11:13:04 +02:00
DerLinkman c134078d60 Add comment about experimental thingy 2023-10-12 11:11:50 +02:00
Niklas Meyer a8bc6aff2e Merge pull request #5451 from mailcow/feat/unbound-healthcheck
[Unbound] Added Healthcheck for Unbound (Dockerfile and Compose)
2023-10-12 10:52:23 +02:00
DerLinkman 0b627017e0 [Compose] Added Healthcheck startup logics 2023-10-11 15:49:00 +02:00
DerLinkman eb3be80286 [Unbound] Added Healthcheck (nslookup) 2023-10-11 15:48:25 +02:00
DerLinkman 1fda71e4fa Update Images which contains Curl to fix CVEs 2023-10-11 12:16:05 +02:00
DerLinkman a02bd4beff [Dovecot] Update to 2.3.21 2023-10-11 12:14:47 +02:00
DerLinkman d7f3ee16aa Update Dovecot Wiki Link for new mailcows 2023-10-10 16:13:28 +02:00
Peter 87e3c91c26 Update Dockerfile 2023-10-08 11:41:39 +02:00
FreddleSpl0it 33a38e6fde [Web] Avoid setting default ACL on create when nothing is selected 2023-10-06 11:31:28 +02:00
renovate[bot] 3d8f45db43 Update dependency composer/composer to v2.6.5
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-10-06 09:17:46 +00:00
Niklas Meyer 40df25dcf0 Merge pull request #5443 from mailcow/fix-generateconfigsh
Change column name in generate_config.sh
2023-10-06 09:41:07 +02:00
Peter 5de151a966 change column name 2023-10-06 00:12:49 +02:00
renovate[bot] 115d0681a7 Update dependency php/pecl-mail-mailparse to v3.1.6
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-10-05 15:20:24 +00:00
Niklas Meyer 1c403a6d60 Merge pull request #5401 from AlexHuebi/master
Improved the FQDN check and Ask before changing Git Repository URL in "update.sh"
2023-10-05 16:27:16 +02:00
DerLinkman e67ba60863 Added Colors, cause there fancy :) + Added in generate_config.sh 2023-10-05 16:21:57 +02:00
renovate[bot] 0c0ec7be58 Update dependency phpredis/phpredis to v6
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-10-05 14:08:31 +00:00
Niklas Meyer a72b3689b0 Merge pull request #5436 from mailcow/fix-renovate
Fix renovate to allow version extracts for Dockerfiles
2023-10-05 16:08:06 +02:00
Niklas Meyer c4c76e0945 Merge pull request #5438 from accolon/master
Update ClamAV to latest LTS version 1.0.3
2023-10-05 16:04:46 +02:00
Niklas Meyer 1a793e0b7e Merge pull request #5441 from mailcow/renovate/nextcloud-server-27.x
Update dependency nextcloud/server to v27.1.2
2023-10-05 16:03:58 +02:00
Niklas Meyer d0562ddbd9 Merge pull request #5398 from mailcow/update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2023-10-05 16:03:45 +02:00
DerLinkman 3851a48ea0 Bumped clamd version in compose.yml 2023-10-05 15:49:19 +02:00
DerLinkman 40dcf86846 Merge branch 'master' into staging 2023-10-05 15:46:22 +02:00
renovate[bot] 257e104d2b Update dependency nextcloud/server to v27.1.2
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-10-05 13:21:58 +00:00
Florian Hillebrand 3f2a9b6973 Update ClamAV to latest LTS version 1.0.3 2023-10-03 20:54:45 +02:00
Peter ed365c35e7 Fix renovate.json to allow version extracts 2023-10-02 20:22:08 +02:00
milkmaker 24ff70759a update postscreen_access.cidr 2023-10-01 00:15:06 +00:00
milkmaker c55c38f77b Translations update from Weblate (#5434)
* [Web] Updated lang.ru-ru.json

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

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

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

---------

Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>
2023-09-30 14:18:55 +02:00
Niklas Meyer 934bc15fae Merge pull request #5433 from mailcow/feat/sogo-5.9.0
[SOGo] Update to 5.9.0
2023-09-29 12:05:41 +02:00
Niklas Meyer c2c994bfbb Merge pull request #5432 from mailcow/fix-docs-domain
mailcow.github.io -> docs.mailcow.email
2023-09-29 11:56:05 +02:00
Peter b1c2ffba6e mailcow.github.io -> docs.mailcow.email 2023-09-27 18:34:53 +02:00
milkmaker b4a56052c5 [Web] Updated lang.nl-nl.json (#5431)
Co-authored-by: Nick Bouwhuis <github@nickbouwhuis.nl>
2023-09-27 17:56:21 +02:00
DerLinkman 69d15df221 [SOGo] Update to 5.9.0 2023-09-27 16:10:10 +02:00
renovate[bot] e5752755d1 Update dependency nextcloud/server to v27.1.1 (#5426)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-26 20:29:47 +02:00
Niklas Meyer d98cfe0fc7 Merge pull request #5422 from mailcow/renovate/nextcloud-server-27.x
Update dependency nextcloud/server to v27.1.0
2023-09-18 11:28:27 +02:00
renovate[bot] 1a1955c1c2 Update dependency nextcloud/server to v27.1.0
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-09-16 09:09:40 +00:00
Patrick Schult 0303dbc1d2 Merge pull request #5227 from mailcow/feat/domain-wide-footer
[Rspamd] add domain wide footer
2023-09-13 15:11:33 +02:00
FreddleSpl0it acee742822 [Web] move domain-wide-footer vars info to lang files 2023-09-13 15:08:07 +02:00
FreddleSpl0it 8d792fbd62 [Rspamd] domain-wide-footer update description 2023-09-13 13:03:46 +02:00
FreddleSpl0it d132a51a4d Merge remote-tracking branch 'origin/staging' into feat/domain-wide-footer 2023-09-13 12:44:41 +02:00
FreddleSpl0it 2111115a73 [Rspamd] domain-wide-footer add more template vars 2023-09-13 12:42:12 +02:00
renovate[bot] 160c9caee3 Update docker/setup-buildx-action action to v3 (#5417)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-12 17:41:16 +02:00
renovate[bot] 33de788453 Update docker/setup-qemu-action action to v3 (#5418)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-12 17:41:09 +02:00
renovate[bot] f86f5657d9 Update docker/login-action action to v3 (#5416)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-12 17:41:00 +02:00
renovate[bot] e02a92a0d0 Update docker/build-push-action action to v5 (#5415)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-12 17:40:44 +02:00
FreddleSpl0it 5ae9605e77 [Rspamd] domain-wide-footer add jinja templating 2023-09-12 12:19:46 +02:00
AlexHuebi 88fbec1e53 fixed remote url override 2023-09-11 21:43:52 +02:00
AlexHuebi d098e7b9e6 fixed remote url override 2023-09-11 21:42:43 +02:00
AlexHuebi a8930e8060 fixed remote url override 2023-09-11 21:39:07 +02:00
AlexHuebi e26501261e "temp" change - removed "git remote set-url" 2023-09-11 20:08:42 +02:00
Christian Schmitt 89bc11ce0f Fix typo in German translation: (#5414)
"gibt Aufschluss darüber"
2023-09-11 15:44:24 +02:00
Patrick Schult 4b096962a9 Merge pull request #5328 from mailcow/feat/backup_action
Update rebuild_backup_image.yml
2023-09-08 16:01:34 +02:00
Patrick Schult c64fdf9aa3 Merge pull request #4342 from FELDSAM-INC/feldsam/enhancements
[Web] apple config app passwords enhancements + translations
2023-09-08 15:41:25 +02:00
Patrick Schult 9caaaa6498 Merge pull request #5403 from FELDSAM-INC/feldsam/css-fixes
[Web] BS5 styling fixes and enhancements
2023-09-08 15:29:47 +02:00
Patrick Schult 105a7a4c74 Merge pull request #5405 from FELDSAM-INC/feldsam/filter-by-domain
[Web] Filter tables by Domain where possible
2023-09-08 15:01:15 +02:00
Patrick Schult 09782e5b47 Merge pull request #5406 from FELDSAM-INC/feldsam/dark-mode-logo
[Web] dark mode logo support
2023-09-08 14:57:43 +02:00
Mirko Ceroni 8d75b570c8 Update data/conf/sogo/sogo.conf
Co-authored-by: Peter <magic@kthx.at>
2023-09-04 21:43:24 +02:00
milkmaker 21121f9827 Translations update from Weblate (#5410)
* [Web] Language file updated by 'Cleanup translation files' addon

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

* [Web] Updated lang.en-gb.json

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

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

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

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

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

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

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

---------

Co-authored-by: Peter <magic@kthx.at>
2023-09-04 19:56:42 +02:00
renovate[bot] 8e87e76dcf Update actions/checkout action to v4 (#5409)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-09-04 18:49:38 +02:00
Patrick Schult 2629f3d865 Merge pull request #5404 from FELDSAM-INC/feldsam/datatables-sk-cz-translations
[Web] translated datatables to CZ and SK
2023-09-04 07:59:01 +02:00
Kristian Feldsam 8e5cd90707 [Web] Filter tables by Domain where possible
This feature was standard in Mailcow in pre-BS5 releases

Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-03 19:55:51 +02:00
Kristian Feldsam 9ffa810054 [Web] Edit Domain/Mailbox - added collapsible tabs for mobile devices
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-03 19:41:25 +02:00
Kristian Feldsam db9562e843 [Web] mailboxes - remove tab dropdown, if not admin
there are no domain and mailbox templates available, so no need to have dropdown in tabs

Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-03 19:05:24 +02:00
Kristian Feldsam 3540075b61 [Web] dark mode logo support
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-03 18:49:12 +02:00
Kristian Feldsam d0ba061f7a [Web] mobile devices - scroll window to opened tab
This feature was in versions before BS5

Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-03 18:36:39 +02:00
Kristian Feldsam 871ae5d7d2 [Web] mobile devices styling fixes and enhancements
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-03 18:36:32 +02:00
Kristian Feldsam 633ebe5e8d [Web] fixed add domain save action button group styling
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-03 14:17:54 +02:00
Kristian Feldsam 1b7cc830ca [Web] standarize select box dropdown buttons
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-03 14:17:54 +02:00
Kristian Feldsam d48193fd0e [Web] edit object - added space after heaading
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-03 14:17:54 +02:00
Kristian Feldsam bb69f39976 [Web] domain and alias domain edit - translated dkim “domain”
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-03 14:17:54 +02:00
Kristian Feldsam f059db54d0 [Web] edit mailbox template - fixed settigns buttons styling
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-03 14:17:54 +02:00
Kristian Feldsam e4e8abb1b9 [Web] Ratelimit settings as input group
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-03 14:17:54 +02:00
Kristian Feldsam 1a207f4d88 [Web] translated datatables to CZ and SK
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-03 12:38:50 +02:00
Mirko Ceroni 25d6e0bbd0 enable search in bodies from EAS
enable search in bodies from EAS
2023-09-02 11:34:29 +02:00
Kristian Feldsam 8e5323023a [Web] checkbox styling
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-02 10:30:45 +02:00
Kristian Feldsam 6d9805109a [Web] styling enhancements
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-02 10:30:39 +02:00
Kristian Feldsam 1822d56efb [Web] fixed new mailbox settings buttons styling
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>

Fixed input with btn in input group styling

Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-02 10:30:33 +02:00
Kristian Feldsam 1e3766e2f1 [Web] revisited dark mode theme, enhanced colors
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-09-02 10:30:25 +02:00
AlexHuebi 718dcb69be improved "FQDN" check 2023-09-02 02:53:55 +02:00
Patrick Schult 372b1c7bbc Merge pull request #5383 from Dexus-Forks/Dexus-patch-1
Update config for nginx >=1.25.1 (http2, server_names_hash_max_size, server_names_hash_bucket_size)
2023-08-29 12:05:44 +02:00
Patrick Schult 9ba5c13702 Merge pull request #5376 from mstilkerich/fix_dockerapi_cpuload
Fix CPU load of dockerapi container
2023-08-28 16:23:27 +02:00
milkmaker 30e241babe Translations update from Weblate (#5390)
* [Web] Updated lang.de-de.json

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

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

Co-authored-by: Marco Truffat <truffatmarco@gmail.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

[Web] Updated lang.hu-hu.json

[Web] Updated lang.hu-hu.json

Co-authored-by: 0xAndrewBlack <0xandrewblack@gmail.com>
Co-authored-by: Kántor Attila <attilalaci300@gmail.com>
Co-authored-by: Mihály Szilágyi <szimih90@gmail.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

Co-authored-by: Vlad M <vlad+mailcow@manoila.co.uk>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

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

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

[Web] Added lang.gr-gr.json

Co-authored-by: Nik Beaver <nik@beavers.forsale>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

Co-authored-by: Adrien Kara <mailcow-translate@iglou.eu>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.en-gb.json

Co-authored-by: Philipp E <ph.ecker@philipp-dev.info>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

Co-authored-by: Marco Truffat <truffatmarco@gmail.com>
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.uk-ua.json

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

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

[Web] Updated lang.si-si.json

[Web] Updated lang.si-si.json

[Web] Added lang.si-si.json

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

* Add Greek + Slovenian

---------

Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: Marco Truffat <truffatmarco@gmail.com>
Co-authored-by: 0xAndrewBlack <0xandrewblack@gmail.com>
Co-authored-by: Kántor Attila <attilalaci300@gmail.com>
Co-authored-by: Mihály Szilágyi <szimih90@gmail.com>
Co-authored-by: Vlad M <vlad+mailcow@manoila.co.uk>
Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>
Co-authored-by: Nik Beaver <nik@beavers.forsale>
Co-authored-by: Adrien Kara <mailcow-translate@iglou.eu>
Co-authored-by: Philipp E <ph.ecker@philipp-dev.info>
Co-authored-by: Michele Caputo <michele@caputoweb.xyz>
Co-authored-by: gomiunik <boris@gomiunik.net>
2023-08-19 21:47:23 +02:00
Niklas Meyer 956b170674 Merge pull request #5385 from mailcow/renovate/nextcloud-server-27.x 2023-08-14 18:11:36 +02:00
renovate[bot] 2c52753adb Update dependency nextcloud/server to v27.0.2
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-08-14 15:21:07 +00:00
Josef Fröhle 095d59c01b Update listen_ssl.template deprecated http2 on listener 2023-08-12 16:59:15 +02:00
Josef Fröhle 1a2f145b28 Update site.conf: server_names_hash_bucket_size 128 2023-08-12 16:58:26 +02:00
Michael Stilkerich 930473a980 Set asyncio timeout to 0 for yielding 2023-08-12 07:20:56 +02:00
DerLinkman 1db8990271 Fixed Branch checkout in generate_config.sh 2023-08-10 13:51:40 +02:00
FreddleSpl0it 025fd03310 [Rspamd] remove X-Moo-Tag header if unnecessary 2023-08-07 14:26:30 +02:00
renovate[bot] e468c59dfc Update thollander/actions-comment-pull-request action to v2.4.2 (#5379)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-07 06:46:07 +02:00
renovate[bot] 340ef866d2 Update thollander/actions-comment-pull-request action to v2.4.1 (#5377)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-08-06 16:39:26 +02:00
Michael Stilkerich 533bd36572 Fix CPU load of dockerapi container
Previously the handle_pubsub_messages() loop was executing every 10ms
when there was no message available. Now reading from the redis network
socket will block (the coroutine) for up to 30s before it returns when
no message is available.

Using channel.listen() would be even better, but it lacks the
ignore_subscribe_messages option and I could not figure out how to
filter the returned messages.
2023-08-05 20:58:34 +02:00
Habetdin 5bf29e6ac1 [Postfix] fix extra.cf updating 2023-08-05 00:25:19 +03:00
Patrick Schult d6c3c58f42 Merge pull request #5360 from mailcow/staging
2023-08 - DQS Hotfixes
2023-08-03 11:36:53 +02:00
FreddleSpl0it b050cb9864 [Postfix] remove dnsbl_reply.map if not required 2023-08-03 09:00:08 +02:00
Patrick Schult e176724775 Merge pull request #5357 from DocFraggle/staging
Add postscreen_dnsbl_reply_map to avoid disclosure of DQS key
2023-08-03 08:15:16 +02:00
DocFraggle 8f9ed9e0df Merge branch 'staging' into staging 2023-08-02 20:20:18 +02:00
FreddleSpl0it 003eecf131 [Postfix] remove spamhaus dbl and zrd from postscreen_dnsbl_sites 2023-08-02 17:08:55 +02:00
Patrick Schult 180b9fc8d2 Merge pull request #5359 from mailcow/fix/gen-dnsbl
[Postfix] rework dns_blocklists.cf generation
2023-08-02 16:51:56 +02:00
FreddleSpl0it 5d3491c801 [Postfix] only apply DNSBL if dns_blocklists.cf is not empty 2023-08-02 16:48:22 +02:00
FreddleSpl0it c45684b986 [Postfix] rework dns_blocklists.cf generation 2023-08-02 16:36:59 +02:00
Patrick Schult 5c886d2f4e Merge pull request #5356 from sriccio/fix-postfix-merge-order
Fix main.cf merging order
2023-08-02 15:17:20 +02:00
Christian Hailer 9f39af46aa Add postscreen_dnsbl_reply_map to avoid disclosure of DQS key with Spamhaus setup 2023-08-01 16:12:44 +02:00
Sébastien RICCIO 7cda9f063f Fix for fix
I did not paid attention to the "User overrides" sed/q
2023-08-01 13:59:23 +02:00
Sébastien RICCIO 5e7583c5e6 Fix main.cf merging order
Now the dnsbl files are merged before extra.cf
2023-08-01 10:49:26 +02:00
Niklas Meyer a1fb962215 Merge pull request #5350 from mailcow/staging
2023-07a
2023-07-31 14:52:24 +02:00
Niklas Meyer 57d849a51b Merge pull request #5349 from DocFraggle/spamhaus_domains
Fix spamhaus query domains (.net only)
2023-07-31 14:34:01 +02:00
Hailer, Christian 3000da6b88 Fix spamhaus query domains (.net only) 2023-07-31 13:50:36 +02:00
Niklas Meyer db75cbbcb0 Merge pull request #5347 from mailcow/feat/sogo-5.8.4
Update SOGo to 5.8.4
2023-07-31 12:36:24 +02:00
Niklas Meyer 22acbb6b57 Merge pull request #5267 from mailcow/update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2023-07-31 12:06:41 +02:00
milkmaker 31cb0f7db1 update postscreen_access.cidr 2023-07-31 10:06:07 +00:00
DerLinkman 6d17b9f504 Added dns_blocklists.cf for customizations 2023-07-31 12:03:31 +02:00
DerLinkman 0f337971ff Reimplemented option for custom dnsbls 2023-07-31 12:03:07 +02:00
DerLinkman 6cf2775e7e Fix Reponse Code for ASN Checks 2023-07-31 12:01:34 +02:00
Niklas Meyer dabf9104ed Merge pull request #5342 from DocFraggle/mailcow_spamhaus
dns_blocklists.cf isn't appended to main.cf and therefore ineffective…
2023-07-30 19:02:01 +02:00
Christian Hailer 952ddb18fd dns_blocklists.cf isn't appended to main.cf and therefore ineffective #5340 2023-07-30 18:56:52 +02:00
DerLinkman 34d990a800 Removed obsolete whois package 2023-07-28 20:35:28 +02:00
DerLinkman 020cb21b35 Added remote Bad ASN Check for Spamhaus DNSBL 2023-07-28 20:33:12 +02:00
DerLinkman 525364ba65 Implemented remote Bad AS lookup 2023-07-28 20:27:38 +02:00
DerLinkman 731fabef58 Fixed Syntax error in generate_config.sh 2023-07-28 12:20:47 +02:00
DerLinkman c10be77a1b Fixed Syntax error in generate_config.sh 2023-07-28 12:13:07 +02:00
DerLinkman a8bc4e3f37 Merge branch 'staging' 2023-07-28 10:35:17 +02:00
DerLinkman 815572f200 Merge branch 'feat/spamhaus-dqs-asn' into staging 2023-07-28 10:33:34 +02:00
Patrick Schult 23fc54f2cf Merge pull request #5332 from mailcow/staging
2023-07
2023-07-28 10:26:49 +02:00
FreddleSpl0it 11407973b1 [Web] change style of f2b active ban actions 2023-07-27 14:19:18 +02:00
FreddleSpl0it b9867e3fe0 [Web] change style of f2b active ban actions 2023-07-27 14:16:11 +02:00
FreddleSpl0it 3814c3294f [Web] add edit/cors api endpoint to swagger 2023-07-27 13:45:57 +02:00
FreddleSpl0it 9c44b5e546 [Web] display is_catch_all and aliases_send_as_all if not empty #5320 2023-07-27 12:10:01 +02:00
FreddleSpl0it cd635ec813 [Dockerapi] Update to 2.05 2023-07-27 11:30:47 +02:00
FreddleSpl0it 03831149f8 [Web] fix visual bug #5322 2023-07-27 11:28:49 +02:00
Peter d8fd023cdb Update rebuild_backup_image.yml 2023-07-24 17:39:41 +02:00
renovate[bot] 521120a448 Update dependency nextcloud/server to v27.0.1 (#5324)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-24 10:43:00 +02:00
DerLinkman ec8d298c36 Update postfix.sh to include pbl for dqs 2023-07-13 16:42:59 +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
Patrick Schult 03580cbf39 Merge pull request #5315 from SnailShea/fix/twig-typos
Fixes several instances of missing </span>, extra role='tabpanel' and…
2023-07-12 08:55:28 +02:00
Niklas Meyer 2b009c71c1 Merge pull request #5316 from mailcow/feat/rspamd-securite-symbols
[Rspamd] Native mailcow Support for Securite ClamAV Signatures
2023-07-12 08:27:20 +02:00
SnailShea b903cf3888 Fixes several instances of missing </span>, extra role='tabpanel' and misspelled 'collapse' 2023-07-11 19:00:05 -04: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
Patrick Schult cf239dd6b2 Merge pull request #5215 from goodygh/5136-fix-logger-error-handling
[web] logger pdo exception handling workaround
2023-07-10 10:31:38 +02:00
Patrick Schult a0723f60d2 Merge pull request #5221 from mailcow/fix/dot-stuffing-bcc
[Rspamd] add dot-stuffing to bcc forwarding
2023-07-10 10:07:31 +02:00
Patrick Schult da8e496430 Merge pull request #5310 from mailcow/feat/ha-pubsub
[Dockerapi] add redis pubsub handler for broadcasting requests
2023-07-10 10:05:07 +02:00
Patrick Schult 722134e474 Merge pull request #5312 from mailcow/fix/ui-logs
[Web] fix loading rspamd-history
2023-07-10 10:03:29 +02:00
FreddleSpl0it cb1a11e551 [Web] fix rspamd-history 2023-07-10 09:35:51 +02:00
Patrick Schult 8984509f58 Merge pull request #5213 from mailcow/feat/cors
[Web] add cors to json_api
2023-07-07 14:13:09 +02:00
FreddleSpl0it 0f0d43b253 [Dockerapi] add missing import os 2023-07-07 11:32:28 +02:00
FreddleSpl0it 0f6956572e [Web] add CLUSTERMODE environment variable 2023-07-07 09:58:51 +02:00
Niklas Meyer 29892dc694 Merge pull request #5262 from mailcow/fix-5252
Rspamd returns 401 on unsuccesful logins
2023-06-27 11:16:34 +02:00
Niklas Meyer 14265f3de8 Merge pull request #5263 from mailcow:update-api
[API] Update swagger version to 5.1.0
2023-06-27 10:41:24 +02:00
Niklas Meyer 0863bffdd2 Merge pull request #5283 from superpuffin:master
Update nextcloud heper script to disable SMTP TLS host verification
2023-06-27 10:40:06 +02:00
Niklas Meyer 3b748a30cc Merge pull request #5284 from mailcow:renovate/nextcloud-server-27.x
Update dependency nextcloud/server to v27
2023-06-27 10:39:08 +02:00
DerLinkman 5619175108 Upate SOGo to 5.8.4 2023-06-27 10:36:53 +02:00
DerLinkman 6e9c024b3c Changed weight to score for CLAMD_SPAM 2023-06-27 10:28:52 +02:00
DerLinkman 8cd4ae1e34 Improved Scores 2023-06-23 16:19:37 +02:00
DerLinkman 689856b186 New Symbols defined for Security ClamAV DBs 2023-06-23 16:13:25 +02:00
DerLinkman 7b645303d6 Added Colorful Outputs for the Spamhaus info in PF 2023-06-23 15:54:49 +02:00
DerLinkman 408381bddb Update Postfix image to 1.69 + improvements 2023-06-23 15:48:13 +02:00
DerLinkman 380cdab6fc Removed dnsbl from main.cf 2023-06-23 14:26:17 +02:00
DerLinkman 03b7a8d639 Implemented Postfix Blocklist generation 2023-06-23 14:25:07 +02:00
DerLinkman bf6a61fa2d Small corrections to update/generate.sh 2023-06-23 14:20:06 +02:00
DerLinkman 1de47072f8 Added DQS Values to update.sh/generate + check of variable 2023-06-23 12:26:57 +02:00
Peter c0c46b7cf5 [API] Update swagger version 2023-06-19 21:35:10 +02:00
Peter 42a91af7ac [API] Update swagger version 2023-06-15 19:20:09 +02:00
renovate[bot] 6e1ee638ff Update dependency nextcloud/server to v27
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-06-13 14:02:53 +00:00
Yorgos Bos 61c8afa088 Fix smtp settings for nextcloud v26 2023-06-13 10:54:42 +02:00
renovate[bot] c873a14127 Update thollander/actions-comment-pull-request action to v2.4.0 (#5280)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-06-12 17:12:59 +02:00
FreddleSpl0it 06cce79806 [Dockerapi] add pubsub handler for broadcasting in ha setup 2023-06-12 16:37:48 +02:00
DerLinkman 0927c5df57 Fixed small typo in update.sh 2023-06-01 15:27:00 +02:00
Niklas Meyer e691d2c782 Merge pull request #5266 from mailcow/staging
[Dovecot] remove pass return in Dovecot lua auth
2023-05-30 16:57:10 +02:00
FreddleSpl0it 67510adb9e [Dovecot] remove pass return in Dovecot lua auth 2023-05-30 16:47:03 +02:00
Niklas Meyer 490d553dfc Merge pull request #5264 from mailcow/staging
2023-05a
2023-05-30 16:23:26 +02:00
DerLinkman 70aab7568e Changed maintainers to tinc (Dockerfiles) 2023-05-30 16:20:35 +02:00
DerLinkman f82aba3e26 [Dovecot] Update to 1.24 2023-05-30 16:18:14 +02:00
FreddleSpl0it f80940efdc [Dovecot] remove pass return in Dovecot lua auth 2023-05-30 09:09:41 +02:00
Peter 6f875398c0 [API] Update swagger version 2023-05-28 23:29:58 +02:00
Peter 7a582afbdc Rspamd returns 401 on unsuccesful logins 2023-05-28 22:43:26 +02:00
renovate[bot] 38cd376228 Update dependency nextcloud/server to v26.0.2 (#5254)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-05-28 15:48:28 +02:00
Niklas Meyer 74bcec45f1 Merge pull request #5250 from mailcow/staging
2023-05
2023-05-25 16:30:16 +02:00
Niklas Meyer 9700b3251f Merge pull request #5214 from mailcow/feat/gh_actions_postscreen
Add GitHub action update_postscreen_access_list.yml
2023-05-25 15:40:20 +02:00
Niklas Meyer 88b8d50cd5 Merge pull request #4028 from Daniel15/patch-2
Enable maildir_very_dirty_syncs by default
2023-05-24 11:00:38 +02:00
DerLinkman 55b0191050 [PHP] Update to 1.84 2023-05-23 10:46:21 +02:00
Peter 33c97fb318 change domain for docs 2023-05-10 20:32:38 +02:00
Niklas Meyer 23d33ad5a8 Merge pull request #5231 from mailcow/renovate/alpine-3.x
Update alpine Docker tag to v3.18
2023-05-10 08:58:47 +02:00
renovate[bot] bd6c98047a Update alpine Docker tag to v3.18
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-05-10 01:50:21 +00:00
Patrick Schult 73d6a29ae1 Merge pull request #5205 from mailcow/clean_sasl_log
Clean up old entries from sasl_log
2023-05-09 09:49:40 +02:00
Patrick Schult 173e39c859 Merge pull request #5200 from mailcow/fix/delete-sender-acl
[Web] Fix deleting sender_acl when mbox is deleted
2023-05-08 16:35:42 +02:00
Patrick Schult c0745c5cde Merge pull request #5197 from mailcow/fix/bcc-validation
[Web] Fix BCC validation
2023-05-08 16:32:12 +02:00
Patrick Schult 1a6f93327e Merge pull request #5203 from mailcow/feat/bad_asn
Add IP Connect Inc to bad_asn.map
2023-05-08 16:01:44 +02:00
Patrick Schult 3c68a53170 Merge pull request #5201 from mailcow/fix/sieve-print
[Dockerapi] Fix typo in dockerapi sieve print
2023-05-08 16:00:22 +02:00
Patrick Schult e38c27ed67 Merge pull request #5211 from goodygh/5175-fix-mobileconfig-redirect
[web] Fix typo in mobileconfig redirect
2023-05-08 15:55:50 +02:00
Patrick Schult 8eaf8bbbde Merge pull request #5220 from mailcow/fix/bcc-selectpicker
[Web] fix bcc localdest selectpicker
2023-05-08 15:53:53 +02:00
Patrick Schult e015c7dbca Merge pull request #5202 from mailcow/feat/user-acl-tabs
[Web] hide user tabs if acl is missing
2023-05-08 15:48:52 +02:00
Patrick Schult 58452abcdf Merge pull request #5204 from mailcow/fix/rspamd-table
[Web] fix rspamd table on sm devices
2023-05-08 15:43:58 +02:00
Patrick Schult 2cbf0da137 Merge pull request #5198 from mailcow/fix/sorting-tla
[Web] Fix temporary email aliases sorting
2023-05-08 15:29:32 +02:00
FreddleSpl0it f295b8cd91 [Rspamd] add domain wide footer 2023-05-08 12:55:38 +02:00
FreddleSpl0it 97a492b891 [Rspamd] add dot-stuffing to bcc forwarding 2023-05-03 15:04:09 +02:00
FreddleSpl0it aabcd10539 [Web] fix bcc localdest selectpicker 2023-05-03 09:59:49 +02:00
milkmaker ee607dc3cc Translations update from Weblate (#5218)
* [Web] Updated lang.en-gb.json

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Peter <magic@kthx.at>
2023-05-02 18:29:38 +02:00
DerLinkman 1265302a8e [DockerAPI] Update to 2.04 2023-05-02 18:11:59 +02:00
DerLinkman b5acf56e20 Added Platform Information on Status Page 2023-05-02 18:11:10 +02:00
goodygh 9752313d24 logger pdo exception handling workaround 2023-04-29 02:39:04 +02:00
FreddleSpl0it fe4a418af4 [Web] fix rspamd table scan_time on sm devices 2023-04-27 10:45:11 +02:00
Peter e5f03e8526 Update update_postscreen_whitelist.sh 2023-04-26 18:44:35 +02:00
Peter fb60c4a150 Add update_postscreen_access_list.yml 2023-04-26 18:43:54 +02:00
FreddleSpl0it 6b82284a41 [Web] cors - add check if origin is valid 2023-04-26 11:19:50 +02:00
FreddleSpl0it 192f67cd41 [Web] add cors to json_api 2023-04-26 10:46:07 +02:00
goodygh fd203abd47 Fix typo in mobileconfig redirect 2023-04-25 22:11:04 +02:00
milkmaker 6b65f0fc74 [Web] Updated lang.ru-ru.json (#5210)
Co-authored-by: Vakhtang <vakhtang.g.st@gmail.com>
2023-04-25 20:59:05 +02:00
Michael Kuron 856b3b62f2 Clean up old sasl_log entries 2023-04-22 14:16:42 +02:00
FreddleSpl0it 0372a2150d [Web] fix rspamd table on sm devices 2023-04-21 20:14:43 +02:00
Peter f3322c0577 Add IP Connect Inc 2023-04-21 19:43:20 +02:00
FreddleSpl0it c2bcc4e086 [Web] hide user tabs if acl is missing 2023-04-21 17:03:40 +02:00
FreddleSpl0it 6e79c48640 [Dockerapi] Fix typo in dockerapi sieve print 2023-04-21 16:15:16 +02:00
FreddleSpl0it d7dfa95e1b [Web] Fix deleting sender_acl when mbox is deleted 2023-04-21 13:47:13 +02:00
FreddleSpl0it cf1cc24e33 [Web] Fix temporary email aliases sorting 2023-04-21 12:26:50 +02:00
FreddleSpl0it 6824a5650f [Web] Fix BCC validation 2023-04-21 11:21:43 +02:00
Niklas Meyer 73570cc8b5 Merge pull request #5196 from ewong012/staging 2023-04-21 08:14:24 +02:00
Ethan Wong 959dcb9980 [Update.sh] Fix install docs link
Old link returns 404.
2023-04-20 13:52:46 -07:00
Patrick Schult 8f28666916 Merge pull request #5195 from mailcow/staging
2023-04b
2023-04-20 16:49:17 +02:00
Patrick Schult 3eaa5a626c Merge pull request #5187 from mailcow/fix-5185
Nextcloud helperscript - redo PHP check
2023-04-20 14:20:03 +02:00
Patrick Schult 8c79056a94 Merge pull request #5194 from mailcow/renovate/nextcloud-server-26.x
Update dependency nextcloud/server to v26.0.1
2023-04-20 14:19:19 +02:00
Patrick Schult ed076dc23e Merge pull request #5186 from goodygh/datatables_sorting
[Web] Datatables sorting
2023-04-20 13:50:57 +02:00
FreddleSpl0it be2286c11c [Dockerapi] fix maildir cleanup for domains 2023-04-20 13:41:11 +02:00
renovate[bot] 0e24c3d300 Update dependency nextcloud/server to v26.0.1
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-04-20 11:36:01 +00:00
FreddleSpl0it e1d8df6580 [Web] check mailbox before replacing sogo_static_view 2023-04-20 13:20:51 +02:00
Patrick Schult 04a08a7d69 Merge pull request #5193 from mailcow/feat/update-sogo
[SOGo] update sogo 5.8.2.20230419
2023-04-20 12:32:42 +02:00
FreddleSpl0it 3c0c8aa01f [SOGo] update sogo 5.8.2.20230419 2023-04-20 12:07:21 +02:00
Patrick Schult 026b278357 Merge pull request #5183 from mailcow/fix/add-mbox-performance
[Web] optimizing mailbox add/edit/delete performance
2023-04-20 11:34:41 +02:00
FreddleSpl0it 4121509ceb [Web] optimizing update_sogo_static_view function 2023-04-20 11:28:59 +02:00
Patrick Schult 00ac61f0a4 Merge pull request #5184 from bdwebnet/fix/ui-allowed-protocols
Added dropdown divider to "allowed protocols" selection on mailbox page
2023-04-19 17:31:05 +02:00
Patrick Schult 4bb0dbb2f7 Merge pull request #5191 from shiz0/patch-1
Fix Typo
2023-04-19 17:26:54 +02:00
Patrick Schult 13b6df74af Merge pull request #5174 from bdwebnet/staging
Fix error  "Deprecated: Using ${var} in strings is deprecated, use {$…
2023-04-19 17:23:26 +02:00
FreddleSpl0it 5c025bf865 [Rspamd] rollback to 3.4 2023-04-19 17:03:04 +02:00
Hannes Happle 20fc9eaf84 Fix Typo 2023-04-16 14:32:44 +02:00
Peter 22a0479fab Redo the PHP check grep 2023-04-13 21:11:40 +02:00
goodygh 3510d5617d Fix sorting for active relayhost 2023-04-13 19:18:04 +02:00
goodygh 236d627fbd Fix sorting for active transport map 2023-04-13 19:14:20 +02:00
goodygh 99739eada0 Fix sorting for active fowrardinghoststable 2023-04-13 19:01:03 +02:00
goodygh 7bfef57894 Fix sorting for active and tla on admins 2023-04-13 18:54:59 +02:00
goodygh d9dfe15253 Fix sorting for active and tla on domain-admins 2023-04-13 18:54:08 +02:00
goodygh 3fe8aaa719 Fix sorting for active tls-policy-map 2023-04-13 18:14:18 +02:00
goodygh 78a8fac6af Fix sorting for active bcc-map and recipient-map 2023-04-13 18:10:21 +02:00
bd 6986e7758f Added dropdown divider to "allowed protocols" selection on mailbox page 2023-04-13 17:33:28 +02:00
BD b4a9df76b8 Merge branch 'mailcow:staging' into staging 2023-04-13 17:22:13 +02:00
FreddleSpl0it d9d958356a [Web] optimizing update_sogo_static_view function 2023-04-13 14:35:55 +02:00
goodygh 96f954a4e2 Fix sorting for active syncjobs 2023-04-12 00:36:46 +02:00
goodygh 44585e1c15 Fix sorting datatable in domain aliases 2023-04-12 00:23:53 +02:00
goodygh c737ff4180 Fix sorting datatable in aliases 2023-04-12 00:21:27 +02:00
goodygh 025279009d Fix sorting for active resources 2023-04-12 00:17:41 +02:00
goodygh a9dc13d567 Fix sorting datatable in mailbox templates 2023-04-12 00:15:16 +02:00
goodygh c3ed01c9b5 Fix sorting for active mailboxes 2023-04-11 23:49:50 +02:00
goodygh bd0b4a521e Fix sorting datatable in domain templates 2023-04-11 23:42:43 +02:00
goodygh 800a0ace71 Fix sorting for active domain in domains table 2023-04-11 23:19:56 +02:00
goodygh db97869472 Datatable hide sorting value 2023-04-11 23:18:13 +02:00
milkmaker f681fcf154 [Web] Updated lang.cs-cz.json (#5177)
Co-authored-by: utaxiu <kontakt@malyjakub.cz>
2023-04-11 17:38:39 +02:00
Patrick Schult db1b5956fc Merge pull request #5133 from FELDSAM-INC/feldsam/bs5-related-fixes
BS5 related fixes
2023-04-11 06:35:41 +02:00
BD bdb07061ed Fix error "Deprecated: Using ${var} in strings is deprecated, use {$var} instead in /web/sogo-auth.php on line 63" 2023-04-08 17:29:34 +02:00
Kristian Feldsam 80dacc015a [web] fixed mailbox/user settings buttons styling
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>

[web] fixed mailbox/user settings buttons styling

Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-03-31 13:19:20 +02:00
Kristian Feldsam e5e6418be8 [web] fixed tooltips in ajax loaded alias table
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-03-20 01:38:34 +01:00
Kristian Feldsam 6507b53bbb [web] fix mailbox badge height
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-03-20 01:38:31 +01:00
Kristian Feldsam 2eafd89412 [web] apple config app passwords enhancements + translations
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-03-18 16:29:11 +01: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
Daniel Lo Nigro 1606658cb1 Add missing spaces 2021-08-28 20:02:39 -07:00
Daniel Lo Nigro 54ba66733e Enable maildir_very_dirty_syncs rather than just adding comment 2021-05-02 16:39:26 -07:00
Daniel Lo Nigro f6847e6f8c Add comment about maildir_very_dirty_syncs to dovecot.conf 2021-03-13 10:46:32 -08:00
455 changed files with 43802 additions and 6494 deletions
+10
View File
@@ -62,6 +62,16 @@ body:
- nightly - nightly
validations: validations:
required: true 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 - type: input
attributes: attributes:
label: "Operating System:" label: "Operating System:"
+6 -3
View File
@@ -1,8 +1,11 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: ❓ Community-driven support - name: ❓ Community-driven support (Free)
url: https://mailcow.github.io/mailcow-dockerized-docs/#get-support url: https://docs.mailcow.email/#get-support
about: Please use the community forum for questions or assistance 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 - 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. about: Please give us appropriate time to verify, respond and fix before disclosure.
+2 -2
View File
@@ -12,7 +12,7 @@
"baseBranches": ["staging"], "baseBranches": ["staging"],
"enabledManagers": ["github-actions", "regex", "docker-compose"], "enabledManagers": ["github-actions", "regex", "docker-compose"],
"ignorePaths": [ "ignorePaths": [
"data\/web\/inc\/lib\/vendor\/matthiasmullie\/minify\/**" "data\/web\/inc\/lib\/vendor\/**"
], ],
"regexManagers": [ "regexManagers": [
{ {
@@ -24,7 +24,7 @@
{ {
"fileMatch": ["(^|/)Dockerfile[^/]*$"], "fileMatch": ["(^|/)Dockerfile[^/]*$"],
"matchStrings": [ "matchStrings": [
"#\\srenovate:\\sdatasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?\\s(ENV|ARG) .*?_VERSION=(?<currentValue>.*)\\s" "#\\srenovate:\\sdatasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?( extractVersion=(?<extractVersion>.*?))?\\s(ENV|ARG) .*?_VERSION=(?<currentValue>.*)\\s"
] ]
} }
] ]
@@ -10,7 +10,7 @@ jobs:
if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging
steps: steps:
- name: Send message - name: Send message
uses: thollander/actions-comment-pull-request@v2.3.1 uses: thollander/actions-comment-pull-request@v2.4.3
with: with:
GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }} GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }}
message: | message: |
@@ -14,7 +14,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Mark/Close Stale Issues and Pull Requests 🗑️ - name: Mark/Close Stale Issues and Pull Requests 🗑️
uses: actions/stale@v8.0.0 uses: actions/stale@v9.0.0
with: with:
repo-token: ${{ secrets.STALE_ACTION_PAT }} repo-token: ${{ secrets.STALE_ACTION_PAT }}
days-before-stale: 60 days-before-stale: 60
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
- "watchdog-mailcow" - "watchdog-mailcow"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup Docker - name: Setup Docker
run: | run: |
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
+1 -1
View File
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Run the Action - name: Run the Action
+6 -5
View File
@@ -11,24 +11,25 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_USERNAME }} username: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_USERNAME }}
password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }} password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64
file: data/Dockerfiles/backup/Dockerfile file: data/Dockerfiles/backup/Dockerfile
push: true push: true
tags: mailcow/backup:latest tags: mailcow/backup:latest
@@ -0,0 +1,39 @@
name: Update postscreen_access.cidr
on:
schedule:
# Monthly
- cron: "0 0 1 * *"
workflow_dispatch: # Allow to run workflow manually
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
Update-postscreen_access_cidr:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Generate postscreen_access.cidr
run: |
bash helper-scripts/update_postscreen_whitelist.sh
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
commit-message: update postscreen_access.cidr
committer: milkmaker <milkmaker@mailcow.de>
author: milkmaker <milkmaker@mailcow.de>
signoff: false
branch: update/postscreen_access.cidr
base: staging
delete-branch: true
add-paths: |
data/conf/postfix/postscreen_access.cidr
title: '[Postfix] update postscreen_access.cidr'
body: |
This PR updates the postscreen_access.cidr using GitHub Actions and [helper-scripts/update_postscreen_whitelist.sh](https://github.com/mailcow/mailcow-dockerized/blob/master/helper-scripts/update_postscreen_whitelist.sh)
+4
View File
@@ -13,6 +13,7 @@ data/conf/dovecot/acl_anyone
data/conf/dovecot/dovecot-master.passwd data/conf/dovecot/dovecot-master.passwd
data/conf/dovecot/dovecot-master.userdb data/conf/dovecot/dovecot-master.userdb
data/conf/dovecot/extra.conf data/conf/dovecot/extra.conf
data/conf/dovecot/mail_replica.conf
data/conf/dovecot/global_sieve_* data/conf/dovecot/global_sieve_*
data/conf/dovecot/last_login data/conf/dovecot/last_login
data/conf/dovecot/lua data/conf/dovecot/lua
@@ -36,6 +37,8 @@ data/conf/postfix/extra.cf
data/conf/postfix/sni.map data/conf/postfix/sni.map
data/conf/postfix/sni.map.db data/conf/postfix/sni.map.db
data/conf/postfix/sql data/conf/postfix/sql
data/conf/postfix/dns_blocklists.cf
data/conf/postfix/dnsbl_reply.map
data/conf/rspamd/custom/* data/conf/rspamd/custom/*
data/conf/rspamd/local.d/* data/conf/rspamd/local.d/*
data/conf/rspamd/override.d/* data/conf/rspamd/override.d/*
@@ -66,3 +69,4 @@ rebuild-images.sh
refresh_images.sh refresh_images.sh
update_diffs/ update_diffs/
create_cold_standby.sh create_cold_standby.sh
!data/conf/nginx/mailcow_auth.conf
+33 -3
View File
@@ -1,9 +1,39 @@
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. 1. Read your logs; follow them to see what the reason for your problem is.
2. Follow the leads given to you in your logfiles and start investigating. 2. Follow the leads given to you in your logfiles and start investigating.
3. Restarting the troubled service or the whole stack to see if the problem persists. 3. Restarting the troubled service or the whole stack to see if the problem persists.
4. Read the [documentation](https://mailcow.github.io/mailcow-dockerized-docs/) of the troubled service and search its bugtracker for your problem. 4. Read the [documentation](https://docs.mailcow.email/) of the troubled service and search its bugtracker for your problem.
5. Search our [issues](https://github.com/mailcow/mailcow-dockerized/issues) for your problem. 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. 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://mailcow.github.io/mailcow-dockerized-docs/#community-support-and-chat). 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.
+7 -3
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/) [![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) [![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? ## Want to support mailcow?
@@ -13,7 +15,7 @@ Or just spread the word: moo.
## Info, documentation and support ## Info, documentation and support
Please see [the official documentation](https://mailcow.github.io/mailcow-dockerized-docs/) for installation and support instructions. 🐄 Please see [the official documentation](https://docs.mailcow.email/) for installation and support instructions. 🐄
🐛 **If you found a critical security issue, please mail us to [info at servercow.de](mailto:info@servercow.de).** 🐛 **If you found a critical security issue, please mail us to [info at servercow.de](mailto:info@servercow.de).**
@@ -25,7 +27,9 @@ Please see [the official documentation](https://mailcow.github.io/mailcow-docker
[Telegram mailcow Off-Topic channel](https://t.me/mailcowOfftopic) [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. 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. The project is managed and maintained by The Infrastructure Company GmbH.
Originated from @andryyy (André) Originated from @andryyy (André)
+3 -2
View File
@@ -1,7 +1,8 @@
FROM alpine:3.17 FROM alpine:3.18
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
ARG PIP_BREAK_SYSTEM_PACKAGES=1
RUN apk upgrade --no-cache \ RUN apk upgrade --no-cache \
&& apk add --update --no-cache \ && apk add --update --no-cache \
bash \ bash \
+7 -3
View File
@@ -1,12 +1,14 @@
FROM clamav/clamav:1.0.1-1_base FROM alpine:3.19
LABEL maintainer "André Peters <andre.peters@servercow.de>" LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
RUN apk upgrade --no-cache \ RUN apk upgrade --no-cache \
&& apk add --update --no-cache \ && apk add --update --no-cache \
rsync \ rsync \
clamav \
bind-tools \ bind-tools \
bash bash \
tini
# init # init
COPY clamd.sh /clamd.sh COPY clamd.sh /clamd.sh
@@ -14,7 +16,9 @@ RUN chmod +x /sbin/tini
# healthcheck # healthcheck
COPY healthcheck.sh /healthcheck.sh COPY healthcheck.sh /healthcheck.sh
COPY clamdcheck.sh /usr/local/bin
RUN chmod +x /healthcheck.sh RUN chmod +x /healthcheck.sh
RUN chmod +x /usr/local/bin/clamdcheck.sh
HEALTHCHECK --start-period=6m CMD "/healthcheck.sh" HEALTHCHECK --start-period=6m CMD "/healthcheck.sh"
ENTRYPOINT [] ENTRYPOINT []
+14
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
+10 -5
View File
@@ -1,7 +1,8 @@
FROM alpine:3.17 FROM alpine:3.19
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
ARG PIP_BREAK_SYSTEM_PACKAGES=1
WORKDIR /app WORKDIR /app
RUN apk add --update --no-cache python3 \ RUN apk add --update --no-cache python3 \
@@ -9,14 +10,18 @@ RUN apk add --update --no-cache python3 \
openssl \ openssl \
tzdata \ tzdata \
py3-psutil \ py3-psutil \
py3-redis \
py3-async-timeout \
&& pip3 install --upgrade pip \ && pip3 install --upgrade pip \
fastapi \ fastapi \
uvicorn \ uvicorn \
aiodocker \ aiodocker \
docker \ docker
redis RUN mkdir /app/modules
COPY docker-entrypoint.sh /app/ COPY docker-entrypoint.sh /app/
COPY dockerapi.py /app/ COPY main.py /app/main.py
COPY modules/ /app/modules/
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"] ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]
CMD exec python main.py
@@ -6,4 +6,4 @@
-subj /CN=dockerapi/O=mailcow \ -subj /CN=dockerapi/O=mailcow \
-addext subjectAltName=DNS:dockerapi` -addext subjectAltName=DNS:dockerapi`
`uvicorn --host 0.0.0.0 --port 443 --ssl-certfile=/app/dockerapi_cert.pem --ssl-keyfile=/app/dockerapi_key.pem dockerapi:app` exec "$@"
-544
View File
@@ -1,544 +0,0 @@
from fastapi import FastAPI, Response, Request
import aiodocker
import docker
import psutil
import sys
import re
import time
import os
import json
import asyncio
import redis
from datetime import datetime
import logging
from logging.config import dictConfig
log_config = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(levelprefix)s %(asctime)s %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
},
},
"loggers": {
"api-logger": {"handlers": ["default"], "level": "INFO"},
},
}
dictConfig(log_config)
containerIds_to_update = []
host_stats_isUpdating = False
app = FastAPI()
logger = logging.getLogger('api-logger')
@app.get("/host/stats")
async def get_host_update_stats():
global host_stats_isUpdating
if host_stats_isUpdating == False:
asyncio.create_task(get_host_stats())
host_stats_isUpdating = True
while True:
if redis_client.exists('host_stats'):
break
await asyncio.sleep(1.5)
stats = json.loads(redis_client.get('host_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
@app.get("/containers/{container_id}/json")
async def get_container(container_id : str):
if container_id and container_id.isalnum():
try:
for container in (await async_docker_client.containers.list()):
if container._id == container_id:
container_info = await container.show()
return Response(content=json.dumps(container_info, indent=4), media_type="application/json")
res = {
"type": "danger",
"msg": "no container found"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
except Exception as e:
res = {
"type": "danger",
"msg": str(e)
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
"type": "danger",
"msg": "no or invalid id defined"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
@app.get("/containers/json")
async def get_containers():
containers = {}
try:
for container in (await async_docker_client.containers.list()):
container_info = await container.show()
containers.update({container_info['Id']: container_info})
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
except Exception as e:
res = {
"type": "danger",
"msg": str(e)
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
@app.post("/containers/{container_id}/{post_action}")
async def post_containers(container_id : str, post_action : str, request: Request):
try :
request_json = await request.json()
except Exception as err:
request_json = {}
if container_id and container_id.isalnum() and post_action:
try:
"""Dispatch container_post api call"""
if post_action == 'exec':
if not request_json or not 'cmd' in request_json:
res = {
"type": "danger",
"msg": "cmd is missing"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
if not request_json or not 'task' in request_json:
res = {
"type": "danger",
"msg": "task is missing"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
api_call_method_name = '__'.join(['container_post', str(post_action), str(request_json['cmd']), str(request_json['task']) ])
else:
api_call_method_name = '__'.join(['container_post', str(post_action) ])
docker_utils = DockerUtils(sync_docker_client)
api_call_method = getattr(docker_utils, api_call_method_name, lambda container_id: Response(content=json.dumps({'type': 'danger', 'msg':'container_post - unknown api call' }, indent=4), media_type="application/json"))
logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id))
return api_call_method(container_id, request_json)
except Exception as e:
logger.error("error - container_post: %s" % str(e))
res = {
"type": "danger",
"msg": str(e)
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
"type": "danger",
"msg": "invalid container id or missing action"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
@app.post("/container/{container_id}/stats/update")
async def post_container_update_stats(container_id : str):
global containerIds_to_update
# start update task for container if no task is running
if container_id not in containerIds_to_update:
asyncio.create_task(get_container_stats(container_id))
containerIds_to_update.append(container_id)
while True:
if redis_client.exists(container_id + '_stats'):
break
await asyncio.sleep(1.5)
stats = json.loads(redis_client.get(container_id + '_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
class DockerUtils:
def __init__(self, docker_client):
self.docker_client = docker_client
# api call: container_post - post_action: stop
def container_post__stop(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
container.stop()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: start
def container_post__start(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
container.start()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: restart
def container_post__restart(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
container.restart()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: top
def container_post__top(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
res = { 'type': 'success', 'msg': container.top()}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: stats
def container_post__stats(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
for stat in container.stats(decode=True, stream=True):
res = { 'type': 'success', 'msg': stat}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: mailq - task: delete
def container_post__exec__mailq__delete(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-d %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids));
for container in self.docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: mailq - task: hold
def container_post__exec__mailq__hold(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-h %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids));
for container in self.docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: mailq - task: cat
def container_post__exec__mailq__cat(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
sanitized_string = str(' '.join(filtered_qids));
for container in self.docker_client.containers.list(filters={"id": container_id}):
postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
if not postcat_return:
postcat_return = 'err: invalid'
return exec_run_handler('utf8_text_only', postcat_return)
# api call: container_post - post_action: exec - cmd: mailq - task: unhold
def container_post__exec__mailq__unhold(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-H %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids));
for container in self.docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: mailq - task: deliver
def container_post__exec__mailq__deliver(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-i %s' % i for i in filtered_qids]
for container in self.docker_client.containers.list(filters={"id": container_id}):
for i in flagged_qids:
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
# todo: check each exit code
res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: mailq - task: list
def container_post__exec__mailq__list(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
return exec_run_handler('utf8_text_only', mailq_return)
# api call: container_post - post_action: exec - cmd: mailq - task: flush
def container_post__exec__mailq__flush(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
return exec_run_handler('generic', postqueue_r)
# api call: container_post - post_action: exec - cmd: mailq - task: super_delete
def container_post__exec__mailq__super_delete(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
return exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: system - task: fts_rescan
def container_post__exec__system__fts_rescan(self, container_id, request_json):
if 'username' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail')
if rescan_return.exit_code == 0:
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
if 'all' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
if rescan_return.exit_code == 0:
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: system - task: df
def container_post__exec__system__df(self, container_id, request_json):
if 'dir' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
if df_return.exit_code == 0:
return df_return.output.decode('utf-8').rstrip()
else:
return "0,0,0,0,0,0"
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
def container_post__exec__system__mysql_upgrade(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
if sql_return.exit_code == 0:
matched = False
for line in sql_return.output.decode('utf-8').split("\n"):
if 'is already upgraded to' in line:
matched = True
if matched:
res = { 'type': 'success', 'msg':'mysql_upgrade: already upgraded', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
container.restart()
res = { 'type': 'warning', 'msg':'mysql_upgrade: upgrade was applied', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'error', 'msg': 'mysql_upgrade: error running command', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
def container_post__exec__system__mysql_tzinfo_to_sql(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
if sql_return.exit_code == 0:
res = { 'type': 'info', 'msg': 'mysql_tzinfo_to_sql: command completed successfully', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'error', 'msg': 'mysql_tzinfo_to_sql: error running command', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
def container_post__exec__reload__dovecot(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
return exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: reload - task: postfix
def container_post__exec__reload__postfix(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
return exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: reload - task: nginx
def container_post__exec__reload__nginx(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
return exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: sieve - task: list
def container_post__exec__sieve__list(self, container_id, request_json):
if 'username' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"])
return exec_run_handler('utf8_text_only', sieve_return)
# api call: container_post - post_action: exec - cmd: sieve - task: print
def container_post__exec__sieve__print(self, container_id, request_json):
if 'username' in request.json and 'script_name' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
sieve_return = container.exec_run(cmd)
return exec_run_handler('utf8_text_only', sieve_return)
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
def container_post__exec__maildir__cleanup(self, container_id, request_json):
if 'maildir' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
sane_name = re.sub(r'\W+', '', request_json['maildir'])
vmail_name = request_json['maildir'].replace("'", "'\\''")
index_name = request_json['maildir'].split("/")
index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].replace("'", "'\\''")
cmd_vmail = "if [[ -d '/var/vmail/" + vmail_name + "' ]]; then /bin/mv '/var/vmail/" + vmail_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"
cmd_vmail_index = "if [[ -d '/var/vmail_index/" + index_name + "' ]]; then /bin/mv '/var/vmail_index/" + index_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "_index'; fi"
cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index]
maildir_cleanup = container.exec_run(cmd, user='vmail')
return exec_run_handler('generic', maildir_cleanup)
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
def container_post__exec__rspamd__worker_password(self, container_id, request_json):
if 'raw' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
cmd = "/usr/bin/rspamadm pw -e -p '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
cmd_response = exec_cmd_container(container, cmd, user="_rspamd")
matched = False
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())
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 = exec_cmd_container(container, cmd, user="_rspamd")
if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response:
container.restart()
matched = True
if matched:
res = { 'type': 'success', 'msg': 'command completed successfully' }
logger.info('success changing Rspamd password')
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
logger.error('failed changing Rspamd password')
res = { 'type': 'danger', 'msg': 'command did not complete' }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
def recv_socket_data(c_socket, timeout):
c_socket.setblocking(0)
total_data=[]
data=''
begin=time.time()
while True:
if total_data and time.time()-begin > timeout:
break
elif time.time()-begin > timeout*2:
break
try:
data = c_socket.recv(8192)
if data:
total_data.append(data.decode('utf-8'))
#change the beginning time for measurement
begin=time.time()
else:
#sleep for sometime to indicate a gap
time.sleep(0.1)
break
except:
pass
return ''.join(total_data)
try :
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
if not cmd.endswith("\n"):
cmd = cmd + "\n"
socket.send(cmd.encode('utf-8'))
data = recv_socket_data(socket, timeout)
socket.close()
return data
except Exception as e:
logger.error("error - exec_cmd_container: %s" % str(e))
traceback.print_exc(file=sys.stdout)
def exec_run_handler(type, output):
if type == 'generic':
if output.exit_code == 0:
res = { 'type': 'success', 'msg': 'command completed successfully' }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'danger', 'msg': 'command failed: ' + output.output.decode('utf-8') }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
if type == 'utf8_text_only':
return Response(content=output.output.decode('utf-8'), media_type="text/plain")
async def get_host_stats(wait=5):
global host_stats_isUpdating
try:
system_time = datetime.now()
host_stats = {
"cpu": {
"cores": psutil.cpu_count(),
"usage": psutil.cpu_percent()
},
"memory": {
"total": psutil.virtual_memory().total,
"usage": psutil.virtual_memory().percent,
"swap": psutil.swap_memory()
},
"uptime": time.time() - psutil.boot_time(),
"system_time": system_time.strftime("%d.%m.%Y %H:%M:%S")
}
redis_client.set('host_stats', json.dumps(host_stats), ex=10)
except Exception as e:
res = {
"type": "danger",
"msg": str(e)
}
await asyncio.sleep(wait)
host_stats_isUpdating = False
async def get_container_stats(container_id, wait=5, stop=False):
global containerIds_to_update
if container_id and container_id.isalnum():
try:
for container in (await async_docker_client.containers.list()):
if container._id == container_id:
res = await container.stats(stream=False)
if redis_client.exists(container_id + '_stats'):
stats = json.loads(redis_client.get(container_id + '_stats'))
else:
stats = []
stats.append(res[0])
if len(stats) > 3:
del stats[0]
redis_client.set(container_id + '_stats', json.dumps(stats), ex=60)
except Exception as e:
res = {
"type": "danger",
"msg": str(e)
}
else:
res = {
"type": "danger",
"msg": "no or invalid id defined"
}
await asyncio.sleep(wait)
if stop == True:
# update task was called second time, stop
containerIds_to_update.remove(container_id)
else:
# call update task a second time
await get_container_stats(container_id, wait=0, stop=True)
if os.environ['REDIS_SLAVEOF_IP'] != "":
redis_client = redis.Redis(host=os.environ['REDIS_SLAVEOF_IP'], port=os.environ['REDIS_SLAVEOF_PORT'], db=0)
else:
redis_client = redis.Redis(host='redis-mailcow', port=6379, db=0)
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')
logger.info('DockerApi started')
+261
View File
@@ -0,0 +1,261 @@
import os
import sys
import uvicorn
import json
import uuid
import async_timeout
import asyncio
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
@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")
async def get_host_update_stats():
global dockerapi
if dockerapi.host_stats_isUpdating == False:
asyncio.create_task(dockerapi.get_host_stats())
dockerapi.host_stats_isUpdating = True
while True:
if await dockerapi.redis_client.exists('host_stats'):
break
await asyncio.sleep(1.5)
stats = json.loads(await dockerapi.redis_client.get('host_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
@app.get("/containers/{container_id}/json")
async def get_container(container_id : str):
global dockerapi
if container_id and container_id.isalnum():
try:
for container in (await dockerapi.async_docker_client.containers.list()):
if container._id == container_id:
container_info = await container.show()
return Response(content=json.dumps(container_info, indent=4), media_type="application/json")
res = {
"type": "danger",
"msg": "no container found"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
except Exception as e:
res = {
"type": "danger",
"msg": str(e)
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
"type": "danger",
"msg": "no or invalid id defined"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
@app.get("/containers/json")
async def get_containers():
global dockerapi
containers = {}
try:
for container in (await dockerapi.async_docker_client.containers.list()):
container_info = await container.show()
containers.update({container_info['Id']: container_info})
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
except Exception as e:
res = {
"type": "danger",
"msg": str(e)
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
@app.post("/containers/{container_id}/{post_action}")
async def post_containers(container_id : str, post_action : str, request: Request):
global dockerapi
try :
request_json = await request.json()
except Exception as err:
request_json = {}
if container_id and container_id.isalnum() and post_action:
try:
"""Dispatch container_post api call"""
if post_action == 'exec':
if not request_json or not 'cmd' in request_json:
res = {
"type": "danger",
"msg": "cmd is missing"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
if not request_json or not 'task' in request_json:
res = {
"type": "danger",
"msg": "task is missing"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
api_call_method_name = '__'.join(['container_post', str(post_action), str(request_json['cmd']), str(request_json['task']) ])
else:
api_call_method_name = '__'.join(['container_post', str(post_action) ])
api_call_method = getattr(dockerapi, api_call_method_name, lambda container_id: Response(content=json.dumps({'type': 'danger', 'msg':'container_post - unknown api call' }, indent=4), media_type="application/json"))
dockerapi.logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id))
return api_call_method(request_json, container_id=container_id)
except Exception as e:
dockerapi.logger.error("error - container_post: %s" % str(e))
res = {
"type": "danger",
"msg": str(e)
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
"type": "danger",
"msg": "invalid container id or missing action"
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
@app.post("/container/{container_id}/stats/update")
async def post_container_update_stats(container_id : str):
global dockerapi
# start update task for container if no task is running
if container_id not in dockerapi.containerIds_to_update:
asyncio.create_task(dockerapi.get_container_stats(container_id))
dockerapi.containerIds_to_update.append(container_id)
while True:
if await dockerapi.redis_client.exists(container_id + '_stats'):
break
await asyncio.sleep(1.5)
stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
# PubSub Handler
async def handle_pubsub_messages(channel: aioredis.client.PubSub):
global dockerapi
while True:
try:
async with async_timeout.timeout(60):
message = await channel.get_message(ignore_subscribe_messages=True, timeout=30)
if message is not None:
# Parse message
data_json = json.loads(message['data'].decode('utf-8'))
dockerapi.logger.info(f"PubSub Received - {json.dumps(data_json)}")
# Handle api_call
if 'api_call' in data_json:
# api_call: container_post
if data_json['api_call'] == "container_post":
if 'post_action' in data_json and 'container_name' in data_json:
try:
"""Dispatch container_post api call"""
request_json = {}
if data_json['post_action'] == 'exec':
if 'request' in data_json:
request_json = data_json['request']
if 'cmd' in request_json:
if 'task' in request_json:
api_call_method_name = '__'.join(['container_post', str(data_json['post_action']), str(request_json['cmd']), str(request_json['task']) ])
else:
dockerapi.logger.error("api call: task missing")
else:
dockerapi.logger.error("api call: cmd missing")
else:
dockerapi.logger.error("api call: request missing")
else:
api_call_method_name = '__'.join(['container_post', str(data_json['post_action'])])
if api_call_method_name:
api_call_method = getattr(dockerapi, api_call_method_name)
if api_call_method:
dockerapi.logger.info("api call: %s, container_name: %s" % (api_call_method_name, data_json['container_name']))
api_call_method(request_json, container_name=data_json['container_name'])
else:
dockerapi.logger.error("api call not found: %s, container_name: %s" % (api_call_method_name, data_json['container_name']))
except Exception as e:
dockerapi.logger.error("container_post: %s" % str(e))
else:
dockerapi.logger.error("api call: missing container_name, post_action or request")
else:
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
else:
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
await asyncio.sleep(0.0)
except asyncio.TimeoutError:
pass
if __name__ == '__main__':
uvicorn.run(
app,
host="0.0.0.0",
port=443,
ssl_certfile="/app/dockerapi_cert.pem",
ssl_keyfile="/app/dockerapi_key.pem",
log_level="info",
loop="none"
)
@@ -0,0 +1,487 @@
import psutil
import sys
import os
import re
import time
import json
import asyncio
import platform
from datetime import datetime
from fastapi import FastAPI, Response, Request
class DockerApi:
def __init__(self, redis_client, sync_docker_client, async_docker_client, logger):
self.redis_client = redis_client
self.sync_docker_client = sync_docker_client
self.async_docker_client = async_docker_client
self.logger = logger
self.host_stats_isUpdating = False
self.containerIds_to_update = []
# api call: container_post - post_action: stop
def container_post__stop(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
container.stop()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: start
def container_post__start(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
container.start()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: restart
def container_post__restart(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
container.restart()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: top
def container_post__top(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
res = { 'type': 'success', 'msg': container.top()}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: stats
def container_post__stats(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(all=True, filters=filters):
for stat in container.stats(decode=True, stream=True):
res = { 'type': 'success', 'msg': stat}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: mailq - task: delete
def container_post__exec__mailq__delete(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-d %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids))
for container in self.sync_docker_client.containers.list(filters=filters):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return self.exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: mailq - task: hold
def container_post__exec__mailq__hold(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-h %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids))
for container in self.sync_docker_client.containers.list(filters=filters):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return self.exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: mailq - task: cat
def container_post__exec__mailq__cat(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
sanitized_string = str(' '.join(filtered_qids))
for container in self.sync_docker_client.containers.list(filters=filters):
postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
if not postcat_return:
postcat_return = 'err: invalid'
return self.exec_run_handler('utf8_text_only', postcat_return)
# api call: container_post - post_action: exec - cmd: mailq - task: unhold
def container_post__exec__mailq__unhold(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-H %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids))
for container in self.sync_docker_client.containers.list(filters=filters):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return self.exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: mailq - task: deliver
def container_post__exec__mailq__deliver(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-i %s' % i for i in filtered_qids]
for container in self.sync_docker_client.containers.list(filters=filters):
for i in flagged_qids:
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
# todo: check each exit code
res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: mailq - task: list
def container_post__exec__mailq__list(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
return self.exec_run_handler('utf8_text_only', mailq_return)
# api call: container_post - post_action: exec - cmd: mailq - task: flush
def container_post__exec__mailq__flush(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
return self.exec_run_handler('generic', postqueue_r)
# api call: container_post - post_action: exec - cmd: mailq - task: super_delete
def container_post__exec__mailq__super_delete(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
return self.exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: system - task: fts_rescan
def container_post__exec__system__fts_rescan(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'username' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail')
if rescan_return.exit_code == 0:
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
if 'all' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
if rescan_return.exit_code == 0:
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: system - task: df
def container_post__exec__system__df(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'dir' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
if df_return.exit_code == 0:
return df_return.output.decode('utf-8').rstrip()
else:
return "0,0,0,0,0,0"
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
def container_post__exec__system__mysql_upgrade(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
if sql_return.exit_code == 0:
matched = False
for line in sql_return.output.decode('utf-8').split("\n"):
if 'is already upgraded to' in line:
matched = True
if matched:
res = { 'type': 'success', 'msg':'mysql_upgrade: already upgraded', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
container.restart()
res = { 'type': 'warning', 'msg':'mysql_upgrade: upgrade was applied', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'error', 'msg': 'mysql_upgrade: error running command', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
def container_post__exec__system__mysql_tzinfo_to_sql(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
if sql_return.exit_code == 0:
res = { 'type': 'info', 'msg': 'mysql_tzinfo_to_sql: command completed successfully', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'error', 'msg': 'mysql_tzinfo_to_sql: error running command', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
def container_post__exec__reload__dovecot(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
return self.exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: reload - task: postfix
def container_post__exec__reload__postfix(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
return self.exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: reload - task: nginx
def container_post__exec__reload__nginx(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
return self.exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: sieve - task: list
def container_post__exec__sieve__list(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'username' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"])
return self.exec_run_handler('utf8_text_only', sieve_return)
# api call: container_post - post_action: exec - cmd: sieve - task: print
def container_post__exec__sieve__print(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'username' in request_json and 'script_name' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
sieve_return = container.exec_run(cmd)
return self.exec_run_handler('utf8_text_only', sieve_return)
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
def container_post__exec__maildir__cleanup(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'maildir' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
sane_name = re.sub(r'\W+', '', request_json['maildir'])
vmail_name = request_json['maildir'].replace("'", "'\\''")
cmd_vmail = "if [[ -d '/var/vmail/" + vmail_name + "' ]]; then /bin/mv '/var/vmail/" + vmail_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"
index_name = request_json['maildir'].split("/")
if len(index_name) > 1:
index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].replace("'", "'\\''")
cmd_vmail_index = "if [[ -d '/var/vmail_index/" + index_name + "' ]]; then /bin/mv '/var/vmail_index/" + index_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "_index'; fi"
cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index]
else:
cmd = ["/bin/bash", "-c", cmd_vmail]
maildir_cleanup = container.exec_run(cmd, user='vmail')
return self.exec_run_handler('generic', maildir_cleanup)
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
def container_post__exec__rspamd__worker_password(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'raw' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
cmd = "/usr/bin/rspamadm pw -e -p '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")
matched = False
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())
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")
if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response:
container.restart()
matched = True
if matched:
res = { 'type': 'success', 'msg': 'command completed successfully' }
self.logger.info('success changing Rspamd password')
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
self.logger.error('failed changing Rspamd password')
res = { 'type': 'danger', 'msg': 'command did not complete' }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# Collect host stats
async def get_host_stats(self, wait=5):
try:
system_time = datetime.now()
host_stats = {
"cpu": {
"cores": psutil.cpu_count(),
"usage": psutil.cpu_percent()
},
"memory": {
"total": psutil.virtual_memory().total,
"usage": psutil.virtual_memory().percent,
"swap": psutil.swap_memory()
},
"uptime": time.time() - psutil.boot_time(),
"system_time": system_time.strftime("%d.%m.%Y %H:%M:%S"),
"architecture": platform.machine()
}
await self.redis_client.set('host_stats', json.dumps(host_stats), ex=10)
except Exception as e:
res = {
"type": "danger",
"msg": str(e)
}
await asyncio.sleep(wait)
self.host_stats_isUpdating = False
# Collect container stats
async def get_container_stats(self, container_id, wait=5, stop=False):
if container_id and container_id.isalnum():
try:
for container in (await self.async_docker_client.containers.list()):
if container._id == container_id:
res = await container.stats(stream=False)
if await self.redis_client.exists(container_id + '_stats'):
stats = json.loads(await self.redis_client.get(container_id + '_stats'))
else:
stats = []
stats.append(res[0])
if len(stats) > 3:
del stats[0]
await self.redis_client.set(container_id + '_stats', json.dumps(stats), ex=60)
except Exception as e:
res = {
"type": "danger",
"msg": str(e)
}
else:
res = {
"type": "danger",
"msg": "no or invalid id defined"
}
await asyncio.sleep(wait)
if stop == True:
# update task was called second time, stop
self.containerIds_to_update.remove(container_id)
else:
# call update task a second time
await self.get_container_stats(container_id, wait=0, stop=True)
def exec_cmd_container(self, container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
def recv_socket_data(c_socket, timeout):
c_socket.setblocking(0)
total_data=[]
data=''
begin=time.time()
while True:
if total_data and time.time()-begin > timeout:
break
elif time.time()-begin > timeout*2:
break
try:
data = c_socket.recv(8192)
if data:
total_data.append(data.decode('utf-8'))
#change the beginning time for measurement
begin=time.time()
else:
#sleep for sometime to indicate a gap
time.sleep(0.1)
break
except:
pass
return ''.join(total_data)
try :
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
if not cmd.endswith("\n"):
cmd = cmd + "\n"
socket.send(cmd.encode('utf-8'))
data = recv_socket_data(socket, timeout)
socket.close()
return data
except Exception as e:
self.logger.error("error - exec_cmd_container: %s" % str(e))
traceback.print_exc(file=sys.stdout)
def exec_run_handler(self, type, output):
if type == 'generic':
if output.exit_code == 0:
res = { 'type': 'success', 'msg': 'command completed successfully' }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'danger', 'msg': 'command failed: ' + output.output.decode('utf-8') }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
if type == 'utf8_text_only':
return Response(content=output.output.decode('utf-8'), media_type="text/plain")
+98 -99
View File
@@ -1,119 +1,118 @@
FROM debian:bullseye-slim FROM alpine:3.19
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
# renovate: datasource=github-tags depName=dovecot/core versioning=semver-coerced
ARG DOVECOT=2.3.20
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
ARG GOSU_VERSION=1.16 ARG GOSU_VERSION=1.16
ENV LC_ALL C ENV LC_ALL C
# Add groups and users before installing Dovecot to not break compatibility # Add groups and users before installing Dovecot to not break compatibility
RUN groupadd -g 5000 vmail \ RUN addgroup -g 5000 vmail \
&& groupadd -g 401 dovecot \ && addgroup -g 401 dovecot \
&& groupadd -g 402 dovenull \ && addgroup -g 402 dovenull \
&& groupadd -g 999 sogo \ && sed -i "s/999/99/" /etc/group \
&& usermod -a -G sogo nobody \ && addgroup -g 999 sogo \
&& useradd -g vmail -u 5000 vmail -d /var/vmail \ && addgroup nobody sogo \
&& useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \ && adduser -D -u 5000 -G vmail -h /var/vmail vmail \
&& useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull \ && adduser -D -G dovecot -u 401 -h /dev/null -s /sbin/nologin dovecot \
&& touch /etc/default/locale \ && adduser -D -G dovenull -u 402 -h /dev/null -s /sbin/nologin dovenull \
&& apt-get update \ && apk add --no-cache --update \
&& apt-get -y --no-install-recommends install \ bash \
build-essential \ bind-tools \
apt-transport-https \ findutils \
envsubst \
ca-certificates \ ca-certificates \
cpanminus \
curl \ curl \
dnsutils \
dirmngr \
gettext \
gnupg2 \
jq \ jq \
libauthen-ntlm-perl \ lua \
libcgi-pm-perl \ lua-cjson \
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-socket \ lua-socket \
lua-sql-mysql \
lua5.3-sql-mysql \
icu-data-full \
mariadb-connector-c \
lua-sec \
mariadb-dev \
glib-dev \
gcompat \
mariadb-client \ mariadb-client \
perl \
perl-dev \
perl-ntlm \
perl-cgi \
perl-crypt-openssl-rsa \
perl-utils \
perl-crypt-ssleay \
perl-data-uniqid \
perl-dbd-mysql \
perl-dbi \
perl-digest-hmac \
perl-dist-checkconflicts \
perl-encode-imaputf7 \
perl-file-copy-recursive \
perl-file-tail \
perl-io-socket-inet6 \
perl-io-gzip \
perl-io-socket-ssl \
perl-io-tee \
perl-ipc-run \
perl-json-webtoken \
perl-mail-imapclient \
perl-module-implementation \
perl-module-scandeps \
perl-net-ssleay \
perl-package-stash \
perl-package-stash-xs \
perl-par-packer \
perl-parse-recdescent \
perl-lockfile-simple --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/ \
libproc \
perl-readonly \
perl-regexp-common \
perl-sys-meminfo \
perl-term-readkey \
perl-test-deep \
perl-test-fatal \
perl-test-mockobject \
perl-test-mock-guard \
perl-test-pod \
perl-test-requires \
perl-test-simple \
perl-test-warn \
perl-try-tiny \
perl-unicode-string \
perl-proc-processtable \
perl-app-cpanminus \
procps \ procps \
python3-pip \ python3 \
redis-server \ py3-mysqlclient \
supervisor \ py3-html2text \
py3-jinja2 \
py3-redis \
redis \
syslog-ng \ syslog-ng \
syslog-ng-core \ syslog-ng-redis \
syslog-ng-mod-redis \ syslog-ng-json \
supervisor \
tzdata \
wget \ wget \
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \ dovecot \
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \ dovecot-dev \
&& 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-lmtpd \ dovecot-lmtpd \
dovecot-lua \
dovecot-ldap \ dovecot-ldap \
dovecot-mysql \ dovecot-mysql \
dovecot-core \ dovecot-sql \
dovecot-submissiond \
dovecot-pigeonhole-plugin \
dovecot-pop3d \ dovecot-pop3d \
dovecot-imapd \ dovecot-fts-solr \
dovecot-solr \ && arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
&& pip3 install mysql-connector-python html2text jinja2 redis \ && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$arch" \
&& apt-get autoremove --purge -y \ && chmod +x /usr/local/bin/gosu \
&& apt-get autoclean \ && gosu nobody true
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /tmp/* /var/tmp/* /root/.cache/ #RUN cpan LockFile::Simple
# imapsync dependencies
RUN cpan Crypt::OpenSSL::PKCS12
COPY trim_logs.sh /usr/local/bin/trim_logs.sh COPY trim_logs.sh /usr/local/bin/trim_logs.sh
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
@@ -135,4 +134,4 @@ COPY quota_notify.py /usr/local/bin/quota_notify.py
COPY repl_health.sh /usr/local/bin/repl_health.sh COPY repl_health.sh /usr/local/bin/repl_health.sh
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
+17 -122
View File
@@ -28,7 +28,7 @@ ${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
# Create missing directories # Create missing directories
[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/ [[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
[[ ! -d /etc/dovecot/lua/ ]] && mkdir -p /etc/dovecot/lua/ [[ ! -d /etc/dovecot/auth/ ]] && mkdir -p /etc/dovecot/auth/
[[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage [[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage
[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve [[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo [[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo
@@ -128,124 +128,6 @@ user_query = SELECT CONCAT(JSON_UNQUOTE(JSON_VALUE(attributes, '$.mailbox_format
iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2'; iterate_query = SELECT username FROM mailbox WHERE active = '1' OR active = '2';
EOF EOF
cat <<EOF > /etc/dovecot/lua/passwd-verify.lua
function auth_password_verify(req, pass)
if req.domain == nil then
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
end
if cur == nil then
script_init()
end
if req.user == nil then
req.user = ''
end
respbody = {}
-- check against mailbox passwds
local cur,errorString = con:execute(string.format([[SELECT password FROM mailbox
WHERE username = '%s'
AND active = '1'
AND domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')
AND IFNULL(JSON_UNQUOTE(JSON_VALUE(mailbox.attributes, '$.force_pw_update')), 0) != '1'
AND IFNULL(JSON_UNQUOTE(JSON_VALUE(attributes, '$.%s_access')), 1) = '1']], con:escape(req.user), con:escape(req.domain), con:escape(req.service)))
local row = cur:fetch ({}, "a")
while row do
if req.password_verify(req, row.password, pass) == 1 then
con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
VALUES ("%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip)))
cur:close()
con:close()
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
end
row = cur:fetch (row, "a")
end
-- check against app passwds for imap and smtp
-- app passwords are only available for imap, smtp, sieve and pop3 when using sasl
if req.service == "smtp" or req.service == "imap" or req.service == "sieve" or req.service == "pop3" then
local cur,errorString = con:execute(string.format([[SELECT app_passwd.id, %s_access AS has_prot_access, app_passwd.password FROM app_passwd
INNER JOIN mailbox ON mailbox.username = app_passwd.mailbox
WHERE mailbox = '%s'
AND app_passwd.active = '1'
AND mailbox.active = '1'
AND app_passwd.domain IN (SELECT domain FROM domain WHERE domain='%s' AND active='1')]], con:escape(req.service), con:escape(req.user), con:escape(req.domain)))
local row = cur:fetch ({}, "a")
while row do
if req.password_verify(req, row.password, pass) == 1 then
-- if password is valid and protocol access is 1 OR real_rip matches SOGo, proceed
if tostring(req.real_rip) == "__IPV4_SOGO__" then
cur:close()
con:close()
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
elseif row.has_prot_access == "1" then
con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
cur:close()
con:close()
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
end
end
row = cur:fetch (row, "a")
end
end
cur:close()
con:close()
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
-- PoC
-- local reqbody = string.format([[{
-- "success":0,
-- "service":"%s",
-- "app_password":false,
-- "username":"%s",
-- "real_rip":"%s"
-- }]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip))
-- http.request {
-- method = "POST",
-- url = "http://nginx:8081/sasl_log.php",
-- source = ltn12.source.string(reqbody),
-- headers = {
-- ["content-type"] = "application/json",
-- ["content-length"] = tostring(#reqbody)
-- },
-- sink = ltn12.sink.table(respbody)
-- }
end
function auth_passdb_lookup(req)
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
end
function script_init()
mysql = require "luasql.mysql"
http = require "socket.http"
http.TIMEOUT = 5
ltn12 = require "ltn12"
env = mysql.mysql()
con = env:connect("__DBNAME__","__DBUSER__","__DBPASS__","localhost")
return 0
end
function script_deinit()
con:close()
env:close()
end
EOF
# 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
sed -i "s/__DBNAME__/${DBNAME}/g" /etc/dovecot/lua/passwd-verify.lua
sed -i "s/__IPV4_SOGO__/${IPV4_NETWORK}.248/g" /etc/dovecot/lua/passwd-verify.lua
# Migrate old sieve_after file # Migrate old sieve_after file
[[ -f /etc/dovecot/sieve_after ]] && mv /etc/dovecot/sieve_after /etc/dovecot/global_sieve_after [[ -f /etc/dovecot/sieve_after ]] && mv /etc/dovecot/sieve_after /etc/dovecot/global_sieve_after
# Create global sieve scripts # Create global sieve scripts
@@ -335,6 +217,15 @@ sys.exit()
EOF EOF
fi 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 # 401 is user dovecot
if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then
openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem
@@ -360,8 +251,8 @@ done
# Fix permissions # Fix permissions
chown root:root /etc/dovecot/sql/*.conf chown root:root /etc/dovecot/sql/*.conf
chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/auth/passwd-verify.lua
chmod 640 /etc/dovecot/sql/*.conf /etc/dovecot/lua/passwd-verify.lua chmod 640 /etc/dovecot/sql/*.conf /etc/dovecot/auth/passwd-verify.lua
chown -R vmail:vmail /var/vmail/sieve chown -R vmail:vmail /var/vmail/sieve
chown -R vmail:vmail /var/volatile chown -R vmail:vmail /var/volatile
chown -R vmail:vmail /var/vmail_index chown -R vmail:vmail /var/vmail_index
@@ -430,6 +321,10 @@ done
# For some strange, unknown and stupid reason, Dovecot may run into a race condition, when this file is not touched before it is read by dovecot/auth # For some strange, unknown and stupid reason, Dovecot may run into a race condition, when this file is not touched before it is read by dovecot/auth
# May be related to something inside Docker, I seriously don't know # May be related to something inside Docker, I seriously don't know
touch /etc/dovecot/lua/passwd-verify.lua touch /etc/dovecot/auth/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 "$@" exec "$@"
+7 -2
View File
@@ -75,7 +75,8 @@ my $sth = $dbh->prepare("SELECT id,
custom_params, custom_params,
subscribeall, subscribeall,
timeout1, timeout1,
timeout2 timeout2,
dry
FROM imapsync FROM imapsync
WHERE active = 1 WHERE active = 1
AND is_running = 0 AND is_running = 0
@@ -111,13 +112,16 @@ while ($row = $sth->fetchrow_arrayref()) {
$subscribeall = @$row[18]; $subscribeall = @$row[18];
$timeout1 = @$row[19]; $timeout1 = @$row[19];
$timeout2 = @$row[20]; $timeout2 = @$row[20];
$dry = @$row[21];
if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; } if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
my $template = $run_dir . '/imapsync.XXXXXXX'; my $template = $run_dir . '/imapsync.XXXXXXX';
my $passfile1 = File::Temp->new(TEMPLATE => $template); my $passfile1 = File::Temp->new(TEMPLATE => $template);
my $passfile2 = File::Temp->new(TEMPLATE => $template); my $passfile2 = File::Temp->new(TEMPLATE => $template);
binmode( $passfile1, ":utf8" );
print $passfile1 "$password1\n"; print $passfile1 "$password1\n";
print $passfile2 trim($master_pass) . "\n"; print $passfile2 trim($master_pass) . "\n";
@@ -148,6 +152,7 @@ while ($row = $sth->fetchrow_arrayref()) {
"--host2", "localhost", "--host2", "localhost",
"--user2", $user2 . '*' . trim($master_user), "--user2", $user2 . '*' . trim($master_user),
"--passfile2", $passfile2->filename, "--passfile2", $passfile2->filename,
($dry eq "1" ? ('--dry') : ()),
'--no-modulesversion', '--no-modulesversion',
'--noreleasecheck']; '--noreleasecheck'];
@@ -3,11 +3,10 @@
import smtplib import smtplib
import os import os
import sys import sys
import mysql.connector import MySQLdb
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.utils import COMMASPACE, formatdate from email.utils import COMMASPACE, formatdate
import cgi
import jinja2 import jinja2
from jinja2 import Template from jinja2 import Template
import json import json
@@ -50,7 +49,7 @@ try:
def query_mysql(query, headers = True, update = False): def query_mysql(query, headers = True, update = False):
while True: while True:
try: 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: except Exception as ex:
print('%s - trying again...' % (ex)) print('%s - trying again...' % (ex))
time.sleep(3) time.sleep(3)
+1 -1
View File
@@ -55,7 +55,7 @@ try:
msg.attach(text_part) msg.attach(text_part)
msg.attach(html_part) msg.attach(html_part)
msg['To'] = username 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')) p.communicate(input=bytes(msg.as_string(), 'utf-8'))
domain = username.split("@")[-1] domain = username.split("@")[-1]
+1 -1
View File
@@ -11,7 +11,7 @@ fi
# Is replication active? # Is replication active?
# grep on file is less expensive than doveconf # 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 ${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
exit exit
fi fi
@@ -13,6 +13,10 @@ autostart=true
[program:dovecot] [program:dovecot]
command=/usr/sbin/dovecot -F command=/usr/sbin/dovecot -F
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true autorestart=true
[eventlistener:processes] [eventlistener:processes]
@@ -1,4 +1,4 @@
@version: 3.28 @version: 4.5
@include "scl.conf" @include "scl.conf"
options { options {
chain_hostnames(off); chain_hostnames(off);
@@ -6,11 +6,11 @@ options {
use_dns(no); use_dns(no);
use_fqdn(no); use_fqdn(no);
owner("root"); group("adm"); perm(0640); owner("root"); group("adm"); perm(0640);
stats_freq(0); stats(freq(0));
bad_hostname("^gconfd$"); bad_hostname("^gconfd$");
}; };
source s_src { source s_dgram {
unix-stream("/dev/log"); unix-dgram("/dev/log");
internal(); internal();
}; };
destination d_stdout { pipe("/dev/stdout"); }; destination d_stdout { pipe("/dev/stdout"); };
@@ -36,7 +36,7 @@ filter f_replica {
not match("Error: sync: Unknown user in remote" value("MESSAGE")); not match("Error: sync: Unknown user in remote" value("MESSAGE"));
}; };
log { log {
source(s_src); source(s_dgram);
filter(f_replica); filter(f_replica);
destination(d_stdout); destination(d_stdout);
filter(f_mail); filter(f_mail);
+5 -5
View File
@@ -1,4 +1,4 @@
@version: 3.28 @version: 4.5
@include "scl.conf" @include "scl.conf"
options { options {
chain_hostnames(off); chain_hostnames(off);
@@ -6,11 +6,11 @@ options {
use_dns(no); use_dns(no);
use_fqdn(no); use_fqdn(no);
owner("root"); group("adm"); perm(0640); owner("root"); group("adm"); perm(0640);
stats_freq(0); stats(freq(0));
bad_hostname("^gconfd$"); bad_hostname("^gconfd$");
}; };
source s_src { source s_dgram {
unix-stream("/dev/log"); unix-dgram("/dev/log");
internal(); internal();
}; };
destination d_stdout { pipe("/dev/stdout"); }; destination d_stdout { pipe("/dev/stdout"); };
@@ -36,7 +36,7 @@ filter f_replica {
not match("Error: sync: Unknown user in remote" value("MESSAGE")); not match("Error: sync: Unknown user in remote" value("MESSAGE"));
}; };
log { log {
source(s_src); source(s_dgram);
filter(f_replica); filter(f_replica);
destination(d_stdout); destination(d_stdout);
filter(f_mail); filter(f_mail);
+1
View File
@@ -23,3 +23,4 @@ catch_non_zero "${REDIS_CMDLINE} LTRIM AUTODISCOVER_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM API_LOG 0 ${LOG_LINES}" catch_non_zero "${REDIS_CMDLINE} LTRIM API_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM RL_LOG 0 ${LOG_LINES}" catch_non_zero "${REDIS_CMDLINE} LTRIM RL_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM WATCHDOG_LOG 0 ${LOG_LINES}" catch_non_zero "${REDIS_CMDLINE} LTRIM WATCHDOG_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM CRON_LOG 0 ${LOG_LINES}"
+16 -4
View File
@@ -1,6 +1,9 @@
FROM alpine:3.17 FROM alpine:3.19
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
WORKDIR /app
ARG PIP_BREAK_SYSTEM_PACKAGES=1
ENV XTABLES_LIBDIR /usr/lib/xtables ENV XTABLES_LIBDIR /usr/lib/xtables
ENV PYTHON_IPTABLES_XTABLES_VERSION 12 ENV PYTHON_IPTABLES_XTABLES_VERSION 12
ENV IPTABLES_LIBDIR /usr/lib ENV IPTABLES_LIBDIR /usr/lib
@@ -12,12 +15,16 @@ RUN apk add --virtual .build-deps \
openssl-dev \ openssl-dev \
&& apk add -U python3 \ && apk add -U python3 \
iptables \ iptables \
iptables-dev \
ip6tables \ ip6tables \
xtables-addons \ xtables-addons \
nftables \
tzdata \ tzdata \
py3-pip \ py3-pip \
py3-nftables \
musl-dev \ musl-dev \
&& pip3 install --ignore-installed --upgrade pip \ && pip3 install --ignore-installed --upgrade pip \
jsonschema \
python-iptables \ python-iptables \
redis \ redis \
ipaddress \ ipaddress \
@@ -26,5 +33,10 @@ RUN apk add --virtual .build-deps \
# && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \ # && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \
COPY server.py / COPY modules /app/modules
CMD ["python3", "-u", "/server.py"] COPY main.py /app/
COPY ./docker-entrypoint.sh /app/
RUN chmod +x /app/docker-entrypoint.sh
CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"]
+29
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
+496
View File
@@ -0,0 +1,496 @@
#!/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] = '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:
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()
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 = 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 = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** 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():
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 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 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)
@@ -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
@@ -0,0 +1,27 @@
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
if self.r is not None:
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
print(message)
def logWarn(self, message):
self.log('warn', message)
def logCrit(self, message):
self.log('crit', message)
def logInfo(self, message):
self.log('info', message)
@@ -0,0 +1,656 @@
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:
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)
target_net = ipaddress.ip_network(snat_target)
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))
daddr_net = ipaddress.ip_network(daddr_ip + '/' + str(daddr_len))
current_target_net = ipaddress.ip_network(target_ip)
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)
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"]
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)
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
-610
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)
+3 -2
View File
@@ -1,6 +1,7 @@
FROM alpine:3.17 FROM alpine:3.19
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
ARG PIP_BREAK_SYSTEM_PACKAGES=1
WORKDIR /app WORKDIR /app
#RUN addgroup -S olefy && adduser -S olefy -G olefy \ #RUN addgroup -S olefy && adduser -S olefy -G olefy \
+13 -13
View File
@@ -1,18 +1,18 @@
FROM php:8.2-fpm-alpine3.17 FROM php:8.2-fpm-alpine3.18
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced # renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG APCU_PECL_VERSION=5.1.22 ARG APCU_PECL_VERSION=5.1.23
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced # renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
ARG IMAGICK_PECL_VERSION=3.7.0 ARG IMAGICK_PECL_VERSION=3.7.0
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced # renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MAILPARSE_PECL_VERSION=3.1.4 ARG MAILPARSE_PECL_VERSION=3.1.6
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced # renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MEMCACHED_PECL_VERSION=3.2.0 ARG MEMCACHED_PECL_VERSION=3.2.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
ARG REDIS_PECL_VERSION=5.3.7 ARG REDIS_PECL_VERSION=6.0.2
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
ARG COMPOSER_VERSION=2.5.5 ARG COMPOSER_VERSION=2.6.6
RUN apk add -U --no-cache autoconf \ RUN apk add -U --no-cache autoconf \
aspell-dev \ aspell-dev \
@@ -110,4 +110,4 @@ COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["php-fpm"] CMD ["php-fpm"]
@@ -172,6 +172,24 @@ BEGIN
END; END;
// //
DELIMITER ; DELIMITER ;
DROP EVENT IF EXISTS clean_sasl_log;
DELIMITER //
CREATE EVENT clean_sasl_log
ON SCHEDULE EVERY 1 DAY DO
BEGIN
DELETE sasl_log.* FROM sasl_log
LEFT JOIN (
SELECT username, service, MAX(datetime) AS lastdate
FROM sasl_log
GROUP BY username, service
) AS last ON sasl_log.username = last.username AND sasl_log.service = last.service
WHERE datetime < DATE_SUB(NOW(), INTERVAL 31 DAY) AND datetime < lastdate;
DELETE FROM sasl_log
WHERE username NOT IN (SELECT username FROM mailbox) AND
datetime < DATE_SUB(NOW(), INTERVAL 31 DAY);
END;
//
DELIMITER ;
EOF EOF
fi fi
@@ -186,6 +204,17 @@ chown -R 82:82 /web/templates/cache
# Clear cache # Clear cache
find /web/templates/cache/* -not -name '.gitkeep' -delete find /web/templates/cache/* -not -name '.gitkeep' -delete
# list client ca of all domains for
CA_LIST="/etc/nginx/conf.d/client_cas.crt"
# Clear the output file
> "$CA_LIST"
# Execute the query and append each value to the output file
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT ssl_client_ca FROM domain;" | while read -r ca; do
echo "$ca" >> "$CA_LIST"
done
echo "SSL client CAs have been appended to $CA_LIST"
# Run hooks # Run hooks
for file in /hooks/*; do for file in /hooks/*; do
if [ -x "${file}" ]; then if [ -x "${file}" ]; then
+4 -4
View File
@@ -1,5 +1,5 @@
FROM debian:bullseye-slim FROM debian:bullseye-slim
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ENV LC_ALL C ENV LC_ALL C
@@ -17,10 +17,10 @@ RUN groupadd -g 102 postfix \
ca-certificates \ ca-certificates \
curl \ curl \
dirmngr \ dirmngr \
dnsutils \ dnsutils \
gnupg \ gnupg \
libsasl2-modules \ libsasl2-modules \
mariadb-client \ mariadb-client \
perl \ perl \
postfix \ postfix \
postfix-mysql \ postfix-mysql \
@@ -32,7 +32,7 @@ RUN groupadd -g 102 postfix \
syslog-ng \ syslog-ng \
syslog-ng-core \ syslog-ng-core \
syslog-ng-mod-redis \ syslog-ng-mod-redis \
tzdata \ tzdata \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& touch /etc/default/locale \ && touch /etc/default/locale \
&& printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \ && printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \
+94 -5
View File
@@ -393,12 +393,101 @@ query = SELECT goto FROM spamalias
AND validity >= UNIX_TIMESTAMP() AND validity >= UNIX_TIMESTAMP()
EOF EOF
sed -i '/User overrides/q' /opt/postfix/conf/main.cf if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
echo >> /opt/postfix/conf/main.cf cat <<EOF > /opt/postfix/conf/dns_blocklists.cf
touch /opt/postfix/conf/extra.cf # This file can be edited.
sed -i '/myhostname/d' /opt/postfix/conf/extra.cf # Delete this file and restart postfix container to revert any changes.
echo -e "myhostname = ${MAILCOW_HOSTNAME}\n$(cat /opt/postfix/conf/extra.cf)" > /opt/postfix/conf/extra.cf postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
hostkarma.junkemailfilter.com=127.0.0.1*-2
list.dnswl.org=127.0.[0..255].0*-2
list.dnswl.org=127.0.[0..255].1*-4
list.dnswl.org=127.0.[0..255].2*-6
list.dnswl.org=127.0.[0..255].3*-8
ix.dnsbl.manitu.net*2
bl.spamcop.net*2
bl.suomispam.net*2
hostkarma.junkemailfilter.com=127.0.0.2*3
hostkarma.junkemailfilter.com=127.0.0.4*2
hostkarma.junkemailfilter.com=127.0.1.2*1
backscatter.spameatingmonkey.net*2
bl.ipv6.spameatingmonkey.net*2
bl.spameatingmonkey.net*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')
if [ ! -z "$DNSBL_CONFIG" ]; then
echo -e "\e[33mChecking if ASN for your IP is listed for Spamhaus Bad ASN List...\e[0m"
if [ -n "$SPAMHAUS_DQS_KEY" ]; then
echo -e "\e[32mDetected SPAMHAUS_DQS_KEY variable from mailcow.conf...\e[0m"
echo -e "\e[33mUsing DQS Blocklists from Spamhaus!\e[0m"
SPAMHAUS_DNSBL_CONFIG=$(cat <<EOF
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[4..7]*6
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[10;11]*8
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.3*4
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.2*3
postscreen_dnsbl_reply_map = texthash:/opt/postfix/conf/dnsbl_reply.map
EOF
cat <<EOF > /opt/postfix/conf/dnsbl_reply.map
# Autogenerated by mailcow, using Spamhaus DQS reply domains
${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net sbl.spamhaus.org
${SPAMHAUS_DQS_KEY}.xbl.dq.spamhaus.net xbl.spamhaus.org
${SPAMHAUS_DQS_KEY}.pbl.dq.spamhaus.net pbl.spamhaus.org
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net zen.spamhaus.org
${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net dbl.spamhaus.org
${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net zrd.spamhaus.org
EOF
)
else
if [ -f "/opt/postfix/conf/dnsbl_reply.map" ]; then
rm /opt/postfix/conf/dnsbl_reply.map
fi
response=$(curl --connect-timeout 15 --max-time 30 -s -o /dev/null -w "%{http_code}" "https://asn-check.mailcow.email")
if [ "$response" -eq 503 ]; then
echo -e "\e[31mThe AS of your IP is listed as a banned AS from Spamhaus!\e[0m"
echo -e "\e[33mNo SPAMHAUS_DQS_KEY found... Skipping Spamhaus blocklists entirely!\e[0m"
SPAMHAUS_DNSBL_CONFIG=""
elif [ "$response" -eq 200 ]; then
echo -e "\e[32mThe AS of your IP is NOT listed as a banned AS from Spamhaus!\e[0m"
echo -e "\e[33mUsing the open Spamhaus blocklists.\e[0m"
SPAMHAUS_DNSBL_CONFIG=$(cat <<EOF
zen.spamhaus.org=127.0.0.[10;11]*8
zen.spamhaus.org=127.0.0.[4..7]*6
zen.spamhaus.org=127.0.0.3*4
zen.spamhaus.org=127.0.0.2*3
EOF
)
else
echo -e "\e[31mWe couldn't determine your AS... (maybe DNS/Network issue?) Response Code: $response\e[0m"
echo -e "\e[33mDeactivating Spamhaus DNS Blocklists to be on the safe site!\e[0m"
SPAMHAUS_DNSBL_CONFIG=""
fi
fi
fi
# Reset main.cf
sed -i '/Overrides/q' /opt/postfix/conf/main.cf
echo >> /opt/postfix/conf/main.cf
# Append postscreen dnsbl sites to main.cf
if [ ! -z "$DNSBL_CONFIG" ]; then
echo -e "${DNSBL_CONFIG}\n${SPAMHAUS_DNSBL_CONFIG}" >> /opt/postfix/conf/main.cf
fi
# Append user overrides
echo -e "\n# User Overrides" >> /opt/postfix/conf/main.cf
touch /opt/postfix/conf/extra.cf
sed -i '/\$myhostname/! { /myhostname/d }' /opt/postfix/conf/extra.cf
echo -e "myhostname = ${MAILCOW_HOSTNAME}\n$(cat /opt/postfix/conf/extra.cf)" > /opt/postfix/conf/extra.cf
cat /opt/postfix/conf/extra.cf >> /opt/postfix/conf/main.cf cat /opt/postfix/conf/extra.cf >> /opt/postfix/conf/main.cf
if [ ! -f /opt/postfix/conf/custom_transport.pcre ]; then if [ ! -f /opt/postfix/conf/custom_transport.pcre ]; then
+6 -6
View File
@@ -1,19 +1,19 @@
FROM debian:bullseye-slim FROM debian:bookworm-slim
LABEL maintainer "Andre Peters <andre.peters@tinc.gmbh>" LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG CODENAME=bullseye ARG CODENAME=bookworm
ENV LC_ALL C ENV LC_ALL C
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y --no-install-recommends \
tzdata \ tzdata \
ca-certificates \ ca-certificates \
gnupg2 \ gnupg2 \
apt-transport-https \ apt-transport-https \
dnsutils \ dnsutils \
netcat \ netcat-traditional \
&& apt-key adv --fetch-keys https://rspamd.com/apt-stable/gpg.key \ && apt-key adv --fetch-keys https://rspamd.com/apt-stable/gpg.key \
&& echo "deb [arch=amd64] https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \ && echo "deb https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \
&& apt-get update \ && apt-get update \
&& apt-get --no-install-recommends -y install rspamd redis-tools procps nano \ && apt-get --no-install-recommends -y install rspamd redis-tools procps nano \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
@@ -79,6 +79,9 @@ EOF
redis-cli -h redis-mailcow SLAVEOF NO ONE redis-cli -h redis-mailcow SLAVEOF NO ONE
fi fi
# Provide additional lua modules
ln -s /usr/lib/$(uname -m)-linux-gnu/liblua5.1-cjson.so.0.0.0 /usr/lib/rspamd/cjson.so
chown -R _rspamd:_rspamd /var/lib/rspamd \ chown -R _rspamd:_rspamd /var/lib/rspamd \
/etc/rspamd/local.d \ /etc/rspamd/local.d \
/etc/rspamd/override.d \ /etc/rspamd/override.d \
+8 -7
View File
@@ -1,10 +1,11 @@
FROM debian:bullseye-slim FROM debian:bookworm-slim
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG SOGO_DEBIAN_REPOSITORY=http://packages.sogo.nu/nightly/5/debian/ ARG DEBIAN_VERSION=bookworm
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced ARG SOGO_DEBIAN_REPOSITORY=http://www.axis.cz/linux/debian
ARG GOSU_VERSION=1.16 # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.17
ENV LC_ALL C ENV LC_ALL C
# Prerequisites # Prerequisites
@@ -21,7 +22,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
syslog-ng-core \ syslog-ng-core \
syslog-ng-mod-redis \ syslog-ng-mod-redis \
dirmngr \ dirmngr \
netcat \ netcat-traditional \
psmisc \ psmisc \
wget \ wget \
patch \ patch \
@@ -32,7 +33,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
&& mkdir /usr/share/doc/sogo \ && mkdir /usr/share/doc/sogo \
&& touch /usr/share/doc/sogo/empty.sh \ && touch /usr/share/doc/sogo/empty.sh \
&& apt-key adv --keyserver keys.openpgp.org --recv-key 74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 \ && 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 \ && apt-get update && apt-get install -y --no-install-recommends \
sogo \ sogo \
sogo-activesync \ sogo-activesync \
-104
View File
@@ -24,110 +24,6 @@ while [[ "${DBV_NOW}" != "${DBV_NEW}" ]]; do
done done
echo "DB schema is ${DBV_NOW}" echo "DB schema is ${DBV_NOW}"
# Recreate view
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "We are master, preparing sogo_view..."
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP VIEW IF EXISTS sogo_view"
while [[ ${VIEW_OK} != 'OK' ]]; do
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
CREATE VIEW sogo_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings) AS
SELECT
mailbox.username,
mailbox.domain,
mailbox.username,
IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.force_pw_update')) = '0', IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.sogo_access')) = 1, password, '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'), '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'),
mailbox.name,
mailbox.username,
IFNULL(GROUP_CONCAT(ga.aliases ORDER BY ga.aliases SEPARATOR ' '), ''),
IFNULL(gda.ad_alias, ''),
IFNULL(external_acl.send_as_acl, ''),
mailbox.kind,
mailbox.multiple_bookings
FROM
mailbox
LEFT OUTER JOIN
grouped_mail_aliases ga
ON ga.username REGEXP CONCAT('(^|,)', mailbox.username, '($|,)')
LEFT OUTER JOIN
grouped_domain_alias_address gda
ON gda.username = mailbox.username
LEFT OUTER JOIN
grouped_sender_acl_external external_acl
ON external_acl.username = mailbox.username
WHERE
mailbox.active = '1'
GROUP BY
mailbox.username;
EOF
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view'") ]]; then
VIEW_OK=OK
else
echo "Will retry to setup SOGo view in 3s..."
sleep 3
fi
done
else
while [[ ${VIEW_OK} != 'OK' ]]; do
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view'") ]]; then
VIEW_OK=OK
else
echo "Waiting for SOGo view to be created by master..."
sleep 3
fi
done
fi
# Wait for static view table if missing after update and update content
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "We are master, preparing _sogo_static_view..."
while [[ ${STATIC_VIEW_OK} != 'OK' ]]; do
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then
STATIC_VIEW_OK=OK
echo "Updating _sogo_static_view content..."
# If changed, also update init_db.inc.php
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "REPLACE INTO _sogo_static_view (c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings) SELECT c_uid, domain, c_name, c_password, c_cn, mail, aliases, ad_aliases, ext_acl, kind, multiple_bookings from sogo_view;"
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "DELETE FROM _sogo_static_view WHERE c_uid NOT IN (SELECT username FROM mailbox WHERE active = '1')"
else
echo "Waiting for database initialization..."
sleep 3
fi
done
else
while [[ ${STATIC_VIEW_OK} != 'OK' ]]; do
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '_sogo_static_view'") ]]; then
STATIC_VIEW_OK=OK
else
echo "Waiting for database initialization by master..."
sleep 3
fi
done
fi
# Recreate password update trigger
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "We are master, preparing update trigger..."
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TRIGGER IF EXISTS sogo_update_password"
while [[ ${TRIGGER_OK} != 'OK' ]]; do
mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} << EOF
DELIMITER -
CREATE TRIGGER sogo_update_password AFTER UPDATE ON _sogo_static_view
FOR EACH ROW
BEGIN
UPDATE mailbox SET password = NEW.c_password WHERE NEW.c_uid = username;
END;
-
DELIMITER ;
EOF
if [[ ! -z $(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -B -e "SELECT 'OK' FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_NAME = 'sogo_update_password'") ]]; then
TRIGGER_OK=OK
else
echo "Will retry to setup SOGo password update trigger in 3s"
sleep 3
fi
done
fi
# cat /dev/urandom seems to hang here occasionally and is not recommended anyway, better use openssl # cat /dev/urandom seems to hang here occasionally and is not recommended anyway, better use openssl
RAND_PASS=$(openssl rand -base64 16 | tr -dc _A-Z-a-z-0-9) RAND_PASS=$(openssl rand -base64 16 | tr -dc _A-Z-a-z-0-9)
+3 -2
View File
@@ -1,9 +1,10 @@
FROM solr:7.7-slim FROM solr:7.7-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
USER root USER root
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=(?<version>.*)$
ARG GOSU_VERSION=1.16 ARG GOSU_VERSION=1.17
COPY solr.sh / COPY solr.sh /
COPY solr-config-7.7.0.xml / COPY solr-config-7.7.0.xml /
+9 -2
View File
@@ -1,9 +1,11 @@
FROM alpine:3.17 FROM alpine:3.18
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
RUN apk add --update --no-cache \ RUN apk add --update --no-cache \
curl \ curl \
bind-tools \
netcat-openbsd \
unbound \ unbound \
bash \ bash \
openssl \ openssl \
@@ -18,6 +20,11 @@ EXPOSE 53/udp 53/tcp
COPY docker-entrypoint.sh /docker-entrypoint.sh COPY docker-entrypoint.sh /docker-entrypoint.sh
# healthcheck (nslookup)
COPY healthcheck.sh /healthcheck.sh
RUN chmod +x /healthcheck.sh
HEALTHCHECK --interval=5s --timeout=30s CMD [ "/healthcheck.sh" ]
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["/usr/sbin/unbound"] CMD ["/usr/sbin/unbound"]
+99
View File
@@ -0,0 +1,99 @@
#!/bin/bash
# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!)
if [[ "${SKIP_UNBOUND_HEALTHCHECK}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
SKIP_UNBOUND_HEALTHCHECK=y
fi
# Declare log function for logfile inside container
function log_to_file() {
echo "$(date +"%Y-%m-%d %H:%M:%S"): $1" > /var/log/healthcheck.log
}
# General Ping function to check general pingability
function check_ping() {
declare -a ipstoping=("1.1.1.1" "8.8.8.8" "9.9.9.9")
for ip in "${ipstoping[@]}" ; do
ping -q -c 3 -w 5 "$ip"
if [ $? -ne 0 ]; then
log_to_file "Healthcheck: Couldn't ping $ip for 5 seconds... Gave up!"
log_to_file "Please check your internet connection or firewall rules to fix this error, because a simple ping test should always go through from the unbound container!"
return 1
fi
done
log_to_file "Healthcheck: Ping Checks WORKING properly!"
return 0
}
# General DNS Resolve Check against Unbound Resolver himself
function check_dns() {
declare -a domains=("mailcow.email" "github.com" "hub.docker.com")
for domain in "${domains[@]}" ; do
for ((i=1; i<=3; i++)); do
dig +short +timeout=2 +tries=1 "$domain" @127.0.0.1 > /dev/null
if [ $? -ne 0 ]; then
log_to_file "Healthcheck: DNS Resolution Failed on $i attempt! Trying again..."
if [ $i -eq 3 ]; then
log_to_file "Healthcheck: DNS Resolution not possible after $i attempts... Gave up!"
log_to_file "Maybe check your outbound firewall, as it needs to resolve DNS over TCP AND UDP!"
return 1
fi
fi
done
done
log_to_file "Healthcheck: DNS Resolver WORKING properly!"
return 0
}
# Simple Netcat Check to connect to common webports
function check_netcat() {
declare -a domains=("mailcow.email" "github.com" "hub.docker.com")
declare -a ports=("80" "443")
for domain in "${domains[@]}" ; do
for port in "${ports[@]}" ; do
nc -z -w 2 $domain $port
if [ $? -ne 0 ]; then
log_to_file "Healthcheck: Could not reach $domain on Port $port... Gave up!"
log_to_file "Please check your internet connection or firewall rules to fix this error."
return 1
fi
done
done
log_to_file "Healthcheck: Netcat Checks WORKING properly!"
return 0
}
if [[ ${SKIP_UNBOUND_HEALTHCHECK} == "y" ]]; then
log_to_file "Healthcheck: ALL CHECKS WERE SKIPPED! Unbound is healthy!"
exit 0
fi
# run checks, if check is not returning 0 (return value if check is ok), healthcheck will exit with 1 (marked in docker as unhealthy)
check_ping
if [ $? -ne 0 ]; then
exit 1
fi
check_dns
if [ $? -ne 0 ]; then
exit 1
fi
check_netcat
if [ $? -ne 0 ]; then
exit 1
fi
log_to_file "Healthcheck: ALL CHECKS WERE SUCCESSFUL! Unbound is healthy!"
exit 0
+2 -2
View File
@@ -1,5 +1,5 @@
FROM alpine:3.17 FROM alpine:3.18
LABEL maintainer "André Peters <andre.peters@servercow.de>" LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
# Installation # Installation
RUN apk add --update \ RUN apk add --update \
+69 -44
View File
@@ -19,9 +19,11 @@ fi
if [[ "${WATCHDOG_VERBOSE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then if [[ "${WATCHDOG_VERBOSE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
SMTP_VERBOSE="--verbose" SMTP_VERBOSE="--verbose"
CURL_VERBOSE="--verbose"
set -xv set -xv
else else
SMTP_VERBOSE="" SMTP_VERBOSE=""
CURL_VERBOSE=""
exec 2>/dev/null exec 2>/dev/null
fi fi
@@ -97,7 +99,9 @@ log_msg() {
echo $(date) $(printf '%s\n' "${1}") 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= THROTTLE=
[[ -z ${1} ]] && return 1 [[ -z ${1} ]] && return 1
# If exists, body will be the content of "/tmp/${1}", even if ${2} is set # If exists, body will be the content of "/tmp/${1}", even if ${2} is set
@@ -122,37 +126,57 @@ function mail_error() {
else else
SUBJECT="${WATCHDOG_SUBJECT}: ${1}" SUBJECT="${WATCHDOG_SUBJECT}: ${1}"
fi fi
IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}"
for rcpt in "${MAIL_RCPTS[@]}"; do # Send mail notification if enabled
RCPT_DOMAIN= if [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]]; then
RCPT_MX= IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}"
RCPT_DOMAIN=$(echo ${rcpt} | awk -F @ {'print $NF'}) for rcpt in "${MAIL_RCPTS[@]}"; do
CHECK_FOR_VALID_MX=$(dig +short ${RCPT_DOMAIN} mx) RCPT_DOMAIN=
if [[ -z ${CHECK_FOR_VALID_MX} ]]; then RCPT_MX=
log_msg "Cannot determine MX for ${rcpt}, skipping email notification..." 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 return 1
fi fi
[ -f "/tmp/${1}" ] && BODY="/tmp/${1}"
timeout 10s ./smtp-cli --missing-modules-ok \ # Replace subject and body placeholders
"${SMTP_VERBOSE}" \ WEBHOOK_BODY=$(echo ${WATCHDOG_NOTIFY_WEBHOOK_BODY} | sed "s/\$SUBJECT\|\${SUBJECT}/$SUBJECT/g" | sed "s/\$BODY\|\${BODY}/$BODY/g")
--charset=UTF-8 \
--subject="${SUBJECT}" \ # POST to webhook
--body-plain="${BODY}" \ curl -X POST -H "Content-Type: application/json" ${CURL_VERBOSE} -d "${WEBHOOK_BODY}" ${WATCHDOG_NOTIFY_WEBHOOK}
--add-header="X-Priority: 1" \
--to=${rcpt} \ log_msg "Sent notification using webhook"
--from="watchdog@${MAILCOW_HOSTNAME}" \ fi
--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
} }
get_container_ip() { get_container_ip() {
@@ -197,7 +221,7 @@ get_container_ip() {
# One-time check # One-time check
if grep -qi "$(echo ${IPV6_NETWORK} | cut -d: -f1-3)" <<< "$(ip a s)"; then if grep -qi "$(echo ${IPV6_NETWORK} | cut -d: -f1-3)" <<< "$(ip a s)"; then
if [[ -z "$(get_ipv6)" ]]; 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
fi fi
@@ -692,8 +716,8 @@ rspamd_checks() {
From: watchdog@localhost From: watchdog@localhost
Empty Empty
' | usr/bin/curl --max-time 10 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan | jq -rc .default.required_score) ' | 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} != "9999" ]]; then if [[ ${SCORE} -ne 9999 ]]; then
echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2 echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2
err_count=$(( ${err_count} + 1)) err_count=$(( ${err_count} + 1))
else else
@@ -746,8 +770,8 @@ olefy_checks() {
} }
# Notify about start # Notify about start
if [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]]; then if [[ ${WATCHDOG_NOTIFY_START} =~ ^([yY][eE][sS]|[yY])+$ ]]; then
mail_error "watchdog-mailcow" "Watchdog started monitoring mailcow." notify_error "watchdog-mailcow" "Watchdog started monitoring mailcow."
fi fi
# Create watchdog agents # Create watchdog agents
@@ -1029,33 +1053,33 @@ while true; do
fi fi
if [[ ${com_pipe_answer} == "ratelimit" ]]; then if [[ ${com_pipe_answer} == "ratelimit" ]]; then
log_msg "At least one ratelimit was applied" 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 elif [[ ${com_pipe_answer} == "mail_queue_status" ]]; then
log_msg "Mail queue status is critical" 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 elif [[ ${com_pipe_answer} == "external_checks" ]]; then
log_msg "Your mailcow is an open relay!" log_msg "Your mailcow is an open relay!"
# Define $2 to override message text, else print service was restarted at ... # 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 elif [[ ${com_pipe_answer} == "mysql_repl_checks" ]]; then
log_msg "MySQL replication is not working properly" log_msg "MySQL replication is not working properly"
# Define $2 to override message text, else print service was restarted at ... # Define $2 to override message text, else print service was restarted at ...
# Once mail per 10 minutes # 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 elif [[ ${com_pipe_answer} == "dovecot_repl_checks" ]]; then
log_msg "Dovecot replication is not working properly" log_msg "Dovecot replication is not working properly"
# Define $2 to override message text, else print service was restarted at ... # Define $2 to override message text, else print service was restarted at ...
# Once mail per 10 minutes # 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 elif [[ ${com_pipe_answer} == "certcheck" ]]; then
log_msg "Certificates are about to expire" log_msg "Certificates are about to expire"
# Define $2 to override message text, else print service was restarted at ... # Define $2 to override message text, else print service was restarted at ...
# Only mail once a day # 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 elif [[ ${com_pipe_answer} == "acme-mailcow" ]]; then
log_msg "acme-mailcow did not complete successfully" log_msg "acme-mailcow did not complete successfully"
# Define $2 to override message text, else print service was restarted at ... # 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 elif [[ ${com_pipe_answer} == "fail2ban" ]]; then
F2B_RES=($(timeout 4s ${REDIS_CMDLINE} --raw GET F2B_RES 2> /dev/null)) F2B_RES=($(timeout 4s ${REDIS_CMDLINE} --raw GET F2B_RES 2> /dev/null))
if [[ ! -z "${F2B_RES}" ]]; then if [[ ! -z "${F2B_RES}" ]]; then
@@ -1065,7 +1089,7 @@ while true; do
log_msg "Banned ${host}" log_msg "Banned ${host}"
rm /tmp/fail2ban 2> /dev/null rm /tmp/fail2ban 2> /dev/null
timeout 2s whois "${host}" > /tmp/fail2ban 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 done
fi fi
elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
@@ -1085,7 +1109,7 @@ while true; do
else else
log_msg "Sending restart command to ${CONTAINER_ID}..." log_msg "Sending restart command to ${CONTAINER_ID}..."
curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/restart curl --silent --insecure -XPOST https://dockerapi/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..." log_msg "Wait for restarted container to settle and continue watching..."
sleep 35 sleep 35
fi fi
@@ -1095,3 +1119,4 @@ while true; do
kill -USR1 ${BACKGROUND_TASKS[*]} kill -USR1 ${BACKGROUND_TASKS[*]}
fi fi
done done
+2 -2
View File
@@ -86,7 +86,7 @@ server {
deny all; 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)(\/.*|)$; fastcgi_split_path_info ^(.+?\.php)(\/.*|)$;
set $path_info $fastcgi_path_info; set $path_info $fastcgi_path_info;
try_files $fastcgi_script_name =404; try_files $fastcgi_script_name =404;
@@ -105,7 +105,7 @@ server {
fastcgi_read_timeout 1200; fastcgi_read_timeout 1200;
} }
location ~ ^\/(?:updater|oc[ms]-provider)(?:$|\/) { location ~ ^\/(?:updater|ocs-provider)(?:$|\/) {
try_files $uri/ =404; try_files $uri/ =404;
index index.php; index index.php;
} }
+75
View File
@@ -0,0 +1,75 @@
<?php
ini_set('error_reporting', 0);
header('Content-Type: application/json');
$post = trim(file_get_contents('php://input'));
if ($post) {
$post = json_decode($post, true);
}
$return = array("success" => false);
if(!isset($post['username']) || !isset($post['password']) || !isset($post['real_rip'])){
error_log("MAILCOWAUTH: Bad Request");
http_response_code(400); // Bad Request
echo json_encode($return);
exit();
}
require_once('../../../web/inc/vars.inc.php');
if (file_exists('../../../web/inc/vars.local.inc.php')) {
include_once('../../../web/inc/vars.local.inc.php');
}
require_once '../../../web/inc/lib/vendor/autoload.php';
// Init database
$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("MAILCOWAUTH: " . $e . PHP_EOL);
http_response_code(500); // Internal Server Error
echo json_encode($return);
exit;
}
// Load core functions first
require_once 'functions.inc.php';
require_once 'functions.auth.inc.php';
require_once 'sessions.inc.php';
require_once 'functions.mailbox.inc.php';
// Init provider
$iam_provider = identity_provider('init');
$protocol = $post['protocol'];
if ($post['real_rip'] == getenv('IPV4_NETWORK') . '.248') {
$protocol = null;
}
$result = user_login($post['username'], $post['password'], $protocol, array('is_internal' => true));
if ($result === false){
$result = apppass_login($post['username'], $post['password'], $protocol, array(
'is_internal' => true,
'remote_addr' => $post['real_rip']
));
}
if ($result) {
http_response_code(200); // OK
$return['success'] = true;
} else {
error_log("MAILCOWAUTH: Login failed for user " . $post['username']);
http_response_code(401); // Unauthorized
}
echo json_encode($return);
session_destroy();
exit;
+42
View File
@@ -0,0 +1,42 @@
function auth_password_verify(request, password)
if request.domain == nil then
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user"
end
json = require "cjson"
ltn12 = require "ltn12"
https = require "ssl.https"
https.TIMEOUT = 5
local req = {
username = request.user,
password = password,
real_rip = request.real_rip,
protocol = {}
}
req.protocol[request.service] = true
local req_json = json.encode(req)
local res = {}
local b, c = https.request {
method = "POST",
url = "https://nginx:9082",
source = ltn12.source.string(req_json),
headers = {
["content-type"] = "application/json",
["content-length"] = tostring(#req_json)
},
sink = ltn12.sink.table(res),
insecure = true
}
local api_response = json.decode(table.concat(res))
if api_response.success == true then
return dovecot.auth.PASSDB_RESULT_OK, ""
end
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate"
end
function auth_passdb_lookup(req)
return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, ""
end
+11 -3
View File
@@ -24,6 +24,11 @@ mail_plugins = </etc/dovecot/mail_plugins
mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix: mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
mail_attachment_dir = /var/attachments mail_attachment_dir = /var/attachments
mail_attachment_min_size = 128k mail_attachment_min_size = 128k
# Significantly speeds up very large mailboxes, but is only safe to enable if
# you do not manually modify the files in the `cur` directories in
# mailcowdockerized_vmail-vol-1.
# https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-performance/
maildir_very_dirty_syncs = yes
# Dovecot 2.2 # Dovecot 2.2
#ssl_protocols = !SSLv3 #ssl_protocols = !SSLv3
@@ -47,7 +52,7 @@ mail_shared_explicit_inbox = yes
mail_prefetch_count = 30 mail_prefetch_count = 30
passdb { passdb {
driver = lua driver = lua
args = file=/etc/dovecot/lua/passwd-verify.lua blocking=yes args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes
result_success = return-ok result_success = return-ok
result_failure = continue result_failure = continue
result_internalfail = continue result_internalfail = continue
@@ -63,7 +68,7 @@ passdb {
# a return of the following passdb is mandatory # a return of the following passdb is mandatory
passdb { passdb {
driver = lua driver = lua
args = file=/etc/dovecot/lua/passwd-verify.lua blocking=yes args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes
} }
# Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing) # Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
service doveadm { service doveadm {
@@ -236,12 +241,15 @@ plugin {
mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem
mail_crypt_save_version = 2 mail_crypt_save_version = 2
# Enable compression while saving, lz4 Dovecot v2.2.11+ # Enable compression while saving, lz4 Dovecot v2.3.17+
zlib_save = lz4 zlib_save = lz4
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
mail_log_fields = uid box msgid size mail_log_fields = uid box msgid size
mail_log_cached_only = yes mail_log_cached_only = yes
# Try set mail_replica
!include_try /etc/dovecot/mail_replica.conf
} }
service quota-warning { service quota-warning {
executable = script /usr/local/bin/quota_notify.py executable = script /usr/local/bin/quota_notify.py
+7 -1
View File
@@ -13,6 +13,8 @@
ssl_session_timeout 1d; ssl_session_timeout 1d;
ssl_session_tickets off; ssl_session_tickets off;
include /etc/nginx/conf.d/includes/ssl_client_auth.conf;
add_header Strict-Transport-Security "max-age=15768000;"; add_header Strict-Transport-Security "max-age=15768000;";
add_header X-Content-Type-Options nosniff; add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block"; add_header X-XSS-Protection "1; mode=block";
@@ -101,6 +103,10 @@
include /etc/nginx/fastcgi_params; include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param TLS_SUCCESS $ssl_client_verify;
fastcgi_param TLS_ISSUER $ssl_client_i_dn;
fastcgi_param TLS_DN $ssl_client_s_dn;
fastcgi_param TLS_CERT $ssl_client_cert;
fastcgi_read_timeout 3600; fastcgi_read_timeout 3600;
fastcgi_send_timeout 3600; fastcgi_send_timeout 3600;
} }
@@ -114,7 +120,7 @@
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_redirect off; proxy_redirect off;
error_page 403 /_rspamderror.php; error_page 401 /_rspamderror.php;
} }
proxy_pass http://rspamd:11334/; proxy_pass http://rspamd:11334/;
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
@@ -0,0 +1,4 @@
ssl_verify_client optional;
ssl_client_certificate /etc/nginx/conf.d/client_cas.crt;
+23
View File
@@ -0,0 +1,23 @@
server {
listen 9082 ssl http2;
ssl_certificate /etc/ssl/mail/cert.pem;
ssl_certificate_key /etc/ssl/mail/key.pem;
index mailcowauth.php;
server_name _;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /mailcowauth;
client_max_body_size 10M;
location ~ \.php$ {
client_max_body_size 10M;
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass phpfpm:9001;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
proxy_cache_path /tmp levels=1:2 keys_zone=sogo:10m inactive=24h max_size=1g; proxy_cache_path /tmp levels=1:2 keys_zone=sogo:10m inactive=24h max_size=1g;
server_names_hash_bucket_size 64; server_names_hash_max_size 512;
server_names_hash_bucket_size 128;
map $http_x_forwarded_proto $client_req_scheme { map $http_x_forwarded_proto $client_req_scheme {
default $scheme; default $scheme;
@@ -1,2 +1,3 @@
listen ${HTTPS_PORT} ssl http2; listen ${HTTPS_PORT} ssl;
listen [::]:${HTTPS_PORT} ssl http2; listen [::]:${HTTPS_PORT} ssl;
http2 on;
+23
View File
@@ -0,0 +1,23 @@
apk add mariadb-client
# List client CA of all domains
CA_LIST="/etc/nginx/conf.d/client_cas.crt"
> "$CA_LIST"
# Define your SQL query
query="SELECT DISTINCT ssl_client_ca FROM domain WHERE ssl_client_ca IS NOT NULL;"
result=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "$query" -B -N)
if [ -n "$result" ]; then
echo "$result" | while IFS= read -r line; do
echo -e "$line"
done > $CA_LIST
#tail -n 1 "$CA_LIST" | wc -c | xargs -I {} truncate "$CA_LIST" -s -{}
echo "
ssl_verify_client optional;
ssl_client_certificate /etc/nginx/conf.d/client_cas.crt;
" > /etc/nginx/conf.d/includes/ssl_client_auth.conf
echo "SSL client CAs have been appended to $CA_LIST"
else
> /etc/nginx/conf.d/includes/ssl_client_auth.conf
echo "No SSL client CAs found"
fi
+222
View File
@@ -0,0 +1,222 @@
<?php
require_once(__DIR__ . '/../web/inc/vars.inc.php');
if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
}
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
// 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) {
logMsg("danger", $e->getMessage());
session_destroy();
exit;
}
// Init Redis
$redis = new Redis();
try {
if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
}
else {
$redis->connect('redis-mailcow', 6379);
}
}
catch (Exception $e) {
echo "Exiting: " . $e->getMessage();
session_destroy();
exit;
}
function logMsg($priority, $message, $task = "Keycloak Sync") {
global $redis;
$finalMsg = array(
"time" => time(),
"priority" => $priority,
"task" => $task,
"message" => $message
);
$redis->lPush('CRON_LOG', json_encode($finalMsg));
}
// Load core functions first
require_once __DIR__ . '/../web/inc/functions.inc.php';
require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
require_once __DIR__ . '/../web/inc/sessions.inc.php';
require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php';
require_once __DIR__ . '/../web/inc/functions.ratelimit.inc.php';
require_once __DIR__ . '/../web/inc/functions.acl.inc.php';
$_SESSION['mailcow_cc_username'] = "admin";
$_SESSION['mailcow_cc_role'] = "admin";
$_SESSION['acl']['tls_policy'] = "1";
$_SESSION['acl']['quarantine_notification'] = "1";
$_SESSION['acl']['quarantine_category'] = "1";
$_SESSION['acl']['ratelimit'] = "1";
$_SESSION['acl']['sogo_access'] = "1";
$_SESSION['acl']['protocol_access'] = "1";
$_SESSION['acl']['mailbox_relayhost'] = "1";
// Init Keycloak Provider
$iam_provider = identity_provider('init');
$iam_settings = identity_provider('get');
if (intval($iam_settings['periodic_sync']) != 1 && $iam_settings['import_users'] != 1) {
session_destroy();
exit;
}
// Set pagination variables
$start = 0;
$max = 25;
// lock sync if already running
$lock_file = '/tmp/iam-sync.lock';
if (file_exists($lock_file)) {
$lock_file_parts = explode("\n", file_get_contents($lock_file));
$pid = $lock_file_parts[0];
if (count($lock_file_parts) > 1){
$last_execution = $lock_file_parts[1];
$elapsed_time = (time() - $last_execution) / 60;
if ($elapsed_time < intval($iam_settings['sync_interval'])) {
logMsg("warning", "Sync not ready (".number_format((float)$elapsed_time, 2, '.', '')."min / ".$iam_settings['sync_interval']."min)");
session_destroy();
exit;
}
}
if (posix_kill($pid, 0)) {
logMsg("warning", "Sync is already running");
session_destroy();
exit;
} else {
unlink($lock_file);
}
}
$lock_file_handle = fopen($lock_file, 'w');
fwrite($lock_file_handle, getmypid());
fclose($lock_file_handle);
// Loop until all users have been retrieved
while (true) {
// Get admin access token
$admin_token = identity_provider("get-keycloak-admin-token");
// Make the API request to retrieve the users
$url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users?first=$start&max=$max";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Content-Type: application/json",
"Authorization: Bearer " . $admin_token
]);
$response = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code != 200){
logMsg("danger", "Recieved HTTP {$code}");
session_destroy();
exit;
}
try {
$response = json_decode($response, true);
} catch (Exception $e) {
logMsg("danger", $e->getMessage());
break;
}
if (!is_array($response)){
logMsg("danger", "Recieved malformed response from keycloak api");
break;
}
if (count($response) == 0) {
break;
}
// Process the batch of users
foreach ($response as $user) {
if (empty($user['email'])){
logMsg("warning", "No email address in keycloak found for user " . $user['name']);
continue;
}
if (!isset($user['attributes'])){
logMsg("warning", "No attributes in keycloak found for user " . $user['email']);
continue;
}
if (!isset($user['attributes']['mailcow_template']) ||
!is_array($user['attributes']['mailcow_template']) ||
count($user['attributes']['mailcow_template']) == 0) {
logMsg("warning", "No mailcow_template in keycloak found for user " . $user['email']);
continue;
}
$mailcow_template = $user['attributes']['mailcow_template'];
// try get mailbox user
$stmt = $pdo->prepare("SELECT `mailbox`.* FROM `mailbox`
INNER JOIN domain on mailbox.domain = domain.domain
WHERE `kind` NOT REGEXP 'location|thing|group'
AND `domain`.`active`='1'
AND `username` = :user");
$stmt->execute(array(':user' => $user['email']));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
// check if matching attribute mapping exists
$mbox_template = null;
foreach ($iam_settings['mappers'] as $index => $mapper){
if (in_array($mapper, $user['attributes']['mailcow_template'])) {
$mbox_template = $mapper;
break;
}
}
if (!$mbox_template){
logMsg("warning", "No matching mapper found for mailbox_template");
continue;
}
if (!$row && intval($iam_settings['import_users']) == 1){
// mailbox user does not exist, create...
logMsg("info", "Creating user " . $user['email']);
mailbox('add', 'mailbox_from_template', array(
'domain' => explode('@', $user['email'])[1],
'local_part' => explode('@', $user['email'])[0],
'authsource' => 'keycloak',
'template' => $mbox_template
));
} else if ($row) {
// mailbox user does exist, sync attribtues...
logMsg("info", "Syncing attributes for user " . $user['email']);
mailbox('edit', 'mailbox_from_template', array(
'username' => $user['email'],
'template' => $mbox_template
));
} else {
// skip mailbox user
logMsg("info", "Skipping user " . $user['email']);
}
sleep(0.025);
}
// Update the pagination variables for the next batch
$start += $max;
sleep(1);
}
logMsg("info", "DONE!");
// add last execution time to lock file
$lock_file_handle = fopen($lock_file, 'w');
fwrite($lock_file_handle, getmypid() . "\n" . time());
fclose($lock_file_handle);
session_destroy();
+2 -1
View File
@@ -12,7 +12,8 @@ if /^\s*Received: from.* \(.*rspamd-mailcow.*mailcow-network.*\).*\(Postcow\)/
REPLACE Received: from rspamd (rspamd $3) by $4 (Postcow) with $5 REPLACE Received: from rspamd (rspamd $3) by $4 (Postcow) with $5
endif endif
/^\s*X-Enigmail/ IGNORE /^\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-Originating-IP/ IGNORE
/^\s*X-Forward/ IGNORE /^\s*X-Forward/ IGNORE
# Not removing UA by default, might be signed # Not removing UA by default, might be signed
+5 -30
View File
@@ -11,6 +11,7 @@ smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtpd_relay_restrictions = permit_mynetworks, smtpd_relay_restrictions = permit_mynetworks,
permit_sasl_authenticated, permit_sasl_authenticated,
defer_unauth_destination defer_unauth_destination
smtpd_forbid_bare_newline = yes
# alias maps are auto-generated in postfix.sh on startup # alias maps are auto-generated in postfix.sh on startup
alias_maps = hash:/etc/aliases alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases alias_database = hash:/etc/aliases
@@ -40,34 +41,6 @@ postscreen_blacklist_action = drop
postscreen_cache_cleanup_interval = 24h postscreen_cache_cleanup_interval = 24h
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
postscreen_dnsbl_action = enforce postscreen_dnsbl_action = enforce
postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
hostkarma.junkemailfilter.com=127.0.0.1*-2
list.dnswl.org=127.0.[0..255].0*-2
list.dnswl.org=127.0.[0..255].1*-4
list.dnswl.org=127.0.[0..255].2*-6
list.dnswl.org=127.0.[0..255].3*-8
ix.dnsbl.manitu.net*2
bl.spamcop.net*2
bl.suomispam.net*2
hostkarma.junkemailfilter.com=127.0.0.2*3
hostkarma.junkemailfilter.com=127.0.0.4*2
hostkarma.junkemailfilter.com=127.0.1.2*1
backscatter.spameatingmonkey.net*2
bl.ipv6.spameatingmonkey.net*2
bl.spameatingmonkey.net*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
zen.spamhaus.org=127.0.0.[10;11]*8
zen.spamhaus.org=127.0.0.[4..7]*6
zen.spamhaus.org=127.0.0.3*4
zen.spamhaus.org=127.0.0.2*3
postscreen_dnsbl_threshold = 6 postscreen_dnsbl_threshold = 6
postscreen_dnsbl_ttl = 5m postscreen_dnsbl_ttl = 5m
postscreen_greet_action = enforce postscreen_greet_action = enforce
@@ -112,6 +85,7 @@ smtp_tls_security_level = dane
smtpd_data_restrictions = reject_unauth_pipelining, permit smtpd_data_restrictions = reject_unauth_pipelining, permit
smtpd_delay_reject = yes smtpd_delay_reject = yes
smtpd_error_sleep_time = 10s smtpd_error_sleep_time = 10s
smtpd_forbid_bare_newline = yes
smtpd_hard_error_limit = ${stress?1}${stress:5} smtpd_hard_error_limit = ${stress?1}${stress:5}
smtpd_helo_required = yes smtpd_helo_required = yes
smtpd_proxy_timeout = 600s smtpd_proxy_timeout = 600s
@@ -188,7 +162,8 @@ transport_maps = pcre:/opt/postfix/conf/custom_transport.pcre,
proxy:mysql:/opt/postfix/conf/sql/mysql_relay_ne.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_relay_ne.cf,
proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf
smtp_sasl_auth_soft_bounce = no smtp_sasl_auth_soft_bounce = no
postscreen_discard_ehlo_keywords = silent-discard, dsn postscreen_discard_ehlo_keywords = silent-discard, dsn, chunking
smtpd_discard_ehlo_keywords = chunking
compatibility_level = 2 compatibility_level = 2
smtputf8_enable = no smtputf8_enable = no
# Define protocols for SMTPS and submission service # Define protocols for SMTPS and submission service
@@ -197,4 +172,4 @@ smtps_smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients
# DO NOT EDIT ANYTHING BELOW # # DO NOT EDIT ANYTHING BELOW #
# User overrides # # Overrides #
+340 -149
View File
@@ -1,15 +1,21 @@
# Whitelist generated by Postwhite v3.4 on Mon 21 Mar 2022 06:50:26 PM CET # Whitelist generated by Postwhite v3.4 on Thu Feb 1 00:13:50 UTC 2024
# https://github.com/stevejenkins/postwhite/ # https://github.com/stevejenkins/postwhite/
# 1898 total rules # 2089 total rules
2a00:1450:4000::/36 permit 2a00:1450:4000::/36 permit
2a01:111:f400::/48 permit 2a01:111:f400::/48 permit
2a01:111:f403::/48 permit 2a01:111:f403:8000::/50 permit
2a01:4180:4050:0400::/64 permit 2a01:111:f403:8000::/51 permit
2a01:4180:4050:0800::/64 permit 2a01:111:f403::/49 permit
2a01:4180:4051:0400::/64 permit 2a01:111:f403:c000::/51 permit
2a01:4180:4051:0800::/64 permit 2a01:111:f403:f000::/52 permit
2a02:a60:0:5::/64 permit 2a02:a60:0:5::/64 permit
2c0f:fb50:4000::/36 permit 2c0f:fb50:4000::/36 permit
2.207.151.53 permit
3.70.123.177 permit
3.93.157.0/24 permit
3.129.120.190 permit
3.137.16.58 permit
3.210.190.0/24 permit
8.20.114.31 permit 8.20.114.31 permit
8.25.194.0/23 permit 8.25.194.0/23 permit
8.25.196.0/23 permit 8.25.196.0/23 permit
@@ -19,41 +25,53 @@
13.70.32.43 permit 13.70.32.43 permit
13.72.50.45 permit 13.72.50.45 permit
13.74.143.28 permit 13.74.143.28 permit
13.77.161.179 permit
13.78.233.182 permit 13.78.233.182 permit
13.92.31.129 permit 13.92.31.129 permit
13.110.208.0/21 permit 13.110.208.0/21 permit
13.110.209.0/24 permit
13.110.216.0/22 permit 13.110.216.0/22 permit
13.110.224.0/20 permit 13.110.224.0/20 permit
13.111.0.0/16 permit 13.111.0.0/16 permit
17.41.0.0/16 permit 15.200.21.50 permit
15.200.44.248 permit
15.200.201.185 permit
17.57.155.0/24 permit 17.57.155.0/24 permit
17.57.156.0/24 permit 17.57.156.0/24 permit
17.58.0.0/16 permit 17.58.0.0/16 permit
17.110.0.0/15 permit 18.156.89.250 permit
17.142.0.0/15 permit 18.157.243.190 permit
17.162.0.0/15 permit
17.164.0.0/16 permit
17.171.37.0/24 permit
17.172.0.0/16 permit
17.179.168.0/23 permit
18.194.95.56 permit 18.194.95.56 permit
18.198.96.88 permit 18.198.96.88 permit
20.47.149.138 permit 18.208.124.128/25 permit
20.48.0.0/12 permit 18.216.232.154 permit
18.234.1.244 permit
18.236.40.242 permit
20.51.6.32/30 permit
20.52.52.2 permit 20.52.52.2 permit
20.52.128.133 permit 20.52.128.133 permit
20.59.80.4/30 permit
20.63.210.192/28 permit 20.63.210.192/28 permit
20.64.0.0/10 permit 20.69.8.108/30 permit
20.70.246.20 permit
20.76.201.171 permit
20.83.222.104/30 permit
20.88.157.184/30 permit
20.94.180.64/28 permit 20.94.180.64/28 permit
20.97.34.220/30 permit
20.98.148.156/30 permit
20.98.194.68/30 permit
20.105.209.76/30 permit
20.107.239.64/30 permit
20.112.250.133 permit
20.118.139.208/30 permit
20.185.213.160/27 permit 20.185.213.160/27 permit
20.185.213.224/27 permit 20.185.213.224/27 permit
20.185.214.0/27 permit 20.185.214.0/27 permit
20.185.214.2 permit 20.185.214.2 permit
20.185.214.32/27 permit 20.185.214.32/27 permit
20.185.214.64/27 permit 20.185.214.64/27 permit
20.192.0.0/10 permit 20.231.239.246 permit
23.100.85.1 permit 20.236.44.162 permit
23.103.224.0/19 permit 23.103.224.0/19 permit
23.249.208.0/20 permit 23.249.208.0/20 permit
23.251.224.0/19 permit 23.251.224.0/19 permit
@@ -78,46 +96,33 @@
27.123.206.56/29 permit 27.123.206.56/29 permit
27.123.206.76/30 permit 27.123.206.76/30 permit
27.123.206.80/28 permit 27.123.206.80/28 permit
34.194.25.167 permit 31.25.48.222 permit
34.194.144.120 permit 34.195.217.107 permit
34.202.239.6 permit
34.212.163.75 permit 34.212.163.75 permit
34.215.104.144 permit
34.225.212.172 permit 34.225.212.172 permit
34.247.168.44 permit 34.247.168.44 permit
35.161.32.253 permit
35.167.93.243 permit
35.176.132.251 permit 35.176.132.251 permit
35.190.247.0/24 permit 35.190.247.0/24 permit
35.191.0.0/16 permit 35.191.0.0/16 permit
37.188.97.188 permit
37.218.248.47 permit 37.218.248.47 permit
37.218.249.47 permit 37.218.249.47 permit
37.218.251.62 permit 37.218.251.62 permit
39.156.163.64/29 permit 39.156.163.64/29 permit
40.71.187.0/24 permit 40.71.187.0/24 permit
40.76.4.15 permit
40.77.102.222 permit
40.92.0.0/15 permit 40.92.0.0/15 permit
40.97.116.82 permit 40.92.0.0/16 permit
40.97.128.194 permit
40.97.148.226 permit
40.97.153.146 permit
40.97.156.114 permit
40.97.160.2 permit
40.97.161.50 permit
40.97.164.146 permit
40.107.0.0/16 permit 40.107.0.0/16 permit
40.112.65.63 permit 40.112.65.63 permit
40.112.72.205 permit
40.113.200.201 permit
40.117.80.0/24 permit
40.121.71.46 permit
41.74.192.0/22 permit
41.74.196.0/22 permit
41.74.200.0/23 permit
41.74.204.0/23 permit
41.74.206.0/24 permit
42.159.163.81 permit
42.159.163.82 permit
42.159.163.83 permit
43.228.184.0/22 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.226.48.0/21 permit 46.226.48.0/21 permit
46.228.36.37 permit 46.228.36.37 permit
46.228.36.38/31 permit 46.228.36.38/31 permit
@@ -167,6 +172,8 @@
46.243.88.175 permit 46.243.88.175 permit
46.243.88.176 permit 46.243.88.176 permit
46.243.88.177 permit 46.243.88.177 permit
46.243.95.179 permit
46.243.95.180 permit
50.18.45.249 permit 50.18.45.249 permit
50.18.121.236 permit 50.18.121.236 permit
50.18.121.248 permit 50.18.121.248 permit
@@ -176,33 +183,37 @@
50.18.125.237 permit 50.18.125.237 permit
50.18.126.162 permit 50.18.126.162 permit
50.31.32.0/19 permit 50.31.32.0/19 permit
50.31.156.96/27 permit 50.56.130.220 permit
50.31.205.0/24 permit 50.56.130.221 permit
51.4.71.62 permit
51.4.72.0/24 permit
51.4.80.0/27 permit
51.5.72.0/24 permit
51.5.80.0/27 permit
51.137.58.21 permit 51.137.58.21 permit
51.140.75.55 permit 51.140.75.55 permit
51.144.100.179 permit 51.144.100.179 permit
51.163.158.0/24 permit
51.163.159.21 permit
52.5.230.59 permit 52.5.230.59 permit
52.27.5.72 permit 52.27.5.72 permit
52.27.28.47 permit 52.27.28.47 permit
52.33.191.91 permit 52.28.63.81 permit
52.36.138.31 permit 52.36.138.31 permit
52.37.142.146 permit 52.37.142.146 permit
52.38.191.253 permit 52.58.216.183 permit
52.41.64.145 permit 52.59.143.3 permit
52.60.41.5 permit 52.60.41.5 permit
52.60.115.116 permit 52.60.115.116 permit
52.61.91.9 permit
52.71.0.205 permit
52.82.172.0/22 permit 52.82.172.0/22 permit
52.94.124.0/28 permit 52.94.124.0/28 permit
52.95.48.152/29 permit 52.95.48.152/29 permit
52.95.49.88/29 permit 52.95.49.88/29 permit
52.96.91.34 permit
52.96.111.82 permit
52.96.214.50 permit
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.100.0.0/14 permit
52.103.0.0/17 permit
52.119.213.144/28 permit 52.119.213.144/28 permit
52.160.39.140 permit 52.160.39.140 permit
52.165.175.144 permit 52.165.175.144 permit
@@ -214,23 +225,29 @@
52.222.73.83 permit 52.222.73.83 permit
52.222.73.120 permit 52.222.73.120 permit
52.222.75.85 permit 52.222.75.85 permit
52.222.89.228 permit
52.234.172.96/28 permit 52.234.172.96/28 permit
52.236.28.240/28 permit 52.236.28.240/28 permit
52.237.141.173 permit
52.244.206.214 permit 52.244.206.214 permit
52.247.53.144 permit 52.247.53.144 permit
52.250.107.196 permit 52.250.107.196 permit
52.250.126.174 permit 52.250.126.174 permit
52.251.55.143 permit
54.90.148.255 permit 54.90.148.255 permit
54.156.255.69 permit
54.172.97.247 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.186.193.102 permit
54.191.223.5 permit 54.191.223.56 permit
54.194.61.95 permit 54.194.61.95 permit
54.195.113.45 permit 54.195.113.45 permit
54.213.20.246 permit
54.214.39.184 permit 54.214.39.184 permit
54.216.77.168 permit 54.216.77.168 permit
54.221.227.204 permit
54.240.0.0/18 permit 54.240.0.0/18 permit
54.240.64.0/19 permit 54.240.64.0/19 permit
54.240.96.0/19 permit 54.240.96.0/19 permit
@@ -238,7 +255,9 @@
54.244.54.130 permit 54.244.54.130 permit
54.244.242.0/24 permit 54.244.242.0/24 permit
54.246.232.180 permit 54.246.232.180 permit
54.255.61.23 permit
62.13.128.0/24 permit 62.13.128.0/24 permit
62.13.128.150 permit
62.13.129.128/25 permit 62.13.129.128/25 permit
62.13.136.0/22 permit 62.13.136.0/22 permit
62.13.140.0/22 permit 62.13.140.0/22 permit
@@ -247,29 +266,32 @@
62.13.150.0/23 permit 62.13.150.0/23 permit
62.13.152.0/23 permit 62.13.152.0/23 permit
62.17.146.128/26 permit 62.17.146.128/26 permit
62.140.7.0/24 permit 62.179.121.0/24 permit
62.140.10.21 permit 62.201.172.0/27 permit
62.201.172.32/27 permit
62.253.227.114 permit
63.32.13.159 permit 63.32.13.159 permit
63.80.14.0/23 permit 63.80.14.0/23 permit
63.111.28.137 permit
63.128.21.0/24 permit 63.128.21.0/24 permit
63.143.57.128/25 permit 63.143.57.128/25 permit
63.143.59.128/25 permit 63.143.59.128/25 permit
64.18.0.0/20 permit 64.18.0.0/20 permit
64.20.241.45 permit 64.20.241.45 permit
64.34.47.128/27 permit 64.69.212.0/24 permit
64.34.57.192/26 permit
64.71.149.160/28 permit 64.71.149.160/28 permit
64.79.155.0/24 permit 64.79.155.0/24 permit
64.79.155.192 permit
64.79.155.193 permit
64.79.155.205 permit
64.79.155.206 permit
64.89.44.85 permit 64.89.44.85 permit
64.89.45.80 permit 64.89.45.80 permit
64.89.45.194 permit 64.89.45.194 permit
64.89.45.196 permit 64.89.45.196 permit
64.95.144.196 permit
64.127.115.252 permit 64.127.115.252 permit
64.132.88.0/23 permit 64.132.88.0/23 permit
64.132.92.0/24 permit 64.132.92.0/24 permit
64.135.77.0/24 permit
64.135.83.0/24 permit
64.147.123.17 permit 64.147.123.17 permit
64.147.123.18 permit 64.147.123.18 permit
64.147.123.19 permit 64.147.123.19 permit
@@ -281,28 +303,35 @@
64.147.123.27 permit 64.147.123.27 permit
64.147.123.28 permit 64.147.123.28 permit
64.147.123.29 permit 64.147.123.29 permit
64.147.123.128/27 permit
64.207.219.7 permit 64.207.219.7 permit
64.207.219.8 permit 64.207.219.8 permit
64.207.219.9 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.13 permit
64.207.219.14 permit 64.207.219.14 permit
64.207.219.15 permit 64.207.219.15 permit
64.207.219.71 permit 64.207.219.71 permit
64.207.219.72 permit 64.207.219.72 permit
64.207.219.73 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.77 permit
64.207.219.78 permit 64.207.219.78 permit
64.207.219.79 permit 64.207.219.79 permit
64.207.219.135 permit 64.207.219.135 permit
64.207.219.136 permit 64.207.219.136 permit
64.207.219.137 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.141 permit
64.207.219.142 permit 64.207.219.142 permit
64.207.219.143 permit 64.207.219.143 permit
64.233.160.0/19 permit 64.233.160.0/19 permit
65.38.115.76 permit
65.38.115.84 permit
65.39.215.0/24 permit
65.52.80.137 permit 65.52.80.137 permit
65.54.51.64/26 permit 65.54.51.64/26 permit
65.54.61.64/26 permit 65.54.61.64/26 permit
@@ -342,6 +371,10 @@
66.111.4.225 permit 66.111.4.225 permit
66.111.4.229 permit 66.111.4.229 permit
66.111.4.230 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.162.193.226/31 permit
66.163.184.0/21 permit 66.163.184.0/21 permit
66.163.184.0/24 permit 66.163.184.0/24 permit
@@ -371,9 +404,8 @@
66.196.81.228/30 permit 66.196.81.228/30 permit
66.196.81.232/31 permit 66.196.81.232/31 permit
66.196.81.234 permit 66.196.81.234 permit
66.211.168.230/31 permit 66.211.170.88/29 permit
66.211.170.86/31 permit 66.211.184.0/23 permit
66.211.170.88/30 permit
66.218.74.64/30 permit 66.218.74.64/30 permit
66.218.74.68/31 permit 66.218.74.68/31 permit
66.218.75.112/30 permit 66.218.75.112/30 permit
@@ -445,6 +477,8 @@
68.142.230.72/30 permit 68.142.230.72/30 permit
68.142.230.76/31 permit 68.142.230.76/31 permit
68.142.230.78 permit 68.142.230.78 permit
68.232.140.138 permit
68.232.157.143 permit
68.232.192.0/20 permit 68.232.192.0/20 permit
69.63.178.128/25 permit 69.63.178.128/25 permit
69.63.181.0/24 permit 69.63.181.0/24 permit
@@ -452,6 +486,10 @@
69.65.42.195 permit 69.65.42.195 permit
69.65.49.192/29 permit 69.65.49.192/29 permit
69.72.32.0/20 permit 69.72.32.0/20 permit
69.72.40.93 permit
69.72.40.94/31 permit
69.72.40.96/30 permit
69.72.47.205 permit
69.147.84.227 permit 69.147.84.227 permit
69.162.98.0/24 permit 69.162.98.0/24 permit
69.169.224.0/20 permit 69.169.224.0/20 permit
@@ -460,7 +498,7 @@
70.37.151.128/25 permit 70.37.151.128/25 permit
70.42.149.0/24 permit 70.42.149.0/24 permit
70.42.149.35 permit 70.42.149.35 permit
72.3.185.0/24 permit 72.3.237.64/28 permit
72.14.192.0/18 permit 72.14.192.0/18 permit
72.21.192.0/19 permit 72.21.192.0/19 permit
72.21.217.142 permit 72.21.217.142 permit
@@ -522,15 +560,11 @@
72.30.239.228/31 permit 72.30.239.228/31 permit
72.30.239.244/30 permit 72.30.239.244/30 permit
72.30.239.248/31 permit 72.30.239.248/31 permit
72.32.154.0/24 permit
72.32.217.0/24 permit
72.32.243.0/24 permit
72.34.168.76 permit 72.34.168.76 permit
72.34.168.80 permit 72.34.168.80 permit
72.34.168.85 permit 72.34.168.85 permit
72.34.168.86 permit 72.34.168.86 permit
72.52.72.32/28 permit 72.52.72.32/28 permit
72.52.72.36 permit
74.6.128.0/21 permit 74.6.128.0/21 permit
74.6.128.0/24 permit 74.6.128.0/24 permit
74.6.129.0/24 permit 74.6.129.0/24 permit
@@ -558,8 +592,13 @@
74.112.67.243 permit 74.112.67.243 permit
74.125.0.0/16 permit 74.125.0.0/16 permit
74.202.227.40 permit 74.202.227.40 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 74.209.250.0/24 permit
74.209.250.12 permit 75.2.70.75 permit
76.223.128.0/19 permit
76.223.176.0/20 permit 76.223.176.0/20 permit
77.238.176.0/22 permit 77.238.176.0/22 permit
77.238.176.0/24 permit 77.238.176.0/24 permit
@@ -582,8 +621,25 @@
77.238.189.142 permit 77.238.189.142 permit
77.238.189.146/31 permit 77.238.189.146/31 permit
77.238.189.148/30 permit 77.238.189.148/30 permit
81.7.169.128/25 permit
81.223.46.0/27 permit 81.223.46.0/27 permit
84.16.77.1 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 85.158.136.0/21 permit
86.61.88.25 permit 86.61.88.25 permit
87.198.219.130 permit 87.198.219.130 permit
@@ -624,11 +680,9 @@
87.248.117.201 permit 87.248.117.201 permit
87.248.117.202 permit 87.248.117.202 permit
87.248.117.205 permit 87.248.117.205 permit
87.252.219.254 permit
87.253.232.0/21 permit 87.253.232.0/21 permit
89.22.108.0/24 permit 89.22.108.0/24 permit
91.220.42.0/24 permit 91.211.240.0/22 permit
94.236.119.0/26 permit
94.245.112.0/27 permit 94.245.112.0/27 permit
94.245.112.10/31 permit 94.245.112.10/31 permit
95.131.104.0/21 permit 95.131.104.0/21 permit
@@ -638,6 +692,7 @@
96.43.148.64/28 permit 96.43.148.64/28 permit
96.43.148.64/31 permit 96.43.148.64/31 permit
96.43.151.64/28 permit 96.43.151.64/28 permit
98.97.248.0/21 permit
98.136.44.181 permit 98.136.44.181 permit
98.136.44.182/31 permit 98.136.44.182/31 permit
98.136.44.184 permit 98.136.44.184 permit
@@ -1141,25 +1196,22 @@
98.139.245.208/30 permit 98.139.245.208/30 permit
98.139.245.212/31 permit 98.139.245.212/31 permit
99.78.197.208/28 permit 99.78.197.208/28 permit
99.83.190.102 permit
103.2.140.0/22 permit 103.2.140.0/22 permit
103.9.8.121 permit
103.9.8.122 permit
103.9.8.123 permit
103.9.96.0/22 permit 103.9.96.0/22 permit
103.13.69.0/24 permit 103.28.42.0/24 permit
103.47.204.0/22 permit 103.47.204.0/22 permit
103.96.21.0/24 permit
103.96.23.0/24 permit
103.151.192.0/23 permit 103.151.192.0/23 permit
103.237.104.0/22 permit 103.168.172.128/27 permit
104.43.243.237 permit 104.43.243.237 permit
104.44.112.128/25 permit
104.47.0.0/17 permit 104.47.0.0/17 permit
104.47.20.0/23 permit
104.47.75.0/24 permit
104.47.108.0/23 permit
104.130.96.0/28 permit 104.130.96.0/28 permit
104.130.122.0/23 permit 104.130.122.0/23 permit
104.214.25.77 permit 104.214.25.77 permit
104.215.148.63 permit
104.215.186.3 permit
104.245.209.192/26 permit
106.10.144.64/27 permit 106.10.144.64/27 permit
106.10.144.100/31 permit 106.10.144.100/31 permit
106.10.144.103 permit 106.10.144.103 permit
@@ -1320,9 +1372,9 @@
117.120.16.0/21 permit 117.120.16.0/21 permit
119.42.242.52/31 permit 119.42.242.52/31 permit
119.42.242.156 permit 119.42.242.156 permit
121.244.91.48 permit
122.15.156.182 permit
123.126.78.64/29 permit 123.126.78.64/29 permit
124.47.150.0/24 permit
124.47.189.0/24 permit
124.108.96.0/24 permit 124.108.96.0/24 permit
124.108.96.24/31 permit 124.108.96.24/31 permit
124.108.96.28/31 permit 124.108.96.28/31 permit
@@ -1335,20 +1387,40 @@
128.127.70.0/26 permit 128.127.70.0/26 permit
128.245.0.0/20 permit 128.245.0.0/20 permit
128.245.64.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
128.245.242.18 permit
128.245.243.0/24 permit
128.245.244.0/24 permit
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.77.70 permit
129.41.169.249 permit 129.41.169.249 permit
129.80.5.164 permit
129.80.67.121 permit
129.145.74.12 permit
129.146.88.28 permit
129.146.147.105 permit
129.146.236.58 permit 129.146.236.58 permit
129.151.67.221 permit
129.153.62.216 permit
129.153.104.71 permit
129.153.168.146 permit
129.153.190.200 permit
129.153.194.228 permit 129.153.194.228 permit
129.159.87.137 permit 129.159.87.137 permit
129.213.195.191 permit
130.61.9.72 permit 130.61.9.72 permit
130.162.39.83 permit
130.211.0.0/22 permit 130.211.0.0/22 permit
130.248.172.0/24 permit
130.248.173.0/24 permit
131.107.0.0/16 permit
131.253.30.0/24 permit 131.253.30.0/24 permit
131.253.121.0/26 permit 131.253.121.0/26 permit
131.253.121.20 permit
131.253.121.52 permit
132.145.13.209 permit 132.145.13.209 permit
132.226.26.225 permit 132.226.26.225 permit
132.226.49.32 permit 132.226.49.32 permit
@@ -1358,45 +1430,70 @@
134.170.141.64/26 permit 134.170.141.64/26 permit
134.170.143.0/24 permit 134.170.143.0/24 permit
134.170.174.0/24 permit 134.170.174.0/24 permit
135.84.80.192/26 permit 135.84.80.0/24 permit
135.84.81.0/24 permit
135.84.82.0/24 permit 135.84.82.0/24 permit
135.84.83.0/24 permit
135.84.216.0/22 permit 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.182.0/23 permit
136.143.184.0/24 permit 136.143.184.0/24 permit
136.143.188.0/24 permit 136.143.188.0/24 permit
136.143.190.0/23 permit
136.147.128.0/20 permit 136.147.128.0/20 permit
136.147.135.0/24 permit 136.147.135.0/24 permit
136.147.176.0/20 permit 136.147.176.0/20 permit
136.147.176.0/24 permit 136.147.176.0/24 permit
136.147.182.0/24 permit 136.147.182.0/24 permit
136.179.50.206 permit
138.91.172.26 permit 138.91.172.26 permit
139.60.152.0/22 permit 139.60.152.0/22 permit
139.178.64.159 permit 139.138.35.44 permit
139.178.64.195 permit 139.138.46.121 permit
139.138.46.176 permit
139.138.46.219 permit
139.138.57.55 permit
139.138.58.119 permit
139.180.17.0/24 permit
141.148.159.229 permit
141.193.32.0/23 permit 141.193.32.0/23 permit
143.55.224.0/21 permit 143.55.224.0/21 permit
143.55.232.0/22 permit 143.55.232.0/22 permit
143.55.236.0/22 permit 143.55.236.0/22 permit
143.244.80.0/20 permit
144.24.6.140 permit
144.34.8.247 permit
144.34.9.247 permit
144.34.32.247 permit
144.34.33.247 permit
144.178.36.0/24 permit 144.178.36.0/24 permit
144.178.38.0/24 permit 144.178.38.0/24 permit
145.253.228.160/29 permit
145.253.239.128/29 permit
146.20.14.105 permit
146.20.14.107 permit
146.20.112.0/26 permit 146.20.112.0/26 permit
146.20.113.0/24 permit 146.20.113.0/24 permit
146.20.191.0/24 permit 146.20.191.0/24 permit
146.20.215.0/24 permit 146.20.215.0/24 permit
146.101.78.0/24 permit 146.20.215.182 permit
147.75.65.173 permit 146.88.28.0/24 permit
147.75.65.174 permit
147.75.98.190 permit
147.160.158.0/24 permit
147.243.1.47 permit 147.243.1.47 permit
147.243.1.48 permit 147.243.1.48 permit
147.243.1.153 permit 147.243.1.153 permit
147.243.128.24 permit 147.243.128.24 permit
147.243.128.26 permit 147.243.128.26 permit
148.105.0.14 permit 148.105.0.0/16 permit
148.105.8.0/21 permit 148.105.8.0/21 permit
149.72.0.0/16 permit 149.72.0.0/16 permit
149.72.248.236 permit
149.97.173.180 permit
150.230.98.160 permit
152.67.105.195 permit 152.67.105.195 permit
152.69.200.236 permit
155.248.208.51 permit
157.55.0.192/26 permit 157.55.0.192/26 permit
157.55.1.128/26 permit 157.55.1.128/26 permit
157.55.2.0/25 permit 157.55.2.0/25 permit
@@ -1412,37 +1509,58 @@
157.56.232.0/21 permit 157.56.232.0/21 permit
157.56.240.0/20 permit 157.56.240.0/20 permit
157.56.248.0/21 permit 157.56.248.0/21 permit
157.58.30.128/25 permit
157.58.196.96/29 permit 157.58.196.96/29 permit
157.58.249.3 permit 157.58.249.3 permit
157.151.208.65 permit 157.151.208.65 permit
157.255.1.64/29 permit 157.255.1.64/29 permit
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.0/24 permit
159.92.157.16 permit
159.92.157.17 permit
159.92.157.18 permit
159.92.158.0/24 permit 159.92.158.0/24 permit
159.92.159.0/24 permit 159.92.159.0/24 permit
159.92.160.0/24 permit 159.92.160.0/24 permit
159.92.161.0/24 permit 159.92.161.0/24 permit
159.92.162.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 159.135.132.128/25 permit
159.135.140.80/29 permit 159.135.140.80/29 permit
159.135.224.0/20 permit 159.135.224.0/20 permit
159.135.228.10 permit
159.183.0.0/16 permit 159.183.0.0/16 permit
160.1.62.192 permit
161.38.192.0/20 permit 161.38.192.0/20 permit
161.38.204.0/22 permit 161.38.204.0/22 permit
161.71.32.0/19 permit 161.71.32.0/19 permit
161.71.64.0/20 permit 161.71.64.0/20 permit
162.208.119.181 permit
162.247.216.0/22 permit 162.247.216.0/22 permit
163.47.180.0/22 permit
163.47.180.0/23 permit 163.47.180.0/23 permit
163.114.130.16 permit 163.114.130.16 permit
163.114.132.120 permit 163.114.132.120 permit
164.177.132.168 permit
164.177.132.169 permit
164.177.132.170 permit
164.177.132.171 permit
165.173.128.0/24 permit
166.78.68.0/22 permit 166.78.68.0/22 permit
166.78.68.221 permit 166.78.68.221 permit
166.78.69.146 permit
166.78.69.169 permit 166.78.69.169 permit
166.78.69.170 permit 166.78.69.170 permit
166.78.71.131 permit 166.78.71.131 permit
167.89.0.0/17 permit 167.89.0.0/17 permit
167.89.46.159 permit 167.89.46.159 permit
167.89.54.103 permit
167.89.64.9 permit 167.89.64.9 permit
167.89.65.0 permit 167.89.65.0 permit
167.89.65.53 permit 167.89.65.53 permit
@@ -1457,10 +1575,18 @@
167.216.129.210 permit 167.216.129.210 permit
167.216.131.180 permit 167.216.131.180 permit
167.220.67.232/29 permit 167.220.67.232/29 permit
167.220.67.238 permit
168.138.5.36 permit 168.138.5.36 permit
168.138.73.51 permit
168.245.0.0/17 permit 168.245.0.0/17 permit
168.245.12.252 permit
168.245.46.9 permit
168.245.127.231 permit
169.148.129.0/24 permit
169.148.131.0/24 permit
169.148.142.10 permit
169.148.144.0/25 permit
170.10.68.0/22 permit 170.10.68.0/22 permit
170.10.128.0/24 permit
170.10.129.0/24 permit 170.10.129.0/24 permit
170.10.133.0/24 permit 170.10.133.0/24 permit
172.217.0.0/19 permit 172.217.0.0/19 permit
@@ -1475,10 +1601,8 @@
173.194.0.0/16 permit 173.194.0.0/16 permit
173.203.79.182 permit 173.203.79.182 permit
173.203.81.39 permit 173.203.81.39 permit
173.224.160.128/25 permit
173.224.160.188 permit
173.224.161.128/25 permit 173.224.161.128/25 permit
173.228.155.0/24 permit 173.224.165.0/26 permit
174.36.84.8/29 permit 174.36.84.8/29 permit
174.36.84.16/29 permit 174.36.84.16/29 permit
174.36.84.32/29 permit 174.36.84.32/29 permit
@@ -1491,27 +1615,27 @@
174.36.114.152/29 permit 174.36.114.152/29 permit
174.37.67.28/30 permit 174.37.67.28/30 permit
174.129.203.189 permit 174.129.203.189 permit
175.41.215.51 permit
176.32.105.0/24 permit 176.32.105.0/24 permit
176.32.127.0/24 permit 176.32.127.0/24 permit
178.236.10.128/26 permit 178.236.10.128/26 permit
180.189.28.0/24 permit
182.50.76.0/22 permit 182.50.76.0/22 permit
182.50.78.64/28 permit 182.50.78.64/28 permit
183.240.219.64/29 permit 183.240.219.64/29 permit
185.4.120.0/23 permit
185.4.122.0/24 permit
185.12.80.0/22 permit 185.12.80.0/22 permit
185.28.196.0/22 permit
185.58.84.93 permit 185.58.84.93 permit
185.58.85.0/24 permit
185.58.86.0/24 permit
185.72.128.75 permit
185.72.128.76 permit
185.72.128.80 permit
185.80.93.204 permit 185.80.93.204 permit
185.80.93.227 permit 185.80.93.227 permit
185.80.95.31 permit 185.80.95.31 permit
185.90.20.0/22 permit
185.189.236.0/22 permit 185.189.236.0/22 permit
185.211.120.0/22 permit 185.211.120.0/22 permit
185.250.236.0/22 permit 185.250.236.0/22 permit
185.250.239.148 permit
185.250.239.168 permit
185.250.239.190 permit
188.125.68.132 permit 188.125.68.132 permit
188.125.68.152/31 permit 188.125.68.152/31 permit
188.125.68.156 permit 188.125.68.156 permit
@@ -1563,7 +1687,7 @@
188.125.85.238 permit 188.125.85.238 permit
188.172.128.0/20 permit 188.172.128.0/20 permit
192.0.64.0/18 permit 192.0.64.0/18 permit
192.28.128.0/18 permit 192.18.139.154 permit
192.30.252.0/22 permit 192.30.252.0/22 permit
192.64.236.0/24 permit 192.64.236.0/24 permit
192.64.237.0/24 permit 192.64.237.0/24 permit
@@ -1579,17 +1703,17 @@
192.254.113.10 permit 192.254.113.10 permit
192.254.113.101 permit 192.254.113.101 permit
192.254.114.176 permit 192.254.114.176 permit
192.254.118.63 permit
193.7.206.0/25 permit
193.7.207.0/25 permit
193.109.254.0/23 permit 193.109.254.0/23 permit
193.122.128.100 permit 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.64.234.129 permit
194.104.109.0/24 permit
194.104.111.0/24 permit
194.106.220.0/23 permit 194.106.220.0/23 permit
194.113.24.0/22 permit
194.154.193.192/27 permit 194.154.193.192/27 permit
195.130.217.0/24 permit 195.4.92.0/23 permit
195.54.172.0/23 permit
195.234.109.226 permit 195.234.109.226 permit
195.245.230.0/23 permit 195.245.230.0/23 permit
198.2.128.0/18 permit 198.2.128.0/18 permit
@@ -1605,19 +1729,25 @@
198.37.144.0/20 permit 198.37.144.0/20 permit
198.37.152.186 permit 198.37.152.186 permit
198.61.254.0/23 permit 198.61.254.0/23 permit
198.61.254.21 permit
198.61.254.231 permit 198.61.254.231 permit
198.74.56.28 permit
198.178.234.57 permit 198.178.234.57 permit
198.244.48.0/20 permit
198.244.60.0/22 permit
198.245.80.0/20 permit 198.245.80.0/20 permit
198.245.81.0/24 permit 198.245.81.0/24 permit
199.15.176.173 permit
199.15.212.0/22 permit
199.15.213.187 permit 199.15.213.187 permit
199.15.226.37 permit 199.15.226.37 permit
199.16.156.0/22 permit 199.16.156.0/22 permit
199.33.145.1 permit 199.33.145.1 permit
199.33.145.32 permit 199.33.145.32 permit
199.34.22.36 permit
199.59.148.0/22 permit 199.59.148.0/22 permit
199.67.80.2 permit
199.67.82.2 permit
199.67.84.0/24 permit
199.67.86.0/24 permit
199.67.88.0/24 permit
199.101.161.130 permit 199.101.161.130 permit
199.101.162.0/25 permit 199.101.162.0/25 permit
199.122.120.0/21 permit 199.122.120.0/21 permit
@@ -1630,8 +1760,10 @@
202.177.148.110 permit 202.177.148.110 permit
203.31.36.0/22 permit 203.31.36.0/22 permit
203.32.4.25 permit 203.32.4.25 permit
203.55.21.0/24 permit
203.81.17.0/24 permit 203.81.17.0/24 permit
203.122.32.250 permit 203.122.32.250 permit
203.145.57.160/27 permit
203.188.194.32 permit 203.188.194.32 permit
203.188.194.151 permit 203.188.194.151 permit
203.188.194.203 permit 203.188.194.203 permit
@@ -1666,28 +1798,33 @@
203.209.230.76/31 permit 203.209.230.76/31 permit
204.11.168.0/21 permit 204.11.168.0/21 permit
204.13.11.48/29 permit 204.13.11.48/29 permit
204.13.11.48/30 permit
204.14.232.0/21 permit 204.14.232.0/21 permit
204.14.232.64/28 permit 204.14.232.64/28 permit
204.14.234.64/28 permit 204.14.234.64/28 permit
204.29.186.0/23 permit 204.29.186.0/23 permit
204.75.142.0/24 permit
204.79.197.212 permit 204.79.197.212 permit
204.92.114.187 permit 204.92.114.187 permit
204.92.114.203 permit 204.92.114.203 permit
204.92.114.204/31 permit 204.92.114.204/31 permit
204.132.224.66 permit
204.141.32.0/23 permit 204.141.32.0/23 permit
204.141.42.0/23 permit 204.141.42.0/23 permit
204.153.121.0/24 permit 204.220.160.0/20 permit
204.232.168.0/24 permit 204.232.168.0/24 permit
205.139.110.0/24 permit 205.139.110.0/24 permit
205.201.128.0/20 permit 205.201.128.0/20 permit
205.201.131.128/25 permit 205.201.131.128/25 permit
205.201.134.128/25 permit 205.201.134.128/25 permit
205.201.136.0/23 permit 205.201.136.0/23 permit
205.201.137.229 permit
205.201.139.0/24 permit 205.201.139.0/24 permit
205.207.104.0/22 permit 205.207.104.0/22 permit
205.207.104.108 permit
205.220.167.17 permit 205.220.167.17 permit
205.220.167.98 permit
205.220.179.17 permit 205.220.179.17 permit
205.220.179.98 permit
205.251.233.32 permit 205.251.233.32 permit
205.251.233.36 permit 205.251.233.36 permit
206.25.247.143 permit 206.25.247.143 permit
@@ -1715,7 +1852,8 @@
207.67.98.192/27 permit 207.67.98.192/27 permit
207.68.176.0/26 permit 207.68.176.0/26 permit
207.68.176.96/27 permit 207.68.176.96/27 permit
207.82.80.0/24 permit 207.97.204.96 permit
207.97.204.97 permit
207.126.144.0/20 permit 207.126.144.0/20 permit
207.171.160.0/19 permit 207.171.160.0/19 permit
207.211.30.64/26 permit 207.211.30.64/26 permit
@@ -1723,6 +1861,7 @@
207.211.31.0/25 permit 207.211.31.0/25 permit
207.211.41.113 permit 207.211.41.113 permit
207.218.90.0/24 permit 207.218.90.0/24 permit
207.218.90.122 permit
207.250.68.0/24 permit 207.250.68.0/24 permit
208.40.232.70 permit 208.40.232.70 permit
208.43.21.28/30 permit 208.43.21.28/30 permit
@@ -1758,8 +1897,10 @@
208.71.42.212/31 permit 208.71.42.212/31 permit
208.71.42.214 permit 208.71.42.214 permit
208.72.249.240/29 permit 208.72.249.240/29 permit
208.74.204.0/22 permit
208.74.204.9 permit 208.74.204.9 permit
208.75.120.0/22 permit 208.75.120.0/22 permit
208.75.121.246 permit
208.75.122.246 permit 208.75.122.246 permit
208.82.237.96/29 permit 208.82.237.96/29 permit
208.82.237.104/31 permit 208.82.237.104/31 permit
@@ -1773,14 +1914,12 @@
209.46.117.168 permit 209.46.117.168 permit
209.46.117.179 permit 209.46.117.179 permit
209.61.151.0/24 permit 209.61.151.0/24 permit
209.61.151.236 permit
209.61.151.249 permit
209.61.151.251 permit
209.67.98.46 permit 209.67.98.46 permit
209.67.98.59 permit 209.67.98.59 permit
209.85.128.0/17 permit 209.85.128.0/17 permit
212.4.136.0/26 permit
212.25.240.80 permit
212.25.240.83 permit
212.25.240.84/31 permit
212.25.240.88 permit
212.82.96.0/24 permit 212.82.96.0/24 permit
212.82.96.32/27 permit 212.82.96.32/27 permit
212.82.96.64/29 permit 212.82.96.64/29 permit
@@ -1821,8 +1960,41 @@
212.82.111.228/31 permit 212.82.111.228/31 permit
212.82.111.230 permit 212.82.111.230 permit
212.123.28.40 permit 212.123.28.40 permit
213.167.75.0/25 permit 212.227.15.3 permit
213.167.81.0/25 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.199.128.139 permit 213.199.128.139 permit
213.199.128.145 permit 213.199.128.145 permit
213.199.138.181 permit 213.199.138.181 permit
@@ -1861,6 +2033,10 @@
216.46.168.0/24 permit 216.46.168.0/24 permit
216.58.192.0/19 permit 216.58.192.0/19 permit
216.66.217.240/29 permit 216.66.217.240/29 permit
216.71.138.33 permit
216.71.152.207 permit
216.71.154.29 permit
216.71.155.89 permit
216.74.162.13 permit 216.74.162.13 permit
216.74.162.14 permit 216.74.162.14 permit
216.82.240.0/20 permit 216.82.240.0/20 permit
@@ -1870,33 +2046,48 @@
216.109.114.0/24 permit 216.109.114.0/24 permit
216.109.114.32/27 permit 216.109.114.32/27 permit
216.109.114.64/29 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.128.126.97 permit
216.136.162.65 permit 216.136.162.65 permit
216.136.162.120/29 permit 216.136.162.120/29 permit
216.136.168.80/28 permit 216.136.168.80/28 permit
216.145.221.0/24 permit
216.198.0.0/18 permit 216.198.0.0/18 permit
216.203.30.55 permit 216.203.30.55 permit
216.203.33.178/31 permit 216.203.33.178/31 permit
216.205.24.0/24 permit 216.205.24.0/24 permit
216.221.160.0/19 permit
216.239.32.0/19 permit 216.239.32.0/19 permit
217.72.192.77 permit
217.72.192.78 permit
217.77.141.52 permit 217.77.141.52 permit
217.77.141.59 permit 217.77.141.59 permit
217.175.194.0/24 permit
222.73.195.64/29 permit 222.73.195.64/29 permit
223.165.113.0/24 permit 223.165.113.0/24 permit
223.165.115.0/24 permit 223.165.115.0/24 permit
223.165.118.0/23 permit 223.165.118.0/23 permit
223.165.120.0/23 permit 223.165.120.0/23 permit
2001:0868:0100:0600::/64 permit
2001:4860:4000::/36 permit 2001:4860:4000::/36 permit
2001:748:100:40::2:0/112 permit
2404:6800:4000::/36 permit 2404:6800:4000::/36 permit
2603:1010:3:3::5b permit
2603:1020:201:10::10f permit
2603:1030:20e:3::23c permit
2603:1030:b:3::152 permit
2603:1030:c02:8::14 permit
2607:f8b0:4000::/36 permit 2607:f8b0:4000::/36 permit
2620:109:c003:104::215 permit
2620:109:c003:104::/64 permit 2620:109:c003:104::/64 permit
2620:109:c006:104::215 permit 2620:109:c003:104::215 permit
2620:109:c006:104::/64 permit 2620:109:c006:104::/64 permit
2620:109:c006:104::215 permit
2620:109:c00d:104::/64 permit 2620:109:c00d:104::/64 permit
2620:10d:c090:450::120 permit 2620:10d:c090:400::8:1 permit
2620:10d:c091:450::16 permit 2620:10d:c091:400::8:1 permit
2620:119:50c0:207::215 permit
2620:119:50c0:207::/64 permit 2620:119:50c0:207::/64 permit
2620:119:50c0:207::215 permit
2800:3f0:4000::/36 permit 2800:3f0:4000::/36 permit
194.25.134.0/24 permit # t-online.de 194.25.134.0/24 permit # t-online.de
+2 -1
View File
@@ -27,4 +27,5 @@
#197518 2 #Rackmarkt SL, Spain #197518 2 #Rackmarkt SL, Spain
#197695 2 #Domain names registrar REG.RU Ltd, Russia #197695 2 #Domain names registrar REG.RU Ltd, Russia
#198068 2 #P.A.G.M. OU, Estonia #198068 2 #P.A.G.M. OU, Estonia
#201942 5 #Soltia Consulting SL, Spain #201942 5 #Soltia Consulting SL, Spain
#213373 4 #IP Connect Inc
+92
View File
@@ -0,0 +1,92 @@
<?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 {
$stmt = $pdo->prepare("SELECT `plain`, `html`, `mbox_exclude`, `skip_replies` FROM `domain_wide_footer`
WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain
));
$footer = $stmt->fetch(PDO::FETCH_ASSOC);
if (in_array($from, json_decode($footer['mbox_exclude']))){
$footer = false;
}
if (empty($footer)){
echo $empty_footer;
exit;
}
error_log("FOOTER: " . json_encode($footer) . PHP_EOL);
$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);
+36
View File
@@ -68,3 +68,39 @@ WL_FWD_HOST {
ENCRYPTED_CHAT { ENCRYPTED_CHAT {
expression = "CHAT_VERSION_HEADER & ENCRYPTED_PGP"; expression = "CHAT_VERSION_HEADER & ENCRYPTED_PGP";
} }
CLAMD_SPAM_FOUND {
expression = "CLAM_SECI_SPAM & !MAILCOW_WHITE";
description = "Probably Spam, Securite Spam Flag set through ClamAV";
score = 5;
}
CLAMD_BAD_PDF {
expression = "CLAM_SECI_PDF & !MAILCOW_WHITE";
description = "Bad PDF Found, Securite bad PDF Flag set through ClamAV";
score = 8;
}
CLAMD_BAD_JPG {
expression = "CLAM_SECI_JPG & !MAILCOW_WHITE";
description = "Bad JPG Found, Securite bad JPG Flag set through ClamAV";
score = 8;
}
CLAMD_ASCII_MALWARE {
expression = "CLAM_SECI_ASCII & !MAILCOW_WHITE";
description = "ASCII malware found, Securite ASCII malware Flag set through ClamAV";
score = 8;
}
CLAMD_HTML_MALWARE {
expression = "CLAM_SECI_HTML & !MAILCOW_WHITE";
description = "HTML malware found, Securite HTML malware Flag set through ClamAV";
score = 8;
}
CLAMD_JS_MALWARE {
expression = "CLAM_SECI_JS & !MAILCOW_WHITE";
description = "JS malware found, Securite JS malware Flag set through ClamAV";
score = 8;
}
+9
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";
# }
+190 -4
View File
@@ -221,6 +221,16 @@ rspamd_config:register_symbol({
local tagged_rcpt = task:get_symbol("TAGGED_RCPT") local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN") local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
local function remove_moo_tag()
local moo_tag_header = task:get_header('X-Moo-Tag', false)
if moo_tag_header then
task:set_milter_reply({
remove_headers = {['X-Moo-Tag'] = 0},
})
end
return true
end
if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then
local tag = tagged_rcpt[1].options[1] local tag = tagged_rcpt[1].options[1]
rspamd_logger.infox("found tag: %s", tag) rspamd_logger.infox("found tag: %s", tag)
@@ -229,6 +239,7 @@ rspamd_config:register_symbol({
if action ~= 'no action' and action ~= 'greylist' then if action ~= 'no action' and action ~= 'greylist' then
rspamd_logger.infox("skipping tag handler for action: %s", action) rspamd_logger.infox("skipping tag handler for action: %s", action)
remove_moo_tag()
return true return true
end end
@@ -243,6 +254,7 @@ rspamd_config:register_symbol({
local function tag_callback_subfolder(err, data) local function tag_callback_subfolder(err, data)
if err or type(data) ~= 'string' then if err or type(data) ~= 'string' then
rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err) rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
remove_moo_tag()
else else
rspamd_logger.infox("Add X-Moo-Tag header") rspamd_logger.infox("Add X-Moo-Tag header")
task:set_milter_reply({ task:set_milter_reply({
@@ -261,6 +273,7 @@ rspamd_config:register_symbol({
) )
if not redis_ret_subfolder then if not redis_ret_subfolder then
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt") rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
remove_moo_tag()
end end
else else
@@ -268,7 +281,10 @@ rspamd_config:register_symbol({
local sbj = task:get_header('Subject') local sbj = task:get_header('Subject')
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?=' new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
task:set_milter_reply({ task:set_milter_reply({
remove_headers = {['Subject'] = 1}, remove_headers = {
['Subject'] = 1,
['X-Moo-Tag'] = 0
},
add_headers = {['Subject'] = new_sbj} add_headers = {['Subject'] = new_sbj}
}) })
end end
@@ -284,6 +300,7 @@ rspamd_config:register_symbol({
) )
if not redis_ret_subject then if not redis_ret_subject then
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt") rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
remove_moo_tag()
end end
end end
@@ -295,6 +312,7 @@ rspamd_config:register_symbol({
if #rcpt_split == 2 then if #rcpt_split == 2 then
if rcpt_split[1] == 'postmaster' then if rcpt_split[1] == 'postmaster' then
rspamd_logger.infox(rspamd_config, "not expanding postmaster alias") rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
remove_moo_tag()
else else
rspamd_http.request({ rspamd_http.request({
task=task, task=task,
@@ -307,7 +325,8 @@ rspamd_config:register_symbol({
end end
end end
end end
else
remove_moo_tag()
end end
end, end,
priority = 19 priority = 19
@@ -340,6 +359,10 @@ rspamd_config:register_symbol({
if not bcc_dest then if not bcc_dest then
return -- stop return -- stop
end end
-- dot stuff content before sending
local email_content = tostring(task:get_content())
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
-- send mail
lua_smtp.sendmail({ lua_smtp.sendmail({
task = task, task = task,
host = os.getenv("IPV4_NETWORK") .. '.253', host = os.getenv("IPV4_NETWORK") .. '.253',
@@ -347,8 +370,8 @@ rspamd_config:register_symbol({
from = task:get_from(stp)[1].addr, from = task:get_from(stp)[1].addr,
recipients = bcc_dest, recipients = bcc_dest,
helo = 'bcc', helo = 'bcc',
timeout = 10, timeout = 20,
}, task:get_content(), sendmail_cb) }, email_content, sendmail_cb)
end end
-- determine from -- determine from
@@ -499,3 +522,166 @@ rspamd_config:register_symbol({
end end
end end
}) })
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_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()
local env_from_addr = envfrom[1].addr:lower()
-- determine newline type
local function newline(task)
local t = task:get_newlines_type()
if t == 'cr' then
return '\r'
elseif t == 'lf' then
return '\n'
end
return '\r\n'
end
-- retrieve footer
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 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
if footer and type(footer) == "table" and (footer.html and footer.html ~= "" or footer.plain and footer.plain ~= "") then
rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s, vars=%s", uname, footer.html, footer.plain, footer.vars)
if footer.skip_replies ~= 0 then
in_reply_to = task:get_header_raw('in-reply-to')
if in_reply_to then
rspamd_logger.infox(rspamd_config, "mail is a reply - skip footer")
return
end
end
local envfrom_mime = task:get_from(2)
local from_name = ""
if envfrom_mime and envfrom_mime[1].name then
from_name = envfrom_mime[1].name
elseif envfrom and envfrom[1].name then
from_name = envfrom[1].name
end
-- default replacements
local replacements = {
auth_user = uname,
from_user = envfrom[1].user,
from_name = from_name,
from_addr = envfrom[1].addr,
from_domain = envfrom[1].domain:lower()
}
-- 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 and footer.plain ~= "" then
footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
end
-- add footer
local out = {}
local rewrite = lua_mime.add_text_footer(task, footer.html, footer.plain) or {}
local seen_cte
local newline_s = newline(task)
local function rewrite_ct_cb(name, hdr)
if rewrite.need_rewrite_ct then
if name:lower() == 'content-type' then
local nct = string.format('%s: %s/%s; charset=utf-8',
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
out[#out + 1] = nct
return
elseif name:lower() == 'content-transfer-encoding' then
out[#out + 1] = string.format('%s: %s',
'Content-Transfer-Encoding', 'quoted-printable')
seen_cte = true
return
end
end
out[#out + 1] = hdr.raw:gsub('\r?\n?$', '')
end
task:headers_foreach(rewrite_ct_cb, {full = true})
if not seen_cte and rewrite.need_rewrite_ct then
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable')
end
-- End of headers
out[#out + 1] = newline_s
if rewrite.out then
for _,o in ipairs(rewrite.out) do
out[#out + 1] = o
end
else
out[#out + 1] = task:get_rawbody()
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
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
rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\")", uname, data)
end
end
end
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,
priority = 1
})
@@ -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"; whitelisted_rcpts = "postmaster,mailer-daemon";
max_rcpt = 25; max_rcpt = 25;
custom_keywords = "/etc/rspamd/lua/ratelimit.lua"; custom_keywords = "/etc/rspamd/lua/ratelimit.lua";
+2
View File
@@ -12,6 +12,7 @@
SOGoJunkFolderName= "Junk"; SOGoJunkFolderName= "Junk";
SOGoMailDomain = "sogo.local"; SOGoMailDomain = "sogo.local";
SOGoEnableEMailAlarms = YES; SOGoEnableEMailAlarms = YES;
SOGoMailHideInlineAttachments = YES;
SOGoFoldersSendEMailNotifications = YES; SOGoFoldersSendEMailNotifications = YES;
SOGoForwardEnabled = YES; SOGoForwardEnabled = YES;
@@ -83,6 +84,7 @@
//SoDebugBaseURL = YES; //SoDebugBaseURL = YES;
//ImapDebugEnabled = YES; //ImapDebugEnabled = YES;
//SOGoEASDebugEnabled = YES; //SOGoEASDebugEnabled = YES;
SOGoEASSearchInBody = YES; // Experimental. Enabled since 2023-10
//LDAPDebugEnabled = YES; //LDAPDebugEnabled = YES;
//PGDebugEnabled = YES; //PGDebugEnabled = YES;
//MySQL4DebugEnabled = YES; //MySQL4DebugEnabled = YES;
+36
View File
@@ -0,0 +1,36 @@
server {
ssl_certificate /etc/ssl/mail/cert.pem;
ssl_certificate_key /etc/ssl/mail/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_ecdh_curve X25519:X448:secp384r1:secp256k1;
ssl_session_cache shared:SSL:50m;
ssl_session_timeout 1d;
ssl_session_tickets off;
index index.php index.html;
client_max_body_size 0;
root /web;
include /etc/nginx/conf.d/listen_plain.active;
include /etc/nginx/conf.d/listen_ssl.active;
server_name dmarcparse.derlinkman.de;
server_tokens off;
location ^~ /.well-known/acme-challenge/ {
allow all;
default_type "text/plain";
}
if ($scheme = http) {
return 301 https://$host$request_uri;
}
location / {
proxy_pass http://dmarcparser:8080/;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 0;
}
}
+1 -1
View File
@@ -20,6 +20,6 @@
<pre>BACKUP_LOCATION=/tmp/ ./helper-scripts/backup_and_restore.sh backup all</pre> <pre>BACKUP_LOCATION=/tmp/ ./helper-scripts/backup_and_restore.sh backup all</pre>
<pre>docker compose down --volumes ; docker compose up -d</pre> <pre>docker compose down --volumes ; docker compose up -d</pre>
<p>Make sure your timezone is correct. Use "America/New_York" for example, do not use spaces. Check <a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">here</a> for a list.</p> <p>Make sure your timezone is correct. Use "America/New_York" for example, do not use spaces. Check <a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">here</a> for a list.</p>
<br>Click to learn more about <a style="color:red;text-decoration:none;" href="https://mailcow.github.io/mailcow-dockerized-docs/#get-support" target="_blank">getting support.</a> <br>Click to learn more about <a style="color:red;text-decoration:none;" href="https://docs.mailcow.email/#get-support" target="_blank">getting support.</a>
</body> </body>
</html> </html>
+18 -1
View File
@@ -80,6 +80,17 @@ foreach ($RSPAMD_MAPS['regex'] as $rspamd_regex_desc => $rspamd_regex_map) {
]; ];
} }
// cors settings
$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');
// identity provider
$iam_settings = identity_provider('get');
// mbox templates
$mbox_templates = mailbox('get', 'mailbox_templates');
$template = 'admin.twig'; $template = 'admin.twig';
$template_data = [ $template_data = [
'tfa_data' => $tfa_data, 'tfa_data' => $tfa_data,
@@ -96,16 +107,22 @@ $template_data = [
'domains' => $domains, 'domains' => $domains,
'all_domains' => $all_domains, 'all_domains' => $all_domains,
'mailboxes' => $mailboxes, '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'), 'q_data' => quarantine('settings'),
'qn_data' => quota_notification('get'), 'qn_data' => quota_notification('get'),
'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'), 'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'),
'rsettings' => $rsettings, 'rsettings' => $rsettings,
'rspamd_regex_maps' => $rspamd_regex_maps, 'rspamd_regex_maps' => $rspamd_regex_maps,
'logo_specs' => customize('get', 'main_logo_specs'), 'logo_specs' => customize('get', 'main_logo_specs'),
'logo_dark_specs' => customize('get', 'main_logo_dark_specs'),
'ip_check' => customize('get', 'ip_check'), 'ip_check' => customize('get', 'ip_check'),
'password_complexity' => password_complexity('get'), 'password_complexity' => password_complexity('get'),
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'], 'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
'cors_settings' => $cors_settings,
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
'iam_settings' => $iam_settings,
'mbox_templates' => $mbox_templates,
'lang_admin' => json_encode($lang['admin']), 'lang_admin' => json_encode($lang['admin']),
'lang_datatables' => json_encode($lang['datatables']) 'lang_datatables' => json_encode($lang['datatables'])
]; ];
+242 -1
View File
@@ -1,4 +1,4 @@
openapi: 3.0.0 openapi: 3.1.0
info: info:
description: >- description: >-
mailcow is complete e-mailing solution with advanced antispam, antivirus, mailcow is complete e-mailing solution with advanced antispam, antivirus,
@@ -3137,6 +3137,86 @@ paths:
type: string type: string
type: object type: object
summary: Update domain 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: /api/v1/edit/fail2ban:
post: post:
responses: responses:
@@ -3336,6 +3416,86 @@ paths:
type: object type: object
type: object type: object
summary: Update mailbox 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: /api/v1/edit/mailq:
post: post:
responses: responses:
@@ -5581,6 +5741,7 @@ paths:
sogo_access: "1" sogo_access: "1"
tls_enforce_in: "0" tls_enforce_in: "0"
tls_enforce_out: "0" tls_enforce_out: "0"
custom_attributes: {}
domain: domain3.tld domain: domain3.tld
is_relayed: 0 is_relayed: 0
local_part: info local_part: info
@@ -5602,6 +5763,84 @@ paths:
description: You can list all mailboxes existing in system for a specific domain. description: You can list all mailboxes existing in system for a specific domain.
operationId: Get mailboxes of a domain operationId: Get mailboxes of a domain
summary: Get mailboxes of a domain summary: Get mailboxes of a domain
/api/v1/edit/cors:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
- type: "success"
log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
msg: "cors_headers_edited"
description: OK
headers: { }
tags:
- Cross-Origin Resource Sharing (CORS)
description: >-
This endpoint allows you to manage Cross-Origin Resource Sharing (CORS) settings for the API.
CORS is a security feature implemented by web browsers to prevent unauthorized cross-origin requests.
By editing the CORS settings, you can specify which domains and which methods are permitted to access the API resources from outside the mailcow domain.
operationId: Edit Cross-Origin Resource Sharing (CORS) settings
requestBody:
content:
application/json:
schema:
example:
attr:
allowed_origins: ["*", "mail.mailcow.tld"]
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
properties:
attr:
type: object
properties:
allowed_origins:
type: array
items:
type: string
allowed_methods:
type: array
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: tags:
- name: Domains - name: Domains
@@ -5646,3 +5885,5 @@ tags:
description: Get the status of your cow description: Get the status of your cow
- name: Ratelimits - name: Ratelimits
description: Edit domain ratelimits description: Edit domain ratelimits
- name: Cross-Origin Resource Sharing (CORS)
description: Manage Cross-Origin Resource Sharing (CORS) settings
+1 -2
View File
@@ -1,6 +1,6 @@
window.onload = function() { window.onload = function() {
// Begin Swagger UI call region // Begin Swagger UI call region
const ui = SwaggerUIBundle({ window.ui = SwaggerUIBundle({
urls: [{url: "/api/openapi.yaml", name: "mailcow API"}], urls: [{url: "/api/openapi.yaml", name: "mailcow API"}],
dom_id: '#swagger-ui', dom_id: '#swagger-ui',
deepLinking: true, deepLinking: true,
@@ -15,5 +15,4 @@ window.onload = function() {
}); });
// End Swagger UI call region // End Swagger UI call region
window.ui = ui;
}; };
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -1,6 +1,7 @@
<?php <?php
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/vars.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/vars.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php';
$default_autodiscover_config = $autodiscover_config; $default_autodiscover_config = $autodiscover_config;
if(file_exists('inc/vars.local.inc.php')) { if(file_exists('inc/vars.local.inc.php')) {
include_once 'inc/vars.local.inc.php'; include_once 'inc/vars.local.inc.php';
+4
View File
@@ -342,6 +342,10 @@ div.dataTables_wrapper div.dt-row {
position: relative; position: relative;
} }
div.dataTables_wrapper span.sorting-value {
display: none;
}
div.dataTables_scrollHead table.dataTable { div.dataTables_scrollHead table.dataTable {
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
-8
View File
@@ -8,9 +8,6 @@
.dtr-details { .dtr-details {
width: 100%; width: 100%;
} }
.table-striped>tbody>tr:nth-of-type(odd) {
background-color: #F2F2F2;
}
td.child>ul>li { td.child>ul>li {
display: flex; display: flex;
} }
@@ -42,11 +39,6 @@ table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before,
table.dataTable td.dt-control:before { table.dataTable td.dt-control:before {
background-color: #979797 !important; background-color: #979797 !important;
} }
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
background-color: #fbfbfb;
}
table.dataTable.table-striped>tbody>tr>td { table.dataTable.table-striped>tbody>tr>td {
vertical-align: middle; vertical-align: middle;
} }
+42 -1
View File
@@ -33,6 +33,13 @@
url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff2') format('woff2'), url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff2') format('woff2'),
url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff') format('woff'); url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff') format('woff');
} }
body {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: #fbfbfb;
}
#maxmsgsize { min-width: 80px; } #maxmsgsize { min-width: 80px; }
#slider1 .slider-selection { #slider1 .slider-selection {
background: #FFD700; background: #FFD700;
@@ -78,6 +85,19 @@
.navbar-fixed-top .navbar-collapse { .navbar-fixed-top .navbar-collapse {
max-height: 1000px max-height: 1000px
} }
.nav-tabs .nav-link, .nav-tabs .nav-link.disabled, .nav-tabs .nav-link.disabled:hover, .nav-tabs .nav-link.disabled:focus {
border-color: #dfdfdf;
}
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
border-color: #dfdfdf;
border-bottom: 1px solid #ffffff;
}
.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {
border-color: #dfdfdf;
}
.nav-tabs {
border-bottom: 1px solid #dfdfdf;
}
.bi { .bi {
display: inline-block; display: inline-block;
font-size: 12pt; font-size: 12pt;
@@ -228,8 +248,8 @@ legend {
margin-top: 20px; margin-top: 20px;
} }
.slave-info { .slave-info {
padding: 15px 0px 15px 15px;
font-weight: bold; font-weight: bold;
color: orange;
} }
.alert-hr { .alert-hr {
margin:3px 0px; margin:3px 0px;
@@ -357,6 +377,7 @@ button[aria-expanded='true'] > .caret {
} }
.progress { .progress {
height: 16px;
background-color: #d5d5d5; background-color: #d5d5d5;
} }
@@ -365,8 +386,28 @@ button[aria-expanded='true'] > .caret {
background-color: #f0f0f0; background-color: #f0f0f0;
} }
.btn.btn-outline-secondary { .btn.btn-outline-secondary {
color: #000000 !important;
border-color: #cfcfcf !important; border-color: #cfcfcf !important;
} }
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show { .btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
background-color: #f0f0f0 !important; background-color: #f0f0f0 !important;
} }
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
color: #fff;
background-color: #555;
background-image: none;
border-color: #4d4d4d;
}
.btn-check:checked+.btn-light:focus, .btn-check:active+.btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, .show>.btn-light.dropdown-toggle:focus,
.btn-check:focus+.btn-light, .btn-light:focus {
box-shadow: none;
}
.btn-group>.btn:not(:last-of-type) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.badge.bg-info > a,
.badge.bg-danger > a {
color: #fff !important;
text-decoration: none;
}
+14 -1
View File
@@ -38,7 +38,7 @@
@media (max-width: 767px) { @media (max-width: 767px) {
.responsive-tabs .tab-pane { .responsive-tabs .tab-pane:not(.rsettings) {
display: block !important; display: block !important;
opacity: 1; opacity: 1;
} }
@@ -206,6 +206,19 @@
.senders-mw220 { .senders-mw220 {
max-width: 100% !important; max-width: 100% !important;
} }
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before,
table.dataTable td.dt-control:before {
height: 2rem;
width: 2rem;
line-height: 2rem;
margin-top: -15px;
}
li .dtr-data {
padding: 0;
}
} }
@media (max-width: 350px) { @media (max-width: 350px) {
+3 -1
View File
@@ -66,4 +66,6 @@ table tbody tr td input[type="checkbox"] {
padding: .2em .4em .3em !important; padding: .2em .4em .3em !important;
background-color: #ececec!important; background-color: #ececec!important;
} }
.badge.bg-info .bi {
font-size: inherit;
}
+210 -121
View File
@@ -1,85 +1,128 @@
body { body {
background-color: #414141; background-color: #1c1c1e;
color: #e0e0e0; color: #f2f2f7;
} }
.card { .card {
border: 1px solid #1c1c1c; border: 1px solid #2c2c2e;
background-color: #3a3a3a; background-color: #2c2c2e;
} }
legend { legend {
color: #f5f5f5; color: #f2f2f7;
} }
.card-header { .card-header {
color: #bbb; color: #8e8e93;
background-color: #2c2c2c; background-color: #1c1c1e;
border-color: transparent; border-color: transparent;
} }
.card-body {
--bs-card-color: #bbb;
}
.btn-secondary, .paginate_button, .page-link, .btn-light { .btn-secondary, .paginate_button, .page-link, .btn-light {
color: #fff !important; color: #f2f2f7 !important;
background-color: #7a7a7a !important; background-color: #5e5e5e !important;
border-color: #5c5c5c !important; border-color: #4c4c4e !important;
} }
.btn-check:checked+.btn-secondary, .btn-check:active+.btn-secondary, .btn-secondary:active, .btn-secondary.active, .show>.btn-secondary.dropdown-toggle {
border-color: #7a7a7a !important; .btn-dark {
} color: #f2f2f7 !important;
.alert-secondary { background-color: #242424 !important;
color: #fff !important; border-color: #1c1c1e !important;
background-color: #7a7a7a !important;
border-color: #5c5c5c !important;
}
.bg-secondary {
color: #fff !important;
background-color: #7a7a7a !important;
}
.alert-secondary, .alert-secondary a, .alert-secondary .alert-link {
color: #fff;
}
.page-item.active .page-link {
background-color: #158cba !important;
border-color: #127ba3 !important;
} }
.btn-secondary:focus, .btn-secondary:hover, .btn-group.open .dropdown-toggle.btn-secondary { .btn-secondary:focus, .btn-secondary:hover, .btn-group.open .dropdown-toggle.btn-secondary {
background-color: #7a7a7a; background-color: #444444;
border-color: #5c5c5c !important; border-color: #4c4c4e !important;
color: #fff; color: #f2f2f7;
} }
.btn-check:checked+.btn-secondary, .btn-check:active+.btn-secondary, .btn-secondary:active, .btn-secondary.active, .show>.btn-secondary.dropdown-toggle {
border-color: #5e5e5e !important;
}
.alert-secondary {
color: #f2f2f7 !important;
background-color: #5e5e5e !important;
border-color: #4c4c4e !important;
}
.bg-secondary {
color: #f2f2f7 !important;
background-color: #5e5e5e !important;
}
.alert-secondary, .alert-secondary a, .alert-secondary .alert-link {
color: #f2f2f7;
}
.page-item.active .page-link {
background-color: #3e3e3e !important;
border-color: #3e3e3e !important;
}
.btn-secondary:focus, .btn-secondary:hover, .btn-group.open .dropdown-toggle.btn-secondary {
background-color: #5e5e5e;
border-color: #4c4c4e !important;
color: #f2f2f7;
}
.btn-secondary:disabled, .btn-secondary.disabled { .btn-secondary:disabled, .btn-secondary.disabled {
border-color: #7a7a7a !important; border-color: #5e5e5e !important;
} }
.modal-content { .modal-content {
background-color: #414141; --bs-modal-color: #bbb;
background-color: #2c2c2e;
} }
.modal-header { .modal-header {
border-bottom: 1px solid #161616; border-bottom: 1px solid #999;
} }
.modal-title { .modal-title {
color: white; color: #bbb;
} }
.modal .btn-close { .modal .btn-close {
filter: invert(1) grayscale(100%) brightness(200%); filter: invert(1) grayscale(100%) brightness(200%);
} }
.navbar.bg-light { .navbar.bg-light {
background-color: #222222 !important; background-color: #1c1c1e !important;
border-color: #181818; border-color: #2c2c2e;
} }
.nav-link { .nav-link {
color: #ccc !important; color: #8e8e93 !important;
} }
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link { .nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
background: none; background: none;
} }
.nav-tabs, .nav-tabs .nav-link {
border-color: #444444 !important;
}
.nav-tabs .nav-link:not(.disabled):hover, .nav-tabs .nav-link:not(.disabled):focus, .nav-tabs .nav-link.active { .nav-tabs .nav-link:not(.disabled):hover, .nav-tabs .nav-link:not(.disabled):focus, .nav-tabs .nav-link.active {
border-bottom-color: #414141; border-bottom-color: #1c1c1e !important;
}
.card .nav-tabs .nav-link:not(.disabled):hover, .card .nav-tabs .nav-link:not(.disabled):focus, .card .nav-tabs .nav-link.active {
border-bottom-color: #2c2c2e !important;
} }
.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr { .table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
color: #ccc !important; color: #f2f2f7 !important;
} }
.dropdown-menu { .dropdown-menu {
background-color: #585858; background-color: #424242;
border: 1px solid #333; border: 1px solid #282828;
} }
.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover { .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover {
color: #fafafa; color: #fafafa;
@@ -92,7 +135,7 @@ legend {
color: #d4d4d4 !important; color: #d4d4d4 !important;
} }
tbody tr { tbody tr {
color: #555; color: #ccc;
} }
.navbar-default .navbar-nav>.open>a, .navbar-default .navbar-nav>.open>a:focus, .navbar-default .navbar-nav>.open>a:hover { .navbar-default .navbar-nav>.open>a, .navbar-default .navbar-nav>.open>a:focus, .navbar-default .navbar-nav>.open>a:hover {
color: #ccc; color: #ccc;
@@ -101,18 +144,15 @@ tbody tr {
color: #ccc; color: #ccc;
} }
.list-group-item { .list-group-item {
background-color: #333; background-color: #282828;
border: 1px solid #555; border: 1px solid #555;
} }
.table-striped>tbody>tr:nth-of-type(odd) { .table-striped>tbody>tr:nth-of-type(odd) {
background-color: #333; background-color: #424242;
} }
table.dataTable>tbody>tr.child ul.dtr-details>li { table.dataTable>tbody>tr.child ul.dtr-details>li {
border-bottom: 1px solid rgba(255, 255, 255, 0.13); border-bottom: 1px solid rgba(255, 255, 255, 0.13);
} }
tbody tr {
color: #ccc;
}
.label.label-last-login { .label.label-last-login {
color: #ccc !important; color: #ccc !important;
background-color: #555 !important; background-color: #555 !important;
@@ -128,20 +168,23 @@ div.numberedtextarea-number {
} }
.well { .well {
border: 1px solid #555; border: 1px solid #555;
background-color: #333; background-color: #282828;
} }
pre { pre {
color: #ccc; color: #ccc;
background-color: #333; background-color: #282828;
border: 1px solid #555; border: 1px solid #555;
} }
.form-control {
background-color: transparent;
}
input.form-control, textarea.form-control { input.form-control, textarea.form-control {
color: #e2e2e2 !important; color: #e2e2e2 !important;
background-color: #555 !important; background-color: #424242 !important;
border: 1px solid #999; border: 1px solid #999;
} }
input.form-control:focus, textarea.form-control { input.form-control:focus, textarea.form-control {
background-color: #555 !important; background-color: #424242 !important;
} }
input.form-control:disabled, textarea.form-disabled { input.form-control:disabled, textarea.form-disabled {
color: #a8a8a8 !important; color: #a8a8a8 !important;
@@ -149,16 +192,14 @@ input.form-control:disabled, textarea.form-disabled {
} }
.input-group-addon { .input-group-addon {
color: #ccc; color: #ccc;
background-color: #555 !important; background-color: #424242 !important;
border: 1px solid #999; border: 1px solid #999;
} }
.input-group-text { .input-group-text {
color: #ccc; color: #ccc;
background-color: #242424; background-color: #1c1c1c;
} }
.list-group-item { .list-group-item {
color: #ccc; color: #ccc;
} }
@@ -170,11 +211,11 @@ input.form-control:disabled, textarea.form-disabled {
} }
.dropdown-item.active:hover { .dropdown-item.active:hover {
color: #fff !important; color: #fff !important;
background-color: #31b1e4; background-color: #007aff;
} }
.form-select { .form-select {
color: #e2e2e2!important; color: #e2e2e2!important;
background-color: #555!important; background-color: #424242!important;
border: 1px solid #999; border: 1px solid #999;
} }
@@ -186,31 +227,6 @@ input.form-control:disabled, textarea.form-disabled {
color: #fff !important; color: #fff !important;
} }
.table-secondary {
--bs-table-bg: #7a7a7a;
--bs-table-striped-bg: #e4e4e4;
--bs-table-striped-color: #000;
--bs-table-active-bg: #d8d8d8;
--bs-table-active-color: #000;
--bs-table-hover-bg: #dedede;
--bs-table-hover-color: #000;
color: #000;
border-color: #d8d8d8;
}
.table-light {
--bs-table-bg: #f6f6f6;
--bs-table-striped-bg: #eaeaea;
--bs-table-striped-color: #000;
--bs-table-active-bg: #dddddd;
--bs-table-active-color: #000;
--bs-table-hover-bg: #e4e4e4;
--bs-table-hover-color: #000;
color: #000;
border-color: #dddddd;
}
.form-control-plaintext { .form-control-plaintext {
color: #e0e0e0; color: #e0e0e0;
} }
@@ -284,12 +300,12 @@ a:hover {
} }
.tag-box { .tag-box {
background-color: #555; background-color: #282828;
border: 1px solid #999; border: 1px solid #555;
} }
.tag-input { .tag-input {
color: #fff; color: #fff;
background-color: #555; background-color: #282828;
} }
.tag-add { .tag-add {
color: #ccc; color: #ccc;
@@ -298,43 +314,24 @@ a:hover {
color: #d1d1d1; color: #d1d1d1;
} }
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
background-color: #7a7a7a !important;
}
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before {
background-color: #7a7a7a !important;
border: 1.5px solid #5c5c5c !important;
color: #fff !important;
}
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before {
background-color: #949494;
}
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
background-color: #444444;
}
.btn-check-label { .btn-check-label {
color: #fff; color: #fff;
} }
.btn-outline-secondary:hover { .btn-outline-secondary:hover {
background-color: #c3c3c3; background-color: #5c5c5c;
} }
.btn.btn-outline-secondary { .btn.btn-outline-secondary {
color: #fff !important; color: #e0e0e0 !important;
border-color: #7a7a7a !important; border-color: #7a7a7a !important;
} }
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show { .btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
background-color: #9b9b9b !important; background-color: #7a7a7a !important;
}
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
color: #f2f2f7 !important;
background-color: #242424 !important;
border-color: #1c1c1e !important;
} }
.btn-input-missing, .btn-input-missing,
.btn-input-missing:hover, .btn-input-missing:hover,
.btn-input-missing:active, .btn-input-missing:active,
@@ -342,27 +339,119 @@ table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
.btn-input-missing:active:hover, .btn-input-missing:active:hover,
.btn-input-missing:active:focus { .btn-input-missing:active:focus {
color: #fff !important; color: #fff !important;
background-color: #ff2f24 !important; background-color: #ff3b30 !important;
border-color: #e21207 !important; border-color: #ff3b30 !important;
} }
.inputMissingAttr { .inputMissingAttr {
border-color: #FF4136 !important; border-color: #ff4136 !important;
} }
.list-group-details { .list-group-details {
background: #444444; background: #555;
} }
.list-group-header { .list-group-header {
background: #333; background: #444;
} }
span.mail-address-item { span.mail-address-item {
background-color: #333; background-color: #444;
border-radius: 4px; border-radius: 4px;
border: 1px solid #555; border: 1px solid #555;
padding: 2px 7px; padding: 2px 7px;
display: inline-block; display: inline-block;
margin: 2px 6px 2px 0; margin: 2px 6px 2px 0;
} }
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
background-color: #7a7a7a !important;
}
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before {
background-color: #7a7a7a !important;
border: 1.5px solid #5c5c5c !important;
color: #e0e0e0 !important;
}
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before {
background-color: #949494;
}
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
background-color: #414141;
}
table.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
color: #ccc !important;
}
.table-secondary {
--bs-table-bg: #282828;
--bs-table-striped-bg: #343434;
--bs-table-striped-color: #f2f2f7;
--bs-table-active-bg: #4c4c4c;
--bs-table-active-color: #f2f2f7;
--bs-table-hover-bg: #3a3a3a;
--bs-table-hover-color: #f2f2f7;
color: #ccc;
border-color: #3a3a3a;
}
.table-light {
--bs-table-bg: #3a3a3a;
--bs-table-striped-bg: #444444;
--bs-table-striped-color: #f2f2f7;
--bs-table-active-bg: #5c5c5c;
--bs-table-active-color: #f2f2f7;
--bs-table-hover-bg: #4c4c4c;
--bs-table-hover-color: #f2f2f7;
color: #ccc;
border-color: #4c4c4c;
}
.table-bordered {
border-color: #3a3a3a;
}
.table-bordered th,
.table-bordered td {
border-color: #3a3a3a !important;
}
.table-bordered thead th,
.table-bordered thead td {
border-bottom-width: 2px;
}
.table-striped>tbody>tr:nth-of-type(odd)>td,
.table-striped>tbody>tr:nth-of-type(odd)>th {
background-color: #282828;
}
.table-hover>tbody>tr:hover {
background-color: #343434;
}
.table>:not(caption)>*>* {
border-color: #5c5c5c;
--bs-table-color-state:#bbb;
--bs-table-bg: #3a3a3a;
}
.text-muted {
--bs-secondary-color: #8e8e93;
}
input::placeholder {
color: #8e8e93 !important;
}
.form-select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%238e8e93' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
}
.btn-light, .btn-light:hover {
background-image: none;
}
+15 -6
View File
@@ -47,6 +47,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
$quota_notification_bcc = quota_notification_bcc('get', $domain); $quota_notification_bcc = quota_notification_bcc('get', $domain);
$rl = ratelimit('get', 'domain', $domain); $rl = ratelimit('get', 'domain', $domain);
$rlyhosts = relayhost('get'); $rlyhosts = relayhost('get');
$domain_footer = mailbox('get', 'domain_wide_footer', $domain);
$template = 'edit/domain.twig'; $template = 'edit/domain.twig';
$template_data = [ $template_data = [
'acl' => $_SESSION['acl'], 'acl' => $_SESSION['acl'],
@@ -56,23 +57,28 @@ if (isset($_SESSION['mailcow_cc_role'])) {
'rlyhosts' => $rlyhosts, 'rlyhosts' => $rlyhosts,
'dkim' => dkim('details', $domain), 'dkim' => dkim('details', $domain),
'domain_details' => $result, 'domain_details' => $result,
'domain_footer' => $domain_footer,
'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]),
'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address')
]; ];
} }
} }
elseif (isset($_GET["template"])){ elseif (isset($_GET['template'])){
$domain_template = mailbox('get', 'domain_templates', $_GET["template"]); $domain_template = mailbox('get', 'domain_templates', $_GET['template']);
if ($domain_template){ if ($domain_template){
$template_data = [ $template_data = [
'template' => $domain_template 'template' => $domain_template,
'rl' => ['frame' => $domain_template['attributes']['rl_frame']],
]; ];
$template = 'edit/domain-templates.twig'; $template = 'edit/domain-templates.twig';
$result = true; $result = true;
} }
else { else {
$mailbox_template = mailbox('get', 'mailbox_templates', $_GET["template"]); $mailbox_template = mailbox('get', 'mailbox_templates', $_GET['template']);
if ($mailbox_template){ if ($mailbox_template){
$template_data = [ $template_data = [
'template' => $mailbox_template 'template' => $mailbox_template,
'rl' => ['frame' => $mailbox_template['attributes']['rl_frame']],
]; ];
$template = 'edit/mailbox-templates.twig'; $template = 'edit/mailbox-templates.twig';
$result = true; $result = true;
@@ -112,6 +118,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
$quarantine_category = mailbox('get', 'quarantine_category', $mailbox); $quarantine_category = mailbox('get', 'quarantine_category', $mailbox);
$get_tls_policy = mailbox('get', 'tls_policy', $mailbox); $get_tls_policy = mailbox('get', 'tls_policy', $mailbox);
$rlyhosts = relayhost('get'); $rlyhosts = relayhost('get');
$iam_settings = identity_provider('get');
$template = 'edit/mailbox.twig'; $template = 'edit/mailbox.twig';
$template_data = [ $template_data = [
'acl' => $_SESSION['acl'], 'acl' => $_SESSION['acl'],
@@ -124,7 +131,8 @@ if (isset($_SESSION['mailcow_cc_role'])) {
'rlyhosts' => $rlyhosts, 'rlyhosts' => $rlyhosts,
'sender_acl_handles' => mailbox('get', 'sender_acl_handles', $mailbox), 'sender_acl_handles' => mailbox('get', 'sender_acl_handles', $mailbox),
'user_acls' => acl('get', 'user', $mailbox), 'user_acls' => acl('get', 'user', $mailbox),
'mailbox_details' => $result 'mailbox_details' => $result,
'iam_settings' => $iam_settings,
]; ];
} }
} }
@@ -214,6 +222,7 @@ $js_minifier->add('/web/js/site/pwgen.js');
$template_data['result'] = $result; $template_data['result'] = $result;
$template_data['return_to'] = $_SESSION['return_to']; $template_data['return_to'] = $_SESSION['return_to'];
$template_data['lang_user'] = json_encode($lang['user']); $template_data['lang_user'] = json_encode($lang['user']);
$template_data['lang_admin'] = json_encode($lang['admin']);
$template_data['lang_datatables'] = json_encode($lang['datatables']); $template_data['lang_datatables'] = json_encode($lang['datatables']);
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
+2
View File
@@ -65,6 +65,8 @@ $globalVariables = [
'lang_acl' => json_encode($lang['acl']), 'lang_acl' => json_encode($lang['acl']),
'lang_tfa' => json_encode($lang['tfa']), 'lang_tfa' => json_encode($lang['tfa']),
'lang_fido2' => json_encode($lang['fido2']), 'lang_fido2' => json_encode($lang['fido2']),
'lang_success' => json_encode($lang['success']),
'lang_danger' => json_encode($lang['danger']),
'docker_timeout' => $DOCKER_TIMEOUT, 'docker_timeout' => $DOCKER_TIMEOUT,
'session_lifetime' => (int)$SESSION_LIFETIME, 'session_lifetime' => (int)$SESSION_LIFETIME,
'csrf_token' => $_SESSION['CSRF']['TOKEN'], 'csrf_token' => $_SESSION['CSRF']['TOKEN'],

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