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

Compare commits

..

196 Commits

Author SHA1 Message Date
FreddleSpl0it b84ba8ded1 Merge pull request #7101 from mailcow/staging
Update 2026-03
2026-03-10 08:08:29 +01:00
FreddleSpl0it 8f883f3d37 Rename dns-101 to dns-01 2026-03-09 15:27:33 +01:00
FreddleSpl0it 709117fe19 prevent false positives in option detection in new_options.sh 2026-03-09 15:19:46 +01:00
FreddleSpl0it 82ea418423 Extend --dev flag to skip _modules update 2026-03-09 15:13:36 +01:00
FreddleSpl0it fd24163c6e prevent false positives in option detection in new_options.sh 2026-03-09 15:13:13 +01:00
FreddleSpl0it 3caef45a12 Merge pull request #7100 from mailcow/feat/rspamd-3.14.3
[Rspamd] Update to 3.14.3-1
2026-03-09 08:15:10 +01:00
FreddleSpl0it 8760e7e5db [Rspamd] Update to 3.14.3-1 2026-03-09 07:59:34 +01:00
FreddleSpl0it e848226062 [SOGo] Update paths from /usr/lib/ to /usr/local/lib/ 2026-03-06 16:26:13 +01:00
FreddleSpl0it efaeb77e13 Merge pull request #7093 from mailcow/fix/7054
[SOGo][Web] use incremental updates for mailbox/alias/resource sync in sogo_static_view
2026-03-06 09:23:00 +01:00
FreddleSpl0it 569b4cf985 Merge pull request #7098 from mailcow/feat/sogo-5.12.5
[SOGo] Update to 5.12.5
2026-03-06 09:21:09 +01:00
FreddleSpl0it 6c857243ec [SOGo] Update to 5.12.5 2026-03-06 09:18:10 +01:00
renovate[bot] 905e93627c Update docker/build-push-action action to v7 (#7097) 2026-03-06 07:02:04 +01:00
renovate[bot] 598ea21827 chore(deps): update docker/setup-buildx-action action to v4 (#7096)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-05 11:03:21 +01:00
milkmaker 6012cf4486 [Web] Updated lang.nl-nl.json (#7095)
Co-authored-by: Tom18314 <tomstokmans5@gmail.com>
2026-03-04 23:03:02 +01:00
FreddleSpl0it fe71f84c82 [SOGo] Fix custom UI patches after moving to source-compiled SOGo 2026-03-04 14:55:52 +01:00
FreddleSpl0it 763ecbc93e Update image tags 2026-03-04 13:38:16 +01:00
FreddleSpl0it 97312c1a9d Update alpine images to 3.23 2026-03-04 13:35:11 +01:00
renovate[bot] baf0e7e658 chore(deps): update docker/login-action action to v4 (#7094) 2026-03-04 12:40:35 +01:00
renovate[bot] a4ca5f9363 Update docker/setup-qemu-action action to v4 (#7092) 2026-03-04 12:40:02 +01:00
FreddleSpl0it dcbea71e67 [ACME] auto-create DNS-01 config template on startup 2026-03-04 11:42:40 +01:00
FreddleSpl0it 524f19ff77 Update Docker Images 2026-03-04 11:36:09 +01:00
FreddleSpl0it 4e33c7143f [SOGo][Web] use incremental updates for mailbox/alias/resource sync in sogo_static_view 2026-03-04 11:16:48 +01:00
milkmaker 4c184b25f4 Translations update from Weblate (#7091)
* [Web] Language file updated by 'Cleanup translation files' addon

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: Matjaž Tekavec <matjaz@moj-svet.si>
2026-03-03 20:25:24 +01:00
FreddleSpl0it ddc2309f1e Merge pull request #7089 from mailcow/fix/7039
[Web] switch from GET to POST for datatable requests
2026-03-03 15:07:17 +01:00
FreddleSpl0it bccfcefd17 Merge pull request #7088 from mailcow/fix/7043
[Web] Add missing EAS and DAV protocol options to mailbox bulk actions
2026-03-03 14:42:47 +01:00
FreddleSpl0it d40252ccce [Web] Add missing EAS and DAV protocol options to mailbox bulk actions 2026-03-03 14:39:41 +01:00
FreddleSpl0it 4abb5cbfab Merge pull request #7086 from mailcow/feat/sogo-build
[SOGo] Build SOGo from source with security patches
2026-03-03 14:24:29 +01:00
FreddleSpl0it b695936273 Merge pull request #6912 from cjlapao/feat-acme-dns
acme: add DNS challenges
2026-03-03 14:23:28 +01:00
DerLinkman d9463c7950 sogo: shrinked image file by ~50% 2026-03-03 14:14:32 +01:00
FreddleSpl0it 579542381a Merge pull request #7060 from jovobe/staging
Bump alpine version of netfilter
2026-03-03 14:06:01 +01:00
FreddleSpl0it ce5659f300 Merge pull request #7082 from JeremieCrinon/fix/show-stopped-containers-api-and-dashboard
fix: show stopped and failed containers in dashboard and API
2026-03-03 14:01:03 +01:00
FreddleSpl0it 1967cb642f Merge pull request #6457 from mailcow/renovate/composer-composer-2.x
chore(deps): update dependency composer/composer to v2.9.5
2026-03-03 13:38:32 +01:00
FreddleSpl0it ba0eb04ebe Merge pull request #6695 from maxi322/feature/check_dns_improvement
check_dns: better time measurement
2026-03-03 13:31:23 +01:00
FreddleSpl0it 609ce6b0d6 Merge pull request #6976 from mailcow/fix/autodiscover-passwordless
feat: Implement passwordless autodiscover endpoint
2026-03-03 13:25:16 +01:00
FreddleSpl0it af61e2d303 [Web] Add fail2ban logging to passwordless autodiscover endpoint 2026-03-03 13:24:44 +01:00
FreddleSpl0it c6e3f517e1 Merge pull request #7047 from jonprocter/staging
Document qitem endpoint in openapi.yaml for editing quarantine mails
2026-03-03 13:11:43 +01:00
FreddleSpl0it 6eaa8d43e9 Merge pull request #7078 from HichemAK/patch-2
Add skip feature to mailcow admin password reset script
2026-03-03 13:09:20 +01:00
FreddleSpl0it c033cd6254 Merge pull request #7077 from mailcow/feat/force-tfa
[Web] Add forced 2FA setup and password update enforcement
2026-03-03 13:01:16 +01:00
FreddleSpl0it 7562578b74 [SOGo] Build SOGo from source with security patches 2026-03-03 11:24:42 +01:00
FreddleSpl0it 43f570e761 [Web] switch from GET to POST for datatable requests 2026-03-03 08:10:16 +01:00
DocFraggle 5e478a32df Fix lua script sub-addressing (#7037)
* Fix lua script sub-addressing

* Fix subject tagging in rspamd.local.lua as well

---------

Co-authored-by: Hailer, Christian <christian.hailer@interhyp.de>
Co-authored-by: DocFraggle <drfraggle@drfraggleshome.de>
2026-03-02 15:49:11 +01:00
milkmaker 22c94a4d2f update postscreen_access.cidr (#7084) 2026-03-01 13:30:57 +01:00
Jérémie Crinon 99dc0f6616 fix: show stopped and failed containers in dashboard and API 2026-02-27 13:41:36 +01:00
Hichem Ammar Khodja 4732f568fa Add skip feature to mailcow admin password reset script
Added option to skip confirmation for resetting the mailcow admin account.
2026-02-24 15:26:56 +01:00
FreddleSpl0it 96d4802cb2 [Web] Switch QR code generation from external API to local library and fix composer.json/lock mismatch 2026-02-24 11:02:47 +01:00
FreddleSpl0it ad5b94af5e [Web] Add forced 2FA setup and password update enforcement 2026-02-24 10:44:33 +01:00
milkmaker 7fce984cac [Web] Updated lang.lv-lv.json (#7069)
Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
2026-02-19 22:15:32 +01:00
renovate[bot] 404e2f0190 Update actions/stale action to v10.2.0 (#7062) 2026-02-18 08:10:48 +01:00
maxi322 1c52eaa3a4 check_dns: better time measurement
alpine does not output ms using date, therefore we use perl to get a more
accurate measurement of the dns response time.
The script output is now even more similar to nagios check_dns.
2026-02-14 18:07:39 +01:00
Johan M. von Behren c7e04b4146 Bump alpine version of netfilter
Bump alpine from 3.21 to 3.23. Closes #7059
2026-02-13 14:26:27 +01:00
renovate[bot] 075959aea9 Update dependency composer/composer to v2.9.5
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2026-02-12 11:09:08 +00:00
milkmaker 885ba2510e Translations update from Weblate (#7055)
* [Web] Updated lang.cs-cz.json

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

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

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

---------

Co-authored-by: Filip Hajny <filip@hajny.net>
Co-authored-by: Habetdin <15926758+Habetdin@users.noreply.github.com>
2026-02-09 01:52:35 +01:00
Carlos Lapao 87563249f1 Add DNS-01 challenge variables to update migration for existing instances
Existing mailcow installations were Not receiving the ACME_DNS_CHALLENGE,
ACME_DNS_PROVIDER, and ACME_ACCOUNT_EMAIL options when running update.sh,  as they were only defined in generate_config.sh for new installations.
2026-02-07 13:02:55 +00:00
Jonathan Procter fb1686065d Add endpoint for editing quarantine mails
Added endpoint to edit quarantine items with actions to release or learn as ham. Functionality already existed but was undocumented.
2026-02-03 21:31:32 +00:00
milkmaker 0428f5c9bd update postscreen_access.cidr (#7042) 2026-02-01 22:45:03 +01:00
milkmaker a70c23065c Translations update from Weblate (#7040)
* [Web] Updated lang.vi-vn.json

Co-authored-by: Phu D. Nguyen <sillycat@duck.com>

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

Co-authored-by: Proton Chang <protonmo@gmail.com>

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

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

---------

Co-authored-by: Phu D. Nguyen <sillycat@duck.com>
Co-authored-by: Proton Chang <protonmo@gmail.com>
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
2026-02-01 22:44:52 +01:00
FreddleSpl0it 4845928e7a Merge pull request #7027 from mailcow/staging
[Hotfix] Update 2026-01
2026-01-29 10:31:23 +01:00
FreddleSpl0it caaa4a414d [Web] Fix datatables search after PR #7022 2026-01-29 10:26:44 +01:00
FreddleSpl0it 4ccfedd6b3 Merge pull request #7024 from mailcow/staging
🐄🛡️ January 2026 Update | Limited EAS/DAV Access and Restricted Alias Sending
2026-01-29 07:25:09 +01:00
FreddleSpl0it c3d841340c [Dovecot][PHP][SOGo] Update Images 2026-01-28 11:28:36 +01:00
FreddleSpl0it 2e8897c2cf Merge branch 'staging' into fix/autodiscover-passwordless 2026-01-28 11:10:28 +01:00
FreddleSpl0it b8cd00111f Merge pull request #7007 from moregeek/feat/allow_preset_passwords
feat: allow preset of passwords via environment vars
2026-01-28 10:18:56 +01:00
FreddleSpl0it 81cda80651 Merge pull request #7021 from mailcow/feat/restrict-alias-sending
[Postfix] Configurable send permissions for alias addresses
2026-01-28 10:03:02 +01:00
FreddleSpl0it c1d4f04c22 Merge branch 'staging' into feat/restrict-alias-sending 2026-01-28 10:02:03 +01:00
FreddleSpl0it 82276cd1ca Merge pull request #7022 from mailcow/feat/eas-dav-access
[Web] Allow admins to limit EAS and DAV access for mailbox users
2026-01-28 09:54:47 +01:00
FreddleSpl0it 56ea4302ed [Web] Allow admins to limit EAS and DAV access for mailbox users 2026-01-28 09:49:33 +01:00
FreddleSpl0it c06112b26e [Postfix] Configurable send permissions for alias addresses 2026-01-27 09:05:51 +01:00
FreddleSpl0it aa5a4f0998 Merge pull request #6710 from mailcow/renovate/tianon-gosu-1.x
chore(deps): update dependency tianon/gosu to v1.19
2026-01-27 08:09:31 +01:00
FreddleSpl0it bf4f471cfd Merge pull request #6837 from mailcow/renovate/php-memcached-dev-php-memcached-3.x
chore(deps): update dependency php-memcached-dev/php-memcached to v3.4.0
2026-01-27 08:08:50 +01:00
FreddleSpl0it 978bff9dbc Merge pull request #6867 from DiscoNova/feat/possible-to-disable-logins-from-autoprotocol-domains
[Web] Disable login UI on autoprotocol domains
2026-01-27 08:08:12 +01:00
FreddleSpl0it 869d9af7dd Merge pull request #6901 from mailcow/renovate/phpredis-phpredis-6.x
chore(deps): update dependency phpredis/phpredis to v6.3.0
2026-01-27 08:05:58 +01:00
FreddleSpl0it af10499ecb Merge pull request #6927 from mailcow/renovate/imagick-imagick-3.x
chore(deps): update dependency imagick/imagick to v3.8.1
2026-01-27 08:04:51 +01:00
FreddleSpl0it a1a4d8ff98 Merge pull request #6947 from mailcow/renovate/krakjoe-apcu-5.x
chore(deps): update dependency krakjoe/apcu to v5.1.28
2026-01-27 08:04:24 +01:00
FreddleSpl0it 95d61e8aa2 Merge pull request #6980 from bluewalk/feat/issue-6489
Configurable displayName(s) - Fixes issue #6489
2026-01-27 08:02:20 +01:00
FreddleSpl0it ec8dd1a54f Merge pull request #6990 from psuet/mobileconfig-with-password-complexity
fix: Password for mobileconfig that conforms to password-complexity policy
2026-01-27 07:56:35 +01:00
milkmaker 382ee34d0e [Web] Updated lang.hu-hu.json (#7020)
Co-authored-by: Sándor <me-github@sandros.hu>
2026-01-26 20:15:47 +01:00
milkmaker 0999c9e9ab Translations update from Weblate (#7014)
* [Web] Updated lang.zh-cn.json

Co-authored-by: 雨 <luotianyi@luotianyi.me>

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

Co-authored-by: Monika Bark <rychert.monika@wp.pl>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

---------

Co-authored-by: 雨 <luotianyi@luotianyi.me>
Co-authored-by: Monika Bark <rychert.monika@wp.pl>
2026-01-23 22:02:55 +01:00
Stefan Morgenthaler c485968e7f feat: allow preset of passwords via environment vars
Signed-off-by: Stefan Morgenthaler <dev@morgenthaler.at>
2026-01-14 11:42:15 +01:00
milkmaker e727620bd3 Translations update from Weblate (#7002)
* [Web] Updated lang.zh-cn.json

Co-authored-by: ガラスのような夢 <i@msdnicrosoft.work>

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

Co-authored-by: Monika Bark <rychert.monika@wp.pl>

---------

Co-authored-by: ガラスのような夢 <i@msdnicrosoft.work>
Co-authored-by: Monika Bark <rychert.monika@wp.pl>
2026-01-07 17:23:31 +01:00
milkmaker 71fa3ecebc update postscreen_access.cidr (#6987) 2026-01-07 17:22:01 +01:00
Paul Sütterlin 70101d1187 fix: Password for mobileconfig that conforms to password-complexity policy 2026-01-01 16:57:21 +01:00
bluewalk c060c205d3 Fixes issue #6489 2025-12-21 16:56:16 +01:00
DerLinkman 5ca900749c autodiscover: use generalized error logging instead of specific to prevent user enumeration 2025-12-18 16:54:45 +01:00
DerLinkman b005803fe0 Add autodiscover debug script with domain override support
- Add view_autodiscover.sh helper script for testing autodiscover responses
- Support -h/--help flag for usage information
- Support -d/--domain flag to override autodiscover target (useful for testing)
- Auto-detect xmllint availability for formatted output
- Email validation with regex
- Interactive mode if no email provided
- Display response length for debugging
2025-12-17 14:27:53 +01:00
DerLinkman ec77406dba Fix autodiscover.php: Use random error IDs and fix SQL type casting
- Replace hardcoded error IDs with random values (1-10 billion range) for better debugging
- Cast SimpleXMLElement email to string before SQL query to prevent type errors
- Qualify ambiguous 'active' column with table names in JOIN query
- Add proper error XML response for database errors instead of die()
- Ensure all error paths return complete XML documents
2025-12-17 14:27:38 +01:00
DerLinkman ee15721550 feat: implement passwordless autodiscover endpoint
- Remove HTTP Basic Authentication requirement from autodiscover.php
- Extract email address from XML request body instead of AUTH headers
- Validate mailbox existence and active status before returning config
- Improve security by eliminating password transmission
- Add comprehensive error handling for invalid/inactive mailboxes
- Follow industry standards (Microsoft, Google, Apple)
- Maintain backward compatibility with existing email clients
- Keep full logging functionality in Redis AUTODISCOVER_LOG

This change enhances security while improving user experience and
follows modern email client configuration best practices.
2025-12-17 13:39:05 +01:00
Copilot 038b2efb75 Add MTA-STS support for alias domains (#6972)
* Initial plan

* Add MTA-STS support for alias domains

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

* Improve domain normalization and code style in mta-sts.php

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

* Add error handling for idn_to_ascii in mta-sts.php

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

* Add database error handling for alias domain query

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

* Add ACME certificate support for MTA-STS on alias domains

Query alias_domain table to find aliases with MTA-STS enabled target domains and request certificates for mta-sts.<alias-domain> subdomains.

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

* compose: bump image tag to 1.95

* Add MTA-STS DNS records display for alias domains in UI

When viewing an alias domain's DNS diagnostics, check if the target domain has MTA-STS enabled and display the required DNS records for the alias domain.

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>
Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
2025-12-15 16:29:21 +01:00
DerLinkman 1fe4cd03e9 ui: fix global filters ui tickbox reappearing (#6966) 2025-12-12 16:01:18 +01:00
milkmaker 12e02e67ff Translations update from Weblate (#6965)
* [Web] Updated lang.fr-fr.json

Co-authored-by: Keo <contact@kbl.netlib.re>

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

Co-authored-by: Germano Pires Ferreira <germanopires@gmail.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

Co-authored-by: Monika Bark <rychert.monika@wp.pl>

---------

Co-authored-by: Keo <contact@kbl.netlib.re>
Co-authored-by: Germano Pires Ferreira <germanopires@gmail.com>
Co-authored-by: Monika Bark <rychert.monika@wp.pl>
2025-12-12 15:21:04 +01:00
Ashitaka e8d9315d4a Merge pull request #6905 from Ashitaka57/6646-pbkdf2-sha512-verify-hash
Support for PBKDF2-SHA512 hash algorithm in verify_hash() (FreeIPA compatibility) (issue 6646)
2025-12-12 14:08:21 +01:00
DerLinkman d977ddb501 backup: add image prefetch function to verify latest image is used 2025-12-12 14:07:57 +01:00
DerLinkman e76f5237ed ofelia: revert fixed cron syntax for sa-rules download 2025-12-12 14:07:47 +01:00
Copilot c11ed5dd1e Prevent duplicate/plaintext login announcement rendering (#6963)
* Initial plan

* Fix duplicate login announcement display

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>
2025-12-12 14:07:36 +01:00
DerLinkman b6f57dfb78 rspamd: update to 3.14.2 2025-12-12 14:06:49 +01:00
Copilot 3ebf2c2d2d Prevent duplicate/plaintext login announcement rendering (#6963)
* Initial plan

* Fix duplicate login announcement display

Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DerLinkman <62480600+DerLinkman@users.noreply.github.com>
2025-12-12 12:34:20 +01:00
DerLinkman 1bac6f1ee7 ofelia: revert fixed cron syntax for sa-rules download 2025-12-11 13:29:11 +01:00
DerLinkman 67e7acd6bd rspamd: upgrade to 3.14.1, trixie rebuild + bcc forwarded hosts fix (#6958)
* rspamd: fix bcc + subadress handling when using forward hosts

* rspamd: build against trixie + use version 3.14.1
2025-12-11 09:45:56 +01:00
renovate[bot] 910ce573d6 chore(deps): update peter-evans/create-pull-request action to v8 (#6953) 2025-12-10 19:48:02 +01:00
renovate[bot] 689336b3e1 chore(deps): update dependency tianon/gosu to v1.19
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2025-12-10 10:41:59 +00:00
renovate[bot] 01cf72cdef chore(deps): update dependency phpredis/phpredis to v6.3.0
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2025-12-10 10:41:54 +00:00
renovate[bot] 4cdb97c699 chore(deps): update dependency php-memcached-dev/php-memcached to v3.4.0
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2025-12-10 10:41:50 +00:00
renovate[bot] 1bd795a9c6 chore(deps): update dependency krakjoe/apcu to v5.1.28
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2025-12-10 10:41:42 +00:00
renovate[bot] 39f29e6c30 chore(deps): update dependency imagick/imagick to v3.8.1
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2025-12-10 10:41:38 +00:00
Ashitaka 1ab6af21e3 Merge pull request #6905 from Ashitaka57/6646-pbkdf2-sha512-verify-hash
Support for PBKDF2-SHA512 hash algorithm in verify_hash() (FreeIPA compatibility) (issue 6646)
2025-12-10 11:41:06 +01:00
DerLinkman 5d95c48e0d backup: add image prefetch function to verify latest image is used 2025-12-10 08:43:04 +01:00
DerLinkman 4ef65fc382 Merge pull request #6948 from mailcow/staging
2025-12
2025-12-09 13:29:15 +01:00
DerLinkman dbb9e474b0 pf-tlspol: upgrade to 1.8.22 (#6951)
* postfix-tlspol: upgrade to 1.8.20

* pf-tlspol: update to 1.8.22
2025-12-09 13:25:50 +01:00
Khurram Malik f8eed8c786 fix(api): add missing break in CORS switch block causing save to hang (#6926) 2025-12-09 11:54:20 +01:00
DerLinkman ef010aa39c Update CONTRIBUTING.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 15:08:25 +01:00
milkmaker 79171ea6f5 [Web] Updated lang.fr-fr.json (#6943)
Co-authored-by: Neuronnexion <support@nnx.com>
2025-12-05 14:40:45 +01:00
milkmaker 4e3294b273 [Web] Updated lang.fr-fr.json (#6941)
[Web] Updated lang.fr-fr.json

[Web] Updated lang.fr-fr.json

Co-authored-by: Neuronnexion <support@nnx.com>
2025-12-03 23:31:37 +01:00
renovate[bot] 32a6ecddb6 chore(deps): update alpine docker tag to v3.23 (#6940)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 23:30:46 +01:00
DerLinkman f3d9833ecf Merge branch 'master' into staging 2025-12-03 16:55:12 +01:00
DerLinkman 930ca76ea7 update: moved _modules initialization and update at the beginning of update script 2025-12-03 16:54:26 +01:00
DerLinkman 9a2887cf46 core: improved docker compose version check 2025-12-03 16:27:04 +01:00
DerLinkman 9950914086 core: improved docker compose version check 2025-12-03 16:26:22 +01:00
renovate[bot] 470cfb0026 chore(deps): update actions/stale action to v10.1.1 (#6937)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-03 13:53:05 +01:00
milkmaker 6c106b4e4d [Web] Updated lang.fr-fr.json (#6936)
Co-authored-by: Neuronnexion <support@nnx.com>
2025-12-02 16:40:36 +01:00
milkmaker 3d6253a2b2 update postscreen_access.cidr (#6933) 2025-12-01 08:48:22 +01:00
milkmaker b873812588 [Web] Updated lang.gr-gr.json (#6930)
Co-authored-by: ChD Computers <chdcomputers@gmail.com>
2025-11-29 13:20:02 +01:00
milkmaker 514fefd2ed Translations update from Weblate (#6924)
* [Web] Updated lang.ca-es.json

Co-authored-by: Pere Montpeó <peremontpeo@gmail.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

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

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

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

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

[Web] Updated lang.pl-pl.json

Co-authored-by: Monika Bark <rychert.monika@wp.pl>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

---------

Co-authored-by: Pere Montpeó <peremontpeo@gmail.com>
Co-authored-by: Chris <chrismfz@gmail.com>
Co-authored-by: Filip Hajny <filip@hajny.net>
Co-authored-by: Monika Bark <rychert.monika@wp.pl>
2025-11-24 16:50:03 +01:00
renovate[bot] 6f9ee2d151 chore(deps): update actions/checkout action to v6 (#6920)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-21 10:55:12 +01:00
milkmaker 9832006141 Translations update from Weblate (#6916)
* [Web] Updated lang.si-si.json

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

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

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

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

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

Co-authored-by: Monika Bark <rychert.monika@wp.pl>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

---------

Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
Co-authored-by: Habetdin <15926758+Habetdin@users.noreply.github.com>
Co-authored-by: Monika Bark <rychert.monika@wp.pl>
Co-authored-by: Peter <magic@kthx.at>
2025-11-17 23:23:07 +01:00
Carlos 890295bbfc Add DNS-01 challenge support with configuration files and scripts 2025-11-14 07:10:17 +00:00
Josh 0413d26855 Allow making spam aliases permanent (#6888)
* Allow making spam aliases permanent

* added german translation

* updated Spamalias Twig + Rename in Spam Alias

* compose: update image tags to align to vendor version

---------

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
2025-11-13 16:05:01 +01:00
Patrik Kernstock 7b29c1f304 Disable nginx server_tokens in http context (#6873) 2025-11-13 15:19:11 +01:00
Patrik Kernstock ae3ef391ee Remove deprecated 'X-XSS-Protection' header (#6871) 2025-11-13 15:16:44 +01:00
Peter 7313f996d3 Update to trixie (#6907) 2025-11-13 15:16:00 +01:00
DerLinkman 62d16c9e56 compose: changes cronjobs to regular cron syntax + fixed sogo creds for cronjobs (#6866)
* cron: restructure cron timer to time on second (instead of random)

* dovecot: fix clearance for cron.creds file
2025-11-13 14:59:49 +01:00
Carlos a52e977b89 Add DNS-01 challenge support for ACME certificates and related configurations 2025-11-13 07:19:38 +00:00
DerLinkman 674b41ce08 updated the Contributing Guidelines 2025-11-12 10:16:54 +01:00
Claas Flint 1b833be760 Replace pigz with zstd for backup compression (#6897)
* Replace pigz with zstd for backup compression

This change replaces pigz (parallel gzip) with zstd (Zstandard) as the
compression algorithm for mailcow backups while maintaining full backward
compatibility with existing .tar.gz backups.

Benefits:
- Better compression ratios (12-37% improvement in tests)
- Improved compression speed with modern algorithm
- Maintains rsyncable functionality for incremental backups
- Full backward compatibility for restoring old .tar.gz backups
- Wide industry adoption and active development

Changes:
- Backup compression: pigz --rsyncable -p → zstd --rsyncable -T
- Backup decompression: pigz -d -p → zstd -d -T
- File extensions: .tar.gz → .tar.zst
- Added get_archive_info() function for intelligent format detection
- Updated backup Dockerfile to install zstd alongside pigz
- Restore function now auto-detects and handles both formats
- Updated FILE_SELECTION regex to recognize both .tar.zst and .tar.gz
- Updated comments to reflect new file extension

Backward Compatibility:
- Restore automatically detects .tar.zst (preferred) or .tar.gz (legacy)
- Existing .tar.gz backups can still be restored without issues
- pigz remains installed in backup image for legacy support
- Graceful fallback if backup file format not found

Testing:
- Added comprehensive test suite (test_backup_and_restore.sh)
- 12 automated tests covering all scenarios:
  * Backup creation (both formats)
  * Restore (both formats)
  * Format detection and priority
  * Error handling (missing files, empty dirs)
  * Content integrity verification
  * Multi-threading configuration
  * Large file compression (8.59 MB realistic data)

Test Results:
✓ zstd compression working
✓ pigz compression working (legacy)
✓ zstd decompression working
✓ pigz decompression working (backward compatible)
✓ Archive detection working
✓ Content integrity verified
✓ Format priority correct (.tar.zst preferred)
✓ Error handling for missing files
✓ Error handling for empty directories
✓ Multi-threading configuration verified
✓ Large file compression: 37.05% improvement
✓ Small file compression: 12.18% improvement

* move testing script into development folder

---------

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
2025-11-12 10:06:36 +01:00
DerLinkman 88adb1adf5 remove dev docker volume from upstream 2025-11-12 09:54:35 +01:00
DerLinkman ec472f13cf sogo: removed URLDecrpytion by default, make it configurable in sogo.conf 2025-11-12 09:50:41 +01:00
milkmaker 2e1d98cc7c [Web] Updated lang.pl-pl.json (#6908)
[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

Co-authored-by: Monika Bark <rychert.monika@wp.pl>
2025-11-10 21:06:13 +01:00
milkmaker 07d7e3dc30 [Web] Updated lang.pl-pl.json (#6906)
[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

[Web] Updated lang.pl-pl.json

Co-authored-by: Monika Bark <rychert.monika@wp.pl>
2025-11-09 23:03:00 +01:00
milkmaker b0f5aee628 [Web] Updated lang.pl-pl.json (#6898)
Co-authored-by: Monika Bark <rychert.monika@wp.pl>
2025-11-05 17:37:26 +01:00
milkmaker d3065612fd update postscreen_access.cidr (#6886) 2025-11-03 21:07:40 +01:00
Josh 9912e41f78 [Web] Correct order of Dansk/Danish in UI (#6887) 2025-11-03 21:07:20 +01:00
milkmaker 04200c99a4 Translations update from Weblate (#6880)
* [Web] Updated lang.vi-vn.json

Co-authored-by: Nguyễn Thái Dũng <nguyenthaidung.work+mailcow.email@gmail.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

Co-authored-by: Runar Ingebrigtsen <runar@rin.no>

---------

Co-authored-by: Nguyễn Thái Dũng <nguyenthaidung.work+mailcow.email@gmail.com>
Co-authored-by: Runar Ingebrigtsen <runar@rin.no>
2025-10-27 20:00:29 +01:00
FreddleSpl0it 45666d2c4e Merge pull request #6874 from mailcow/staging
Update 2025-10a Hotfix
2025-10-24 08:27:23 +02:00
FreddleSpl0it 9a806e64ce [PHP] remove opcache.revalidate_freq 2025-10-24 08:18:49 +02:00
Markku Post 95e0608749 [Web] Disable login on autodiscover/autoconfig domains
Autodiscover and autoconfig domains (autodiscover.*, autoconfig.*) are intended solely for client autoconfiguration endpoints and should not display the mailcow login page. This change check the hostname and disables unauthenticated users from seeing the login page on those domains; HTTP 404 response is returned when necessary.
2025-10-24 06:03:40 +03:00
FreddleSpl0it 22a09b9795 [PHP] re-add opcache.revalidate_freq setting 2025-10-23 15:16:24 +02:00
FreddleSpl0it 04d5c43550 Merge pull request #6847 from patschi/disable-opcache-jit
Disable PHP opcache.jit
2025-10-23 09:32:02 +02:00
milkmaker fbcb8cbeb9 [Web] Updated lang.vi-vn.json (#6861)
Co-authored-by: Nguyễn Thái Dũng <nguyenthaidung.work+mailcow.email@gmail.com>
2025-10-21 18:03:22 +02:00
renovate[bot] 0338a36ecf chore(deps): update alpine docker tag to v3.22 (#6417)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 18:03:02 +02:00
milkmaker 23fb5e2fca Add Vietnamese language (#6854)
* [Web] Updated lang.vi-vn.json

[Web] Added lang.vi-vn.json

Co-authored-by: Nguyễn Thái Dũng <nguyenthaidung.work+mailcow.email@gmail.com>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* Add Vietnamese language

---------

Co-authored-by: Nguyễn Thái Dũng <nguyenthaidung.work+mailcow.email@gmail.com>
Co-authored-by: Peter <magic@kthx.at>
2025-10-20 18:35:00 +02:00
renovate[bot] 3507ff2773 chore(deps): update devops-infra/action-pull-request action to v1.0.2 (#6850) 2025-10-19 23:18:09 +02:00
Patrik Kernstock a4970397f1 Disable PHP opcache.jit 2025-10-17 13:17:56 +02:00
renovate[bot] 4132f6bd48 chore(deps): update devops-infra/action-pull-request action to v1 (#6840) 2025-10-16 07:28:57 +02:00
FreddleSpl0it 586b3a2ed1 Merge pull request #6838 from mailcow/staging
Update 2025-10
2025-10-15 08:11:08 +02:00
FreddleSpl0it 6af2addf3c [PHPFPM] Update Image to Version 1.94 2025-10-14 10:25:06 +02:00
FreddleSpl0it f6eed6c441 Merge pull request #6836 from mailcow/fix/6802
[Web] Add password verification when setting recovery email
2025-10-13 12:10:57 +02:00
FreddleSpl0it b85837c803 [Web] Add password verification when setting recovery email 2025-10-13 12:05:17 +02:00
FreddleSpl0it 653fc40d4c Merge pull request #6783 from patschi/phpfpm-moar-speeeed
Optimize phpfpm opcache: more aggressive caching, enable JIT
2025-10-13 11:43:07 +02:00
FreddleSpl0it c17d80a6fd Merge pull request #6821 from tjmills-dev/feat/show-app-passwd-logins
Show app passwords for successful logins on user page
2025-10-13 11:41:39 +02:00
FreddleSpl0it 980bfa3aa0 Merge pull request #6696 from mailcow/renovate/krakjoe-apcu-5.x
chore(deps): update dependency krakjoe/apcu to v5.1.27
2025-10-10 14:07:24 +02:00
FreddleSpl0it 664a954393 Merge pull request #6798 from mailcow/renovate/php-pecl-mail-mailparse-3.x
chore(deps): update dependency php/pecl-mail-mailparse to v3.1.9
2025-10-10 14:07:05 +02:00
FreddleSpl0it d5a27c4ccb Merge pull request #6830 from mailcow/feat/rspamd-3.13.2
[Rspamd] Update to 3.13.2
2025-10-10 13:10:54 +02:00
FreddleSpl0it 6a8a2e2136 Merge pull request #6829 from mailcow/feat/redis-7.4.6
[Redis] Update to Redis 7.4.6
2025-10-10 13:09:47 +02:00
FreddleSpl0it b859a52b8e Merge pull request #6828 from mailcow/fix/6818
[Web] Fix SOGo redirection after login
2025-10-10 13:08:22 +02:00
FreddleSpl0it 10e0c42eff Merge pull request #6797 from Hobby-Student/fix/autodiscover-with-ldap-attribute-mapping
fix autodiscover when using ldap with attribute mapping templates
2025-10-10 13:07:58 +02:00
FreddleSpl0it f47df263d7 [Rspamd] Update to 3.13.2 2025-10-10 13:04:01 +02:00
FreddleSpl0it 2642d9109e [Redis] Update to Redis 7.4.6 2025-10-10 12:48:57 +02:00
FreddleSpl0it 6708b94ebb [Web] Fix SOGo redirection after login 2025-10-10 10:05:56 +02:00
Thomas Mills 3dcacc4187 Change icon to filled key 2025-10-09 11:39:24 +01:00
Thomas Mills 69f0552d4f Decrease margin size 2025-10-08 21:48:03 +01:00
Thomas Mills c443a9400a Move flag in front of IP 2025-10-08 21:48:03 +01:00
Thomas Mills 5c9f387d94 Add margin 2025-10-08 21:48:02 +01:00
Thomas Mills e9414d17e4 Show app password for last logins 2025-10-08 21:47:50 +01:00
FreddleSpl0it 6bfa58611e Merge pull request #6813 from mailcow/staging
Update 2025-09c
2025-10-07 11:43:15 +02:00
FreddleSpl0it e31b6d9a07 Merge pull request #6812 from mailcow/staging
Update 2025-09c
2025-10-07 10:47:19 +02:00
FreddleSpl0it 24c62b2f09 Merge pull request #6810 from mailcow/staging
Update 2025-09c
2025-10-07 10:03:47 +02:00
renovate[bot] dd160cd508 Update dependency php/pecl-mail-mailparse to v3.1.9
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2025-09-30 13:42:36 +00:00
Hobby-Student 732b321962 fix autodiscover when using ldap with attribute mapping templates 2025-09-30 14:37:19 +02:00
Patrik Kernstock 2f8a181281 Fix comments, added some comments 2025-09-26 04:16:57 +02:00
Patrik Kernstock 83ba8d5840 Optimize opcache settings, enable JIT 2025-09-26 04:01:17 +02:00
FreddleSpl0it ef0f366d1c Merge pull request #6738 from mailcow/staging
Update 2025-09b
2025-09-12 11:29:26 +02:00
DerLinkman 4db1569c93 Squashed commit of the following:
commit 94c1a6c4e1
Author: DerLinkman <niklas.meyer@servercow.de>
Date:   Wed Sep 10 16:20:58 2025 +0200

    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:22:19 +02:00
FreddleSpl0it 7ce3b0faed Merge pull request #6719 from mailcow/staging
Update 2025-09
2025-09-10 11:18:17 +02:00
FreddleSpl0it b1c088a57f Merge pull request #6718 from mailcow/staging
Update 2025-09
2025-09-10 11:05:09 +02:00
renovate[bot] 6dc90186f9 chore(deps): update dependency krakjoe/apcu to v5.1.27
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2025-08-29 16:22:28 +00:00
FreddleSpl0it 527f27d249 Merge pull request #6632 from mailcow/staging
Update 2025-07
2025-07-15 07:48:37 +02:00
FreddleSpl0it 1994b9895b Merge pull request #6537 from mailcow/staging
Update 2025-05_2
2025-05-13 10:16:30 +02:00
FreddleSpl0it 798e6a4c00 Merge pull request #6535 from mailcow/staging
Update 2025-05
2025-05-13 09:58:32 +02:00
FreddleSpl0it 3f493e043d Merge pull request #6468 from mailcow/staging
Update 2025-03b
2025-04-07 09:09:39 +02:00
FreddleSpl0it 2c47145dee Merge pull request #6419 from mailcow/staging
Update 2025-03a
2025-03-27 09:19:29 +01:00
FreddleSpl0it c3c68360dc Merge pull request #6391 from mailcow/staging
Update 2025-03
2025-03-25 08:10:50 +01:00
FreddleSpl0it a632980871 Merge pull request #6336 from mailcow/staging
Update 2025-02
2025-02-27 11:48:57 +01:00
FreddleSpl0it 2d1ef41d32 Merge pull request #6335 from mailcow/staging
Update 2025-02
2025-02-27 11:05:55 +01:00
FreddleSpl0it 120366fec7 Merge pull request #6291 from mailcow/staging
Update 2025-01a
2025-02-04 13:55:30 +01:00
DerLinkman 244d4b8c4c compose: rollback clamd version until next major... accidentally pushed 2025-01-29 13:46:53 +01:00
DerLinkman f92ddd86c5 clamd: update to 1.4.2 + build from source instead using alpine packages (#6273)
* clamd: update to 1.4.2 + build from source instead using alpine packages

* clamd: remove exposed ports from buildfile

* clamd: cleanup dockerfile
2025-01-29 09:49:04 +01:00
FreddleSpl0it ba0349a911 Merge pull request #6256 from mailcow/staging
[Nginx] move conf.d include to end of nginx.conf
2025-01-23 14:55:38 +01:00
FreddleSpl0it 8caf09cd80 Merge pull request #6253 from mailcow/staging
2025-01
2025-01-23 12:01:38 +01:00
511 changed files with 23158 additions and 20726 deletions
@@ -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@v10.1.0 uses: actions/stale@v10.2.0
with: with:
repo-token: ${{ secrets.STALE_ACTION_PAT }} repo-token: ${{ secrets.STALE_ACTION_PAT }}
days-before-stale: 60 days-before-stale: 60
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- "watchdog-mailcow" - "watchdog-mailcow"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- 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
+2 -2
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@v5 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Run the Action - name: Run the Action
uses: devops-infra/action-pull-request@v0.6.1 uses: devops-infra/action-pull-request@v1.0.2
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}}
+5 -5
View File
@@ -13,24 +13,24 @@ jobs:
packages: write packages: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Login to GHCR - name: Login to GHCR
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
@@ -15,14 +15,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Generate postscreen_access.cidr - name: Generate postscreen_access.cidr
run: | run: |
bash helper-scripts/update_postscreen_whitelist.sh bash helper-scripts/update_postscreen_whitelist.sh
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v8
with: with:
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }} token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
commit-message: update postscreen_access.cidr commit-message: update postscreen_access.cidr
+1
View File
@@ -51,6 +51,7 @@ data/conf/sogo/cron.creds
data/conf/sogo/custom-fulllogo.svg data/conf/sogo/custom-fulllogo.svg
data/conf/sogo/custom-shortlogo.svg data/conf/sogo/custom-shortlogo.svg
data/conf/sogo/custom-fulllogo.png data/conf/sogo/custom-fulllogo.png
data/conf/acme/dns-01.conf
data/gitea/ data/gitea/
data/gogs/ data/gogs/
data/hooks/dovecot/* data/hooks/dovecot/*
+7 -3
View File
@@ -1,11 +1,11 @@
# Contribution Guidelines # Contribution Guidelines
**_Last modified on 15th August 2024_** **_Last modified on 12th November 2025_**
First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow! First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow!
As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly. As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly.
**PLEASE NOTE, THAT WE MIGHT CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULLFIL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request. **PLEASE NOTE, THAT WE WILL CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULFILL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
## Topics ## Topics
@@ -27,14 +27,18 @@ However, please note the following regarding pull requests:
6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.* 6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.*
7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project. 7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project.
8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort! 8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort!
9. If your PR requires a Docker image rebuild (changes to Dockerfiles or files in data/Dockerfiles/), update the image tag in docker-compose.yml. Use the base-image versioning (e.g. ghcr.io/mailcow/sogo:5.12.4 → :5.12.5 for version bumps; append a letter for patch fixes, e.g. :5.12.4a). Follow this scheme.
--- ---
## Issue Reporting ## Issue Reporting
**_Last modified on 15th August 2024_** **_Last modified on 12th November 2025_**
If you plan to report a issue within mailcow please read and understand the following rules: If you plan to report a issue within mailcow please read and understand the following rules:
### Security disclosures / Security-related fixes
- Security vulnerabilities and security fixes must always be reported confidentially first to the contact address specified in SECURITY.md before they are integrated, published, or publicly disclosed in issues/PRs. Please wait for a response from the specified contact to ensure coordinated and responsible disclosure.
### Issue Reporting Guidelines ### Issue Reporting Guidelines
1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support). 1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support).
+37 -37
View File
@@ -38,45 +38,45 @@ get_docker_version(){
} }
get_compose_type(){ get_compose_type(){
if docker compose > /dev/null 2>&1; then if docker compose > /dev/null 2>&1; then
if docker compose version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then if docker compose version --short | grep -e "^[2-9]\." -e "^v[2-9]\." -e "^[1-9][0-9]\." -e "^v[1-9][0-9]\." > /dev/null 2>&1; then
COMPOSE_VERSION=native COMPOSE_VERSION=native
COMPOSE_COMMAND="docker compose" COMPOSE_COMMAND="docker compose"
if [[ "$caller" == "update.sh" ]]; then if [[ "$caller" == "update.sh" ]]; then
sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf" sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf"
fi fi
echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m" echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m"
echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m" echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m"
sleep 2 sleep 2
echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m" 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 else
echo -e "\e[31mCannot find Docker Compose.\e[0m" echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m"
echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m" echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m"
exit 1 exit 1
fi fi
elif docker-compose > /dev/null 2>&1; then
if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then
if docker-compose version --short | grep -e "^[2-9]\." -e "^[1-9][0-9]\." > /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() { detect_bad_asn() {
+18 -1
View File
@@ -57,11 +57,14 @@ adapt_new_options() {
"DISABLE_NETFILTER_ISOLATION_RULE" "DISABLE_NETFILTER_ISOLATION_RULE"
"HTTP_REDIRECT" "HTTP_REDIRECT"
"ENABLE_IPV6" "ENABLE_IPV6"
"ACME_DNS_CHALLENGE"
"ACME_DNS_PROVIDER"
"ACME_ACCOUNT_EMAIL"
) )
sed -i --follow-symlinks '$a\' mailcow.conf sed -i --follow-symlinks '$a\' mailcow.conf
for option in ${CONFIG_ARRAY[@]}; do for option in ${CONFIG_ARRAY[@]}; do
if grep -q "${option}" mailcow.conf; then if grep -q "^#\?${option}=" mailcow.conf; then
continue continue
fi fi
@@ -292,6 +295,20 @@ adapt_new_options() {
echo '# This key is used to encrypt email addresses within SOGo URLs' >> 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 "SOGO_URL_ENCRYPTION_KEY=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 2>/dev/null | head -c 16)" >> mailcow.conf
;; ;;
ACME_DNS_CHALLENGE)
echo '# Enable DNS-01 challenge for ACME (acme-mailcow) - y/n' >> mailcow.conf
echo '# This requires you to set ACME_DNS_PROVIDER and ACME_ACCOUNT_EMAIL below' >> mailcow.conf
echo 'ACME_DNS_CHALLENGE=n' >> mailcow.conf
;;
ACME_DNS_PROVIDER)
echo '# DNS provider for DNS-01 challenge (e.g. dns_cf, dns_azure, dns_gd, etc.)' >> mailcow.conf
echo '# See the dns-01 provider documentation for more information.' >> mailcow.conf
echo 'ACME_DNS_PROVIDER=dns_xxx' >> mailcow.conf
;;
ACME_ACCOUNT_EMAIL)
echo '# Account email for ACME DNS-01 challenge registration' >> mailcow.conf
echo 'ACME_ACCOUNT_EMAIL=me@example.com' >> mailcow.conf
;;
*) *)
echo "${option}=" >> mailcow.conf echo "${option}=" >> mailcow.conf
;; ;;
+13 -2
View File
@@ -1,4 +1,4 @@
FROM alpine:3.21 FROM alpine:3.23
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
@@ -14,11 +14,22 @@ RUN apk upgrade --no-cache \
tini \ tini \
tzdata \ tzdata \
python3 \ python3 \
acme-tiny acme-tiny \
git \
socat \
&& git clone --depth 1 https://github.com/acmesh-official/acme.sh.git /opt/acme.sh \
&& chmod +x /opt/acme.sh/acme.sh \
&& mkdir -p /var/lib/acme/acme-sh
ENV ACME_SH_BIN=/opt/acme.sh/acme.sh \
ACME_SH_HOME=/opt/acme.sh \
ACME_SH_CONFIG_HOME=/var/lib/acme/acme-sh
COPY acme.sh /srv/acme.sh COPY acme.sh /srv/acme.sh
COPY functions.sh /srv/functions.sh COPY functions.sh /srv/functions.sh
COPY obtain-certificate.sh /srv/obtain-certificate.sh COPY obtain-certificate.sh /srv/obtain-certificate.sh
COPY obtain-certificate-dns.sh /srv/obtain-certificate-dns.sh
COPY load-dns-config.sh /srv/load-dns-config.sh
COPY reload-configurations.sh /srv/reload-configurations.sh COPY reload-configurations.sh /srv/reload-configurations.sh
COPY expand6.sh /srv/expand6.sh COPY expand6.sh /srv/expand6.sh
+42 -8
View File
@@ -3,17 +3,28 @@ 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 ${VALKEY_SLAVEOF_IP} ]]; then if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
export VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning" export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
else else
export VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning" export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
fi fi
until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Valkey..." echo "Waiting for Redis..."
sleep 2 sleep 2
done done
# Create DNS-01 configuration template if it doesn't exist
if [[ ! -f /etc/acme/dns-01.conf ]]; then
mkdir -p /etc/acme
cat > /etc/acme/dns-01.conf <<'EOF'
# Add here your DNS-01 challenge configuration
# For more information, visit the acme.sh documentation:
# https://github.com/acmesh-official/acme.sh/wiki/dnsapi
EOF
echo "Created DNS-01 configuration template at /etc/acme/dns-01.conf"
fi
source /srv/functions.sh source /srv/functions.sh
# Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6 # Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6
source /srv/expand6.sh source /srv/expand6.sh
@@ -42,6 +53,10 @@ if [[ "${ENABLE_SSL_SNI}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
ENABLE_SSL_SNI=y ENABLE_SSL_SNI=y
fi fi
if [[ "${ACME_DNS_CHALLENGE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
ACME_DNS_CHALLENGE=y
fi
if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..." log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..."
sleep 365d sleep 365d
@@ -246,6 +261,25 @@ while true; do
done done
VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}") VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}")
done done
# Fetch alias domains where target domain has MTA-STS enabled
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
SQL_ALIAS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT ad.alias_domain FROM alias_domain ad INNER JOIN mta_sts m ON ad.target_domain = m.domain WHERE ad.active = 1 AND m.active = 1" -Bs)
if [[ $? -eq 0 ]]; then
while read alias_domain; do
if [[ -z "${alias_domain}" ]]; then
# ignore empty lines
continue
fi
# Only add mta-sts subdomain for alias domains
if [[ "mta-sts.${alias_domain}" != "${MAILCOW_HOSTNAME}" ]]; then
if check_domain "mta-sts.${alias_domain}"; then
VALIDATED_CONFIG_DOMAINS+=("mta-sts.${alias_domain}")
fi
fi
done <<< "${SQL_ALIAS_DOMAINS}"
fi
fi
fi fi
if check_domain ${MAILCOW_HOSTNAME}; then if check_domain ${MAILCOW_HOSTNAME}; then
@@ -348,7 +382,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."
${VALKEY_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)" ${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
sleep 1h sleep 1h
exec $(readlink -f "$0") exec $(readlink -f "$0")
fi fi
@@ -389,7 +423,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!"
${VALKEY_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)" ${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
break; break;
fi fi
done done
@@ -410,7 +444,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..."
${VALKEY_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)" ${REDIS_CMDLINE} SET ACME_FAIL_TIME "$(date +%s)"
sleep 30m sleep 30m
exec $(readlink -f "$0") exec $(readlink -f "$0")
;; ;;
+8 -3
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} != "valkey_only" ]]; then elif [[ ${2} != "redis_only" ]]; then
echo "$(date) - ${1}" echo "$(date) - ${1}"
fi fi
if [[ ${3} == "b64" ]]; then if [[ ${3} == "b64" ]]; then
${VALKEY_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"base64,$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}")\"}" > /dev/null ${REDIS_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"base64,$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}")\"}" > /dev/null
else else
${VALKEY_CMDLINE} LPUSH ACME_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${MAILCOW_HOSTNAME} - ${1}" | \ ${REDIS_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
} }
@@ -80,6 +80,11 @@ check_domain(){
return 1 return 1
fi fi
fi fi
if [[ ${ACME_DNS_CHALLENGE} == "y" ]]; then
log_f "ACME_DNS_CHALLENGE=y - skipping IP and HTTP validation for ${DOMAIN}"
return 0
fi
# Check if CNAME without v6 enabled target # Check if CNAME without v6 enabled target
if [[ ! -z ${AAAA_DOMAIN} ]] && [[ -z $(echo ${AAAA_DOMAIN} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") ]]; then if [[ ! -z ${AAAA_DOMAIN} ]] && [[ -z $(echo ${AAAA_DOMAIN} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") ]]; then
AAAA_DOMAIN= AAAA_DOMAIN=
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
SCRIPT_SOURCE="${BASH_SOURCE[0]:-${0}}"
if [[ "${SCRIPT_SOURCE}" == "${0}" ]]; then
__dns_loader_standalone=1
else
__dns_loader_standalone=0
fi
CONFIG_PATH="${ACME_DNS_CONFIG_FILE:-/etc/acme/dns-01.conf}"
if [[ ! -f "${CONFIG_PATH}" ]]; then
if [[ $__dns_loader_standalone -eq 1 ]]; then
exit 0
else
return 0
fi
fi
source /srv/functions.sh
log_f "Loading DNS-01 configuration from ${CONFIG_PATH}"
LINE_NO=0
while IFS= read -r line || [[ -n "${line}" ]]; do
LINE_NO=$((LINE_NO+1))
line="${line%$'\r'}"
line_trimmed="$(printf '%s' "${line}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
[[ -z "${line_trimmed}" ]] && continue
[[ "${line_trimmed:0:1}" == "#" ]] && continue
if [[ "${line_trimmed}" != *=* ]]; then
log_f "Skipping invalid DNS config line ${LINE_NO} (missing key=value)"
continue
fi
KEY="${line_trimmed%%=*}"
VALUE="${line_trimmed#*=}"
KEY="$(printf '%s' "${KEY}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
VALUE="$(printf '%s' "${VALUE}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
if [[ -z "${KEY}" ]]; then
log_f "Skipping invalid DNS config line ${LINE_NO} (empty key)"
continue
fi
if [[ "${VALUE}" =~ ^\".*\"$ ]]; then
VALUE="${VALUE:1:-1}"
elif [[ "${VALUE}" =~ ^\'.*\'$ ]]; then
VALUE="${VALUE:1:-1}"
fi
export "${KEY}"="${VALUE}"
log_f "Exported DNS config key ${KEY}"
done < "${CONFIG_PATH}"
if [[ $__dns_loader_standalone -eq 1 ]]; then
exit 0
else
return 0
fi
@@ -0,0 +1,177 @@
#!/bin/bash
# Return values / exit codes
# 0 = cert created successfully
# 1 = cert renewed successfully
# 2 = cert not due for renewal
# * = errors
source /srv/functions.sh
CERT_DOMAINS=(${DOMAINS[@]})
CERT_DOMAIN=${CERT_DOMAINS[0]}
ACME_BASE=/var/lib/acme
# Load optional DNS provider secrets from /etc/acme/dns-01.conf
if [[ -f /srv/load-dns-config.sh ]]; then
source /srv/load-dns-config.sh
if declare -F log_f >/dev/null; then
log_f "ACME_DNS_CHALLENGE is enabled, DNS provider secrets loaded"
fi
fi
TYPE=${1}
PREFIX=""
# only support rsa certificates for now
if [[ "${TYPE}" != "rsa" ]]; then
log_f "Unknown certificate type '${TYPE}' requested"
exit 5
fi
if [[ -z "${ACME_DNS_PROVIDER}" ]]; then
log_f "ACME_DNS_PROVIDER is required when ACME_DNS_CHALLENGE is enabled"
exit 6
fi
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
KEY=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}key.pem
CSR=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}acme.csr
if [[ -z ${CERT_DOMAINS[*]} ]]; then
log_f "Missing CERT_DOMAINS to obtain a certificate"
exit 3
fi
if [[ "${LE_STAGING}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
if [[ ! -z "${DIRECTORY_URL}" ]]; then
log_f "Cannot use DIRECTORY_URL with LE_STAGING=y - ignoring DIRECTORY_URL"
fi
log_f "Using Let's Encrypt staging servers"
ACME_SH_SERVER_ARGS=("--staging")
elif [[ ! -z "${DIRECTORY_URL}" ]]; then
log_f "Using custom directory URL ${DIRECTORY_URL}"
ACME_SH_SERVER_ARGS=("--server" "${DIRECTORY_URL}")
else
log_f "Using Let's Encrypt production servers"
ACME_SH_SERVER_ARGS=("--server" "letsencrypt")
fi
if [[ -f ${DOMAINS_FILE} && "$(cat ${DOMAINS_FILE})" == "${CERT_DOMAINS[*]}" ]]; then
if [[ ! -f ${CERT} || ! -f "${KEY}" || -f "${ACME_BASE}/force_renew" ]]; then
log_f "Certificate ${CERT} doesn't exist yet or forced renewal - start obtaining"
elif ! openssl x509 -checkend 2592000 -noout -in ${CERT} > /dev/null; then
log_f "Certificate ${CERT} is due for renewal (< 30 days) - start renewing"
else
log_f "Certificate ${CERT} validation done, neither changed nor due for renewal."
exit 2
fi
else
log_f "Certificate ${CERT} missing or changed domains '${CERT_DOMAINS[*]}' - start obtaining"
fi
# Make backup
if [[ -f ${CERT} ]]; then
DATE=$(date +%Y-%m-%d_%H_%M_%S)
BACKUP_DIR=${ACME_BASE}/backups/${CERT_DOMAIN}/${PREFIX}${DATE}
log_f "Creating backups in ${BACKUP_DIR} ..."
mkdir -p ${BACKUP_DIR}/
[[ -f ${DOMAINS_FILE} ]] && cp ${DOMAINS_FILE} ${BACKUP_DIR}/
[[ -f ${CERT} ]] && cp ${CERT} ${BACKUP_DIR}/
[[ -f ${KEY} ]] && cp ${KEY} ${BACKUP_DIR}/
[[ -f ${CSR} ]] && cp ${CSR} ${BACKUP_DIR}/
fi
mkdir -p ${ACME_BASE}/${CERT_DOMAIN}
if [[ ! -f ${KEY} ]]; then
log_f "Copying shared private key for this certificate..."
cp ${SHARED_KEY} ${KEY}
chmod 600 ${KEY}
fi
# Generating CSR to keep layout parity with HTTP challenge flow
printf "[SAN]\nsubjectAltName=" > /tmp/_SAN
printf "DNS:%s," "${CERT_DOMAINS[@]}" >> /tmp/_SAN
sed -i '$s/,$//' /tmp/_SAN
openssl req -new -sha256 -key ${KEY} -subj "/" -reqexts SAN -config <(cat "$(openssl version -d | sed 's/.*\"\(.*\)\"/\1/g')/openssl.cnf" /tmp/_SAN) > ${CSR}
log_f "Checking resolver..."
until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do
sleep 2
done
log_f "Resolver OK"
ACME_SH_BIN_PATH=${ACME_SH_BIN:-/opt/acme.sh/acme.sh}
ACME_SH_WORK_HOME=${ACME_SH_CONFIG_HOME:-/var/lib/acme/acme-sh}
mkdir -p ${ACME_SH_WORK_HOME}
if [[ ! -x ${ACME_SH_BIN_PATH} ]]; then
log_f "acme.sh binary not found at ${ACME_SH_BIN_PATH}"
exit 7
fi
if [[ ! -f ${ACME_SH_WORK_HOME}/account.conf ]]; then
if [[ -z "${ACME_ACCOUNT_EMAIL}" ]]; then
log_f "ACME_ACCOUNT_EMAIL is required to register a new acme.sh account"
exit 8
fi
log_f "Registering acme.sh account for ${ACME_ACCOUNT_EMAIL}"
REGISTER_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}" "--register-account" "-m" "${ACME_ACCOUNT_EMAIL}")
REGISTER_CMD+=("${ACME_SH_SERVER_ARGS[@]}")
REGISTER_RESPONSE=$("${REGISTER_CMD[@]}" 2>&1)
if [[ $? -ne 0 ]]; then
log_f "Failed to register acme.sh account: ${REGISTER_RESPONSE}"
exit 9
fi
fi
TMP_CERT=$(mktemp /tmp/acme-cert.XXXXXX)
TMP_FULLCHAIN=$(mktemp /tmp/acme-fullchain.XXXXXX)
ACME_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}")
ACME_CMD+=("${ACME_SH_SERVER_ARGS[@]}")
ACME_CMD+=("--issue" "--dns" "${ACME_DNS_PROVIDER}" "--key-file" "${KEY}" "--cert-file" "${TMP_CERT}" "--fullchain-file" "${TMP_FULLCHAIN}" "--force")
for domain in "${CERT_DOMAINS[@]}"; do
ACME_CMD+=("-d" "${domain}")
done
log_f "Using command ${ACME_CMD[*]}"
if [[ -n "${ACME_DNS_PROVIDER}" ]]; then
log_f "DNS provider: ${ACME_DNS_PROVIDER}"
fi
if compgen -A variable | grep -Eq "^DNS_|^ACME_"; then
LOG_KEYS=$(env | grep -E "^(DNS_|ACME_)" | cut -d= -f1 | tr '\n' ' ')
log_f "Available DNS/ACME env keys: ${LOG_KEYS}" redis_only
fi
ACME_RESPONSE=$("${ACME_CMD[@]}" 2>&1 | tee /dev/fd/5; exit ${PIPESTATUS[0]})
SUCCESS="$?"
ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64)
log_f "${ACME_RESPONSE_B64}" redis_only b64
case "$SUCCESS" in
0)
log_f "Deploying certificate ${CERT}..."
if verify_hash_match ${TMP_FULLCHAIN} ${KEY}; then
RETURN=0
if [[ -f ${CERT} ]]; then
RETURN=1
fi
mv -f ${TMP_FULLCHAIN} ${CERT}
rm -f ${TMP_CERT}
echo -n ${CERT_DOMAINS[*]} > ${DOMAINS_FILE}
log_f "Certificate successfully obtained via DNS challenge"
exit ${RETURN}
else
log_f "Certificate was requested, but key and certificate hashes do not match"
rm -f ${TMP_CERT} ${TMP_FULLCHAIN}
exit 4
fi
;;
*)
log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}' via DNS challenge"
redis-cli -h redis -a ${REDISPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)"
rm -f ${TMP_CERT} ${TMP_FULLCHAIN}
exit 100${SUCCESS}
;;
esac
+6 -2
View File
@@ -20,6 +20,10 @@ if [[ "${TYPE}" != "rsa" ]]; then
log_f "Unknown certificate type '${TYPE}' requested" log_f "Unknown certificate type '${TYPE}' requested"
exit 5 exit 5
fi fi
if [[ "${ACME_DNS_CHALLENGE}" == "y" ]]; then
exec /srv/obtain-certificate-dns.sh "$@"
fi
DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains
CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem
SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist
@@ -101,7 +105,7 @@ ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} \
--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}" valkey_only b64 log_f "${ACME_RESPONSE_B64}" redis_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 +128,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 valkey-mailcow -a ${VALKEYPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)" redis-cli -h redis -a ${REDISPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)"
exit 100${SUCCESS} exit 100${SUCCESS}
;; ;;
esac esac
+2 -2
View File
@@ -1,3 +1,3 @@
FROM debian:bookworm-slim FROM debian:trixie-slim
RUN apt update && apt install pigz -y --no-install-recommends RUN apt update && apt install pigz zstd -y --no-install-recommends
+1 -1
View File
@@ -1,4 +1,4 @@
FROM alpine:3.21 FROM alpine:3.23
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
+16 -16
View File
@@ -32,21 +32,21 @@ async def lifespan(app: FastAPI):
logger.info("Init APP") logger.info("Init APP")
# Init valkey client # Init redis client
if os.environ['VALKEY_SLAVEOF_IP'] != "": if os.environ['REDIS_SLAVEOF_IP'] != "":
valkey_client = valkey = await aioredis.from_url(f"redis://{os.environ['VALKEY_SLAVEOF_IP']}:{os.environ['VALKEY_SLAVEOF_PORT']}/0", password=os.environ['VALKEYPASS']) redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0", password=os.environ['REDISPASS'])
else: else:
valkey_client = valkey = await aioredis.from_url("redis://valkey-mailcow:6379/0", password=os.environ['VALKEYPASS']) redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0", password=os.environ['REDISPASS'])
# 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(valkey_client, sync_docker_client, async_docker_client, logger) dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
logger.info("Subscribe to valkey channel") logger.info("Subscribe to redis channel")
# Subscribe to valkey channel # Subscribe to redis channel
dockerapi.pubsub = valkey.pubsub() dockerapi.pubsub = redis.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 valkey # Close redis
await dockerapi.pubsub.unsubscribe("MC_CHANNEL") await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
await dockerapi.valkey_client.close() await dockerapi.redis_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.valkey_client.exists('host_stats'): if await dockerapi.redis_client.exists('host_stats'):
break break
await asyncio.sleep(1.5) await asyncio.sleep(1.5)
stats = json.loads(await dockerapi.valkey_client.get('host_stats')) stats = json.loads(await dockerapi.redis_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")
@@ -110,12 +110,12 @@ async def get_container(container_id : str):
return Response(content=json.dumps(res, indent=4), media_type="application/json") return Response(content=json.dumps(res, indent=4), media_type="application/json")
@app.get("/containers/json") @app.get("/containers/json")
async def get_containers(): async def get_containers(all: bool = False):
global dockerapi global dockerapi
containers = {} containers = {}
try: try:
for container in (await dockerapi.async_docker_client.containers.list()): for container in (await dockerapi.async_docker_client.containers.list(all=all)):
container_info = await container.show() container_info = await container.show()
containers.update({container_info['Id']: container_info}) containers.update({container_info['Id']: container_info})
return Response(content=json.dumps(containers, indent=4), media_type="application/json") return Response(content=json.dumps(containers, indent=4), media_type="application/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.valkey_client.exists(container_id + '_stats'): if await dockerapi.redis_client.exists(container_id + '_stats'):
break break
await asyncio.sleep(1.5) await asyncio.sleep(1.5)
stats = json.loads(await dockerapi.valkey_client.get(container_id + '_stats')) stats = json.loads(await dockerapi.redis_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")
@@ -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, valkey_client, sync_docker_client, async_docker_client, logger): def __init__(self, redis_client, sync_docker_client, async_docker_client, logger):
self.valkey_client = valkey_client self.redis_client = redis_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.valkey_client.set('host_stats', json.dumps(host_stats), ex=10) await self.redis_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.valkey_client.exists(container_id + '_stats'): if await self.redis_client.exists(container_id + '_stats'):
stats = json.loads(await self.valkey_client.get(container_id + '_stats')) stats = json.loads(await self.redis_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.valkey_client.set(container_id + '_stats', json.dumps(stats), ex=60) await self.redis_client.set(container_id + '_stats', json.dumps(stats), ex=60)
except Exception as e: except Exception as e:
res = { res = {
"type": "danger", "type": "danger",
+2 -2
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.17 ARG GOSU_VERSION=1.19
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-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_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
+1 -1
View File
@@ -2,7 +2,7 @@
source /source_env.sh source /source_env.sh
MAX_AGE=$(redis-cli --raw -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET Q_MAX_AGE) MAX_AGE=$(redis-cli --raw -h redis-mailcow -a ${REDISPASS} --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"
+12 -11
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 ${VALKEY_SLAVEOF_IP} ]]; then if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning" REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
else else
VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning" REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
fi fi
until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Valkey..." echo "Waiting for Redis..."
sleep 2 sleep 2
done done
${VALKEY_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null ${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
# Create missing directories # Create missing directories
[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/ [[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
@@ -204,16 +204,17 @@ EOF
# Create random master Password for SOGo SSO # Create random master Password for SOGo SSO
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1) RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
# Creating additional creds file for SOGo notify crons (calendars, etc)
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
cat <<EOF > /etc/dovecot/sogo-sso.conf cat <<EOF > /etc/dovecot/sogo-sso.conf
# Autogenerated by mailcow # Autogenerated by mailcow
passdb { passdb {
driver = static driver = static
args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS} args = allow_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS}
} }
EOF EOF
# Creating additional creds file for SOGo notify crons (calendars, etc) (dummy user, sso password)
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then
# Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated # Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated
cat <<'EOF' > /usr/local/bin/quota_notify.py cat <<'EOF' > /usr/local/bin/quota_notify.py
@@ -341,8 +342,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 ${VALKEY_SLAVEOF_IP} ]]; then if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
fi fi
exec "$@" exec "$@"
@@ -32,7 +32,7 @@ try:
while True: while True:
try: try:
r = redis.StrictRedis(host='valkey-mailcow', decode_responses=True, port=6379, db=0, password=os.environ['VALKEYPASS']) r = redis.StrictRedis(host='redis', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS'])
r.ping() r.ping()
except Exception as ex: except Exception as ex:
print('%s - trying again...' % (ex)) print('%s - trying again...' % (ex))
+1 -1
View File
@@ -23,7 +23,7 @@ else:
while True: while True:
try: try:
r = redis.StrictRedis(host='valkey-mailcow', decode_responses=True, port=6379, db=0, username='quota_notify', password='') r = redis.StrictRedis(host='redis', 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))
+6 -6
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 ${VALKEY_SLAVEOF_IP} ]]; then if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning" REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
else else
VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning" REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --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
${VALKEY_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null ${REDIS_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}"
${VALKEY_CMDLINE} SET DOVECOT_REPL_HEALTH "${FAILED_SYNCS}" > /dev/null ${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH "${FAILED_SYNCS}" > /dev/null
else else
${VALKEY_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null ${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
fi fi
@@ -15,21 +15,21 @@ source s_dgram {
internal(); internal();
}; };
destination d_stdout { pipe("/dev/stdout"); }; destination d_stdout { pipe("/dev/stdout"); };
destination d_valkey_ui_log { destination d_redis_ui_log {
redis( redis(
host("`VALKEY_SLAVEOF_IP`") host("`REDIS_SLAVEOF_IP`")
persist-name("valkey1") persist-name("redis1")
port(`VALKEY_SLAVEOF_PORT`) port(`REDIS_SLAVEOF_PORT`)
auth("`VALKEYPASS`") auth("`REDISPASS`")
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_valkey_f2b_channel { destination d_redis_f2b_channel {
redis( redis(
host("`VALKEY_SLAVEOF_IP`") host("`REDIS_SLAVEOF_IP`")
persist-name("valkey2") persist-name("redis2")
port(`VALKEY_SLAVEOF_PORT`) port(`REDIS_SLAVEOF_PORT`)
auth("`VALKEYPASS`") auth("`REDISPASS`")
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_valkey_ui_log); destination(d_redis_ui_log);
destination(d_valkey_f2b_channel); destination(d_redis_f2b_channel);
}; };
+10 -10
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_valkey_ui_log { destination d_redis_ui_log {
redis( redis(
host("valkey-mailcow") host("redis-mailcow")
persist-name("valkey1") persist-name("redis1")
port(6379) port(6379)
auth("`VALKEYPASS`") auth("`REDISPASS`")
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_valkey_f2b_channel { destination d_redis_f2b_channel {
redis( redis(
host("valkey-mailcow") host("redis-mailcow")
persist-name("valkey2") persist-name("redis2")
port(6379) port(6379)
auth("`VALKEYPASS`") auth("`REDISPASS`")
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_valkey_ui_log); destination(d_redis_ui_log);
destination(d_valkey_f2b_channel); destination(d_redis_f2b_channel);
}; };
+13 -12
View File
@@ -9,17 +9,18 @@ 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 ${VALKEY_SLAVEOF_IP} ]]; then if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning" REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
else else
VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning" REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
fi fi
catch_non_zero "${VALKEY_CMDLINE} LTRIM ACME_LOG 0 ${LOG_LINES}" catch_non_zero "${REDIS_CMDLINE} LTRIM ACME_LOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM POSTFIX_MAILLOG 0 ${LOG_LINES}" catch_non_zero "${REDIS_CMDLINE} LTRIM POSTFIX_MAILLOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM DOVECOT_MAILLOG 0 ${LOG_LINES}" catch_non_zero "${REDIS_CMDLINE} LTRIM DOVECOT_MAILLOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM SOGO_LOG 0 ${LOG_LINES}" catch_non_zero "${REDIS_CMDLINE} LTRIM SOGO_LOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM NETFILTER_LOG 0 ${LOG_LINES}" catch_non_zero "${REDIS_CMDLINE} LTRIM NETFILTER_LOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM AUTODISCOVER_LOG 0 ${LOG_LINES}" catch_non_zero "${REDIS_CMDLINE} LTRIM AUTODISCOVER_LOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM API_LOG 0 ${LOG_LINES}" catch_non_zero "${REDIS_CMDLINE} LTRIM API_LOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM RL_LOG 0 ${LOG_LINES}" catch_non_zero "${REDIS_CMDLINE} LTRIM RL_LOG 0 ${LOG_LINES}"
catch_non_zero "${VALKEY_CMDLINE} LTRIM WATCHDOG_LOG 0 ${LOG_LINES}" catch_non_zero "${REDIS_CMDLINE} LTRIM WATCHDOG_LOG 0 ${LOG_LINES}"
catch_non_zero "${REDIS_CMDLINE} LTRIM CRON_LOG 0 ${LOG_LINES}"
+2 -2
View File
@@ -1,4 +1,4 @@
FROM alpine:3.21 FROM alpine:3.23
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
@@ -40,4 +40,4 @@ COPY ./docker-entrypoint.sh /app/
RUN chmod +x /app/docker-entrypoint.sh RUN chmod +x /app/docker-entrypoint.sh
CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"] CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"]
+46 -45
View File
@@ -44,24 +44,25 @@ def refreshF2boptions():
global exit_code global exit_code
f2boptions = {} f2boptions = {}
if not valkey.get('F2B_OPTIONS'): if not r.get('F2B_OPTIONS'):
f2boptions['ban_time'] = valkey.get('F2B_BAN_TIME') f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
f2boptions['max_ban_time'] = valkey.get('F2B_MAX_BAN_TIME') f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
f2boptions['ban_time_increment'] = valkey.get('F2B_BAN_TIME_INCREMENT') f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
f2boptions['max_attempts'] = valkey.get('F2B_MAX_ATTEMPTS') f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
f2boptions['retry_window'] = valkey.get('F2B_RETRY_WINDOW') f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
f2boptions['netban_ipv4'] = valkey.get('F2B_NETBAN_IPV4') f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
f2boptions['netban_ipv6'] = valkey.get('F2B_NETBAN_IPV6') f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
else: else:
try: try:
f2boptions = json.loads(valkey.get('F2B_OPTIONS')) f2boptions = json.loads(r.get('F2B_OPTIONS'))
except ValueError: except ValueError as e:
logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json') logger.logCrit(
'Error loading F2B options: F2B_OPTIONS is not json. Exception: %s' % e)
quit_now = True quit_now = True
exit_code = 2 exit_code = 2
verifyF2boptions(f2boptions) verifyF2boptions(f2boptions)
valkey.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False)) r.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)
@@ -81,7 +82,7 @@ def refreshF2bregex():
global f2bregex global f2bregex
global quit_now global quit_now
global exit_code global exit_code
if not valkey.get('F2B_REGEX'): if not r.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\.:]+)'
@@ -92,11 +93,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 .+'
valkey.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False)) r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
else: else:
try: try:
f2bregex = {} f2bregex = {}
f2bregex = json.loads(valkey.get('F2B_REGEX')) f2bregex = json.loads(r.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
@@ -175,7 +176,7 @@ def ban(address):
logdebug("Updating F2B_ACTIVE_BANS[%s]=%d" % logdebug("Updating F2B_ACTIVE_BANS[%s]=%d" %
(net, cur_time + NET_BAN_TIME)) (net, cur_time + NET_BAN_TIME))
valkey.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME) r.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' % ( logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (
MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)) MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
@@ -186,7 +187,7 @@ def unban(net):
if not net in bans: if not net in bans:
logger.logInfo( logger.logInfo(
'%s is not banned, skipping unban and deleting from queue (if any)' % net) '%s is not banned, skipping unban and deleting from queue (if any)' % net)
valkey.hdel('F2B_QUEUE_UNBAN', '%s' % net) r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
return 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:
@@ -197,8 +198,8 @@ def unban(net):
with lock: with lock:
logdebug("Calling tables.unbanIPv6(%s)" % net) 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) logdebug("Unban for %s, setting attempts=0, ban_counter+=1" % net)
bans[net]['attempts'] = 0 bans[net]['attempts'] = 0
@@ -225,10 +226,10 @@ def permBan(net, unban=False):
if is_unbanned: if is_unbanned:
valkey.hdel('F2B_PERM_BANS', '%s' % net) r.hdel('F2B_PERM_BANS', '%s' % net)
logger.logCrit('Removed host/network %s from denylist' % net) logger.logCrit('Removed host/network %s from denylist' % net)
elif is_banned: elif is_banned:
valkey.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
logger.logCrit('Added host/network %s to denylist' % net) logger.logCrit('Added host/network %s to denylist' % net)
def clear(): def clear():
@@ -243,17 +244,17 @@ def clear():
tables.clearIPv6Table() tables.clearIPv6Table()
try: try:
if r is not None: if r is not None:
valkey.delete('F2B_ACTIVE_BANS') r.delete('F2B_ACTIVE_BANS')
valkey.delete('F2B_PERM_BANS') r.delete('F2B_PERM_BANS')
except Exception as ex: except Exception as ex:
logger.logWarn('Error clearing valkey keys F2B_ACTIVE_BANS and F2B_PERM_BANS: %s' % ex) logger.logWarn('Error clearing redis 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 Valkey channel F2B_CHANNEL') logger.logInfo('Watching Redis channel F2B_CHANNEL')
pubsub.subscribe('F2B_CHANNEL') pubsub.subscribe('F2B_CHANNEL')
while not quit_now: while not quit_now:
@@ -305,7 +306,7 @@ def autopurge():
time.sleep(10) time.sleep(10)
refreshF2boptions() refreshF2boptions()
MAX_ATTEMPTS = int(f2boptions['max_attempts']) MAX_ATTEMPTS = int(f2boptions['max_attempts'])
QUEUE_UNBAN = valkey.hgetall('F2B_QUEUE_UNBAN') QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
logdebug("QUEUE_UNBAN: %s" % QUEUE_UNBAN) logdebug("QUEUE_UNBAN: %s" % QUEUE_UNBAN)
if QUEUE_UNBAN: if QUEUE_UNBAN:
for net in QUEUE_UNBAN: for net in QUEUE_UNBAN:
@@ -390,7 +391,7 @@ def whitelistUpdate():
global WHITELIST global WHITELIST
while not quit_now: while not quit_now:
start_time = time.time() start_time = time.time()
list = valkey.hgetall('F2B_WHITELIST') list = r.hgetall('F2B_WHITELIST')
new_whitelist = [] new_whitelist = []
if list: if list:
new_whitelist = genNetworkList(list) new_whitelist = genNetworkList(list)
@@ -405,7 +406,7 @@ def blacklistUpdate():
global BLACKLIST global BLACKLIST
while not quit_now: while not quit_now:
start_time = time.time() start_time = time.time()
list = valkey.hgetall('F2B_BLACKLIST') list = r.hgetall('F2B_BLACKLIST')
new_blacklist = [] new_blacklist = []
if list: if list:
new_blacklist = genNetworkList(list) new_blacklist = genNetworkList(list)
@@ -466,35 +467,35 @@ if __name__ == '__main__':
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 valkey # connect to redis
while True: while True:
try: try:
valkey_slaveof_ip = os.getenv('VALKEY_SLAVEOF_IP', '') redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
valkey_slaveof_port = os.getenv('VALKEY_SLAVEOF_PORT', '') redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
logdebug( logdebug(
"Connecting valkey (SLAVEOF_IP:%s, PORT:%s)" % (valkey_slaveof_ip, valkey_slaveof_port)) "Connecting redis (SLAVEOF_IP:%s, PORT:%s)" % (redis_slaveof_ip, redis_slaveof_port))
if "".__eq__(valkey_slaveof_ip): if "".__eq__(redis_slaveof_ip):
valkey = redis.StrictRedis( r = redis.StrictRedis(
host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['VALKEYPASS']) host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS'])
else: else:
valkey = redis.StrictRedis( r = redis.StrictRedis(
host=valkey_slaveof_ip, decode_responses=True, port=valkey_slaveof_port, db=0, password=os.environ['VALKEYPASS']) host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS'])
valkey.ping() r.ping()
pubsub = valkey.pubsub() pubsub = r.pubsub()
except Exception as ex: except Exception as ex:
logdebug( logdebug(
'Redis connection failed: %s - trying again in 3 seconds' % (ex)) 'Redis connection failed: %s - trying again in 3 seconds' % (ex))
time.sleep(3) time.sleep(3)
else: else:
break break
logger.set_valkey(valkey) logger.set_redis(r)
logdebug("Valkey connection established, setting up F2B keys") logdebug("Redis connection established, setting up F2B keys")
if valkey.exists('F2B_LOG'): if r.exists('F2B_LOG'):
logdebug("Renaming F2B_LOG to NETFILTER_LOG") logdebug("Renaming F2B_LOG to NETFILTER_LOG")
valkey.rename('F2B_LOG', 'NETFILTER_LOG') r.rename('F2B_LOG', 'NETFILTER_LOG')
valkey.delete('F2B_ACTIVE_BANS') r.delete('F2B_ACTIVE_BANS')
valkey.delete('F2B_PERM_BANS') r.delete('F2B_PERM_BANS')
refreshF2boptions() refreshF2boptions()
+8 -8
View File
@@ -4,19 +4,19 @@ import datetime
class Logger: class Logger:
def __init__(self): def __init__(self):
self.valkey = None self.r = None
def set_valkey(self, valkey): def set_redis(self, redis):
self.valkey = valkey self.r = redis
def _format_timestamp(self): def _format_timestamp(self):
# Local time with milliseconds # Local time with milliseconds
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def log(self, priority, message): def log(self, priority, message):
# build valkey-friendly dict # build redis-friendly dict
tolog = { tolog = {
'time': int(round(time.time())), # keep raw timestamp for Valkey 'time': int(round(time.time())), # keep raw timestamp for Redis
'priority': priority, 'priority': priority,
'message': message 'message': message
} }
@@ -26,11 +26,11 @@ class Logger:
print(f"{ts} {priority.upper()}: {message}", flush=True) print(f"{ts} {priority.upper()}: {message}", flush=True)
# also push JSON to Redis if connected # also push JSON to Redis if connected
if self.valkey is not None: if self.r is not None:
try: try:
self.valkey.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False)) self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
except Exception as ex: except Exception as ex:
print(f'{ts} WARN: Failed logging to valkey: {ex}', flush=True) print(f'{ts} WARN: Failed logging to redis: {ex}', flush=True)
def logWarn(self, message): def logWarn(self, message):
self.log('warn', message) self.log('warn', message)
+6 -6
View File
@@ -3,17 +3,17 @@ 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.26 ARG APCU_PECL_VERSION=5.1.28
# 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.1
# 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.9
# 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.3.0 ARG MEMCACHED_PECL_VERSION=3.4.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.2.0 ARG REDIS_PECL_VERSION=6.3.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.9.5
RUN apk add -U --no-cache autoconf \ RUN apk add -U --no-cache autoconf \
aspell-dev \ aspell-dev \
+24 -24
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 ${VALKEY_SLAVEOF_IP} ]]; then if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
VALKEY_HOST=$VALKEY_SLAVEOF_IP REDIS_HOST=$REDIS_SLAVEOF_IP
VALKEY_PORT=$VALKEY_SLAVEOF_PORT REDIS_PORT=$REDIS_SLAVEOF_PORT
else else
VALKEY_HOST="valkey-mailcow" REDIS_HOST="redis"
VALKEY_PORT="6379" REDIS_PORT="6379"
fi fi
VALKEY_CMDLINE="redis-cli -h ${VALKEY_HOST} -p ${VALKEY_PORT} -a ${VALKEYPASS} --no-auth-warning" REDIS_CMDLINE="redis-cli -h ${REDIS_HOST} -p ${REDIS_PORT} -a ${REDISPASS} --no-auth-warning"
until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Valkey..." echo "Waiting for Redis..."
sleep 2 sleep 2
done done
# Set valkey session store # Set redis session store
echo -n ' echo -n '
session.save_handler = redis session.save_handler = redis
session.save_path = "tcp://'${VALKEY_HOST}':'${VALKEY_PORT}'?auth='${VALKEYPASS}'" session.save_path = "tcp://'${REDIS_HOST}':'${REDIS_PORT}'?auth='${REDISPASS}'"
' > /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 $(${VALKEY_CMDLINE} --raw GET Q_RELEASE_FORMAT) ]]; then if [[ -z $(${REDIS_CMDLINE} --raw GET Q_RELEASE_FORMAT) ]]; then
${VALKEY_CMDLINE} --raw SET Q_RELEASE_FORMAT raw ${REDIS_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 $(${VALKEY_CMDLINE} --raw GET Q_MAX_AGE) ]]; then if [[ -z $(${REDIS_CMDLINE} --raw GET Q_MAX_AGE) ]]; then
${VALKEY_CMDLINE} --raw SET Q_MAX_AGE 365 ${REDIS_CMDLINE} --raw SET Q_MAX_AGE 365
fi fi
# Set default password policy - if unset # Set default password policy - if unset
if [[ -z $(${VALKEY_CMDLINE} --raw HGET PASSWD_POLICY length) ]]; then if [[ -z $(${REDIS_CMDLINE} --raw HGET PASSWD_POLICY length) ]]; then
${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY length 6 ${REDIS_CMDLINE} --raw HSET PASSWD_POLICY length 6
${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY chars 0 ${REDIS_CMDLINE} --raw HSET PASSWD_POLICY chars 0
${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY special_chars 0 ${REDIS_CMDLINE} --raw HSET PASSWD_POLICY special_chars 0
${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY lowerupper 0 ${REDIS_CMDLINE} --raw HSET PASSWD_POLICY lowerupper 0
${VALKEY_CMDLINE} --raw HSET PASSWD_POLICY numbers 0 ${REDIS_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 Valkey..." echo "Rebuilding domain map in Redis..."
declare -a DOMAIN_ARR declare -a DOMAIN_ARR
${VALKEY_CMDLINE} DEL DOMAIN_MAP > /dev/null ${REDIS_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
${VALKEY_CMDLINE} HSET DOMAIN_MAP ${domain} 1 > /dev/null ${REDIS_CMDLINE} HSET DOMAIN_MAP ${domain} 1 > /dev/null
done done
fi fi
@@ -167,7 +167,7 @@ DELIMITER //
CREATE EVENT clean_spamalias CREATE EVENT clean_spamalias
ON SCHEDULE EVERY 1 DAY DO ON SCHEDULE EVERY 1 DAY DO
BEGIN BEGIN
DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP(); DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP() AND permanent = 0;
END; END;
// //
DELIMITER ; DELIMITER ;
+2 -2
View File
@@ -4,7 +4,7 @@ WORKDIR /src
ENV CGO_ENABLED=0 \ ENV CGO_ENABLED=0 \
GO111MODULE=on \ GO111MODULE=on \
NOOPT=1 \ NOOPT=1 \
VERSION=1.8.14 VERSION=1.8.22
RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \ RUN git clone --branch v${VERSION} https://github.com/Zuplu/postfix-tlspol && \
cd /src/postfix-tlspol && \ cd /src/postfix-tlspol && \
@@ -34,7 +34,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
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-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf
COPY postfix-tlspol.sh /opt/postfix-tlspol.sh COPY postfix-tlspol.sh /opt/postfix-tlspol.sh
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
COPY docker-entrypoint.sh /docker-entrypoint.sh COPY docker-entrypoint.sh /docker-entrypoint.sh
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
fi fi
exec "$@" exec "$@"
@@ -17,14 +17,14 @@ 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 ${VALKEY_SLAVEOF_IP} ]]; then if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
export VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning" export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
else else
export VALKEY_CMDLINE="redis-cli -h valkey -p 6379 -a ${VALKEYPASS} --no-auth-warning" export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
fi fi
until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Valkey..." echo "Waiting for Redis..."
sleep 2 sleep 2
done done
@@ -15,12 +15,12 @@ source s_src {
internal(); internal();
}; };
destination d_stdout { pipe("/dev/stdout"); }; destination d_stdout { pipe("/dev/stdout"); };
destination d_valkey_ui_log { destination d_redis_ui_log {
redis( redis(
host("`VALKEY_SLAVEOF_IP`") host("`REDIS_SLAVEOF_IP`")
persist-name("valkey1") persist-name("redis1")
port(`VALKEY_SLAVEOF_PORT`) port(`REDIS_SLAVEOF_PORT`)
auth("`VALKEYPASS`") auth("`REDISPASS`")
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")
); );
}; };
@@ -41,5 +41,5 @@ log {
filter(f_ignore); filter(f_ignore);
destination(d_stdout); destination(d_stdout);
filter(f_mail); filter(f_mail);
destination(d_valkey_ui_log); destination(d_redis_ui_log);
}; };
@@ -15,12 +15,12 @@ source s_src {
internal(); internal();
}; };
destination d_stdout { pipe("/dev/stdout"); }; destination d_stdout { pipe("/dev/stdout"); };
destination d_valkey_ui_log { destination d_redis_ui_log {
redis( redis(
host("valkey-mailcow") host("redis-mailcow")
persist-name("valkey1") persist-name("redis1")
port(6379) port(6379)
auth("`VALKEYPASS`") auth("`REDISPASS`")
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")
); );
}; };
@@ -41,5 +41,5 @@ log {
filter(f_ignore); filter(f_ignore);
destination(d_stdout); destination(d_stdout);
filter(f_mail); filter(f_mail);
destination(d_valkey_ui_log); destination(d_redis_ui_log);
}; };
+1 -1
View File
@@ -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-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_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
@@ -8,8 +8,8 @@ for file in /hooks/*; do
fi fi
done done
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf cp /etc/syslog-ng/syslog-ng-redis_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 "$@"
+5 -2
View File
@@ -329,14 +329,17 @@ query = SELECT goto FROM alias
SELECT id FROM alias SELECT id FROM alias
WHERE address='%s' WHERE address='%s'
AND (active='1' OR active='2') AND (active='1' OR active='2')
AND sender_allowed='1'
), ( ), (
SELECT id FROM alias SELECT id FROM alias
WHERE address='@%d' WHERE address='@%d'
AND (active='1' OR active='2') AND (active='1' OR active='2')
AND sender_allowed='1'
) )
) )
) )
AND active='1' AND active='1'
AND sender_allowed='1'
AND (domain IN AND (domain IN
(SELECT domain FROM domain (SELECT domain FROM domain
WHERE domain='%d' WHERE domain='%d'
@@ -390,7 +393,7 @@ hosts = unix:/var/run/mysqld/mysqld.sock
dbname = ${DBNAME} dbname = ${DBNAME}
query = SELECT goto FROM spamalias query = SELECT goto FROM spamalias
WHERE address='%s' WHERE address='%s'
AND validity >= UNIX_TIMESTAMP() AND (validity >= UNIX_TIMESTAMP() OR permanent != 0)
EOF EOF
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
@@ -524,4 +527,4 @@ if [[ $? != 0 ]]; then
else else
postfix -c /opt/postfix/conf start postfix -c /opt/postfix/conf start
sleep 126144000 sleep 126144000
fi fi
@@ -15,21 +15,21 @@ source s_src {
internal(); internal();
}; };
destination d_stdout { pipe("/dev/stdout"); }; destination d_stdout { pipe("/dev/stdout"); };
destination d_valkey_ui_log { destination d_redis_ui_log {
redis( redis(
host("`VALKEY_SLAVEOF_IP`") host("`REDIS_SLAVEOF_IP`")
persist-name("valkey1") persist-name("redis1")
port(`VALKEY_SLAVEOF_PORT`) port(`REDIS_SLAVEOF_PORT`)
auth("`VALKEYPASS`") auth("`REDISPASS`")
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_valkey_f2b_channel { destination d_redis_f2b_channel {
redis( redis(
host("`VALKEY_SLAVEOF_IP`") host("`REDIS_SLAVEOF_IP`")
persist-name("valkey2") persist-name("redis2")
port(`VALKEY_SLAVEOF_PORT`) port(`REDIS_SLAVEOF_PORT`)
auth("`VALKEYPASS`") auth("`REDISPASS`")
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_valkey_ui_log); destination(d_redis_ui_log);
destination(d_valkey_f2b_channel); destination(d_redis_f2b_channel);
}; };
+10 -10
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_valkey_ui_log { destination d_redis_ui_log {
redis( redis(
host("valkey-mailcow") host("redis-mailcow")
persist-name("valkey1") persist-name("redis1")
port(6379) port(6379)
auth("`VALKEYPASS`") auth("`REDISPASS`")
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_valkey_f2b_channel { destination d_redis_f2b_channel {
redis( redis(
host("valkey-mailcow") host("redis-mailcow")
persist-name("valkey2") persist-name("redis2")
port(6379) port(6379)
auth("`VALKEYPASS`") auth("`REDISPASS`")
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_valkey_ui_log); destination(d_redis_ui_log);
destination(d_valkey_f2b_channel); destination(d_redis_f2b_channel);
}; };
+5 -5
View File
@@ -1,9 +1,9 @@
FROM debian:bookworm-slim FROM debian:trixie-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.12.1-1~6dbfca2fa ARG RSPAMD_VER=rspamd_3.14.3-1~236eb65
ARG CODENAME=bookworm ARG CODENAME=trixie
ENV LC_ALL=C ENV LC_ALL=C
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -14,8 +14,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
dnsutils \ dnsutils \
netcat-traditional \ netcat-traditional \
wget \ wget \
redis-tools \ redis-tools \
procps \ procps \
nano \ nano \
lua-cjson \ lua-cjson \
&& arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \ && arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
+14 -14
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 ${VALKEY_SLAVEOF_IP} ]]; then if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
cat <<EOF > /etc/rspamd/local.d/redis.conf cat <<EOF > /etc/rspamd/local.d/redis.conf
read_servers = "valkey-mailcow:6379"; read_servers = "redis:6379";
write_servers = "${VALKEY_SLAVEOF_IP}:${VALKEY_SLAVEOF_PORT}"; write_servers = "${REDIS_SLAVEOF_IP}:${REDIS_SLAVEOF_PORT}";
password = "${VALKEYPASS}"; password = "${REDISPASS}";
timeout = 10; timeout = 10;
EOF EOF
until [[ $(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning PING) == "PONG" ]]; do until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Valkey @valkey-mailcow..." echo "Waiting for Redis @redis-mailcow..."
sleep 2 sleep 2
done done
until [[ $(redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning PING) == "PONG" ]]; do until [[ $(redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Valkey @${VALKEY_SLAVEOF_IP}..." echo "Waiting for Redis @${REDIS_SLAVEOF_IP}..."
sleep 2 sleep 2
done done
redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning SLAVEOF ${VALKEY_SLAVEOF_IP} ${VALKEY_SLAVEOF_PORT} redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF ${REDIS_SLAVEOF_IP} ${REDIS_SLAVEOF_PORT}
else else
cat <<EOF > /etc/rspamd/local.d/redis.conf cat <<EOF > /etc/rspamd/local.d/redis.conf
servers = "valkey-mailcow:6379"; servers = "redis:6379";
password = "${VALKEYPASS}"; password = "${REDISPASS}";
timeout = 10; timeout = 10;
EOF EOF
until [[ $(redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning PING) == "PONG" ]]; do until [[ $(redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning PING) == "PONG" ]]; do
echo "Waiting for Valkey slave..." echo "Waiting for Redis slave..."
sleep 2 sleep 2
done done
redis-cli -h valkey-mailcow -a ${VALKEYPASS} --no-auth-warning SLAVEOF NO ONE redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF NO ONE
fi fi
if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
+146 -32
View File
@@ -1,50 +1,164 @@
FROM debian:bookworm-slim # SOGo built from source to enable security patch application
# Repository: https://github.com/Alinto/sogo
# Version: SOGo-5.12.4
#
# Applied security patches:
# -
#
# To add new patches, modify SOGO_SECURITY_PATCHES ARG below with space-separated commit hashes
FROM debian:bookworm
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 DEBIAN_VERSION=bookworm ARG SOGO_VERSION=SOGo-5.12.5
ARG SOGO_DEBIAN_REPOSITORY=https://packagingv2.sogo.nu/sogo-nightly-debian/ ARG SOPE_VERSION=SOPE-5.12.5
# Security patches to apply (space-separated commit hashes)
ARG SOGO_SECURITY_PATCHES=""
# 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.17 ARG GOSU_VERSION=1.19
ENV LC_ALL=C ENV LC_ALL=C
# Prerequisites # Install dependencies, build SOPE and SOGo, then clean up (all in one layer to minimize image size)
RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \ RUN apt-get update && apt-get install -y --no-install-recommends \
&& apt-get update && apt-get install -y --no-install-recommends \ # Build dependencies
apt-transport-https \ git \
ca-certificates \ build-essential \
gettext \ gobjc \
gnupg \ gnustep-make \
mariadb-client \ gnustep-base-runtime \
rsync \ libgnustep-base-dev \
supervisor \ libxml2-dev \
syslog-ng \ libldap2-dev \
syslog-ng-core \ libssl-dev \
syslog-ng-mod-redis \ zlib1g-dev \
dirmngr \ libpq-dev \
netcat-traditional \ libmariadb-dev-compat \
psmisc \ libmemcached-dev \
wget \ libsodium-dev \
patch \ libcurl4-openssl-dev \
libzip-dev \
libytnef0-dev \
curl \
ca-certificates \
# Runtime dependencies
apt-transport-https \
gettext \
gnupg \
mariadb-client \
rsync \
supervisor \
syslog-ng \
syslog-ng-core \
syslog-ng-mod-redis \
dirmngr \
netcat-traditional \
psmisc \
wget \
patch \
libobjc4 \
libxml2 \
libldap-2.5-0 \
libssl3 \
zlib1g \
libmariadb3 \
libmemcached11 \
libsodium23 \
libcurl4 \
libzip4 \
libytnef0 \
# Download gosu
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \ && dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \ && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
&& chmod +x /usr/local/bin/gosu \ && chmod +x /usr/local/bin/gosu \
&& gosu nobody true \ && gosu nobody true \
&& mkdir /usr/share/doc/sogo \ # Build SOPE
&& touch /usr/share/doc/sogo/empty.sh \ && git clone --depth 1 --branch ${SOPE_VERSION} https://github.com/Alinto/sope.git /tmp/sope \
&& wget -O- https://keys.openpgp.org/vks/v1/by-fingerprint/74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 | gpg --dearmor | apt-key add - \ && cd /tmp/sope \
&& echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} main" > /etc/apt/sources.list.d/sogo.list \ && rm -rf .git \
&& apt-get update && apt-get install -y --no-install-recommends \ && . /usr/share/GNUstep/Makefiles/GNUstep.sh \
sogo \ && ./configure --prefix=/usr --disable-debug --disable-strip \
sogo-activesync \ && make -j$(nproc) \
&& apt-get autoclean \ && make install \
&& cd / \
&& rm -rf /tmp/sope \
# Build SOGo with security patches
&& git clone --depth 1 --branch ${SOGO_VERSION} https://github.com/Alinto/sogo.git /tmp/sogo \
&& cd /tmp/sogo \
&& git config user.email "builder@mailcow.local" \
&& git config user.name "SOGo Builder" \
&& for patch in ${SOGO_SECURITY_PATCHES}; do \
echo "Applying security patch: ${patch}"; \
git fetch origin ${patch} && git cherry-pick ${patch}; \
done \
&& rm -rf .git \
&& . /usr/share/GNUstep/Makefiles/GNUstep.sh \
&& ./configure --disable-debug --disable-strip \
&& make -j$(nproc) \
&& make install \
&& cd / \
&& rm -rf /tmp/sogo \
# Strip binaries
&& strip --strip-unneeded /usr/local/sbin/sogod 2>/dev/null || true \
&& strip --strip-unneeded /usr/local/sbin/sogo-tool 2>/dev/null || true \
&& strip --strip-unneeded /usr/local/sbin/sogo-ealarms-notify 2>/dev/null || true \
&& strip --strip-unneeded /usr/local/sbin/sogo-slapd-sockd 2>/dev/null || true \
# Remove build dependencies and clean up
&& apt-get purge -y --auto-remove \
git \
build-essential \
gobjc \
gnustep-make \
libgnustep-base-dev \
libxml2-dev \
libldap2-dev \
libssl-dev \
zlib1g-dev \
libpq-dev \
libmariadb-dev-compat \
libmemcached-dev \
libsodium-dev \
libcurl4-openssl-dev \
libzip-dev \
libytnef0-dev \
curl \
&& apt-get autoremove -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& rm -rf /usr/share/doc/* \
&& rm -rf /usr/share/man/* \
&& rm -rf /var/cache/debconf/* \
&& rm -rf /tmp/* \
&& rm -rf /root/.cache \
&& find /usr/local/lib -name '*.a' -delete \
&& find /usr/lib -name '*.a' -delete \
&& mkdir -p /usr/share/doc/sogo \
&& touch /usr/share/doc/sogo/empty.sh \
&& touch /etc/default/locale && touch /etc/default/locale
# Configure library paths
RUN echo "/usr/lib64" > /etc/ld.so.conf.d/sogo.conf \
&& echo "/usr/local/lib/sogo" >> /etc/ld.so.conf.d/sogo.conf \
&& echo "/usr/local/lib/GNUstep/Frameworks/SOGo.framework/Versions/5/sogo" >> /etc/ld.so.conf.d/sogo.conf \
&& ldconfig
# Create sogo user and group
RUN groupadd -r -g 999 sogo \
&& useradd -r -u 999 -g sogo -d /var/lib/sogo -s /bin/bash -c "SOGo Daemon" sogo \
&& mkdir -p /var/lib/sogo /var/run/sogo /var/log/sogo \
&& chown -R sogo:sogo /var/lib/sogo /var/run/sogo /var/log/sogo
# Create symlinks for SOGo binaries
RUN ln -s /usr/local/sbin/sogod /usr/sbin/sogod \
&& ln -s /usr/local/sbin/sogo-tool /usr/sbin/sogo-tool \
&& ln -s /usr/local/sbin/sogo-ealarms-notify /usr/sbin/sogo-ealarms-notify \
&& ln -s /usr/local/sbin/sogo-slapd-sockd /usr/sbin/sogo-slapd-sockd
# Copy configuration files and scripts
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-valkey_slave.conf /etc/syslog-ng/syslog-ng-valkey_slave.conf COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_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
@@ -56,4 +170,4 @@ RUN chmod +x /bootstrap-sogo.sh \
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
+2 -2
View File
@@ -1,5 +1,5 @@
--- /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200 --- /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200
+++ /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200 +++ /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200
@@ -46,7 +46,7 @@ @@ -46,7 +46,7 @@
</md-item-template> </md-item-template>
</md-autocomplete> </md-autocomplete>
+11 -11
View File
@@ -50,10 +50,6 @@ 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>
@@ -134,18 +130,22 @@ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist
# Patch ACLs # Patch ACLs
#if [[ ${ACL_ANYONE} == 'allow' ]]; then #if [[ ${ACL_ANYONE} == 'allow' ]]; then
# #enable any or authenticated targets for ACL # #enable any or authenticated targets for ACL
# if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then # if patch -R -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
# patch -R /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; # patch -R /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
# fi # fi
#else #else
# #disable any or authenticated targets for ACL # #disable any or authenticated targets for ACL
# if patch -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then # if patch -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then
# patch /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; # patch /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff;
# fi # fi
#fi #fi
if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then # Apply custom UI patch (reverse patch to ADD buttons)
patch -R /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff; if patch -R -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then
echo "Applying navMailcowBtns patch (reverse to add buttons)..."
patch -R /usr/local/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff;
else
echo "navMailcowBtns patch already applied or cannot be applied"
fi fi
# Rename custom logo, if any # Rename custom logo, if any
@@ -153,7 +153,7 @@ fi
# Rsync web content # Rsync web content
echo "Syncing web content with named volume" echo "Syncing web content with named volume"
rsync -a /usr/lib/GNUstep/SOGo/. /sogo_web/ rsync -a /usr/local/lib/GNUstep/SOGo/. /sogo_web/
# Chown backup path # Chown backup path
chown -R sogo:sogo /sogo_backup chown -R sogo:sogo /sogo_backup
+2 -2
View File
@@ -6,8 +6,8 @@ if [[ "${SKIP_SOGO}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
exit 0 exit 0
fi fi
if [[ ! -z ${VALKEY_SLAVEOF_IP} ]]; then if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-valkey_slave.conf /etc/syslog-ng/syslog-ng.conf cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
fi fi
echo "$TZ" > /etc/timezone echo "$TZ" > /etc/timezone
@@ -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_valkey_ui_log { destination d_redis_ui_log {
redis( redis(
host("`VALKEY_SLAVEOF_IP`") host("`REDIS_SLAVEOF_IP`")
persist-name("valkey1") persist-name("redis1")
port(`VALKEY_SLAVEOF_PORT`) port(`REDIS_SLAVEOF_PORT`)
auth("`VALKEYPASS`") auth("`REDISPASS`")
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_valkey_f2b_channel { destination d_redis_f2b_channel {
redis( redis(
host("`VALKEY_SLAVEOF_IP`") host("`REDIS_SLAVEOF_IP`")
persist-name("valkey2") persist-name("redis2")
port(`VALKEY_SLAVEOF_PORT`) port(`REDIS_SLAVEOF_PORT`)
auth("`VALKEYPASS`") auth("`REDISPASS`")
command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
); );
}; };
log { log {
source(s_sogo); source(s_sogo);
destination(d_valkey_ui_log); destination(d_redis_ui_log);
destination(d_valkey_f2b_channel); destination(d_redis_f2b_channel);
}; };
log { log {
source(s_sogo); source(s_sogo);
+10 -10
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_valkey_ui_log { destination d_redis_ui_log {
redis( redis(
host("valkey-mailcow") host("redis-mailcow")
persist-name("valkey1") persist-name("redis1")
port(6379) port(6379)
auth("`VALKEYPASS`") auth("`REDISPASS`")
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_valkey_f2b_channel { destination d_redis_f2b_channel {
redis( redis(
host("valkey-mailcow") host("redis-mailcow")
persist-name("valkey2") persist-name("redis2")
port(6379) port(6379)
auth("`VALKEYPASS`") auth("`REDISPASS`")
command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)") command("PUBLISH" "F2B_CHANNEL" "$(sanitize $MESSAGE)")
); );
}; };
log { log {
source(s_sogo); source(s_sogo);
destination(d_valkey_ui_log); destination(d_redis_ui_log);
destination(d_valkey_f2b_channel); destination(d_redis_f2b_channel);
}; };
log { log {
source(s_sogo); source(s_sogo);
+1 -1
View File
@@ -1,4 +1,4 @@
FROM alpine:3.21 FROM alpine:3.23
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
@@ -1,8 +0,0 @@
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"]
@@ -1,78 +0,0 @@
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
+1 -1
View File
@@ -1,4 +1,4 @@
FROM alpine:3.21 FROM alpine:3.23
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
+4 -4
View File
@@ -19,19 +19,19 @@ if [ -z "$HOST" ]; then
fi fi
# run dig and measure the time it takes to run # run dig and measure the time it takes to run
START_TIME=$(date +%s%3N) START_TIME=$(perl -MTime::HiRes -e 'print Time::HiRes::time')
dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null) dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null)
dig_rc=$? dig_rc=$?
END_TIME=$(perl -MTime::HiRes -e 'print Time::HiRes::time')
dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -) dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -)
END_TIME=$(date +%s%3N) ELAPSED_TIME=$(perl -e "printf('%.3f', $END_TIME - $START_TIME)")
ELAPSED_TIME=$((END_TIME - START_TIME))
# validate and perform nagios like output and exit codes # validate and perform nagios like output and exit codes
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
echo "Domain $HOST was not found by the server" echo "Domain $HOST was not found by the server"
exit 2 exit 2
elif [ $dig_rc -eq 0 ]; then elif [ $dig_rc -eq 0 ]; then
echo "DNS OK: $ELAPSED_TIME ms response time. $HOST returns $dig_output_ips" echo "DNS OK: $ELAPSED_TIME seconds response time. $HOST returns $dig_output_ips"
exit 0 exit 0
else else
echo "Unknown error" echo "Unknown error"
+37 -37
View File
@@ -44,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 ${VALKEY_SLAVEOF_IP} ]]; then if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
VALKEY_CMDLINE="redis-cli -h ${VALKEY_SLAVEOF_IP} -p ${VALKEY_SLAVEOF_PORT} -a ${VALKEYPASS} --no-auth-warning" REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning"
else else
VALKEY_CMDLINE="redis-cli -h valkey-mailcow -p 6379 -a ${VALKEYPASS} --no-auth-warning" REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning"
fi fi
until [[ $(${VALKEY_CMDLINE} PING) == "PONG" ]]; do until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Valkey..." echo "Waiting for Redis..."
sleep 2 sleep 2
done done
${VALKEY_CMDLINE} DEL F2B_RES > /dev/null ${REDIS_CMDLINE} DEL F2B_RES > /dev/null
# Common functions # Common functions
get_ipv6(){ get_ipv6(){
@@ -90,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} ))
${VALKEY_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"service\":\"${SERVICE}\",\"lvl\":\"${PERCENT}\",\"hpnow\":\"${CURRENT}\",\"hptotal\":\"${TOTAL}\",\"hpdiff\":\"${DIFF}\"}" > /dev/null ${REDIS_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_valkey log_msg "${SERVICE} health level: ${PERCENT}% (${CURRENT}/${TOTAL}), health trend: ${DIFF}" no_redis
# 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_valkey" ]]; then if [[ ${2} != "no_redis" ]]; then
${VALKEY_CMDLINE} LPUSH WATCHDOG_LOG "{\"time\":\"$(date +%s)\",\"message\":\"$(printf '%s' "${1}" | \ ${REDIS_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}")
@@ -114,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="$(${VALKEY_CMDLINE} TTL THROTTLE_${1} 2> /dev/null)" TTL_LEFT="$(${REDIS_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
${VALKEY_CMDLINE} SET THROTTLE_${1} 1 EX ${THROTTLE} ${REDIS_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
@@ -324,21 +324,21 @@ unbound_checks() {
return 1 return 1
} }
valkey_checks() { redis_checks() {
# A check for the local valkey container # A check for the local redis container
err_count=0 err_count=0
diff_c=0 diff_c=0
THRESHOLD=${VALKEY_THRESHOLD} THRESHOLD=${REDIS_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/valkey-mailcow; echo "$(tail -50 /tmp/valkey-mailcow)" > /tmp/valkey-mailcow touch /tmp/redis-mailcow; echo "$(tail -50 /tmp/redis-mailcow)" > /tmp/redis-mailcow
host_ip=$(get_container_ip valkey-mailcow) host_ip=$(get_container_ip redis-mailcow)
err_c_cur=${err_count} err_c_cur=${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} + $? )) /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} + $? ))
[ ${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 "Valkey" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} progress "Redis" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c}
if [[ $? == 10 ]]; then if [[ $? == 10 ]]; then
diff_c=0 diff_c=0
sleep 1 sleep 1
@@ -533,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 valkey-mailcow -a ${VALKEYPASS} --no-auth-warning -r GET DOVECOT_REPL_HEALTH) D_REPL_STATUS=$(redis-cli -h redis -a ${REDISPASS} --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 valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET DOVECOT_REPL_HEALTH) D_REPL_STATUS=$(redis-cli --raw -h redis -a ${REDISPASS} --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
@@ -608,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 valkey-mailcow -a ${VALKEYPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid) RL_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --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 valkey-mailcow -a ${VALKEYPASS} --no-auth-warning LRANGE RL_LOG 0 0 | jq .qid) RL_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --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 valkey-mailcow -a ${VALKEYPASS} --no-auth-warning LRANGE RL_LOG 0 10 | jq . >> /tmp/ratelimit redis-cli --raw -h redis -a ${REDISPASS} --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} ))
@@ -669,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=($(${VALKEY_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS)) F2B_LOG_STATUS=($(${REDIS_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=($(${VALKEY_CMDLINE} --raw HKEYS F2B_ACTIVE_BANS)) F2B_LOG_STATUS=($(${REDIS_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 ${VALKEY_CMDLINE} -x SET F2B_RES > /dev/null echo -n "${F2B_RES[@]}" | tr -cd "[a-fA-F0-9.:/] " | timeout 3s ${REDIS_CMDLINE} -x SET F2B_RES > /dev/null
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
${VALKEY_CMDLINE} -x DEL F2B_RES ${REDIS_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
@@ -703,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 valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET ACME_FAIL_TIME) ACME_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --no-auth-warning GET ACME_FAIL_TIME)
if [[ -z "${ACME_LOG_STATUS}" ]]; then if [[ -z "${ACME_LOG_STATUS}" ]]; then
${VALKEY_CMDLINE} SET ACME_FAIL_TIME 0 ${REDIS_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
@@ -715,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 valkey-mailcow -a ${VALKEYPASS} --no-auth-warning GET ACME_FAIL_TIME 2> /dev/null) ACME_LOG_STATUS=$(redis-cli -h redis -a ${REDISPASS} --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
@@ -864,14 +864,14 @@ BACKGROUND_TASKS+=(${PID})
( (
while true; do while true; do
if ! valkey_checks; then if ! redis_checks; then
log_msg "Local Valkey hit error limit" log_msg "Local Redis hit error limit"
echo valkey-mailcow > /tmp/com_pipe echo redis-mailcow > /tmp/com_pipe
fi fi
done done
) & ) &
PID=$! PID=$!
echo "Spawned valkey_checks with PID ${PID}" echo "Spawned redis_checks with PID ${PID}"
BACKGROUND_TASKS+=(${PID}) BACKGROUND_TASKS+=(${PID})
( (
@@ -1129,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 ${VALKEY_CMDLINE} --raw GET F2B_RES 2> /dev/null)) F2B_RES=($(timeout 4s ${REDIS_CMDLINE} --raw GET F2B_RES 2> /dev/null))
if [[ ! -z "${F2B_RES}" ]]; then if [[ ! -z "${F2B_RES}" ]]; then
${VALKEY_CMDLINE} DEL F2B_RES > /dev/null ${REDIS_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}"
+17 -10
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 Valkey // Init Redis
$valkey = new Redis(); $redis = new Redis();
try { try {
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) { if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT')); $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
} }
else { else {
$valkey->connect('valkey-mailcow', 6379); $redis->connect('redis-mailcow', 6379);
} }
$valkey->auth(getenv("VALKEYPASS")); $redis->auth(getenv("REDISPASS"));
} }
catch (Exception $e) { catch (Exception $e) {
error_log("MAILCOWAUTH: " . $e . PHP_EOL); error_log("MAILCOWAUTH: " . $e . PHP_EOL);
@@ -80,14 +80,21 @@ if ($isSOGoRequest) {
} }
if ($result === false){ if ($result === false){
// If it's a SOGo Request, don't check for protocol access // If it's a SOGo Request, don't check for protocol access
$service = ($isSOGoRequest) ? false : array($post['service'] => true); if ($isSOGoRequest) {
$result = apppass_login($post['username'], $post['password'], $service, array( $service = 'SOGO';
$post['service'] = 'NONE';
} else {
$service = $post['service'];
}
$result = apppass_login($post['username'], $post['password'], array(
'service' => $post['service'],
'is_internal' => true, 'is_internal' => true,
'remote_addr' => $post['real_rip'] 'remote_addr' => $post['real_rip']
)); ));
if ($result) { if ($result) {
error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']); error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $service . " from IP " . $post['real_rip']);
set_sasl_log($post['username'], $post['real_rip'], $post['service']); set_sasl_log($post['username'], $post['real_rip'], $service);
} }
} }
if ($result === false){ if ($result === false){
+1
View File
@@ -13,6 +13,7 @@ events {
http { http {
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
default_type application/octet-stream; default_type application/octet-stream;
server_tokens off;
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" ' '$status $body_bytes_sent "$http_referer" '
@@ -14,7 +14,6 @@ ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=15768000;"; add_header Strict-Transport-Security "max-age=15768000;";
add_header X-Content-Type-Options nosniff; add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none; add_header X-Robots-Tag none;
add_header X-Download-Options noopen; add_header X-Download-Options noopen;
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
@@ -262,19 +261,19 @@ location ~* /sogo$ {
} }
location /SOGo.woa/WebServerResources/ { location /SOGo.woa/WebServerResources/ {
alias /usr/lib/GNUstep/SOGo/WebServerResources/; alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
} }
location /.woa/WebServerResources/ { location /.woa/WebServerResources/ {
alias /usr/lib/GNUstep/SOGo/WebServerResources/; alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
} }
location /SOGo/WebServerResources/ { location /SOGo/WebServerResources/ {
alias /usr/lib/GNUstep/SOGo/WebServerResources/; alias /usr/local/lib/GNUstep/SOGo/WebServerResources/;
} }
location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) { location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) {
alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2; alias /usr/local/lib/GNUstep/SOGo/$1.SOGo/Resources/$2;
} }
{% endif %} {% endif %}
+8 -8
View File
@@ -23,16 +23,16 @@ catch (PDOException $e) {
exit; exit;
} }
// Init Valkey // Init Redis
$valkey = new Redis(); $redis = new Redis();
try { try {
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) { if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT')); $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
} }
else { else {
$valkey->connect('valkey-mailcow', 6379); $redis->connect('redis-mailcow', 6379);
} }
$valkey->auth(getenv("VALKEYPASS")); $redis->auth(getenv("REDISPASS"));
} }
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 $valkey; global $redis;
$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
); );
$valkey->lPush('CRON_LOG', json_encode($finalMsg)); $redis->lPush('CRON_LOG', json_encode($finalMsg));
} }
// Load core functions first // Load core functions first
+8 -8
View File
@@ -23,16 +23,16 @@ catch (PDOException $e) {
exit; exit;
} }
// Init Valkey // Init Redis
$valkey = new Redis(); $redis = new Redis();
try { try {
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) { if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT')); $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
} }
else { else {
$valkey->connect('valkey-mailcow', 6379); $redis->connect('redis-mailcow', 6379);
} }
$valkey->auth(getenv("VALKEYPASS")); $redis->auth(getenv("REDISPASS"));
} }
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 $valkey; global $redis;
$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
); );
$valkey->lPush('CRON_LOG', json_encode($finalMsg)); $redis->lPush('CRON_LOG', json_encode($finalMsg));
} }
// Load core functions first // Load core functions first
@@ -1,7 +1,16 @@
; NOTE: Restart phpfpm on ANY manual changes to PHP files!
; opcache
opcache.enable=1 opcache.enable=1
opcache.enable_cli=1 opcache.enable_cli=1
opcache.interned_strings_buffer=16 opcache.interned_strings_buffer=16
opcache.max_accelerated_files=10000 opcache.max_accelerated_files=10000
opcache.memory_consumption=128 opcache.memory_consumption=128
opcache.save_comments=1 opcache.save_comments=1
opcache.revalidate_freq=1 opcache.validate_timestamps=0
; JIT
; Disabled for now due to some PHP segmentation faults observed
; in certain environments. Possibly some PHP or PHP extension bug.
opcache.jit=disable
opcache.jit_buffer_size=0
+44 -86
View File
@@ -1,6 +1,6 @@
# Whitelist generated by Postwhite v3.4 on Wed Oct 1 00:21:33 UTC 2025 # Whitelist generated by Postwhite v3.4 on Sun Mar 1 00:29:01 UTC 2026
# https://github.com/stevejenkins/postwhite/ # https://github.com/stevejenkins/postwhite/
# 2216 total rules # 2174 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:2800::/53 permit 2a01:111:f403:2800::/53 permit
@@ -29,7 +29,9 @@
2a01:b747:3005:200::/56 permit 2a01:b747:3005:200::/56 permit
2a01:b747:3006:200::/56 permit 2a01:b747:3006:200::/56 permit
2a02:a60:0:5::/64 permit 2a02:a60:0:5::/64 permit
2a0f:f640::/56 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.64.237.68 permit
3.65.3.180 permit 3.65.3.180 permit
@@ -56,8 +58,9 @@
8.40.222.0/23 permit 8.40.222.0/23 permit
8.40.222.250/31 permit 8.40.222.250/31 permit
12.130.86.238 permit 12.130.86.238 permit
13.107.213.41 permit 13.107.213.51 permit
13.107.246.41 permit 13.107.246.51 permit
13.108.16.0/20 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
@@ -66,6 +69,7 @@
13.111.191.0/24 permit 13.111.191.0/24 permit
13.216.7.111 permit 13.216.7.111 permit
13.216.54.180 permit 13.216.54.180 permit
13.247.164.219 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
@@ -299,15 +303,6 @@
52.94.124.0/28 permit 52.94.124.0/28 permit
52.95.48.152/29 permit 52.95.48.152/29 permit
52.95.49.88/29 permit 52.95.49.88/29 permit
52.96.91.34 permit
52.96.111.82 permit
52.96.172.98 permit
52.96.214.50 permit
52.96.222.194 permit
52.96.222.226 permit
52.96.223.2 permit
52.96.228.130 permit
52.96.229.242 permit
52.100.0.0/15 permit 52.100.0.0/15 permit
52.102.0.0/16 permit 52.102.0.0/16 permit
52.103.0.0/17 permit 52.103.0.0/17 permit
@@ -402,27 +397,8 @@
64.207.219.143 permit 64.207.219.143 permit
64.233.160.0/19 permit 64.233.160.0/19 permit
65.52.80.137 permit 65.52.80.137 permit
65.54.51.64/26 permit
65.54.61.64/26 permit
65.54.121.120/29 permit
65.54.190.0/24 permit
65.54.241.0/24 permit
65.55.29.77 permit 65.55.29.77 permit
65.55.33.64/28 permit
65.55.34.0/24 permit
65.55.42.224/28 permit 65.55.42.224/28 permit
65.55.52.224/27 permit
65.55.78.128/25 permit
65.55.81.48/28 permit
65.55.90.0/24 permit
65.55.94.0/25 permit
65.55.111.0/24 permit
65.55.113.64/26 permit
65.55.116.0/25 permit
65.55.126.0/25 permit
65.55.174.0/25 permit
65.55.178.128/27 permit
65.55.234.192/26 permit
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
@@ -543,7 +519,6 @@
69.169.224.0/20 permit 69.169.224.0/20 permit
69.171.232.0/24 permit 69.171.232.0/24 permit
69.171.244.0/23 permit 69.171.244.0/23 permit
70.37.151.128/25 permit
70.42.149.35 permit 70.42.149.35 permit
72.3.185.0/24 permit 72.3.185.0/24 permit
72.14.192.0/18 permit 72.14.192.0/18 permit
@@ -640,7 +615,6 @@
74.208.4.220 permit 74.208.4.220 permit
74.208.4.221 permit 74.208.4.221 permit
74.209.250.0/24 permit 74.209.250.0/24 permit
75.2.70.75 permit
76.223.128.0/19 permit 76.223.128.0/19 permit
76.223.176.0/20 permit 76.223.176.0/20 permit
77.238.176.0/24 permit 77.238.176.0/24 permit
@@ -733,11 +707,11 @@
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.198.2.177 permit
91.198.2.217 permit
91.198.2.222 permit
91.211.240.0/22 permit 91.211.240.0/22 permit
94.236.119.0/26 permit 94.236.119.0/26 permit
94.245.112.0/27 permit
94.245.112.10/31 permit
95.131.104.0/21 permit 95.131.104.0/21 permit
95.217.114.154 permit 95.217.114.154 permit
96.43.144.0/20 permit 96.43.144.0/20 permit
@@ -1230,9 +1204,9 @@
98.139.245.208/30 permit 98.139.245.208/30 permit
98.139.245.212/31 permit 98.139.245.212/31 permit
99.78.197.208/28 permit 99.78.197.208/28 permit
99.83.190.102 permit
103.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.15 permit
103.84.217.238 permit 103.84.217.238 permit
103.89.75.238 permit 103.89.75.238 permit
103.151.192.0/23 permit 103.151.192.0/23 permit
@@ -1378,11 +1352,6 @@
108.179.144.0/20 permit 108.179.144.0/20 permit
109.224.244.0/24 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.26.0/27 permit
111.221.66.0/25 permit
111.221.69.128/25 permit
111.221.112.0/21 permit
112.19.199.64/29 permit 112.19.199.64/29 permit
112.19.242.64/29 permit 112.19.242.64/29 permit
116.214.12.47 permit 116.214.12.47 permit
@@ -1430,6 +1399,7 @@
128.245.248.0/21 permit 128.245.248.0/21 permit
129.41.77.70 permit 129.41.77.70 permit
129.41.169.249 permit 129.41.169.249 permit
129.77.16.0/20 permit
129.80.5.164 permit 129.80.5.164 permit
129.80.64.36 permit 129.80.64.36 permit
129.80.67.121 permit 129.80.67.121 permit
@@ -1446,6 +1416,7 @@
129.153.194.228 permit 129.153.194.228 permit
129.154.255.129 permit 129.154.255.129 permit
129.158.56.255 permit 129.158.56.255 permit
129.158.62.153 permit
129.159.22.159 permit 129.159.22.159 permit
129.159.87.137 permit 129.159.87.137 permit
129.213.195.191 permit 129.213.195.191 permit
@@ -1481,6 +1452,7 @@
136.143.184.0/24 permit 136.143.184.0/24 permit
136.143.188.0/24 permit 136.143.188.0/24 permit
136.143.190.0/23 permit 136.143.190.0/23 permit
136.146.128.0/20 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
@@ -1498,6 +1470,8 @@
139.167.79.86 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.55.217 permit
141.148.91.244 permit
141.148.159.229 permit 141.148.159.229 permit
141.193.32.0/23 permit 141.193.32.0/23 permit
141.193.184.32/27 permit 141.193.184.32/27 permit
@@ -1543,6 +1517,7 @@
149.72.234.184 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.136.21.199 permit
150.230.98.160 permit 150.230.98.160 permit
151.145.38.14 permit 151.145.38.14 permit
152.67.105.195 permit 152.67.105.195 permit
@@ -1552,20 +1527,7 @@
155.248.220.138 permit 155.248.220.138 permit
155.248.234.149 permit 155.248.234.149 permit
155.248.237.141 permit 155.248.237.141 permit
157.55.0.192/26 permit
157.55.1.128/26 permit
157.55.2.0/25 permit
157.55.9.128/25 permit
157.55.11.0/25 permit
157.55.49.0/25 permit
157.55.61.0/24 permit
157.55.157.128/25 permit
157.55.225.0/25 permit
157.56.24.0/25 permit
157.56.120.128/26 permit 157.56.120.128/26 permit
157.56.232.0/21 permit
157.56.240.0/20 permit
157.56.248.0/21 permit
157.58.30.128/25 permit 157.58.30.128/25 permit
157.58.196.96/29 permit 157.58.196.96/29 permit
157.58.249.3 permit 157.58.249.3 permit
@@ -1594,8 +1556,10 @@
159.135.224.0/20 permit 159.135.224.0/20 permit
159.135.228.10 permit 159.135.228.10 permit
159.183.0.0/16 permit 159.183.0.0/16 permit
159.183.14.233 permit
159.183.68.71 permit 159.183.68.71 permit
159.183.79.38 permit 159.183.79.38 permit
159.183.121.182 permit
159.183.129.172 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
@@ -1615,10 +1579,14 @@
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 163.192.116.87 permit
163.192.125.176 permit
163.192.196.146 permit
163.192.204.161 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.128.0/24 permit
165.173.180.1 permit
165.173.180.250/31 permit 165.173.180.250/31 permit
165.173.182.250/31 permit 165.173.182.250/31 permit
166.78.68.0/22 permit 166.78.68.0/22 permit
@@ -1659,16 +1627,15 @@
169.148.144.10 permit 169.148.144.10 permit
169.148.146.0/23 permit 169.148.146.0/23 permit
169.148.175.3 permit 169.148.175.3 permit
169.148.179.3 permit
169.148.188.0/24 permit 169.148.188.0/24 permit
169.148.188.182 permit 169.148.188.182 permit
170.9.232.254 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.32.0/20 permit
172.253.56.0/21 permit
172.253.112.0/20 permit
173.0.84.0/29 permit 173.0.84.0/29 permit
173.0.84.224/27 permit 173.0.84.224/27 permit
173.0.94.244/30 permit 173.0.94.244/30 permit
@@ -1696,8 +1663,7 @@
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.144 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
@@ -1711,8 +1677,16 @@
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.188.68 permit
185.233.190.0/23 permit 185.233.188.75 permit
185.233.188.84 permit
185.233.188.160 permit
185.233.188.176 permit
185.233.188.247 permit
185.233.189.44 permit
185.233.189.98 permit
185.233.189.122 permit
185.233.189.228 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
@@ -1788,7 +1762,9 @@
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.15 permit
193.142.157.125 permit
193.142.157.158 permit
193.142.157.191 permit 193.142.157.191 permit
193.142.157.198 permit 193.142.157.198 permit
194.19.134.0/25 permit 194.19.134.0/25 permit
@@ -1812,6 +1788,7 @@
194.97.212.12 permit 194.97.212.12 permit
194.106.220.0/23 permit 194.106.220.0/23 permit
194.113.24.0/22 permit 194.113.24.0/22 permit
194.113.42.0/26 permit
194.154.193.192/27 permit 194.154.193.192/27 permit
195.4.92.0/23 permit 195.4.92.0/23 permit
195.54.172.0/23 permit 195.54.172.0/23 permit
@@ -1825,6 +1802,7 @@
198.61.254.21 permit 198.61.254.21 permit
198.61.254.231 permit 198.61.254.231 permit
198.178.234.57 permit 198.178.234.57 permit
198.202.211.1 permit
198.244.48.0/20 permit 198.244.48.0/20 permit
198.244.56.107 permit 198.244.56.107 permit
198.244.56.108 permit 198.244.56.108 permit
@@ -1908,7 +1886,6 @@
204.14.232.64/28 permit 204.14.232.64/28 permit
204.14.234.64/28 permit 204.14.234.64/28 permit
204.75.142.0/24 permit 204.75.142.0/24 permit
204.79.197.212 permit
204.92.114.187 permit 204.92.114.187 permit
204.92.114.203 permit 204.92.114.203 permit
204.92.114.204/31 permit 204.92.114.204/31 permit
@@ -1936,24 +1913,13 @@
206.165.246.80/29 permit 206.165.246.80/29 permit
206.191.224.0/19 permit 206.191.224.0/19 permit
206.246.157.1 permit 206.246.157.1 permit
207.46.4.128/25 permit
207.46.22.35 permit 207.46.22.35 permit
207.46.50.72 permit 207.46.50.72 permit
207.46.50.82 permit 207.46.50.82 permit
207.46.50.192/26 permit
207.46.50.224 permit
207.46.52.71 permit 207.46.52.71 permit
207.46.52.79 permit 207.46.52.79 permit
207.46.58.128/25 permit
207.46.116.128/29 permit
207.46.117.0/24 permit
207.46.132.128/27 permit
207.46.198.0/25 permit
207.46.200.0/27 permit
207.67.38.0/24 permit 207.67.38.0/24 permit
207.67.98.192/27 permit 207.67.98.192/27 permit
207.68.176.0/26 permit
207.68.176.96/27 permit
207.97.204.96/29 permit 207.97.204.96/29 permit
207.126.144.0/20 permit 207.126.144.0/20 permit
207.171.160.0/19 permit 207.171.160.0/19 permit
@@ -2102,14 +2068,10 @@
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.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
213.199.138.191 permit 213.199.138.191 permit
213.199.161.128/27 permit
213.199.177.0/26 permit
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
@@ -2137,7 +2099,6 @@
216.39.62.60/31 permit 216.39.62.60/31 permit
216.39.62.136/29 permit 216.39.62.136/29 permit
216.39.62.144/31 permit 216.39.62.144/31 permit
216.58.192.0/19 permit
216.66.217.240/29 permit 216.66.217.240/29 permit
216.71.138.33 permit 216.71.138.33 permit
216.71.152.207 permit 216.71.152.207 permit
@@ -2196,11 +2157,6 @@
2001:748:400:3301::3 permit 2001:748:400:3301::3 permit
2001:748:400:3301::4 permit 2001:748:400:3301::4 permit
2404:6800:4000::/36 permit 2404:6800:4000::/36 permit
2603:1010:3:3::5b permit
2603:1020:201:10::10f permit
2603:1030:20e:3::23c permit
2603:1030:b:3::152 permit
2603:1030:c02:8::14 permit
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit 2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit 2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit 2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
@@ -2216,6 +2172,8 @@
2620:10d:c09c:400::8:1 permit 2620:10d:c09c:400::8:1 permit
2620:119:50c0:207::/64 permit 2620:119:50c0:207::/64 permit
2620:119:50c0:207::215 permit 2620:119:50c0:207::215 permit
2620:1ec:46::51 permit
2620:1ec:bdf::51 permit
2800:3f0:4000::/36 permit 2800:3f0:4000::/36 permit
49.12.4.251 permit # checks.mailcow.email 49.12.4.251 permit # checks.mailcow.email
2a01:4f8:c17:7906::10 permit # checks.mailcow.email 2a01:4f8:c17:7906::10 permit # checks.mailcow.email
+12
View File
@@ -0,0 +1,12 @@
#!/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
+6 -6
View File
@@ -22,10 +22,10 @@ catch (PDOException $e) {
exit; exit;
} }
// Init Valkey // Init Redis
$valkey = new Redis(); $redis = new Redis();
$valkey->connect('valkey-mailcow', 6379); $redis->connect('redis-mailcow', 6379);
$valkey->auth(getenv("VALKEYPASS")); $redis->auth(getenv("REDISPASS"));
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 (!$valkey->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { if (!$redis->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 (!$valkey->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { if (!$redis->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 {
+5 -5
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);
$valkey = new Redis(); $redis = new Redis();
$valkey->connect('valkey-mailcow', 6379); $redis->connect('redis-mailcow', 6379);
$valkey->auth(getenv("VALKEYPASS")); $redis->auth(getenv("REDISPASS"));
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 ($valkey->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) { foreach ($redis->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 ($valkey->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) { foreach ($redis->hGetAll('WHITELISTED_FWD_HOST') as $host => $source) {
echo $host . PHP_EOL; echo $host . PHP_EOL;
} }
} }
+332 -96
View File
@@ -146,8 +146,171 @@ rspamd_config:register_symbol({
return false return false
end end
-- Helper function to parse IPv6 into 8 segments
local function ipv6_to_segments(ip_str)
-- Remove zone identifier if present (e.g., %eth0)
ip_str = ip_str:gsub("%%.*$", "")
local segments = {}
-- Handle :: compression
if ip_str:find('::') then
local before, after = ip_str:match('^(.*)::(.*)$')
before = before or ''
after = after or ''
local before_parts = {}
local after_parts = {}
if before ~= '' then
for seg in before:gmatch('[^:]+') do
table.insert(before_parts, tonumber(seg, 16) or 0)
end
end
if after ~= '' then
for seg in after:gmatch('[^:]+') do
table.insert(after_parts, tonumber(seg, 16) or 0)
end
end
-- Add before segments
for _, seg in ipairs(before_parts) do
table.insert(segments, seg)
end
-- Add compressed zeros
local zeros_needed = 8 - #before_parts - #after_parts
for i = 1, zeros_needed do
table.insert(segments, 0)
end
-- Add after segments
for _, seg in ipairs(after_parts) do
table.insert(segments, seg)
end
else
-- No compression
for seg in ip_str:gmatch('[^:]+') do
table.insert(segments, tonumber(seg, 16) or 0)
end
end
-- Ensure we have exactly 8 segments
while #segments < 8 do
table.insert(segments, 0)
end
return segments
end
-- Generate all common IPv6 notations
local function get_ipv6_variants(ip_str)
local variants = {}
local seen = {}
local function add_variant(v)
if v and not seen[v] then
table.insert(variants, v)
seen[v] = true
end
end
-- For IPv4, just return the original
if not ip_str:find(':') then
add_variant(ip_str)
return variants
end
local segments = ipv6_to_segments(ip_str)
-- 1. Fully expanded form (all zeros shown as 0000)
local expanded_parts = {}
for _, seg in ipairs(segments) do
table.insert(expanded_parts, string.format('%04x', seg))
end
add_variant(table.concat(expanded_parts, ':'))
-- 2. Standard form (no leading zeros, but all segments present)
local standard_parts = {}
for _, seg in ipairs(segments) do
table.insert(standard_parts, string.format('%x', seg))
end
add_variant(table.concat(standard_parts, ':'))
-- 3. Find all possible :: compressions
-- RFC 5952: compress the longest run of consecutive zeros
-- But we need to check all possibilities since Redis might have any form
-- Find all zero runs
local zero_runs = {}
local in_run = false
local run_start = 0
local run_length = 0
for i = 1, 8 do
if segments[i] == 0 then
if not in_run then
in_run = true
run_start = i
run_length = 1
else
run_length = run_length + 1
end
else
if in_run then
if run_length >= 1 then -- Allow single zero compression too
table.insert(zero_runs, {start = run_start, length = run_length})
end
in_run = false
end
end
end
-- Don't forget the last run
if in_run and run_length >= 1 then
table.insert(zero_runs, {start = run_start, length = run_length})
end
-- Generate variant for each zero run compression
for _, run in ipairs(zero_runs) do
local parts = {}
-- Before compression
for i = 1, run.start - 1 do
table.insert(parts, string.format('%x', segments[i]))
end
-- The compression
if run.start == 1 then
table.insert(parts, '')
table.insert(parts, '')
elseif run.start + run.length - 1 == 8 then
table.insert(parts, '')
table.insert(parts, '')
else
table.insert(parts, '')
end
-- After compression
for i = run.start + run.length, 8 do
table.insert(parts, string.format('%x', segments[i]))
end
local compressed = table.concat(parts, ':'):gsub('::+', '::')
add_variant(compressed)
end
return variants
end
local from_ip_string = tostring(ip) local from_ip_string = tostring(ip)
ip_check_table = {from_ip_string} local ip_check_table = {}
-- Add all variants of the exact IP
for _, variant in ipairs(get_ipv6_variants(from_ip_string)) do
table.insert(ip_check_table, variant)
end
local maxbits = 128 local maxbits = 128
local minbits = 32 local minbits = 32
@@ -155,10 +318,18 @@ rspamd_config:register_symbol({
maxbits = 32 maxbits = 32
minbits = 8 minbits = 8
end end
-- Add all CIDR notations with variants
for i=maxbits,minbits,-1 do for i=maxbits,minbits,-1 do
local nip = ip:apply_mask(i):to_string() .. "/" .. i local masked_ip = ip:apply_mask(i)
table.insert(ip_check_table, nip) local cidr_base = masked_ip:to_string()
for _, variant in ipairs(get_ipv6_variants(cidr_base)) do
local cidr = variant .. "/" .. i
table.insert(ip_check_table, cidr)
end
end end
local function keep_spam_cb(err, data) local function keep_spam_cb(err, data)
if err then if err then
rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err) rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
@@ -166,12 +337,15 @@ rspamd_config:register_symbol({
else else
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 %s (checked as: %s) in keep_spam map, setting pre-result accept", from_ip_string, ip_check_table[k])
task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam') task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam')
task:set_flag('no_stat')
return
end end
end end
end end
end end
table.insert(ip_check_table, 1, 'KEEP_SPAM') table.insert(ip_check_table, 1, 'KEEP_SPAM')
local redis_ret_user = rspamd_redis_make_request(task, local redis_ret_user = rspamd_redis_make_request(task,
redis_params, -- connect params redis_params, -- connect params
@@ -210,6 +384,7 @@ rspamd_config:register_symbol({
rspamd_config:register_symbol({ rspamd_config:register_symbol({
name = 'TAG_MOO', name = 'TAG_MOO',
type = 'postfilter', type = 'postfilter',
flags = 'ignore_passthrough',
callback = function(task) callback = function(task)
local util = require("rspamd_util") local util = require("rspamd_util")
local rspamd_logger = require "rspamd_logger" local rspamd_logger = require "rspamd_logger"
@@ -217,9 +392,7 @@ rspamd_config:register_symbol({
local rspamd_http = require "rspamd_http" local rspamd_http = require "rspamd_http"
local rcpts = task:get_recipients('smtp') local rcpts = task:get_recipients('smtp')
local lua_util = require "lua_util" local lua_util = require "lua_util"
local tagged_rcpt = task:get_symbol("TAGGED_RCPT") local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
local function remove_moo_tag() local function remove_moo_tag()
local moo_tag_header = task:get_header('X-Moo-Tag', false) local moo_tag_header = task:get_header('X-Moo-Tag', false)
@@ -231,101 +404,147 @@ rspamd_config:register_symbol({
return true return true
end end
if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then -- Check if we have exactly one recipient
local tag = tagged_rcpt[1].options[1] if not (rcpts and #rcpts == 1) then
rspamd_logger.infox("found tag: %s", tag) rspamd_logger.infox("TAG_MOO: not exactly one rcpt (%s), removing moo tag", rcpts and #rcpts or 0)
local action = task:get_metric_action('default') remove_moo_tag()
rspamd_logger.infox("metric action now: %s", action) return
end
if action ~= 'no action' and action ~= 'greylist' then local rcpt_addr = rcpts[1]['addr']
rspamd_logger.infox("skipping tag handler for action: %s", action) local rcpt_user = rcpts[1]['user']
remove_moo_tag() local rcpt_domain = rcpts[1]['domain']
return true
-- Check if recipient has a tag (contains '+')
local tag = nil
if tagged_rcpt ~= nil then
tag = tagged_rcpt
rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag)
end
if not tag then
rspamd_logger.infox("TAG_MOO: no tag found in recipient %s, removing moo tag", rcpt_addr)
remove_moo_tag()
return
end
-- Optional: Check if domain is a mailcow domain
-- When KEEP_SPAM is active, RCPT_MAILCOW_DOMAIN might not be set
-- If the mail is being delivered, we can assume it's valid
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
if not mailcow_domain then
rspamd_logger.infox("TAG_MOO: RCPT_MAILCOW_DOMAIN not set (possibly due to pre-result), proceeding anyway for domain %s", rcpt_domain)
end
local action = task:get_metric_action('default')
rspamd_logger.infox("TAG_MOO: metric action: %s", action)
-- Check if we have a pre-result (e.g., from KEEP_SPAM or POSTMASTER_HANDLER)
local allow_processing = false
if task.has_pre_result then
local has_pre, pre_action = task:has_pre_result()
if has_pre then
rspamd_logger.infox("TAG_MOO: pre-result detected: %s", tostring(pre_action))
if pre_action == 'accept' then
allow_processing = true
rspamd_logger.infox("TAG_MOO: pre-result is accept, will process")
end
end end
end
local function http_callback(err_message, code, body, headers) -- Allow processing for mild actions or when we have pre-result accept
if body ~= nil and body ~= "" then if not allow_processing and action ~= 'no action' and action ~= 'greylist' then
rspamd_logger.infox(rspamd_config, "expanding rcpt to \"%s\"", body) rspamd_logger.infox("TAG_MOO: skipping tag handler for action: %s", action)
remove_moo_tag()
return true
end
local function tag_callback_subject(err, data) rspamd_logger.infox("TAG_MOO: processing allowed")
if err or type(data) ~= 'string' then
rspamd_logger.infox(rspamd_config, "subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
local function tag_callback_subfolder(err, data) local function http_callback(err_message, code, body, headers)
if err or type(data) ~= 'string' then if body ~= nil and body ~= "" then
rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err) rspamd_logger.infox(rspamd_config, "TAG_MOO: expanding rcpt to \"%s\"", body)
remove_moo_tag()
else
rspamd_logger.infox("Add X-Moo-Tag header")
task:set_milter_reply({
add_headers = {['X-Moo-Tag'] = 'YES'}
})
end
end
local redis_ret_subfolder = rspamd_redis_make_request(task, local function tag_callback_subject(err, data)
redis_params, -- connect params if err or type(data) ~= 'string' or data == '' then
body, -- hash key rspamd_logger.infox(rspamd_config, "TAG_MOO: subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
false, -- is write
tag_callback_subfolder, --callback local function tag_callback_subfolder(err, data)
'HGET', -- command if err or type(data) ~= 'string' or data == '' then
{'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments rspamd_logger.infox(rspamd_config, "TAG_MOO: subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
)
if not redis_ret_subfolder then
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
remove_moo_tag() remove_moo_tag()
else
rspamd_logger.infox("TAG_MOO: User wants subfolder tag, adding X-Moo-Tag header")
task:set_milter_reply({
add_headers = {['X-Moo-Tag'] = 'YES'}
})
end end
else
rspamd_logger.infox("user wants subject modified for tagged mail")
local sbj = task:get_header('Subject')
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
task:set_milter_reply({
remove_headers = {
['Subject'] = 1,
['X-Moo-Tag'] = 0
},
add_headers = {['Subject'] = new_sbj}
})
end end
end
local redis_ret_subject = rspamd_redis_make_request(task, local redis_ret_subfolder = rspamd_redis_make_request(task,
redis_params, -- connect params redis_params, -- connect params
body, -- hash key body, -- hash key
false, -- is write false, -- is write
tag_callback_subject, --callback tag_callback_subfolder, --callback
'HGET', -- command 'HGET', -- command
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments {'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
) )
if not redis_ret_subject then if not redis_ret_subfolder then
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt") rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
remove_moo_tag()
end
end
end
if rcpts and #rcpts == 1 then
for _,rcpt in ipairs(rcpts) do
local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
if #rcpt_split == 2 then
if rcpt_split[1] == 'postmaster' then
rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
remove_moo_tag() remove_moo_tag()
else
rspamd_http.request({
task=task,
url='http://nginx:8081/aliasexp.php',
body='',
callback=http_callback,
headers={Rcpt=rcpt['addr']},
})
end end
else
rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
local sbj = task:get_header('Subject') or ''
local tag_value = tag[1] and tag[1].options and tag[1].options[1] or ''
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag_value .. '] ' .. sbj)) .. '?='
task:set_milter_reply({
remove_headers = {
['Subject'] = 1,
['X-Moo-Tag'] = 0
},
add_headers = {['Subject'] = new_sbj}
})
end end
end end
local redis_ret_subject = rspamd_redis_make_request(task,
redis_params, -- connect params
body, -- hash key
false, -- is write
tag_callback_subject, --callback
'HGET', -- command
{'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
)
if not redis_ret_subject then
rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
remove_moo_tag()
end
else
rspamd_logger.infox("TAG_MOO: alias expansion returned empty body")
remove_moo_tag()
end
end
local rcpt_split = rspamd_str_split(rcpt_addr, '@')
if #rcpt_split == 2 then
if rcpt_split[1]:match('^postmaster') then
rspamd_logger.infox(rspamd_config, "TAG_MOO: not expanding postmaster alias")
remove_moo_tag()
else
rspamd_logger.infox("TAG_MOO: requesting alias expansion for %s", rcpt_addr)
rspamd_http.request({
task=task,
url='http://nginx:8081/aliasexp.php',
body='',
callback=http_callback,
headers={Rcpt=rcpt_addr},
})
end end
else else
rspamd_logger.infox("TAG_MOO: invalid rcpt format")
remove_moo_tag() remove_moo_tag()
end end
end, end,
@@ -335,6 +554,7 @@ rspamd_config:register_symbol({
rspamd_config:register_symbol({ rspamd_config:register_symbol({
name = 'BCC', name = 'BCC',
type = 'postfilter', type = 'postfilter',
flags = 'ignore_passthrough',
callback = function(task) callback = function(task)
local util = require("rspamd_util") local util = require("rspamd_util")
local rspamd_http = require "rspamd_http" local rspamd_http = require "rspamd_http"
@@ -363,11 +583,13 @@ rspamd_config:register_symbol({
local email_content = tostring(task:get_content()) local email_content = tostring(task:get_content())
email_content = string.gsub(email_content, "\r\n%.", "\r\n..") email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
-- send mail -- send mail
local from_smtp = task:get_from('smtp')
local from_addr = (from_smtp and from_smtp[1] and from_smtp[1].addr) or 'mailer-daemon@localhost'
lua_smtp.sendmail({ lua_smtp.sendmail({
task = task, task = task,
host = os.getenv("IPV4_NETWORK") .. '.253', host = os.getenv("IPV4_NETWORK") .. '.253',
port = 591, port = 591,
from = task:get_from(stp)[1].addr, from = from_addr,
recipients = bcc_dest, recipients = bcc_dest,
helo = 'bcc', helo = 'bcc',
timeout = 20, timeout = 20,
@@ -397,27 +619,41 @@ rspamd_config:register_symbol({
end end
local action = task:get_metric_action('default') local action = task:get_metric_action('default')
rspamd_logger.infox("metric action now: %s", action) rspamd_logger.infox("BCC: metric action: %s", action)
-- Check for pre-result accept (e.g., from KEEP_SPAM)
local allow_bcc = false
if task.has_pre_result then
local has_pre, pre_action = task:has_pre_result()
if has_pre and pre_action == 'accept' then
allow_bcc = true
rspamd_logger.infox("BCC: pre-result accept detected, will send BCC")
end
end
-- Allow BCC for mild actions or when we have pre-result accept
if not allow_bcc and action ~= 'no action' and action ~= 'add header' and action ~= 'rewrite subject' then
rspamd_logger.infox("BCC: skipping for action: %s", action)
return
end
local function rcpt_callback(err_message, code, body, headers) local function rcpt_callback(err_message, code, body, headers)
if err_message == nil and code == 201 and body ~= nil then if err_message == nil and code == 201 and body ~= nil then
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then rspamd_logger.infox("BCC: sending BCC to %s for rcpt match", body)
send_mail(task, body) send_mail(task, body)
end
end end
end end
local function from_callback(err_message, code, body, headers) local function from_callback(err_message, code, body, headers)
if err_message == nil and code == 201 and body ~= nil then if err_message == nil and code == 201 and body ~= nil then
if action == 'no action' or action == 'add header' or action == 'rewrite subject' then rspamd_logger.infox("BCC: sending BCC to %s for from match", body)
send_mail(task, body) send_mail(task, body)
end
end end
end end
if rcpt_table then if rcpt_table then
for _,e in ipairs(rcpt_table) do for _,e in ipairs(rcpt_table) do
rspamd_logger.infox(rspamd_config, "checking bcc for rcpt address %s", e) rspamd_logger.infox(rspamd_config, "BCC: checking bcc for rcpt address %s", e)
rspamd_http.request({ rspamd_http.request({
task=task, task=task,
url='http://nginx:8081/bcc.php', url='http://nginx:8081/bcc.php',
@@ -430,7 +666,7 @@ rspamd_config:register_symbol({
if from_table then if from_table then
for _,e in ipairs(from_table) do for _,e in ipairs(from_table) do
rspamd_logger.infox(rspamd_config, "checking bcc for from address %s", e) rspamd_logger.infox(rspamd_config, "BCC: checking bcc for from address %s", e)
rspamd_http.request({ rspamd_http.request({
task=task, task=task,
url='http://nginx:8081/bcc.php', url='http://nginx:8081/bcc.php',
@@ -441,7 +677,7 @@ rspamd_config:register_symbol({
end end
end end
return true -- Don't return true to avoid symbol being logged
end, end,
priority = 20 priority = 20
}) })
+9 -9
View File
@@ -21,10 +21,10 @@ catch (PDOException $e) {
http_response_code(501); http_response_code(501);
exit; exit;
} }
// Init Valkey // Init Redis
$valkey = new Redis(); $redis = new Redis();
$valkey->connect('valkey-mailcow', 6379); $redis->connect('redis-mailcow', 6379);
$valkey->auth(getenv("VALKEYPASS")); $redis->auth(getenv("REDISPASS"));
// Functions // Functions
function parse_email($email) { function parse_email($email) {
@@ -74,16 +74,16 @@ if ($fuzzy == 'unknown') {
} }
try { try {
$max_size = (int)$valkey->Get('Q_MAX_SIZE'); $max_size = (int)$redis->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 = $valkey->Get('Q_EXCLUDE_DOMAINS')) { if ($exclude_domains = $redis->Get('Q_EXCLUDE_DOMAINS')) {
$exclude_domains = json_decode($exclude_domains, true); $exclude_domains = json_decode($exclude_domains, true);
} }
$retention_size = (int)$valkey->Get('Q_RETENTION_SIZE'); $retention_size = (int)$redis->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 (!$valkey->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { if (!$redis->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 (!$valkey->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { if (!$redis->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 {
+7 -7
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 Valkey // Init Redis
$valkey = new Redis(); $redis = new Redis();
try { try {
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) { if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT')); $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
} }
else { else {
$valkey->connect('valkey-mailcow', 6379); $redis->connect('redis-mailcow', 6379);
} }
$valkey->auth(getenv("VALKEYPASS")); $redis->auth(getenv("REDISPASS"));
} }
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']);
$valkey->lpush('RL_LOG', json_encode($data)); $redis->lpush('RL_LOG', json_encode($data));
exit; exit;
+6 -6
View File
@@ -21,10 +21,10 @@ catch (PDOException $e) {
http_response_code(501); http_response_code(501);
exit; exit;
} }
// Init Valkey // Init Redis
$valkey = new Redis(); $redis = new Redis();
$valkey->connect('valkey-mailcow', 6379); $redis->connect('redis-mailcow', 6379);
$valkey->auth(getenv("VALKEYPASS")); $redis->auth(getenv("REDISPASS"));
// 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 (!$valkey->hGet('DOMAIN_MAP', $parsed_rcpt['domain'])) { if (!$redis->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 (!$valkey->hGet('DOMAIN_MAP', $parsed_goto['domain'])) { if (!$redis->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 {
+6
View File
@@ -86,6 +86,12 @@
SOGoMaximumFailedLoginInterval = 900; SOGoMaximumFailedLoginInterval = 900;
SOGoFailedLoginBlockInterval = 900; SOGoFailedLoginBlockInterval = 900;
// Enable SOGo URL Description for GDPR compliance, this may cause some issues with calendars and contacts. Also uncomment the encryption key below to use it.
//SOGoURLEncryptionEnabled = NO;
// Set a 16 character encryption key for SOGo URL Description, change this to your own value
//SOGoURLPathEncryptionKey = "SOGoSuperSecret0";
GCSChannelCollectionTimer = 60; GCSChannelCollectionTimer = 60;
GCSChannelExpireAge = 60; GCSChannelExpireAge = 60;
-12
View File
@@ -1,12 +0,0 @@
#!/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 "$@"
+6 -6
View File
@@ -1,13 +1,13 @@
<?php <?php
$valkey = new Redis(); $redis = new Redis();
try { try {
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) { if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT')); $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
} }
else { else {
$valkey->connect('valkey-mailcow', 6379); $redis->connect('redis-mailcow', 6379);
} }
$valkey->auth(getenv("VALKEYPASS")); $redis->auth(getenv("REDISPASS"));
} }
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']);
$valkey->publish("F2B_CHANNEL", "Rspamd UI: Invalid password by " . $_SERVER['REMOTE_ADDR']); $redis->publish("F2B_CHANNEL", "Rspamd UI: Invalid password by " . $_SERVER['REMOTE_ADDR']);
+2 -13
View File
@@ -2,18 +2,7 @@
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { protect_route(['admin']);
header('Location: /domainadmin/mailbox');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
header('Location: /admin');
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
@@ -21,7 +10,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 = $valkey->Get('LICENSE_STATUS_CACHE')) { if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) {
$_SESSION['gal'] = json_decode($license_cache, true); $_SESSION['gal'] = json_decode($license_cache, true);
} }
+5 -2
View File
@@ -3,8 +3,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
header('Location: /admin/dashboard'); // Only redirect to dashboard if NO pending actions
exit(); if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) {
header('Location: /admin/dashboard');
exit();
}
} }
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
header('Location: /domainadmin/mailbox'); header('Location: /domainadmin/mailbox');
+1 -12
View File
@@ -2,18 +2,7 @@
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { protect_route(['admin']);
header('Location: /domainadmin/mailbox');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
header('Location: /admin');
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
+1 -13
View File
@@ -2,19 +2,7 @@
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { protect_route(['admin']);
header('Location: /domainadmin/mailbox');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
header('Location: /admin');
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$js_minifier->add('/web/js/site/queue.js'); $js_minifier->add('/web/js/site/queue.js');
+1 -12
View File
@@ -2,18 +2,7 @@
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php';
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { protect_route(['admin']);
header('Location: /domainadmin/mailbox');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") {
header('Location: /admin');
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
+87 -3
View File
@@ -2454,6 +2454,90 @@ paths:
type: object type: object
type: object type: object
summary: Delete mails in Quarantine summary: Delete mails in Quarantine
/api/v1/edit/qitem:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
release:
value:
- log:
- quarantine
- edit
- id:
- "33"
action: release
msg:
- item_released
- "33"
type: success
learnham:
value:
- log:
- quarantine
- edit
- id:
- "34"
action: learnham
msg:
- item_learned
- "34"
type: success
schema:
properties:
log:
description: contains request object
items: {}
type: array
msg:
items: {}
type: array
type:
enum:
- success
- danger
- error
type: string
type: object
description: OK
headers: {}
tags:
- Quarantine
description: >-
Using this endpoint you can perform actions on quarantine items. It is possible to release
emails from quarantine into to the inbox, or learn them as ham to improve Rspamd filtering.
You must provide the quarantine item IDs. You can get the IDs using the GET method.
operationId: Edit mails in Quarantine
requestBody:
content:
application/json:
schema:
example:
items:
- "33"
- "34"
attr:
action: release
properties:
items:
description: contains list of quarantine item IDs to release or learn as ham
type: object
attr:
description: attributes for the action
type: object
properties:
action:
type: string
enum:
- release
- learnham
description: "release - return email to inbox; learnham - learn as ham to improve filtering"
type: object
summary: Edit mails in Quarantine
/api/v1/delete/recipient_map: /api/v1/delete/recipient_map:
post: post:
responses: responses:
@@ -5412,9 +5496,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
valkey-mailcow: redis-mailcow:
container: valkey-mailcow container: redis-mailcow
image: "valkey:7.2.8-alpine" image: "redis:5-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
+2 -2
View File
@@ -29,8 +29,8 @@ header('Content-Type: application/xml');
<clientConfig version="1.1"> <clientConfig version="1.1">
<emailProvider id="<?=$mailcow_hostname; ?>"> <emailProvider id="<?=$mailcow_hostname; ?>">
<domain>%EMAILDOMAIN%</domain> <domain>%EMAILDOMAIN%</domain>
<displayName>A mailcow mail server</displayName> <displayName><?=$autodiscover_config['displayName']; ?></displayName>
<displayShortName>mail server</displayShortName> <displayShortName><?=$autodiscover_config['displayShortName']; ?></displayShortName>
<incomingServer type="imap"> <incomingServer type="imap">
<hostname><?=$autodiscover_config['imap']['server']; ?></hostname> <hostname><?=$autodiscover_config['imap']['server']; ?></hostname>
+154 -89
View File
@@ -7,19 +7,21 @@ if(file_exists('inc/vars.local.inc.php')) {
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.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);
// Valkey // Redis
$valkey = new Redis(); $redis = new Redis();
try { try {
if (!empty(getenv('VALKEY_SLAVEOF_IP'))) { if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$valkey->connect(getenv('VALKEY_SLAVEOF_IP'), getenv('VALKEY_SLAVEOF_PORT')); $redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
} }
else { else {
$valkey->connect('valkey-mailcow', 6379); $redis->connect('redis-mailcow', 6379);
} }
$valkey->auth(getenv("VALKEYPASS")); $redis->auth(getenv("REDISPASS"));
} }
catch (Exception $e) { catch (Exception $e) {
exit; exit;
@@ -58,58 +60,43 @@ $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
$iam_provider = identity_provider('init'); $iam_provider = identity_provider('init');
$iam_settings = identity_provider('get'); $iam_settings = identity_provider('get');
$login_user = strtolower(trim($_SERVER['PHP_AUTH_USER'])); // Passwordless autodiscover - no authentication required
$login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW'])); // Email will be extracted from the request body
$login_user = null;
$login_role = null;
if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) { header("Content-Type: application/xml");
$json = json_encode( echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
array(
"time" => time(),
"ua" => $_SERVER['HTTP_USER_AGENT'],
"user" => "none",
"ip" => $_SERVER['REMOTE_ADDR'],
"service" => "Error: must be authenticated"
)
);
$valkey->lPush('AUTODISCOVER_LOG', $json);
header('WWW-Authenticate: Basic realm="' . $_SERVER['HTTP_HOST'] . '"');
header('HTTP/1.0 401 Unauthorized');
exit(0);
}
$login_role = check_login($login_user, $login_pass, array('eas' => TRUE));
if ($login_role === "user") {
header("Content-Type: application/xml");
echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
?> ?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006"> <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
<?php <?php
if(!$data) { if(!$data) {
try { try {
$json = json_encode( $json = json_encode(
array( array(
"time" => time(), "time" => time(),
"ua" => $_SERVER['HTTP_USER_AGENT'], "ua" => $_SERVER['HTTP_USER_AGENT'],
"user" => $_SERVER['PHP_AUTH_USER'], "user" => "none",
"ip" => $_SERVER['REMOTE_ADDR'], "ip" => $_SERVER['REMOTE_ADDR'],
"service" => "Error: invalid or missing request data" "service" => "Error: invalid or missing request data"
) )
); );
$valkey->lPush('AUTODISCOVER_LOG', $json); $redis->lPush('AUTODISCOVER_LOG', $json);
$valkey->lTrim('AUTODISCOVER_LOG', 0, 100); $redis->lTrim('AUTODISCOVER_LOG', 0, 100);
} $redis->publish("F2B_CHANNEL", "Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
catch (RedisException $e) { error_log("Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
$_SESSION['return'][] = array( }
'type' => 'danger', catch (RedisException $e) {
'msg' => 'Valkey: '.$e $_SESSION['return'][] = array(
); 'type' => 'danger',
return false; 'msg' => 'Redis: '.$e
} );
list($usec, $sec) = explode(' ', microtime()); return false;
}
list($usec, $sec) = explode(' ', microtime());
?> ?>
<Response> <Response>
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="2477272013"> <Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
<ErrorCode>600</ErrorCode> <ErrorCode>600</ErrorCode>
<Message>Invalid Request</Message> <Message>Invalid Request</Message>
<DebugData /> <DebugData />
@@ -117,51 +104,132 @@ if ($login_role === "user") {
</Response> </Response>
</Autodiscover> </Autodiscover>
<?php <?php
exit(0); exit(0);
} }
try { try {
$discover = new SimpleXMLElement($data); $discover = new SimpleXMLElement($data);
$email = $discover->Request->EMailAddress; $email = $discover->Request->EMailAddress;
} catch (Exception $e) { } catch (Exception $e) {
$email = $_SERVER['PHP_AUTH_USER']; // If parsing fails, return error
}
$username = trim($email);
try {
$stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username");
$stmt->execute(array(':username' => $username));
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
}
catch(PDOException $e) {
die("Failed to determine name from SQL");
}
if (!empty($MailboxData['name'])) {
$displayname = $MailboxData['name'];
}
else {
$displayname = $email;
}
try { try {
$json = json_encode( $json = json_encode(
array( array(
"time" => time(), "time" => time(),
"ua" => $_SERVER['HTTP_USER_AGENT'], "ua" => $_SERVER['HTTP_USER_AGENT'],
"user" => $_SERVER['PHP_AUTH_USER'], "user" => "none",
"ip" => $_SERVER['REMOTE_ADDR'], "ip" => $_SERVER['REMOTE_ADDR'],
"service" => $autodiscover_config['autodiscoverType'] "service" => "Error: could not parse email from request"
) )
); );
$valkey->lPush('AUTODISCOVER_LOG', $json); $redis->lPush('AUTODISCOVER_LOG', $json);
$valkey->lTrim('AUTODISCOVER_LOG', 0, 100); $redis->lTrim('AUTODISCOVER_LOG', 0, 100);
$redis->publish("F2B_CHANNEL", "Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
error_log("Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( // Silently fail
'type' => 'danger',
'msg' => 'Valkey: '.$e
);
return false;
} }
if ($autodiscover_config['autodiscoverType'] == 'imap') { list($usec, $sec) = explode(' ', microtime());
?>
<Response>
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
<ErrorCode>600</ErrorCode>
<Message>Invalid Request</Message>
<DebugData />
</Error>
</Response>
</Autodiscover>
<?php
exit(0);
}
$username = trim((string)$email);
try {
$stmt = $pdo->prepare("SELECT `mailbox`.`name`, `mailbox`.`active` FROM `mailbox`
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
WHERE `mailbox`.`username` = :username
AND `mailbox`.`active` = '1'
AND `domain`.`active` = '1'");
$stmt->execute(array(':username' => $username));
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
}
catch(PDOException $e) {
// Database error - return error response with complete XML
list($usec, $sec) = explode(' ', microtime());
?>
<Response>
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
<ErrorCode>500</ErrorCode>
<Message>Database Error</Message>
<DebugData />
</Error>
</Response>
</Autodiscover>
<?php
exit(0);
}
// Mailbox not found or not active - return generic error to prevent user enumeration
if (empty($MailboxData)) {
try {
$json = json_encode(
array(
"time" => time(),
"ua" => $_SERVER['HTTP_USER_AGENT'],
"user" => $email,
"ip" => $_SERVER['REMOTE_ADDR'],
"service" => "Error: mailbox not found or inactive"
)
);
$redis->lPush('AUTODISCOVER_LOG', $json);
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
error_log("Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
}
catch (RedisException $e) {
// Silently fail
}
list($usec, $sec) = explode(' ', microtime());
?>
<Response>
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
<ErrorCode>600</ErrorCode>
<Message>Invalid Request</Message>
<DebugData />
</Error>
</Response>
</Autodiscover>
<?php
exit(0);
}
if (!empty($MailboxData['name'])) {
$displayname = $MailboxData['name'];
}
else {
$displayname = $email;
}
try {
$json = json_encode(
array(
"time" => time(),
"ua" => $_SERVER['HTTP_USER_AGENT'],
"user" => $email,
"ip" => $_SERVER['REMOTE_ADDR'],
"service" => $autodiscover_config['autodiscoverType']
)
);
$redis->lPush('AUTODISCOVER_LOG', $json);
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'msg' => 'Redis: '.$e
);
return false;
}
if ($autodiscover_config['autodiscoverType'] == 'imap') {
?> ?>
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a"> <Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
<User> <User>
@@ -236,6 +304,3 @@ if ($login_role === "user") {
} }
?> ?>
</Autodiscover> </Autodiscover>
<?php
}
?>
+5 -2
View File
@@ -3,8 +3,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
header('Location: /domainadmin/mailbox'); // Only redirect to mailbox if NO pending actions
exit(); if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) {
header('Location: /domainadmin/mailbox');
exit();
}
} }
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
header('Location: /admin/dashboard'); header('Location: /admin/dashboard');
+1 -12
View File
@@ -2,18 +2,7 @@
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { protect_route(['domainadmin']);
header('Location: /admin/dashboard');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "domainadmin") {
header('Location: /domainadmin');
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
+19 -32
View File
@@ -2,41 +2,28 @@
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php';
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { /*
/ DOMAIN ADMIN
*/
/* protect_route(['domainadmin']);
/ DOMAIN ADMIN
*/
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $_SESSION['return_to'] = $_SERVER['REQUEST_URI'];
$tfa_data = get_tfa(); $tfa_data = get_tfa();
$fido2_data = fido2(array("action" => "get_friendly_names")); $fido2_data = fido2(array("action" => "get_friendly_names"));
$username = $_SESSION['mailcow_cc_username']; $username = $_SESSION['mailcow_cc_username'];
$template = 'domainadmin.twig'; $template = 'domainadmin.twig';
$template_data = [ $template_data = [
'acl' => $_SESSION['acl'], 'acl' => $_SESSION['acl'],
'acl_json' => json_encode($_SESSION['acl']), 'acl_json' => json_encode($_SESSION['acl']),
'user_spam_score' => mailbox('get', 'spam_score', $username), 'user_spam_score' => mailbox('get', 'spam_score', $username),
'tfa_data' => $tfa_data, 'tfa_data' => $tfa_data,
'fido2_data' => $fido2_data, 'fido2_data' => $fido2_data,
'lang_user' => json_encode($lang['user']), 'lang_user' => json_encode($lang['user']),
'lang_datatables' => json_encode($lang['datatables']), 'lang_datatables' => json_encode($lang['datatables']),
]; ];
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
header('Location: /admin/dashboard');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
else {
header('Location: /domainadmin');
exit();
}
$js_minifier->add('/web/js/site/user.js'); $js_minifier->add('/web/js/site/user.js');
$js_minifier->add('/web/js/site/pwgen.js'); $js_minifier->add('/web/js/site/pwgen.js');
+3 -5
View File
@@ -1,10 +1,8 @@
<?php <?php
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
$AuthUsers = array("admin", "domainadmin", "user");
if (!isset($_SESSION['mailcow_cc_role']) OR !in_array($_SESSION['mailcow_cc_role'], $AuthUsers)) { protect_route();
header('Location: /');
exit();
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php';
$template = 'edit.twig'; $template = 'edit.twig';
+10 -1
View File
@@ -129,7 +129,16 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm
); );
} }
$mta_sts = mailbox('get', 'mta_sts', $domain); // Check if domain is an alias domain and get target domain's MTA-STS
$alias_domain_details = mailbox('get', 'alias_domain_details', $domain);
$mta_sts_domain = $domain;
if ($alias_domain_details !== false && !empty($alias_domain_details['target_domain'])) {
// This is an alias domain, check target domain for MTA-STS
$mta_sts_domain = $alias_domain_details['target_domain'];
}
$mta_sts = mailbox('get', 'mta_sts', $mta_sts_domain);
if (count($mta_sts) > 0 && $mta_sts['active'] == 1) { if (count($mta_sts) > 0 && $mta_sts['active'] == 1) {
if (!in_array($domain, $alias_domains)) { if (!in_array($domain, $alias_domains)) {
$records[] = array( $records[] = array(
+2
View File
@@ -64,6 +64,8 @@ $globalVariables = [
'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'], 'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'],
'pending_tfa_authmechs' => $pending_tfa_authmechs, 'pending_tfa_authmechs' => $pending_tfa_authmechs,
'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'], 'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'],
'pending_tfa_setup' => !empty($_SESSION['pending_tfa_setup']),
'pending_pw_update_modal' => !empty($_SESSION['pending_pw_update']),
'lang_footer' => json_encode($lang['footer']), 'lang_footer' => json_encode($lang['footer']),
'lang_acl' => json_encode($lang['acl']), 'lang_acl' => json_encode($lang['acl']),
'lang_tfa' => json_encode($lang['tfa']), 'lang_tfa' => json_encode($lang['tfa']),
+49 -25
View File
@@ -121,34 +121,56 @@ function admin($_action, $_data = null) {
continue; continue;
} }
} }
if (!empty($password)) { // Check if this is a self password change via forced update
if (password_check($password, $password2) !== true) { if ($username == $_SESSION['mailcow_cc_username'] && !empty($_SESSION['pending_pw_update'])) {
return false; // Forced password update: only change password and clear force_pw_update flag
if (!empty($password)) {
if (password_check($password, $_data['password2']) !== true) {
return false;
}
$password_hashed = hash_password($password);
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed,
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_pw_update', '0')
WHERE `username` = :username");
$stmt->execute(array(
':password_hashed' => $password_hashed,
':username' => $username
));
unset($_SESSION['pending_pw_update']);
} }
$password_hashed = hash_password($password); } else {
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); // Normal admin edit: update all attributes
$stmt->execute(array( $force_tfa = intval($_data['force_tfa'] ?? 0) ? 1 : 0;
':password_hashed' => $password_hashed, $force_pw_update = intval($_data['force_pw_update'] ?? 0) ? 1 : 0;
':username_new' => $username_new, if (!empty($password)) {
':username' => $username, if (password_check($password, $password2) !== true) {
':active' => $active return false;
)); }
if (isset($_data['disable_tfa'])) { $password_hashed = hash_password($password);
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed,
$stmt->execute(array(':username' => $username)); `attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
WHERE `username` = :username");
$stmt->execute(array(
':password_hashed' => $password_hashed,
':username_new' => $username_new,
':username' => $username,
':active' => $active,
':force_tfa' => strval($force_tfa),
':force_pw_update' => strval($force_pw_update)
));
} }
else { else {
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active,
$stmt->execute(array(':username_new' => $username_new, ':username' => $username)); `attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
WHERE `username` = :username");
$stmt->execute(array(
':username_new' => $username_new,
':username' => $username,
':active' => $active,
':force_tfa' => strval($force_tfa),
':force_pw_update' => strval($force_pw_update)
));
} }
}
else {
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
$stmt->execute(array(
':username_new' => $username_new,
':username' => $username,
':active' => $active
));
if (isset($_data['disable_tfa'])) { if (isset($_data['disable_tfa'])) {
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
$stmt->execute(array(':username' => $username)); $stmt->execute(array(':username' => $username));
@@ -223,7 +245,8 @@ function admin($_action, $_data = null) {
`tfa`.`active` AS `tfa_active`, `tfa`.`active` AS `tfa_active`,
`admin`.`username`, `admin`.`username`,
`admin`.`created`, `admin`.`created`,
`admin`.`active` AS `active` `admin`.`active` AS `active`,
`admin`.`attributes` AS `attributes`
FROM `admin` FROM `admin`
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`admin`.`username` LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`admin`.`username`
WHERE `admin`.`username`= :admin AND `superadmin` = '1'"); WHERE `admin`.`username`= :admin AND `superadmin` = '1'");
@@ -240,6 +263,7 @@ function admin($_action, $_data = null) {
$admindata['active'] = $row['active']; $admindata['active'] = $row['active'];
$admindata['active_int'] = $row['active']; $admindata['active_int'] = $row['active'];
$admindata['created'] = $row['created']; $admindata['created'] = $row['created'];
$admindata['attributes'] = json_decode($row['attributes'], true) ?? array('force_tfa' => '0', 'force_pw_update' => '0');
return $admindata; return $admindata;
break; break;
} }
+55 -46
View File
@@ -1,10 +1,11 @@
<?php <?php
function check_login($user, $pass, $app_passwd_data = false, $extra = null) { function check_login($user, $pass, $extra = null) {
global $pdo; global $pdo;
global $valkey; global $redis;
$is_internal = $extra['is_internal']; $is_internal = $extra['is_internal'];
$role = $extra['role']; $role = $extra['role'];
$extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
// Try validate admin // Try validate admin
if (!isset($role) || $role == "admin") { if (!isset($role) || $role == "admin") {
@@ -25,34 +26,20 @@ function check_login($user, $pass, $app_passwd_data = false, $extra = null) {
// Try validate app password // Try validate app password
if (!isset($role) || $role == "app") { if (!isset($role) || $role == "app") {
$result = apppass_login($user, $pass, $app_passwd_data); $result = apppass_login($user, $pass, $extra);
if ($result !== false) { if ($result !== false) {
if ($app_passwd_data['eas'] === true) {
$service = 'EAS';
} elseif ($app_passwd_data['dav'] === true) {
$service = 'DAV';
} else {
$service = 'NONE';
}
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']); $real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
set_sasl_log($user, $real_rip, $service, $pass); set_sasl_log($user, $real_rip, $extra['service'], $pass);
return $result; return $result;
} }
} }
// Try validate user // Try validate user
if (!isset($role) || $role == "user") { if (!isset($role) || $role == "user") {
$result = user_login($user, $pass); $result = user_login($user, $pass, $extra);
if ($result !== false) { if ($result !== false) {
if ($app_passwd_data['eas'] === true) {
$service = 'EAS';
} elseif ($app_passwd_data['dav'] === true) {
$service = 'DAV';
} else {
$service = 'MAILCOWUI';
}
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']); $real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
set_sasl_log($user, $real_rip, $service); set_sasl_log($user, $real_rip, $extra['service']);
return $result; return $result;
} }
} }
@@ -62,12 +49,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";
$valkey->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); $redis->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;
$valkey->publish("F2B_CHANNEL", "mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); $redis->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(
@@ -95,7 +82,7 @@ function admin_login($user, $pass){
} }
$user = strtolower(trim($user)); $user = strtolower(trim($user));
$stmt = $pdo->prepare("SELECT `password` FROM `admin` $stmt = $pdo->prepare("SELECT `password`, `attributes` FROM `admin`
WHERE `superadmin` = '1' WHERE `superadmin` = '1'
AND `active` = '1' AND `active` = '1'
AND `username` = :user"); AND `username` = :user");
@@ -104,6 +91,13 @@ function admin_login($user, $pass){
// verify password // verify password
if (verify_hash($row['password'], $pass)) { if (verify_hash($row['password'], $pass)) {
$admin_attrs = json_decode($row['attributes'], true) ?? [];
// Check force_pw_update
if (intval($admin_attrs['force_pw_update'] ?? 0) == 1) {
$_SESSION['pending_pw_update'] = true;
}
// check for tfa authenticators // check for tfa authenticators
$authenticators = get_tfa($user); $authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
@@ -123,6 +117,10 @@ function admin_login($user, $pass){
// Reactivate TFA if it was set to "deactivate TFA for next login" // Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
// Check force_tfa: only force setup if NO TFA exists at all
if (intval($admin_attrs['force_tfa'] ?? 0) == 1 && !tfa_exists($user)) {
$_SESSION['pending_tfa_setup'] = true;
}
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'), 'log' => array(__FUNCTION__, $user, '*'),
@@ -148,7 +146,7 @@ function domainadmin_login($user, $pass){
return false; return false;
} }
$stmt = $pdo->prepare("SELECT `password` FROM `admin` $stmt = $pdo->prepare("SELECT `password`, `attributes` FROM `admin`
WHERE `superadmin` = '0' WHERE `superadmin` = '0'
AND `active`='1' AND `active`='1'
AND `username` = :user"); AND `username` = :user");
@@ -157,6 +155,13 @@ function domainadmin_login($user, $pass){
// verify password // verify password
if (verify_hash($row['password'], $pass) !== false) { if (verify_hash($row['password'], $pass) !== false) {
$admin_attrs = json_decode($row['attributes'], true) ?? [];
// Check force_pw_update
if (intval($admin_attrs['force_pw_update'] ?? 0) == 1) {
$_SESSION['pending_pw_update'] = true;
}
// check for tfa authenticators // check for tfa authenticators
$authenticators = get_tfa($user); $authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
@@ -176,6 +181,10 @@ function domainadmin_login($user, $pass){
// Reactivate TFA if it was set to "deactivate TFA for next login" // Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
// Check force_tfa: only force setup if NO TFA exists at all
if (intval($admin_attrs['force_tfa'] ?? 0) == 1 && !tfa_exists($user)) {
$_SESSION['pending_tfa_setup'] = true;
}
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'), 'log' => array(__FUNCTION__, $user, '*'),
@@ -193,7 +202,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']; $extra['service'] = !isset($extra['service']) ? 'NONE' : $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){
@@ -236,10 +245,10 @@ 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 // check if user has access to service (imap, smtp, pop3, sieve, dav, eas) if service is set
$row['attributes'] = json_decode($row['attributes'], true); $row['attributes'] = json_decode($row['attributes'], true);
if (isset($service)) { if ($extra['service'] != 'NONE') {
$key = strtolower($service) . "_access"; $key = strtolower($extra['service']) . "_access";
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') { if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
return false; return false;
} }
@@ -253,8 +262,8 @@ function user_login($user, $pass, $extra = null){
// check if user has access to service (imap, smtp, pop3, sieve) if service is set // 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)) { if ($extra['service'] != 'NONE') {
$key = strtolower($service) . "_access"; $key = strtolower($extra['service']) . "_access";
if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') { if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') {
return false; return false;
} }
@@ -299,6 +308,10 @@ function user_login($user, $pass, $extra = null){
// Reactivate TFA if it was set to "deactivate TFA for next login" // Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
// Check force_tfa: only force setup if NO TFA exists at all
if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) {
$_SESSION['pending_tfa_setup'] = true;
}
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $user, '*', 'Provider: Keycloak'), 'log' => array(__FUNCTION__, $user, '*', 'Provider: Keycloak'),
@@ -351,6 +364,10 @@ function user_login($user, $pass, $extra = null){
// Reactivate TFA if it was set to "deactivate TFA for next login" // Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
// Check force_tfa: only force setup if NO TFA exists at all
if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) {
$_SESSION['pending_tfa_setup'] = true;
}
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $user, '*', 'Provider: LDAP'), 'log' => array(__FUNCTION__, $user, '*', 'Provider: LDAP'),
@@ -394,6 +411,10 @@ function user_login($user, $pass, $extra = null){
// Reactivate TFA if it was set to "deactivate TFA for next login" // Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
// Check force_tfa: only force setup if NO TFA exists at all
if (intval($row['attributes']['force_tfa']) == 1 && !tfa_exists($user)) {
$_SESSION['pending_tfa_setup'] = true;
}
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $user, '*', 'Provider: mailcow'), 'log' => array(__FUNCTION__, $user, '*', 'Provider: mailcow'),
@@ -408,7 +429,7 @@ function user_login($user, $pass, $extra = null){
return false; return false;
} }
function apppass_login($user, $pass, $app_passwd_data, $extra = null){ function apppass_login($user, $pass, $extra = null){
global $pdo; global $pdo;
$is_internal = $extra['is_internal']; $is_internal = $extra['is_internal'];
@@ -424,20 +445,8 @@ function apppass_login($user, $pass, $app_passwd_data, $extra = null){
return false; return false;
} }
$protocol = false; $extra['service'] = !isset($extra['service']) ? 'NONE' : $extra['service'];
if ($app_passwd_data['eas']){ if (!$is_internal && $extra['service'] == 'NONE') {
$protocol = 'eas';
} else if ($app_passwd_data['dav']){
$protocol = 'dav';
} else if ($app_passwd_data['smtp']){
$protocol = 'smtp';
} else if ($app_passwd_data['imap']){
$protocol = 'imap';
} else if ($app_passwd_data['sieve']){
$protocol = 'sieve';
} else if ($app_passwd_data['pop3']){
$protocol = 'pop3';
} else if (!$is_internal) {
return false; return false;
} }
@@ -458,7 +467,7 @@ function apppass_login($user, $pass, $app_passwd_data, $extra = null){
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) { foreach ($rows as $row) {
if ($protocol && $row[$protocol . '_access'] != '1'){ if ($extra['service'] != 'NONE' && $row[strtolower($extra['service']) . '_access'] != '1'){
continue; continue;
} }
+38 -38
View File
@@ -1,6 +1,6 @@
<?php <?php
function customize($_action, $_item, $_data = null) { function customize($_action, $_item, $_data = null) {
global $valkey; global $redis;
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 {
$valkey->Set(strtoupper($_item), 'data:' . $_data[$_item]['type'] . ';base64,' . base64_encode(file_get_contents($_data[$_item]['tmp_name']))); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -134,13 +134,13 @@ function customize($_action, $_item, $_data = null) {
)); ));
} }
try { try {
$valkey->set('APP_LINKS', json_encode($out)); $redis->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('valkey_error', $e) 'msg' => array('redis_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 {
$valkey->set('TITLE_NAME', htmlspecialchars($title_name)); $redis->set('TITLE_NAME', htmlspecialchars($title_name));
$valkey->set('MAIN_NAME', htmlspecialchars($main_name)); $redis->set('MAIN_NAME', htmlspecialchars($main_name));
$valkey->set('APPS_NAME', htmlspecialchars($apps_name)); $redis->set('APPS_NAME', htmlspecialchars($apps_name));
$valkey->set('HELP_TEXT', $help_text); $redis->set('HELP_TEXT', $help_text);
$valkey->set('UI_FOOTER', $ui_footer); $redis->set('UI_FOOTER', $ui_footer);
$valkey->set('UI_ANNOUNCEMENT_TEXT', $ui_announcement_text); $redis->set('UI_ANNOUNCEMENT_TEXT', $ui_announcement_text);
$valkey->set('UI_ANNOUNCEMENT_TYPE', $ui_announcement_type); $redis->set('UI_ANNOUNCEMENT_TYPE', $ui_announcement_type);
$valkey->set('UI_ANNOUNCEMENT_ACTIVE', $ui_announcement_active); $redis->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('valkey_error', $e) 'msg' => array('redis_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 {
$valkey->set('IP_CHECK', $ip_check); $redis->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('valkey_error', $e) 'msg' => array('redis_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 {
$valkey->set('CUSTOM_LOGIN', json_encode($custom_login)); $redis->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 ($valkey->del(strtoupper($_item))) { if ($redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -281,13 +281,13 @@ function customize($_action, $_item, $_data = null) {
switch ($_item) { switch ($_item) {
case 'app_links': case 'app_links':
try { try {
$app_links = json_decode($valkey->get('APP_LINKS'), true); $app_links = json_decode($redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -312,13 +312,13 @@ function customize($_action, $_item, $_data = null) {
case 'main_logo': case 'main_logo':
case 'main_logo_dark': case 'main_logo_dark':
try { try {
return $valkey->get(strtoupper($_item)); return $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -327,25 +327,25 @@ function customize($_action, $_item, $_data = null) {
try { try {
$mailcow_hostname = strtolower(getenv("MAILCOW_HOSTNAME")); $mailcow_hostname = strtolower(getenv("MAILCOW_HOSTNAME"));
$data['title_name'] = ($title_name = $valkey->get('TITLE_NAME')) ? $title_name : "$mailcow_hostname - mail UI"; $data['title_name'] = ($title_name = $redis->get('TITLE_NAME')) ? $title_name : "$mailcow_hostname - mail UI";
$data['main_name'] = ($main_name = $valkey->get('MAIN_NAME')) ? $main_name : "$mailcow_hostname - mail UI"; $data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : "$mailcow_hostname - mail UI";
$data['apps_name'] = ($apps_name = $valkey->get('APPS_NAME')) ? $apps_name : $lang['header']['apps']; $data['apps_name'] = ($apps_name = $redis->get('APPS_NAME')) ? $apps_name : $lang['header']['apps'];
$data['help_text'] = ($help_text = $valkey->get('HELP_TEXT')) ? $help_text : false; $data['help_text'] = ($help_text = $redis->get('HELP_TEXT')) ? $help_text : false;
if (!empty($valkey->get('UI_IMPRESS'))) { if (!empty($redis->get('UI_IMPRESS'))) {
$valkey->set('UI_FOOTER', $valkey->get('UI_IMPRESS')); $redis->set('UI_FOOTER', $redis->get('UI_IMPRESS'));
$valkey->del('UI_IMPRESS'); $redis->del('UI_IMPRESS');
} }
$data['ui_footer'] = ($ui_footer = $valkey->get('UI_FOOTER')) ? $ui_footer : false; $data['ui_footer'] = ($ui_footer = $redis->get('UI_FOOTER')) ? $ui_footer : false;
$data['ui_announcement_text'] = ($ui_announcement_text = $valkey->get('UI_ANNOUNCEMENT_TEXT')) ? $ui_announcement_text : false; $data['ui_announcement_text'] = ($ui_announcement_text = $redis->get('UI_ANNOUNCEMENT_TEXT')) ? $ui_announcement_text : false;
$data['ui_announcement_type'] = ($ui_announcement_type = $valkey->get('UI_ANNOUNCEMENT_TYPE')) ? $ui_announcement_type : false; $data['ui_announcement_type'] = ($ui_announcement_type = $redis->get('UI_ANNOUNCEMENT_TYPE')) ? $ui_announcement_type : false;
$data['ui_announcement_active'] = ($valkey->get('UI_ANNOUNCEMENT_ACTIVE') == 1) ? 1 : 0; $data['ui_announcement_active'] = ($redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -376,21 +376,21 @@ function customize($_action, $_item, $_data = null) {
break; break;
case 'ip_check': case 'ip_check':
try { try {
$ip_check = ($ip_check = $valkey->get('IP_CHECK')) ? $ip_check : 0; $ip_check = ($ip_check = $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
break; break;
case 'custom_login': case 'custom_login':
try { try {
$custom_login = $valkey->get('CUSTOM_LOGIN'); $custom_login = $redis->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) {
+32 -32
View File
@@ -1,7 +1,7 @@
<?php <?php
function dkim($_action, $_data = null, $privkey = false) { function dkim($_action, $_data = null, $privkey = false) {
global $valkey; global $redis;
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 ($valkey->hGet('DKIM_PUB_KEYS', $domain)) { if ($redis->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 valkey // Save public key and selector to redis
try { try {
$valkey->hSet('DKIM_PUB_KEYS', $domain, $pubKey); $redis->hSet('DKIM_PUB_KEYS', $domain, $pubKey);
$valkey->hSet('DKIM_SELECTORS', $domain, $dkim_selector); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
continue; continue;
} }
// Export private key and save private key to valkey // Export private key and save private key to redis
openssl_pkey_export($keypair_ressource, $privKey); openssl_pkey_export($keypair_ressource, $privKey);
if (isset($privKey) && !empty($privKey)) { if (isset($privKey) && !empty($privKey)) {
try { try {
$valkey->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, trim($privKey)); $redis->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('valkey_error', $e) 'msg' => array('redis_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 {
$valkey->hSet('DKIM_PUB_KEYS', $to_domain, $from_domain_dkim['pubkey']); $redis->hSet('DKIM_PUB_KEYS', $to_domain, $from_domain_dkim['pubkey']);
$valkey->hSet('DKIM_SELECTORS', $to_domain, $from_domain_dkim['dkim_selector']); $redis->hSet('DKIM_SELECTORS', $to_domain, $from_domain_dkim['dkim_selector']);
$valkey->hSet('DKIM_PRIV_KEYS', $from_domain_dkim['dkim_selector'] . '.' . $to_domain, base64_decode(trim($from_domain_dkim['privkey']))); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
continue; continue;
} }
@@ -178,7 +178,7 @@ function dkim($_action, $_data = null, $privkey = false) {
); );
return false; return false;
} }
if ($valkey->hGet('DKIM_PUB_KEYS', $domain)) { if ($redis->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));
$valkey->hSet('DKIM_PUB_KEYS', $domain, $pem_public_key); $redis->hSet('DKIM_PUB_KEYS', $domain, $pem_public_key);
$valkey->hSet('DKIM_SELECTORS', $domain, $dkim_selector); $redis->hSet('DKIM_SELECTORS', $domain, $dkim_selector);
$valkey->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, $private_key_normalized); $redis->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('valkey_error', $e) 'msg' => array('redis_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('valkey_error', $e) 'msg' => array('redis_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 ($valkey_dkim_key_data = $valkey->hGet('DKIM_PUB_KEYS', $_data)) { if ($redis_dkim_key_data = $redis->hGet('DKIM_PUB_KEYS', $_data)) {
$dkimdata['pubkey'] = $valkey_dkim_key_data; $dkimdata['pubkey'] = $redis_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=' . $valkey_dkim_key_data, 255); $dkim_txt_tmp = str_split('v=DKIM1;k=rsa;t=s;s=email;p=' . $redis_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=' . $valkey_dkim_key_data; $dkimdata['dkim_txt'] = 'v=DKIM1;k=rsa;t=s;s=email;p=' . $redis_dkim_key_data;
} }
$dkimdata['dkim_selector'] = $valkey->hGet('DKIM_SELECTORS', $_data); $dkimdata['dkim_selector'] = $redis->hGet('DKIM_SELECTORS', $_data);
if ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] || $privkey == true) { if ($GLOBALS['SHOW_DKIM_PRIV_KEYS'] || $privkey == true) {
$dkimdata['privkey'] = base64_encode($valkey->hGet('DKIM_PRIV_KEYS', $dkimdata['dkim_selector'] . '.' . $_data)); $dkimdata['privkey'] = base64_encode($redis->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 ($valkey->hKeys('DKIM_PUB_KEYS') as $valkey_dkim_domain) { foreach ($redis->hKeys('DKIM_PUB_KEYS') as $redis_dkim_domain) {
$blinddkim[] = $valkey_dkim_domain; $blinddkim[] = $redis_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 = $valkey->hGet('DKIM_SELECTORS', $domain); $selector = $redis->hGet('DKIM_SELECTORS', $domain);
$valkey->hDel('DKIM_PUB_KEYS', $domain); $redis->hDel('DKIM_PUB_KEYS', $domain);
$valkey->hDel('DKIM_PRIV_KEYS', $selector . '.' . $domain); $redis->hDel('DKIM_PRIV_KEYS', $selector . '.' . $domain);
$valkey->hDel('DKIM_SELECTORS', $domain); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
continue; continue;
} }
+4 -4
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 $valkey; global $redis;
$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
@@ -63,7 +63,7 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
break; break;
case 'info': case 'info':
if (empty($service_name)) { if (empty($service_name)) {
curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json'); curl_setopt($curl, CURLOPT_URL, 'https://dockerapi:443/containers/json?all=true');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 0); curl_setopt($curl, CURLOPT_POST, 0);
curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT); curl_setopt($curl, CURLOPT_TIMEOUT, $DOCKER_TIMEOUT);
@@ -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
); );
$valkey->publish("MC_CHANNEL", json_encode($request)); $redis->publish("MC_CHANNEL", json_encode($request));
return true; return true;
break; break;
} }
+37 -18
View File
@@ -195,17 +195,23 @@ function domain_admin($_action, $_data = null) {
)); ));
} }
} }
$force_tfa = intval($_data['force_tfa'] ?? 0) ? 1 : 0;
$force_pw_update = intval($_data['force_pw_update'] ?? 0) ? 1 : 0;
if (!empty($password)) { if (!empty($password)) {
if (password_check($password, $password2) !== true) { if (password_check($password, $password2) !== true) {
return false; return false;
} }
$password_hashed = hash_password($password); $password_hashed = hash_password($password);
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed,
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':password_hashed' => $password_hashed, ':password_hashed' => $password_hashed,
':username_new' => $username_new, ':username_new' => $username_new,
':username' => $username, ':username' => $username,
':active' => $active ':active' => $active,
':force_tfa' => strval($force_tfa),
':force_pw_update' => strval($force_pw_update)
)); ));
if (isset($_data['disable_tfa'])) { if (isset($_data['disable_tfa'])) {
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
@@ -217,11 +223,15 @@ function domain_admin($_action, $_data = null) {
} }
} }
else { else {
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active,
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_tfa', :force_tfa, '$.force_pw_update', :force_pw_update)
WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':username_new' => $username_new, ':username_new' => $username_new,
':username' => $username, ':username' => $username,
':active' => $active ':active' => $active,
':force_tfa' => strval($force_tfa),
':force_pw_update' => strval($force_pw_update)
)); ));
if (isset($_data['disable_tfa'])) { if (isset($_data['disable_tfa'])) {
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
@@ -244,31 +254,37 @@ function domain_admin($_action, $_data = null) {
// Can only edit itself // Can only edit itself
elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") { elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
$username = $_SESSION['mailcow_cc_username']; $username = $_SESSION['mailcow_cc_username'];
$password_old = $_data['user_old_pass']; $password_old = $_data['user_old_pass'] ?? '';
$password_new = $_data['user_new_pass']; $password_new = $_data['user_new_pass'];
$password_new2 = $_data['user_new_pass2']; $password_new2 = $_data['user_new_pass2'];
$stmt = $pdo->prepare("SELECT `password` FROM `admin` // Only verify old password if this is NOT a forced password update
WHERE `username` = :user"); if (empty($_SESSION['pending_pw_update'])) {
$stmt->execute(array(':user' => $username)); $stmt = $pdo->prepare("SELECT `password` FROM `admin`
$row = $stmt->fetch(PDO::FETCH_ASSOC); WHERE `username` = :user");
if (!verify_hash($row['password'], $password_old)) { $stmt->execute(array(':user' => $username));
$_SESSION['return'][] = array( $row = $stmt->fetch(PDO::FETCH_ASSOC);
'type' => 'danger', if (!verify_hash($row['password'], $password_old)) {
'log' => array(__FUNCTION__, $_action, $_data_log), $_SESSION['return'][] = array(
'msg' => 'access_denied' 'type' => 'danger',
); 'log' => array(__FUNCTION__, $_action, $_data_log),
return false; 'msg' => 'access_denied'
);
return false;
}
} }
if (password_check($password_new, $password_new2) !== true) { if (password_check($password_new, $password_new2) !== true) {
return false; return false;
} }
$password_hashed = hash_password($password_new); $password_hashed = hash_password($password_new);
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username"); $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed,
`attributes` = JSON_SET(COALESCE(`attributes`, '{}'), '$.force_pw_update', '0')
WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':password_hashed' => $password_hashed, ':password_hashed' => $password_hashed,
':username' => $username ':username' => $username
)); ));
unset($_SESSION['pending_pw_update']);
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
@@ -360,9 +376,11 @@ function domain_admin($_action, $_data = null) {
`tfa`.`active` AS `tfa_active`, `tfa`.`active` AS `tfa_active`,
`domain_admins`.`username`, `domain_admins`.`username`,
`domain_admins`.`created`, `domain_admins`.`created`,
`domain_admins`.`active` AS `active` `domain_admins`.`active` AS `active`,
`admin`.`attributes` AS `attributes`
FROM `domain_admins` FROM `domain_admins`
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username` LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
LEFT OUTER JOIN `admin` ON `admin`.`username`=`domain_admins`.`username`
WHERE `domain_admins`.`username`= :domain_admin"); WHERE `domain_admins`.`username`= :domain_admin");
$stmt->execute(array( $stmt->execute(array(
':domain_admin' => $_data ':domain_admin' => $_data
@@ -377,6 +395,7 @@ function domain_admin($_action, $_data = null) {
$domainadmindata['active'] = $row['active']; $domainadmindata['active'] = $row['active'];
$domainadmindata['active_int'] = $row['active']; $domainadmindata['active_int'] = $row['active'];
$domainadmindata['created'] = $row['created']; $domainadmindata['created'] = $row['created'];
$domainadmindata['attributes'] = json_decode($row['attributes'], true) ?? array('force_tfa' => '0', 'force_pw_update' => '0');
// GET SELECTED // GET SELECTED
$stmt = $pdo->prepare("SELECT `domain` FROM `domain` $stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain` IN ( WHERE `domain` IN (
+39 -39
View File
@@ -1,6 +1,6 @@
<?php <?php
function fail2ban($_action, $_data = null, $_extra = null) { function fail2ban($_action, $_data = null, $_extra = null) {
global $valkey; global $redis;
$_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($valkey->Get('F2B_OPTIONS'), true); $f2b_options = json_decode($redis->Get('F2B_OPTIONS'), true);
$f2b_options['regex'] = json_decode($valkey->Get('F2B_REGEX'), true); $f2b_options['regex'] = json_decode($redis->Get('F2B_REGEX'), true);
$wl = $valkey->hGetAll('F2B_WHITELIST'); $wl = $redis->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 = $valkey->hGetAll('F2B_BLACKLIST'); $bl = $redis->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 = $valkey->hGetAll('F2B_PERM_BANS'); $pb = $redis->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 = $valkey->hGetAll('F2B_ACTIVE_BANS'); $active_bans = $redis->hGetAll('F2B_ACTIVE_BANS');
$queue_unban = $valkey->hGetAll('F2B_QUEUE_UNBAN'); $queue_unban = $redis->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('valkey_error', $e) 'msg' => array('redis_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 {
$valkey->Del('F2B_REGEX'); $redis->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('valkey_error', $e) 'msg' => array('redis_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($valkey->Get('F2B_REGEX'), true); $regex_result = json_decode($redis->Get('F2B_REGEX'), true);
while (empty($regex_result) && $fail_count < 10) { while (empty($regex_result) && $fail_count < 10) {
$regex_result = json_decode($valkey->Get('F2B_REGEX'), true); $regex_result = json_decode($redis->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)) {
$valkey->Set('F2B_REGEX', json_encode($regex_array, JSON_UNESCAPED_SLASHES)); $redis->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 {
$valkey->hSet('F2B_QUEUE_UNBAN', $network, 1); $redis->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('valkey_error', $e) 'msg' => array('redis_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 {
$valkey->hSet('F2B_WHITELIST', $network, 1); $redis->hSet('F2B_WHITELIST', $network, 1);
$valkey->hDel('F2B_BLACKLIST', $network, 1); $redis->hDel('F2B_BLACKLIST', $network, 1);
$valkey->hSet('F2B_QUEUE_UNBAN', $network, 1); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
continue; continue;
} }
@@ -204,15 +204,15 @@ function fail2ban($_action, $_data = null, $_extra = null) {
getenv('IPV6_NETWORK') getenv('IPV6_NETWORK')
))) { ))) {
try { try {
$valkey->hSet('F2B_BLACKLIST', $network, 1); $redis->hSet('F2B_BLACKLIST', $network, 1);
$valkey->hDel('F2B_WHITELIST', $network, 1); $redis->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('valkey_error', $e) 'msg' => array('redis_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 {
$valkey->Set('F2B_OPTIONS', json_encode($f2b_options)); $redis->Set('F2B_OPTIONS', json_encode($f2b_options));
$valkey->Del('F2B_WHITELIST'); $redis->Del('F2B_WHITELIST');
$valkey->Del('F2B_BLACKLIST'); $redis->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)) {
$valkey->hSet('F2B_WHITELIST', $wl_item, 1); $redis->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')
))) { ))) {
$valkey->hSet('F2B_BLACKLIST', $bl_item, 1); $redis->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('valkey_error', $e) 'msg' => array('redis_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($valkey->Get('F2B_OPTIONS'), true); $f2b_options = json_decode($redis->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('valkey_error', $e) 'msg' => array('redis_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 = $valkey->hKeys('F2B_BLACKLIST'); $bl = $redis->hKeys('F2B_BLACKLIST');
$active_bans = $valkey->hKeys('F2B_ACTIVE_BANS'); $active_bans = $redis->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('valkey_error', $e) 'msg' => array('redis_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 {
$valkey->Set('F2B_OPTIONS', json_encode($f2b_options)); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
+18 -18
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 $valkey; global $redis;
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 {
$valkey->hSet('WHITELISTED_FWD_HOST', $host, $source); $redis->hSet('WHITELISTED_FWD_HOST', $host, $source);
if ($filter_spam == 0) { if ($filter_spam == 0) {
$valkey->hSet('KEEP_SPAM', $host, 1); $redis->hSet('KEEP_SPAM', $host, 1);
} }
elseif ($valkey->hGet('KEEP_SPAM', $host)) { elseif ($redis->hGet('KEEP_SPAM', $host)) {
$valkey->hDel('KEEP_SPAM', $host); $redis->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('valkey_error', $e) 'msg' => array('redis_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) {
$valkey->hSet('KEEP_SPAM', $fwdhost, 1); $redis->hSet('KEEP_SPAM', $fwdhost, 1);
} }
else { else {
$valkey->hDel('KEEP_SPAM', $fwdhost); $redis->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('valkey_error', $e) 'msg' => array('redis_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 {
$valkey->hDel('WHITELISTED_FWD_HOST', $host); $redis->hDel('WHITELISTED_FWD_HOST', $host);
$valkey->hDel('KEEP_SPAM', $host); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
continue; continue;
} }
@@ -135,10 +135,10 @@ function fwdhost($_action, $_data = null) {
} }
$fwdhostsdata = array(); $fwdhostsdata = array();
try { try {
$fwd_hosts = $valkey->hGetAll('WHITELISTED_FWD_HOST'); $fwd_hosts = $redis->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 = ($valkey->hGet('KEEP_SPAM', $fwd_host)) ? "yes" : "no"; $keep_spam = ($redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -163,17 +163,17 @@ function fwdhost($_action, $_data = null) {
return false; return false;
} }
try { try {
if ($source = $valkey->hGet('WHITELISTED_FWD_HOST', $_data)) { if ($source = $redis->hGet('WHITELISTED_FWD_HOST', $_data)) {
$fwdhostdetails['host'] = $_data; $fwdhostdetails['host'] = $_data;
$fwdhostdetails['source'] = $source; $fwdhostdetails['source'] = $source;
$fwdhostdetails['keep_spam'] = ($valkey->hGet('KEEP_SPAM', $_data)) ? "yes" : "no"; $fwdhostdetails['keep_spam'] = ($redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
+280 -103
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 $valkey; global $redis;
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 {
$valkey->hMSet('PASSWD_POLICY', [ $redis->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('valkey_error', $e) 'msg' => array('redis_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 = $valkey->hGet('PASSWD_POLICY', 'length'); $length = $redis->hGet('PASSWD_POLICY', 'length');
$chars = $valkey->hGet('PASSWD_POLICY', 'chars'); $chars = $redis->hGet('PASSWD_POLICY', 'chars');
$special_chars = $valkey->hGet('PASSWD_POLICY', 'special_chars'); $special_chars = $redis->hGet('PASSWD_POLICY', 'special_chars');
$lowerupper = $valkey->hGet('PASSWD_POLICY', 'lowerupper'); $lowerupper = $redis->hGet('PASSWD_POLICY', 'lowerupper');
$numbers = $valkey->hGet('PASSWD_POLICY', 'numbers'); $numbers = $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -205,6 +205,42 @@ function password_complexity($_action, $_data = null) {
break; break;
} }
} }
function password_generate(){
$password_complexity = password_complexity('get');
$min_length = max(16, intval($password_complexity['length']));
$lowercase = range('a', 'z');
$uppercase = range('A', 'Z');
$digits = range(0, 9);
$special_chars = str_split('!@#$%^&*()?=');
$password = [
$lowercase[random_int(0, count($lowercase) - 1)],
$uppercase[random_int(0, count($uppercase) - 1)],
$digits[random_int(0, count($digits) - 1)],
$special_chars[random_int(0, count($special_chars) - 1)],
];
$all = array_merge($lowercase, $uppercase, $digits, $special_chars);
while (count($password) < $min_length) {
$password[] = $all[random_int(0, count($all) - 1)];
}
// Cryptographically secure shuffle using Fisher-Yates algorithm
$count = count($password);
for ($i = $count - 1; $i > 0; $i--) {
$j = random_int(0, $i);
$temp = $password[$i];
$password[$i] = $password[$j];
$password[$j] = $temp;
}
return implode('', $password);
}
function password_check($password1, $password2) { function password_check($password1, $password2) {
$password_complexity = password_complexity('get'); $password_complexity = password_complexity('get');
@@ -253,7 +289,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 $valkey; global $redis;
$sasl_limit_days = intval($sasl_limit_days); $sasl_limit_days = intval($sasl_limit_days);
switch ($action) { switch ($action) {
case 'get': case 'get':
@@ -272,13 +308,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'] = $valkey->hGet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip']); $sasl[$k]['location'] = $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -294,13 +330,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 {
$valkey->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['shortcountry']); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
curl_close($curl); curl_close($curl);
return false; return false;
@@ -814,6 +850,32 @@ function verify_hash($hash, $password) {
$hash = $components[4]; $hash = $components[4];
return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash); return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash);
case "PBKDF2-SHA512":
// Handle FreeIPA-style hash: {PBKDF2-SHA512}10000$<base64_salt>$<base64_hash>
$components = explode('$', $hash);
if (count($components) !== 3) return false;
// 1st part: iteration count (integer)
$iterations = intval($components[0]);
if ($iterations <= 0) return false;
// 2nd part: salt (base64-encoded)
$salt = $components[1];
// 3rd part: hash (base64-encoded)
$stored_hash_b64 = $components[2];
// Decode salt and hash from base64
$salt_bin = base64_decode($salt, true);
$hash_bin = base64_decode($stored_hash_b64, true);
if ($salt_bin === false || $hash_bin === false) return false;
// Get length of hash in bytes
$hash_len = strlen($hash_bin);
if ($hash_len === 0) return false;
// Calculate PBKDF2-SHA512 hash for provided password
$test_hash = hash_pbkdf2('sha512', $password, $salt_bin, $iterations, $hash_len, true);
return hash_equals($hash_bin, $test_hash);
case "PLAIN-MD4": case "PLAIN-MD4":
return hash_equals(hash('md4', $password), $hash); return hash_equals(hash('md4', $password), $hash);
@@ -971,20 +1033,24 @@ function edit_user_account($_data) {
} }
// edit password // edit password
if (!empty($password_old) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) { $is_forced_pw_update = !empty($_SESSION['pending_pw_update']);
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox` if (((!empty($password_old) || $is_forced_pw_update) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2']))) {
WHERE `kind` NOT REGEXP 'location|thing|group' // Only verify old password if this is NOT a forced password update
AND `username` = :user AND authsource = 'mailcow'"); if (!$is_forced_pw_update) {
$stmt->execute(array(':user' => $username)); $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
$row = $stmt->fetch(PDO::FETCH_ASSOC); WHERE `kind` NOT REGEXP 'location|thing|group'
AND `username` = :user AND authsource = 'mailcow'");
$stmt->execute(array(':user' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!verify_hash($row['password'], $password_old)) { if (!verify_hash($row['password'], $password_old)) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log), 'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied' 'msg' => 'access_denied'
); );
return false; return false;
}
} }
$password_new = $_data['user_new_pass']; $password_new = $_data['user_new_pass'];
@@ -1006,7 +1072,7 @@ function edit_user_account($_data) {
update_sogo_static_view(); update_sogo_static_view();
} }
// edit password recovery email // edit password recovery email
elseif (isset($pw_recovery_email)) { elseif (!empty($password_old) && isset($pw_recovery_email)) {
if (!isset($_SESSION['acl']['pw_reset']) || $_SESSION['acl']['pw_reset'] != "1" ) { if (!isset($_SESSION['acl']['pw_reset']) || $_SESSION['acl']['pw_reset'] != "1" ) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@@ -1016,6 +1082,21 @@ function edit_user_account($_data) {
return false; return false;
} }
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
WHERE `kind` NOT REGEXP 'location|thing|group'
AND `username` = :user AND authsource = 'mailcow'");
$stmt->execute(array(':user' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!verify_hash($row['password'], $password_old)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$pw_recovery_email = (!filter_var($pw_recovery_email, FILTER_VALIDATE_EMAIL)) ? '' : $pw_recovery_email; $pw_recovery_email = (!filter_var($pw_recovery_email, FILTER_VALIDATE_EMAIL)) ? '' : $pw_recovery_email;
$stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email) $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email)
WHERE `username` = :username AND authsource = 'mailcow'"); WHERE `username` = :username AND authsource = 'mailcow'");
@@ -1133,50 +1214,52 @@ function set_tfa($_data) {
global $iam_settings; global $iam_settings;
$_data_log = $_data; $_data_log = $_data;
$access_denied = null;
!isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*'; !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*';
$username = $_SESSION['mailcow_cc_username'];
// check for empty user and role // skip password check if this is a forced TFA enrollment after login
if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true; if (!empty($_SESSION['pending_tfa_setup'])) {
$username = $_SESSION['mailcow_cc_username'];
// check admin confirm password if (empty($username) || !isset($_SESSION['mailcow_cc_role'])) {
if ($access_denied === null) { $_SESSION['return'][] = array('type' => 'danger', 'log' => array(__FUNCTION__, $_data_log), 'msg' => 'access_denied');
$stmt = $pdo->prepare("SELECT `password` FROM `admin` return false;
WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
else $access_denied = false;
} }
} } else {
$username = $_SESSION['mailcow_cc_username'];
$access_denied = null;
// check mailbox confirm password if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
if ($access_denied === null) {
$stmt = $pdo->prepare("SELECT `password`, `authsource` FROM `mailbox` // check admin password
WHERE `username` = :username"); if ($access_denied === null) {
$stmt->execute(array(':username' => $username)); $stmt = $pdo->prepare("SELECT `password` FROM `admin` WHERE `username` = :username");
$row = $stmt->fetch(PDO::FETCH_ASSOC); $stmt->execute(array(':username' => $username));
if ($row) { $row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row['authsource'] == 'ldap'){ if ($row) {
if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true;
else $access_denied = false;
} else {
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true; if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
else $access_denied = false; else $access_denied = false;
} }
} }
}
// set access_denied error // check mailbox password
if ($access_denied){ if ($access_denied === null) {
$_SESSION['return'][] = array( $stmt = $pdo->prepare("SELECT `password`, `authsource` FROM `mailbox` WHERE `username` = :username");
'type' => 'danger', $stmt->execute(array(':username' => $username));
'log' => array(__FUNCTION__, $_data_log), $row = $stmt->fetch(PDO::FETCH_ASSOC);
'msg' => 'access_denied' if ($row) {
); if ($row['authsource'] == 'ldap'){
return false; if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true;
else $access_denied = false;
} else {
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
else $access_denied = false;
}
}
}
if ($access_denied) {
$_SESSION['return'][] = array('type' => 'danger', 'log' => array(__FUNCTION__, $_data_log), 'msg' => 'access_denied');
return false;
}
} }
switch ($_data["tfa_method"]) { switch ($_data["tfa_method"]) {
@@ -1229,6 +1312,7 @@ function set_tfa($_data) {
); );
return false; return false;
} }
unset($_SESSION['pending_tfa_setup']);
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_data_log), 'log' => array(__FUNCTION__, $_data_log),
@@ -1242,6 +1326,7 @@ function set_tfa($_data) {
//$stmt->execute(array(':username' => $username)); //$stmt->execute(array(':username' => $username));
$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')"); $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')");
$stmt->execute(array($username, $key_id, $_POST['totp_secret'])); $stmt->execute(array($username, $key_id, $_POST['totp_secret']));
unset($_SESSION['pending_tfa_setup']);
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_data_log), 'log' => array(__FUNCTION__, $_data_log),
@@ -1270,6 +1355,7 @@ function set_tfa($_data) {
0 0
)); ));
unset($_SESSION['pending_tfa_setup']);
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_data_log), 'log' => array(__FUNCTION__, $_data_log),
@@ -1277,6 +1363,25 @@ function set_tfa($_data) {
); );
break; break;
case "none": case "none":
// Block TFA removal if force_tfa policy is active
$is_forced_tfa = false;
if ($_SESSION['mailcow_cc_role'] === 'user') {
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
$stmt_check->execute(array($username));
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
} else {
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
$stmt_check->execute(array($username));
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
}
if ($is_forced_tfa) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'tfa_removal_blocked'
);
return false;
}
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username"); $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
$stmt->execute(array(':username' => $username)); $stmt->execute(array(':username' => $username));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -1529,6 +1634,26 @@ function unset_tfa_key($_data) {
return false; return false;
} }
// Block key removal if force_tfa policy is active
$is_forced_tfa = false;
if ($_SESSION['mailcow_cc_role'] === 'user') {
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `mailbox` WHERE `username` = ?");
$stmt_check->execute(array($username));
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
} else {
$stmt_check = $pdo->prepare("SELECT JSON_EXTRACT(`attributes`, '$.force_tfa') FROM `admin` WHERE `username` = ?");
$stmt_check->execute(array($username));
$is_forced_tfa = ($stmt_check->fetchColumn() == '1');
}
if ($is_forced_tfa) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'tfa_removal_blocked'
);
return false;
}
// check if it's last key // check if it's last key
$stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa` $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
WHERE `username` = :username AND `active` = '1'"); WHERE `username` = :username AND `active` = '1'");
@@ -1561,6 +1686,15 @@ function unset_tfa_key($_data) {
return false; return false;
} }
} }
function tfa_exists($username) {
global $pdo;
if (empty($username)) {
return false;
}
$stmt = $pdo->prepare("SELECT COUNT(*) as count FROM `tfa` WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
return $stmt->fetch(PDO::FETCH_ASSOC)['count'] > 0;
}
function get_tfa($username = null, $id = null) { function get_tfa($username = null, $id = null) {
global $pdo; global $pdo;
if (empty($username) && isset($_SESSION['mailcow_cc_username'])) { if (empty($username) && isset($_SESSION['mailcow_cc_username'])) {
@@ -1999,7 +2133,7 @@ function admin_api($access, $action, $data = null) {
} }
function license($action, $data = null) { function license($action, $data = null) {
global $pdo; global $pdo;
global $valkey; global $redis;
global $lang; global $lang;
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -2049,13 +2183,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
$valkey->Set('LICENSE_STATUS_CACHE', json_encode($_SESSION['gal'])); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -2136,7 +2270,7 @@ function rspamd_ui($action, $data = null) {
} }
} }
function cors($action, $data = null) { function cors($action, $data = null) {
global $valkey; global $redis;
switch ($action) { switch ($action) {
case "edit": case "edit":
@@ -2177,7 +2311,7 @@ function cors($action, $data = null) {
} }
try { try {
$valkey->hMSet('CORS_SETTINGS', array( $redis->hMSet('CORS_SETTINGS', array(
'allowed_origins' => implode(', ', $allowed_origins), 'allowed_origins' => implode(', ', $allowed_origins),
'allowed_methods' => implode(', ', $allowed_methods) 'allowed_methods' => implode(', ', $allowed_methods)
)); ));
@@ -2185,7 +2319,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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -2199,12 +2333,12 @@ function cors($action, $data = null) {
break; break;
case "get": case "get":
try { try {
$cors_settings = $valkey->hMGet('CORS_SETTINGS', array('allowed_origins', 'allowed_methods')); $cors_settings = $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
} }
@@ -2967,7 +3101,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 $valkey; global $redis;
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;
@@ -3203,10 +3337,10 @@ function reset_password($action, $data = null) {
$type = $data; $type = $data;
try { try {
$settings['from'] = $valkey->Get('PW_RESET_FROM'); $settings['from'] = $redis->Get('PW_RESET_FROM');
$settings['subject'] = $valkey->Get('PW_RESET_SUBJ'); $settings['subject'] = $redis->Get('PW_RESET_SUBJ');
$settings['html_tmpl'] = $valkey->Get('PW_RESET_HTML'); $settings['html_tmpl'] = $redis->Get('PW_RESET_HTML');
$settings['text_tmpl'] = $valkey->Get('PW_RESET_TEXT'); $settings['text_tmpl'] = $redis->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");
@@ -3221,7 +3355,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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -3324,16 +3458,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 {
$valkey->Set('PW_RESET_FROM', $from); $redis->Set('PW_RESET_FROM', $from);
$valkey->Set('PW_RESET_SUBJ', $subject); $redis->Set('PW_RESET_SUBJ', $subject);
$valkey->Set('PW_RESET_HTML', $html); $redis->Set('PW_RESET_HTML', $html);
$valkey->Set('PW_RESET_TEXT', $text); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -3363,6 +3497,49 @@ function set_user_loggedin_session($user) {
unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_methods']); unset($_SESSION['pending_tfa_methods']);
} }
function protect_route($allowed_roles = ['admin', 'domainadmin', 'user'], $redirects = []) {
// Check if user is authenticated
if (!isset($_SESSION['mailcow_cc_role'])) {
if (isset($redirects['unauthenticated'])) {
header('Location: ' . $redirects['unauthenticated']);
} else {
header('Location: /');
}
exit();
}
// Check for pending actions (2FA setup, password update)
if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) {
$pending_redirect = '/';
if ($_SESSION['mailcow_cc_role'] === 'admin') {
$pending_redirect = '/admin';
} elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') {
$pending_redirect = '/domainadmin';
}
header('Location: ' . $pending_redirect);
exit();
}
// Check if user's role is in the allowed roles for the route
if (!in_array($_SESSION['mailcow_cc_role'], $allowed_roles)) {
if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
header('Location: /admin/dashboard');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') {
header('Location: /domainadmin/mailbox');
exit();
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {
header('Location: /user');
exit();
}
else {
header('Location: /');
exit();
}
}
}
function get_logs($application, $lines = false) { function get_logs($application, $lines = false) {
if ($lines === false) { if ($lines === false) {
$lines = $GLOBALS['LOG_LINES'] - 1; $lines = $GLOBALS['LOG_LINES'] - 1;
@@ -3376,7 +3553,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 $valkey; global $redis;
global $pdo; global $pdo;
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
return false; return false;
@@ -3425,10 +3602,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 = $valkey->lRange('DOVECOT_MAILLOG', $from - 1, $to - 1); $data = $redis->lRange('DOVECOT_MAILLOG', $from - 1, $to - 1);
} }
else { else {
$data = $valkey->lRange('DOVECOT_MAILLOG', 0, $lines); $data = $redis->lRange('DOVECOT_MAILLOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3439,10 +3616,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 = $valkey->lRange('CRON_LOG', $from - 1, $to - 1); $data = $redis->lRange('CRON_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $valkey->lRange('CRON_LOG', 0, $lines); $data = $redis->lRange('CRON_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3453,10 +3630,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 = $valkey->lRange('POSTFIX_MAILLOG', $from - 1, $to - 1); $data = $redis->lRange('POSTFIX_MAILLOG', $from - 1, $to - 1);
} }
else { else {
$data = $valkey->lRange('POSTFIX_MAILLOG', 0, $lines); $data = $redis->lRange('POSTFIX_MAILLOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3467,10 +3644,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 = $valkey->lRange('SOGO_LOG', $from - 1, $to - 1); $data = $redis->lRange('SOGO_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $valkey->lRange('SOGO_LOG', 0, $lines); $data = $redis->lRange('SOGO_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3481,10 +3658,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 = $valkey->lRange('WATCHDOG_LOG', $from - 1, $to - 1); $data = $redis->lRange('WATCHDOG_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $valkey->lRange('WATCHDOG_LOG', 0, $lines); $data = $redis->lRange('WATCHDOG_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3495,10 +3672,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 = $valkey->lRange('ACME_LOG', $from - 1, $to - 1); $data = $redis->lRange('ACME_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $valkey->lRange('ACME_LOG', 0, $lines); $data = $redis->lRange('ACME_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3509,10 +3686,10 @@ function get_logs($application, $lines = false) {
} }
if ($application == "ratelimited") { if ($application == "ratelimited") {
if (isset($from) && isset($to)) { if (isset($from) && isset($to)) {
$data = $valkey->lRange('RL_LOG', $from - 1, $to - 1); $data = $redis->lRange('RL_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $valkey->lRange('RL_LOG', 0, $lines); $data = $redis->lRange('RL_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3523,10 +3700,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 = $valkey->lRange('API_LOG', $from - 1, $to - 1); $data = $redis->lRange('API_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $valkey->lRange('API_LOG', 0, $lines); $data = $redis->lRange('API_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3537,10 +3714,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 = $valkey->lRange('NETFILTER_LOG', $from - 1, $to - 1); $data = $redis->lRange('NETFILTER_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $valkey->lRange('NETFILTER_LOG', 0, $lines); $data = $redis->lRange('NETFILTER_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
@@ -3551,10 +3728,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 = $valkey->lRange('AUTODISCOVER_LOG', $from - 1, $to - 1); $data = $redis->lRange('AUTODISCOVER_LOG', $from - 1, $to - 1);
} }
else { else {
$data = $valkey->lRange('AUTODISCOVER_LOG', 0, $lines); $data = $redis->lRange('AUTODISCOVER_LOG', 0, $lines);
} }
if ($data) { if ($data) {
foreach ($data as $json_line) { foreach ($data as $json_line) {
+197 -75
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 $valkey; global $redis;
global $lang; global $lang;
global $MAILBOX_DEFAULT_ATTRIBUTES; global $MAILBOX_DEFAULT_ATTRIBUTES;
global $iam_settings; global $iam_settings;
@@ -9,6 +9,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data_log = $_data; $_data_log = $_data;
!isset($_data_log['password']) ?: $_data_log['password'] = '*'; !isset($_data_log['password']) ?: $_data_log['password'] = '*';
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*'; !isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
// Track mailboxes affected by alias operations for incremental SOGo updates
$update_sogo_mailboxes = array();
switch ($_action) { switch ($_action) {
case 'add': case 'add':
switch ($_type) { switch ($_type) {
@@ -49,6 +53,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
// Default to 1 yr // Default to 1 yr
$_data["validity"] = 8760; $_data["validity"] = 8760;
} }
if (isset($_data["permanent"]) && filter_var($_data["permanent"], FILTER_VALIDATE_BOOL)) {
$permanent = 1;
}
else {
$permanent = 0;
}
$domain = $_data['domain']; $domain = $_data['domain'];
$description = $_data['description']; $description = $_data['description'];
$valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain']; $valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain'];
@@ -65,13 +75,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
return false; return false;
} }
$validity = strtotime("+" . $_data["validity"] . " hour"); $validity = strtotime("+" . $_data["validity"] . " hour");
$stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`) VALUES $stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`, `permanent`) VALUES
(:address, :description, :goto, :validity)"); (:address, :description, :goto, :validity, :permanent)");
$stmt->execute(array( $stmt->execute(array(
':address' => readable_random_string(rand(rand(3, 9), rand(3, 9))) . '.' . readable_random_string(rand(rand(3, 9), rand(3, 9))) . '@' . $domain, ':address' => readable_random_string(rand(rand(3, 9), rand(3, 9))) . '.' . readable_random_string(rand(rand(3, 9), rand(3, 9))) . '@' . $domain,
':description' => $description, ':description' => $description,
':goto' => $username, ':goto' => $username,
':validity' => $validity ':validity' => $validity,
':permanent' => $permanent
)); ));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
@@ -628,13 +639,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
try { try {
$valkey->hSet('DOMAIN_MAP', $domain, 1); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -646,7 +657,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($valkey->hGet('DKIM_SELECTORS', $domain))) { if (!empty($redis->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),
@@ -688,6 +699,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto'])); $gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto']));
$internal = intval($_data['internal']); $internal = intval($_data['internal']);
$active = intval($_data['active']); $active = intval($_data['active']);
$sender_allowed = intval($_data['sender_allowed']);
$sogo_visible = intval($_data['sogo_visible']); $sogo_visible = intval($_data['sogo_visible']);
$goto_null = intval($_data['goto_null']); $goto_null = intval($_data['goto_null']);
$goto_spam = intval($_data['goto_spam']); $goto_spam = intval($_data['goto_spam']);
@@ -843,8 +855,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`, `internal`, `active`) $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `sender_allowed`, `active`)
VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :active)"); VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :sender_allowed, :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,
@@ -855,6 +867,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':domain' => $domain, ':domain' => $domain,
':sogo_visible' => $sogo_visible, ':sogo_visible' => $sogo_visible,
':internal' => $internal, ':internal' => $internal,
':sender_allowed' => $sender_allowed,
':active' => $active ':active' => $active
)); ));
} }
@@ -867,6 +880,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':domain' => $domain, ':domain' => $domain,
':sogo_visible' => $sogo_visible, ':sogo_visible' => $sogo_visible,
':internal' => $internal, ':internal' => $internal,
':sender_allowed' => $sender_allowed,
':active' => $active ':active' => $active
)); ));
} }
@@ -876,6 +890,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('alias_added', $address, $id) 'msg' => array('alias_added', $address, $id)
); );
// Track affected mailboxes for SOGo update
if (!empty($goto)) {
$gotos = array_map('trim', explode(',', $goto));
foreach ($gotos as $g) {
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
$update_sogo_mailboxes[] = $g;
}
}
}
} }
break; break;
case 'alias_domain': case 'alias_domain':
@@ -974,13 +999,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':active' => $active ':active' => $active
)); ));
try { try {
$valkey->hSet('DOMAIN_MAP', $alias_domain, 1); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -988,7 +1013,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($valkey->hGet('DKIM_SELECTORS', $alias_domain))) { if (!empty($redis->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),
@@ -1068,9 +1093,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$_data['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$_data['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
} }
$active = (isset($_data['active'])) ? intval($_data['active']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['active']); $active = (isset($_data['active'])) ? intval($_data['active']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['active']);
$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']); $force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
$force_tfa = (isset($_data['force_tfa'])) ? intval($_data['force_tfa']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_tfa']);
$tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']); $tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
$tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']); $tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
$sogo_access = (isset($_data['sogo_access'])) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']); $sogo_access = (isset($_data['sogo_access'])) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
@@ -1078,6 +1106,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$pop3_access = (isset($_data['pop3_access'])) ? intval($_data['pop3_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']); $pop3_access = (isset($_data['pop3_access'])) ? intval($_data['pop3_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
$smtp_access = (isset($_data['smtp_access'])) ? intval($_data['smtp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']); $smtp_access = (isset($_data['smtp_access'])) ? intval($_data['smtp_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
$sieve_access = (isset($_data['sieve_access'])) ? intval($_data['sieve_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']); $sieve_access = (isset($_data['sieve_access'])) ? intval($_data['sieve_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
$eas_access = (isset($_data['eas_access'])) ? intval($_data['eas_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['eas_access']);
$dav_access = (isset($_data['dav_access'])) ? intval($_data['dav_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['dav_access']);
$relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : 0; $relayhost = (isset($_data['relayhost'])) ? intval($_data['relayhost']) : 0;
$quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']); $quarantine_notification = (isset($_data['quarantine_notification'])) ? strval($_data['quarantine_notification']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']);
$quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']); $quarantine_category = (isset($_data['quarantine_category'])) ? strval($_data['quarantine_category']) : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']);
@@ -1085,10 +1115,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : ''; $attribute_hash = (!empty($_data['attribute_hash'])) ? $_data['attribute_hash'] : '';
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){ if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
$force_pw_update = 0; $force_pw_update = 0;
$force_tfa = 0;
} }
$mailbox_attrs = json_encode( $mailbox_attrs = json_encode(
array( array(
'force_pw_update' => strval($force_pw_update), 'force_pw_update' => strval($force_pw_update),
'force_tfa' => strval($force_tfa),
'tls_enforce_in' => strval($tls_enforce_in), 'tls_enforce_in' => strval($tls_enforce_in),
'tls_enforce_out' => strval($tls_enforce_out), 'tls_enforce_out' => strval($tls_enforce_out),
'sogo_access' => strval($sogo_access), 'sogo_access' => strval($sogo_access),
@@ -1096,6 +1128,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'pop3_access' => strval($pop3_access), 'pop3_access' => strval($pop3_access),
'smtp_access' => strval($smtp_access), 'smtp_access' => strval($smtp_access),
'sieve_access' => strval($sieve_access), 'sieve_access' => strval($sieve_access),
'eas_access' => strval($eas_access),
'dav_access' => strval($dav_access),
'relayhost' => strval($relayhost), 'relayhost' => strval($relayhost),
'passwd_update' => time(), 'passwd_update' => time(),
'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']), 'mailbox_format' => strval($MAILBOX_DEFAULT_ATTRIBUTES['mailbox_format']),
@@ -1349,15 +1383,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
), $_extra); ), $_extra);
} }
try { // Track affected mailboxes for SOGo update
update_sogo_static_view($username); $update_sogo_mailboxes[] = $username;
} catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => $e->getMessage()
);
}
$_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),
@@ -1588,6 +1615,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('resource_added', htmlspecialchars($name)) 'msg' => array('resource_added', htmlspecialchars($name))
); );
// Track affected mailboxes for SOGo update
$update_sogo_mailboxes[] = $name;
break; break;
case 'domain_templates': case 'domain_templates':
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
@@ -1704,6 +1734,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s"; $attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s";
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : ""; $attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : "";
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']); $attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
$attr["force_tfa"] = isset($_data['force_tfa']) ? intval($_data['force_tfa']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_tfa']);
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']); $attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['sogo_access']);
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : 1; $attr["active"] = isset($_data['active']) ? intval($_data['active']) : 1;
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']); $attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
@@ -1714,12 +1745,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$attr['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$attr['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
} }
else { else {
$attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']); $attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
$attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']); $attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
$attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']); $attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
$attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']); $attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
$attr['eas_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['eas_access']);
$attr['dav_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['dav_access']);
} }
if (isset($_data['acl'])) { if (isset($_data['acl'])) {
$_data['acl'] = (array)$_data['acl']; $_data['acl'] = (array)$_data['acl'];
@@ -2103,15 +2138,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
continue; continue;
} }
if (empty($_data['validity'])) { if (empty($_data['validity']) && empty($_data['permanent'])) {
continue; continue;
} }
$validity = round((int)time() + ($_data['validity'] * 3600)); if (isset($_data['permanent']) && filter_var($_data['permanent'], FILTER_VALIDATE_BOOL)) {
$stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity WHERE $permanent = 1;
$validity = 0;
}
else if (isset($_data['validity'])) {
$permanent = 0;
$validity = round((int)time() + ($_data['validity'] * 3600));
}
$stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity, `permanent` = :permanent WHERE
`address` = :address"); `address` = :address");
$stmt->execute(array( $stmt->execute(array(
':address' => $address, ':address' => $address,
':validity' => $validity ':validity' => $validity,
':permanent' => $permanent
)); ));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
@@ -2147,42 +2190,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 {
$valkey->hSet('RCPT_WANTS_SUBJECT_TAG', $username, 1); $redis->hSet('RCPT_WANTS_SUBJECT_TAG', $username, 1);
$valkey->hDel('RCPT_WANTS_SUBFOLDER_TAG', $username); $redis->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('valkey_error', $e) 'msg' => array('redis_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 {
$valkey->hSet('RCPT_WANTS_SUBFOLDER_TAG', $username, 1); $redis->hSet('RCPT_WANTS_SUBFOLDER_TAG', $username, 1);
$valkey->hDel('RCPT_WANTS_SUBJECT_TAG', $username); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
continue; continue;
} }
} }
else { else {
try { try {
$valkey->hDel('RCPT_WANTS_SUBJECT_TAG', $username); $redis->hDel('RCPT_WANTS_SUBJECT_TAG', $username);
$valkey->hDel('RCPT_WANTS_SUBFOLDER_TAG', $username); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
continue; continue;
} }
@@ -2486,6 +2529,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
if (!empty($is_now)) { if (!empty($is_now)) {
$internal = (isset($_data['internal'])) ? intval($_data['internal']) : $is_now['internal']; $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'];
$sender_allowed = (isset($_data['sender_allowed'])) ? intval($_data['sender_allowed']) : $is_now['sender_allowed'];
$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;
$goto_spam = (isset($_data['goto_spam'])) ? intval($_data['goto_spam']) : 0; $goto_spam = (isset($_data['goto_spam'])) ? intval($_data['goto_spam']) : 0;
@@ -2671,6 +2715,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`goto` = :goto, `goto` = :goto,
`sogo_visible`= :sogo_visible, `sogo_visible`= :sogo_visible,
`internal`= :internal, `internal`= :internal,
`sender_allowed`= :sender_allowed,
`active`= :active `active`= :active
WHERE `id` = :id"); WHERE `id` = :id");
$stmt->execute(array( $stmt->execute(array(
@@ -2681,6 +2726,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':goto' => $goto, ':goto' => $goto,
':sogo_visible' => $sogo_visible, ':sogo_visible' => $sogo_visible,
':internal' => $internal, ':internal' => $internal,
':sender_allowed' => $sender_allowed,
':active' => $active, ':active' => $active,
':id' => $is_now['id'] ':id' => $is_now['id']
)); ));
@@ -2690,6 +2736,28 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('alias_modified', htmlspecialchars($address)) 'msg' => array('alias_modified', htmlspecialchars($address))
); );
// Track affected mailboxes for SOGo update (both old and new goto addresses)
// Old goto: to remove alias from their view
if (!empty($is_now['goto'])) {
$old_gotos = array_map('trim', explode(',', $is_now['goto']));
foreach ($old_gotos as $g) {
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
$update_sogo_mailboxes[] = $g;
}
}
}
// New goto: to add alias to their view
if (!empty($goto)) {
$new_gotos = array_map('trim', explode(',', $goto));
foreach ($new_gotos as $g) {
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
$update_sogo_mailboxes[] = $g;
}
}
}
} }
break; break;
case 'domain': case 'domain':
@@ -3028,15 +3096,20 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $_data['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$_data['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$_data['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
} }
if (!empty($is_now)) { if (!empty($is_now)) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
(int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']); (int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
(int)$force_tfa = (isset($_data['force_tfa'])) ? intval($_data['force_tfa']) : intval($is_now['attributes']['force_tfa']);
(int)$sogo_access = (isset($_data['sogo_access']) && hasACLAccess("sogo_access")) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']); (int)$sogo_access = (isset($_data['sogo_access']) && hasACLAccess("sogo_access")) ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
(int)$imap_access = (isset($_data['imap_access']) && hasACLAccess("protocol_access")) ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']); (int)$imap_access = (isset($_data['imap_access']) && hasACLAccess("protocol_access")) ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
(int)$pop3_access = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']); (int)$pop3_access = (isset($_data['pop3_access']) && hasACLAccess("protocol_access")) ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
(int)$smtp_access = (isset($_data['smtp_access']) && hasACLAccess("protocol_access")) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']); (int)$smtp_access = (isset($_data['smtp_access']) && hasACLAccess("protocol_access")) ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
(int)$sieve_access = (isset($_data['sieve_access']) && hasACLAccess("protocol_access")) ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']); (int)$sieve_access = (isset($_data['sieve_access']) && hasACLAccess("protocol_access")) ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
(int)$eas_access = (isset($_data['eas_access']) && hasACLAccess("protocol_access")) ? intval($_data['eas_access']) : intval($is_now['attributes']['eas_access']);
(int)$dav_access = (isset($_data['dav_access']) && hasACLAccess("protocol_access")) ? intval($_data['dav_access']) : intval($is_now['attributes']['dav_access']);
(int)$relayhost = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']); (int)$relayhost = (isset($_data['relayhost']) && hasACLAccess("mailbox_relayhost")) ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
(int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576); (int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
$name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name']; $name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
@@ -3053,6 +3126,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){ if (in_array($authsource, array('keycloak', 'generic-oidc', 'ldap'))){
$force_pw_update = 0; $force_pw_update = 0;
$force_tfa = 0;
} }
$pw_recovery_email = (isset($_data['pw_recovery_email']) && $authsource == 'mailcow') ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email']; $pw_recovery_email = (isset($_data['pw_recovery_email']) && $authsource == 'mailcow') ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
} }
@@ -3170,9 +3244,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
if (isset($_data['sender_acl'])) { if (isset($_data['sender_acl'])) {
// Get sender_acl items set by admin // Get sender_acl items set by admin
$current_sender_acls = mailbox('get', 'sender_acl_handles', $username);
$sender_acl_admin = array_merge( $sender_acl_admin = array_merge(
mailbox('get', 'sender_acl_handles', $username)['sender_acl_domains']['ro'], $current_sender_acls['sender_acl_domains']['ro'],
mailbox('get', 'sender_acl_handles', $username)['sender_acl_addresses']['ro'] $current_sender_acls['sender_acl_addresses']['ro']
); );
// Get sender_acl items from POST array // Get sender_acl items from POST array
// Set sender_acl_domain_admin to empty array if sender_acl contains "default" to trigger a reset // Set sender_acl_domain_admin to empty array if sender_acl contains "default" to trigger a reset
@@ -3260,16 +3335,25 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array( $stmt->execute(array(
':username' => $username ':username' => $username
)); ));
$fixed_sender_aliases = mailbox('get', 'sender_acl_handles', $username)['fixed_sender_aliases']; $sender_acl_handles = mailbox('get', 'sender_acl_handles', $username);
$fixed_sender_aliases_allowed = $sender_acl_handles['fixed_sender_aliases_allowed'];
$fixed_sender_aliases_blocked = $sender_acl_handles['fixed_sender_aliases_blocked'];
foreach ($sender_acl_merged as $sender_acl) { foreach ($sender_acl_merged as $sender_acl) {
$domain = ltrim($sender_acl, '@'); $domain = ltrim($sender_acl, '@');
if (is_valid_domain_name($domain)) { if (is_valid_domain_name($domain)) {
$sender_acl = '@' . $domain; $sender_acl = '@' . $domain;
} }
// Don't add if allowed by alias
if (in_array($sender_acl, $fixed_sender_aliases)) { // Always add to sender_acl table to create explicit permission
// Skip only if it's in allowed list (would be redundant)
// But DO add if it's in blocked list (creates override)
if (in_array($sender_acl, $fixed_sender_aliases_allowed)) {
// Skip: already allowed by sender_allowed=1, no need for sender_acl entry
continue; continue;
} }
// Add to sender_acl (either override for blocked aliases, or grant for selectable ones)
$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`) $stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`)
VALUES (:sender_acl, :username)"); VALUES (:sender_acl, :username)");
$stmt->execute(array( $stmt->execute(array(
@@ -3314,12 +3398,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`quota` = :quota_b, `quota` = :quota_b,
`authsource` = :authsource, `authsource` = :authsource,
`attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update), `attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update),
`attributes` = JSON_SET(`attributes`, '$.force_tfa', :force_tfa),
`attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access), `attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
`attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access), `attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
`attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access), `attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access),
`attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access), `attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
`attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost), `attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access), `attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
`attributes` = JSON_SET(`attributes`, '$.eas_access', :eas_access),
`attributes` = JSON_SET(`attributes`, '$.dav_access', :dav_access),
`attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email), `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email),
`attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash) `attributes` = JSON_SET(`attributes`, '$.attribute_hash', :attribute_hash)
WHERE `username` = :username"); WHERE `username` = :username");
@@ -3329,11 +3416,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':quota_b' => $quota_b, ':quota_b' => $quota_b,
':attribute_hash' => $attribute_hash, ':attribute_hash' => $attribute_hash,
':force_pw_update' => $force_pw_update, ':force_pw_update' => $force_pw_update,
':force_tfa' => $force_tfa,
':sogo_access' => $sogo_access, ':sogo_access' => $sogo_access,
':imap_access' => $imap_access, ':imap_access' => $imap_access,
':pop3_access' => $pop3_access, ':pop3_access' => $pop3_access,
':sieve_access' => $sieve_access, ':sieve_access' => $sieve_access,
':smtp_access' => $smtp_access, ':smtp_access' => $smtp_access,
':eas_access' => $eas_access,
':dav_access' => $dav_access,
':recovery_email' => $pw_recovery_email, ':recovery_email' => $pw_recovery_email,
':relayhost' => $relayhost, ':relayhost' => $relayhost,
':username' => $username, ':username' => $username,
@@ -3382,15 +3472,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'msg' => array('mailbox_modified', $username) 'msg' => array('mailbox_modified', $username)
); );
try { // Track affected mailboxes for SOGo update
update_sogo_static_view($username); $update_sogo_mailboxes[] = $username;
} catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => $e->getMessage()
);
}
} }
return true; return true;
break; break;
@@ -3716,6 +3799,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
$attr['eas_access'] = (in_array('eas', $_data['protocol_access'])) ? 1 : 0;
$attr['dav_access'] = (in_array('dav', $_data['protocol_access'])) ? 1 : 0;
} }
else { else {
foreach ($is_now as $key => $value){ foreach ($is_now as $key => $value){
@@ -4017,6 +4102,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('resource_modified', htmlspecialchars($name)) 'msg' => array('resource_modified', htmlspecialchars($name))
); );
// Track affected mailboxes for SOGo update
$update_sogo_mailboxes[] = $name;
} }
break; break;
case 'domain_wide_footer': case 'domain_wide_footer':
@@ -4145,13 +4233,22 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$data['sender_acl_addresses']['rw'] = array(); $data['sender_acl_addresses']['rw'] = array();
$data['sender_acl_addresses']['selectable'] = array(); $data['sender_acl_addresses']['selectable'] = array();
$data['fixed_sender_aliases'] = array(); $data['fixed_sender_aliases'] = array();
$data['fixed_sender_aliases_allowed'] = array();
$data['fixed_sender_aliases_blocked'] = array();
$data['external_sender_aliases'] = array(); $data['external_sender_aliases'] = array();
// Fixed addresses // Fixed addresses - split by sender_allowed status
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'"); $stmt = $pdo->prepare("SELECT `address`, `sender_allowed` FROM `alias` WHERE `goto` REGEXP :goto AND `address` NOT LIKE '@%'");
$stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)')); $stmt->execute(array(':goto' => '(^|,)'.preg_quote($_data, '/').'($|,)'));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) { while ($row = array_shift($rows)) {
// Keep old array for backward compatibility
$data['fixed_sender_aliases'][] = $row['address']; $data['fixed_sender_aliases'][] = $row['address'];
// Split into allowed/blocked for proper display
if ($row['sender_allowed'] == '1') {
$data['fixed_sender_aliases_allowed'][] = $row['address'];
} else {
$data['fixed_sender_aliases_blocked'][] = $row['address'];
}
} }
$stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias_domain_alias` FROM `mailbox`, `alias_domain` $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias_domain_alias` FROM `mailbox`, `alias_domain`
WHERE `alias_domain`.`target_domain` = `mailbox`.`domain` WHERE `alias_domain`.`target_domain` = `mailbox`.`domain`
@@ -4584,10 +4681,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`description`, `description`,
`validity`, `validity`,
`created`, `created`,
`modified` `modified`,
`permanent`
FROM `spamalias` FROM `spamalias`
WHERE `goto` = :username WHERE `goto` = :username
AND `validity` >= :unixnow"); AND (`validity` >= :unixnow
OR `permanent` != 0)");
$stmt->execute(array(':username' => $_data, ':unixnow' => time())); $stmt->execute(array(':username' => $_data, ':unixnow' => time()));
$tladata = $stmt->fetchAll(PDO::FETCH_ASSOC); $tladata = $stmt->fetchAll(PDO::FETCH_ASSOC);
return $tladata; return $tladata;
@@ -4603,10 +4702,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data = $_SESSION['mailcow_cc_username']; $_data = $_SESSION['mailcow_cc_username'];
} }
try { try {
if ($valkey->hGet('RCPT_WANTS_SUBJECT_TAG', $_data)) { if ($redis->hGet('RCPT_WANTS_SUBJECT_TAG', $_data)) {
return "subject"; return "subject";
} }
elseif ($valkey->hGet('RCPT_WANTS_SUBFOLDER_TAG', $_data)) { elseif ($redis->hGet('RCPT_WANTS_SUBFOLDER_TAG', $_data)) {
return "subfolder"; return "subfolder";
} }
else { else {
@@ -4617,7 +4716,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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
return false; return false;
} }
@@ -4709,6 +4808,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
`internal`, `internal`,
`active`, `active`,
`sogo_visible`, `sogo_visible`,
`sender_allowed`,
`created`, `created`,
`modified` `modified`
FROM `alias` FROM `alias`
@@ -4742,6 +4842,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$aliasdata['active_int'] = $row['active']; $aliasdata['active_int'] = $row['active'];
$aliasdata['sogo_visible'] = $row['sogo_visible']; $aliasdata['sogo_visible'] = $row['sogo_visible'];
$aliasdata['sogo_visible_int'] = $row['sogo_visible']; $aliasdata['sogo_visible_int'] = $row['sogo_visible'];
$aliasdata['sender_allowed'] = $row['sender_allowed'];
$aliasdata['created'] = $row['created']; $aliasdata['created'] = $row['created'];
$aliasdata['modified'] = $row['modified']; $aliasdata['modified'] = $row['modified'];
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) { if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $aliasdata['domain'])) {
@@ -5162,7 +5263,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt = $pdo->prepare("SELECT COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE (`kind` = '' OR `kind` = NULL) AND `domain` = :domain AND `username` != :username"); $stmt = $pdo->prepare("SELECT COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE (`kind` = '' OR `kind` = NULL) AND `domain` = :domain AND `username` != :username");
$stmt->execute(array(':domain' => $row['domain'], ':username' => $_data)); $stmt->execute(array(':domain' => $row['domain'], ':username' => $_data));
$MailboxUsage = $stmt->fetch(PDO::FETCH_ASSOC); $MailboxUsage = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND `validity` >= :unixnow"); $stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND (`validity` >= :unixnow OR `permanent` != 0)");
$stmt->execute(array(':address' => $_data, ':unixnow' => time())); $stmt->execute(array(':address' => $_data, ':unixnow' => time()));
$SpamaliasUsage = $stmt->fetch(PDO::FETCH_ASSOC); $SpamaliasUsage = $stmt->fetch(PDO::FETCH_ASSOC);
$mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use']; $mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use'];
@@ -5636,14 +5737,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$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 {
$valkey->hDel('DOMAIN_MAP', $domain); $redis->hDel('DOMAIN_MAP', $domain);
$valkey->hDel('RL_VALUE', $domain); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
continue; continue;
} }
@@ -5708,6 +5809,18 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
continue; continue;
} }
// Track affected mailboxes for SOGo update (capture before deletion)
if (!empty($alias_data['goto'])) {
$gotos = array_map('trim', explode(',', $alias_data['goto']));
foreach ($gotos as $g) {
if (filter_var($g, FILTER_VALIDATE_EMAIL) &&
!in_array($g, array('null@localhost', 'spam@localhost', 'ham@localhost'))) {
$update_sogo_mailboxes[] = $g;
}
}
}
$stmt = $pdo->prepare("DELETE FROM `alias` WHERE `id` = :id"); $stmt = $pdo->prepare("DELETE FROM `alias` WHERE `id` = :id");
$stmt->execute(array( $stmt->execute(array(
':id' => $alias_data['id'] ':id' => $alias_data['id']
@@ -5769,14 +5882,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':alias_domain' => $alias_domain, ':alias_domain' => $alias_domain,
)); ));
try { try {
$valkey->hDel('DOMAIN_MAP', $alias_domain); $redis->hDel('DOMAIN_MAP', $alias_domain);
$valkey->hDel('RL_VALUE', $domain); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
continue; continue;
} }
@@ -5955,31 +6068,25 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
)); ));
} }
try { try {
$valkey->hDel('RL_VALUE', $username); $redis->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('valkey_error', $e) 'msg' => array('redis_error', $e)
); );
continue; continue;
} }
try {
update_sogo_static_view($username);
}catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => $e->getMessage()
);
}
$_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),
'msg' => array('mailbox_removed', htmlspecialchars($username)) 'msg' => array('mailbox_removed', htmlspecialchars($username))
); );
// Track affected mailboxes for SOGo update
$update_sogo_mailboxes[] = $username;
} }
return true; return true;
break; break;
@@ -6081,6 +6188,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('resource_removed', htmlspecialchars($name)) 'msg' => array('resource_removed', htmlspecialchars($name))
); );
// Track affected mailboxes for SOGo update
$update_sogo_mailboxes[] = $name;
} }
break; break;
case 'tags_domain': case 'tags_domain':
@@ -6187,9 +6297,21 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
break; break;
} }
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'resource')) && getenv('SKIP_SOGO') != "y") { if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'resource', 'mailbox')) && getenv('SKIP_SOGO') != "y") {
try { try {
update_sogo_static_view(); if (($_type == 'alias' || $_type == 'resource' || $_type == 'mailbox') && !empty($update_sogo_mailboxes)) {
// INCREMENTAL UPDATE: Update only affected mailboxes/resources
$update_sogo_mailboxes = array_unique($update_sogo_mailboxes);
foreach ($update_sogo_mailboxes as $mailbox) {
update_sogo_static_view($mailbox);
}
}
else {
// FULL REBUILD: For domain and alias_domain operations or if no tracked mailboxes
// Domain operations affect all mailboxes
// Alias_domain operations affect entire target domain
update_sogo_static_view();
}
}catch (PDOException $e) { }catch (PDOException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
+1 -1
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 $valkey; global $redis;
global $lang; global $lang;
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(

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