Compare commits

...

153 Commits

Author SHA1 Message Date
FreddleSpl0it
c0f06bfc52 Merge branch 'staging' into feat/valkey 2025-10-10 12:40:41 +02:00
milkmaker
79cf0abc6e [Web] Updated lang.zh-cn.json (#6826)
Co-authored-by: Easton Man <me@eastonman.com>
2025-10-09 19:54:12 +02:00
Olavo Rocha Neto
7de70322d6 Update pt-br lang (#6803)
* [Web] Updated lang.si-si.json

Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>

* Update pt-br lang

* Complimentary adjustments

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

This reverts commit b23848e0f2.

---------

Co-authored-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
2025-10-09 19:36:36 +02:00
DerLinkman
417835dea8 netfilter: improve logging and mark iptables-legacy as deprecated 2025-10-09 16:37:05 +02:00
FreddleSpl0it
df4d3bb6e0 [Web] Fix dashboard host stats 2025-10-07 11:41:57 +02:00
FreddleSpl0it
455ef084b4 [Web] clear old app_passwd log entries 2025-10-07 10:37:44 +02:00
FreddleSpl0it
c2948735f2 [Web] clear old app_passwd log entries 2025-10-07 10:18:07 +02:00
FreddleSpl0it
1ef0149076 [Web] make SameSite policy and cookie name configurable via vars.local.inc 2025-10-06 11:00:03 +02:00
FreddleSpl0it
922d173540 [Web] include hostname in default website title 2025-10-06 10:58:35 +02:00
renovate[bot]
fd088cb504 chore(deps): update actions/stale action to v10.1.0 (#6806) 2025-10-04 14:13:48 +02:00
Valentin Brandl
721ee2394e Update variable name for prometheus-exporter security token (#6776)
* update variable name for prometheus-exporter security token

* update `MAILCOW_EXPORTER_TOKEN_DISABLE` variable name
2025-10-03 18:03:03 +01:00
Colin Kubon
c217be06c6 scripts: make sure /etc/docker exists (#6791) 2025-10-02 09:24:06 +02:00
Jonas
871c422ec1 Fix typos in config (#6792)
Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
2025-10-02 09:22:35 +02:00
sdsys-ch
3cc28af607 [Helper] Fix cold-standby script to support digits and override files (#6800)
This commit fixes two bugs in the cold-standby script:

1. Support digits in COMPOSE_PROJECT_NAME
   The script was stripping digits from COMPOSE_PROJECT_NAME, while
   backup_and_restore.sh (fixed in a71d991c) correctly supports them.
   Added '0-9' to the tr character set to align behavior.

2. Support docker-compose.override.yml on remote
   Lines 172 and 287 explicitly used '-f docker-compose.yml' which
   causes Docker Compose to ignore docker-compose.override.yml even
   when present. Changed to 'cd && compose' pattern (matching line 296)
   to auto-discover override files.

   Impact: Users with custom volumes/services in override file would
   experience silent failures - volumes not created, images not pulled,
   data syncing to wrong locations.

Both fixes ensure cold-standby works correctly with standard Docker
Compose conventions and user customizations.

Co-authored-by: Christophe Neuerburg <c.neuerburg@sdsys.ch>
2025-10-02 09:21:26 +02:00
milkmaker
796e131c3a update postscreen_access.cidr (#6801) 2025-10-01 11:14:57 +02:00
milkmaker
c51a769aec [Web] Updated lang.si-si.json (#6794)
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
2025-09-29 18:10:39 +02:00
FreddleSpl0it
45a61755a5 Merge pull request #6777 from patschi/enable-https-redirect-default
Enable HTTPS redirect by default on new setups
2025-09-29 11:56:46 +02:00
FreddleSpl0it
769c57c355 Merge pull request #6779 from patschi/remove-debug-consolelog
Remove debug console.log calls
2025-09-29 11:54:23 +02:00
FreddleSpl0it
2e7eb7c0fd Merge pull request #6780 from patschi/fix-pwcomplexity-apppasswds
Fixed password complexity check for AppPasswords creation/edit
2025-09-29 11:53:26 +02:00
FreddleSpl0it
4c83147d01 Merge pull request #6781 from patschi/pw-field-name-consistency
Rename password fields for AppPasswords same way for consistency
2025-09-29 11:52:08 +02:00
FreddleSpl0it
ca0bec4fc2 Merge pull request #6782 from patschi/fix-footer-escape
Fixed wrong footer escaping for certain characters
2025-09-29 11:45:42 +02:00
FreddleSpl0it
6f50dd17da Merge pull request #6786 from patschi/fix-sql-typo
Fix several SQL statements
2025-09-29 11:39:30 +02:00
FreddleSpl0it
4a331929d0 Merge pull request #6787 from patschi/hide-relayhosts-if-no-acl
Hide relayhosts when ACL does not allow
2025-09-29 11:38:52 +02:00
FreddleSpl0it
748bc893b6 Merge pull request #6788 from patschi/lastmodified-default-value
Show "Never" by default if no last-modified date saved
2025-09-29 11:37:52 +02:00
FreddleSpl0it
e462602ddc Merge pull request #6789 from patschi/domain-descr-readonly-when-no-acl
Make domain description field readonly when no ACL
2025-09-29 11:36:42 +02:00
milkmaker
4e0f435d12 [Web] Updated lang.si-si.json (#6793) 2025-09-28 15:12:14 +02:00
milkmaker
46f0581936 [Web] Updated lang.si-si.json (#6790)
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
2025-09-26 19:14:07 +02:00
Patrik Kernstock
20f04ecf6b Make domain description field readonly when no ACL 2025-09-26 17:13:24 +02:00
Patrik Kernstock
ff43799763 Show "Never" by default if no last-modified date 2025-09-26 17:02:22 +02:00
Patrik Kernstock
85ca197615 Hide relayhosts when ACL does not allow 2025-09-26 16:50:58 +02:00
Patrik Kernstock
d06d23bbaf Fix several SQL statements 2025-09-26 14:58:04 +02:00
Patrik Kernstock
702ed85dfd Fixed footer escaping 2025-09-26 14:41:19 +02:00
milkmaker
8abe74a562 [Web] Updated lang.si-si.json (#6785)
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
2025-09-26 10:57:32 +02:00
Patrik Kernstock
5c5287ca21 Fixed wrong footer escaping 2025-09-26 04:04:45 +02:00
Patrik Kernstock
ce219668cf Rename AppPasswds fields uniquely like 'add' 2025-09-26 03:37:49 +02:00
Patrik Kernstock
5b1b49a418 Fixed password complexity check for AppPasswords 2025-09-26 02:37:02 +02:00
Patrik Kernstock
8978a9ad79 Remove debug console.log() lines 2025-09-26 02:13:22 +02:00
Patrik Kernstock
5f4a4fd759 Removed new lines for consistency 2025-09-26 01:14:33 +02:00
Patrik Kernstock
171c591da4 Enable REDIRECT_HTTP=y by default 2025-09-26 01:14:23 +02:00
FreddleSpl0it
9133b9899c Merge pull request #6764 from patschi/tools-install-clear-msg
Clearer message to install required tool, e.g. jq
2025-09-25 09:00:41 +02:00
FreddleSpl0it
701c9fb1b4 Merge pull request #6772 from patschi/update-issue-template
Update GitHub's issue template
2025-09-25 08:53:18 +02:00
Patrik Kernstock
eabd22188b Re-intend checkboxes 2025-09-24 21:20:48 +02:00
Patrik Kernstock
7028619742 Update GitHub's issue template 2025-09-24 21:17:29 +02:00
Patrik Kernstock
c915bf2ee2 Add docs link to get_installed_tools() message 2025-09-24 19:06:47 +02:00
milkmaker
011edd5ac9 [Web] Updated lang.si-si.json (#6771)
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
2025-09-24 17:36:09 +02:00
FreddleSpl0it
7ba3de4ced Merge pull request #6767 from mailcow/fix/rename-phpsessid
[Web] Rename PHP Cookie to MCSESSID
2025-09-23 12:41:01 +02:00
FreddleSpl0it
8ead77083f [Web] Rename PHP Cookie to MCSESSID 2025-09-23 12:39:48 +02:00
FreddleSpl0it
b2774fb50b Merge pull request #6766 from mailcow/fix/samesite-cookie
[Web] set cookie SameSite attribute to Lax
2025-09-23 12:36:11 +02:00
FreddleSpl0it
4440bd46ad [Web] set cookie SameSite attribute to Lax 2025-09-23 12:24:25 +02:00
FreddleSpl0it
28985973eb [Web] Revert - allow "*" as wildcard domain 2025-09-23 10:07:33 +02:00
Christian 🦄
f2c4697ca3 Fixed typo in lang de-de (#6765) 2025-09-22 22:45:54 +01:00
Patrik Kernstock
383b5affb5 More clearer message to install required tool 2025-09-22 19:49:31 +02:00
FreddleSpl0it
ed4dcff63b [Web] allow "*" as wildcard domain 2025-09-22 14:42:14 +02:00
FreddleSpl0it
caca32bbba Merge pull request #6759 from mailcow/fix/6720
[Web] Allow wildcard subdomains for MTA-STS
2025-09-22 14:20:36 +02:00
FreddleSpl0it
d31e74c778 Merge pull request #6760 from mailcow/fix/6739
[Web] Remove Port from HTTP_HOST
2025-09-22 14:20:15 +02:00
FreddleSpl0it
6c00e29276 Merge pull request #6762 from mailcow/fix/6740
[Nginx] do not invert ENABLE_IPV6
2025-09-22 14:19:57 +02:00
FreddleSpl0it
9940c503a2 [Nginx] do not invert ENABLE_IPV6 2025-09-22 14:16:42 +02:00
FreddleSpl0it
4b2862cb3c [Web] Remove Port from HTTP_HOST 2025-09-22 14:07:17 +02:00
FreddleSpl0it
a36485f0f1 [Web] Allow wildcard subdomains for MTA-STS 2025-09-22 13:55:18 +02:00
FreddleSpl0it
78168ee80a Merge pull request #6758 from mailcow/feat/sogo-url-encryption
[SOGo][Web] SOGo URL Encryption support
2025-09-22 13:32:58 +02:00
FreddleSpl0it
610609378f [SOGo][Web] Set URL encryption key in mailcow.conf 2025-09-22 12:58:05 +02:00
FreddleSpl0it
260906e350 [SOGo][Web] Enable SOGo URL Encryption 2025-09-22 12:28:09 +02:00
milkmaker
2891bbf82a Translations update from Weblate (#6749)
* [Web] Updated lang.cs-cz.json

Co-authored-by: Filip Hajny <filip@hajny.net>

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

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

---------

Co-authored-by: Filip Hajny <filip@hajny.net>
Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
2025-09-16 18:24:12 +02:00
milkmaker
eb26bcbc94 Translations update from Weblate (#6743)
* [Web] Updated lang.zh-cn.json

Co-authored-by: Easton Man <me@eastonman.com>

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

[Web] Updated lang.si-si.json

Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

---------

Co-authored-by: Easton Man <me@eastonman.com>
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
2025-09-13 21:41:59 +02:00
patr_
84e230de8f [Nginx] fix: Disable IPv6 support in Nginx configuration (#6736)
Co-authored-by: patr_ <patbernh@gmail.com>
2025-09-12 11:17:18 +02:00
FreddleSpl0it
f67a12d157 Merge pull request #6726 from mailcow/fix/6135
[Web] remove unused bcc dest column from alias table
2025-09-11 13:50:25 +02:00
FreddleSpl0it
34b48eedfc Merge pull request #6727 from mailcow/fix/6423
[SOGo] Drop deprecated `sogo_update_password` sql trigger if it still exists
2025-09-11 13:50:05 +02:00
FreddleSpl0it
0d900d4fc8 [SOGo] Drop deprecated sogo_update_password sql trigger if it still exists 2025-09-11 11:01:50 +02:00
FreddleSpl0it
642ac6d02c [Web] remove unused bcc dest column from alias table 2025-09-11 10:34:35 +02:00
DerLinkman
94c1a6c4e1 scripts: ipv6_controller improvement + fix modules handling (#6722)
* Fix subscript handling for modules

* ipv6: detect case when link local is present

* v6-controller: removed fixed-cidr for docker 28+
2025-09-10 16:20:58 +02:00
FreddleSpl0it
262fe04286 change MAJOR_VERSIONS 2025-08 to 2025-09 2025-09-10 11:17:34 +02:00
FreddleSpl0it
1c438330c6 [postfix-tlspol] build with NOOPT=1 for wider CPU compatibility 2025-09-10 10:14:37 +02:00
FreddleSpl0it
8cb25709ae [Clamd] update to 1.71 2025-09-10 08:23:22 +02:00
FreddleSpl0it
221f2989b0 Merge pull request #6698 from mailcow/6644_clamd-tmp-folder-naming-change
Changed clamavs tmp folder structure
2025-09-09 13:12:54 +02:00
FreddleSpl0it
3d05207bc7 Merge pull request #6717 from mailcow/fix/6664
[Rspamd] only recreate external_services.conf file if it was deleted
2025-09-09 12:53:03 +02:00
FreddleSpl0it
8c8497d885 [Rspamd] only recreate external_services.conf file if it was deleted 2025-09-09 12:50:19 +02:00
FreddleSpl0it
56d083ced4 Merge pull request #6682 from psuet/fix/imapsync
fix: imapsync gets correct timeouts from imapsync_runner.pl
2025-09-09 12:34:02 +02:00
FreddleSpl0it
a90b3544a7 Merge pull request #6651 from psuet/fix/php-warnings
Fix multiple PHP Warnings present in "stock" installation
2025-09-09 12:27:14 +02:00
FreddleSpl0it
08aea7fb26 Merge pull request #6716 from mailcow/fix/6610
Prevent user login if protocol access has been disabled
2025-09-09 12:16:09 +02:00
FreddleSpl0it
13f7f9830b Prevent user login if protocol access has been disabled 2025-09-09 12:11:19 +02:00
FreddleSpl0it
2f75039194 Merge pull request #6715 from mailcow/fix/version-tag
[Web] Only include mailcow_info in JS when mailcow_cc_username is set
2025-09-09 11:10:25 +02:00
FreddleSpl0it
1e192e14f4 [Web] Only include mailcow_info in JS when mailcow_cc_username is set 2025-09-09 11:09:09 +02:00
FreddleSpl0it
9cd1f931fc Merge pull request #6714 from mailcow/fix/domain-wide-footer
[RSPAMD] Add boundary if present when applying domain-wide footer
2025-09-09 10:57:18 +02:00
FreddleSpl0it
8d7235b535 [RSPAMD] Add boundary if present when applying domain-wide footer 2025-09-09 10:52:19 +02:00
FreddleSpl0it
8446abd484 Merge pull request #6713 from mailcow/feat/internal-alias
[Rspamd][Web] Internal alias support
2025-09-09 10:45:56 +02:00
FreddleSpl0it
f67c0530f5 [Rspamd][Web] Internal alias support 2025-09-09 10:37:54 +02:00
Dmitriy Alekseev
06db1d6a72 [Rspamd] Do not increment rate limit for emails from user to himself (#6706)
* [Rspamd] Do not increment rate limit for emails from user to himself

* Lowercase username and recipient address for comparison

Normalize username and recipient address comparison to lowercase.
2025-09-05 03:37:59 +02:00
renovate[bot]
81775ab4d5 chore(deps): update actions/stale action to v10 (#6708)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-04 18:02:11 +02:00
DerLinkman
34877ecf9c watchdog: added postfix-tlspol check (#6691) 2025-09-03 08:18:04 +02:00
milkmaker
dbde144014 update postscreen_access.cidr (#6703) 2025-09-03 08:14:14 +02:00
DerLinkman
5361a4a4ee updated sponsors in Readme.md 2025-09-01 12:32:27 +02:00
milkmaker
0997548d7f Translations update from Weblate (#6699)
* [Web] Updated lang.de-de.json

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

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

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

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

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

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

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

Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>

---------

Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
2025-08-29 22:38:06 +02:00
Sándor
921de02a2b Update lang.hu-hu.json (#6697)
Extended Hungarian translation
2025-08-29 18:32:17 +02:00
Peter
48e90a72dc Changed clamavs tmp folder structure 2025-08-29 18:27:34 +02:00
renovate[bot]
c0b7a98e6c chore(deps): update actions/checkout action to v5 (#6671)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-29 18:23:56 +02:00
DerLinkman
0b0a65a3f3 web: rename login placeholder for mailbox to email address (#6693) 2025-08-28 17:02:16 +02:00
DerLinkman
6c5d82c4df expanded postscreen whitelist with modern freemailers + included checks.mailcow.email 2025-08-28 14:06:17 +02:00
maxi322
5e66ffa366 watchdog: use dig instead of check_dns (#6685)
* watchdog: use dig instead of check_dns

check_dns is slower and uses more system resources,
dig wrapped in a script is a more performant approach and uses
fewer system resources

* added debug mode + compose image bump

---------

Co-authored-by: maxi322 <maxi322@users.noreply.github.com>
Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
2025-08-28 12:56:37 +02:00
Sajjad hassanzadeh
4d88e19106 Feat/prometheus-exporter : Add prometheus exporter and grafana dashboard for mailcow. (#6314)
* add : readme for prometheus exporter configs

* add : grafana dashboard json file

* add: prometheus exporter service on docker-compose.override.yml

* migrate: doc files into docs.mailcow.email project

* add : security configs in prometheus exporter compose file

* add : explain more in my comment part in prometheus override compose file

* remove : mailcow dockerized docs

---------

Co-authored-by: Saji <saji@abrnoc>
2025-08-28 12:36:43 +02:00
DerLinkman
29e28b47ed compose: add depends on for postfix-tlspol 2025-08-28 10:20:21 +02:00
DerLinkman
1cb38bacdb Postfix: Split TLSPol companion app into separate container (#6688)
* postfix: split postfix-tlspol service into new container

* postfix-tls-pol: added debug mode

* pf-tlspol: removed obsoleted standalone conf from Dockerfiles

* pf-tlspol: use git instead of wget
2025-08-28 10:18:18 +02:00
DerLinkman
169aafec50 compose: fix dovecot image tag 2025-08-27 14:09:29 +02:00
DerLinkman
3826c4b5be fix postfix tlspol missing folders for config 2025-08-26 10:10:16 +02:00
DerLinkman
e1410baaeb fix gitignore 2025-08-26 09:58:18 +02:00
DerLinkman
c39712af67 pf/php: add mta-sts support (outbound) (#6686)
* added mta-sts-resolver into postfix config + daemon

* [Web] Add MTA-STS support

* [Web] Fix mta-sts server_name

* updated .gitignore

* [ACME] fetch cert for mta-sts subdomain

* [Web] change MTA-STS id to human-readable timestamp

* [Web] Remove MTA-STS version STSv2

* [Web] Fix MTA-STS DNS check

* [Web] add max_age limit for MTA-STS policy

* Added tooltips and info texts to mta-sts webui page

* postfix: replace mta-sts-resolver with postfix-tlspol

---------

Co-authored-by: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com>
2025-08-26 09:57:05 +02:00
Paul Sütterlin
53c35493a5 fix: imapsync gets correct timeouts
Previously imapsync only attached the timeout1 / timeout2 arguments if the argument was negative (which is not even possible). Now the argument is added for every positive number.

Fixes #6590
2025-08-21 18:36:01 +00:00
renovate[bot]
af871fdacb chore(deps): update devops-infra/action-pull-request action to v0.6.1 (#6676)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-13 18:47:28 +02:00
DerLinkman
2b93b59cdd db: change qhash varchar to 64 instead of 255 2025-08-06 16:11:23 +02:00
Christoph Lechleitner
2b2da1679e [DB][Web] optimize qhandler by keeping SHA2 in new column qhash (#6556)
* [DB][Web] optimize qhandler by keeping SHA2(id+qid) in new column quarantine.qhash, for feature #6555, might also help with #6361

* rspamd: only add qhash to new entries while passing rspamd not all existing

* compose: bump dovecot image + push to registry

---------

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
2025-08-06 16:02:25 +02:00
CodeShell
8cdb0b869e fixed favicon.png (#6570) 2025-08-06 09:42:43 +02:00
FreddleSpl0it
1e42b8dd21 [Web] Add delimiter_action to mailbox and mailbox_template add/edit admin forms (#6620) 2025-08-06 09:40:47 +02:00
Dmitriy Alekseev
842cb235b6 [Rspamd] Fill module name for set_pre_result actions (#6630)
* [Rspamd] Fill module name for postmaster handler

* Update rspamd.local.lua
2025-08-06 09:38:22 +02:00
DerLinkman
e91d678bd1 fix docker version detection 2025-08-06 09:36:05 +02:00
DerLinkman
ef5739c32f add 2025-08 as breaking major release 2025-08-06 08:39:21 +02:00
DerLinkman
88bf9b02e1 core: modules splitting + ipv6 nat rewrite (#6634)
* ipv6: added ipv6 detection + removed ip6 nat container

* nginx: renamed DISABLE_IPv6 to ENABLE_IPV6 to align

* initial commit for script overhauls

* rewrite to scripts after testing (improved error handling)

* fixed missing fi in update.sh

* fixed/added comments for modules

* fix broken EXIT_CODE var handling

* added jq as dependancy

* fixed docker version check for daemon

* improved _modules handling while running

* reintegrated module loading (update.sh)

* added error handling for blank daemon.json

* adapted removal of ACME_CONTACT for nightly

* move detect_major_update func to core submodule

* removed unnecessary message on every call of function

* Update _modules/scripts/new_options.sh

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update _modules/scripts/core.sh

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* improve ENABLE_IPV6 check in nginx bootstrap

* improve detection of ENABLE_IPV6

* ip6_controller: moved docker major detection upwards

* Update _modules/scripts/new_options.sh

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update _modules/scripts/new_options.sh

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* reuse DOCKER_MAJOR Variable in ip6_controller

* fix some smaller typos in update.sh

* smaller bugfixes in submodules

* completely remove ACME_CONTACT Variable

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-06 08:36:40 +02:00
renovate[bot]
3803b5d351 Update dependency php-memcached-dev/php-memcached to v3.3.0 (#6638)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 08:35:14 +02:00
renovate[bot]
14d58c8163 Update dependency phpredis/phpredis to v6.2.0 (#6639)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 08:34:53 +02:00
renovate[bot]
728fcdb375 Update dependency tianon/gosu to v1.17 (#6640)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 08:34:30 +02:00
renovate[bot]
1fc36263dc chore(deps): update dependency krakjoe/apcu to v5.1.26 (#6656)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 08:33:41 +02:00
Markus Machatschek
69420113f7 rspamd: update rspamd to 3.12.1 (#6643)
* rspamd: update rspamd to 3.12.1

* compose: correct rspamd tag + pushed image

---------

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
2025-08-06 08:33:11 +02:00
DerLinkman
360fe03497 sogo: update to 5.12.3 2025-08-05 16:01:47 +02:00
milkmaker
7557802933 [Web] Updated lang.de-de.json (#6661)
Co-authored-by: whitehotaru <whitehotaru@posteo.net>
2025-08-05 06:37:55 +02:00
milkmaker
2e9ba1e9b3 update postscreen_access.cidr (#6660) 2025-08-05 00:37:47 +02:00
milkmaker
795bcdc5d2 [Web] Updated lang.ru-ru.json (#6654) 2025-07-27 19:30:10 +02:00
Paul Sütterlin
ad9b328ed5 fix: Undefined array key "pending_tfa_methods" in
/web/inc/footer.inc.php
on line
29
2025-07-26 01:12:48 +00:00
Paul Sütterlin
3d5b57889a fix: Empty App Links
The return value of the function caused a warning
in header.inc.php:42 if no additional links were set.

header.inc.php is the only caller of this function,
thus it is safe to return an empty array here.
2025-07-26 01:08:28 +00:00
Paul Sütterlin
6b8e981bdc fix: Only use HTTP_ORIGIN if it is sent. 2025-07-26 01:06:24 +00:00
milkmaker
2f1eb4b004 Translations update from Weblate (#6649)
* [Web] Updated lang.es-es.json

Co-authored-by: sariegos <informatica@sariegos.es>

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

Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>

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

[Web] Updated lang.pt-br.json

Co-authored-by: Bruno Zouein Pereira <zopostyle@gmail.com>
Co-authored-by: Peter <magic@kthx.at>

---------

Co-authored-by: sariegos <informatica@sariegos.es>
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
Co-authored-by: Bruno Zouein Pereira <zopostyle@gmail.com>
Co-authored-by: Peter <magic@kthx.at>
2025-07-24 21:42:00 +02:00
milkmaker
3ee3d7d969 Translations update from Weblate (#6637)
* [Web] Language file updated by 'Cleanup translation files' addon

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

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

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

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

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

---------

Co-authored-by: Anonymous <noreply@weblate.org>
2025-07-16 20:31:58 +02:00
Denis Evers
95eb350f15 [netfilter] fix negative timer, no unbanning of IPs (#6575)
* [netfilter] added debug logs and updated autopurge

* updated "Allow/Blacklist" terms

* netfilter: bumped compose version

* netfilter: changed black/whitelist terms in code

---------

Co-authored-by: Denis Evers <git@evers.sh>
Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
2025-07-16 10:08:55 +02:00
Peter
1e5fcfe392 Bulgarian language added (#6623) 2025-07-16 09:29:35 +02:00
FreddleSpl0it
02557b2098 Merge pull request #6631 from mailcow/fix/jinja2-rendering
[Dovecot] Use Jinja2 sandbox for rendering quota and quarantine notif…
2025-07-15 07:38:20 +02:00
milkmaker
4c7a9ed195 Translations update from Weblate (#6629)
* [Web] Updated lang.fr-fr.json

Co-authored-by: Tagada <githubtagada@cant.at>

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

Co-authored-by: robsonek <admin@licytant.pl>

---------

Co-authored-by: Tagada <githubtagada@cant.at>
Co-authored-by: robsonek <admin@licytant.pl>
2025-07-13 16:20:40 +02:00
milkmaker
d5b30a7a08 [Web] Updated lang.pt-pt.json (#6622)
Co-authored-by: luiscanato <luiscanato@gmail.com>
2025-07-06 16:42:58 +02:00
FreddleSpl0it
b7acef4d9d Merge pull request #6617 from mailcow/fix/acme-contact
[ACME] Remove deprecated ACME_CONTACT variable
2025-07-03 12:49:17 +02:00
FreddleSpl0it
fc43c26c48 Remove obsolete ACME_CONTACT option and related comments from mailcow.conf 2025-07-03 12:38:28 +02:00
milkmaker
b12ce1eacd update postscreen_access.cidr (#6611) 2025-07-02 17:12:06 +02:00
FreddleSpl0it
ec6dbb099a [ACME] Remove deprecated ACME_CONTACT variable 2025-07-02 10:37:23 +02:00
FreddleSpl0it
2fbbbbe9a9 [Dovecot] Use Jinja2 sandbox for rendering quota and quarantine notifications 2025-07-02 08:59:29 +02:00
milkmaker
1e4f3c55d8 [Web] Updated lang.pt-pt.json (#6614)
Co-authored-by: luiscanato <luiscanato@gmail.com>
2025-07-01 17:14:24 +02:00
milkmaker
a0f5454c2a [Web] Updated lang.si-si.json (#6609)
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
2025-06-29 18:12:59 +02:00
milkmaker
4e7adacda9 [Web] Updated lang.si-si.json (#6601)
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
2025-06-24 20:06:38 +02:00
milkmaker
4c64cf18a6 [Web] Updated lang.si-si.json (#6600)
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
2025-06-23 21:58:50 +02:00
DerLinkman
8a89f5c685 updated Readme (sponsors) 2025-06-23 10:36:49 +02:00
milkmaker
cc0e4fee9d [Web] Updated lang.si-si.json (#6599)
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
2025-06-20 18:14:22 +02:00
milkmaker
5861c9af29 [Web] Updated lang.cs-cz.json (#6589) 2025-06-16 19:45:13 +02:00
milkmaker
dd475c0ab3 update postscreen_access.cidr (#6573) 2025-06-12 13:14:20 +02:00
milkmaker
407e9d3584 Translations update from Weblate (#6582)
* [Web] Updated lang.en-gb.json

[Web] Updated lang.en-gb.json

Co-authored-by: Filip Hajny <filip@hajny.net>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

Co-authored-by: Filip Hajny <filip@hajny.net>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

[Web] Updated lang.si-si.json

Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

Co-authored-by: Stefano <stefano.vassena@gmail.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

---------

Co-authored-by: Filip Hajny <filip@hajny.net>
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
Co-authored-by: Stefano <stefano.vassena@gmail.com>
2025-06-12 13:13:57 +02:00
DerLinkman
d4f899b091 compose: add selinux label to mysql-socket-vol to prevent "access denied" (#6560) 2025-05-26 11:35:34 +02:00
milkmaker
372923ae2f [Web] Updated lang.zh-cn.json (#6552)
Co-authored-by: Easton Man <me@eastonman.com>
2025-05-19 13:36:15 +02:00
milkmaker
3bd01190bf Translations update from Weblate (#6548)
* [Web] Updated lang.fr-fr.json

[Web] Updated lang.fr-fr.json

Co-authored-by: Samuel F. <20537389+samuelfranzini@users.noreply.github.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

[Web] Updated lang.ru-ru.json

[Web] Updated lang.ru-ru.json

Co-authored-by: Habetdin <15926758+Habetdin@users.noreply.github.com>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

Co-authored-by: Easton Man <me@eastonman.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

Co-authored-by: Habetdin <15926758+Habetdin@users.noreply.github.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

---------

Co-authored-by: Samuel F. <20537389+samuelfranzini@users.noreply.github.com>
Co-authored-by: Habetdin <15926758+Habetdin@users.noreply.github.com>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: Easton Man <me@eastonman.com>
2025-05-16 23:07:25 +02:00
FreddleSpl0it
0698159f07 Add redis-to-valkey migratior 2025-02-28 15:49:28 +01:00
FreddleSpl0it
c27000215e migrate from redis to valkey 2025-02-28 15:36:19 +01:00
155 changed files with 7685 additions and 2966 deletions

View File

@@ -11,22 +11,35 @@ body:
required: true required: true
- type: checkboxes - type: checkboxes
attributes: attributes:
label: I've found a bug and checked that ... label: Checklist prior issue creation
description: Prior to placing the issue, please check following:** *(fill out each checkbox with an `X` once done)* description: Prior to creating the issue...
options: options:
- label: ... I understand that not following the below instructions will result in immediate closure and/or deletion of my issue. - label: I understand that failure to follow below instructions may cause this issue to be closed.
required: true required: true
- label: ... I have understood that this bug report is dedicated for bugs, and not for support-related inquiries. - label: I understand that vague, incomplete or inaccurate information may cause this issue to be closed.
required: true required: true
- label: ... I have understood that answers are voluntary and community-driven, and not commercial support. - label: I understand that this form is intended solely for reporting software bugs and not for support-related inquiries.
required: true required: true
- label: ... I have verified that my issue has not been already answered in the past. I also checked previous [issues](https://github.com/mailcow/mailcow-dockerized/issues). - label: I understand that all responses are voluntary and community-driven, and do not constitute commercial support.
required: true
- label: I confirm that I have reviewed previous [issues](https://github.com/mailcow/mailcow-dockerized/issues) to ensure this matter has not already been addressed.
required: true
- label: I confirm that my environment meets all [prerequisite requirements](https://docs.mailcow.email/getstarted/prerequisite-system/) as specified in the official documentation.
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Description label: Description
description: Please provide a brief description of the bug in 1-2 sentences. If applicable, add screenshots to help explain your problem. Very useful for bugs in mailcow UI. description: Please provide a brief description of the bug. If applicable, add screenshots to help explain your problem. (Very useful for bugs in mailcow UI.)
render: plain text validations:
required: true
- type: textarea
attributes:
label: "Steps to reproduce:"
description: "Please describe the steps to reproduce the bug. Screenshots can be added, if helpful."
placeholder: |-
1. ...
2. ...
3. ...
validations: validations:
required: true required: true
- type: textarea - type: textarea
@@ -36,45 +49,36 @@ body:
render: plain text render: plain text
validations: validations:
required: true required: true
- type: textarea
attributes:
label: "Steps to reproduce:"
description: "Please describe the steps to reproduce the bug. Screenshots can be added, if helpful."
render: plain text
placeholder: |-
1. ...
2. ...
3. ...
validations:
required: true
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
## System information ## System information
### In this stage we would kindly ask you to attach general system information about your setup. In this stage we would kindly ask you to attach general system information about your setup.
- type: dropdown - type: dropdown
attributes: attributes:
label: "Which branch are you using?" label: "Which branch are you using?"
description: "#### `git rev-parse --abbrev-ref HEAD`" description: "#### Run: `git rev-parse --abbrev-ref HEAD`"
multiple: false multiple: false
options: options:
- master - master (stable)
- staging
- nightly - nightly
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
attributes: attributes:
label: "Which architecture are you using?" label: "Which architecture are you using?"
description: "#### `uname -m`" description: "#### Run: `uname -m`"
multiple: false multiple: false
options: options:
- x86 - x86_64
- ARM64 (aarch64) - ARM64 (aarch64)
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: "Operating System:" label: "Operating System:"
description: "#### Run: `lsb_release -ds`"
placeholder: "e.g. Ubuntu 22.04 LTS" placeholder: "e.g. Ubuntu 22.04 LTS"
validations: validations:
required: true required: true
@@ -93,43 +97,44 @@ body:
- type: input - type: input
attributes: attributes:
label: "Virtualization technology:" label: "Virtualization technology:"
placeholder: "KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported**" description: "LXC and OpenVZ are not supported!"
placeholder: "KVM, VMware ESXi, Xen, etc"
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: "Docker version:" label: "Docker version:"
description: "#### `docker version`" description: "#### Run: `docker version`"
placeholder: "20.10.21" placeholder: "20.10.21"
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: "docker-compose version or docker compose version:" label: "docker-compose version or docker compose version:"
description: "#### `docker-compose version` or `docker compose version`" description: "#### Run: `docker-compose version` or `docker compose version`"
placeholder: "v2.12.2" placeholder: "v2.12.2"
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: "mailcow version:" label: "mailcow version:"
description: "#### ```git describe --tags `git rev-list --tags --max-count=1` ```" description: "#### Run: ```git describe --tags `git rev-list --tags --max-count=1` ```"
placeholder: "2022-08" placeholder: "2022-08x"
validations: validations:
required: true required: true
- type: input - type: input
attributes: attributes:
label: "Reverse proxy:" label: "Reverse proxy:"
placeholder: "e.g. Nginx/Traefik" placeholder: "e.g. nginx/Traefik, or none"
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: "Logs of git diff:" label: "Logs of git diff:"
description: "#### Output of `git diff origin/master`, any other changes to the code? If so, **please post them**:" description: "#### Output of `git diff origin/master`, any other changes to the code? Sanitize if needed. If so, **please post them**:"
render: plain text render: plain text
validations: validations:
required: true required: false
- type: textarea - type: textarea
attributes: attributes:
label: "Logs of iptables -L -vn:" label: "Logs of iptables -L -vn:"

View File

@@ -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@v9.1.0 uses: actions/stale@v10.1.0
with: with:
repo-token: ${{ secrets.STALE_ACTION_PAT }} repo-token: ${{ secrets.STALE_ACTION_PAT }}
days-before-stale: 60 days-before-stale: 60

View File

@@ -27,7 +27,7 @@ jobs:
- "watchdog-mailcow" - "watchdog-mailcow"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- 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

View File

@@ -8,11 +8,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Run the Action - name: Run the Action
uses: devops-infra/action-pull-request@v0.6.0 uses: devops-infra/action-pull-request@v0.6.1
with: with:
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }} github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}} title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}

View File

@@ -13,7 +13,7 @@ jobs:
packages: write packages: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Generate postscreen_access.cidr - name: Generate postscreen_access.cidr
run: | run: |

1
.gitignore vendored
View File

@@ -75,3 +75,4 @@ refresh_images.sh
update_diffs/ update_diffs/
create_cold_standby.sh create_cold_standby.sh
!data/conf/nginx/mailcow_auth.conf !data/conf/nginx/mailcow_auth.conf
data/conf/postfix/postfix-tlspol

230
_modules/scripts/core.sh Normal file
View File

@@ -0,0 +1,230 @@
#!/usr/bin/env bash
# _modules/scripts/core.sh
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
# ANSI color for red errors
RED='\e[31m'
GREEN='\e[32m'
YELLOW='\e[33m'
BLUE='\e[34m'
MAGENTA='\e[35m'
LIGHT_RED='\e[91m'
LIGHT_GREEN='\e[92m'
NC='\e[0m'
caller="${BASH_SOURCE[1]##*/}"
get_installed_tools(){
for bin in openssl curl docker git awk sha1sum grep cut jq; do
if [[ -z $(command -v ${bin}) ]]; then
echo "Error: Cannot find command '${bin}'. Cannot proceed."
echo "Solution: Please review system requirements and install requirements. Then, re-run the script."
echo "See System Requirements: https://docs.mailcow.email/getstarted/install/"
echo "Exiting..."
exit 1
fi
done
if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo -e "${LIGHT_RED}BusyBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\"${NC}"; exit 1; fi
# This will also cover sort
if cp --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo -e "${LIGHT_RED}BusyBox cp detected, please install coreutils, \"apk add --no-cache --upgrade coreutils\"${NC}"; exit 1; fi
if sed --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo -e "${LIGHT_RED}BusyBox sed detected, please install gnu sed, \"apk add --no-cache --upgrade sed\"${NC}"; exit 1; fi
}
get_docker_version(){
# Check Docker Version (need at least 24.X)
docker_version=$(docker version --format '{{.Server.Version}}' | cut -d '.' -f 1)
}
get_compose_type(){
if docker compose > /dev/null 2>&1; then
if docker compose version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then
COMPOSE_VERSION=native
COMPOSE_COMMAND="docker compose"
if [[ "$caller" == "update.sh" ]]; then
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf"
fi
echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m"
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
sleep 2
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m"
else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
elif docker-compose > /dev/null 2>&1; then
if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
if docker-compose version --short | grep "^2." > /dev/null 2>&1; then
COMPOSE_VERSION=standalone
COMPOSE_COMMAND="docker-compose"
if [[ "$caller" == "update.sh" ]]; then
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf"
fi
echo -e "\e[33mFound Docker Compose Standalone.\e[0m"
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m"
sleep 2
echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m"
else
echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
fi
else
echo -e "\e[31mCannot find Docker Compose.\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1
fi
}
detect_bad_asn() {
echo -e "\e[33mDetecting if your IP is listed on Spamhaus Bad ASN List...\e[0m"
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
if [ -z "$SPAMHAUS_DQS_KEY" ]; then
echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m"
echo -e "\e[33mmailcow did not detected a value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf!\e[0m"
sleep 2
echo ""
echo -e "\e[33mTo use the Spamhaus DNS Blocklists again, you will need to create a FREE account for their Data Query Service (DQS) at: https://www.spamhaus.com/free-trial/sign-up-for-a-free-data-query-service-account\e[0m"
echo -e "\e[33mOnce done, enter your DQS API key in mailcow.conf and mailcow will do the rest for you!\e[0m"
echo ""
sleep 2
else
echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m"
echo -e "\e[32mmailcow detected a Value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf. Postfix will use DQS with the given API key...\e[0m"
fi
elif [ "$response" -eq 200 ]; then
echo -e "\e[33mCheck completed! Your IP is \e[32mclean\e[0m"
elif [ "$response" -eq 429 ]; then
echo -e "\e[33mCheck completed! \e[31mYour IP seems to be rate limited on the ASN Check service... please try again later!\e[0m"
else
echo -e "\e[31mCheck failed! \e[0mMaybe a DNS or Network problem?\e[0m"
fi
}
check_online_status() {
CHECK_ONLINE_DOMAINS=('https://github.com' 'https://hub.docker.com')
for domain in "${CHECK_ONLINE_DOMAINS[@]}"; do
if timeout 6 curl --head --silent --output /dev/null ${domain}; then
return 0
fi
done
return 1
}
prefetch_images() {
[[ -z ${BRANCH} ]] && { echo -e "\e[33m\nUnknown branch...\e[0m"; exit 1; }
git fetch origin #${BRANCH}
while read image; do
RET_C=0
until docker pull "${image}"; do
RET_C=$((RET_C + 1))
echo -e "\e[33m\nError pulling $image, retrying...\e[0m"
[ ${RET_C} -gt 3 ] && { echo -e "\e[31m\nToo many failed retries, exiting\e[0m"; exit 1; }
sleep 1
done
done < <(git show "origin/${BRANCH}:docker-compose.yml" | grep "image:" | awk '{ gsub("image:","", $3); print $2 }')
}
docker_garbage() {
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )"
IMGS_TO_DELETE=()
declare -A IMAGES_INFO
COMPOSE_IMAGES=($(grep -oP "image: \K(ghcr\.io/)?mailcow.+" "${SCRIPT_DIR}/docker-compose.yml"))
for existing_image in $(docker images --format "{{.ID}}:{{.Repository}}:{{.Tag}}" | grep -E '(mailcow/|ghcr\.io/mailcow/)'); do
ID=$(echo "$existing_image" | cut -d ':' -f 1)
REPOSITORY=$(echo "$existing_image" | cut -d ':' -f 2)
TAG=$(echo "$existing_image" | cut -d ':' -f 3)
if [[ "$REPOSITORY" == "mailcow/backup" || "$REPOSITORY" == "ghcr.io/mailcow/backup" ]]; then
if [[ "$TAG" != "<none>" ]]; then
continue
fi
fi
if [[ " ${COMPOSE_IMAGES[@]} " =~ " ${REPOSITORY}:${TAG} " ]]; then
continue
else
IMGS_TO_DELETE+=("$ID")
IMAGES_INFO["$ID"]="$REPOSITORY:$TAG"
fi
done
if [[ ! -z ${IMGS_TO_DELETE[*]} ]]; then
echo "The following unused mailcow images were found:"
for id in "${IMGS_TO_DELETE[@]}"; do
echo " ${IMAGES_INFO[$id]} ($id)"
done
if [ -z "$FORCE" ]; then
read -r -p "Do you want to delete them to free up some space? [y/N] " response
if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
docker rmi ${IMGS_TO_DELETE[*]}
else
echo "OK, skipped."
fi
else
echo "Running in forced mode! Force removing old mailcow images..."
docker rmi ${IMGS_TO_DELETE[*]}
fi
echo -e "\e[32mFurther cleanup...\e[0m"
echo "If you want to cleanup further garbage collected by Docker, please make sure all containers are up and running before cleaning your system by executing \"docker system prune\""
fi
}
in_array() {
local e match="$1"
shift
for e; do [[ "$e" == "$match" ]] && return 0; done
return 1
}
detect_major_update() {
if [ ${BRANCH} == "master" ]; then
# Array with major versions
# Add major versions here
MAJOR_VERSIONS=(
"2025-02"
"2025-03"
"2025-09"
)
current_version=""
if [[ -f "${SCRIPT_DIR}/data/web/inc/app_info.inc.php" ]]; then
current_version=$(grep 'MAILCOW_GIT_VERSION' ${SCRIPT_DIR}/data/web/inc/app_info.inc.php | sed -E 's/.*MAILCOW_GIT_VERSION="([^"]+)".*/\1/')
fi
if [[ -z "$current_version" ]]; then
return 1
fi
release_url="https://github.com/mailcow/mailcow-dockerized/releases/tag"
updates_to_apply=()
for version in "${MAJOR_VERSIONS[@]}"; do
if [[ "$current_version" < "$version" ]]; then
updates_to_apply+=("$version")
fi
done
if [[ ${#updates_to_apply[@]} -gt 0 ]]; then
echo -e "\e[33m\nMAJOR UPDATES to be applied:\e[0m"
for update in "${updates_to_apply[@]}"; do
echo "$update - $release_url/$update"
done
echo -e "\nPlease read the release notes before proceeding."
read -p "Do you want to proceed with the update? [y/n] " response
if [[ "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "Proceeding with the update..."
else
echo "Update canceled. Exiting."
exit 1
fi
fi
fi
}

View File

@@ -0,0 +1,239 @@
#!/usr/bin/env bash
# _modules/scripts/ipv6_controller.sh
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
# 1) Check if the host supports IPv6
get_ipv6_support() {
# ---- helper: probe external IPv6 connectivity without DNS ----
_probe_ipv6_connectivity() {
# Use literal, always-on IPv6 echo responders (no DNS required)
local PROBE_IPS=("2001:4860:4860::8888" "2606:4700:4700::1111")
local ip rc=1
for ip in "${PROBE_IPS[@]}"; do
if command -v ping6 &>/dev/null; then
ping6 -c1 -W2 "$ip" &>/dev/null || ping6 -c1 -w2 "$ip" &>/dev/null
rc=$?
elif command -v ping &>/dev/null; then
ping -6 -c1 -W2 "$ip" &>/dev/null || ping -6 -c1 -w2 "$ip" &>/dev/null
rc=$?
else
rc=1
fi
[[ $rc -eq 0 ]] && return 0
done
return 1
}
if [[ ! -f /proc/net/if_inet6 ]] || grep -qs '^1' /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null; then
DETECTED_IPV6=false
echo -e "${YELLOW}IPv6 not detected on host ${LIGHT_RED}IPv6 is administratively disabled${YELLOW}.${NC}"
return
fi
if ip -6 route show default 2>/dev/null | grep -qE '^default'; then
echo -e "${YELLOW}Default IPv6 route found testing external IPv6 connectivity...${NC}"
if _probe_ipv6_connectivity; then
DETECTED_IPV6=true
echo -e "IPv6 detected on host ${LIGHT_GREEN}leaving IPv6 support enabled${YELLOW}.${NC}"
else
DETECTED_IPV6=false
echo -e "${YELLOW}Default IPv6 route present but external IPv6 connectivity failed ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
fi
return
fi
if ip -6 addr show scope global 2>/dev/null | grep -q 'inet6'; then
DETECTED_IPV6=false
echo -e "${YELLOW}Global IPv6 address present but no default route ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
return
fi
if ip -6 addr show scope link 2>/dev/null | grep -q 'inet6'; then
echo -e "${YELLOW}Only link-local IPv6 addresses found testing external IPv6 connectivity...${NC}"
if _probe_ipv6_connectivity; then
DETECTED_IPV6=true
echo -e "External IPv6 connectivity available ${LIGHT_GREEN}leaving IPv6 support enabled${YELLOW}.${NC}"
else
DETECTED_IPV6=false
echo -e "${YELLOW}Only link-local IPv6 present and no external connectivity ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
fi
return
fi
DETECTED_IPV6=false
echo -e "${YELLOW}IPv6 not detected on host ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}"
}
# 2) Ensure Docker daemon.json has (or create) the required IPv6 settings
docker_daemon_edit(){
DOCKER_DAEMON_CONFIG="/etc/docker/daemon.json"
DOCKER_MAJOR=$(docker version --format '{{.Server.Version}}' 2>/dev/null | cut -d. -f1)
MISSING=()
_has_kv() { grep -Eq "\"$1\"[[:space:]]*:[[:space:]]*$2" "$DOCKER_DAEMON_CONFIG" 2>/dev/null; }
if [[ -f "$DOCKER_DAEMON_CONFIG" ]]; then
# reject empty or whitespace-only file immediately
if [[ ! -s "$DOCKER_DAEMON_CONFIG" ]] || ! grep -Eq '[{}]' "$DOCKER_DAEMON_CONFIG"; then
echo -e "${RED}ERROR: $DOCKER_DAEMON_CONFIG exists but is empty or contains no JSON braces please initialize it with valid JSON (e.g. {}).${NC}"
exit 1
fi
# Validate JSON if jq is present
if command -v jq &>/dev/null && ! jq empty "$DOCKER_DAEMON_CONFIG" &>/dev/null; then
echo -e "${RED}ERROR: Invalid JSON in $DOCKER_DAEMON_CONFIG please correct manually.${NC}"
exit 1
fi
# Gather missing keys
! _has_kv ipv6 true && MISSING+=("ipv6: true")
# For Docker < 28, keep requiring fixed-cidr-v6 (default bridge needs it on old engines)
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 28 ]]; then
! grep -Eq '"fixed-cidr-v6"[[:space:]]*:[[:space:]]*".+"' "$DOCKER_DAEMON_CONFIG" \
&& MISSING+=('fixed-cidr-v6: "fd00:dead:beef:c0::/80"')
fi
# For Docker < 27, ip6tables needed and was tied to experimental in older releases
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then
_has_kv ipv6 true && ! _has_kv ip6tables true && MISSING+=("ip6tables: true")
! _has_kv experimental true && MISSING+=("experimental: true")
fi
# Fix if needed
if ((${#MISSING[@]}>0)); then
echo -e "${MAGENTA}Your daemon.json is missing: ${YELLOW}${MISSING[*]}${NC}"
if [[ -n "$FORCE" ]]; then
ans=Y
else
read -p "Would you like to update $DOCKER_DAEMON_CONFIG now? [Y/n] " ans
ans=${ans:-Y}
fi
if [[ $ans =~ ^[Yy]$ ]]; then
cp "$DOCKER_DAEMON_CONFIG" "${DOCKER_DAEMON_CONFIG}.bak"
if command -v jq &>/dev/null; then
TMP=$(mktemp)
# Base filter: ensure ipv6 = true
JQ_FILTER='.ipv6 = true'
# Add fixed-cidr-v6 only for Docker < 28
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 28 ]]; then
JQ_FILTER+=' | .["fixed-cidr-v6"] = (.["fixed-cidr-v6"] // "fd00:dead:beef:c0::/80")'
fi
# Add ip6tables/experimental only for Docker < 27
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then
JQ_FILTER+=' | .ip6tables = true | .experimental = true'
fi
jq "$JQ_FILTER" "$DOCKER_DAEMON_CONFIG" >"$TMP" && mv "$TMP" "$DOCKER_DAEMON_CONFIG"
echo -e "${LIGHT_GREEN}daemon.json updated. Restarting Docker...${NC}"
(command -v systemctl &>/dev/null && systemctl restart docker) || service docker restart
echo -e "${YELLOW}Docker restarted.${NC}"
else
echo -e "${RED}Please install jq or manually update daemon.json and restart Docker.${NC}"
exit 1
fi
else
echo -e "${YELLOW}User declined Docker update please insert these changes manually:${NC}"
echo "${MISSING[*]}"
exit 1
fi
fi
else
# Create new daemon.json if missing
if [[ -n "$FORCE" ]]; then
ans=Y
else
read -p "$DOCKER_DAEMON_CONFIG not found. Create it with IPv6 settings? [Y/n] " ans
ans=${ans:-Y}
fi
if [[ $ans =~ ^[Yy]$ ]]; then
mkdir -p "$(dirname "$DOCKER_DAEMON_CONFIG")"
if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then
cat > "$DOCKER_DAEMON_CONFIG" <<EOF
{
"ipv6": true,
"fixed-cidr-v6": "fd00:dead:beef:c0::/80",
"ip6tables": true,
"experimental": true
}
EOF
elif [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 28 ]]; then
cat > "$DOCKER_DAEMON_CONFIG" <<EOF
{
"ipv6": true,
"fixed-cidr-v6": "fd00:dead:beef:c0::/80"
}
EOF
else
# Docker 28+: ipv6 works without fixed-cidr-v6
cat > "$DOCKER_DAEMON_CONFIG" <<EOF
{
"ipv6": true
}
EOF
fi
echo -e "${GREEN}Created $DOCKER_DAEMON_CONFIG with IPv6 settings.${NC}"
echo "Restarting Docker..."
(command -v systemctl &>/dev/null && systemctl restart docker) || service docker restart
echo "Docker restarted."
else
echo "User declined to create daemon.json please manually merge the docker daemon with these configs:"
echo "${MISSING[*]}"
exit 1
fi
fi
}
# 3) Main wrapper for generate_config.sh and update.sh
configure_ipv6() {
# detect manual override if mailcow.conf is present
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]] && grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
MANUAL_SETTING=$(grep '^ENABLE_IPV6=' "$MAILCOW_CONF" | cut -d= -f2)
elif [[ -z "$MAILCOW_CONF" ]] && [[ -n "${ENABLE_IPV6:-}" ]]; then
MANUAL_SETTING="$ENABLE_IPV6"
else
MANUAL_SETTING=""
fi
get_ipv6_support
# if user manually set it, check for mismatch
if [[ "$DETECTED_IPV6" != "true" ]]; then
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]]; then
if grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
sed -i 's/^ENABLE_IPV6=.*/ENABLE_IPV6=false/' "$MAILCOW_CONF"
else
echo "ENABLE_IPV6=false" >> "$MAILCOW_CONF"
fi
else
export IPV6_BOOL=false
fi
echo "Skipping Docker IPv6 configuration because host does not support IPv6."
echo "Make sure to check if your docker daemon.json does not include \"enable_ipv6\": true if you do not want IPv6."
echo "IPv6 configuration complete: ENABLE_IPV6=false"
sleep 2
return
fi
docker_daemon_edit
if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]]; then
if grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then
sed -i 's/^ENABLE_IPV6=.*/ENABLE_IPV6=true/' "$MAILCOW_CONF"
else
echo "ENABLE_IPV6=true" >> "$MAILCOW_CONF"
fi
else
export IPV6_BOOL=true
fi
echo "IPv6 configuration complete: ENABLE_IPV6=true"
}

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# _modules/scripts/migrate_options.sh
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
migrate_config_options() {
sed -i --follow-symlinks '$a\' mailcow.conf
KEYS=(
SOLR_HEAP
SKIP_SOLR
SOLR_PORT
FLATCURVE_EXPERIMENTAL
DISABLE_IPv6
ACME_CONTACT
)
for key in "${KEYS[@]}"; do
if grep -q "${key}" mailcow.conf; then
case "${key}" in
SOLR_HEAP)
echo "Removing ${key} in mailcow.conf"
sed -i '/# Solr heap size in MB\b/d' mailcow.conf
sed -i '/# Solr is a prone to run\b/d' mailcow.conf
sed -i '/SOLR_HEAP\b/d' mailcow.conf
;;
SKIP_SOLR)
echo "Removing ${key} in mailcow.conf"
sed -i '/\bSkip Solr on low-memory\b/d' mailcow.conf
sed -i '/\bSolr is disabled by default\b/d' mailcow.conf
sed -i '/\bDisable Solr or\b/d' mailcow.conf
sed -i '/\bSKIP_SOLR\b/d' mailcow.conf
;;
SOLR_PORT)
echo "Removing ${key} in mailcow.conf"
sed -i '/\bSOLR_PORT\b/d' mailcow.conf
;;
FLATCURVE_EXPERIMENTAL)
echo "Removing ${key} in mailcow.conf"
sed -i '/\bFLATCURVE_EXPERIMENTAL\b/d' mailcow.conf
;;
DISABLE_IPv6)
echo "Migrating ${key} to ENABLE_IPv6 in mailcow.conf"
local old=$(grep '^DISABLE_IPv6=' "mailcow.conf" | cut -d'=' -f2)
local new
if [[ "$old" == "y" ]]; then
new="false"
else
new="true"
fi
sed -i '/^DISABLE_IPv6=/d' "mailcow.conf"
echo "ENABLE_IPV6=$new" >> "mailcow.conf"
;;
ACME_CONTACT)
echo "Deleting obsoleted ${key} in mailcow.conf"
sed -i '/^# Lets Encrypt registration contact information/d' mailcow.conf
sed -i '/^# Optional: Leave empty for none/d' mailcow.conf
sed -i '/^# This value is only used on first order!/d' mailcow.conf
sed -i '/^# Setting it at a later point will require the following steps:/d' mailcow.conf
sed -i '/^# https:\/\/docs.mailcow.email\/troubleshooting\/debug-reset_tls\//d' mailcow.conf
sed -i '/^ACME_CONTACT=.*/d' mailcow.conf
sed -i '/^#ACME_CONTACT=.*/d' mailcow.conf
;;
esac
fi
done
solr_volume=$(docker volume ls -qf name=^${COMPOSE_PROJECT_NAME}_solr-vol-1)
if [[ -n $solr_volume ]]; then
echo -e "\e[34mSolr has been replaced within mailcow since 2025-01.\nThe volume $solr_volume is unused.\e[0m"
sleep 1
if [ ! "$FORCE" ]; then
read -r -p "Remove $solr_volume? [y/N] " response
if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo -e "\e[33mRemoving $solr_volume...\e[0m"
docker volume rm $solr_volume || echo -e "\e[31mFailed to remove. Remove it manually!\e[0m"
echo -e "\e[32mSuccessfully removed $solr_volume!\e[0m"
else
echo -e "Not removing $solr_volume. Run \`docker volume rm $solr_volume\` manually if needed."
fi
else
echo -e "\e[33mForce removing $solr_volume...\e[0m"
docker volume rm $solr_volume || echo -e "\e[31mFailed to remove. Remove it manually!\e[0m"
echo -e "\e[32mSuccessfully removed $solr_volume!\e[0m"
fi
fi
# Delete old fts.conf before forced switch to flatcurve to ensure update is working properly
FTS_CONF_PATH="${SCRIPT_DIR}/data/conf/dovecot/conf.d/fts.conf"
if [[ -f "$FTS_CONF_PATH" ]]; then
if grep -q "Autogenerated by mailcow" "$FTS_CONF_PATH"; then
rm -rf $FTS_CONF_PATH
fi
fi
}

View File

@@ -0,0 +1,300 @@
#!/usr/bin/env bash
# _modules/scripts/new_options.sh
# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY!
# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!!
adapt_new_options() {
CONFIG_ARRAY=(
"AUTODISCOVER_SAN"
"SKIP_LETS_ENCRYPT"
"SKIP_SOGO"
"USE_WATCHDOG"
"WATCHDOG_NOTIFY_EMAIL"
"WATCHDOG_NOTIFY_WEBHOOK"
"WATCHDOG_NOTIFY_WEBHOOK_BODY"
"WATCHDOG_NOTIFY_BAN"
"WATCHDOG_NOTIFY_START"
"WATCHDOG_EXTERNAL_CHECKS"
"WATCHDOG_SUBJECT"
"SKIP_CLAMD"
"SKIP_OLEFY"
"SKIP_IP_CHECK"
"ADDITIONAL_SAN"
"DOVEADM_PORT"
"IPV4_NETWORK"
"IPV6_NETWORK"
"LOG_LINES"
"SNAT_TO_SOURCE"
"SNAT6_TO_SOURCE"
"COMPOSE_PROJECT_NAME"
"DOCKER_COMPOSE_VERSION"
"SQL_PORT"
"API_KEY"
"API_KEY_READ_ONLY"
"API_ALLOW_FROM"
"MAILDIR_GC_TIME"
"MAILDIR_SUB"
"ACL_ANYONE"
"FTS_HEAP"
"FTS_PROCS"
"SKIP_FTS"
"ENABLE_SSL_SNI"
"ALLOW_ADMIN_EMAIL_LOGIN"
"SKIP_HTTP_VERIFICATION"
"SOGO_EXPIRE_SESSION"
"SOGO_URL_ENCRYPTION_KEY"
"REDIS_PORT"
"REDISPASS"
"DOVECOT_MASTER_USER"
"DOVECOT_MASTER_PASS"
"MAILCOW_PASS_SCHEME"
"ADDITIONAL_SERVER_NAMES"
"WATCHDOG_VERBOSE"
"WEBAUTHN_ONLY_TRUSTED_VENDORS"
"SPAMHAUS_DQS_KEY"
"SKIP_UNBOUND_HEALTHCHECK"
"DISABLE_NETFILTER_ISOLATION_RULE"
"HTTP_REDIRECT"
"ENABLE_IPV6"
)
sed -i --follow-symlinks '$a\' mailcow.conf
for option in ${CONFIG_ARRAY[@]}; do
if grep -q "${option}" mailcow.conf; then
continue
fi
echo "Adding new option \"${option}\" to mailcow.conf"
case "${option}" in
AUTODISCOVER_SAN)
echo '# Obtain certificates for autodiscover.* and autoconfig.* domains.' >> mailcow.conf
echo '# This can be useful to switch off in case you are in a scenario where a reverse proxy already handles those.' >> mailcow.conf
echo '# There are mixed scenarios where ports 80,443 are occupied and you do not want to share certs' >> mailcow.conf
echo '# between services. So acme-mailcow obtains for maildomains and all web-things get handled' >> mailcow.conf
echo '# in the reverse proxy.' >> mailcow.conf
echo 'AUTODISCOVER_SAN=y' >> mailcow.conf
;;
DOCKER_COMPOSE_VERSION)
echo "# Used Docker Compose version" >> mailcow.conf
echo "# Switch here between native (compose plugin) and standalone" >> mailcow.conf
echo "# For more informations take a look at the mailcow docs regarding the configuration options." >> mailcow.conf
echo "# Normally this should be untouched but if you decided to use either of those you can switch it manually here." >> mailcow.conf
echo "# Please be aware that at least one of those variants should be installed on your machine or mailcow will fail." >> mailcow.conf
echo "" >> mailcow.conf
echo "DOCKER_COMPOSE_VERSION=${DOCKER_COMPOSE_VERSION}" >> mailcow.conf
;;
DOVEADM_PORT)
echo "DOVEADM_PORT=127.0.0.1:19991" >> mailcow.conf
;;
LOG_LINES)
echo '# Max log lines per service to keep in Redis logs' >> mailcow.conf
echo "LOG_LINES=9999" >> mailcow.conf
;;
IPV4_NETWORK)
echo '# Internal IPv4 /24 subnet, format n.n.n. (expands to n.n.n.0/24)' >> mailcow.conf
echo "IPV4_NETWORK=172.22.1" >> mailcow.conf
;;
IPV6_NETWORK)
echo '# Internal IPv6 subnet in fc00::/7' >> mailcow.conf
echo "IPV6_NETWORK=fd4d:6169:6c63:6f77::/64" >> mailcow.conf
;;
SQL_PORT)
echo '# Bind SQL to 127.0.0.1 on port 13306' >> mailcow.conf
echo "SQL_PORT=127.0.0.1:13306" >> mailcow.conf
;;
API_KEY)
echo '# Create or override API key for web UI' >> mailcow.conf
echo "#API_KEY=" >> mailcow.conf
;;
API_KEY_READ_ONLY)
echo '# Create or override read-only API key for web UI' >> mailcow.conf
echo "#API_KEY_READ_ONLY=" >> mailcow.conf
;;
API_ALLOW_FROM)
echo '# Must be set for API_KEY to be active' >> mailcow.conf
echo '# IPs only, no networks (networks can be set via UI)' >> mailcow.conf
echo "#API_ALLOW_FROM=" >> mailcow.conf
;;
SNAT_TO_SOURCE)
echo '# Use this IPv4 for outgoing connections (SNAT)' >> mailcow.conf
echo "#SNAT_TO_SOURCE=" >> mailcow.conf
;;
SNAT6_TO_SOURCE)
echo '# Use this IPv6 for outgoing connections (SNAT)' >> mailcow.conf
echo "#SNAT6_TO_SOURCE=" >> mailcow.conf
;;
MAILDIR_GC_TIME)
echo '# Garbage collector cleanup' >> mailcow.conf
echo '# Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring' >> mailcow.conf
echo '# How long should objects remain in the garbage until they are being deleted? (value in minutes)' >> mailcow.conf
echo '# Check interval is hourly' >> mailcow.conf
echo 'MAILDIR_GC_TIME=1440' >> mailcow.conf
;;
ACL_ANYONE)
echo '# Set this to "allow" to enable the anyone pseudo user. Disabled by default.' >> mailcow.conf
echo '# When enabled, ACL can be created, that apply to "All authenticated users"' >> mailcow.conf
echo '# This should probably only be activated on mail hosts, that are used exclusively by one organisation.' >> mailcow.conf
echo '# Otherwise a user might share data with too many other users.' >> mailcow.conf
echo 'ACL_ANYONE=disallow' >> mailcow.conf
;;
FTS_HEAP)
echo '# Dovecot Indexing (FTS) Process maximum heap size in MB, there is no recommendation, please see Dovecot docs.' >> mailcow.conf
echo '# Flatcurve is used as FTS Engine. It is supposed to be pretty efficient in CPU and RAM consumption.' >> mailcow.conf
echo '# Please always monitor your Resource consumption!' >> mailcow.conf
echo "FTS_HEAP=128" >> mailcow.conf
;;
SKIP_FTS)
echo '# Skip FTS (Fulltext Search) for Dovecot on low-memory, low-threaded systems or if you simply want to disable it.' >> mailcow.conf
echo "# Dovecot inside mailcow use Flatcurve as FTS Backend." >> mailcow.conf
echo "SKIP_FTS=y" >> mailcow.conf
;;
FTS_PROCS)
echo '# Controls how many processes the Dovecot indexing process can spawn at max.' >> mailcow.conf
echo '# Too many indexing processes can use a lot of CPU and Disk I/O' >> mailcow.conf
echo '# Please visit: https://doc.dovecot.org/configuration_manual/service_configuration/#indexer-worker for more informations' >> mailcow.conf
echo "FTS_PROCS=1" >> mailcow.conf
;;
ENABLE_SSL_SNI)
echo '# Create seperate certificates for all domains - y/n' >> mailcow.conf
echo '# this will allow adding more than 100 domains, but some email clients will not be able to connect with alternative hostnames' >> mailcow.conf
echo '# see https://wiki.dovecot.org/SSL/SNIClientSupport' >> mailcow.conf
echo "ENABLE_SSL_SNI=n" >> mailcow.conf
;;
SKIP_SOGO)
echo '# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n' >> mailcow.conf
echo "SKIP_SOGO=n" >> mailcow.conf
;;
MAILDIR_SUB)
echo '# MAILDIR_SUB defines a path in a users virtual home to keep the maildir in. Leave empty for updated setups.' >> mailcow.conf
echo "#MAILDIR_SUB=Maildir" >> mailcow.conf
echo "MAILDIR_SUB=" >> mailcow.conf
;;
WATCHDOG_NOTIFY_WEBHOOK)
echo '# Send notifications to a webhook URL that receives a POST request with the content type "application/json".' >> mailcow.conf
echo '# You can use this to send notifications to services like Discord, Slack and others.' >> mailcow.conf
echo '#WATCHDOG_NOTIFY_WEBHOOK=https://discord.com/api/webhooks/XXXXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' >> mailcow.conf
;;
WATCHDOG_NOTIFY_WEBHOOK_BODY)
echo '# JSON body included in the webhook POST request. Needs to be in single quotes.' >> mailcow.conf
echo '# Following variables are available: SUBJECT, BODY' >> mailcow.conf
WEBHOOK_BODY='{"username": "mailcow Watchdog", "content": "**${SUBJECT}**\n${BODY}"}'
echo "#WATCHDOG_NOTIFY_WEBHOOK_BODY='${WEBHOOK_BODY}'" >> mailcow.conf
;;
WATCHDOG_NOTIFY_BAN)
echo '# Notify about banned IP. Includes whois lookup.' >> mailcow.conf
echo "WATCHDOG_NOTIFY_BAN=y" >> mailcow.conf
;;
WATCHDOG_NOTIFY_START)
echo '# Send a notification when the watchdog is started.' >> mailcow.conf
echo "WATCHDOG_NOTIFY_START=y" >> mailcow.conf
;;
WATCHDOG_SUBJECT)
echo '# Subject for watchdog mails. Defaults to "Watchdog ALERT" followed by the error message.' >> mailcow.conf
echo "#WATCHDOG_SUBJECT=" >> mailcow.conf
;;
WATCHDOG_EXTERNAL_CHECKS)
echo '# Checks if mailcow is an open relay. Requires a SAL. More checks will follow.' >> mailcow.conf
echo '# No data is collected. Opt-in and anonymous.' >> mailcow.conf
echo '# Will only work with unmodified mailcow setups.' >> mailcow.conf
echo "WATCHDOG_EXTERNAL_CHECKS=n" >> mailcow.conf
;;
SOGO_EXPIRE_SESSION)
echo '# SOGo session timeout in minutes' >> mailcow.conf
echo "SOGO_EXPIRE_SESSION=480" >> mailcow.conf
;;
REDIS_PORT)
echo "REDIS_PORT=127.0.0.1:7654" >> mailcow.conf
;;
DOVECOT_MASTER_USER)
echo '# DOVECOT_MASTER_USER and _PASS must _both_ be provided. No special chars.' >> mailcow.conf
echo '# Empty by default to auto-generate master user and password on start.' >> mailcow.conf
echo '# User expands to DOVECOT_MASTER_USER@mailcow.local' >> mailcow.conf
echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf
echo "DOVECOT_MASTER_USER=" >> mailcow.conf
;;
DOVECOT_MASTER_PASS)
echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf
echo "DOVECOT_MASTER_PASS=" >> mailcow.conf
;;
MAILCOW_PASS_SCHEME)
echo '# Password hash algorithm' >> mailcow.conf
echo '# Only certain password hash algorithm are supported. For a fully list of supported schemes,' >> mailcow.conf
echo '# see https://docs.mailcow.email/models/model-passwd/' >> mailcow.conf
echo "MAILCOW_PASS_SCHEME=BLF-CRYPT" >> mailcow.conf
;;
ADDITIONAL_SERVER_NAMES)
echo '# Additional server names for mailcow UI' >> mailcow.conf
echo '#' >> mailcow.conf
echo '# Specify alternative addresses for the mailcow UI to respond to' >> mailcow.conf
echo '# This is useful when you set mail.* as ADDITIONAL_SAN and want to make sure mail.maildomain.com will always point to the mailcow UI.' >> mailcow.conf
echo '# If the server name does not match a known site, Nginx decides by best-guess and may redirect users to the wrong web root.' >> mailcow.conf
echo '# You can understand this as server_name directive in Nginx.' >> mailcow.conf
echo '# Comma separated list without spaces! Example: ADDITIONAL_SERVER_NAMES=a.b.c,d.e.f' >> mailcow.conf
echo 'ADDITIONAL_SERVER_NAMES=' >> mailcow.conf
;;
WEBAUTHN_ONLY_TRUSTED_VENDORS)
echo "# WebAuthn device manufacturer verification" >> mailcow.conf
echo '# After setting WEBAUTHN_ONLY_TRUSTED_VENDORS=y only devices from trusted manufacturers are allowed' >> mailcow.conf
echo '# root certificates can be placed for validation under mailcow-dockerized/data/web/inc/lib/WebAuthn/rootCertificates' >> mailcow.conf
echo 'WEBAUTHN_ONLY_TRUSTED_VENDORS=n' >> mailcow.conf
;;
SPAMHAUS_DQS_KEY)
echo "# Spamhaus Data Query Service Key" >> mailcow.conf
echo '# Optional: Leave empty for none' >> mailcow.conf
echo '# Enter your key here if you are using a blocked ASN (OVH, AWS, Cloudflare e.g) for the unregistered Spamhaus Blocklist.' >> mailcow.conf
echo '# If empty, it will completely disable Spamhaus blocklists if it detects that you are running on a server using a blocked AS.' >> mailcow.conf
echo '# Otherwise it will work as usual.' >> mailcow.conf
echo 'SPAMHAUS_DQS_KEY=' >> mailcow.conf
;;
WATCHDOG_VERBOSE)
echo '# Enable watchdog verbose logging' >> mailcow.conf
echo 'WATCHDOG_VERBOSE=n' >> mailcow.conf
;;
SKIP_UNBOUND_HEALTHCHECK)
echo '# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) - y/n' >> mailcow.conf
echo 'SKIP_UNBOUND_HEALTHCHECK=n' >> mailcow.conf
;;
DISABLE_NETFILTER_ISOLATION_RULE)
echo '# Prevent netfilter from setting an iptables/nftables rule to isolate the mailcow docker network - y/n' >> mailcow.conf
echo '# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost' >> mailcow.conf
echo 'DISABLE_NETFILTER_ISOLATION_RULE=n' >> mailcow.conf
;;
HTTP_REDIRECT)
echo '# Redirect HTTP connections to HTTPS - y/n' >> mailcow.conf
echo 'HTTP_REDIRECT=n' >> mailcow.conf
;;
ENABLE_IPV6)
echo '# IPv6 Controller Section' >> mailcow.conf
echo '# This variable controls the usage of IPv6 within mailcow.' >> mailcow.conf
echo '# Can either be true or false | Defaults to true' >> mailcow.conf
echo '# WARNING: MAKE SURE TO PROPERLY CONFIGURE IPv6 ON YOUR HOST FIRST BEFORE ENABLING THIS AS FAULTY CONFIGURATIONS CAN LEAD TO OPEN RELAYS!' >> mailcow.conf
echo '# A COMPLETE DOCKER STACK REBUILD (compose down && compose up -d) IS NEEDED TO APPLY THIS.' >> mailcow.conf
echo ENABLE_IPV6=${IPV6_BOOL} >> mailcow.conf
;;
SKIP_CLAMD)
echo '# Skip ClamAV (clamd-mailcow) anti-virus (Rspamd will auto-detect a missing ClamAV container) - y/n' >> mailcow.conf
echo 'SKIP_CLAMD=n' >> mailcow.conf
;;
SKIP_OLEFY)
echo '# Skip Olefy (olefy-mailcow) anti-virus for Office documents (Rspamd will auto-detect a missing Olefy container) - y/n' >> mailcow.conf
echo 'SKIP_OLEFY=n' >> mailcow.conf
;;
REDISPASS)
echo "REDISPASS=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 28)" >> mailcow.conf
;;
SOGO_URL_ENCRYPTION_KEY)
echo '# SOGo URL encryption key (exactly 16 characters, limited to AZ, az, 09)' >> mailcow.conf
echo '# This key is used to encrypt email addresses within SOGo URLs' >> mailcow.conf
echo "SOGO_URL_ENCRYPTION_KEY=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 16)" >> mailcow.conf
;;
*)
echo "${option}=" >> mailcow.conf
;;
esac
done
}

View File

@@ -3,14 +3,14 @@ set -o pipefail
exec 5>&1 exec 5>&1
# Do not attempt to write to slave # Do not attempt to write to slave
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" export VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
else else
export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" export VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning"
fi fi
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..." echo "Waiting for Valkey..."
sleep 2 sleep 2
done done
@@ -159,18 +159,6 @@ while true; do
fi fi
if [[ ! -f ${ACME_BASE}/acme/account.pem ]]; then if [[ ! -f ${ACME_BASE}/acme/account.pem ]]; then
log_f "Generating missing Lets Encrypt account key..." log_f "Generating missing Lets Encrypt account key..."
if [[ ! -z ${ACME_CONTACT} ]]; then
if ! verify_email "${ACME_CONTACT}"; then
log_f "Invalid email address, will not start registration!"
sleep 365d
exec $(readlink -f "$0")
else
ACME_CONTACT_PARAMETER="--contact mailto:${ACME_CONTACT}"
log_f "Valid email address, using ${ACME_CONTACT} for registration"
fi
else
ACME_CONTACT_PARAMETER=""
fi
openssl genrsa 4096 > ${ACME_BASE}/acme/account.pem openssl genrsa 4096 > ${ACME_BASE}/acme/account.pem
else else
log_f "Using existing Lets Encrypt account key ${ACME_BASE}/acme/account.pem" log_f "Using existing Lets Encrypt account key ${ACME_BASE}/acme/account.pem"
@@ -218,7 +206,7 @@ while true; do
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
# Fetch certs for autoconfig and autodiscover subdomains # Fetch certs for autoconfig and autodiscover subdomains
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig') ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig' 'mta-sts')
fi fi
if [[ ${SKIP_IP_CHECK} != "y" ]]; then if [[ ${SKIP_IP_CHECK} != "y" ]]; then
@@ -299,7 +287,7 @@ while true; do
VALIDATED_CERTIFICATES+=("${CERT_NAME}") VALIDATED_CERTIFICATES+=("${CERT_NAME}")
# obtain server certificate if required # obtain server certificate if required
ACME_CONTACT_PARAMETER=${ACME_CONTACT_PARAMETER} DOMAINS=${SERVER_SAN_VALIDATED[@]} /srv/obtain-certificate.sh rsa DOMAINS=${SERVER_SAN_VALIDATED[@]} /srv/obtain-certificate.sh rsa
RETURN="$?" RETURN="$?"
if [[ "$RETURN" == "0" ]]; then # 0 = cert created successfully if [[ "$RETURN" == "0" ]]; then # 0 = cert created successfully
CERT_AMOUNT_CHANGED=1 CERT_AMOUNT_CHANGED=1
@@ -360,7 +348,7 @@ while true; do
if [[ -z ${VALIDATED_CERTIFICATES[*]} ]]; then if [[ -z ${VALIDATED_CERTIFICATES[*]} ]]; then
log_f "Cannot validate any hostnames, skipping Let's Encrypt for 1 hour." log_f "Cannot validate any hostnames, skipping Let's Encrypt for 1 hour."
log_f "Use SKIP_LETS_ENCRYPT=y in mailcow.conf to skip it permanently." log_f "Use SKIP_LETS_ENCRYPT=y in mailcow.conf to skip it permanently."
${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)" ${VALKEY_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
sleep 1h sleep 1h
exec $(readlink -f "$0") exec $(readlink -f "$0")
fi fi
@@ -401,7 +389,7 @@ while true; do
DOVECOT_CERT_SERIAL_NEW="$(echo | openssl s_client -connect dovecot:143 -starttls imap 2>/dev/null | openssl x509 -inform pem -noout -serial | cut -d "=" -f 2)" DOVECOT_CERT_SERIAL_NEW="$(echo | openssl s_client -connect dovecot:143 -starttls imap 2>/dev/null | openssl x509 -inform pem -noout -serial | cut -d "=" -f 2)"
if [[ ${RELOAD_LOOP_C} -gt 3 ]]; then if [[ ${RELOAD_LOOP_C} -gt 3 ]]; then
log_f "Some services do return old end dates, something went wrong!" log_f "Some services do return old end dates, something went wrong!"
${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)" ${VALKEY_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
break; break;
fi fi
done done
@@ -422,7 +410,7 @@ while true; do
;; ;;
*) # non-zero *) # non-zero
log_f "Some errors occurred, retrying in 30 minutes..." log_f "Some errors occurred, retrying in 30 minutes..."
${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)" ${VALKEY_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
sleep 30m sleep 30m
exec $(readlink -f "$0") exec $(readlink -f "$0")
;; ;;

View File

@@ -5,13 +5,13 @@ log_f() {
echo -n "$(date) - ${1}" echo -n "$(date) - ${1}"
elif [[ ${2} == "no_date" ]]; then elif [[ ${2} == "no_date" ]]; then
echo "${1}" echo "${1}"
elif [[ ${2} != "redis_only" ]]; then elif [[ ${2} != "valkey_only" ]]; then
echo "$(date) - ${1}" echo "$(date) - ${1}"
fi fi
if [[ ${3} == "b64" ]]; then if [[ ${3} == "b64" ]]; then
${REDIS_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"base64,$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}")\"}" > /dev/null ${VALKEY_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"base64,$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}")\"}" > /dev/null
else else
${REDIS_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}" | \ ${VALKEY_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}" | \
tr '%&;$"[]{}-\r\n' ' ')\"}" > /dev/null tr '%&;$"[]{}-\r\n' ' ')\"}" > /dev/null
fi fi
} }

View File

@@ -93,15 +93,15 @@ until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do
sleep 2 sleep 2
done done
log_f "Resolver OK" log_f "Resolver OK"
log_f "Using command acme-tiny ${DIRECTORY_URL} ${ACME_CONTACT_PARAMETER} --account-key ${ACME_BASE}/acme/account.pem --disable-check --csr ${CSR} --acme-dir /var/www/acme/" log_f "Using command acme-tiny ${DIRECTORY_URL} --account-key ${ACME_BASE}/acme/account.pem --disable-check --csr ${CSR} --acme-dir /var/www/acme/"
ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} ${ACME_CONTACT_PARAMETER} \ ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} \
--account-key ${ACME_BASE}/acme/account.pem \ --account-key ${ACME_BASE}/acme/account.pem \
--disable-check \ --disable-check \
--csr ${CSR} \ --csr ${CSR} \
--acme-dir /var/www/acme/ 2>&1 > /tmp/_cert.pem | tee /dev/fd/5; exit ${PIPESTATUS[0]}) --acme-dir /var/www/acme/ 2>&1 > /tmp/_cert.pem | tee /dev/fd/5; exit ${PIPESTATUS[0]})
SUCCESS="$?" SUCCESS="$?"
ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64) ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
log_f "${ACME_RESPONSE_B64}" redis_only b64 log_f "${ACME_RESPONSE_B64}" valkey_only b64
case "$SUCCESS" in case "$SUCCESS" in
0) # cert requested 0) # cert requested
log_f "Deploying certificate ${CERT}..." log_f "Deploying certificate ${CERT}..."
@@ -124,7 +124,7 @@ case "$SUCCESS" in
;; ;;
*) # non-zero is non-fun *) # non-zero is non-fun
log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}'" log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}'"
redis-cli -h redis -a ${REDISPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)" redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)"
exit 100${SUCCESS} exit 100${SUCCESS}
;; ;;
esac esac

View File

@@ -8,7 +8,7 @@ fi
# Cleaning up garbage # Cleaning up garbage
echo "Cleaning up tmp files..." echo "Cleaning up tmp files..."
rm -rf /var/lib/clamav/clamav-*.tmp rm -rf /var/lib/clamav/tmp.*
# Prepare whitelist # Prepare whitelist

View File

@@ -32,21 +32,21 @@ async def lifespan(app: FastAPI):
logger.info("Init APP") logger.info("Init APP")
# Init redis client # Init valkey client
if os.environ['REDIS_SLAVEOF_IP'] != "": if os.environ['VALKEY_SLAVEOF_IP'] != "":
redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0", password=os.environ['REDISPASS']) valkey_client = valkey = await aioredis.from_url(f"redis://{os.environ['VALKEY_SLAVEOF_IP']}:{os.environ['VALKEY_SLAVEOF_PORT']}/0", password=os.environ['VALKEYPASS'])
else: else:
redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0", password=os.environ['REDISPASS']) valkey_client = valkey = await aioredis.from_url("redis://valkey-mailcow:6379/0", password=os.environ['VALKEYPASS'])
# Init docker clients # Init docker clients
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto') 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') async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger) dockerapi = DockerApi(valkey_client, sync_docker_client, async_docker_client, logger)
logger.info("Subscribe to redis channel") logger.info("Subscribe to valkey channel")
# Subscribe to redis channel # Subscribe to valkey channel
dockerapi.pubsub = redis.pubsub() dockerapi.pubsub = valkey.pubsub()
await dockerapi.pubsub.subscribe("MC_CHANNEL") await dockerapi.pubsub.subscribe("MC_CHANNEL")
asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub)) asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
@@ -57,9 +57,9 @@ async def lifespan(app: FastAPI):
dockerapi.sync_docker_client.close() dockerapi.sync_docker_client.close()
await dockerapi.async_docker_client.close() await dockerapi.async_docker_client.close()
# Close redis # Close valkey
await dockerapi.pubsub.unsubscribe("MC_CHANNEL") await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
await dockerapi.redis_client.close() await dockerapi.valkey_client.close()
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
@@ -73,11 +73,11 @@ async def get_host_update_stats():
dockerapi.host_stats_isUpdating = True dockerapi.host_stats_isUpdating = True
while True: while True:
if await dockerapi.redis_client.exists('host_stats'): if await dockerapi.valkey_client.exists('host_stats'):
break break
await asyncio.sleep(1.5) await asyncio.sleep(1.5)
stats = json.loads(await dockerapi.redis_client.get('host_stats')) stats = json.loads(await dockerapi.valkey_client.get('host_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json") return Response(content=json.dumps(stats, indent=4), media_type="application/json")
@app.get("/containers/{container_id}/json") @app.get("/containers/{container_id}/json")
@@ -185,11 +185,11 @@ async def post_container_update_stats(container_id : str):
dockerapi.containerIds_to_update.append(container_id) dockerapi.containerIds_to_update.append(container_id)
while True: while True:
if await dockerapi.redis_client.exists(container_id + '_stats'): if await dockerapi.valkey_client.exists(container_id + '_stats'):
break break
await asyncio.sleep(1.5) await asyncio.sleep(1.5)
stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats')) stats = json.loads(await dockerapi.valkey_client.get(container_id + '_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json") return Response(content=json.dumps(stats, indent=4), media_type="application/json")

View File

@@ -10,8 +10,8 @@ from datetime import datetime
from fastapi import FastAPI, Response, Request from fastapi import FastAPI, Response, Request
class DockerApi: class DockerApi:
def __init__(self, redis_client, sync_docker_client, async_docker_client, logger): def __init__(self, valkey_client, sync_docker_client, async_docker_client, logger):
self.redis_client = redis_client self.valkey_client = valkey_client
self.sync_docker_client = sync_docker_client self.sync_docker_client = sync_docker_client
self.async_docker_client = async_docker_client self.async_docker_client = async_docker_client
self.logger = logger self.logger = logger
@@ -533,7 +533,7 @@ class DockerApi:
"architecture": platform.machine() "architecture": platform.machine()
} }
await self.redis_client.set('host_stats', json.dumps(host_stats), ex=10) await self.valkey_client.set('host_stats', json.dumps(host_stats), ex=10)
except Exception as e: except Exception as e:
res = { res = {
"type": "danger", "type": "danger",
@@ -550,14 +550,14 @@ class DockerApi:
if container._id == container_id: if container._id == container_id:
res = await container.stats(stream=False) res = await container.stats(stream=False)
if await self.redis_client.exists(container_id + '_stats'): if await self.valkey_client.exists(container_id + '_stats'):
stats = json.loads(await self.redis_client.get(container_id + '_stats')) stats = json.loads(await self.valkey_client.get(container_id + '_stats'))
else: else:
stats = [] stats = []
stats.append(res[0]) stats.append(res[0])
if len(stats) > 3: if len(stats) > 3:
del stats[0] del stats[0]
await self.redis_client.set(container_id + '_stats', json.dumps(stats), ex=60) await self.valkey_client.set(container_id + '_stats', json.dumps(stats), ex=60)
except Exception as e: except Exception as e:
res = { res = {
"type": "danger", "type": "danger",

View File

@@ -3,7 +3,7 @@ FROM alpine:3.21
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$ # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.16 ARG GOSU_VERSION=1.17
ENV LANG=C.UTF-8 ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8 ENV LC_ALL=C.UTF-8
@@ -118,7 +118,7 @@ RUN addgroup -g 5000 vmail \
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
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf COPY syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf
COPY imapsync /usr/local/bin/imapsync COPY imapsync /usr/local/bin/imapsync
COPY imapsync_runner.pl /usr/local/bin/imapsync_runner.pl COPY imapsync_runner.pl /usr/local/bin/imapsync_runner.pl
COPY report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve COPY report-spam.sieve /usr/lib/dovecot/sieve/report-spam.sieve

View File

@@ -2,7 +2,7 @@
source /source_env.sh source /source_env.sh
MAX_AGE=$(redis-cli --raw -h redis-mailcow -a ${REDISPASS} --no-auth-warning GET Q_MAX_AGE) MAX_AGE=$(redis-cli --raw -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET Q_MAX_AGE)
if [[ -z ${MAX_AGE} ]]; then if [[ -z ${MAX_AGE} ]]; then
echo "Max age for quarantine items not defined" echo "Max age for quarantine items not defined"

View File

@@ -13,18 +13,18 @@ until dig +short mailcow.email > /dev/null; do
done done
# Do not attempt to write to slave # Do not attempt to write to slave
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
else else
REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning"
fi fi
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..." echo "Waiting for Valkey..."
sleep 2 sleep 2
done done
${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null ${VALKEY_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/
@@ -341,8 +341,8 @@ done
# 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/auth/passwd-verify.lua touch /etc/dovecot/auth/passwd-verify.lua
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf
fi fi
exec "$@" exec "$@"

View File

@@ -132,8 +132,8 @@ while ($row = $sth->fetchrow_arrayref()) {
"--tmpdir", "/tmp", "--tmpdir", "/tmp",
"--nofoldersizes", "--nofoldersizes",
"--addheader", "--addheader",
($timeout1 gt "0" ? () : ('--timeout1', $timeout1)), ($timeout1 le "0" ? () : ('--timeout1', $timeout1)),
($timeout2 gt "0" ? () : ('--timeout2', $timeout2)), ($timeout2 le "0" ? () : ('--timeout2', $timeout2)),
($exclude eq "" ? () : ("--exclude", $exclude)), ($exclude eq "" ? () : ("--exclude", $exclude)),
($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)),
($maxage eq "0" ? () : ('--maxage', $maxage)), ($maxage eq "0" ? () : ('--maxage', $maxage)),

View File

@@ -8,7 +8,8 @@ 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 jinja2 import jinja2
from jinja2 import Template from jinja2 import TemplateError
from jinja2.sandbox import SandboxedEnvironment
import json import json
import redis import redis
import time import time
@@ -31,7 +32,7 @@ try:
while True: while True:
try: try:
r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS']) r = redis.StrictRedis(host='valkey-mailcow', decode_responses=True, port=6379, db=0, password=os.environ['VALKEYPASS'])
r.ping() r.ping()
except Exception as ex: except Exception as ex:
print('%s - trying again...' % (ex)) print('%s - trying again...' % (ex))
@@ -75,22 +76,27 @@ try:
def notify_rcpt(rcpt, msg_count, quarantine_acl, category): def notify_rcpt(rcpt, msg_count, quarantine_acl, category):
if category == "add_header": category = "add header" if category == "add_header": category = "add header"
meta_query = query_mysql('SELECT SHA2(CONCAT(id, qid), 256) AS qhash, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category)) meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category))
print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count)) print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count))
if len(meta_query) == 0: if len(meta_query) == 0:
return return
msg_count = len(meta_query) msg_count = len(meta_query)
env = SandboxedEnvironment()
if r.get('Q_HTML'): if r.get('Q_HTML'):
try: try:
template = Template(r.get('Q_HTML')) template = env.from_string(r.get('Q_HTML'))
except: except Exception:
print("Error: Cannot parse quarantine template, falling back to default template.") print("Error: Cannot parse quarantine template, falling back to default template.")
with open('/templates/quarantine.tpl') as file_: with open('/templates/quarantine.tpl') as file_:
template = Template(file_.read()) template = env.from_string(file_.read())
else: else:
with open('/templates/quarantine.tpl') as file_: with open('/templates/quarantine.tpl') as file_:
template = Template(file_.read()) template = env.from_string(file_.read())
html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl) try:
html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl)
except (jinja2.exceptions.SecurityError, TemplateError) as ex:
print(f"SecurityError or TemplateError in template rendering: {ex}")
return
text = html2text.html2text(html) text = html2text.html2text(html)
count = 0 count = 0
while count < 15: while count < 15:

View File

@@ -6,7 +6,7 @@ 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 jinja2 import jinja2
from jinja2 import Template from jinja2.sandbox import SandboxedEnvironment
import redis import redis
import time import time
import json import json
@@ -23,7 +23,7 @@ else:
while True: while True:
try: try:
r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0, username='quota_notify', password='') r = redis.StrictRedis(host='valkey-mailcow', decode_responses=True, port=6379, db=0, username='quota_notify', password='')
r.ping() r.ping()
except Exception as ex: except Exception as ex:
print('%s - trying again...' % (ex)) print('%s - trying again...' % (ex))
@@ -33,16 +33,24 @@ while True:
if r.get('QW_HTML'): if r.get('QW_HTML'):
try: try:
template = Template(r.get('QW_HTML')) env = SandboxedEnvironment()
except: template = env.from_string(r.get('QW_HTML'))
print("Error: Cannot parse quarantine template, falling back to default template.") except Exception:
print("Error: Cannot parse quota template, falling back to default template.")
with open('/templates/quota.tpl') as file_: with open('/templates/quota.tpl') as file_:
template = Template(file_.read()) env = SandboxedEnvironment()
template = env.from_string(file_.read())
else: else:
with open('/templates/quota.tpl') as file_: with open('/templates/quota.tpl') as file_:
template = Template(file_.read()) env = SandboxedEnvironment()
template = env.from_string(file_.read())
try:
html = template.render(username=username, percent=percent)
except (jinja2.exceptions.SecurityError, jinja2.TemplateError) as ex:
print(f"SecurityError or TemplateError in template rendering: {ex}")
sys.exit(1)
html = template.render(username=username, percent=percent)
text = html2text.html2text(html) text = html2text.html2text(html)
try: try:

View File

@@ -3,16 +3,16 @@
source /source_env.sh source /source_env.sh
# Do not attempt to write to slave # Do not attempt to write to slave
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
else else
REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning"
fi fi
# Is replication active? # Is replication active?
# grep on file is less expensive than doveconf # grep on file is less expensive than doveconf
if [ -n ${MAILCOW_REPLICA_IP} ]; then if [ -n ${MAILCOW_REPLICA_IP} ]; then
${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null ${VALKEY_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
exit exit
fi fi
@@ -22,7 +22,7 @@ FAILED_SYNCS=$(doveadm replicator status | grep "Waiting 'failed' requests" | gr
# 1 failed job for mailcow.local is expected and healthy # 1 failed job for mailcow.local is expected and healthy
if [[ "${FAILED_SYNCS}" != 0 ]] && [[ "${FAILED_SYNCS}" != 1 ]]; then if [[ "${FAILED_SYNCS}" != 0 ]] && [[ "${FAILED_SYNCS}" != 1 ]]; then
printf "Dovecot replicator has %d failed jobs\n" "${FAILED_SYNCS}" printf "Dovecot replicator has %d failed jobs\n" "${FAILED_SYNCS}"
${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH "${FAILED_SYNCS}" > /dev/null ${VALKEY_CMDLINE} SET DOVECOT_REPL_HEALTH "${FAILED_SYNCS}" > /dev/null
else else
${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null ${VALKEY_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
fi fi

View File

@@ -15,21 +15,21 @@ source s_dgram {
internal(); internal();
}; };
destination d_stdout { pipe("/dev/stdout"); }; destination d_stdout { pipe("/dev/stdout"); };
destination d_redis_ui_log { destination d_valkey_ui_log {
redis( redis(
host("`REDIS_SLAVEOF_IP`") host("`VALKEY_SLAVEOF_IP`")
persist-name("redis1") persist-name("valkey1")
port(`REDIS_SLAVEOF_PORT`) port(`VALKEY_SLAVEOF_PORT`)
auth("`REDISPASS`") auth("`VALKEYPASS`")
command("LPUSH" "DOVECOT_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") command("LPUSH" "DOVECOT_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
); );
}; };
destination d_redis_f2b_channel { destination d_valkey_f2b_channel {
redis( redis(
host("`REDIS_SLAVEOF_IP`") host("`VALKEY_SLAVEOF_IP`")
persist-name("redis2") persist-name("valkey2")
port(`REDIS_SLAVEOF_PORT`) port(`VALKEY_SLAVEOF_PORT`)
auth("`REDISPASS`") auth("`VALKEYPASS`")
command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
); );
}; };
@@ -48,6 +48,6 @@ log {
filter(f_replica); filter(f_replica);
destination(d_stdout); destination(d_stdout);
filter(f_mail); filter(f_mail);
destination(d_redis_ui_log); destination(d_valkey_ui_log);
destination(d_redis_f2b_channel); destination(d_valkey_f2b_channel);
}; };

View File

@@ -15,21 +15,21 @@ source s_dgram {
internal(); internal();
}; };
destination d_stdout { pipe("/dev/stdout"); }; destination d_stdout { pipe("/dev/stdout"); };
destination d_redis_ui_log { destination d_valkey_ui_log {
redis( redis(
host("redis-mailcow") host("valkey-mailcow")
persist-name("redis1") persist-name("valkey1")
port(6379) port(6379)
auth("`REDISPASS`") auth("`VALKEYPASS`")
command("LPUSH" "DOVECOT_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") command("LPUSH" "DOVECOT_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
); );
}; };
destination d_redis_f2b_channel { destination d_valkey_f2b_channel {
redis( redis(
host("redis-mailcow") host("valkey-mailcow")
persist-name("redis2") persist-name("valkey2")
port(6379) port(6379)
auth("`REDISPASS`") auth("`VALKEYPASS`")
command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
); );
}; };
@@ -48,6 +48,6 @@ log {
filter(f_replica); filter(f_replica);
destination(d_stdout); destination(d_stdout);
filter(f_mail); filter(f_mail);
destination(d_redis_ui_log); destination(d_valkey_ui_log);
destination(d_redis_f2b_channel); destination(d_valkey_f2b_channel);
}; };

View File

@@ -9,18 +9,17 @@ catch_non_zero() {
} }
source /source_env.sh source /source_env.sh
# Do not attempt to write to slave # Do not attempt to write to slave
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
else else
REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning"
fi fi
catch_non_zero "${REDIS_CMDLINE} LTRIM ACME_LOG 0 ${LOG_LINES}" catch_non_zero "${VALKEY_CMDLINE} LTRIM ACME_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM POSTFIX_MAILLOG 0 ${LOG_LINES}" catch_non_zero "${VALKEY_CMDLINE} LTRIM POSTFIX_MAILLOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM DOVECOT_MAILLOG 0 ${LOG_LINES}" catch_non_zero "${VALKEY_CMDLINE} LTRIM DOVECOT_MAILLOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM SOGO_LOG 0 ${LOG_LINES}" catch_non_zero "${VALKEY_CMDLINE} LTRIM SOGO_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM NETFILTER_LOG 0 ${LOG_LINES}" catch_non_zero "${VALKEY_CMDLINE} LTRIM NETFILTER_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM AUTODISCOVER_LOG 0 ${LOG_LINES}" catch_non_zero "${VALKEY_CMDLINE} LTRIM AUTODISCOVER_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM API_LOG 0 ${LOG_LINES}" catch_non_zero "${VALKEY_CMDLINE} LTRIM API_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM RL_LOG 0 ${LOG_LINES}" catch_non_zero "${VALKEY_CMDLINE} LTRIM RL_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM WATCHDOG_LOG 0 ${LOG_LINES}" catch_non_zero "${VALKEY_CMDLINE} LTRIM WATCHDOG_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM CRON_LOG 0 ${LOG_LINES}"

View File

@@ -1,6 +1,6 @@
#!/bin/sh #!/bin/sh
backend=iptables backend=nftables
nft list table ip filter &>/dev/null nft list table ip filter &>/dev/null
nftables_found=$? nftables_found=$?

View File

@@ -1,5 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
DEBUG = False
import re import re
import os import os
import sys import sys
@@ -20,10 +22,13 @@ from modules.Logger import Logger
from modules.IPTables import IPTables from modules.IPTables import IPTables
from modules.NFTables import NFTables from modules.NFTables import NFTables
def logdebug(msg):
if DEBUG:
logger.logInfo("DEBUG: %s" % msg)
# globals # Globals
WHITELIST = [] WHITELIST = []
BLACKLIST= [] BLACKLIST = []
bans = {} bans = {}
quit_now = False quit_now = False
exit_code = 0 exit_code = 0
@@ -33,43 +38,41 @@ r = None
pubsub = None pubsub = None
clear_before_quit = False clear_before_quit = False
def refreshF2boptions(): def refreshF2boptions():
global f2boptions global f2boptions
global quit_now global quit_now
global exit_code global exit_code
f2boptions = {} f2boptions = {}
if not r.get('F2B_OPTIONS'): if not valkey.get('F2B_OPTIONS'):
f2boptions['ban_time'] = r.get('F2B_BAN_TIME') f2boptions['ban_time'] = valkey.get('F2B_BAN_TIME')
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME') f2boptions['max_ban_time'] = valkey.get('F2B_MAX_BAN_TIME')
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT') f2boptions['ban_time_increment'] = valkey.get('F2B_BAN_TIME_INCREMENT')
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') f2boptions['max_attempts'] = valkey.get('F2B_MAX_ATTEMPTS')
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') f2boptions['retry_window'] = valkey.get('F2B_RETRY_WINDOW')
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') f2boptions['netban_ipv4'] = valkey.get('F2B_NETBAN_IPV4')
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') f2boptions['netban_ipv6'] = valkey.get('F2B_NETBAN_IPV6')
else: else:
try: try:
f2boptions = json.loads(r.get('F2B_OPTIONS')) f2boptions = json.loads(valkey.get('F2B_OPTIONS'))
except ValueError: except ValueError:
logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json') logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json')
quit_now = True quit_now = True
exit_code = 2 exit_code = 2
verifyF2boptions(f2boptions) verifyF2boptions(f2boptions)
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False)) valkey.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
def verifyF2boptions(f2boptions): def verifyF2boptions(f2boptions):
verifyF2boption(f2boptions,'ban_time', 1800) verifyF2boption(f2boptions, 'ban_time', 1800)
verifyF2boption(f2boptions,'max_ban_time', 10000) verifyF2boption(f2boptions, 'max_ban_time', 10000)
verifyF2boption(f2boptions,'ban_time_increment', True) verifyF2boption(f2boptions, 'ban_time_increment', True)
verifyF2boption(f2boptions,'max_attempts', 10) verifyF2boption(f2boptions, 'max_attempts', 10)
verifyF2boption(f2boptions,'retry_window', 600) verifyF2boption(f2boptions, 'retry_window', 600)
verifyF2boption(f2boptions,'netban_ipv4', 32) verifyF2boption(f2boptions, 'netban_ipv4', 32)
verifyF2boption(f2boptions,'netban_ipv6', 128) verifyF2boption(f2boptions, 'netban_ipv6', 128)
verifyF2boption(f2boptions,'banlist_id', str(uuid.uuid4())) verifyF2boption(f2boptions, 'banlist_id', str(uuid.uuid4()))
verifyF2boption(f2boptions,'manage_external', 0) verifyF2boption(f2boptions, 'manage_external', 0)
def verifyF2boption(f2boptions, f2boption, f2bdefault): def verifyF2boption(f2boptions, f2boption, f2bdefault):
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
@@ -78,7 +81,7 @@ def refreshF2bregex():
global f2bregex global f2bregex
global quit_now global quit_now
global exit_code global exit_code
if not r.get('F2B_REGEX'): if not valkey.get('F2B_REGEX'):
f2bregex = {} f2bregex = {}
f2bregex[1] = r'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)' f2bregex[1] = r'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
f2bregex[2] = r'Rspamd UI: Invalid password by ([0-9a-f\.:]+)' f2bregex[2] = r'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
@@ -89,11 +92,11 @@ def refreshF2bregex():
f2bregex[7] = r'\w+\([^,]+,([0-9a-f\.:]+),<[^>]+>\): unknown user \(SHA1 of given password: [a-f0-9]+\)' f2bregex[7] = r'\w+\([^,]+,([0-9a-f\.:]+),<[^>]+>\): unknown user \(SHA1 of given password: [a-f0-9]+\)'
f2bregex[8] = r'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked' f2bregex[8] = r'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
f2bregex[9] = r'([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+' f2bregex[9] = r'([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False)) valkey.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
else: else:
try: try:
f2bregex = {} f2bregex = {}
f2bregex = json.loads(r.get('F2B_REGEX')) f2bregex = json.loads(valkey.get('F2B_REGEX'))
except ValueError: except ValueError:
logger.logCrit('Error loading F2B options: F2B_REGEX is not json') logger.logCrit('Error loading F2B options: F2B_REGEX is not json')
quit_now = True quit_now = True
@@ -111,7 +114,7 @@ def get_ip(address):
def ban(address): def ban(address):
global f2boptions global f2boptions
global lock global lock
logdebug("ban() called with address=%s" % address)
refreshF2boptions() refreshF2boptions()
MAX_ATTEMPTS = int(f2boptions['max_attempts']) MAX_ATTEMPTS = int(f2boptions['max_attempts'])
RETRY_WINDOW = int(f2boptions['retry_window']) RETRY_WINDOW = int(f2boptions['retry_window'])
@@ -119,31 +122,43 @@ def ban(address):
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6']) NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
ip = get_ip(address) ip = get_ip(address)
if not ip: return if not ip:
logdebug("No valid IP -- skipping ban()")
return
address = str(ip) address = str(ip)
self_network = ipaddress.ip_network(address) self_network = ipaddress.ip_network(address)
with lock: with lock:
temp_whitelist = set(WHITELIST) temp_whitelist = set(WHITELIST)
if temp_whitelist: logdebug("Checking if %s overlaps with any WHITELIST entries" % self_network)
for wl_key in temp_whitelist: if temp_whitelist:
wl_net = ipaddress.ip_network(wl_key, False) for wl_key in temp_whitelist:
if wl_net.overlaps(self_network): wl_net = ipaddress.ip_network(wl_key, False)
logger.logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net)) logdebug("Checking overlap between %s and %s" % (self_network, wl_net))
return if wl_net.overlaps(self_network):
logger.logInfo(
'Address %s is allowlisted 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 = ipaddress.ip_network(
(address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
net = str(net) net = str(net)
logdebug("Ban net: %s" % net)
if not net in bans: if not net in bans:
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0} bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
logdebug("Initing new ban counter for %s" % net)
current_attempt = time.time() current_attempt = time.time()
logdebug("Current attempt ts=%s, previous: %s, retry_window: %s" %
(current_attempt, bans[net]['last_attempt'], RETRY_WINDOW))
if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW: if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW:
bans[net]['attempts'] = 0 bans[net]['attempts'] = 0
logdebug("Ban counter for %s reset as window expired" % net)
bans[net]['attempts'] += 1 bans[net]['attempts'] += 1
bans[net]['last_attempt'] = current_attempt bans[net]['last_attempt'] = current_attempt
logdebug("%s attempts now %d" % (net, bans[net]['attempts']))
if bans[net]['attempts'] >= MAX_ATTEMPTS: if bans[net]['attempts'] >= MAX_ATTEMPTS:
cur_time = int(round(time.time())) cur_time = int(round(time.time()))
@@ -151,34 +166,41 @@ def ban(address):
logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 )) logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1: if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1:
with lock: with lock:
logdebug("Calling tables.banIPv4(%s)" % net)
tables.banIPv4(net) tables.banIPv4(net)
elif int(f2boptions['manage_external']) != 1: elif int(f2boptions['manage_external']) != 1:
with lock: with lock:
logdebug("Calling tables.banIPv6(%s)" % net)
tables.banIPv6(net) tables.banIPv6(net)
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME) logdebug("Updating F2B_ACTIVE_BANS[%s]=%d" %
(net, cur_time + NET_BAN_TIME))
valkey.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
else: else:
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)) 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): def unban(net):
global lock global lock
logdebug("Calling unban() with net=%s" % net)
if not net in bans: if not net in bans:
logger.logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net) logger.logInfo(
r.hdel('F2B_QUEUE_UNBAN', '%s' % net) '%s is not banned, skipping unban and deleting from queue (if any)' % net)
return valkey.hdel('F2B_QUEUE_UNBAN', '%s' % net)
return
logger.logInfo('Unbanning %s' % net) logger.logInfo('Unbanning %s' % net)
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network: if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
with lock: with lock:
logdebug("Calling tables.unbanIPv4(%s)" % net)
tables.unbanIPv4(net) tables.unbanIPv4(net)
else: else:
with lock: with lock:
logdebug("Calling tables.unbanIPv6(%s)" % net)
tables.unbanIPv6(net) tables.unbanIPv6(net)
valkey.hdel('F2B_ACTIVE_BANS', '%s' % net)
r.hdel('F2B_ACTIVE_BANS', '%s' % net) valkey.hdel('F2B_QUEUE_UNBAN', '%s' % net)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
if net in bans: if net in bans:
logdebug("Unban for %s, setting attempts=0, ban_counter+=1" % net)
bans[net]['attempts'] = 0 bans[net]['attempts'] = 0
bans[net]['ban_counter'] += 1 bans[net]['ban_counter'] += 1
@@ -203,33 +225,35 @@ def permBan(net, unban=False):
if is_unbanned: if is_unbanned:
r.hdel('F2B_PERM_BANS', '%s' % net) valkey.hdel('F2B_PERM_BANS', '%s' % net)
logger.logCrit('Removed host/network %s from blacklist' % net) logger.logCrit('Removed host/network %s from denylist' % net)
elif is_banned: elif is_banned:
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) valkey.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
logger.logCrit('Added host/network %s to blacklist' % net) logger.logCrit('Added host/network %s to denylist' % net)
def clear(): def clear():
global lock global lock
logger.logInfo('Clearing all bans') logger.logInfo('Clearing all bans')
for net in bans.copy(): for net in bans.copy():
logdebug("Unbanning net: %s" % net)
unban(net) unban(net)
with lock: with lock:
logdebug("Clearing IPv4/IPv6 table")
tables.clearIPv4Table() tables.clearIPv4Table()
tables.clearIPv6Table() tables.clearIPv6Table()
try: try:
if r is not None: if r is not None:
r.delete('F2B_ACTIVE_BANS') valkey.delete('F2B_ACTIVE_BANS')
r.delete('F2B_PERM_BANS') valkey.delete('F2B_PERM_BANS')
except Exception as ex: except Exception as ex:
logger.logWarn('Error clearing redis keys F2B_ACTIVE_BANS and F2B_PERM_BANS: %s' % ex) logger.logWarn('Error clearing valkey keys F2B_ACTIVE_BANS and F2B_PERM_BANS: %s' % ex)
def watch(): def watch():
global pubsub global pubsub
global quit_now global quit_now
global exit_code global exit_code
logger.logInfo('Watching Redis channel F2B_CHANNEL') logger.logInfo('Watching Valkey channel F2B_CHANNEL')
pubsub.subscribe('F2B_CHANNEL') pubsub.subscribe('F2B_CHANNEL')
while not quit_now: while not quit_now:
@@ -275,21 +299,35 @@ def snat6(snat_target):
def autopurge(): def autopurge():
global f2boptions global f2boptions
logdebug("autopurge thread started")
while not quit_now: while not quit_now:
logdebug("autopurge tick")
time.sleep(10) time.sleep(10)
refreshF2boptions() refreshF2boptions()
MAX_ATTEMPTS = int(f2boptions['max_attempts']) MAX_ATTEMPTS = int(f2boptions['max_attempts'])
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN') QUEUE_UNBAN = valkey.hgetall('F2B_QUEUE_UNBAN')
logdebug("QUEUE_UNBAN: %s" % QUEUE_UNBAN)
if QUEUE_UNBAN: if QUEUE_UNBAN:
for net in QUEUE_UNBAN: for net in QUEUE_UNBAN:
logdebug("Autopurge: unbanning queued net: %s" % net)
unban(str(net)) unban(str(net))
for net in bans.copy(): # Only check expiry for actively banned IPs:
if bans[net]['attempts'] >= MAX_ATTEMPTS: active_bans = r.hgetall('F2B_ACTIVE_BANS')
NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter']) now = time.time()
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt'] for net_str, expire_str in active_bans.items():
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME: logdebug("Checking ban expiry for (actively banned): %s" % net_str)
unban(net) # Defensive: always process if timer missing or expired
try:
expire = float(expire_str)
except Exception:
logdebug("Invalid expire time for %s; unbanning" % net_str)
unban(net_str)
continue
time_left = expire - now
logdebug("Time left for %s: %.1f seconds" % (net_str, time_left))
if time_left <= 0:
logdebug("Ban expired for %s" % net_str)
unban(net_str)
def mailcowChainOrder(): def mailcowChainOrder():
global lock global lock
@@ -352,14 +390,14 @@ def whitelistUpdate():
global WHITELIST global WHITELIST
while not quit_now: while not quit_now:
start_time = time.time() start_time = time.time()
list = r.hgetall('F2B_WHITELIST') list = valkey.hgetall('F2B_WHITELIST')
new_whitelist = [] new_whitelist = []
if list: if list:
new_whitelist = genNetworkList(list) new_whitelist = genNetworkList(list)
with lock: with lock:
if Counter(new_whitelist) != Counter(WHITELIST): if Counter(new_whitelist) != Counter(WHITELIST):
WHITELIST = new_whitelist WHITELIST = new_whitelist
logger.logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST)) logger.logInfo('Allowlist was changed, it has %s entries' % len(WHITELIST))
time.sleep(60.0 - ((time.time() - start_time) % 60.0)) time.sleep(60.0 - ((time.time() - start_time) % 60.0))
def blacklistUpdate(): def blacklistUpdate():
@@ -367,7 +405,7 @@ def blacklistUpdate():
global BLACKLIST global BLACKLIST
while not quit_now: while not quit_now:
start_time = time.time() start_time = time.time()
list = r.hgetall('F2B_BLACKLIST') list = valkey.hgetall('F2B_BLACKLIST')
new_blacklist = [] new_blacklist = []
if list: if list:
new_blacklist = genNetworkList(list) new_blacklist = genNetworkList(list)
@@ -375,7 +413,7 @@ def blacklistUpdate():
addban = set(new_blacklist).difference(BLACKLIST) addban = set(new_blacklist).difference(BLACKLIST)
delban = set(BLACKLIST).difference(new_blacklist) delban = set(BLACKLIST).difference(new_blacklist)
BLACKLIST = new_blacklist BLACKLIST = new_blacklist
logger.logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST)) logger.logInfo('Denylist was changed, it has %s entries' % len(BLACKLIST))
if addban: if addban:
for net in addban: for net in addban:
permBan(net=net) permBan(net=net)
@@ -386,71 +424,77 @@ def blacklistUpdate():
def sigterm_quit(signum, frame): def sigterm_quit(signum, frame):
global clear_before_quit global clear_before_quit
logdebug("SIGTERM received, setting clear_before_quit to True and exiting")
clear_before_quit = True clear_before_quit = True
sys.exit(exit_code) sys.exit(exit_code)
def berfore_quit(): def before_quit():
logdebug("before_quit called, clear_before_quit=%s" % clear_before_quit)
if clear_before_quit: if clear_before_quit:
clear() clear()
if pubsub is not None: if pubsub is not None:
pubsub.unsubscribe() pubsub.unsubscribe()
if __name__ == '__main__': if __name__ == '__main__':
atexit.register(berfore_quit) logger = Logger()
logdebug("Sys.argv: %s" % sys.argv)
atexit.register(before_quit)
signal.signal(signal.SIGTERM, sigterm_quit) signal.signal(signal.SIGTERM, sigterm_quit)
# init Logger
logger = Logger()
# init backend
backend = sys.argv[1] backend = sys.argv[1]
logdebug("Backend: %s" % backend)
if backend == "nftables": if backend == "nftables":
logger.logInfo('Using NFTables backend') logger.logInfo('Using NFTables backend')
tables = NFTables(chain_name, logger) tables = NFTables(chain_name, logger)
else: else:
logger.logInfo('Using IPTables backend') logger.logInfo('Using IPTables backend')
logger.logWarn(
"DEPRECATION: iptables-legacy is deprecated and will be removed in future releases. "
"Please switch to nftables on your host to ensure complete compatibility."
)
time.sleep(5)
tables = IPTables(chain_name, logger) tables = IPTables(chain_name, logger)
# In case a previous session was killed without cleanup
clear() clear()
# Reinit MAILCOW chain
# Is called before threads start, no locking
logger.logInfo("Initializing mailcow netfilter chain") logger.logInfo("Initializing mailcow netfilter chain")
tables.initChainIPv4() tables.initChainIPv4()
tables.initChainIPv6() tables.initChainIPv6()
if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE").lower() in ("y", "yes"): if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE", "").lower() in ("y", "yes"):
logger.logInfo(f"Skipping {chain_name} isolation") logger.logInfo(f"Skipping {chain_name} isolation")
else: else:
logger.logInfo(f"Setting {chain_name} isolation") logger.logInfo(f"Setting {chain_name} isolation")
tables.create_mailcow_isolation_rule("br-mailcow", [3306, 6379, 8983, 12345], os.getenv("MAILCOW_REPLICA_IP")) tables.create_mailcow_isolation_rule("br-mailcow", [3306, 6379, 8983, 12345], os.getenv("MAILCOW_REPLICA_IP"))
# connect to redis # connect to valkey
while True: while True:
try: try:
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '') valkey_slaveof_ip = os.getenv('VALKEY_SLAVEOF_IP', '')
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '') valkey_slaveof_port = os.getenv('VALKEY_SLAVEOF_PORT', '')
if "".__eq__(redis_slaveof_ip): logdebug(
r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS']) "Connecting valkey (SLAVEOF_IP:%s, PORT:%s)" % (valkey_slaveof_ip, valkey_slaveof_port))
if "".__eq__(valkey_slaveof_ip):
valkey = redis.StrictRedis(
host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['VALKEYPASS'])
else: else:
r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS']) valkey = redis.StrictRedis(
r.ping() host=valkey_slaveof_ip, decode_responses=True, port=valkey_slaveof_port, db=0, password=os.environ['VALKEYPASS'])
pubsub = r.pubsub() valkey.ping()
pubsub = valkey.pubsub()
except Exception as ex: except Exception as ex:
print('%s - trying again in 3 seconds' % (ex)) logdebug(
'Redis connection failed: %s - trying again in 3 seconds' % (ex))
time.sleep(3) time.sleep(3)
else: else:
break break
logger.set_redis(r) logger.set_valkey(valkey)
logdebug("Valkey connection established, setting up F2B keys")
# rename fail2ban to netfilter if valkey.exists('F2B_LOG'):
if r.exists('F2B_LOG'): logdebug("Renaming F2B_LOG to NETFILTER_LOG")
r.rename('F2B_LOG', 'NETFILTER_LOG') valkey.rename('F2B_LOG', 'NETFILTER_LOG')
# clear bans in redis valkey.delete('F2B_ACTIVE_BANS')
r.delete('F2B_ACTIVE_BANS') valkey.delete('F2B_PERM_BANS')
r.delete('F2B_PERM_BANS')
refreshF2boptions() refreshF2boptions()
@@ -463,7 +507,7 @@ if __name__ == '__main__':
snat_ip = os.getenv('SNAT_TO_SOURCE') snat_ip = os.getenv('SNAT_TO_SOURCE')
snat_ipo = ipaddress.ip_address(snat_ip) snat_ipo = ipaddress.ip_address(snat_ip)
if type(snat_ipo) is ipaddress.IPv4Address: if type(snat_ipo) is ipaddress.IPv4Address:
snat4_thread = Thread(target=snat4,args=(snat_ip,)) snat4_thread = Thread(target=snat4, args=(snat_ip,))
snat4_thread.daemon = True snat4_thread.daemon = True
snat4_thread.start() snat4_thread.start()
except ValueError: except ValueError:
@@ -499,4 +543,5 @@ if __name__ == '__main__':
while not quit_now: while not quit_now:
time.sleep(0.5) time.sleep(0.5)
sys.exit(exit_code) logdebug("Exiting with code %s" % exit_code)
sys.exit(exit_code)

View File

@@ -1,24 +1,36 @@
import time import time
import json import json
import datetime
class Logger: class Logger:
def __init__(self): def __init__(self):
self.r = None self.valkey = None
def set_redis(self, redis): def set_valkey(self, valkey):
self.r = redis self.valkey = valkey
def _format_timestamp(self):
# Local time with milliseconds
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def log(self, priority, message): def log(self, priority, message):
tolog = {} # build valkey-friendly dict
tolog['time'] = int(round(time.time())) tolog = {
tolog['priority'] = priority 'time': int(round(time.time())), # keep raw timestamp for Valkey
tolog['message'] = message 'priority': priority,
print(message) 'message': message
if self.r is not None: }
# print human-readable message with timestamp
ts = self._format_timestamp()
print(f"{ts} {priority.upper()}: {message}", flush=True)
# also push JSON to Redis if connected
if self.valkey is not None:
try: try:
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False)) self.valkey.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
except Exception as ex: except Exception as ex:
print('Failed logging to redis: %s' % (ex)) print(f'{ts} WARN: Failed logging to valkey: {ex}', flush=True)
def logWarn(self, message): def logWarn(self, message):
self.log('warn', message) self.log('warn', message)
@@ -27,4 +39,4 @@ class Logger:
self.log('crit', message) self.log('crit', message)
def logInfo(self, message): def logInfo(self, message):
self.log('info', message) self.log('info', message)

View File

@@ -10,7 +10,7 @@ def includes_conf(env, template_vars):
server_name_config = f"server_name {template_vars['MAILCOW_HOSTNAME']} autodiscover.* autoconfig.* {' '.join(template_vars['ADDITIONAL_SERVER_NAMES'])};" server_name_config = f"server_name {template_vars['MAILCOW_HOSTNAME']} autodiscover.* autoconfig.* {' '.join(template_vars['ADDITIONAL_SERVER_NAMES'])};"
listen_plain_config = f"listen {template_vars['HTTP_PORT']};" listen_plain_config = f"listen {template_vars['HTTP_PORT']};"
listen_ssl_config = f"listen {template_vars['HTTPS_PORT']};" listen_ssl_config = f"listen {template_vars['HTTPS_PORT']};"
if not template_vars['DISABLE_IPv6']: if template_vars['ENABLE_IPV6']:
listen_plain_config += f"\nlisten [::]:{template_vars['HTTP_PORT']};" listen_plain_config += f"\nlisten [::]:{template_vars['HTTP_PORT']};"
listen_ssl_config += f"\nlisten [::]:{template_vars['HTTPS_PORT']} ssl;" listen_ssl_config += f"\nlisten [::]:{template_vars['HTTPS_PORT']} ssl;"
listen_ssl_config += "\nhttp2 on;" listen_ssl_config += "\nhttp2 on;"
@@ -58,7 +58,7 @@ def prepare_template_vars():
'SOGOHOST': os.getenv("SOGOHOST", ipv4_network + ".248"), 'SOGOHOST': os.getenv("SOGOHOST", ipv4_network + ".248"),
'RSPAMDHOST': os.getenv("RSPAMDHOST", "rspamd-mailcow"), 'RSPAMDHOST': os.getenv("RSPAMDHOST", "rspamd-mailcow"),
'PHPFPMHOST': os.getenv("PHPFPMHOST", "php-fpm-mailcow"), 'PHPFPMHOST': os.getenv("PHPFPMHOST", "php-fpm-mailcow"),
'DISABLE_IPv6': os.getenv("DISABLE_IPv6", "n").lower() in ("y", "yes"), 'ENABLE_IPV6': os.getenv("ENABLE_IPV6", "true").lower() != "false",
'HTTP_REDIRECT': os.getenv("HTTP_REDIRECT", "n").lower() in ("y", "yes"), 'HTTP_REDIRECT': os.getenv("HTTP_REDIRECT", "n").lower() in ("y", "yes"),
} }

View File

@@ -3,15 +3,15 @@ FROM php:8.2-fpm-alpine3.21
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG APCU_PECL_VERSION=5.1.24 ARG APCU_PECL_VERSION=5.1.26
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$ # renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
ARG IMAGICK_PECL_VERSION=3.8.0 ARG IMAGICK_PECL_VERSION=3.8.0
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MAILPARSE_PECL_VERSION=3.1.8 ARG MAILPARSE_PECL_VERSION=3.1.8
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$ # 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.3.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$ # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
ARG REDIS_PECL_VERSION=6.1.0 ARG REDIS_PECL_VERSION=6.2.0
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$ # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
ARG COMPOSER_VERSION=2.8.6 ARG COMPOSER_VERSION=2.8.6

View File

@@ -9,24 +9,24 @@ while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u
done done
# Do not attempt to write to slave # Do not attempt to write to slave
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
REDIS_HOST=$REDIS_SLAVEOF_IP VALKEY_HOST=$VALKEY_SLAVEOF_IP
REDIS_PORT=$REDIS_SLAVEOF_PORT VALKEY_PORT=$VALKEY_SLAVEOF_PORT
else else
REDIS_HOST="redis" VALKEY_HOST="valkey-mailcow"
REDIS_PORT="6379" VALKEY_PORT="6379"
fi fi
REDIS_CMDLINE="redis-cli -h ${REDIS_HOST} -p ${REDIS_PORT} -a ${REDISPASS} --no-auth-warning" VALKEY_CMDLINE="redis-cli -h ${VALKEY_HOST} -p ${VALKEY_PORT} -a ${VALKEYPASS} --no-auth-warning"
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..." echo "Waiting for Valkey..."
sleep 2 sleep 2
done done
# Set redis session store # Set valkey session store
echo -n ' echo -n '
session.save_handler = redis session.save_handler = redis
session.save_path = "tcp://'${REDIS_HOST}':'${REDIS_PORT}'?auth='${REDISPASS}'" session.save_path = "tcp://'${VALKEY_HOST}':'${VALKEY_PORT}'?auth='${VALKEYPASS}'"
' > /usr/local/etc/php/conf.d/session_store.ini ' > /usr/local/etc/php/conf.d/session_store.ini
# Check mysql_upgrade (master and slave) # Check mysql_upgrade (master and slave)
@@ -91,22 +91,22 @@ fi
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "We are master, preparing..." echo "We are master, preparing..."
# Set a default release format # Set a default release format
if [[ -z $(${REDIS_CMDLINE} --raw GET Q_RELEASE_FORMAT) ]]; then if [[ -z $(${VALKEY_CMDLINE} --raw GET Q_RELEASE_FORMAT) ]]; then
${REDIS_CMDLINE} --raw SET Q_RELEASE_FORMAT raw ${VALKEY_CMDLINE} --raw SET Q_RELEASE_FORMAT raw
fi fi
# Set max age of q items - if unset # Set max age of q items - if unset
if [[ -z $(${REDIS_CMDLINE} --raw GET Q_MAX_AGE) ]]; then if [[ -z $(${VALKEY_CMDLINE} --raw GET Q_MAX_AGE) ]]; then
${REDIS_CMDLINE} --raw SET Q_MAX_AGE 365 ${VALKEY_CMDLINE} --raw SET Q_MAX_AGE 365
fi fi
# Set default password policy - if unset # Set default password policy - if unset
if [[ -z $(${REDIS_CMDLINE} --raw HGET PASSWD_POLICY length) ]]; then if [[ -z $(${VALKEY_CMDLINE} --raw HGET PASSWD_POLICY length) ]]; then
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY length 6 ${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY length 6
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY chars 0 ${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY chars 0
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY special_chars 0 ${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY special_chars 0
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY lowerupper 0 ${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY lowerupper 0
${REDIS_CMDLINE} --raw HSET PASSWD_POLICY numbers 0 ${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY numbers 0
fi fi
# Trigger db init # Trigger db init
@@ -114,9 +114,9 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
php -c /usr/local/etc/php -f /web/inc/init_db.inc.php php -c /usr/local/etc/php -f /web/inc/init_db.inc.php
# Recreating domain map # Recreating domain map
echo "Rebuilding domain map in Redis..." echo "Rebuilding domain map in Valkey..."
declare -a DOMAIN_ARR declare -a DOMAIN_ARR
${REDIS_CMDLINE} DEL DOMAIN_MAP > /dev/null ${VALKEY_CMDLINE} DEL DOMAIN_MAP > /dev/null
while read line while read line
do do
DOMAIN_ARR+=("$line") DOMAIN_ARR+=("$line")
@@ -128,7 +128,7 @@ if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
if [[ ! -z ${DOMAIN_ARR} ]]; then if [[ ! -z ${DOMAIN_ARR} ]]; then
for domain in "${DOMAIN_ARR[@]}"; do for domain in "${DOMAIN_ARR[@]}"; do
${REDIS_CMDLINE} HSET DOMAIN_MAP ${domain} 1 > /dev/null ${VALKEY_CMDLINE} HSET DOMAIN_MAP ${domain} 1 > /dev/null
done done
fi fi

View File

@@ -0,0 +1,50 @@
FROM golang:1.25-bookworm AS builder
WORKDIR /src
ENV CGO_ENABLED=0 \
GO111MODULE=on \
NOOPT=1 \
VERSION=1.8.14
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
cd /src/postfix-tlspol && \
scripts/build.sh build-only
FROM debian:bookworm-slim
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ENV LC_ALL=C
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
dirmngr \
dnsutils \
iputils-ping \
sudo \
supervisor \
redis-tools \
syslog-ng \
syslog-ng-core \
syslog-ng-mod-redis \
tzdata \
&& rm -rf /var/lib/apt/lists/* \
&& touch /etc/default/locale
COPY supervisord.conf /etc/supervisor/supervisord.conf
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
COPY syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf
COPY postfix-tlspol.sh /opt/postfix-tlspol.sh
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
COPY docker-entrypoint.sh /docker-entrypoint.sh
COPY --from=builder /src/postfix-tlspol/build/postfix-tlspol /usr/local/bin/postfix-tlspol
RUN chmod +x /opt/postfix-tlspol.sh \
/usr/local/sbin/stop-supervisor.sh \
/docker-entrypoint.sh
RUN rm -rf /tmp/* /var/tmp/*
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]

View File

@@ -0,0 +1,7 @@
#!/bin/bash
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf
fi
exec "$@"

View File

@@ -0,0 +1,52 @@
#!/bin/bash
LOGLVL=info
if [ ${DEV_MODE} != "n" ]; then
echo -e "\e[31mEnabling debug mode\e[0m"
set -x
LOGLVL=debug
fi
[[ ! -d /etc/postfix-tlspol ]] && mkdir -p /etc/postfix-tlspol
[[ ! -d /var/lib/postfix-tlspol ]] && mkdir -p /var/lib/postfix-tlspol
until dig +short mailcow.email > /dev/null; do
echo "Waiting for DNS..."
sleep 1
done
# Do not attempt to write to slave
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
export VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
else
export VALKEY_CMDLINE="redis-cli -h valkey -p 6379 -a ${VALKEYPASS} --no-auth-warning"
fi
until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Valkey..."
sleep 2
done
echo "Waiting for Postfix..."
until ping postfix -c1 > /dev/null; do
sleep 1
done
echo "Postfix OK"
cat <<EOF > /etc/postfix-tlspol/config.yaml
server:
address: 0.0.0.0:8642
log-level: ${LOGLVL}
prefetch: true
cache-file: /var/lib/postfix-tlspol/cache.db
dns:
# must support DNSSEC
address: 127.0.0.11:53
EOF
/usr/local/bin/postfix-tlspol -config /etc/postfix-tlspol/config.yaml

View File

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

View File

@@ -0,0 +1,25 @@
[supervisord]
pidfile=/var/run/supervisord.pid
nodaemon=true
user=root
[program:syslog-ng]
command=/usr/sbin/syslog-ng --foreground --no-caps
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autostart=true
[program:postfix-tlspol]
startsecs=10
autorestart=true
command=/opt/postfix-tlspol.sh
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[eventlistener:processes]
command=/usr/local/sbin/stop-supervisor.sh
events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL

View File

@@ -0,0 +1,45 @@
@version: 3.38
@include "scl.conf"
options {
chain_hostnames(off);
flush_lines(0);
use_dns(no);
dns_cache(no);
use_fqdn(no);
owner("root"); group("adm"); perm(0640);
stats_freq(0);
bad_hostname("^gconfd$");
};
source s_src {
unix-stream("/dev/log");
internal();
};
destination d_stdout { pipe("/dev/stdout"); };
destination d_valkey_ui_log {
redis(
host("`VALKEY_SLAVEOF_IP`")
persist-name("valkey1")
port(`VALKEY_SLAVEOF_PORT`)
auth("`VALKEYPASS`")
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
filter f_mail { facility(mail); };
# start
# overriding warnings are still displayed when the entrypoint runs its initial check
# warnings logged by postfix-mailcow to syslog are hidden to reduce repeating msgs
# Some other warnings are ignored
filter f_ignore {
not match("overriding earlier entry" value("MESSAGE"));
not match("TLS SNI from checks.mailcow.email" value("MESSAGE"));
not match("no SASL support" value("MESSAGE"));
not facility (local0, local1, local2, local3, local4, local5, local6, local7);
};
# end
log {
source(s_src);
filter(f_ignore);
destination(d_stdout);
filter(f_mail);
destination(d_valkey_ui_log);
};

View File

@@ -0,0 +1,45 @@
@version: 3.38
@include "scl.conf"
options {
chain_hostnames(off);
flush_lines(0);
use_dns(no);
dns_cache(no);
use_fqdn(no);
owner("root"); group("adm"); perm(0640);
stats_freq(0);
bad_hostname("^gconfd$");
};
source s_src {
unix-stream("/dev/log");
internal();
};
destination d_stdout { pipe("/dev/stdout"); };
destination d_valkey_ui_log {
redis(
host("valkey-mailcow")
persist-name("valkey1")
port(6379)
auth("`VALKEYPASS`")
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
);
};
filter f_mail { facility(mail); };
# start
# overriding warnings are still displayed when the entrypoint runs its initial check
# warnings logged by postfix-mailcow to syslog are hidden to reduce repeating msgs
# Some other warnings are ignored
filter f_ignore {
not match("overriding earlier entry" value("MESSAGE"));
not match("TLS SNI from checks.mailcow.email" value("MESSAGE"));
not match("no SASL support" value("MESSAGE"));
not facility (local0, local1, local2, local3, local4, local5, local6, local7);
};
# end
log {
source(s_src);
filter(f_ignore);
destination(d_stdout);
filter(f_mail);
destination(d_valkey_ui_log);
};

View File

@@ -1,9 +1,9 @@
FROM debian:bookworm-slim FROM debian:bookworm-slim
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ENV LC_ALL C ENV LC_ALL=C
RUN dpkg-divert --local --rename --add /sbin/initctl \ RUN dpkg-divert --local --rename --add /sbin/initctl \
&& ln -sf /bin/true /sbin/initctl \ && ln -sf /bin/true /sbin/initctl \
@@ -41,7 +41,7 @@ RUN groupadd -g 102 postfix \
COPY supervisord.conf /etc/supervisor/supervisord.conf COPY supervisord.conf /etc/supervisor/supervisord.conf
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf COPY syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf
COPY postfix.sh /opt/postfix.sh COPY postfix.sh /opt/postfix.sh
COPY rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham COPY rspamd-pipe-ham /usr/local/bin/rspamd-pipe-ham
COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam COPY rspamd-pipe-spam /usr/local/bin/rspamd-pipe-spam

View File

@@ -8,8 +8,8 @@ for file in /hooks/*; do
fi fi
done done
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf
fi fi
# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20) # Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
@@ -21,6 +21,6 @@ if grep -qE '\!SSLv2|\!SSLv3|>=TLSv1(\.[0-1])?$' /opt/postfix/conf/main.cf /opt/
echo "[tls_system_default]" >> /etc/ssl/openssl.cnf echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
fi fi
exec "$@" exec "$@"

View File

@@ -524,4 +524,4 @@ if [[ $? != 0 ]]; then
else else
postfix -c /opt/postfix/conf start postfix -c /opt/postfix/conf start
sleep 126144000 sleep 126144000
fi fi

View File

@@ -15,21 +15,21 @@ source s_src {
internal(); internal();
}; };
destination d_stdout { pipe("/dev/stdout"); }; destination d_stdout { pipe("/dev/stdout"); };
destination d_redis_ui_log { destination d_valkey_ui_log {
redis( redis(
host("`REDIS_SLAVEOF_IP`") host("`VALKEY_SLAVEOF_IP`")
persist-name("redis1") persist-name("valkey1")
port(`REDIS_SLAVEOF_PORT`) port(`VALKEY_SLAVEOF_PORT`)
auth("`REDISPASS`") auth("`VALKEYPASS`")
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
); );
}; };
destination d_redis_f2b_channel { destination d_valkey_f2b_channel {
redis( redis(
host("`REDIS_SLAVEOF_IP`") host("`VALKEY_SLAVEOF_IP`")
persist-name("redis2") persist-name("valkey2")
port(`REDIS_SLAVEOF_PORT`) port(`VALKEY_SLAVEOF_PORT`)
auth("`REDISPASS`") auth("`VALKEYPASS`")
command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
); );
}; };
@@ -50,6 +50,6 @@ log {
filter(f_ignore); filter(f_ignore);
destination(d_stdout); destination(d_stdout);
filter(f_mail); filter(f_mail);
destination(d_redis_ui_log); destination(d_valkey_ui_log);
destination(d_redis_f2b_channel); destination(d_valkey_f2b_channel);
}; };

View File

@@ -15,21 +15,21 @@ source s_src {
internal(); internal();
}; };
destination d_stdout { pipe("/dev/stdout"); }; destination d_stdout { pipe("/dev/stdout"); };
destination d_redis_ui_log { destination d_valkey_ui_log {
redis( redis(
host("redis-mailcow") host("valkey-mailcow")
persist-name("redis1") persist-name("valkey1")
port(6379) port(6379)
auth("`REDISPASS`") auth("`VALKEYPASS`")
command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
); );
}; };
destination d_redis_f2b_channel { destination d_valkey_f2b_channel {
redis( redis(
host("redis-mailcow") host("valkey-mailcow")
persist-name("redis2") persist-name("valkey2")
port(6379) port(6379)
auth("`REDISPASS`") auth("`VALKEYPASS`")
command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
); );
}; };
@@ -50,6 +50,6 @@ log {
filter(f_ignore); filter(f_ignore);
destination(d_stdout); destination(d_stdout);
filter(f_mail); filter(f_mail);
destination(d_redis_ui_log); destination(d_valkey_ui_log);
destination(d_redis_f2b_channel); destination(d_valkey_f2b_channel);
}; };

View File

@@ -2,7 +2,7 @@ FROM debian:bookworm-slim
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG RSPAMD_VER=rspamd_3.11.1-1~ab0b44951 ARG RSPAMD_VER=rspamd_3.12.1-1~6dbfca2fa
ARG CODENAME=bookworm ARG CODENAME=bookworm
ENV LC_ALL=C ENV LC_ALL=C

View File

@@ -52,33 +52,33 @@ if [[ ! -z ${RSPAMD_V6} ]]; then
echo ${RSPAMD_V6}/128 >> /etc/rspamd/custom/rspamd_trusted.map echo ${RSPAMD_V6}/128 >> /etc/rspamd/custom/rspamd_trusted.map
fi fi
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
cat <<EOF > /etc/rspamd/local.d/redis.conf cat <<EOF > /etc/rspamd/local.d/redis.conf
read_servers = "redis:6379"; read_servers = "valkey-mailcow:6379";
write_servers = "${REDIS_SLAVEOF_IP}:${REDIS_SLAVEOF_PORT}"; write_servers = "${VALKEY_SLAVEOF_IP}:${VALKEY_SLAVEOF_PORT}";
password = "${REDISPASS}"; password = "${VALKEYPASS}";
timeout = 10; timeout = 10;
EOF EOF
until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do until [[ $(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Redis @redis-mailcow..." echo "Waiting for Valkey @valkey-mailcow..."
sleep 2 sleep 2
done done
until [[ $(redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do until [[ $(redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Redis @${REDIS_SLAVEOF_IP}..." echo "Waiting for Valkey @${VALKEY_SLAVEOF_IP}..."
sleep 2 sleep 2
done done
redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF ${REDIS_SLAVEOF_IP} ${REDIS_SLAVEOF_PORT} redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning SLAVEOF ${VALKEY_SLAVEOF_IP} ${VALKEY_SLAVEOF_PORT}
else else
cat <<EOF > /etc/rspamd/local.d/redis.conf cat <<EOF > /etc/rspamd/local.d/redis.conf
servers = "redis:6379"; servers = "valkey-mailcow:6379";
password = "${REDISPASS}"; password = "${VALKEYPASS}";
timeout = 10; timeout = 10;
EOF EOF
until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do until [[ $(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Redis slave..." echo "Waiting for Valkey slave..."
sleep 2 sleep 2
done done
redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF NO ONE redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning SLAVEOF NO ONE
fi fi
if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
@@ -86,7 +86,8 @@ if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
rm /etc/rspamd/local.d/external_services.conf rm /etc/rspamd/local.d/external_services.conf
fi fi
else else
cat <<EOF > /etc/rspamd/local.d/external_services.conf if [[ ! -f /etc/rspamd/local.d/external_services.conf ]]; then
cat <<EOF > /etc/rspamd/local.d/external_services.conf
oletools { oletools {
# default olefy settings # default olefy settings
servers = "olefy:10055"; servers = "olefy:10055";
@@ -100,6 +101,7 @@ oletools {
retransmits = 1; retransmits = 1;
} }
EOF EOF
fi
fi fi
# Provide additional lua modules # Provide additional lua modules

View File

@@ -44,7 +44,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf COPY syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf
COPY supervisord.conf /etc/supervisor/supervisord.conf COPY supervisord.conf /etc/supervisor/supervisord.conf
COPY acl.diff /acl.diff COPY acl.diff /acl.diff
COPY navMailcowBtns.diff /navMailcowBtns.diff COPY navMailcowBtns.diff /navMailcowBtns.diff

View File

@@ -24,6 +24,10 @@ while [[ "${DBV_NOW}" != "${DBV_NEW}" ]]; do
done done
echo "DB schema is ${DBV_NOW}" echo "DB schema is ${DBV_NOW}"
if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TRIGGER IF EXISTS sogo_update_password"
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)
@@ -46,6 +50,10 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
<string>YES</string> <string>YES</string>
<key>SOGoEncryptionKey</key> <key>SOGoEncryptionKey</key>
<string>${RAND_PASS}</string> <string>${RAND_PASS}</string>
<key>SOGoURLEncryptionEnabled</key>
<string>YES</string>
<key>SOGoURLEncryptionPassphrase</key>
<string>${SOGO_URL_ENCRYPTION_KEY}</string>
<key>OCSAdminURL</key> <key>OCSAdminURL</key>
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string> <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
<key>OCSCacheFolderURL</key> <key>OCSCacheFolderURL</key>

View File

@@ -6,8 +6,8 @@ if [[ "${SKIP_SOGO}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
exit 0 exit 0
fi fi
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf
fi fi
echo "$TZ" > /etc/timezone echo "$TZ" > /etc/timezone

View File

@@ -17,28 +17,28 @@ source s_sogo {
pipe("/dev/sogo_log" owner(sogo) group(sogo)); pipe("/dev/sogo_log" owner(sogo) group(sogo));
}; };
destination d_stdout { pipe("/dev/stdout"); }; destination d_stdout { pipe("/dev/stdout"); };
destination d_redis_ui_log { destination d_valkey_ui_log {
redis( redis(
host("`REDIS_SLAVEOF_IP`") host("`VALKEY_SLAVEOF_IP`")
persist-name("redis1") persist-name("valkey1")
port(`REDIS_SLAVEOF_PORT`) port(`VALKEY_SLAVEOF_PORT`)
auth("`REDISPASS`") auth("`VALKEYPASS`")
command("LPUSH" "SOGO_LOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") command("LPUSH" "SOGO_LOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
); );
}; };
destination d_redis_f2b_channel { destination d_valkey_f2b_channel {
redis( redis(
host("`REDIS_SLAVEOF_IP`") host("`VALKEY_SLAVEOF_IP`")
persist-name("redis2") persist-name("valkey2")
port(`REDIS_SLAVEOF_PORT`) port(`VALKEY_SLAVEOF_PORT`)
auth("`REDISPASS`") auth("`VALKEYPASS`")
command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
); );
}; };
log { log {
source(s_sogo); source(s_sogo);
destination(d_redis_ui_log); destination(d_valkey_ui_log);
destination(d_redis_f2b_channel); destination(d_valkey_f2b_channel);
}; };
log { log {
source(s_sogo); source(s_sogo);

View File

@@ -17,28 +17,28 @@ source s_sogo {
pipe("/dev/sogo_log" owner(sogo) group(sogo)); pipe("/dev/sogo_log" owner(sogo) group(sogo));
}; };
destination d_stdout { pipe("/dev/stdout"); }; destination d_stdout { pipe("/dev/stdout"); };
destination d_redis_ui_log { destination d_valkey_ui_log {
redis( redis(
host("redis-mailcow") host("valkey-mailcow")
persist-name("redis1") persist-name("valkey1")
port(6379) port(6379)
auth("`REDISPASS`") auth("`VALKEYPASS`")
command("LPUSH" "SOGO_LOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") command("LPUSH" "SOGO_LOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n")
); );
}; };
destination d_redis_f2b_channel { destination d_valkey_f2b_channel {
redis( redis(
host("redis-mailcow") host("valkey-mailcow")
persist-name("redis2") persist-name("valkey2")
port(6379) port(6379)
auth("`REDISPASS`") auth("`VALKEYPASS`")
command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
); );
}; };
log { log {
source(s_sogo); source(s_sogo);
destination(d_redis_ui_log); destination(d_valkey_ui_log);
destination(d_redis_f2b_channel); destination(d_valkey_f2b_channel);
}; };
log { log {
source(s_sogo); source(s_sogo);

View File

@@ -0,0 +1,8 @@
FROM python:3.13.2-alpine3.21
WORKDIR /app
COPY migrate.py /app/migrate.py
RUN pip install --no-cache-dir redis
CMD ["python", "/app/migrate.py"]

View File

@@ -0,0 +1,78 @@
import subprocess
import redis
import time
import os
# Container names
SOURCE_CONTAINER = "redis-old-mailcow"
DEST_CONTAINER = "valkey-mailcow"
VALKEYPASS = os.getenv("VALKEYPASS")
def migrate_redis():
src_redis = redis.StrictRedis(host=SOURCE_CONTAINER, port=6379, db=0, password=VALKEYPASS, decode_responses=False)
dest_redis = redis.StrictRedis(host=DEST_CONTAINER, port=6379, db=0, password=VALKEYPASS, decode_responses=False)
cursor = 0
batch_size = 100
migrated_count = 0
print("Starting migration...")
while True:
cursor, keys = src_redis.scan(cursor=cursor, match="*", count=batch_size)
keys_to_migrate = [key for key in keys if not key.startswith(b"PHPREDIS_SESSION:")]
for key in keys_to_migrate:
key_type = src_redis.type(key)
print(f"Import {key} of type {key_type}")
if key_type == b"string":
value = src_redis.get(key)
dest_redis.set(key, value)
elif key_type == b"hash":
value = src_redis.hgetall(key)
dest_redis.hset(key, mapping=value)
elif key_type == b"list":
value = src_redis.lrange(key, 0, -1)
for v in value:
dest_redis.rpush(key, v)
elif key_type == b"set":
value = src_redis.smembers(key)
for v in value:
dest_redis.sadd(key, v)
elif key_type == b"zset":
value = src_redis.zrange(key, 0, -1, withscores=True)
for v, score in value:
dest_redis.zadd(key, {v: score})
# Preserve TTL if exists
ttl = src_redis.ttl(key)
if ttl > 0:
dest_redis.expire(key, ttl)
migrated_count += 1
if cursor == 0:
break # No more keys to scan
print(f"Migration completed! {migrated_count} keys migrated.")
print("Forcing Valkey to save data...")
try:
dest_redis.save() # Immediate RDB save (blocking)
dest_redis.bgrewriteaof() # Rewrites the AOF file in the background
print("Data successfully saved to disk.")
except Exception as e:
print(f"Failed to save data: {e}")
# Main script execution
if __name__ == "__main__":
try:
migrate_redis()
finally:
pass

View File

@@ -16,7 +16,6 @@ RUN apk add --update \
fcgi \ fcgi \
openssl \ openssl \
nagios-plugins-mysql \ nagios-plugins-mysql \
nagios-plugins-dns \
nagios-plugins-disk \ nagios-plugins-disk \
bind-tools \ bind-tools \
redis \ redis \
@@ -32,9 +31,11 @@ RUN apk add --update \
tzdata \ tzdata \
whois \ whois \
&& curl https://raw.githubusercontent.com/mludvig/smtp-cli/v3.10/smtp-cli -o /smtp-cli \ && curl https://raw.githubusercontent.com/mludvig/smtp-cli/v3.10/smtp-cli -o /smtp-cli \
&& chmod +x smtp-cli && chmod +x smtp-cli \
&& mkdir /usr/lib/mailcow
COPY watchdog.sh /watchdog.sh COPY watchdog.sh /watchdog.sh
COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
COPY check_dns.sh /usr/lib/mailcow/check_dns.sh
CMD ["/watchdog.sh"] CMD ["/watchdog.sh"]

View File

@@ -0,0 +1,39 @@
#!/bin/sh
while getopts "H:s:" opt; do
case "$opt" in
H) HOST="$OPTARG" ;;
s) SERVER="$OPTARG" ;;
*) echo "Usage: $0 -H host -s server"; exit 3 ;;
esac
done
if [ -z "$SERVER" ]; then
echo "No DNS Server provided"
exit 3
fi
if [ -z "$HOST" ]; then
echo "No host to test provided"
exit 3
fi
# run dig and measure the time it takes to run
START_TIME=$(date +%s%3N)
dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null)
dig_rc=$?
dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -)
END_TIME=$(date +%s%3N)
ELAPSED_TIME=$((END_TIME - START_TIME))
# validate and perform nagios like output and exit codes
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
echo "Domain $HOST was not found by the server"
exit 2
elif [ $dig_rc -eq 0 ]; then
echo "DNS OK: $ELAPSED_TIME ms response time. $HOST returns $dig_output_ips"
exit 0
else
echo "Unknown error"
exit 3
fi

View File

@@ -1,5 +1,10 @@
#!/bin/bash #!/bin/bash
if [ "${DEV_MODE}" != "n" ]; then
echo -e "\e[31mEnabled Debug Mode\e[0m"
set -x
fi
trap "exit" INT TERM trap "exit" INT TERM
trap "kill 0" EXIT trap "kill 0" EXIT
@@ -39,18 +44,18 @@ while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u
done done
# Do not attempt to write to slave # Do not attempt to write to slave
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then
REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning"
else else
REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning"
fi fi
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..." echo "Waiting for Valkey..."
sleep 2 sleep 2
done done
${REDIS_CMDLINE} DEL F2B_RES > /dev/null ${VALKEY_CMDLINE} DEL F2B_RES > /dev/null
# Common functions # Common functions
get_ipv6(){ get_ipv6(){
@@ -85,15 +90,15 @@ progress() {
[[ ${CURRENT} -gt ${TOTAL} ]] && return [[ ${CURRENT} -gt ${TOTAL} ]] && return
[[ ${CURRENT} -lt 0 ]] && CURRENT=0 [[ ${CURRENT} -lt 0 ]] && CURRENT=0
PERCENT=$(( 200 * ${CURRENT} / ${TOTAL} % 2 + 100 * ${CURRENT} / ${TOTAL} )) PERCENT=$(( 200 * ${CURRENT} / ${TOTAL} % 2 + 100 * ${CURRENT} / ${TOTAL} ))
${REDIS_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"service\":\"${SERVICE}\",\"lvl\":\"${PERCENT}\",\"hpnow\":\"${CURRENT}\",\"hptotal\":\"${TOTAL}\",\"hpdiff\":\"${DIFF}\"}" > /dev/null ${VALKEY_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"service\":\"${SERVICE}\",\"lvl\":\"${PERCENT}\",\"hpnow\":\"${CURRENT}\",\"hptotal\":\"${TOTAL}\",\"hpdiff\":\"${DIFF}\"}" > /dev/null
log_msg "${SERVICE} health level: ${PERCENT}% (${CURRENT}/${TOTAL}), health trend: ${DIFF}" no_redis log_msg "${SERVICE} health level: ${PERCENT}% (${CURRENT}/${TOTAL}), health trend: ${DIFF}" no_valkey
# Return 10 to indicate a dead service # Return 10 to indicate a dead service
[ ${CURRENT} -le 0 ] && return 10 [ ${CURRENT} -le 0 ] && return 10
} }
log_msg() { log_msg() {
if [[ ${2} != "no_redis" ]]; then if [[ ${2} != "no_valkey" ]]; then
${REDIS_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \ ${VALKEY_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \
tr '\r\n%&;$"_[]{}-' ' ')\"}" > /dev/null tr '\r\n%&;$"_[]{}-' ' ')\"}" > /dev/null
fi fi
echo $(date) $(printf '%s\n' "${1}") echo $(date) $(printf '%s\n' "${1}")
@@ -109,10 +114,10 @@ function notify_error() {
# If exists, mail will be throttled by argument in seconds # If exists, mail will be throttled by argument in seconds
[[ ! -z ${3} ]] && THROTTLE=${3} [[ ! -z ${3} ]] && THROTTLE=${3}
if [[ ! -z ${THROTTLE} ]]; then if [[ ! -z ${THROTTLE} ]]; then
TTL_LEFT="$(${REDIS_CMDLINE} TTL THROTTLE_${1} 2> /dev/null)" TTL_LEFT="$(${VALKEY_CMDLINE} TTL THROTTLE_${1} 2> /dev/null)"
if [[ "${TTL_LEFT}" == "-2" ]]; then if [[ "${TTL_LEFT}" == "-2" ]]; then
# Delay key not found, setting a delay key now # Delay key not found, setting a delay key now
${REDIS_CMDLINE} SET THROTTLE_${1} 1 EX ${THROTTLE} ${VALKEY_CMDLINE} SET THROTTLE_${1} 1 EX ${THROTTLE}
else else
log_msg "Not sending notification email now, blocked for ${TTL_LEFT} seconds..." log_msg "Not sending notification email now, blocked for ${TTL_LEFT} seconds..."
return 1 return 1
@@ -297,7 +302,7 @@ unbound_checks() {
touch /tmp/unbound-mailcow; echo "$(tail -50 /tmp/unbound-mailcow)" > /tmp/unbound-mailcow touch /tmp/unbound-mailcow; echo "$(tail -50 /tmp/unbound-mailcow)" > /tmp/unbound-mailcow
host_ip=$(get_container_ip unbound-mailcow) host_ip=$(get_container_ip unbound-mailcow)
err_c_cur=${err_count} err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_dns -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? )) /usr/lib/mailcow/check_dns.sh -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
DNSSEC=$(dig com +dnssec | egrep 'flags:.+ad') DNSSEC=$(dig com +dnssec | egrep 'flags:.+ad')
if [[ -z ${DNSSEC} ]]; then if [[ -z ${DNSSEC} ]]; then
echo "DNSSEC failure" 2>> /tmp/unbound-mailcow 1>&2 echo "DNSSEC failure" 2>> /tmp/unbound-mailcow 1>&2
@@ -319,21 +324,21 @@ unbound_checks() {
return 1 return 1
} }
redis_checks() { valkey_checks() {
# A check for the local redis container # A check for the local valkey container
err_count=0 err_count=0
diff_c=0 diff_c=0
THRESHOLD=${REDIS_THRESHOLD} THRESHOLD=${VALKEY_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container # Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/redis-mailcow; echo "$(tail -50 /tmp/redis-mailcow)" > /tmp/redis-mailcow touch /tmp/valkey-mailcow; echo "$(tail -50 /tmp/valkey-mailcow)" > /tmp/valkey-mailcow
host_ip=$(get_container_ip redis-mailcow) host_ip=$(get_container_ip valkey-mailcow)
err_c_cur=${err_count} err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_tcp -4 -H redis-mailcow -p 6379 -E -s "AUTH ${REDISPASS}\nPING\n" -q "QUIT" -e "PONG" 2>> /tmp/redis-mailcow 1>&2; err_count=$(( ${err_count} + $? )) /usr/lib/nagios/plugins/check_tcp -4 -H valkey-mailcow -p 6379 -E -s "AUTH ${VALKEYPASS}\nPING\n" -q "QUIT" -e "PONG" 2>> /tmp/valkey-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Redis" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} progress "Valkey" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then if [[ $? == 10 ]]; then
diff_c=0 diff_c=0
sleep 1 sleep 1
@@ -445,6 +450,31 @@ postfix_checks() {
return 1 return 1
} }
postfix-tlspol_checks() {
err_count=0
diff_c=0
THRESHOLD=${POSTFIX_TLSPOL_THRESHOLD}
# Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do
touch /tmp/postfix-tlspol-mailcow; echo "$(tail -50 /tmp/postfix-tlspol-mailcow)" > /tmp/postfix-tlspol-mailcow
host_ip=$(get_container_ip postfix-tlspol-mailcow)
err_c_cur=${err_count}
/usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 8642 2>> /tmp/postfix-tlspol-mailcow 1>&2; err_count=$(( ${err_count} + $? ))
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
progress "Postfix TLS Policy companion" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then
diff_c=0
sleep 1
else
diff_c=0
sleep $(( ( RANDOM % 60 ) + 20 ))
fi
done
return 1
}
clamd_checks() { clamd_checks() {
err_count=0 err_count=0
diff_c=0 diff_c=0
@@ -503,12 +533,12 @@ dovecot_repl_checks() {
err_count=0 err_count=0
diff_c=0 diff_c=0
THRESHOLD=${DOVECOT_REPL_THRESHOLD} THRESHOLD=${DOVECOT_REPL_THRESHOLD}
D_REPL_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning -r GET DOVECOT_REPL_HEALTH) D_REPL_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning -r GET DOVECOT_REPL_HEALTH)
# Reduce error count by 2 after restarting an unhealthy container # Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count} err_c_cur=${err_count}
D_REPL_STATUS=$(redis-cli --raw -h redis -a ${REDISPASS} --no-auth-warning GET DOVECOT_REPL_HEALTH) D_REPL_STATUS=$(redis-cli --raw -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET DOVECOT_REPL_HEALTH)
if [[ "${D_REPL_STATUS}" != "1" ]]; then if [[ "${D_REPL_STATUS}" != "1" ]]; then
err_count=$(( ${err_count} + 1 )) err_count=$(( ${err_count} + 1 ))
fi fi
@@ -578,19 +608,19 @@ ratelimit_checks() {
err_count=0 err_count=0
diff_c=0 diff_c=0
THRESHOLD=${RATELIMIT_THRESHOLD} THRESHOLD=${RATELIMIT_THRESHOLD}
RL_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid) RL_LOG_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid)
# Reduce error count by 2 after restarting an unhealthy container # Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count} err_c_cur=${err_count}
RL_LOG_STATUS_PREV=${RL_LOG_STATUS} RL_LOG_STATUS_PREV=${RL_LOG_STATUS}
RL_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid) RL_LOG_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid)
if [[ ${RL_LOG_STATUS_PREV} != ${RL_LOG_STATUS} ]]; then if [[ ${RL_LOG_STATUS_PREV} != ${RL_LOG_STATUS} ]]; then
err_count=$(( ${err_count} + 1 )) err_count=$(( ${err_count} + 1 ))
echo 'Last 10 applied ratelimits (may overlap with previous reports).' > /tmp/ratelimit echo 'Last 10 applied ratelimits (may overlap with previous reports).' > /tmp/ratelimit
echo 'Full ratelimit buckets can be emptied by deleting the ratelimit hash from within mailcow UI (see /debug -> Protocols -> Ratelimit):' >> /tmp/ratelimit echo 'Full ratelimit buckets can be emptied by deleting the ratelimit hash from within mailcow UI (see /debug -> Protocols -> Ratelimit):' >> /tmp/ratelimit
echo >> /tmp/ratelimit echo >> /tmp/ratelimit
redis-cli --raw -h redis -a ${REDISPASS} --no-auth-warning LRANGE RL_LOG 0 10 | jq . >> /tmp/ratelimit redis-cli --raw -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning LRANGE RL_LOG 0 10 | jq . >> /tmp/ratelimit
fi fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
[ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} ))
@@ -639,20 +669,20 @@ fail2ban_checks() {
err_count=0 err_count=0
diff_c=0 diff_c=0
THRESHOLD=${FAIL2BAN_THRESHOLD} THRESHOLD=${FAIL2BAN_THRESHOLD}
F2B_LOG_STATUS=($(${REDIS_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS)) F2B_LOG_STATUS=($(${VALKEY_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS))
F2B_RES= F2B_RES=
# Reduce error count by 2 after restarting an unhealthy container # Reduce error count by 2 after restarting an unhealthy container
trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1
while [ ${err_count} -lt ${THRESHOLD} ]; do while [ ${err_count} -lt ${THRESHOLD} ]; do
err_c_cur=${err_count} err_c_cur=${err_count}
F2B_LOG_STATUS_PREV=(${F2B_LOG_STATUS[@]}) F2B_LOG_STATUS_PREV=(${F2B_LOG_STATUS[@]})
F2B_LOG_STATUS=($(${REDIS_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS)) F2B_LOG_STATUS=($(${VALKEY_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS))
array_diff F2B_RES F2B_LOG_STATUS F2B_LOG_STATUS_PREV array_diff F2B_RES F2B_LOG_STATUS F2B_LOG_STATUS_PREV
if [[ ! -z "${F2B_RES}" ]]; then if [[ ! -z "${F2B_RES}" ]]; then
err_count=$(( ${err_count} + 1 )) err_count=$(( ${err_count} + 1 ))
echo -n "${F2B_RES[@]}" | tr -cd "[a-fA-F0-9.:/] " | timeout 3s ${REDIS_CMDLINE} -x SET F2B_RES > /dev/null echo -n "${F2B_RES[@]}" | tr -cd "[a-fA-F0-9.:/] " | timeout 3s ${VALKEY_CMDLINE} -x SET F2B_RES > /dev/null
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
${REDIS_CMDLINE} -x DEL F2B_RES ${VALKEY_CMDLINE} -x DEL F2B_RES
fi fi
fi fi
[ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1
@@ -673,9 +703,9 @@ acme_checks() {
err_count=0 err_count=0
diff_c=0 diff_c=0
THRESHOLD=${ACME_THRESHOLD} THRESHOLD=${ACME_THRESHOLD}
ACME_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning GET ACME_FAIL_TIME) ACME_LOG_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET ACME_FAIL_TIME)
if [[ -z "${ACME_LOG_STATUS}" ]]; then if [[ -z "${ACME_LOG_STATUS}" ]]; then
${REDIS_CMDLINE} SET ACME_FAIL_TIME 0 ${VALKEY_CMDLINE} SET ACME_FAIL_TIME 0
ACME_LOG_STATUS=0 ACME_LOG_STATUS=0
fi fi
# Reduce error count by 2 after restarting an unhealthy container # Reduce error count by 2 after restarting an unhealthy container
@@ -685,7 +715,7 @@ acme_checks() {
ACME_LOG_STATUS_PREV=${ACME_LOG_STATUS} ACME_LOG_STATUS_PREV=${ACME_LOG_STATUS}
ACME_LC=0 ACME_LC=0
until [[ ! -z ${ACME_LOG_STATUS} ]] || [ ${ACME_LC} -ge 3 ]; do until [[ ! -z ${ACME_LOG_STATUS} ]] || [ ${ACME_LC} -ge 3 ]; do
ACME_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning GET ACME_FAIL_TIME 2> /dev/null) ACME_LOG_STATUS=$(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET ACME_FAIL_TIME 2> /dev/null)
sleep 3 sleep 3
ACME_LC=$((ACME_LC+1)) ACME_LC=$((ACME_LC+1))
done done
@@ -834,14 +864,14 @@ BACKGROUND_TASKS+=(${PID})
( (
while true; do while true; do
if ! redis_checks; then if ! valkey_checks; then
log_msg "Local Redis hit error limit" log_msg "Local Valkey hit error limit"
echo redis-mailcow > /tmp/com_pipe echo valkey-mailcow > /tmp/com_pipe
fi fi
done done
) & ) &
PID=$! PID=$!
echo "Spawned redis_checks with PID ${PID}" echo "Spawned valkey_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID}) BACKGROUND_TASKS+=(${PID})
( (
@@ -922,6 +952,18 @@ PID=$!
echo "Spawned mailq_checks with PID ${PID}" echo "Spawned mailq_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID}) BACKGROUND_TASKS+=(${PID})
(
while true; do
if ! postfix-tlspol_checks; then
log_msg "Postfix TLS Policy hit error limit"
echo postfix-tlspol-mailcow > /tmp/com_pipe
fi
done
) &
PID=$!
echo "Spawned postfix-tlspol_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID})
( (
while true; do while true; do
if ! dovecot_checks; then if ! dovecot_checks; then
@@ -1087,9 +1129,9 @@ while true; do
# Define $2 to override message text, else print service was restarted at ... # Define $2 to override message text, else print service was restarted at ...
notify_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 ${VALKEY_CMDLINE} --raw GET F2B_RES 2> /dev/null))
if [[ ! -z "${F2B_RES}" ]]; then if [[ ! -z "${F2B_RES}" ]]; then
${REDIS_CMDLINE} DEL F2B_RES > /dev/null ${VALKEY_CMDLINE} DEL F2B_RES > /dev/null
host= host=
for host in "${F2B_RES[@]}"; do for host in "${F2B_RES[@]}"; do
log_msg "Banned ${host}" log_msg "Banned ${host}"

View File

@@ -23,16 +23,16 @@ if (file_exists('../../../web/inc/vars.local.inc.php')) {
require_once '../../../web/inc/lib/vendor/autoload.php'; require_once '../../../web/inc/lib/vendor/autoload.php';
// Init Redis // Init Valkey
$redis = new Redis(); $valkey = new Redis();
try { try {
if (!empty(getenv('REDIS_SLAVEOF_IP'))) { if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); $valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
} }
else { else {
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
} }
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
} }
catch (Exception $e) { catch (Exception $e) {
error_log("MAILCOWAUTH: " . $e . PHP_EOL); error_log("MAILCOWAUTH: " . $e . PHP_EOL);
@@ -86,7 +86,7 @@ if ($result === false){
'remote_addr' => $post['real_rip'] 'remote_addr' => $post['real_rip']
)); ));
if ($result) { if ($result) {
error_log('MAILCOWAUTH: App auth for user ' . $post['username']); error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
set_sasl_log($post['username'], $post['real_rip'], $post['service']); set_sasl_log($post['username'], $post['real_rip'], $post['service']);
} }
} }
@@ -94,9 +94,9 @@ if ($result === false){
// Init Identity Provider // Init Identity Provider
$iam_provider = identity_provider('init'); $iam_provider = identity_provider('init');
$iam_settings = identity_provider('get'); $iam_settings = identity_provider('get');
$result = user_login($post['username'], $post['password'], array('is_internal' => true)); $result = user_login($post['username'], $post['password'], array('is_internal' => true, 'service' => $post['service']));
if ($result) { if ($result) {
error_log('MAILCOWAUTH: User auth for user ' . $post['username']); error_log('MAILCOWAUTH: User auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
set_sasl_log($post['username'], $post['real_rip'], $post['service']); set_sasl_log($post['username'], $post['real_rip'], $post['service']);
} }
} }
@@ -105,7 +105,7 @@ if ($result) {
http_response_code(200); // OK http_response_code(200); // OK
$return['success'] = true; $return['success'] = true;
} else { } else {
error_log("MAILCOWAUTH: Login failed for user " . $post['username']); error_log("MAILCOWAUTH: Login failed for user " . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
http_response_code(401); // Unauthorized http_response_code(401); // Unauthorized
} }

View File

@@ -48,13 +48,21 @@ http {
listen {{ HTTP_PORT }} default_server; listen {{ HTTP_PORT }} default_server;
listen [::]:{{ HTTP_PORT }} default_server; listen [::]:{{ HTTP_PORT }} default_server;
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }}; server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
if ( $request_uri ~* "%0A|%0D" ) { return 403; } if ( $request_uri ~* "%0A|%0D" ) { return 403; }
location ^~ /.well-known/acme-challenge/ { location ^~ /.well-known/acme-challenge/ {
allow all; allow all;
default_type "text/plain"; default_type "text/plain";
} }
location ^~ /.well-known/mta-sts.txt {
allow all;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass {{ PHPFPMHOST }}:9002;
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/mta-sts.php;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location / { location / {
return 301 https://$host$uri$is_args$args; return 301 https://$host$uri$is_args$args;
} }
@@ -70,7 +78,7 @@ http {
{%endif%} {%endif%}
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
{% if not DISABLE_IPv6 %} {% if ENABLE_IPV6 %}
{% if not HTTP_REDIRECT %} {% if not HTTP_REDIRECT %}
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
{%endif%} {%endif%}
@@ -82,7 +90,7 @@ http {
ssl_certificate /etc/ssl/mail/cert.pem; ssl_certificate /etc/ssl/mail/cert.pem;
ssl_certificate_key /etc/ssl/mail/key.pem; ssl_certificate_key /etc/ssl/mail/key.pem;
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.*; server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.*;
include /etc/nginx/includes/sites-default.conf; include /etc/nginx/includes/sites-default.conf;
} }
@@ -97,7 +105,7 @@ http {
{%endif%} {%endif%}
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
{% if not DISABLE_IPv6 %} {% if ENABLE_IPV6 %}
{% if not HTTP_REDIRECT %} {% if not HTTP_REDIRECT %}
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
{%endif%} {%endif%}
@@ -118,7 +126,7 @@ http {
# rspamd dynmaps: # rspamd dynmaps:
server { server {
listen 8081; listen 8081;
{% if not DISABLE_IPv6 %} {% if ENABLE_IPV6 %}
listen [::]:8081; listen [::]:8081;
{%endif%} {%endif%}
index index.php index.html; index index.php index.html;
@@ -191,7 +199,7 @@ http {
{%endif%} {%endif%}
listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl;
{% if not DISABLE_IPv6 %} {% if ENABLE_IPV6 %}
{% if not HTTP_REDIRECT %} {% if not HTTP_REDIRECT %}
listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%};
{%endif%} {%endif%}

View File

@@ -76,6 +76,14 @@ location ^~ /.well-known/acme-challenge/ {
allow all; allow all;
default_type "text/plain"; default_type "text/plain";
} }
location ^~ /.well-known/mta-sts.txt {
allow all;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass {{ PHPFPMHOST }}:9002;
include /etc/nginx/fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/mta-sts.php;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
rewrite ^/.well-known/caldav$ /SOGo/dav/ permanent; rewrite ^/.well-known/caldav$ /SOGo/dav/ permanent;
rewrite ^/.well-known/carddav$ /SOGo/dav/ permanent; rewrite ^/.well-known/carddav$ /SOGo/dav/ permanent;

View File

@@ -23,16 +23,16 @@ catch (PDOException $e) {
exit; exit;
} }
// Init Redis // Init Valkey
$redis = new Redis(); $valkey = new Redis();
try { try {
if (!empty(getenv('REDIS_SLAVEOF_IP'))) { if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); $valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
} }
else { else {
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
} }
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
} }
catch (Exception $e) { catch (Exception $e) {
echo "Exiting: " . $e->getMessage(); echo "Exiting: " . $e->getMessage();
@@ -41,7 +41,7 @@ catch (Exception $e) {
} }
function logMsg($priority, $message, $task = "Keycloak Sync") { function logMsg($priority, $message, $task = "Keycloak Sync") {
global $redis; global $valkey;
$finalMsg = array( $finalMsg = array(
"time" => time(), "time" => time(),
@@ -49,7 +49,7 @@ function logMsg($priority, $message, $task = "Keycloak Sync") {
"task" => $task, "task" => $task,
"message" => $message "message" => $message
); );
$redis->lPush('CRON_LOG', json_encode($finalMsg)); $valkey->lPush('CRON_LOG', json_encode($finalMsg));
} }
// Load core functions first // Load core functions first

View File

@@ -23,16 +23,16 @@ catch (PDOException $e) {
exit; exit;
} }
// Init Redis // Init Valkey
$redis = new Redis(); $valkey = new Redis();
try { try {
if (!empty(getenv('REDIS_SLAVEOF_IP'))) { if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); $valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
} }
else { else {
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
} }
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
} }
catch (Exception $e) { catch (Exception $e) {
echo "Exiting: " . $e->getMessage(); echo "Exiting: " . $e->getMessage();
@@ -41,7 +41,7 @@ catch (Exception $e) {
} }
function logMsg($priority, $message, $task = "LDAP Sync") { function logMsg($priority, $message, $task = "LDAP Sync") {
global $redis; global $valkey;
$finalMsg = array( $finalMsg = array(
"time" => time(), "time" => time(),
@@ -49,7 +49,7 @@ function logMsg($priority, $message, $task = "LDAP Sync") {
"task" => $task, "task" => $task,
"message" => $message "message" => $message
); );
$redis->lPush('CRON_LOG', json_encode($finalMsg)); $valkey->lPush('CRON_LOG', json_encode($finalMsg));
} }
// Load core functions first // Load core functions first

View File

@@ -152,7 +152,7 @@ smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf smtp_sasl_password_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_sasl_passwd_maps_sender_dependent.cf
smtp_sasl_security_options = smtp_sasl_security_options =
smtp_sasl_mechanism_filter = plain, login smtp_sasl_mechanism_filter = plain, login
smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf socketmap:inet:postfix-tlspol:8642:QUERY
smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre smtp_header_checks = pcre:/opt/postfix/conf/anonymize_headers.pcre
mail_name = Postcow mail_name = Postcow
# local_transport map catches local destinations and prevents routing local dests when the next map would route "*" # local_transport map catches local destinations and prevents routing local dests when the next map would route "*"

View File

@@ -1,13 +1,26 @@
# Whitelist generated by Postwhite v3.4 on Thu May 1 00:21:10 UTC 2025 # Whitelist generated by Postwhite v3.4 on Wed Oct 1 00:21:33 UTC 2025
# https://github.com/stevejenkins/postwhite/ # https://github.com/stevejenkins/postwhite/
# 2058 total rules # 2216 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:8000::/50 permit 2a01:111:f403:2800::/53 permit
2a01:111:f403:8000::/51 permit 2a01:111:f403:8000::/51 permit
2a01:111:f403::/49 permit 2a01:111:f403::/49 permit
2a01:111:f403:c000::/51 permit 2a01:111:f403:c000::/51 permit
2a01:111:f403:d000::/53 permit
2a01:111:f403:f000::/52 permit 2a01:111:f403:f000::/52 permit
2a01:238:20a:202:5370::1 permit
2a01:238:20a:202:5372::1 permit
2a01:238:20a:202:5373::1 permit
2a01:238:400:101:53::1 permit
2a01:238:400:102:53::1 permit
2a01:238:400:103:53::1 permit
2a01:238:400:301:53::1 permit
2a01:238:400:302:53::1 permit
2a01:238:400:303:53::1 permit
2a01:238:400:470:53::1 permit
2a01:238:400:471:53::1 permit
2a01:238:400:472:53::1 permit
2a01:b747:3000:200::/56 permit 2a01:b747:3000:200::/56 permit
2a01:b747:3001:200::/56 permit 2a01:b747:3001:200::/56 permit
2a01:b747:3002:200::/56 permit 2a01:b747:3002:200::/56 permit
@@ -17,26 +30,42 @@
2a01:b747:3006:200::/56 permit 2a01:b747:3006:200::/56 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
2.207.217.30 permit 2.207.217.30 permit
3.64.237.68 permit
3.65.3.180 permit
3.70.123.177 permit 3.70.123.177 permit
3.72.182.33 permit
3.74.81.189 permit
3.74.125.228 permit
3.75.33.185 permit
3.93.157.0/24 permit 3.93.157.0/24 permit
3.94.40.108 permit 3.94.40.108 permit
3.121.107.214 permit
3.129.120.190 permit 3.129.120.190 permit
3.210.190.0/24 permit 3.210.190.0/24 permit
3.211.80.218 permit
3.216.221.67 permit
3.221.209.22 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
8.36.116.0/24 permit 8.36.116.0/24 permit
8.39.54.0/23 permit
8.39.54.250/31 permit
8.39.144.0/24 permit 8.39.144.0/24 permit
8.40.222.0/23 permit
8.40.222.250/31 permit
12.130.86.238 permit 12.130.86.238 permit
13.107.246.59 permit 13.107.213.41 permit
13.107.246.41 permit
13.110.208.0/21 permit 13.110.208.0/21 permit
13.110.209.0/24 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
13.111.191.0/24 permit 13.111.191.0/24 permit
13.216.7.111 permit
13.216.54.180 permit
15.200.21.50 permit 15.200.21.50 permit
15.200.44.248 permit 15.200.44.248 permit
15.200.201.185 permit 15.200.201.185 permit
@@ -49,16 +78,21 @@
18.97.1.184/29 permit 18.97.1.184/29 permit
18.97.2.64/26 permit 18.97.2.64/26 permit
18.156.89.250 permit 18.156.89.250 permit
18.156.205.64 permit
18.157.70.148 permit
18.157.114.255 permit
18.157.243.190 permit 18.157.243.190 permit
18.158.153.154 permit
18.194.95.56 permit 18.194.95.56 permit
18.197.217.180 permit
18.198.96.88 permit 18.198.96.88 permit
18.199.210.3 permit
18.207.52.234 permit
18.208.124.128/25 permit 18.208.124.128/25 permit
18.216.232.154 permit 18.216.232.154 permit
18.235.27.253 permit 18.235.27.253 permit
18.236.40.242 permit 18.236.40.242 permit
18.236.56.161 permit
20.51.6.32/30 permit 20.51.6.32/30 permit
20.51.98.61 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.59.80.4/30 permit
@@ -88,6 +122,7 @@
23.253.183.147 permit 23.253.183.147 permit
23.253.183.148 permit 23.253.183.148 permit
23.253.183.150 permit 23.253.183.150 permit
24.110.64.0/18 permit
27.123.204.128/30 permit 27.123.204.128/30 permit
27.123.204.132/31 permit 27.123.204.132/31 permit
27.123.204.148/30 permit 27.123.204.148/30 permit
@@ -100,7 +135,6 @@
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
31.25.48.222 permit
31.47.251.17 permit 31.47.251.17 permit
31.186.239.0/24 permit 31.186.239.0/24 permit
34.2.64.0/22 permit 34.2.64.0/22 permit
@@ -125,16 +159,25 @@
34.74.74.140 permit 34.74.74.140 permit
34.83.159.189 permit 34.83.159.189 permit
34.141.160.224 permit 34.141.160.224 permit
34.193.58.168 permit
34.195.217.107 permit 34.195.217.107 permit
34.197.10.50 permit
34.197.254.9 permit
34.198.94.229 permit
34.198.218.121 permit
34.212.163.75 permit 34.212.163.75 permit
34.215.104.144 permit 34.215.104.144 permit
34.218.116.3 permit 34.218.115.239 permit
34.225.212.172 permit 34.225.212.172 permit
34.241.242.183 permit
35.83.148.184 permit
35.155.198.111 permit
35.158.23.94 permit
35.161.32.253 permit 35.161.32.253 permit
35.162.73.231 permit
35.167.93.243 permit 35.167.93.243 permit
35.174.145.124 permit
35.176.132.251 permit 35.176.132.251 permit
35.190.247.0/24 permit
35.191.0.0/16 permit
35.205.92.9 permit 35.205.92.9 permit
35.228.216.85 permit 35.228.216.85 permit
35.242.169.159 permit 35.242.169.159 permit
@@ -150,12 +193,21 @@
40.233.64.216 permit 40.233.64.216 permit
40.233.83.78 permit 40.233.83.78 permit
40.233.88.28 permit 40.233.88.28 permit
43.239.212.33 permit
44.206.138.57 permit 44.206.138.57 permit
44.210.169.44 permit
44.217.45.156 permit 44.217.45.156 permit
44.236.56.93 permit 44.236.56.93 permit
44.238.220.251 permit 44.238.220.251 permit
44.245.243.92 permit
44.246.1.125 permit
44.246.68.102 permit
44.246.77.92 permit
45.14.148.0/22 permit 45.14.148.0/22 permit
46.19.170.16 permit 45.143.132.0/24 permit
45.143.133.0/24 permit
45.143.134.0/24 permit
45.143.135.0/24 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
@@ -206,6 +258,7 @@
46.243.88.177 permit 46.243.88.177 permit
46.243.95.179 permit 46.243.95.179 permit
46.243.95.180 permit 46.243.95.180 permit
50.16.246.183 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
@@ -219,14 +272,24 @@
50.56.130.220 permit 50.56.130.220 permit
50.56.130.221 permit 50.56.130.221 permit
50.56.130.222 permit 50.56.130.222 permit
50.112.246.219 permit
52.1.14.157 permit 52.1.14.157 permit
52.5.230.59 permit 52.5.230.59 permit
52.6.74.205 permit
52.12.53.23 permit
52.13.214.179 permit
52.26.1.71 permit
52.27.5.72 permit 52.27.5.72 permit
52.27.28.47 permit 52.27.28.47 permit
52.28.63.81 permit 52.28.63.81 permit
52.28.197.132 permit
52.34.181.151 permit
52.35.192.45 permit
52.36.138.31 permit 52.36.138.31 permit
52.37.142.146 permit 52.37.142.146 permit
52.42.203.116 permit
52.50.24.208 permit 52.50.24.208 permit
52.57.120.243 permit
52.58.216.183 permit 52.58.216.183 permit
52.59.143.3 permit 52.59.143.3 permit
52.60.41.5 permit 52.60.41.5 permit
@@ -269,23 +332,24 @@
54.174.63.0/24 permit 54.174.63.0/24 permit
54.186.193.102 permit 54.186.193.102 permit
54.191.223.56 permit 54.191.223.56 permit
54.211.126.101 permit
54.213.20.246 permit 54.213.20.246 permit
54.214.39.184 permit 54.214.39.184 permit
54.240.0.0/18 permit 54.240.0.0/18 permit
54.240.64.0/19 permit 54.240.64.0/18 permit
54.240.96.0/19 permit
54.241.16.209 permit 54.241.16.209 permit
54.244.54.130 permit 54.244.54.130 permit
54.244.242.0/24 permit 54.244.242.0/24 permit
54.255.61.23 permit 54.255.61.23 permit
56.124.6.228 permit
57.103.64.0/18 permit 57.103.64.0/18 permit
57.129.93.249 permit
62.13.128.0/24 permit 62.13.128.0/24 permit
62.13.129.128/25 permit 62.13.129.128/25 permit
62.13.136.0/21 permit 62.13.136.0/21 permit
62.13.144.0/21 permit 62.13.144.0/21 permit
62.13.152.0/21 permit 62.13.152.0/21 permit
62.17.146.128/26 permit 62.17.146.128/26 permit
62.179.121.0/24 permit
62.201.172.0/27 permit 62.201.172.0/27 permit
62.201.172.32/27 permit 62.201.172.32/27 permit
62.253.227.114 permit 62.253.227.114 permit
@@ -293,6 +357,9 @@
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
63.176.194.123 permit
63.178.132.221 permit
63.178.143.178 permit
64.18.0.0/20 permit 64.18.0.0/20 permit
64.20.241.45 permit 64.20.241.45 permit
64.69.212.0/24 permit 64.69.212.0/24 permit
@@ -305,6 +372,7 @@
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.181.194.190 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
@@ -358,10 +426,10 @@
65.110.161.77 permit 65.110.161.77 permit
65.123.29.213 permit 65.123.29.213 permit
65.123.29.220 permit 65.123.29.220 permit
65.154.166.0/24 permit
65.212.180.36 permit 65.212.180.36 permit
66.102.0.0/20 permit 66.102.0.0/20 permit
66.119.150.192/26 permit 66.119.150.192/26 permit
66.162.193.226/31 permit
66.163.184.0/24 permit 66.163.184.0/24 permit
66.163.185.0/24 permit 66.163.185.0/24 permit
66.163.186.0/24 permit 66.163.186.0/24 permit
@@ -567,7 +635,6 @@
74.86.241.250/31 permit 74.86.241.250/31 permit
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.208.4.200 permit 74.208.4.200 permit
74.208.4.201 permit 74.208.4.201 permit
74.208.4.220 permit 74.208.4.220 permit
@@ -596,6 +663,11 @@
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
79.135.106.0/24 permit
79.135.107.0/24 permit
81.169.146.243 permit
81.169.146.245 permit
81.169.146.246 permit
81.223.46.0/27 permit 81.223.46.0/27 permit
82.165.159.2 permit 82.165.159.2 permit
82.165.159.3 permit 82.165.159.3 permit
@@ -611,10 +683,17 @@
82.165.159.45 permit 82.165.159.45 permit
82.165.159.130 permit 82.165.159.130 permit
82.165.159.131 permit 82.165.159.131 permit
84.116.6.0/23 permit 85.9.206.169 permit
84.116.36.0/24 permit 85.9.210.45 permit
84.116.50.0/23 permit
85.158.136.0/21 permit 85.158.136.0/21 permit
85.215.255.39 permit
85.215.255.40 permit
85.215.255.41 permit
85.215.255.45 permit
85.215.255.46 permit
85.215.255.47 permit
85.215.255.48 permit
85.215.255.49 permit
86.61.88.25 permit 86.61.88.25 permit
87.238.80.0/21 permit 87.238.80.0/21 permit
87.248.103.12 permit 87.248.103.12 permit
@@ -654,12 +733,13 @@
87.248.117.205 permit 87.248.117.205 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.198.2.0/24 permit
91.211.240.0/22 permit 91.211.240.0/22 permit
94.169.2.0/23 permit
94.236.119.0/26 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
95.217.114.154 permit
96.43.144.0/20 permit 96.43.144.0/20 permit
96.43.144.64/28 permit 96.43.144.64/28 permit
96.43.144.64/31 permit 96.43.144.64/31 permit
@@ -1153,15 +1233,14 @@
99.83.190.102 permit 99.83.190.102 permit
103.9.96.0/22 permit 103.9.96.0/22 permit
103.28.42.0/24 permit 103.28.42.0/24 permit
103.84.217.238 permit
103.89.75.238 permit
103.151.192.0/23 permit 103.151.192.0/23 permit
103.168.172.128/27 permit 103.168.172.128/27 permit
103.237.104.0/22 permit 103.237.104.0/22 permit
104.43.243.237 permit 104.43.243.237 permit
104.44.112.128/25 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
106.10.144.64/27 permit 106.10.144.64/27 permit
@@ -1287,6 +1366,7 @@
106.50.16.0/28 permit 106.50.16.0/28 permit
107.20.18.111 permit 107.20.18.111 permit
107.20.210.250 permit 107.20.210.250 permit
107.22.191.150 permit
108.174.0.0/24 permit 108.174.0.0/24 permit
108.174.0.215 permit 108.174.0.215 permit
108.174.3.0/24 permit 108.174.3.0/24 permit
@@ -1295,9 +1375,8 @@
108.174.6.215 permit 108.174.6.215 permit
108.175.18.45 permit 108.175.18.45 permit
108.175.30.45 permit 108.175.30.45 permit
108.177.8.0/21 permit
108.177.96.0/19 permit
108.179.144.0/20 permit 108.179.144.0/20 permit
109.224.244.0/24 permit
109.237.142.0/24 permit 109.237.142.0/24 permit
111.221.23.128/25 permit 111.221.23.128/25 permit
111.221.26.0/27 permit 111.221.26.0/27 permit
@@ -1321,6 +1400,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
121.244.91.52 permit
122.15.156.182 permit
123.126.78.64/29 permit 123.126.78.64/29 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
@@ -1369,7 +1451,6 @@
129.213.195.191 permit 129.213.195.191 permit
130.61.9.72 permit 130.61.9.72 permit
130.162.39.83 permit 130.162.39.83 permit
130.211.0.0/22 permit
130.248.172.0/24 permit 130.248.172.0/24 permit
130.248.173.0/24 permit 130.248.173.0/24 permit
131.253.30.0/24 permit 131.253.30.0/24 permit
@@ -1378,12 +1459,28 @@
132.226.26.225 permit 132.226.26.225 permit
132.226.49.32 permit 132.226.49.32 permit
132.226.56.24 permit 132.226.56.24 permit
134.128.64.0/19 permit
134.128.96.0/19 permit
134.170.27.8 permit 134.170.27.8 permit
134.170.113.0/26 permit 134.170.113.0/26 permit
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.0/24 permit
135.84.81.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.162.0/24 permit
136.143.176.0/24 permit
136.143.177.0/24 permit
136.143.178.49 permit
136.143.182.0/23 permit
136.143.184.0/24 permit
136.143.188.0/24 permit
136.143.190.0/23 permit
136.147.128.0/20 permit 136.147.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
@@ -1398,6 +1495,7 @@
139.138.46.219 permit 139.138.46.219 permit
139.138.57.55 permit 139.138.57.55 permit
139.138.58.119 permit 139.138.58.119 permit
139.167.79.86 permit
139.180.17.0/24 permit 139.180.17.0/24 permit
140.238.148.191 permit 140.238.148.191 permit
141.148.159.229 permit 141.148.159.229 permit
@@ -1442,7 +1540,7 @@
148.105.0.0/16 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.223.204 permit 149.72.234.184 permit
149.72.248.236 permit 149.72.248.236 permit
149.97.173.180 permit 149.97.173.180 permit
150.230.98.160 permit 150.230.98.160 permit
@@ -1498,6 +1596,7 @@
159.183.0.0/16 permit 159.183.0.0/16 permit
159.183.68.71 permit 159.183.68.71 permit
159.183.79.38 permit 159.183.79.38 permit
159.183.129.172 permit
160.1.62.192 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
@@ -1515,9 +1614,13 @@
163.114.134.16 permit 163.114.134.16 permit
163.114.135.16 permit 163.114.135.16 permit
163.116.128.0/17 permit 163.116.128.0/17 permit
163.192.116.87 permit
164.152.23.32 permit 164.152.23.32 permit
164.152.25.241 permit 164.152.25.241 permit
164.177.132.168/30 permit 164.177.132.168/30 permit
165.173.128.0/24 permit
165.173.180.250/31 permit
165.173.182.250/31 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.169 permit 166.78.69.169 permit
@@ -1542,20 +1645,28 @@
168.138.5.36 permit 168.138.5.36 permit
168.138.73.51 permit 168.138.73.51 permit
168.138.77.31 permit 168.138.77.31 permit
168.138.237.153 permit
168.245.0.0/17 permit 168.245.0.0/17 permit
168.245.12.252 permit 168.245.12.252 permit
168.245.46.9 permit 168.245.46.9 permit
168.245.127.231 permit 168.245.127.231 permit
169.148.129.0/24 permit
169.148.131.0/24 permit
169.148.138.0/24 permit
169.148.142.10 permit
169.148.142.33 permit
169.148.144.0/25 permit
169.148.144.10 permit
169.148.146.0/23 permit
169.148.175.3 permit
169.148.188.0/24 permit
169.148.188.182 permit
170.10.128.0/24 permit 170.10.128.0/24 permit
170.10.129.0/24 permit 170.10.129.0/24 permit
170.10.132.56/29 permit 170.10.132.56/29 permit
170.10.132.64/29 permit 170.10.132.64/29 permit
170.10.133.0/24 permit 170.10.133.0/24 permit
172.217.0.0/19 permit
172.217.32.0/20 permit 172.217.32.0/20 permit
172.217.128.0/19 permit
172.217.160.0/20 permit
172.217.192.0/19 permit
172.253.56.0/21 permit 172.253.56.0/21 permit
172.253.112.0/20 permit 172.253.112.0/20 permit
173.0.84.0/29 permit 173.0.84.0/29 permit
@@ -1585,9 +1696,14 @@
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/22 permit 185.4.120.0/22 permit
185.11.253.128/27 permit
185.11.255.0/24 permit
185.12.80.0/22 permit 185.12.80.0/22 permit
185.28.196.0/22 permit 185.28.196.0/22 permit
185.58.84.93 permit 185.58.84.93 permit
185.70.40.0/24 permit
185.70.41.0/24 permit
185.70.43.0/24 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
@@ -1595,6 +1711,8 @@
185.138.56.128/25 permit 185.138.56.128/25 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.233.188.0/23 permit
185.233.190.0/23 permit
185.250.236.0/22 permit 185.250.236.0/22 permit
185.250.239.148 permit 185.250.239.148 permit
185.250.239.168 permit 185.250.239.168 permit
@@ -1647,6 +1765,7 @@
188.125.85.234/31 permit 188.125.85.234/31 permit
188.125.85.236/31 permit 188.125.85.236/31 permit
188.125.85.238 permit 188.125.85.238 permit
188.165.51.139 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.18.139.154 permit 192.18.139.154 permit
@@ -1669,7 +1788,12 @@
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 193.123.56.63 permit
193.142.157.0/24 permit
193.142.157.191 permit
193.142.157.198 permit
194.19.134.0/25 permit 194.19.134.0/25 permit
194.25.134.16/28 permit
194.25.134.80/28 permit
194.64.234.129 permit 194.64.234.129 permit
194.97.196.0/24 permit 194.97.196.0/24 permit
194.97.196.3 permit 194.97.196.3 permit
@@ -1722,7 +1846,16 @@
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.80.20 permit
199.67.82.2 permit
199.67.82.20 permit
199.67.84.0/24 permit
199.67.86.0/24 permit
199.67.88.0/24 permit
199.67.90.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
@@ -1779,9 +1912,13 @@
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.141.32.0/23 permit
204.141.42.0/23 permit
204.216.164.202 permit
204.220.160.0/21 permit 204.220.160.0/21 permit
204.220.168.0/21 permit 204.220.168.0/21 permit
204.220.176.0/20 permit 204.220.176.0/20 permit
204.220.181.105 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
@@ -1856,8 +1993,6 @@
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.5 permit
208.74.204.9 permit
208.75.120.0/22 permit 208.75.120.0/22 permit
208.76.62.0/24 permit 208.76.62.0/24 permit
208.76.63.0/24 permit 208.76.63.0/24 permit
@@ -1921,6 +2056,8 @@
212.227.15.4 permit 212.227.15.4 permit
212.227.15.5 permit 212.227.15.5 permit
212.227.15.6 permit 212.227.15.6 permit
212.227.15.7 permit
212.227.15.8 permit
212.227.15.14 permit 212.227.15.14 permit
212.227.15.15 permit 212.227.15.15 permit
212.227.15.18 permit 212.227.15.18 permit
@@ -1937,21 +2074,36 @@
212.227.15.53 permit 212.227.15.53 permit
212.227.15.54 permit 212.227.15.54 permit
212.227.15.55 permit 212.227.15.55 permit
212.227.17.1 permit
212.227.17.2 permit
212.227.17.7 permit
212.227.17.11 permit 212.227.17.11 permit
212.227.17.12 permit 212.227.17.12 permit
212.227.17.16 permit
212.227.17.17 permit
212.227.17.18 permit 212.227.17.18 permit
212.227.17.19 permit 212.227.17.19 permit
212.227.17.20 permit 212.227.17.20 permit
212.227.17.21 permit 212.227.17.21 permit
212.227.17.22 permit 212.227.17.22 permit
212.227.17.26 permit 212.227.17.26 permit
212.227.17.27 permit
212.227.17.28 permit 212.227.17.28 permit
212.227.17.29 permit 212.227.17.29 permit
212.227.126.206 permit
212.227.126.207 permit
212.227.126.208 permit
212.227.126.209 permit
212.227.126.220 permit
212.227.126.221 permit
212.227.126.222 permit
212.227.126.223 permit
212.227.126.224 permit 212.227.126.224 permit
212.227.126.225 permit 212.227.126.225 permit
212.227.126.226 permit 212.227.126.226 permit
212.227.126.227 permit 212.227.126.227 permit
213.46.255.0/24 permit 213.95.19.64/27 permit
213.95.135.4 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
@@ -1961,6 +2113,7 @@
216.17.150.242 permit 216.17.150.242 permit
216.17.150.251 permit 216.17.150.251 permit
216.24.224.0/20 permit 216.24.224.0/20 permit
216.27.86.152/31 permit
216.39.60.154/31 permit 216.39.60.154/31 permit
216.39.60.156/30 permit 216.39.60.156/30 permit
216.39.60.160/30 permit 216.39.60.160/30 permit
@@ -1998,6 +2151,8 @@
216.99.5.68 permit 216.99.5.68 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.162.65 permit
216.113.163.65 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
@@ -2046,6 +2201,9 @@
2603:1030:20e:3::23c permit 2603:1030:20e:3::23c permit
2603:1030:b:3::152 permit 2603:1030:b:3::152 permit
2603:1030:c02:8::14 permit 2603:1030:c02:8::14 permit
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
2607:f8b0:4000::/36 permit 2607:f8b0:4000::/36 permit
2620:109:c003:104::/64 permit 2620:109:c003:104::/64 permit
2620:109:c003:104::215 permit 2620:109:c003:104::215 permit
@@ -2059,4 +2217,5 @@
2620:119:50c0:207::/64 permit 2620:119:50c0:207::/64 permit
2620:119:50c0:207::215 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 49.12.4.251 permit # checks.mailcow.email
2a01:4f8:c17:7906::10 permit # checks.mailcow.email

View File

@@ -1,12 +0,0 @@
#!/bin/sh
cat <<EOF > /redis.conf
requirepass $REDISPASS
user quota_notify on nopass ~QW_* -@all +get +hget +ping
EOF
if [ -n "$REDISMASTERPASS" ]; then
echo "masterauth $REDISMASTERPASS" >> /redis.conf
fi
exec redis-server /redis.conf

View File

@@ -22,10 +22,10 @@ catch (PDOException $e) {
exit; exit;
} }
// Init Redis // Init Valkey
$redis = new Redis(); $valkey = new Redis();
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
function parse_email($email) { function parse_email($email) {
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false; if(!filter_var($email, FILTER_VALIDATE_EMAIL)) return false;
@@ -60,7 +60,7 @@ $rcpt_final_mailboxes = array();
// Skip if not a mailcow handled domain // Skip if not a mailcow handled domain
try { try {
if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { if (!$valkey->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
exit; exit;
} }
} }
@@ -122,7 +122,7 @@ try {
} }
else { else {
$parsed_goto = parse_email($goto); $parsed_goto = parse_email($goto);
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { if (!$valkey->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
error_log("ALIAS EXPANDER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL); error_log("ALIAS EXPANDER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
} }
else { else {
@@ -133,7 +133,7 @@ try {
error_log("ALIAS EXPANDER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL); error_log("ALIAS EXPANDER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = explode(',', $goto_branch); $goto_branch_array = explode(',', $goto_branch);
} else { } else {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'"); $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
$stmt->execute(array(':domain' => $parsed_goto['domain'])); $stmt->execute(array(':domain' => $parsed_goto['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) { if ($goto_branch) {

View File

@@ -2,9 +2,9 @@
header('Content-Type: text/plain'); header('Content-Type: text/plain');
ini_set('error_reporting', 0); ini_set('error_reporting', 0);
$redis = new Redis(); $valkey = new Redis();
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
function in_net($addr, $net) { function in_net($addr, $net) {
$net = explode('/', $net); $net = explode('/', $net);
@@ -31,7 +31,7 @@ function in_net($addr, $net) {
if (isset($_GET['host'])) { if (isset($_GET['host'])) {
try { try {
foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) { foreach ($valkey->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) {
if (in_net($_GET['host'], $host)) { if (in_net($_GET['host'], $host)) {
echo '200 PERMIT'; echo '200 PERMIT';
exit; exit;
@@ -46,7 +46,7 @@ if (isset($_GET['host'])) {
} else { } else {
try { try {
echo '240.240.240.240' . PHP_EOL; echo '240.240.240.240' . PHP_EOL;
foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) { foreach ($valkey->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) {
echo $host . PHP_EOL; echo $host . PHP_EOL;
} }
} }

View File

@@ -56,7 +56,7 @@ function normalize_email($email) {
$email = explode('@', $email); $email = explode('@', $email);
$email[0] = str_replace('.', '', $email[0]); $email[0] = str_replace('.', '', $email[0]);
$email = implode('@', $email); $email = implode('@', $email);
} }
$gm_alt = "@googlemail.com"; $gm_alt = "@googlemail.com";
if (substr_compare($email, $gm_alt, -strlen($gm_alt)) == 0) { if (substr_compare($email, $gm_alt, -strlen($gm_alt)) == 0) {
$email = explode('@', $email); $email = explode('@', $email);
@@ -114,7 +114,7 @@ function ucl_rcpts($object, $type) {
$rcpt[] = str_replace('/', '\/', $row['address']); $rcpt[] = str_replace('/', '\/', $row['address']);
} }
// Aliases by alias domains // Aliases by alias domains
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox` $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox`
LEFT OUTER JOIN `alias_domain` ON `mailbox`.`domain` = `alias_domain`.`target_domain` LEFT OUTER JOIN `alias_domain` ON `mailbox`.`domain` = `alias_domain`.`target_domain`
WHERE `mailbox`.`username` = :object"); WHERE `mailbox`.`username` = :object");
$stmt->execute(array( $stmt->execute(array(
@@ -184,7 +184,7 @@ while ($row = array_shift($rows)) {
rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>; rcpt = <?=json_encode($rcpt, JSON_UNESCAPED_SLASHES);?>;
<?php <?php
} }
$stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf` $stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf`
WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel') WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel')
AND `object`= :object"); AND `object`= :object");
$stmt->execute(array(':object' => $row['object'])); $stmt->execute(array(':object' => $row['object']));
@@ -468,4 +468,36 @@ while ($row = array_shift($rows)) {
<?php <?php
} }
?> ?>
<?php
// Start internal aliases
$stmt = $pdo->query("SELECT `id`, `address`, `domain` FROM `alias` WHERE `active` = '1' AND `internal` = '1'");
$aliases = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($alias = array_shift($aliases)) {
// build allowed_domains regex and add target domain and alias domains
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `active` = '1' AND `target_domain` = :target_domain");
$stmt->execute(array(':target_domain' => $alias['domain']));
$allowed_domains = $stmt->fetchAll(PDO::FETCH_ASSOC);
$allowed_domains = array_map(function($item) {
return str_replace('.', '\.', $item['alias_domain']);
}, $allowed_domains);
$allowed_domains[] = str_replace('.', '\.', $alias['domain']);
$allowed_domains = implode('|', $allowed_domains);
?>
internal_alias_<?=$alias['id'];?> {
priority = 10;
rcpt = "<?=$alias['address'];?>";
from = "/^((?!.*@(<?=$allowed_domains;?>)).)*$/";
apply "default" {
MAILCOW_INTERNAL_ALIAS = 9999.0;
}
symbols [
"MAILCOW_INTERNAL_ALIAS"
]
}
<?php
}
?>
} }

View File

@@ -102,7 +102,7 @@ rspamd_config:register_symbol({
local rcpt_split = rspamd_str_split(rcpt['addr'], '@') local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
if #rcpt_split == 2 then if #rcpt_split == 2 then
if rcpt_split[1] == 'postmaster' then if rcpt_split[1] == 'postmaster' then
task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt') task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt', 'postmaster')
return return
end end
end end
@@ -167,7 +167,7 @@ rspamd_config:register_symbol({
for k,v in pairs(data) do for k,v in pairs(data) do
if (v and v ~= userdata and v == '1') then if (v and v ~= userdata and v == '1') then
rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result") rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result")
task:set_pre_result('accept', 'ip matched with forward hosts') task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam')
end end
end end
end end
@@ -454,12 +454,18 @@ rspamd_config:register_symbol({
local redis_params = rspamd_parse_redis_server('dyn_rl') local redis_params = rspamd_parse_redis_server('dyn_rl')
local rspamd_logger = require "rspamd_logger" local rspamd_logger = require "rspamd_logger"
local envfrom = task:get_from(1) local envfrom = task:get_from(1)
local envrcpt = task:get_recipients(1) or {}
local uname = task:get_user() local uname = task:get_user()
if not envfrom or not uname then if not envfrom or not uname then
return false return false
end end
local uname = uname:lower() local uname = uname:lower()
if #envrcpt == 1 and envrcpt[1].addr:lower() == uname then
return false
end
local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case
local function redis_cb_user(err, data) local function redis_cb_user(err, data)
@@ -544,13 +550,13 @@ rspamd_config:register_symbol({
-- determine newline type -- determine newline type
local function newline(task) local function newline(task)
local t = task:get_newlines_type() local t = task:get_newlines_type()
if t == 'cr' then if t == 'cr' then
return '\r' return '\r'
elseif t == 'lf' then elseif t == 'lf' then
return '\n' return '\n'
end end
return '\r\n' return '\r\n'
end end
-- retrieve footer -- retrieve footer
@@ -558,7 +564,7 @@ rspamd_config:register_symbol({
if err or type(data) ~= 'string' then 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) 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 else
-- parse json string -- parse json string
local footer = cjson.decode(data) local footer = cjson.decode(data)
if not footer then if not footer then
@@ -607,26 +613,30 @@ rspamd_config:register_symbol({
if footer.plain and footer.plain ~= "" then if footer.plain and footer.plain ~= "" then
footer.plain = lua_util.jinja_template(footer.plain, replacements, true) footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
end end
-- add footer -- add footer
local out = {} local out = {}
local rewrite = lua_mime.add_text_footer(task, footer.html, footer.plain) or {} local rewrite = lua_mime.add_text_footer(task, footer.html, footer.plain) or {}
local seen_cte local seen_cte
local newline_s = newline(task) local newline_s = newline(task)
local function rewrite_ct_cb(name, hdr) local function rewrite_ct_cb(name, hdr)
if rewrite.need_rewrite_ct then if rewrite.need_rewrite_ct then
if name:lower() == 'content-type' then if name:lower() == 'content-type' then
local nct = string.format('%s: %s/%s; charset=utf-8', -- include boundary if present
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype) local boundary_part = rewrite.new_ct.boundary and
string.format('; boundary="%s"', rewrite.new_ct.boundary) or ''
local nct = string.format('%s: %s/%s; charset=utf-8%s',
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)
out[#out + 1] = nct out[#out + 1] = nct
-- update Content-Type header -- update Content-Type header (include boundary if present)
task:set_milter_reply({ task:set_milter_reply({
remove_headers = {['Content-Type'] = 0}, remove_headers = {['Content-Type'] = 0},
}) })
task:set_milter_reply({ task:set_milter_reply({
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8', rewrite.new_ct.type, rewrite.new_ct.subtype)} add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8%s',
rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)}
}) })
return return
elseif name:lower() == 'content-transfer-encoding' then elseif name:lower() == 'content-transfer-encoding' then
@@ -645,16 +655,16 @@ rspamd_config:register_symbol({
end end
out[#out + 1] = hdr.raw:gsub('\r?\n?$', '') out[#out + 1] = hdr.raw:gsub('\r?\n?$', '')
end end
task:headers_foreach(rewrite_ct_cb, {full = true}) task:headers_foreach(rewrite_ct_cb, {full = true})
if not seen_cte and rewrite.need_rewrite_ct then if not seen_cte and rewrite.need_rewrite_ct then
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable') out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable')
end end
-- End of headers -- End of headers
out[#out + 1] = newline_s out[#out + 1] = newline_s
if rewrite.out then if rewrite.out then
for _,o in ipairs(rewrite.out) do for _,o in ipairs(rewrite.out) do
out[#out + 1] = o out[#out + 1] = o

View File

@@ -21,10 +21,10 @@ catch (PDOException $e) {
http_response_code(501); http_response_code(501);
exit; exit;
} }
// Init Redis // Init Valkey
$redis = new Redis(); $valkey = new Redis();
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
// Functions // Functions
function parse_email($email) { function parse_email($email) {
@@ -74,16 +74,16 @@ if ($fuzzy == 'unknown') {
} }
try { try {
$max_size = (int)$redis->Get('Q_MAX_SIZE'); $max_size = (int)$valkey->Get('Q_MAX_SIZE');
if (($max_size * 1048576) < $raw_size) { if (($max_size * 1048576) < $raw_size) {
error_log(sprintf("QUARANTINE: Message too large: %d b exceeds %d b", $raw_size, ($max_size * 1048576)) . PHP_EOL); error_log(sprintf("QUARANTINE: Message too large: %d b exceeds %d b", $raw_size, ($max_size * 1048576)) . PHP_EOL);
http_response_code(505); http_response_code(505);
exit; exit;
} }
if ($exclude_domains = $redis->Get('Q_EXCLUDE_DOMAINS')) { if ($exclude_domains = $valkey->Get('Q_EXCLUDE_DOMAINS')) {
$exclude_domains = json_decode($exclude_domains, true); $exclude_domains = json_decode($exclude_domains, true);
} }
$retention_size = (int)$redis->Get('Q_RETENTION_SIZE'); $retention_size = (int)$valkey->Get('Q_RETENTION_SIZE');
} }
catch (RedisException $e) { catch (RedisException $e) {
error_log("QUARANTINE: " . $e . PHP_EOL); error_log("QUARANTINE: " . $e . PHP_EOL);
@@ -103,7 +103,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
// Skip if not a mailcow handled domain // Skip if not a mailcow handled domain
try { try {
if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { if (!$valkey->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
continue; continue;
} }
} }
@@ -171,7 +171,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
} }
else { else {
$parsed_goto = parse_email($goto); $parsed_goto = parse_email($goto);
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { if (!$valkey->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL); error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
} }
else { else {
@@ -182,7 +182,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL); error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = explode(',', $goto_branch); $goto_branch_array = explode(',', $goto_branch);
} else { } else {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'"); $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
$stmt->execute(array(':domain' => $parsed_goto['domain'])); $stmt->execute(array(':domain' => $parsed_goto['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) { if ($goto_branch) {
@@ -236,6 +236,9 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) {
':action' => $action, ':action' => $action,
':fuzzy_hashes' => $fuzzy ':fuzzy_hashes' => $fuzzy
)); ));
$lastId = $pdo->lastInsertId();
$stmt_update = $pdo->prepare("UPDATE `quarantine` SET `qhash` = SHA2(CONCAT(`id`, `qid`), 256) WHERE `id` = :id");
$stmt_update->execute(array(':id' => $lastId));
$stmt = $pdo->prepare('DELETE FROM `quarantine` WHERE `rcpt` = :rcpt AND `id` NOT IN ( $stmt = $pdo->prepare('DELETE FROM `quarantine` WHERE `rcpt` = :rcpt AND `id` NOT IN (
SELECT `id` SELECT `id`
FROM ( FROM (

View File

@@ -5,16 +5,16 @@ header('Content-Type: text/plain');
require_once "vars.inc.php"; require_once "vars.inc.php";
// Do not show errors, we log to using error_log // Do not show errors, we log to using error_log
ini_set('error_reporting', 0); ini_set('error_reporting', 0);
// Init Redis // Init Valkey
$redis = new Redis(); $valkey = new Redis();
try { try {
if (!empty(getenv('REDIS_SLAVEOF_IP'))) { if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); $valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
} }
else { else {
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
} }
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
} }
catch (Exception $e) { catch (Exception $e) {
exit; exit;
@@ -44,6 +44,6 @@ $data['message_id'] = $raw_data_decoded['message_id'];
$data['header_subject'] = implode(' ', $raw_data_decoded['header_subject']); $data['header_subject'] = implode(' ', $raw_data_decoded['header_subject']);
$data['header_from'] = implode(', ', $raw_data_decoded['header_from']); $data['header_from'] = implode(', ', $raw_data_decoded['header_from']);
$redis->lpush('RL_LOG', json_encode($data)); $valkey->lpush('RL_LOG', json_encode($data));
exit; exit;

View File

@@ -21,10 +21,10 @@ catch (PDOException $e) {
http_response_code(501); http_response_code(501);
exit; exit;
} }
// Init Redis // Init Valkey
$redis = new Redis(); $valkey = new Redis();
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
// Functions // Functions
function parse_email($email) { function parse_email($email) {
@@ -94,7 +94,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
// Skip if not a mailcow handled domain // Skip if not a mailcow handled domain
try { try {
if (!$redis->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { if (!$valkey->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) {
continue; continue;
} }
} }
@@ -156,7 +156,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
} }
else { else {
$parsed_goto = parse_email($goto); $parsed_goto = parse_email($goto);
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { if (!$valkey->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL); error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
} }
else { else {
@@ -167,7 +167,7 @@ foreach (json_decode($rcpts, true) as $rcpt) {
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL); error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = explode(',', $goto_branch); $goto_branch_array = explode(',', $goto_branch);
} else { } else {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` AND '1'"); $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
$stmt->execute(array(':domain' => $parsed_goto['domain'])); $stmt->execute(array(':domain' => $parsed_goto['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain']; $goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) { if ($goto_branch) {

12
data/conf/valkey/valkey-conf.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/sh
cat <<EOF > /valkey.conf
requirepass $VALKEYPASS
user quota_notify on nopass ~QW_* -@all +get +hget +ping
EOF
if [ -n "$VALKEYMASTERPASS" ]; then
echo "masterauth $VALKEYMASTERPASS" >> /valkey.conf
fi
exec "$@"

View File

@@ -1,13 +1,13 @@
<?php <?php
$redis = new Redis(); $valkey = new Redis();
try { try {
if (!empty(getenv('REDIS_SLAVEOF_IP'))) { if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); $valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
} }
else { else {
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
} }
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
} }
catch (Exception $e) { catch (Exception $e) {
exit; exit;
@@ -15,4 +15,4 @@ catch (Exception $e) {
header('Content-Type: application/json'); header('Content-Type: application/json');
echo '{"error":"Unauthorized"}'; echo '{"error":"Unauthorized"}';
error_log("Rspamd UI: Invalid password by " . $_SERVER['REMOTE_ADDR']); error_log("Rspamd UI: Invalid password by " . $_SERVER['REMOTE_ADDR']);
$redis->publish("F2B_CHANNEL", "Rspamd UI: Invalid password by " . $_SERVER['REMOTE_ADDR']); $valkey->publish("F2B_CHANNEL", "Rspamd UI: Invalid password by " . $_SERVER['REMOTE_ADDR']);

View File

@@ -21,7 +21,7 @@ $clamd_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_CLAMD"])) ?
$olefy_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_OLEFY"])) ? false : true; $olefy_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_OLEFY"])) ? false : true;
if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) { if (!isset($_SESSION['gal']) && $license_cache = $valkey->Get('LICENSE_STATUS_CACHE')) {
$_SESSION['gal'] = json_decode($license_cache, true); $_SESSION['gal'] = json_decode($license_cache, true);
} }

View File

@@ -5412,9 +5412,9 @@ paths:
started_at: "2019-12-22T21:00:07.186717617Z" started_at: "2019-12-22T21:00:07.186717617Z"
state: running state: running
type: info type: info
redis-mailcow: valkey-mailcow:
container: redis-mailcow container: valkey-mailcow
image: "redis:5-alpine" image: "valkey:7.2.8-alpine"
started_at: "2019-12-22T20:59:56.827166834Z" started_at: "2019-12-22T20:59:56.827166834Z"
state: running state: running
type: info type: info

View File

@@ -10,16 +10,16 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
$default_autodiscover_config = $autodiscover_config; $default_autodiscover_config = $autodiscover_config;
$autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config); $autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config);
// Redis // Valkey
$redis = new Redis(); $valkey = new Redis();
try { try {
if (!empty(getenv('REDIS_SLAVEOF_IP'))) { if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); $valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
} }
else { else {
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
} }
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
} }
catch (Exception $e) { catch (Exception $e) {
exit; exit;
@@ -71,7 +71,7 @@ if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) {
"service" => "Error: must be authenticated" "service" => "Error: must be authenticated"
) )
); );
$redis->lPush('AUTODISCOVER_LOG', $json); $valkey->lPush('AUTODISCOVER_LOG', $json);
header('WWW-Authenticate: Basic realm="' . $_SERVER['HTTP_HOST'] . '"'); header('WWW-Authenticate: Basic realm="' . $_SERVER['HTTP_HOST'] . '"');
header('HTTP/1.0 401 Unauthorized'); header('HTTP/1.0 401 Unauthorized');
exit(0); exit(0);
@@ -96,13 +96,13 @@ if ($login_role === "user") {
"service" => "Error: invalid or missing request data" "service" => "Error: invalid or missing request data"
) )
); );
$redis->lPush('AUTODISCOVER_LOG', $json); $valkey->lPush('AUTODISCOVER_LOG', $json);
$redis->lTrim('AUTODISCOVER_LOG', 0, 100); $valkey->lTrim('AUTODISCOVER_LOG', 0, 100);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'msg' => 'Redis: '.$e 'msg' => 'Valkey: '.$e
); );
return false; return false;
} }
@@ -151,13 +151,13 @@ if ($login_role === "user") {
"service" => $autodiscover_config['autodiscoverType'] "service" => $autodiscover_config['autodiscoverType']
) )
); );
$redis->lPush('AUTODISCOVER_LOG', $json); $valkey->lPush('AUTODISCOVER_LOG', $json);
$redis->lTrim('AUTODISCOVER_LOG', 0, 100); $valkey->lTrim('AUTODISCOVER_LOG', 0, 100);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'msg' => 'Redis: '.$e 'msg' => 'Valkey: '.$e
); );
return false; return false;
} }

View File

@@ -48,6 +48,12 @@ if (isset($_SESSION['mailcow_cc_role'])) {
$rl = ratelimit('get', 'domain', $domain); $rl = ratelimit('get', 'domain', $domain);
$rlyhosts = relayhost('get'); $rlyhosts = relayhost('get');
$domain_footer = mailbox('get', 'domain_wide_footer', $domain); $domain_footer = mailbox('get', 'domain_wide_footer', $domain);
$mta_sts = mailbox('get', 'mta_sts', $domain);
if (count($mta_sts) == 0) {
$mta_sts = false;
} elseif (isset($mta_sts['mx'])) {
$mta_sts['mx'] = implode(',', $mta_sts['mx']);
}
$template = 'edit/domain.twig'; $template = 'edit/domain.twig';
$template_data = [ $template_data = [
'acl' => $_SESSION['acl'], 'acl' => $_SESSION['acl'],
@@ -58,6 +64,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
'dkim' => dkim('details', $domain), 'dkim' => dkim('details', $domain),
'domain_details' => $result, 'domain_details' => $result,
'domain_footer' => $domain_footer, 'domain_footer' => $domain_footer,
'mta_sts' => $mta_sts,
'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]), 'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]),
'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address'), 'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address'),
'alias_domains' => mailbox('get', 'alias_domains', $_GET["domain"]) 'alias_domains' => mailbox('get', 'alias_domains', $_GET["domain"])
@@ -125,6 +132,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
'mailbox' => $mailbox, 'mailbox' => $mailbox,
'rl' => $rl, 'rl' => $rl,
'pushover_data' => $pushover_data, 'pushover_data' => $pushover_data,
'get_tagging_options' => mailbox('get', 'delimiter_action', $mailbox),
'quarantine_notification' => $quarantine_notification, 'quarantine_notification' => $quarantine_notification,
'quarantine_category' => $quarantine_category, 'quarantine_category' => $quarantine_category,
'get_tls_policy' => $get_tls_policy, 'get_tls_policy' => $get_tls_policy,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -71,6 +71,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
// Init records array // Init records array
$spf_link = '<a href="http://www.open-spf.org/SPF_Record_Syntax/" target="_blank">SPF Record Syntax</a><br />'; $spf_link = '<a href="http://www.open-spf.org/SPF_Record_Syntax/" target="_blank">SPF Record Syntax</a><br />';
$dmarc_link = '<a href="https://www.kitterman.com/dmarc/assistant.html" target="_blank">DMARC Assistant</a>'; $dmarc_link = '<a href="https://www.kitterman.com/dmarc/assistant.html" target="_blank">DMARC Assistant</a>';
$mtasts_report_link = '<a href="https://mxtoolbox.com/dmarc/smtp-tls/how-to-setup-smtp-tls-reports" target="_blank">TLS Report Record Syntax</a>';
$records = array(); $records = array();
@@ -128,6 +129,27 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
); );
} }
$mta_sts = mailbox('get', 'mta_sts', $domain);
if (count($mta_sts) > 0 && $mta_sts['active'] == 1) {
if (!in_array($domain, $alias_domains)) {
$records[] = array(
'mta-sts.' . $domain,
'CNAME',
$mailcow_hostname
);
}
$records[] = array(
'_mta-sts.' . $domain,
'TXT',
"v={$mta_sts['version']};id={$mta_sts['id']};",
);
$records[] = array(
'_smtp._tls.' . $domain,
'TXT',
$mtasts_report_link,
);
}
$records[] = array( $records[] = array(
$domain, $domain,
'TXT', 'TXT',
@@ -341,15 +363,25 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
} }
foreach ($currents as &$current) { foreach ($currents as &$current) {
if ($current['type'] == "TXT" &&
stripos(strtolower($current['txt']), 'v=sts') === 0) {
if (strtolower($current[$data_field[$current['type']]]) == strtolower($record[2])) {
$state = state_good;
}
else {
$state = state_nomatch;
}
$state .= '<br />' . $current[$data_field[$current['type']]];
}
if ($current['type'] == 'TXT' && if ($current['type'] == 'TXT' &&
stripos($current['txt'], 'v=dmarc') === 0 && stripos($current['txt'], 'v=dmarc') === 0 &&
$record[2] == $dmarc_link) { $record[2] == $dmarc_link) {
$current['txt'] = str_replace(' ', '', $current['txt']); $current['txt'] = str_replace(' ', '', $current['txt']);
$state = $current[$data_field[$current['type']]] . state_optional; $state = $current[$data_field[$current['type']]] . state_optional;
} }
elseif ($current['type'] == 'TXT' && elseif ($current['type'] == 'TXT' &&
stripos($current['txt'], 'v=spf') === 0 && stripos($current['txt'], 'v=spf') === 0 &&
$record[2] == $spf_link) { $record[2] == $spf_link) {
$state = state_nomatch; $state = state_nomatch;
$rslt = get_spf_allowed_hosts($record[0], true); $rslt = get_spf_allowed_hosts($record[0], true);
if (in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)) { if (in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)) {
@@ -358,8 +390,8 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
$state .= '<br />' . $current[$data_field[$current['type']]] . state_optional; $state .= '<br />' . $current[$data_field[$current['type']]] . state_optional;
} }
elseif ($current['type'] == 'TXT' && elseif ($current['type'] == 'TXT' &&
stripos($current['txt'], 'v=dkim') === 0 && stripos($current['txt'], 'v=dkim') === 0 &&
stripos($record[2], 'v=dkim') === 0) { stripos($record[2], 'v=dkim') === 0) {
preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $current[$data_field[$current['type']]], $dkim_matches_current); preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $current[$data_field[$current['type']]], $dkim_matches_current);
preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $record[2], $dkim_matches_good); preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $record[2], $dkim_matches_good);
if ($dkim_matches_current[1] == $dkim_matches_good[1]) { if ($dkim_matches_current[1] == $dkim_matches_good[1]) {
@@ -367,7 +399,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
} }
} }
elseif ($current['type'] != 'TXT' && elseif ($current['type'] != 'TXT' &&
isset($data_field[$current['type']]) && $state != state_good) { isset($data_field[$current['type']]) && $state != state_good) {
$state = state_nomatch; $state = state_nomatch;
if ($current[$data_field[$current['type']]] == $record[2]) { if ($current[$data_field[$current['type']]] == $record[2]) {
$state = state_good; $state = state_good;

View File

@@ -26,23 +26,25 @@ if (is_array($alertbox_log_parser)) {
// map tfa details for twig // map tfa details for twig
$pending_tfa_authmechs = []; $pending_tfa_authmechs = [];
foreach($_SESSION['pending_tfa_methods'] as $authdata){ if (array_key_exists('pending_tfa_methods', $_SESSION)) {
$pending_tfa_authmechs[$authdata['authmech']] = false; foreach($_SESSION['pending_tfa_methods'] as $authdata){
} $pending_tfa_authmechs[$authdata['authmech']] = false;
if (isset($pending_tfa_authmechs['webauthn'])) { }
$pending_tfa_authmechs['webauthn'] = true; if (isset($pending_tfa_authmechs['webauthn'])) {
} $pending_tfa_authmechs['webauthn'] = true;
if (!isset($pending_tfa_authmechs['webauthn']) }
&& isset($pending_tfa_authmechs['yubi_otp'])) { if (!isset($pending_tfa_authmechs['webauthn'])
$pending_tfa_authmechs['yubi_otp'] = true; && isset($pending_tfa_authmechs['yubi_otp'])) {
} $pending_tfa_authmechs['yubi_otp'] = true;
if (!isset($pending_tfa_authmechs['webauthn']) }
&& !isset($pending_tfa_authmechs['yubi_otp']) if (!isset($pending_tfa_authmechs['webauthn'])
&& isset($pending_tfa_authmechs['totp'])) { && !isset($pending_tfa_authmechs['yubi_otp'])
$pending_tfa_authmechs['totp'] = true; && isset($pending_tfa_authmechs['totp'])) {
} $pending_tfa_authmechs['totp'] = true;
if (isset($pending_tfa_authmechs['u2f'])) { }
$pending_tfa_authmechs['u2f'] = true; if (isset($pending_tfa_authmechs['u2f'])) {
$pending_tfa_authmechs['u2f'] = true;
}
} }
// globals // globals

View File

@@ -1,7 +1,7 @@
<?php <?php
function app_passwd($_action, $_data = null) { function app_passwd($_action, $_data = null) {
global $pdo; global $pdo;
global $lang; global $lang;
$_data_log = $_data; $_data_log = $_data;
!isset($_data_log['app_passwd']) ?: $_data_log['app_passwd'] = '*'; !isset($_data_log['app_passwd']) ?: $_data_log['app_passwd'] = '*';
!isset($_data_log['app_passwd2']) ?: $_data_log['app_passwd2'] = '*'; !isset($_data_log['app_passwd2']) ?: $_data_log['app_passwd2'] = '*';
@@ -43,20 +43,7 @@ function app_passwd($_action, $_data = null) {
); );
return false; return false;
} }
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { if (password_check($password, $password2) !== true) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_complexity'
);
return false;
}
if ($password != $password2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'password_mismatch'
);
return false; return false;
} }
$password_hashed = hash_password($password); $password_hashed = hash_password($password);
@@ -88,15 +75,15 @@ function app_passwd($_action, $_data = null) {
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'app_passwd_added' 'msg' => 'app_passwd_added'
); );
break; break;
case 'edit': case 'edit':
$ids = (array)$_data['id']; $ids = (array)$_data['id'];
foreach ($ids as $id) { foreach ($ids as $id) {
$is_now = app_passwd('details', $id); $is_now = app_passwd('details', $id);
if (!empty($is_now)) { if (!empty($is_now)) {
$app_name = (!empty($_data['app_name'])) ? $_data['app_name'] : $is_now['name']; $app_name = (!empty($_data['app_name'])) ? $_data['app_name'] : $is_now['name'];
$password = (!empty($_data['password'])) ? $_data['password'] : null; $password = (!empty($_data['app_passwd'])) ? $_data['app_passwd'] : null;
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null; $password2 = (!empty($_data['app_passwd2'])) ? $_data['app_passwd2'] : null;
if (isset($_data['protocols'])) { if (isset($_data['protocols'])) {
$protocols = (array)$_data['protocols']; $protocols = (array)$_data['protocols'];
$imap_access = (in_array('imap_access', $protocols)) ? 1 : 0; $imap_access = (in_array('imap_access', $protocols)) ? 1 : 0;
@@ -126,20 +113,7 @@ function app_passwd($_action, $_data = null) {
} }
$app_name = htmlspecialchars(trim($app_name)); $app_name = htmlspecialchars(trim($app_name));
if (!empty($password) && !empty($password2)) { if (!empty($password) && !empty($password2)) {
if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { if (password_check($password, $password2) !== true) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_complexity'
);
continue;
}
if ($password != $password2) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'password_mismatch'
);
continue; continue;
} }
$password_hashed = hash_password($password); $password_hashed = hash_password($password);
@@ -182,7 +156,7 @@ function app_passwd($_action, $_data = null) {
'msg' => array('object_modified', htmlspecialchars(implode(', ', $ids))) 'msg' => array('object_modified', htmlspecialchars(implode(', ', $ids)))
); );
} }
break; break;
case 'delete': case 'delete':
$ids = (array)$_data['id']; $ids = (array)$_data['id'];
foreach ($ids as $id) { foreach ($ids as $id) {
@@ -213,19 +187,17 @@ function app_passwd($_action, $_data = null) {
'msg' => array('app_passwd_removed', htmlspecialchars($id)) 'msg' => array('app_passwd_removed', htmlspecialchars($id))
); );
} }
break; break;
case 'get': case 'get':
$app_passwds = array(); $app_passwds = array();
$stmt = $pdo->prepare("SELECT `id`, `name` FROM `app_passwd` WHERE `mailbox` = :username"); $stmt = $pdo->prepare("SELECT `id`, `name` FROM `app_passwd` WHERE `mailbox` = :username");
$stmt->execute(array(':username' => $username)); $stmt->execute(array(':username' => $username));
$app_passwds = $stmt->fetchAll(PDO::FETCH_ASSOC); $app_passwds = $stmt->fetchAll(PDO::FETCH_ASSOC);
return $app_passwds; return $app_passwds;
break; break;
case 'details': case 'details':
$app_passwd_data = array(); $app_passwd_data = array();
$stmt = $pdo->prepare("SELECT * $stmt = $pdo->prepare("SELECT * FROM `app_passwd` WHERE `id` = :id");
FROM `app_passwd`
WHERE `id` = :id");
$stmt->execute(array(':id' => $_data)); $stmt->execute(array(':id' => $_data));
$app_passwd_data = $stmt->fetch(PDO::FETCH_ASSOC); $app_passwd_data = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($app_passwd_data)) { if (empty($app_passwd_data)) {
@@ -237,6 +209,6 @@ function app_passwd($_action, $_data = null) {
} }
$app_passwd_data['name'] = htmlspecialchars(trim($app_passwd_data['name'])); $app_passwd_data['name'] = htmlspecialchars(trim($app_passwd_data['name']));
return $app_passwd_data; return $app_passwd_data;
break; break;
} }
} }

View File

@@ -1,7 +1,7 @@
<?php <?php
function check_login($user, $pass, $app_passwd_data = false, $extra = null) { function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
global $pdo; global $pdo;
global $redis; global $valkey;
$is_internal = $extra['is_internal']; $is_internal = $extra['is_internal'];
$role = $extra['role']; $role = $extra['role'];
@@ -62,12 +62,12 @@ function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
if (!isset($_SESSION['ldelay'])) { if (!isset($_SESSION['ldelay'])) {
$_SESSION['ldelay'] = "0"; $_SESSION['ldelay'] = "0";
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); $valkey->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
} }
elseif (!isset($_SESSION['mailcow_cc_username'])) { elseif (!isset($_SESSION['mailcow_cc_username'])) {
$_SESSION['ldelay'] = $_SESSION['ldelay']+0.5; $_SESSION['ldelay'] = $_SESSION['ldelay']+0.5;
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); $valkey->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
} }
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -193,6 +193,7 @@ function user_login($user, $pass, $extra = null){
global $iam_settings; global $iam_settings;
$is_internal = $extra['is_internal']; $is_internal = $extra['is_internal'];
$service = $extra['service'];
if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) { if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) {
if (!$is_internal){ if (!$is_internal){
@@ -235,6 +236,14 @@ function user_login($user, $pass, $extra = null){
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!empty($row)) { if (!empty($row)) {
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
$row['attributes'] = json_decode($row['attributes'], true);
if (isset($service)) {
$key = strtolower($service) . "_access";
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
return false;
}
}
return true; return true;
} }
} }
@@ -242,7 +251,14 @@ function user_login($user, $pass, $extra = null){
return false; return false;
} }
// check if user has access to service (imap, smtp, pop3, sieve) if service is set
$row['attributes'] = json_decode($row['attributes'], true); $row['attributes'] = json_decode($row['attributes'], true);
if (isset($service)) {
$key = strtolower($service) . "_access";
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
return false;
}
}
switch ($row['authsource']) { switch ($row['authsource']) {
case 'keycloak': case 'keycloak':
// user authsource is keycloak, try using via rest flow // user authsource is keycloak, try using via rest flow

View File

@@ -1,6 +1,6 @@
<?php <?php
function customize($_action, $_item, $_data = null) { function customize($_action, $_item, $_data = null) {
global $redis; global $valkey;
global $lang; global $lang;
global $LOGO_LIMITS; global $LOGO_LIMITS;
@@ -82,13 +82,13 @@ function customize($_action, $_item, $_data = null) {
return false; return false;
} }
try { try {
$redis->Set(strtoupper($_item), 'data:' . $_data[$_item]['type'] . ';base64,' . base64_encode(file_get_contents($_data[$_item]['tmp_name']))); $valkey->Set(strtoupper($_item), 'data:' . $_data[$_item]['type'] . ';base64,' . base64_encode(file_get_contents($_data[$_item]['tmp_name'])));
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data), 'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -134,13 +134,13 @@ function customize($_action, $_item, $_data = null) {
)); ));
} }
try { try {
$redis->set('APP_LINKS', json_encode($out)); $valkey->set('APP_LINKS', json_encode($out));
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data), 'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -162,20 +162,20 @@ function customize($_action, $_item, $_data = null) {
$ui_announcement_active = (!empty($_data['ui_announcement_active']) ? 1 : 0); $ui_announcement_active = (!empty($_data['ui_announcement_active']) ? 1 : 0);
try { try {
$redis->set('TITLE_NAME', htmlspecialchars($title_name)); $valkey->set('TITLE_NAME', htmlspecialchars($title_name));
$redis->set('MAIN_NAME', htmlspecialchars($main_name)); $valkey->set('MAIN_NAME', htmlspecialchars($main_name));
$redis->set('APPS_NAME', htmlspecialchars($apps_name)); $valkey->set('APPS_NAME', htmlspecialchars($apps_name));
$redis->set('HELP_TEXT', $help_text); $valkey->set('HELP_TEXT', $help_text);
$redis->set('UI_FOOTER', $ui_footer); $valkey->set('UI_FOOTER', $ui_footer);
$redis->set('UI_ANNOUNCEMENT_TEXT', $ui_announcement_text); $valkey->set('UI_ANNOUNCEMENT_TEXT', $ui_announcement_text);
$redis->set('UI_ANNOUNCEMENT_TYPE', $ui_announcement_type); $valkey->set('UI_ANNOUNCEMENT_TYPE', $ui_announcement_type);
$redis->set('UI_ANNOUNCEMENT_ACTIVE', $ui_announcement_active); $valkey->set('UI_ANNOUNCEMENT_ACTIVE', $ui_announcement_active);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data), 'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -188,13 +188,13 @@ function customize($_action, $_item, $_data = null) {
case 'ip_check': case 'ip_check':
$ip_check = ($_data['ip_check_opt_in'] == "1") ? 1 : 0; $ip_check = ($_data['ip_check_opt_in'] == "1") ? 1 : 0;
try { try {
$redis->set('IP_CHECK', $ip_check); $valkey->set('IP_CHECK', $ip_check);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data), 'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -217,7 +217,7 @@ function customize($_action, $_item, $_data = null) {
"force_sso" => $force_sso, "force_sso" => $force_sso,
); );
try { try {
$redis->set('CUSTOM_LOGIN', json_encode($custom_login)); $valkey->set('CUSTOM_LOGIN', json_encode($custom_login));
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -257,7 +257,7 @@ function customize($_action, $_item, $_data = null) {
case 'main_logo': case 'main_logo':
case 'main_logo_dark': case 'main_logo_dark':
try { try {
if ($redis->del(strtoupper($_item))) { if ($valkey->del(strtoupper($_item))) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_item, $_data), 'log' => array(__FUNCTION__, $_action, $_item, $_data),
@@ -270,7 +270,7 @@ function customize($_action, $_item, $_data = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data), 'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -281,19 +281,19 @@ function customize($_action, $_item, $_data = null) {
switch ($_item) { switch ($_item) {
case 'app_links': case 'app_links':
try { try {
$app_links = json_decode($redis->get('APP_LINKS'), true); $app_links = json_decode($valkey->get('APP_LINKS'), true);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data), 'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
if (empty($app_links)){ if (empty($app_links)){
return false; return [];
} }
// convert from old style // convert from old style
@@ -312,38 +312,40 @@ function customize($_action, $_item, $_data = null) {
case 'main_logo': case 'main_logo':
case 'main_logo_dark': case 'main_logo_dark':
try { try {
return $redis->get(strtoupper($_item)); return $valkey->get(strtoupper($_item));
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data), 'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
break; break;
case 'ui_texts': case 'ui_texts':
try { try {
$data['title_name'] = ($title_name = $redis->get('TITLE_NAME')) ? $title_name : 'mailcow UI'; $mailcow_hostname = strtolower(getenv("MAILCOW_HOSTNAME"));
$data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : 'mailcow UI';
$data['apps_name'] = ($apps_name = $redis->get('APPS_NAME')) ? $apps_name : $lang['header']['apps']; $data['title_name'] = ($title_name = $valkey->get('TITLE_NAME')) ? $title_name : "$mailcow_hostname - mail UI";
$data['help_text'] = ($help_text = $redis->get('HELP_TEXT')) ? $help_text : false; $data['main_name'] = ($main_name = $valkey->get('MAIN_NAME')) ? $main_name : "$mailcow_hostname - mail UI";
if (!empty($redis->get('UI_IMPRESS'))) { $data['apps_name'] = ($apps_name = $valkey->get('APPS_NAME')) ? $apps_name : $lang['header']['apps'];
$redis->set('UI_FOOTER', $redis->get('UI_IMPRESS')); $data['help_text'] = ($help_text = $valkey->get('HELP_TEXT')) ? $help_text : false;
$redis->del('UI_IMPRESS'); if (!empty($valkey->get('UI_IMPRESS'))) {
$valkey->set('UI_FOOTER', $valkey->get('UI_IMPRESS'));
$valkey->del('UI_IMPRESS');
} }
$data['ui_footer'] = ($ui_footer = $redis->get('UI_FOOTER')) ? $ui_footer : false; $data['ui_footer'] = ($ui_footer = $valkey->get('UI_FOOTER')) ? $ui_footer : false;
$data['ui_announcement_text'] = ($ui_announcement_text = $redis->get('UI_ANNOUNCEMENT_TEXT')) ? $ui_announcement_text : false; $data['ui_announcement_text'] = ($ui_announcement_text = $valkey->get('UI_ANNOUNCEMENT_TEXT')) ? $ui_announcement_text : false;
$data['ui_announcement_type'] = ($ui_announcement_type = $redis->get('UI_ANNOUNCEMENT_TYPE')) ? $ui_announcement_type : false; $data['ui_announcement_type'] = ($ui_announcement_type = $valkey->get('UI_ANNOUNCEMENT_TYPE')) ? $ui_announcement_type : false;
$data['ui_announcement_active'] = ($redis->get('UI_ANNOUNCEMENT_ACTIVE') == 1) ? 1 : 0; $data['ui_announcement_active'] = ($valkey->get('UI_ANNOUNCEMENT_ACTIVE') == 1) ? 1 : 0;
return $data; return $data;
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data), 'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -374,21 +376,21 @@ function customize($_action, $_item, $_data = null) {
break; break;
case 'ip_check': case 'ip_check':
try { try {
$ip_check = ($ip_check = $redis->get('IP_CHECK')) ? $ip_check : 0; $ip_check = ($ip_check = $valkey->get('IP_CHECK')) ? $ip_check : 0;
return $ip_check; return $ip_check;
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data), 'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
break; break;
case 'custom_login': case 'custom_login':
try { try {
$custom_login = $redis->get('CUSTOM_LOGIN'); $custom_login = $valkey->get('CUSTOM_LOGIN');
return $custom_login ? json_decode($custom_login, true) : array(); return $custom_login ? json_decode($custom_login, true) : array();
} }
catch (RedisException $e) { catch (RedisException $e) {

View File

@@ -1,7 +1,7 @@
<?php <?php
function dkim($_action, $_data = null, $privkey = false) { function dkim($_action, $_data = null, $privkey = false) {
global $redis; global $valkey;
global $lang; global $lang;
switch ($_action) { switch ($_action) {
case 'add': case 'add':
@@ -18,7 +18,7 @@ function dkim($_action, $_data = null, $privkey = false) {
); );
continue; continue;
} }
if ($redis->hGet('DKIM_PUB_KEYS', $domain)) { if ($valkey->hGet('DKIM_PUB_KEYS', $domain)) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data), 'log' => array(__FUNCTION__, $_action, $_data),
@@ -54,30 +54,30 @@ function dkim($_action, $_data = null, $privkey = false) {
explode(PHP_EOL, $key_details['key']) explode(PHP_EOL, $key_details['key'])
), 1, -1) ), 1, -1)
); );
// Save public key and selector to redis // Save public key and selector to valkey
try { try {
$redis->hSet('DKIM_PUB_KEYS', $domain, $pubKey); $valkey->hSet('DKIM_PUB_KEYS', $domain, $pubKey);
$redis->hSet('DKIM_SELECTORS', $domain, $dkim_selector); $valkey->hSet('DKIM_SELECTORS', $domain, $dkim_selector);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data), 'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
// Export private key and save private key to redis // Export private key and save private key to valkey
openssl_pkey_export($keypair_ressource, $privKey); openssl_pkey_export($keypair_ressource, $privKey);
if (isset($privKey) && !empty($privKey)) { if (isset($privKey) && !empty($privKey)) {
try { try {
$redis->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, trim($privKey)); $valkey->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, trim($privKey));
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data), 'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -121,15 +121,15 @@ function dkim($_action, $_data = null, $privkey = false) {
$to_domains = array_filter($to_domains); $to_domains = array_filter($to_domains);
foreach ($to_domains as $to_domain) { foreach ($to_domains as $to_domain) {
try { try {
$redis->hSet('DKIM_PUB_KEYS', $to_domain, $from_domain_dkim['pubkey']); $valkey->hSet('DKIM_PUB_KEYS', $to_domain, $from_domain_dkim['pubkey']);
$redis->hSet('DKIM_SELECTORS', $to_domain, $from_domain_dkim['dkim_selector']); $valkey->hSet('DKIM_SELECTORS', $to_domain, $from_domain_dkim['dkim_selector']);
$redis->hSet('DKIM_PRIV_KEYS', $from_domain_dkim['dkim_selector'] . '.' . $to_domain, base64_decode(trim($from_domain_dkim['privkey']))); $valkey->hSet('DKIM_PRIV_KEYS', $from_domain_dkim['dkim_selector'] . '.' . $to_domain, base64_decode(trim($from_domain_dkim['privkey'])));
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data), 'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -178,7 +178,7 @@ function dkim($_action, $_data = null, $privkey = false) {
); );
return false; return false;
} }
if ($redis->hGet('DKIM_PUB_KEYS', $domain)) { if ($valkey->hGet('DKIM_PUB_KEYS', $domain)) {
if ($overwrite_existing == 0) { if ($overwrite_existing == 0) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@@ -198,15 +198,15 @@ function dkim($_action, $_data = null, $privkey = false) {
} }
try { try {
dkim('delete', array('domains' => $domain)); dkim('delete', array('domains' => $domain));
$redis->hSet('DKIM_PUB_KEYS', $domain, $pem_public_key); $valkey->hSet('DKIM_PUB_KEYS', $domain, $pem_public_key);
$redis->hSet('DKIM_SELECTORS', $domain, $dkim_selector); $valkey->hSet('DKIM_SELECTORS', $domain, $dkim_selector);
$redis->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, $private_key_normalized); $valkey->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, $private_key_normalized);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data), 'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -219,7 +219,7 @@ function dkim($_action, $_data = null, $privkey = false) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data), 'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -235,8 +235,8 @@ function dkim($_action, $_data = null, $privkey = false) {
return false; return false;
} }
$dkimdata = array(); $dkimdata = array();
if ($redis_dkim_key_data = $redis->hGet('DKIM_PUB_KEYS', $_data)) { if ($valkey_dkim_key_data = $valkey->hGet('DKIM_PUB_KEYS', $_data)) {
$dkimdata['pubkey'] = $redis_dkim_key_data; $dkimdata['pubkey'] = $valkey_dkim_key_data;
if (strlen($dkimdata['pubkey']) < 391) { if (strlen($dkimdata['pubkey']) < 391) {
$dkimdata['length'] = "1024"; $dkimdata['length'] = "1024";
} }
@@ -253,15 +253,15 @@ function dkim($_action, $_data = null, $privkey = false) {
$dkimdata['length'] = ">= 8192"; $dkimdata['length'] = ">= 8192";
} }
if ($GLOBALS['SPLIT_DKIM_255'] === true) { if ($GLOBALS['SPLIT_DKIM_255'] === true) {
$dkim_txt_tmp = str_split('v=DKIM1;k=rsa;t=s;s=email;p=' . $redis_dkim_key_data, 255); $dkim_txt_tmp = str_split('v=DKIM1;k=rsa;t=s;s=email;p=' . $valkey_dkim_key_data, 255);
$dkimdata['dkim_txt'] = sprintf('"%s"', implode('" "', (array)$dkim_txt_tmp ) ); $dkimdata['dkim_txt'] = sprintf('"%s"', implode('" "', (array)$dkim_txt_tmp ) );
} }
else { else {
$dkimdata['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . $redis_dkim_key_data; $dkimdata['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . $valkey_dkim_key_data;
} }
$dkimdata['dkim_selector'] = $redis->hGet('DKIM_SELECTORS', $_data); $dkimdata['dkim_selector'] = $valkey->hGet('DKIM_SELECTORS', $_data);
if ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] || $privkey == true) { if ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] || $privkey == true) {
$dkimdata['privkey'] = base64_encode($redis->hGet('DKIM_PRIV_KEYS', $dkimdata['dkim_selector'] . '.' . $_data)); $dkimdata['privkey'] = base64_encode($valkey->hGet('DKIM_PRIV_KEYS', $dkimdata['dkim_selector'] . '.' . $_data));
} }
else { else {
$dkimdata['privkey'] = ''; $dkimdata['privkey'] = '';
@@ -279,8 +279,8 @@ function dkim($_action, $_data = null, $privkey = false) {
return false; return false;
} }
$blinddkim = array(); $blinddkim = array();
foreach ($redis->hKeys('DKIM_PUB_KEYS') as $redis_dkim_domain) { foreach ($valkey->hKeys('DKIM_PUB_KEYS') as $valkey_dkim_domain) {
$blinddkim[] = $redis_dkim_domain; $blinddkim[] = $valkey_dkim_domain;
} }
return array_diff($blinddkim, array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'))); return array_diff($blinddkim, array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains')));
break; break;
@@ -304,16 +304,16 @@ function dkim($_action, $_data = null, $privkey = false) {
continue; continue;
} }
try { try {
$selector = $redis->hGet('DKIM_SELECTORS', $domain); $selector = $valkey->hGet('DKIM_SELECTORS', $domain);
$redis->hDel('DKIM_PUB_KEYS', $domain); $valkey->hDel('DKIM_PUB_KEYS', $domain);
$redis->hDel('DKIM_PRIV_KEYS', $selector . '.' . $domain); $valkey->hDel('DKIM_PRIV_KEYS', $selector . '.' . $domain);
$redis->hDel('DKIM_SELECTORS', $domain); $valkey->hDel('DKIM_SELECTORS', $domain);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data), 'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }

View File

@@ -1,7 +1,7 @@
<?php <?php
function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $extra_headers = null) { function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $extra_headers = null) {
global $DOCKER_TIMEOUT; global $DOCKER_TIMEOUT;
global $redis; global $valkey;
$curl = curl_init(); $curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: application/json' )); curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: application/json' ));
// We are using our mail certificates for dockerapi, the names will not match, the certs are trusted anyway // We are using our mail certificates for dockerapi, the names will not match, the certs are trusted anyway
@@ -102,7 +102,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
} }
} }
else { else {
if (isset($decoded_response['Config']['Labels']['com.docker.compose.project']) if (isset($decoded_response['Config']['Labels']['com.docker.compose.project'])
&& strtolower($decoded_response['Config']['Labels']['com.docker.compose.project']) == strtolower(getenv('COMPOSE_PROJECT_NAME'))) { && strtolower($decoded_response['Config']['Labels']['com.docker.compose.project']) == strtolower(getenv('COMPOSE_PROJECT_NAME'))) {
unset($container['Config']['Env']); unset($container['Config']['Env']);
$out[$decoded_response['Config']['Labels']['com.docker.compose.service']]['State'] = $decoded_response['State']; $out[$decoded_response['Config']['Labels']['com.docker.compose.service']]['State'] = $decoded_response['State'];
@@ -200,7 +200,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
"request" => $attr2 "request" => $attr2
); );
$redis->publish("MC_CHANNEL", json_encode($request)); $valkey->publish("MC_CHANNEL", json_encode($request));
return true; return true;
break; break;
} }

View File

@@ -1,6 +1,6 @@
<?php <?php
function fail2ban($_action, $_data = null, $_extra = null) { function fail2ban($_action, $_data = null, $_extra = null) {
global $redis; global $valkey;
$_data_log = $_data; $_data_log = $_data;
switch ($_action) { switch ($_action) {
case 'get': case 'get':
@@ -9,9 +9,9 @@ function fail2ban($_action, $_data = null, $_extra = null) {
return false; return false;
} }
try { try {
$f2b_options = json_decode($redis->Get('F2B_OPTIONS'), true); $f2b_options = json_decode($valkey->Get('F2B_OPTIONS'), true);
$f2b_options['regex'] = json_decode($redis->Get('F2B_REGEX'), true); $f2b_options['regex'] = json_decode($valkey->Get('F2B_REGEX'), true);
$wl = $redis->hGetAll('F2B_WHITELIST'); $wl = $valkey->hGetAll('F2B_WHITELIST');
if (is_array($wl)) { if (is_array($wl)) {
foreach ($wl as $key => $value) { foreach ($wl as $key => $value) {
$tmp_wl_data[] = $key; $tmp_wl_data[] = $key;
@@ -27,7 +27,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
else { else {
$f2b_options['whitelist'] = ""; $f2b_options['whitelist'] = "";
} }
$bl = $redis->hGetAll('F2B_BLACKLIST'); $bl = $valkey->hGetAll('F2B_BLACKLIST');
if (is_array($bl)) { if (is_array($bl)) {
foreach ($bl as $key => $value) { foreach ($bl as $key => $value) {
$tmp_bl_data[] = $key; $tmp_bl_data[] = $key;
@@ -43,7 +43,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
else { else {
$f2b_options['blacklist'] = ""; $f2b_options['blacklist'] = "";
} }
$pb = $redis->hGetAll('F2B_PERM_BANS'); $pb = $valkey->hGetAll('F2B_PERM_BANS');
if (is_array($pb)) { if (is_array($pb)) {
foreach ($pb as $key => $value) { foreach ($pb as $key => $value) {
$f2b_options['perm_bans'][] = array( $f2b_options['perm_bans'][] = array(
@@ -56,8 +56,8 @@ function fail2ban($_action, $_data = null, $_extra = null) {
else { else {
$f2b_options['perm_bans'] = ""; $f2b_options['perm_bans'] = "";
} }
$active_bans = $redis->hGetAll('F2B_ACTIVE_BANS'); $active_bans = $valkey->hGetAll('F2B_ACTIVE_BANS');
$queue_unban = $redis->hGetAll('F2B_QUEUE_UNBAN'); $queue_unban = $valkey->hGetAll('F2B_QUEUE_UNBAN');
if (is_array($active_bans)) { if (is_array($active_bans)) {
foreach ($active_bans as $network => $banned_until) { foreach ($active_bans as $network => $banned_until) {
$queued_for_unban = (isset($queue_unban[$network]) && $queue_unban[$network] == 1) ? 1 : 0; $queued_for_unban = (isset($queue_unban[$network]) && $queue_unban[$network] == 1) ? 1 : 0;
@@ -78,7 +78,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -98,22 +98,22 @@ function fail2ban($_action, $_data = null, $_extra = null) {
// Reset regex filters // Reset regex filters
if ($_data['action'] == "reset-regex") { if ($_data['action'] == "reset-regex") {
try { try {
$redis->Del('F2B_REGEX'); $valkey->Del('F2B_REGEX');
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
// Rules will also be recreated on log events, but rules may seem empty for a second in the UI // Rules will also be recreated on log events, but rules may seem empty for a second in the UI
docker('post', 'netfilter-mailcow', 'restart'); docker('post', 'netfilter-mailcow', 'restart');
$fail_count = 0; $fail_count = 0;
$regex_result = json_decode($redis->Get('F2B_REGEX'), true); $regex_result = json_decode($valkey->Get('F2B_REGEX'), true);
while (empty($regex_result) && $fail_count < 10) { while (empty($regex_result) && $fail_count < 10) {
$regex_result = json_decode($redis->Get('F2B_REGEX'), true); $regex_result = json_decode($valkey->Get('F2B_REGEX'), true);
$fail_count++; $fail_count++;
sleep(1); sleep(1);
} }
@@ -135,7 +135,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$rule_id++; $rule_id++;
} }
if (!empty($regex_array)) { if (!empty($regex_array)) {
$redis->Set('F2B_REGEX', json_encode($regex_array, JSON_UNESCAPED_SLASHES)); $valkey->Set('F2B_REGEX', json_encode($regex_array, JSON_UNESCAPED_SLASHES));
} }
} }
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -154,13 +154,13 @@ function fail2ban($_action, $_data = null, $_extra = null) {
if ($_data['action'] == "unban") { if ($_data['action'] == "unban") {
if (valid_network($network)) { if (valid_network($network)) {
try { try {
$redis->hSet('F2B_QUEUE_UNBAN', $network, 1); $valkey->hSet('F2B_QUEUE_UNBAN', $network, 1);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -171,15 +171,15 @@ function fail2ban($_action, $_data = null, $_extra = null) {
if (empty($network)) { continue; } if (empty($network)) { continue; }
if (valid_network($network)) { if (valid_network($network)) {
try { try {
$redis->hSet('F2B_WHITELIST', $network, 1); $valkey->hSet('F2B_WHITELIST', $network, 1);
$redis->hDel('F2B_BLACKLIST', $network, 1); $valkey->hDel('F2B_BLACKLIST', $network, 1);
$redis->hSet('F2B_QUEUE_UNBAN', $network, 1); $valkey->hSet('F2B_QUEUE_UNBAN', $network, 1);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -204,15 +204,15 @@ function fail2ban($_action, $_data = null, $_extra = null) {
getenv('IPV6_NETWORK') getenv('IPV6_NETWORK')
))) { ))) {
try { try {
$redis->hSet('F2B_BLACKLIST', $network, 1); $valkey->hSet('F2B_BLACKLIST', $network, 1);
$redis->hDel('F2B_WHITELIST', $network, 1); $valkey->hDel('F2B_WHITELIST', $network, 1);
//$response = docker('post', 'netfilter-mailcow', 'restart'); //$response = docker('post', 'netfilter-mailcow', 'restart');
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -270,16 +270,16 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$f2b_options['banlist_id'] = $is_now['banlist_id']; $f2b_options['banlist_id'] = $is_now['banlist_id'];
$f2b_options['manage_external'] = ($manage_external > 0) ? 1 : 0; $f2b_options['manage_external'] = ($manage_external > 0) ? 1 : 0;
try { try {
$redis->Set('F2B_OPTIONS', json_encode($f2b_options)); $valkey->Set('F2B_OPTIONS', json_encode($f2b_options));
$redis->Del('F2B_WHITELIST'); $valkey->Del('F2B_WHITELIST');
$redis->Del('F2B_BLACKLIST'); $valkey->Del('F2B_BLACKLIST');
if(!empty($wl)) { if(!empty($wl)) {
$wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl)); $wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl));
$wl_array = array_filter($wl_array); $wl_array = array_filter($wl_array);
if (is_array($wl_array)) { if (is_array($wl_array)) {
foreach ($wl_array as $wl_item) { foreach ($wl_array as $wl_item) {
if (valid_network($wl_item) || valid_hostname($wl_item)) { if (valid_network($wl_item) || valid_hostname($wl_item)) {
$redis->hSet('F2B_WHITELIST', $wl_item, 1); $valkey->hSet('F2B_WHITELIST', $wl_item, 1);
} }
else { else {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -304,7 +304,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
getenv('IPV4_NETWORK') . '0', getenv('IPV4_NETWORK') . '0',
getenv('IPV6_NETWORK') getenv('IPV6_NETWORK')
))) { ))) {
$redis->hSet('F2B_BLACKLIST', $bl_item, 1); $valkey->hSet('F2B_BLACKLIST', $bl_item, 1);
} }
else { else {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -322,7 +322,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -334,13 +334,13 @@ function fail2ban($_action, $_data = null, $_extra = null) {
break; break;
case 'banlist': case 'banlist':
try { try {
$f2b_options = json_decode($redis->Get('F2B_OPTIONS'), true); $f2b_options = json_decode($valkey->Get('F2B_OPTIONS'), true);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra), 'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
http_response_code(500); http_response_code(500);
return false; return false;
@@ -356,14 +356,14 @@ function fail2ban($_action, $_data = null, $_extra = null) {
switch ($_data) { switch ($_data) {
case 'get': case 'get':
try { try {
$bl = $redis->hKeys('F2B_BLACKLIST'); $bl = $valkey->hKeys('F2B_BLACKLIST');
$active_bans = $redis->hKeys('F2B_ACTIVE_BANS'); $active_bans = $valkey->hKeys('F2B_ACTIVE_BANS');
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra), 'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
http_response_code(500); http_response_code(500);
return false; return false;
@@ -378,13 +378,13 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$f2b_options['banlist_id'] = uuid4(); $f2b_options['banlist_id'] = uuid4();
try { try {
$redis->Set('F2B_OPTIONS', json_encode($f2b_options)); $valkey->Set('F2B_OPTIONS', json_encode($f2b_options));
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra), 'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }

View File

@@ -1,7 +1,7 @@
<?php <?php
function fwdhost($_action, $_data = null) { function fwdhost($_action, $_data = null) {
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/spf.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/spf.inc.php';
global $redis; global $valkey;
global $lang; global $lang;
$_data_log = $_data; $_data_log = $_data;
switch ($_action) { switch ($_action) {
@@ -37,19 +37,19 @@ function fwdhost($_action, $_data = null) {
} }
foreach ($hosts as $host) { foreach ($hosts as $host) {
try { try {
$redis->hSet('WHITELISTED_FWD_HOST', $host, $source); $valkey->hSet('WHITELISTED_FWD_HOST', $host, $source);
if ($filter_spam == 0) { if ($filter_spam == 0) {
$redis->hSet('KEEP_SPAM', $host, 1); $valkey->hSet('KEEP_SPAM', $host, 1);
} }
elseif ($redis->hGet('KEEP_SPAM', $host)) { elseif ($valkey->hGet('KEEP_SPAM', $host)) {
$redis->hDel('KEEP_SPAM', $host); $valkey->hDel('KEEP_SPAM', $host);
} }
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -86,17 +86,17 @@ function fwdhost($_action, $_data = null) {
} }
try { try {
if ($keep_spam == 1) { if ($keep_spam == 1) {
$redis->hSet('KEEP_SPAM', $fwdhost, 1); $valkey->hSet('KEEP_SPAM', $fwdhost, 1);
} }
else { else {
$redis->hDel('KEEP_SPAM', $fwdhost); $valkey->hDel('KEEP_SPAM', $fwdhost);
} }
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -111,14 +111,14 @@ function fwdhost($_action, $_data = null) {
$hosts = (array)$_data['forwardinghost']; $hosts = (array)$_data['forwardinghost'];
foreach ($hosts as $host) { foreach ($hosts as $host) {
try { try {
$redis->hDel('WHITELISTED_FWD_HOST', $host); $valkey->hDel('WHITELISTED_FWD_HOST', $host);
$redis->hDel('KEEP_SPAM', $host); $valkey->hDel('KEEP_SPAM', $host);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -135,10 +135,10 @@ function fwdhost($_action, $_data = null) {
} }
$fwdhostsdata = array(); $fwdhostsdata = array();
try { try {
$fwd_hosts = $redis->hGetAll('WHITELISTED_FWD_HOST'); $fwd_hosts = $valkey->hGetAll('WHITELISTED_FWD_HOST');
if (!empty($fwd_hosts)) { if (!empty($fwd_hosts)) {
foreach ($fwd_hosts as $fwd_host => $source) { foreach ($fwd_hosts as $fwd_host => $source) {
$keep_spam = ($redis->hGet('KEEP_SPAM', $fwd_host)) ? "yes" : "no"; $keep_spam = ($valkey->hGet('KEEP_SPAM', $fwd_host)) ? "yes" : "no";
$fwdhostsdata[] = array( $fwdhostsdata[] = array(
'host' => $fwd_host, 'host' => $fwd_host,
'source' => $source, 'source' => $source,
@@ -151,7 +151,7 @@ function fwdhost($_action, $_data = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -163,17 +163,17 @@ function fwdhost($_action, $_data = null) {
return false; return false;
} }
try { try {
if ($source = $redis->hGet('WHITELISTED_FWD_HOST', $_data)) { if ($source = $valkey->hGet('WHITELISTED_FWD_HOST', $_data)) {
$fwdhostdetails['host'] = $_data; $fwdhostdetails['host'] = $_data;
$fwdhostdetails['source'] = $source; $fwdhostdetails['source'] = $source;
$fwdhostdetails['keep_spam'] = ($redis->hGet('KEEP_SPAM', $_data)) ? "yes" : "no"; $fwdhostdetails['keep_spam'] = ($valkey->hGet('KEEP_SPAM', $_data)) ? "yes" : "no";
} }
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }

View File

@@ -126,7 +126,7 @@ function hash_password($password) {
return $pw_hash; return $pw_hash;
} }
function password_complexity($_action, $_data = null) { function password_complexity($_action, $_data = null) {
global $redis; global $valkey;
global $lang; global $lang;
switch ($_action) { switch ($_action) {
case 'edit': case 'edit':
@@ -147,7 +147,7 @@ function password_complexity($_action, $_data = null) {
$numbers = (isset($_data['numbers'])) ? intval($_data['numbers']) : $is_now['numbers']; $numbers = (isset($_data['numbers'])) ? intval($_data['numbers']) : $is_now['numbers'];
} }
try { try {
$redis->hMSet('PASSWD_POLICY', [ $valkey->hMSet('PASSWD_POLICY', [
'length' => $length, 'length' => $length,
'chars' => $chars, 'chars' => $chars,
'special_chars' => $special_chars, 'special_chars' => $special_chars,
@@ -159,7 +159,7 @@ function password_complexity($_action, $_data = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data), 'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -171,11 +171,11 @@ function password_complexity($_action, $_data = null) {
break; break;
case 'get': case 'get':
try { try {
$length = $redis->hGet('PASSWD_POLICY', 'length'); $length = $valkey->hGet('PASSWD_POLICY', 'length');
$chars = $redis->hGet('PASSWD_POLICY', 'chars'); $chars = $valkey->hGet('PASSWD_POLICY', 'chars');
$special_chars = $redis->hGet('PASSWD_POLICY', 'special_chars'); $special_chars = $valkey->hGet('PASSWD_POLICY', 'special_chars');
$lowerupper = $redis->hGet('PASSWD_POLICY', 'lowerupper'); $lowerupper = $valkey->hGet('PASSWD_POLICY', 'lowerupper');
$numbers = $redis->hGet('PASSWD_POLICY', 'numbers'); $numbers = $valkey->hGet('PASSWD_POLICY', 'numbers');
return array( return array(
'length' => $length, 'length' => $length,
'chars' => $chars, 'chars' => $chars,
@@ -188,7 +188,7 @@ function password_complexity($_action, $_data = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data), 'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -253,7 +253,7 @@ function password_check($password1, $password2) {
} }
function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) { function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {
global $pdo; global $pdo;
global $redis; global $valkey;
$sasl_limit_days = intval($sasl_limit_days); $sasl_limit_days = intval($sasl_limit_days);
switch ($action) { switch ($action) {
case 'get': case 'get':
@@ -272,13 +272,13 @@ function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {
} }
elseif (filter_var($sasl[$k]['real_rip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { elseif (filter_var($sasl[$k]['real_rip'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
try { try {
$sasl[$k]['location'] = $redis->hGet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip']); $sasl[$k]['location'] = $valkey->hGet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip']);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -294,13 +294,13 @@ function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {
if ($ip_data_array !== false and !empty($ip_data_array['shortcountry'])) { if ($ip_data_array !== false and !empty($ip_data_array['shortcountry'])) {
$sasl[$k]['location'] = $ip_data_array['shortcountry']; $sasl[$k]['location'] = $ip_data_array['shortcountry'];
try { try {
$redis->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['shortcountry']); $valkey->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['shortcountry']);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
curl_close($curl); curl_close($curl);
return false; return false;
@@ -1107,11 +1107,21 @@ function user_get_alias_details($username) {
} }
return $data; return $data;
} }
function is_valid_domain_name($domain_name) { function is_valid_domain_name($domain_name, $options = array()) {
if (empty($domain_name)) { if (empty($domain_name)) {
return false; return false;
} }
// Convert domain name to ASCII for validation
$domain_name = idn_to_ascii($domain_name, 0, INTL_IDNA_VARIANT_UTS46); $domain_name = idn_to_ascii($domain_name, 0, INTL_IDNA_VARIANT_UTS46);
if (isset($options['allow_wildcard']) && $options['allow_wildcard'] == true) {
// Remove '*.' if wildcard subdomains are allowed
if (strpos($domain_name, '*.') === 0) {
$domain_name = substr($domain_name, 2);
}
}
return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name) return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name)
&& preg_match("/^.{1,253}$/", $domain_name) && preg_match("/^.{1,253}$/", $domain_name)
&& preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name)); && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name));
@@ -1989,7 +1999,7 @@ function admin_api($access, $action, $data = null) {
} }
function license($action, $data = null) { function license($action, $data = null) {
global $pdo; global $pdo;
global $redis; global $valkey;
global $lang; global $lang;
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -2039,13 +2049,13 @@ function license($action, $data = null) {
} }
try { try {
// json_encode needs "true"/"false" instead of true/false, to not encode it to 0 or 1 // json_encode needs "true"/"false" instead of true/false, to not encode it to 0 or 1
$redis->Set('LICENSE_STATUS_CACHE', json_encode($_SESSION['gal'])); $valkey->Set('LICENSE_STATUS_CACHE', json_encode($_SESSION['gal']));
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -2126,7 +2136,7 @@ function rspamd_ui($action, $data = null) {
} }
} }
function cors($action, $data = null) { function cors($action, $data = null) {
global $redis; global $valkey;
switch ($action) { switch ($action) {
case "edit": case "edit":
@@ -2167,7 +2177,7 @@ function cors($action, $data = null) {
} }
try { try {
$redis->hMSet('CORS_SETTINGS', array( $valkey->hMSet('CORS_SETTINGS', array(
'allowed_origins' => implode(', ', $allowed_origins), 'allowed_origins' => implode(', ', $allowed_origins),
'allowed_methods' => implode(', ', $allowed_methods) 'allowed_methods' => implode(', ', $allowed_methods)
)); ));
@@ -2175,7 +2185,7 @@ function cors($action, $data = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $action, $data), 'log' => array(__FUNCTION__, $action, $data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -2189,12 +2199,12 @@ function cors($action, $data = null) {
break; break;
case "get": case "get":
try { try {
$cors_settings = $redis->hMGet('CORS_SETTINGS', array('allowed_origins', 'allowed_methods')); $cors_settings = $valkey->hMGet('CORS_SETTINGS', array('allowed_origins', 'allowed_methods'));
} catch (RedisException $e) { } catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $action, $data), 'log' => array(__FUNCTION__, $action, $data),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
} }
@@ -2211,7 +2221,7 @@ function cors($action, $data = null) {
$cors_settings['allowed_origins'] = $allowed_origins[0]; $cors_settings['allowed_origins'] = $allowed_origins[0];
if (in_array('*', $allowed_origins)){ if (in_array('*', $allowed_origins)){
$cors_settings['allowed_origins'] = '*'; $cors_settings['allowed_origins'] = '*';
} else if (in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) { } else if (array_key_exists('HTTP_ORIGIN', $_SERVER) && in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
$cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN']; $cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN'];
} }
// always allow OPTIONS for preflight request // always allow OPTIONS for preflight request
@@ -2957,7 +2967,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) {
} }
function reset_password($action, $data = null) { function reset_password($action, $data = null) {
global $pdo; global $pdo;
global $redis; global $valkey;
global $mailcow_hostname; global $mailcow_hostname;
global $PW_RESET_TOKEN_LIMIT; global $PW_RESET_TOKEN_LIMIT;
global $PW_RESET_TOKEN_LIFETIME; global $PW_RESET_TOKEN_LIFETIME;
@@ -3193,10 +3203,10 @@ function reset_password($action, $data = null) {
$type = $data; $type = $data;
try { try {
$settings['from'] = $redis->Get('PW_RESET_FROM'); $settings['from'] = $valkey->Get('PW_RESET_FROM');
$settings['subject'] = $redis->Get('PW_RESET_SUBJ'); $settings['subject'] = $valkey->Get('PW_RESET_SUBJ');
$settings['html_tmpl'] = $redis->Get('PW_RESET_HTML'); $settings['html_tmpl'] = $valkey->Get('PW_RESET_HTML');
$settings['text_tmpl'] = $redis->Get('PW_RESET_TEXT'); $settings['text_tmpl'] = $valkey->Get('PW_RESET_TEXT');
if (empty($settings['html_tmpl']) && empty($settings['text_tmpl'])) { if (empty($settings['html_tmpl']) && empty($settings['text_tmpl'])) {
$settings['html_tmpl'] = file_get_contents("/tpls/pw_reset_html.tpl"); $settings['html_tmpl'] = file_get_contents("/tpls/pw_reset_html.tpl");
$settings['text_tmpl'] = file_get_contents("/tpls/pw_reset_text.tpl"); $settings['text_tmpl'] = file_get_contents("/tpls/pw_reset_text.tpl");
@@ -3211,7 +3221,7 @@ function reset_password($action, $data = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log), 'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -3314,16 +3324,16 @@ function reset_password($action, $data = null) {
$html = (empty($data['html_tmpl'])) ? "" : $data['html_tmpl']; $html = (empty($data['html_tmpl'])) ? "" : $data['html_tmpl'];
try { try {
$redis->Set('PW_RESET_FROM', $from); $valkey->Set('PW_RESET_FROM', $from);
$redis->Set('PW_RESET_SUBJ', $subject); $valkey->Set('PW_RESET_SUBJ', $subject);
$redis->Set('PW_RESET_HTML', $html); $valkey->Set('PW_RESET_HTML', $html);
$redis->Set('PW_RESET_TEXT', $text); $valkey->Set('PW_RESET_TEXT', $text);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log), 'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -3366,7 +3376,7 @@ function get_logs($application, $lines = false) {
$to = intval($to); $to = intval($to);
if ($from < 1 || $to < $from) { return false; } if ($from < 1 || $to < $from) { return false; }
} }
global $redis; global $valkey;
global $pdo; global $pdo;
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
return false; return false;
@@ -3415,10 +3425,10 @@ function get_logs($application, $lines = false) {
// Redis // Redis
if ($application == "dovecot-mailcow") { if ($application == "dovecot-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('DOVECOT_MAILLOG', $from - 1, $to - 1); $data = $valkey->lRange('DOVECOT_MAILLOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('DOVECOT_MAILLOG', 0, $lines); $data = $valkey->lRange('DOVECOT_MAILLOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3429,10 +3439,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "cron-mailcow") { if ($application == "cron-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('CRON_LOG', $from - 1, $to - 1); $data = $valkey->lRange('CRON_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('CRON_LOG', 0, $lines); $data = $valkey->lRange('CRON_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3443,10 +3453,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "postfix-mailcow") { if ($application == "postfix-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('POSTFIX_MAILLOG', $from - 1, $to - 1); $data = $valkey->lRange('POSTFIX_MAILLOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('POSTFIX_MAILLOG', 0, $lines); $data = $valkey->lRange('POSTFIX_MAILLOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3457,10 +3467,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "sogo-mailcow") { if ($application == "sogo-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('SOGO_LOG', $from - 1, $to - 1); $data = $valkey->lRange('SOGO_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('SOGO_LOG', 0, $lines); $data = $valkey->lRange('SOGO_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3471,10 +3481,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "watchdog-mailcow") { if ($application == "watchdog-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('WATCHDOG_LOG', $from - 1, $to - 1); $data = $valkey->lRange('WATCHDOG_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('WATCHDOG_LOG', 0, $lines); $data = $valkey->lRange('WATCHDOG_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3485,10 +3495,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "acme-mailcow") { if ($application == "acme-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('ACME_LOG', $from - 1, $to - 1); $data = $valkey->lRange('ACME_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('ACME_LOG', 0, $lines); $data = $valkey->lRange('ACME_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3499,10 +3509,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "ratelimited") { if ($application == "ratelimited") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('RL_LOG', $from - 1, $to - 1); $data = $valkey->lRange('RL_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('RL_LOG', 0, $lines); $data = $valkey->lRange('RL_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3513,10 +3523,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "api-mailcow") { if ($application == "api-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('API_LOG', $from - 1, $to - 1); $data = $valkey->lRange('API_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('API_LOG', 0, $lines); $data = $valkey->lRange('API_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3527,10 +3537,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "netfilter-mailcow") { if ($application == "netfilter-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('NETFILTER_LOG', $from - 1, $to - 1); $data = $valkey->lRange('NETFILTER_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('NETFILTER_LOG', 0, $lines); $data = $valkey->lRange('NETFILTER_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3541,10 +3551,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "autodiscover-mailcow") { if ($application == "autodiscover-mailcow") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $redis->lRange('AUTODISCOVER_LOG', $from - 1, $to - 1); $data = $valkey->lRange('AUTODISCOVER_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $redis->lRange('AUTODISCOVER_LOG', 0, $lines); $data = $valkey->lRange('AUTODISCOVER_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {

View File

@@ -1,7 +1,7 @@
<?php <?php
function mailbox($_action, $_type, $_data = null, $_extra = null) { function mailbox($_action, $_type, $_data = null, $_extra = null) {
global $pdo; global $pdo;
global $redis; global $valkey;
global $lang; global $lang;
global $MAILBOX_DEFAULT_ATTRIBUTES; global $MAILBOX_DEFAULT_ATTRIBUTES;
global $iam_settings; global $iam_settings;
@@ -628,13 +628,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
try { try {
$redis->hSet('DOMAIN_MAP', $domain, 1); $valkey->hSet('DOMAIN_MAP', $domain, 1);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -646,7 +646,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['key_size'] = (isset($_data['key_size'])) ? intval($_data['key_size']) : $DOMAIN_DEFAULT_ATTRIBUTES['key_size']; $_data['key_size'] = (isset($_data['key_size'])) ? intval($_data['key_size']) : $DOMAIN_DEFAULT_ATTRIBUTES['key_size'];
$_data['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : $DOMAIN_DEFAULT_ATTRIBUTES['dkim_selector']; $_data['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : $DOMAIN_DEFAULT_ATTRIBUTES['dkim_selector'];
if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) { if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) {
if (!empty($redis->hGet('DKIM_SELECTORS', $domain))) { if (!empty($valkey->hGet('DKIM_SELECTORS', $domain))) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -684,15 +684,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return true; return true;
break; break;
case 'alias': case 'alias':
$addresses = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['address'])); $addresses = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['address']));
$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto'])); $gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto']));
$active = intval($_data['active']); $internal = intval($_data['internal']);
$sogo_visible = intval($_data['sogo_visible']); $active = intval($_data['active']);
$goto_null = intval($_data['goto_null']); $sogo_visible = intval($_data['sogo_visible']);
$goto_spam = intval($_data['goto_spam']); $goto_null = intval($_data['goto_null']);
$goto_ham = intval($_data['goto_ham']); $goto_spam = intval($_data['goto_spam']);
$goto_ham = intval($_data['goto_ham']);
$private_comment = $_data['private_comment']; $private_comment = $_data['private_comment'];
$public_comment = $_data['public_comment']; $public_comment = $_data['public_comment'];
if (strlen($private_comment) > 160 | strlen($public_comment) > 160){ if (strlen($private_comment) > 160 | strlen($public_comment) > 160){
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@@ -842,8 +843,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
continue; continue;
} }
$stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `active`) $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `active`)
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :active)"); VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :active)");
if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) { if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) {
$stmt->execute(array( $stmt->execute(array(
':address' => '@'.$domain, ':address' => '@'.$domain,
@@ -853,6 +854,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':goto' => $goto, ':goto' => $goto,
':domain' => $domain, ':domain' => $domain,
':sogo_visible' => $sogo_visible, ':sogo_visible' => $sogo_visible,
':internal' => $internal,
':active' => $active ':active' => $active
)); ));
} }
@@ -864,6 +866,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':goto' => $goto, ':goto' => $goto,
':domain' => $domain, ':domain' => $domain,
':sogo_visible' => $sogo_visible, ':sogo_visible' => $sogo_visible,
':internal' => $internal,
':active' => $active ':active' => $active
)); ));
} }
@@ -971,13 +974,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':active' => $active ':active' => $active
)); ));
try { try {
$redis->hSet('DOMAIN_MAP', $alias_domain, 1); $valkey->hSet('DOMAIN_MAP', $alias_domain, 1);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -985,7 +988,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
ratelimit('edit', 'domain', array('rl_value' => $_data['rl_value'], 'rl_frame' => $_data['rl_frame'], 'object' => $alias_domain)); ratelimit('edit', 'domain', array('rl_value' => $_data['rl_value'], 'rl_frame' => $_data['rl_frame'], 'object' => $alias_domain));
} }
if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) { if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) {
if (!empty($redis->hGet('DKIM_SELECTORS', $alias_domain))) { if (!empty($valkey->hGet('DKIM_SELECTORS', $alias_domain))) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -1223,6 +1226,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array( $stmt->execute(array(
':username' => $username ':username' => $username
)); ));
// save delimiter_action
if (isset($_data['tagged_mail_handler'])) {
mailbox('edit', 'delimiter_action', array(
'username' => $username,
'tagged_mail_handler' => $_data['tagged_mail_handler']
));
}
// save tags // save tags
foreach($tags as $index => $tag){ foreach($tags as $index => $tag){
if (empty($tag)) continue; if (empty($tag)) continue;
@@ -1392,6 +1403,80 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return mailbox('add', 'mailbox', $mailbox_attributes); return mailbox('add', 'mailbox', $mailbox_attributes);
break; break;
case 'mta_sts':
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
$version = strtolower($_data['version']);
$mode = strtolower($_data['mode']);
$mx = explode(",", preg_replace('/\s+/', '', $_data['mx']));
$max_age = intval($_data['max_age']);
$active = (intval($_data['active']) == 1) ? 1 : 0;
$id = date('YmdHis');
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => 'access_denied'
);
return false;
}
if (empty($version) || !in_array($version, array('stsv1'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('version_invalid', htmlspecialchars($domain))
);
return false;
}
if (empty($mode) || !in_array($mode, array('enforce', 'testing', 'none'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('mode_invalid', htmlspecialchars($domain))
);
return false;
}
if (empty($max_age) || $max_age < 0 || $max_age > 31536000) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('max_age_invalid', htmlspecialchars($domain))
);
return false;
}
foreach ($mx as $index => $mx_domain) {
$mx_domain = idn_to_ascii(strtolower(trim($mx_domain)), 0, INTL_IDNA_VARIANT_UTS46);
if (!is_valid_domain_name($mx_domain, array('allow_wildcard' => true))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('mx_invalid', htmlspecialchars($mx_domain))
);
return false;
}
}
try {
$stmt = $pdo->prepare("INSERT INTO `mta_sts` (`id`, `domain`, `version`, `mode`, `mx`, `max_age`, `active`)
VALUES (:id, :domain, :version, :mode, :mx, :max_age, :active)");
$stmt->execute(array(
':id' => $id,
':domain' => $domain,
':version' => $version,
':mode' => $mode,
':mx' => implode(",", $mx),
':max_age' => $max_age,
':active' => $active
));
} catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data),
'msg' => $e->getMessage()
);
return false;
}
break;
case 'resource': case 'resource':
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46); $domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
$description = $_data['description']; $description = $_data['description'];
@@ -1613,6 +1698,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr = array(); $attr = array();
$attr["quota"] = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0; $attr["quota"] = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0;
$attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array(); $attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array();
$attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['tagged_mail_handler']);
$attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']); $attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
$attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']); $attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s"; $attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
@@ -2061,42 +2147,42 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
if (isset($_data['tagged_mail_handler']) && $_data['tagged_mail_handler'] == "subject") { if (isset($_data['tagged_mail_handler']) && $_data['tagged_mail_handler'] == "subject") {
try { try {
$redis->hSet('RCPT_WANTS_SUBJECT_TAG', $username, 1); $valkey->hSet('RCPT_WANTS_SUBJECT_TAG', $username, 1);
$redis->hDel('RCPT_WANTS_SUBFOLDER_TAG', $username); $valkey->hDel('RCPT_WANTS_SUBFOLDER_TAG', $username);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
} }
else if (isset($_data['tagged_mail_handler']) && $_data['tagged_mail_handler'] == "subfolder") { else if (isset($_data['tagged_mail_handler']) && $_data['tagged_mail_handler'] == "subfolder") {
try { try {
$redis->hSet('RCPT_WANTS_SUBFOLDER_TAG', $username, 1); $valkey->hSet('RCPT_WANTS_SUBFOLDER_TAG', $username, 1);
$redis->hDel('RCPT_WANTS_SUBJECT_TAG', $username); $valkey->hDel('RCPT_WANTS_SUBJECT_TAG', $username);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
} }
else { else {
try { try {
$redis->hDel('RCPT_WANTS_SUBJECT_TAG', $username); $valkey->hDel('RCPT_WANTS_SUBJECT_TAG', $username);
$redis->hDel('RCPT_WANTS_SUBFOLDER_TAG', $username); $valkey->hDel('RCPT_WANTS_SUBFOLDER_TAG', $username);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -2398,6 +2484,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
foreach ($ids as $id) { foreach ($ids as $id) {
$is_now = mailbox('get', 'alias_details', $id); $is_now = mailbox('get', 'alias_details', $id);
if (!empty($is_now)) { if (!empty($is_now)) {
$internal = (isset($_data['internal'])) ? intval($_data['internal']) : $is_now['internal'];
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$sogo_visible = (isset($_data['sogo_visible'])) ? intval($_data['sogo_visible']) : $is_now['sogo_visible']; $sogo_visible = (isset($_data['sogo_visible'])) ? intval($_data['sogo_visible']) : $is_now['sogo_visible'];
$goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0; $goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0;
@@ -2583,6 +2670,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`domain` = :domain, `domain` = :domain,
`goto` = :goto, `goto` = :goto,
`sogo_visible`= :sogo_visible, `sogo_visible`= :sogo_visible,
`internal`= :internal,
`active`= :active `active`= :active
WHERE `id` = :id"); WHERE `id` = :id");
$stmt->execute(array( $stmt->execute(array(
@@ -2592,6 +2680,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':domain' => $domain, ':domain' => $domain,
':goto' => $goto, ':goto' => $goto,
':sogo_visible' => $sogo_visible, ':sogo_visible' => $sogo_visible,
':internal' => $internal,
':active' => $active, ':active' => $active,
':id' => $is_now['id'] ':id' => $is_now['id']
)); ));
@@ -3259,6 +3348,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
return false; return false;
} }
// save delimiter_action
if (isset($_data['tagged_mail_handler'])) {
mailbox('edit', 'delimiter_action', array(
'username' => $username,
'tagged_mail_handler' => $_data['tagged_mail_handler']
));
}
// save tags // save tags
foreach($tags as $index => $tag){ foreach($tags as $index => $tag){
if (empty($tag)) continue; if (empty($tag)) continue;
@@ -3604,6 +3700,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr = array(); $attr = array();
$attr["quota"] = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0; $attr["quota"] = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0;
$attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : $is_now['tags']; $attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : $is_now['tags'];
$attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : $is_now['tagged_mail_handler'];
$attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification']; $attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification'];
$attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category']; $attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category'];
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : $is_now['rl_frame']; $attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : $is_now['rl_frame'];
@@ -3724,6 +3821,125 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return true; return true;
break; break;
case 'mta_sts':
if (!is_array($_data['domains'])) {
$domains = array();
$domains[] = $_data['domains'];
}
else {
$domains = $_data['domains'];
}
foreach ($domains as $domain) {
$domain = idn_to_ascii(strtolower(trim($domain)), 0, INTL_IDNA_VARIANT_UTS46);
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => 'access_denied'
);
continue;
}
$is_now = mailbox('get', 'mta_sts', $domain);
if (!empty($is_now)) {
$version = (isset($_data['version'])) ? strtolower($_data['version']) : $is_now['version'];
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$active = ($active == 1) ? 1 : 0;
$mode = (isset($_data['mode'])) ? strtolower($_data['mode']) : $is_now['mode'];
$mx = (isset($_data['mx'])) ? explode(",", preg_replace('/\s+/', '', $_data['mx'])) : $is_now['mx'];
$max_age = (isset($_data['max_age'])) ? intval($_data['max_age']) : $is_now['max_age'];
// Update ID if neccesary
if ($version != strtolower($is_now['version']) ||
$mode != strtolower($is_now['mode']) ||
$mx != $is_now['mx'] ||
$max_age != $is_now['max_age']) {
$id = date('YmdHis');
} else {
$id = $is_now['id'];
}
} else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (empty($version) || !in_array($version, array('stsv1'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('version_invalid', htmlspecialchars($version))
);
continue;
}
if (empty($mode) || !in_array($mode, array('enforce', 'testing', 'none'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('mode_invalid', htmlspecialchars($domain))
);
continue;
}
if (empty($max_age) || $max_age < 0 || $max_age > 31557600) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('max_age_invalid', htmlspecialchars($domain))
);
continue;
}
foreach ($mx as $index => $mx_domain) {
$mx_domain = idn_to_ascii(strtolower(trim($mx_domain)), 0, INTL_IDNA_VARIANT_UTS46);
$invalid_mx = false;
if (!is_valid_domain_name($mx_domain, array('allow_wildcard' => true))) {
$invalid_mx = $mx_domain;
break;
}
}
if ($invalid_mx) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('mx_invalid', htmlspecialchars($invalid_mx))
);
continue;
}
try {
$stmt = $pdo->prepare("UPDATE `mta_sts` SET `id` = :id, `version` = :version, `mode` = :mode, `mx` = :mx, `max_age` = :max_age, `active` = :active WHERE `domain` = :domain");
$stmt->execute(array(
':id' => $id,
':domain' => $domain,
':version' => $version,
':mode' => $mode,
':mx' => implode(",", $mx),
':max_age' => $max_age,
':active' => $active
));
} catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data),
'msg' => $e->getMessage()
);
continue;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr),
'msg' => array('object_modified', $domain)
);
}
return true;
break;
case 'resource': case 'resource':
if (!is_array($_data['name'])) { if (!is_array($_data['name'])) {
$names = array(); $names = array();
@@ -4387,10 +4603,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data = $_SESSION['mailcow_cc_username']; $_data = $_SESSION['mailcow_cc_username'];
} }
try { try {
if ($redis->hGet('RCPT_WANTS_SUBJECT_TAG', $_data)) { if ($valkey->hGet('RCPT_WANTS_SUBJECT_TAG', $_data)) {
return "subject"; return "subject";
} }
elseif ($redis->hGet('RCPT_WANTS_SUBFOLDER_TAG', $_data)) { elseif ($valkey->hGet('RCPT_WANTS_SUBFOLDER_TAG', $_data)) {
return "subfolder"; return "subfolder";
} }
else { else {
@@ -4401,7 +4617,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -4490,6 +4706,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`address`, `address`,
`public_comment`, `public_comment`,
`private_comment`, `private_comment`,
`internal`,
`active`, `active`,
`sogo_visible`, `sogo_visible`,
`created`, `created`,
@@ -4520,6 +4737,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$aliasdata['goto'] = $row['goto']; $aliasdata['goto'] = $row['goto'];
$aliasdata['address'] = $row['address']; $aliasdata['address'] = $row['address'];
(!filter_var($aliasdata['address'], FILTER_VALIDATE_EMAIL)) ? $aliasdata['is_catch_all'] = 1 : $aliasdata['is_catch_all'] = 0; (!filter_var($aliasdata['address'], FILTER_VALIDATE_EMAIL)) ? $aliasdata['is_catch_all'] = 1 : $aliasdata['is_catch_all'] = 0;
$aliasdata['internal'] = $row['internal'];
$aliasdata['active'] = $row['active']; $aliasdata['active'] = $row['active'];
$aliasdata['active_int'] = $row['active']; $aliasdata['active_int'] = $row['active'];
$aliasdata['sogo_visible'] = $row['sogo_visible']; $aliasdata['sogo_visible'] = $row['sogo_visible'];
@@ -5012,6 +5230,20 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return $rows; return $rows;
} }
break; break;
case 'mta_sts':
$stmt = $pdo->prepare("SELECT * FROM `mta_sts` WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $_data,
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($row)){
return [];
}
$row['mx'] = explode(',', $row['mx']);
$row['version'] = strtoupper(substr($row['version'], 0, 3)) . substr($row['version'], 3);
return $row;
break;
case 'resource_details': case 'resource_details':
$resourcedata = array(); $resourcedata = array();
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) { if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
@@ -5397,17 +5629,21 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array( $stmt->execute(array(
':domain' => $domain, ':domain' => $domain,
)); ));
$stmt = $pdo->prepare("DELETE FROM `mta_sts` WHERE `domain` = :domain");
$stmt->execute(array(
':domain' => $domain,
));
$stmt = $pdo->query("DELETE FROM `admin` WHERE `superadmin` = 0 AND `username` NOT IN (SELECT `username`FROM `domain_admins`);"); $stmt = $pdo->query("DELETE FROM `admin` WHERE `superadmin` = 0 AND `username` NOT IN (SELECT `username`FROM `domain_admins`);");
$stmt = $pdo->query("DELETE FROM `da_acl` WHERE `username` NOT IN (SELECT `username`FROM `domain_admins`);"); $stmt = $pdo->query("DELETE FROM `da_acl` WHERE `username` NOT IN (SELECT `username`FROM `domain_admins`);");
try { try {
$redis->hDel('DOMAIN_MAP', $domain); $valkey->hDel('DOMAIN_MAP', $domain);
$redis->hDel('RL_VALUE', $domain); $valkey->hDel('RL_VALUE', $domain);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -5533,14 +5769,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':alias_domain' => $alias_domain, ':alias_domain' => $alias_domain,
)); ));
try { try {
$redis->hDel('DOMAIN_MAP', $alias_domain); $valkey->hDel('DOMAIN_MAP', $alias_domain);
$redis->hDel('RL_VALUE', $domain); $valkey->hDel('RL_VALUE', $domain);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -5719,13 +5955,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
)); ));
} }
try { try {
$redis->hDel('RL_VALUE', $username); $valkey->hDel('RL_VALUE', $username);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }

View File

@@ -1,7 +1,7 @@
<?php <?php
function oauth2($_action, $_type, $_data = null) { function oauth2($_action, $_type, $_data = null) {
global $pdo; global $pdo;
global $redis; global $valkey;
global $lang; global $lang;
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(

View File

@@ -1,7 +1,7 @@
<?php <?php
function policy($_action, $_scope, $_data = null) { function policy($_action, $_scope, $_data = null) {
global $pdo; global $pdo;
global $redis; global $valkey;
global $lang; global $lang;
$_data_log = $_data; $_data_log = $_data;
switch ($_action) { switch ($_action) {

View File

@@ -4,7 +4,7 @@ use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception; use PHPMailer\PHPMailer\Exception;
function quarantine($_action, $_data = null) { function quarantine($_action, $_data = null) {
global $pdo; global $pdo;
global $redis; global $valkey;
global $lang; global $lang;
$_data_log = $_data; $_data_log = $_data;
switch ($_action) { switch ($_action) {
@@ -22,7 +22,7 @@ function quarantine($_action, $_data = null) {
return false; return false;
} }
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt` $stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash WHERE `qhash` = :hash
AND user_acl.quarantine = 1 AND user_acl.quarantine = 1
AND rcpt IN (SELECT username FROM mailbox)'); AND rcpt IN (SELECT username FROM mailbox)');
$stmt->execute(array(':hash' => $hash)); $stmt->execute(array(':hash' => $hash));
@@ -65,7 +65,7 @@ function quarantine($_action, $_data = null) {
return false; return false;
} }
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt` $stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash WHERE `qhash` = :hash
AND `user_acl`.`quarantine` = 1 AND `user_acl`.`quarantine` = 1
AND `username` IN (SELECT `username` FROM `mailbox`)'); AND `username` IN (SELECT `username` FROM `mailbox`)');
$stmt->execute(array(':hash' => $hash)); $stmt->execute(array(':hash' => $hash));
@@ -102,14 +102,14 @@ function quarantine($_action, $_data = null) {
return false; return false;
} }
try { try {
$release_format = $redis->Get('Q_RELEASE_FORMAT'); $release_format = $valkey->Get('Q_RELEASE_FORMAT');
} }
catch (RedisException $e) { catch (RedisException $e) {
logger(array('return' => array( logger(array('return' => array(
array( array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
) )
))); )));
return false; return false;
@@ -180,7 +180,7 @@ function quarantine($_action, $_data = null) {
array('221', '') array('221', '')
); );
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php // Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1); $smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
if (!$smtp_connection) { if (!$smtp_connection) {
logger(array('return' => array( logger(array('return' => array(
array( array(
@@ -192,7 +192,7 @@ function quarantine($_action, $_data = null) {
return false; return false;
} }
for ($i=0; $i < count($postfix_talk); $i++) { for ($i=0; $i < count($postfix_talk); $i++) {
$smtp_resource = fgets($smtp_connection, 256); $smtp_resource = fgets($smtp_connection, 256);
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) { if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
$ret = substr($smtp_resource, 0, 3); $ret = substr($smtp_resource, 0, 3);
$ret = (empty($ret)) ? '-' : $ret; $ret = (empty($ret)) ? '-' : $ret;
@@ -332,23 +332,23 @@ function quarantine($_action, $_data = null) {
} }
$exclude_domains = (array)$_data['exclude_domains']; $exclude_domains = (array)$_data['exclude_domains'];
try { try {
$redis->Set('Q_RETENTION_SIZE', intval($retention_size)); $valkey->Set('Q_RETENTION_SIZE', intval($retention_size));
$redis->Set('Q_MAX_SIZE', intval($max_size)); $valkey->Set('Q_MAX_SIZE', intval($max_size));
$redis->Set('Q_MAX_SCORE', $max_score); $valkey->Set('Q_MAX_SCORE', $max_score);
$redis->Set('Q_MAX_AGE', $max_age); $valkey->Set('Q_MAX_AGE', $max_age);
$redis->Set('Q_EXCLUDE_DOMAINS', json_encode($exclude_domains)); $valkey->Set('Q_EXCLUDE_DOMAINS', json_encode($exclude_domains));
$redis->Set('Q_RELEASE_FORMAT', $release_format); $valkey->Set('Q_RELEASE_FORMAT', $release_format);
$redis->Set('Q_SENDER', $sender); $valkey->Set('Q_SENDER', $sender);
$redis->Set('Q_BCC', $bcc); $valkey->Set('Q_BCC', $bcc);
$redis->Set('Q_REDIRECT', $redirect); $valkey->Set('Q_REDIRECT', $redirect);
$redis->Set('Q_SUBJ', $subject); $valkey->Set('Q_SUBJ', $subject);
$redis->Set('Q_HTML', $html); $valkey->Set('Q_HTML', $html);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -403,13 +403,13 @@ function quarantine($_action, $_data = null) {
continue; continue;
} }
try { try {
$release_format = $redis->Get('Q_RELEASE_FORMAT'); $release_format = $valkey->Get('Q_RELEASE_FORMAT');
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -475,7 +475,7 @@ function quarantine($_action, $_data = null) {
array('221', '') array('221', '')
); );
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php // Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1); $smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
if (!$smtp_connection) { if (!$smtp_connection) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'warning', 'type' => 'warning',
@@ -485,7 +485,7 @@ function quarantine($_action, $_data = null) {
return false; return false;
} }
for ($i=0; $i < count($postfix_talk); $i++) { for ($i=0; $i < count($postfix_talk); $i++) {
$smtp_resource = fgets($smtp_connection, 256); $smtp_resource = fgets($smtp_connection, 256);
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) { if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
$ret = substr($smtp_resource, 0, 3); $ret = substr($smtp_resource, 0, 3);
$ret = (empty($ret)) ? '-' : $ret; $ret = (empty($ret)) ? '-' : $ret;
@@ -776,18 +776,18 @@ function quarantine($_action, $_data = null) {
case 'settings': case 'settings':
try { try {
if ($_SESSION['mailcow_cc_role'] == "admin") { if ($_SESSION['mailcow_cc_role'] == "admin") {
$settings['exclude_domains'] = json_decode($redis->Get('Q_EXCLUDE_DOMAINS'), true); $settings['exclude_domains'] = json_decode($valkey->Get('Q_EXCLUDE_DOMAINS'), true);
} }
$settings['max_size'] = $redis->Get('Q_MAX_SIZE'); $settings['max_size'] = $valkey->Get('Q_MAX_SIZE');
$settings['max_score'] = $redis->Get('Q_MAX_SCORE'); $settings['max_score'] = $valkey->Get('Q_MAX_SCORE');
$settings['max_age'] = $redis->Get('Q_MAX_AGE'); $settings['max_age'] = $valkey->Get('Q_MAX_AGE');
$settings['retention_size'] = $redis->Get('Q_RETENTION_SIZE'); $settings['retention_size'] = $valkey->Get('Q_RETENTION_SIZE');
$settings['release_format'] = $redis->Get('Q_RELEASE_FORMAT'); $settings['release_format'] = $valkey->Get('Q_RELEASE_FORMAT');
$settings['subject'] = $redis->Get('Q_SUBJ'); $settings['subject'] = $valkey->Get('Q_SUBJ');
$settings['sender'] = $redis->Get('Q_SENDER'); $settings['sender'] = $valkey->Get('Q_SENDER');
$settings['bcc'] = $redis->Get('Q_BCC'); $settings['bcc'] = $valkey->Get('Q_BCC');
$settings['redirect'] = $redis->Get('Q_REDIRECT'); $settings['redirect'] = $valkey->Get('Q_REDIRECT');
$settings['html_tmpl'] = htmlspecialchars($redis->Get('Q_HTML')); $settings['html_tmpl'] = htmlspecialchars($valkey->Get('Q_HTML'));
if (empty($settings['html_tmpl'])) { if (empty($settings['html_tmpl'])) {
$settings['html_tmpl'] = htmlspecialchars(file_get_contents("/tpls/quarantine.tpl")); $settings['html_tmpl'] = htmlspecialchars(file_get_contents("/tpls/quarantine.tpl"));
} }
@@ -796,7 +796,7 @@ function quarantine($_action, $_data = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -833,7 +833,7 @@ function quarantine($_action, $_data = null) {
))); )));
return false; return false;
} }
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash'); $stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash');
$stmt->execute(array(':hash' => $hash)); $stmt->execute(array(':hash' => $hash));
return $stmt->fetch(PDO::FETCH_ASSOC); return $stmt->fetch(PDO::FETCH_ASSOC);
break; break;

View File

@@ -1,6 +1,6 @@
<?php <?php
function quota_notification($_action, $_data = null) { function quota_notification($_action, $_data = null) {
global $redis; global $valkey;
$_data_log = $_data; $_data_log = $_data;
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -26,15 +26,15 @@ function quota_notification($_action, $_data = null) {
} }
$html = $_data['html_tmpl']; $html = $_data['html_tmpl'];
try { try {
$redis->Set('QW_SENDER', $sender); $valkey->Set('QW_SENDER', $sender);
$redis->Set('QW_SUBJ', $subject); $valkey->Set('QW_SUBJ', $subject);
$redis->Set('QW_HTML', $html); $valkey->Set('QW_HTML', $html);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -46,9 +46,9 @@ function quota_notification($_action, $_data = null) {
break; break;
case 'get': case 'get':
try { try {
$settings['subject'] = $redis->Get('QW_SUBJ'); $settings['subject'] = $valkey->Get('QW_SUBJ');
$settings['sender'] = $redis->Get('QW_SENDER'); $settings['sender'] = $valkey->Get('QW_SENDER');
$settings['html_tmpl'] = htmlspecialchars($redis->Get('QW_HTML')); $settings['html_tmpl'] = htmlspecialchars($valkey->Get('QW_HTML'));
if (empty($settings['html_tmpl'])) { if (empty($settings['html_tmpl'])) {
$settings['html_tmpl'] = htmlspecialchars(file_get_contents("/tpls/quota.tpl")); $settings['html_tmpl'] = htmlspecialchars(file_get_contents("/tpls/quota.tpl"));
} }
@@ -57,7 +57,7 @@ function quota_notification($_action, $_data = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -66,7 +66,7 @@ function quota_notification($_action, $_data = null) {
} }
} }
function quota_notification_bcc($_action, $_data = null) { function quota_notification_bcc($_action, $_data = null) {
global $redis; global $valkey;
$_data_log = $_data; $_data_log = $_data;
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") { if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -105,16 +105,16 @@ function quota_notification_bcc($_action, $_data = null) {
$bcc_rcpts = array_filter($bcc_rcpts); $bcc_rcpts = array_filter($bcc_rcpts);
if (empty($bcc_rcpts)) { if (empty($bcc_rcpts)) {
$active = 0; $active = 0;
} }
try { try {
$redis->hSet('QW_BCC', $domain, json_encode(array('bcc_rcpts' => $bcc_rcpts, 'active' => $active))); $valkey->hSet('QW_BCC', $domain, json_encode(array('bcc_rcpts' => $bcc_rcpts, 'active' => $active)));
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -135,13 +135,13 @@ function quota_notification_bcc($_action, $_data = null) {
return false; return false;
} }
try { try {
return json_decode($redis->hGet('QW_BCC', $domain), true); return json_decode($valkey->hGet('QW_BCC', $domain), true);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }

View File

@@ -1,6 +1,6 @@
<?php <?php
function ratelimit($_action, $_scope, $_data = null, $_extra = null) { function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
global $redis; global $valkey;
$_data_log = $_data; $_data_log = $_data;
switch ($_action) { switch ($_action) {
case 'edit': case 'edit':
@@ -42,26 +42,26 @@ function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
} }
if (empty($rl_value)) { if (empty($rl_value)) {
try { try {
$redis->hDel('RL_VALUE', $object); $valkey->hDel('RL_VALUE', $object);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log), 'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
} }
else { else {
try { try {
$redis->hSet('RL_VALUE', $object, $rl_value . ' / 1' . $rl_frame); $valkey->hSet('RL_VALUE', $object, $rl_value . ' / 1' . $rl_frame);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log), 'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -103,26 +103,26 @@ function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
} }
if (empty($rl_value)) { if (empty($rl_value)) {
try { try {
$redis->hDel('RL_VALUE', $object); $valkey->hDel('RL_VALUE', $object);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log), 'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
} }
else { else {
try { try {
$redis->hSet('RL_VALUE', $object, $rl_value . ' / 1' . $rl_frame); $valkey->hSet('RL_VALUE', $object, $rl_value . ' / 1' . $rl_frame);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log), 'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
continue; continue;
} }
@@ -143,7 +143,7 @@ function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
return false; return false;
} }
try { try {
if ($rl_value = $redis->hGet('RL_VALUE', $_data)) { if ($rl_value = $valkey->hGet('RL_VALUE', $_data)) {
$rl = explode(' / 1', $rl_value); $rl = explode(' / 1', $rl_value);
$data['value'] = $rl[0]; $data['value'] = $rl[0];
$data['frame'] = $rl[1]; $data['frame'] = $rl[1];
@@ -157,7 +157,7 @@ function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log), 'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -169,7 +169,7 @@ function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
return false; return false;
} }
try { try {
if ($rl_value = $redis->hGet('RL_VALUE', $_data)) { if ($rl_value = $valkey->hGet('RL_VALUE', $_data)) {
$rl = explode(' / 1', $rl_value); $rl = explode(' / 1', $rl_value);
$data['value'] = $rl[0]; $data['value'] = $rl[0];
$data['frame'] = $rl[1]; $data['frame'] = $rl[1];
@@ -183,7 +183,7 @@ function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log), 'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }
@@ -202,16 +202,16 @@ function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
return false; return false;
} }
try { try {
$data_rllog = $redis->lRange('RL_LOG', 0, -1); $data_rllog = $valkey->lRange('RL_LOG', 0, -1);
if ($data_rllog) { if ($data_rllog) {
foreach ($data_rllog as $json_line) { foreach ($data_rllog as $json_line) {
if (preg_match('/' . $data['hash'] . '/i', $json_line)) { if (preg_match('/' . $data['hash'] . '/i', $json_line)) {
$redis->lRem('RL_LOG', $json_line, 0); $valkey->lRem('RL_LOG', $json_line, 0);
} }
} }
} }
if ($redis->type($data['hash']) == Redis::REDIS_HASH) { if ($valkey->type($data['hash']) == Redis::REDIS_HASH) {
$redis->delete($data['hash']); $valkey->delete($data['hash']);
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log), 'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
@@ -232,7 +232,7 @@ function ratelimit($_action, $_scope, $_data = null, $_extra = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_scope, $_data_log), 'log' => array(__FUNCTION__, $_action, $_scope, $_data_log),
'msg' => array('redis_error', $e) 'msg' => array('valkey_error', $e)
); );
return false; return false;
} }

View File

@@ -62,7 +62,11 @@ if ($app_links_processed){
} }
} }
// Workaround to get text with <br> straight to twig.
// Using "nl2br" doesn't work with Twig as it would escape everything by default.
if (isset($UI_TEXTS["ui_footer"])) {
$UI_TEXTS["ui_footer"] = nl2br($UI_TEXTS["ui_footer"]);
}
$globalVariables = [ $globalVariables = [
'mailcow_hostname' => getenv('MAILCOW_HOSTNAME'), 'mailcow_hostname' => getenv('MAILCOW_HOSTNAME'),

View File

@@ -4,7 +4,7 @@ function init_db_schema()
try { try {
global $pdo; global $pdo;
$db_version = "27012025_1555"; $db_version = "07102025_1015";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -184,6 +184,7 @@ function init_db_schema()
"private_comment" => "TEXT", "private_comment" => "TEXT",
"public_comment" => "TEXT", "public_comment" => "TEXT",
"sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'", "sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'",
"internal" => "TINYINT(1) NOT NULL DEFAULT '0'",
"active" => "TINYINT(1) NOT NULL DEFAULT '1'" "active" => "TINYINT(1) NOT NULL DEFAULT '1'"
), ),
"keys" => array( "keys" => array(
@@ -345,10 +346,14 @@ function init_db_schema()
"notified" => "TINYINT(1) NOT NULL DEFAULT '0'", "notified" => "TINYINT(1) NOT NULL DEFAULT '0'",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
"user" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'", "user" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'",
"qhash" => "VARCHAR(64)",
), ),
"keys" => array( "keys" => array(
"primary" => array( "primary" => array(
"" => array("id") "" => array("id")
),
"key" => array(
"qhash" => array("qhash")
) )
), ),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
@@ -471,6 +476,23 @@ function init_db_schema()
), ),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
), ),
"mta_sts" => array(
"cols" => array(
"id" => "BIGINT NOT NULL",
"domain" => "VARCHAR(255) NOT NULL",
"version" => "VARCHAR(255) NOT NULL",
"mode" => "VARCHAR(255) NOT NULL",
"mx" => "VARCHAR(255) NOT NULL",
"max_age" => "VARCHAR(255) NOT NULL",
"active" => "TINYINT(1) NOT NULL DEFAULT '1'"
),
"keys" => array(
"primary" => array(
"" => array("domain")
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"user_acl" => array( "user_acl" => array(
"cols" => array( "cols" => array(
"username" => "VARCHAR(255) NOT NULL", "username" => "VARCHAR(255) NOT NULL",
@@ -1315,6 +1337,14 @@ function init_db_schema()
$pdo->query($create); $pdo->query($create);
} }
// Clear old app_passwd log entries
$pdo->exec("DELETE FROM logs
WHERE role != 'unauthenticated'
AND JSON_EXTRACT(`call`, '$[0]') = 'app_passwd'
AND JSON_EXTRACT(`call`, '$[1]') = 'edit'
AND (JSON_CONTAINS_PATH(`call`, 'one', '$[2].password')
OR JSON_CONTAINS_PATH(`call`, 'one', '$[2].password2'));");
// Mitigate imapsync argument injection issue // Mitigate imapsync argument injection issue
$pdo->query("UPDATE `imapsync` SET `custom_params` = '' $pdo->query("UPDATE `imapsync` SET `custom_params` = ''
WHERE `custom_params` LIKE '%pipemess%' WHERE `custom_params` LIKE '%pipemess%'
@@ -1482,6 +1512,10 @@ function init_db_schema()
'msg' => 'db_init_complete' 'msg' => 'db_init_complete'
); );
} }
// fill quarantine.qhash
$pdo->query("UPDATE `quarantine` SET `qhash` = SHA2(CONCAT(`id`, `qid`), 256) WHERE ISNULL(`qhash`)");
} catch (PDOException $e) { } catch (PDOException $e) {
if (php_sapi_name() == "cli") { if (php_sapi_name() == "cli") {
echo "DB initialization failed: " . print_r($e, true) . PHP_EOL; echo "DB initialization failed: " . print_r($e, true) . PHP_EOL;

View File

@@ -57,22 +57,22 @@ $WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $server_name, $form
// only include root ca's when needed // only include root ca's when needed
if (getenv('WEBAUTHN_ONLY_TRUSTED_VENDORS') == 'y') $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates'); if (getenv('WEBAUTHN_ONLY_TRUSTED_VENDORS') == 'y') $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates');
// Redis // Valkey
$redis = new Redis(); $valkey = new Redis();
try { try {
if (!empty(getenv('REDIS_SLAVEOF_IP'))) { if (!empty(getenv('VALKEY_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT')); $valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT'));
} }
else { else {
$redis->connect('redis-mailcow', 6379); $valkey->connect('valkey-mailcow', 6379);
} }
$redis->auth(getenv("REDISPASS")); $valkey->auth(getenv("VALKEYPASS"));
} }
catch (Exception $e) { catch (Exception $e) {
// Stop when redis is not available // Stop when valkey is not available
http_response_code(500); http_response_code(500);
?> ?>
<center style='font-family:sans-serif;'>Connection to Redis failed.<br /><br />The following error was reported:<br/><?=$e->getMessage();?></center> <center style='font-family:sans-serif;'>Connection to Valkey failed.<br /><br />The following error was reported:<br/><?=$e->getMessage();?></center>
<?php <?php
exit; exit;
} }

View File

@@ -1,7 +1,9 @@
<?php <?php
// Start session // Start session
if (session_status() !== PHP_SESSION_ACTIVE) { if (session_status() !== PHP_SESSION_ACTIVE) {
session_name($SESSION_NAME);
ini_set("session.cookie_httponly", 1); ini_set("session.cookie_httponly", 1);
ini_set("session.cookie_samesite", $SESSION_SAMESITE_POLICY);
ini_set('session.gc_maxlifetime', $SESSION_LIFETIME); ini_set('session.gc_maxlifetime', $SESSION_LIFETIME);
} }
@@ -67,7 +69,7 @@ if (!empty($_SERVER['HTTP_X_API_KEY'])) {
} }
} }
else { else {
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); $valkey->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
http_response_code(401); http_response_code(401);
echo json_encode(array( echo json_encode(array(
@@ -79,7 +81,7 @@ if (!empty($_SERVER['HTTP_X_API_KEY'])) {
} }
} }
else { else {
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); $valkey->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
http_response_code(401); http_response_code(401);
echo json_encode(array( echo json_encode(array(

View File

@@ -80,7 +80,7 @@ if (isset($_POST["verify_tfa_login"])) {
intval($user_details['attributes']['force_pw_update']) != 1 && intval($user_details['attributes']['force_pw_update']) != 1 &&
getenv('SKIP_SOGO') != "y" && getenv('SKIP_SOGO') != "y" &&
!$is_dual) { !$is_dual) {
header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}"); header("Location: /SOGo/so/");
die(); die();
} else { } else {
header("Location: /user"); header("Location: /user");
@@ -146,7 +146,7 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
intval($user_details['attributes']['force_pw_update']) != 1 && intval($user_details['attributes']['force_pw_update']) != 1 &&
getenv('SKIP_SOGO') != "y" && getenv('SKIP_SOGO') != "y" &&
!$is_dual) { !$is_dual) {
header("Location: /SOGo/so/{$login_user}"); header("Location: /SOGo/so/");
die(); die();
} else { } else {
header("Location: /user"); header("Location: /user");

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