Compare commits

..

172 Commits

Author SHA1 Message Date
Niklas Meyer
b51a659515 Merge pull request #4698 from mailcow/staging
2022-07a
2022-07-29 14:23:53 +02:00
Niklas Meyer
44a6f09a09 [CLAMAV] Update to 0.105.1 2022-07-29 14:08:26 +02:00
Erisa A
4c10525078 [Web] Update keyHandle max length to 1023 (#4696)
https://w3c.github.io/webauthn/#credential-id

Co-authored-by: Niklas Meyer <62480600+DerLinkman@users.noreply.github.com>
2022-07-26 09:16:23 +02:00
Peter
c9ab8b2eff [GH-Actions][stale] Upgrade to v5.1.0 and add close-issue-reason 2022-07-19 21:43:24 +02:00
Peter
4bf38bf00f Mailcow -> mailcow (#4687) 2022-07-19 20:31:25 +02:00
Peter
7c7c67948e Use yaml list style in docker build workflow (#4688)
* Use yaml list style

* Mailcow -> mailcow
2022-07-19 20:24:24 +02:00
l-with
263cb96786 Improve domain api schema (#4689)
* change response of add domain to array

* add tags to request body of add domain

* add gal to request body of add domain

* add relay_unknown_only to request body of add domain

* add relay_unknown_only to request body of edit domain

* add rl_frame, rl_value to request body of edit domain

* fix indentation

* add tags to request body of edit domain

* change response of edit domain to array

* Revert "change response of edit domain to array"

This reverts commit 692384e21b.

* change response type of edit domain to application/json

* change response type of edit domain

* change items in body of edit domain to array of strings

* change response of edit domain to array

* fix indentation

* revert changing response type of edit domain-admin

* change response type of edit domain to array

* fix response type of edit domains

* change msg in response of edit domains to array

* change items in body of delete domain to array of strings

* change request body of delete domain to array of strings

* fix

* remove properties

* change request body of delete domain to array of strings (fix)

* change reponse type of delete domain to array
2022-07-19 20:22:45 +02:00
Niklas Meyer
b6e3e7a658 Merge pull request #4691 from mailcow/staging
Merge staging into master
2022-07-18 10:49:47 +02:00
DerLinkman
ceaf1423f4 Moved general compose v2 check below the parameter section to respect --force 2022-07-18 10:39:17 +02:00
Niklas Meyer
9598b503ec Merge branch 'master' into staging 2022-07-15 14:03:38 +02:00
Niklas Meyer
94f4ec8b96 Update tweet-trigger-publish-release.yml 2022-07-15 10:53:51 +02:00
DerLinkman
7aab2c55ff Changed which to command -v + seperated compose check from for loop 2022-07-15 10:30:01 +02:00
Niklas Meyer
6abb4d34c1 Merge pull request #4682 from mailcow/feature/badge-readme
Add Integration Tests badge
2022-07-14 22:45:34 +02:00
Peter
c8ccf080f3 Add Integration Tests badge 2022-07-14 20:01:38 +02:00
Niklas Meyer
528f7da5ef Merge pull request #4680 from mailcow/staging
Mooly Update 2022 - TFA Flow Update
2022-07-14 16:28:59 +02:00
DerLinkman
7d72ae3449 Added update-compose to update.sh and create-coldstandby 2022-07-14 11:29:38 +02:00
FreddleSpl0it
753cde0b85 parse host from url for webauthn library 2022-07-14 09:40:02 +02:00
FreddleSpl0it
223ba44b61 rearrange custom params validation 2022-07-14 09:39:24 +02:00
FreddleSpl0it
cd02483b19 prevent auth wipe out at yubi otp registration 2022-07-14 09:38:44 +02:00
FreddleSpl0it
f724662874 readd imapsync fix 2022-07-13 17:13:25 +02:00
FreddleSpl0it
bee762737e readd imapsync fix 2022-07-13 17:02:14 +02:00
Niklas Meyer
83efd3e506 Merge pull request #4662 from mailcow/feature/updatesh-compose-update-prompt
[Update.sh] Added docker-compose Update prompt + Version check
2022-07-13 16:04:42 +02:00
Niklas Meyer
2278a6cc73 Merge pull request #4674 from mailcow/feature/SECURITY.md
Create SECURITY.md
2022-07-13 16:03:33 +02:00
DerLinkman
586b60b276 Unspecified direct compose version (lower then 2.X.X) 2022-07-13 15:13:14 +02:00
DerLinkman
f07b9ea304 Corrected pip check 2022-07-13 15:08:31 +02:00
Niklas Meyer
09dca5d76c Merge pull request #4677 from mhofer117/patch-1
fix blank page on /user when not logged
2022-07-13 15:07:43 +02:00
DerLinkman
65bb808441 Muted which Pip in update_compose 2022-07-13 11:02:41 +02:00
DerLinkman
83b79edb42 Fixed PIP Check 2022-07-13 08:57:50 +02:00
DerLinkman
b8ec244d92 Modified pip compose check 2022-07-13 08:50:28 +02:00
Marcel Hofer
5b924614aa fix blank page on /user when not logged
the current condition to redirect to / was never matching, so a blank page was displayed on /user when not logged in or when logged in as admin.
this will fix it and always redirect to / if nothing is rendered in the user.php
2022-07-12 15:26:03 +02:00
Niklas Meyer
43103add47 Merge pull request #4671 from ntimo/task/remove-drone-ci
Removed DroneCI & Travis CI
2022-07-12 12:16:29 +02:00
Niklas Meyer
124d5d6bb2 Merge pull request #4673 from ntimo/task/run-tests-using-actions
[CI] Added Mailcow tests & image builds
2022-07-12 12:15:18 +02:00
Timo
58fde558f7 Merge pull request #4670 from ntimo/task/fix-open-api-yml
Fixed OpenAPI docs to be spec compliant
2022-07-12 09:43:13 +02:00
Peter
8b314acfcf Create SECURITY.md 2022-07-11 21:06:23 +02:00
ntimo
1c0eab9893 [CI] Added Mailcow tests & image builds 2022-07-11 17:06:00 +00:00
DerLinkman
c62daa0c59 Corrected , to . for new workflow 2022-07-08 21:41:48 +02:00
Niklas Meyer
1a05101f50 Create tweet-trigger-publish-release,yml 2022-07-08 21:39:22 +02:00
ntimo
47fb46c837 Removed DroneCI & Travis CI 2022-07-08 18:50:51 +00:00
ntimo
d29580aa02 Fixed OpenAPI docs to be spec compliant 2022-07-08 18:47:28 +00:00
Niklas Meyer
d0fc62ef13 Merge pull request #4669 from mailcow/feature/issue-4668
Fix permissions of create_cold_standby.sh
2022-07-08 20:16:30 +02:00
Peter
b14c0e4c11 Fix permissions chmod +x 2022-07-08 18:29:34 +02:00
DerLinkman
43ec12f4f0 Readded (again) the new update script check... 2022-07-08 13:49:33 +02:00
DerLinkman
40cf2c85e6 Re-aranged the functions position to top 2022-07-08 13:48:31 +02:00
DerLinkman
6195b7c334 [Backup Script]Check for docker and docker-compose in each step seperate 2022-07-08 13:29:05 +02:00
DerLinkman
385570c1e8 Fixed wrongly override_backup overwriting 2022-07-08 10:54:50 +02:00
DerLinkman
d82cfc6c62 Changed no compose warning color 2022-07-08 10:43:44 +02:00
André
fdf52dcb17 [Rspamd] Prevent LUA crash
Fixes LUA error when inserting unknown symbol from settings map
2022-07-07 09:20:59 +02:00
milkmaker
1ff220ccf8 Translations update from Weblate (#4664)
* [Web] Updated lang.ru.json [CI SKIP]

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

* [Web] Updated lang.uk.json [CI SKIP]

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

Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>
2022-07-06 22:02:15 +02:00
Niklas Meyer
536ab34955 Merge pull request #4634 from opsone-ch/staging 2022-07-05 12:55:10 +02:00
DerLinkman
f7369f0611 [Update.sh] Added docker-compose Update prompt + Version check 2022-07-05 12:07:10 +02:00
Rafael Kraut
14bc105d43 [Web] Remove default selection for sync job target mailbox (#4661)
+ Don't cache that form, closes #4642
2022-07-05 11:51:05 +02:00
Niklas Meyer
2efb4365bf Merge pull request #4659 from mailcow/feature/dovecot-2.3.19.1
[Dovecot] Update to 2.3.19.1
2022-07-05 09:08:11 +02:00
Niklas Meyer
c1b86fc782 Merge pull request #4632 from mailcow/sogo-5.7.0
Update SOGo to 5.7.0
2022-07-05 09:00:22 +02:00
FreddleSpl0it
52e92cc0db fix sql query for tfa registration 2022-07-04 17:17:31 +02:00
FreddleSpl0it
3af2f636a5 Merge branch 'feature/tfa-flow' of https://github.com/mailcow/mailcow-dockerized into feature/tfa-flow 2022-07-04 17:01:41 +02:00
FreddleSpl0it
6fb967cf79 extra tfa register debugging 2022-07-04 17:01:35 +02:00
DerLinkman
03c49ea1f8 Merge branch 'staging' into feature/tfa-flow 2022-07-04 16:43:49 +02:00
Patrick Schult
11700d7ecb Merge pull request #4403 from El-Virus/master
Fix "The operation is insecure." when trying to register fido2 device.
2022-06-30 13:55:07 +02:00
DerLinkman
33eb2c8801 Improved [::] Section check + included prior override backup 2022-06-24 23:10:00 +02:00
FreddleSpl0it
a835419168 fix imapsync 2022-06-23 18:36:54 +02:00
Niklas Meyer
4ce16d1ea4 Merge pull request #4644 from mailcow/feature/update-composev2
Added auto correction of composev1 Binds in compose.yml
2022-06-23 15:43:49 +02:00
DerLinkman
c1c7167ace Added auto correction of composev1 Binds in compose.yml 2022-06-23 15:41:48 +02:00
Niklas Meyer
3d538d4f14 Merge pull request #4643 from mailcow/staging
2022-06a | IMAPSync Hardening + Docker-Compose v2 now needed
2022-06-23 15:04:46 +02:00
Niklas Meyer
7969e7116d Merge branch 'feature/imapsync' into staging 2022-06-23 11:26:07 +02:00
Niklas Meyer
4f58f2caee Merge branch 'feature/update-composev2' into staging 2022-06-23 11:25:58 +02:00
DerLinkman
263baa81c0 Improved .yml and .yaml check for Port Removal 2022-06-23 10:55:12 +02:00
DerLinkman
092890b6ab Changed message in nginx-port removal function 2022-06-23 10:23:48 +02:00
DerLinkman
db7d7ea288 Added override NGINX Port Removal 2022-06-23 10:05:09 +02:00
DerLinkman
452daf5d5e Restored docker-compose Command 2022-06-23 08:55:06 +02:00
FreddleSpl0it
d373164e13 hotfix imapsync 2022-06-20 21:18:57 +02:00
FreddleSpl0it
cd7715fa0e Restore docker-compose check in scripts 2022-06-20 10:32:14 +02:00
milkmaker
af9c3a8565 [Web] Updated lang.es.json [CI SKIP] (#4638)
Co-authored-by: Daniel Castellanos <decacis@gmail.com>

Co-authored-by: Daniel Castellanos <decacis@gmail.com>
2022-06-19 22:23:35 +02:00
DerLinkman
dd6b8c44a4 Added automatic docker-compose standalone installation 2022-06-16 14:13:08 +02:00
DerLinkman
499273dbb7 Readded docker-compose check 2022-06-16 13:50:44 +02:00
DerLinkman
6612b892b7 Removed extra checkout line 2022-06-16 12:56:54 +02:00
DerLinkman
89cea31475 Revert "Before update on 2022-06-16_12_41_05"
This reverts commit 36e4ee7738.
2022-06-16 12:51:51 +02:00
DerLinkman
872fa07213 Restore docker-compose check in scripts 2022-06-16 12:49:17 +02:00
DerLinkman
36e4ee7738 Before update on 2022-06-16_12_41_05 2022-06-16 12:41:49 +02:00
DerLinkman
a139eb9bce Readded Dual Stack availability of WebUI (default) 2022-06-16 12:38:06 +02:00
DerLinkman
7166696aa2 Readded Compose Standalone Download 2022-06-16 12:28:04 +02:00
Markus Ritzmann
537a7908f1 Clamd: Fix Docker Healthcheck 2022-06-16 09:50:33 +02:00
Peter
3fe776ee69 Update SOGo to 5.7.0 2022-06-14 18:55:26 +02:00
DerLinkman
581be02e53 [Dovecot] Update to 2.3.19.1 2022-06-14 15:02:40 +02:00
FreddleSpl0it
71db83efce hotfix imapsync 2022-06-13 12:46:39 +02:00
andryyy
7ae7f25580 [Web] Re-use DKIM key if available 2022-06-11 11:42:36 +02:00
DerLinkman
5d14baa43a Fixed typo for previous commit 2022-06-07 18:38:43 +02:00
DerLinkman
141b397c82 Fix Docker Compose recognition for docker-compose syntax 2022-06-07 18:34:41 +02:00
Niklas Meyer
fd853cfc6f [Compose] Rollback watchdog to 1.96
Due to https://github.com/nagios-plugins/nagios-plugins/pull/649 which cause the Certificate Check to fail.
2022-06-07 17:50:42 +02:00
Niklas Meyer
63f718178e 🌕🐄 Moone Update 2022 - The Docker Compose v2 Update (Part I)
The next major mailcow release.
2022-06-07 15:37:41 +02:00
DerLinkman
74baf20feb Optimized if-else arguments and outputs 2022-06-07 14:45:19 +02:00
FreddleSpl0it
958112af6b [Compose] Remove >&2 in if block 2022-06-07 14:07:35 +02:00
FreddleSpl0it
08d0f9448e [Compose] move then in if statement 2022-06-07 13:59:59 +02:00
Niklas Meyer
7bcc8bd3a2 [Compose] Removed volume Bind from rspamd-vol 2022-06-07 10:34:59 +02:00
FreddleSpl0it
0eb2545773 [WebAuthn] send empty transports array to fix android bug 2022-06-07 09:01:04 +02:00
Niklas Meyer
714511b0a8 [Compose] Update to Docker Compose v2 (#4605)
* Change default HTTP_BIND, HTTPS_BIND

https://github.com/mailcow/mailcow-dockerized/issues/4315#issuecomment-1083034329

* [Compose] Removed Colon after fallback IP in docker-compose.yml

* [Compose] Remove bind options from volumes (#4577)

(cherry picked from commit 4d53216c05)

* Migration (partially) of update.sh + cold-standby.sh to composev2

* Migration of update.sh + cold-standby.sh to composev2

* Migration of update.sh + cold-standby.sh to composev2

* Migration of update.sh + cold-standby.sh to composev2

* [ClamAV] Fixed ClamAV start before unbound

* Migration of update.sh + cold-standby.sh to composev2

* Formulation and values adjusted (IPv4 bind in generate-config.sh)

Co-authored-by: Amin Vakil <info@aminvakil.com>
Co-authored-by: qupfer <github@qupfer.de>
Co-authored-by: FreddleSpl0it <patschul@posteo.de>
2022-06-07 08:53:08 +02:00
Niklas Meyer
c9700773f4 Merge pull request #4613 from mailcow/phpfpm-alpine3.16
PHP-FPM base image update
2022-06-05 20:26:24 +02:00
Peter
2229f87d9b Update base image to alpine 3.16 and updated some dependencies 2022-06-05 19:36:09 +02:00
Niklas Meyer
d360503443 Merge pull request #4609 from mailcow/unbound-alpine3.16
Unbound base image update
2022-06-05 19:20:15 +02:00
Niklas Meyer
838182a8b4 Merge pull request #4608 from mailcow/watchdog-alpine3.16
Watchdog base image update
2022-06-05 19:18:36 +02:00
Niklas Meyer
967cfedbb3 Merge pull request #4610 from mailcow/olefy-alpine3.16
Olefy base image update
2022-06-05 19:15:06 +02:00
Niklas Meyer
a36645a282 Merge pull request #4611 from mailcow/dockerapi-alpine3.16
Dockerapi base image update
2022-06-05 19:14:33 +02:00
Niklas Meyer
3368a70f88 Merge pull request #4612 from mailcow/acme-alpine3.16
acme base image update
2022-06-05 19:14:07 +02:00
Peter
cd1715ba52 Update base image to alpine 3.16 2022-06-05 19:06:03 +02:00
Peter
0bc2a16093 Update base image to alpine 3.16 2022-06-05 19:04:51 +02:00
Peter
a21b3cd606 Update base image to alpine 3.16 2022-06-05 19:03:37 +02:00
Peter
1c479684fc Revert "Update base image to alpine 3.16"
This reverts commit c9dbc7c7b7.
2022-06-05 19:02:21 +02:00
Peter
c9dbc7c7b7 Update base image to alpine 3.16 2022-06-05 19:01:55 +02:00
Peter
c41dc9d8c0 Update base image to alpine 3.16 2022-06-05 19:01:06 +02:00
Peter
1db5841424 Update base image to alpine 3.16 2022-06-05 18:59:56 +02:00
Niklas Meyer
e53b068902 Merge pull request #4607 from mailcow/netfilter-alpine3.16
Netfilter base image update
2022-06-05 18:44:38 +02:00
Peter
2bd436dfd8 Update base image to alpine 3.16 2022-06-05 18:41:54 +02:00
Peter
d13be25f45 Update base image to alpine 3.16 2022-06-05 18:38:16 +02:00
Niklas Meyer
6efd9dc5f9 [Postfix] Update to 3.5.6 (Rebase to Debian 11)
New Postfix Image is: mailcow/postfix:1.67
2022-06-05 14:48:03 +02:00
Niklas Meyer
1edd4012e4 [Web] escapehtml in mailbox.js (#4604)
Co-authored-by: FreddleSpl0it <patschul@posteo.de>
2022-06-03 14:37:56 +02:00
milkmaker
4390c9855a [Web] Updated lang.de.json [CI SKIP] (#4600)
[Web] Updated lang.de.json [CI SKIP]

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

Co-authored-by: Peter <magic@kthx.at>
2022-05-31 19:59:00 +02:00
qupfer
4d53216c05 [Compose] Remove bind options from volumes (#4577) 2022-05-31 09:34:06 +02:00
DerLinkman
040206859f [DB] Remove pipemes from custom_params
(cherry picked from commit c27ad97287)
2022-05-20 09:45:54 +02:00
DerLinkman
d06119a21d [IMAPSYNC] Hardened pipemess exploit prevention (pipemes)
(cherry picked from commit b1658c0f83)
2022-05-20 09:45:45 +02:00
DerLinkman
c27ad97287 [DB] Remove pipemes from custom_params 2022-05-20 09:44:11 +02:00
DerLinkman
b1658c0f83 [IMAPSYNC] Hardened pipemess exploit prevention (pipemes) 2022-05-20 09:30:42 +02:00
Niklas Meyer
05b8609073 [Postfix] Update to 3.5.6 (Rebase to Debian 11) 2022-05-19 18:49:01 +02:00
DerLinkman
552f09f48a [DB] Update DB Version to remove pipemess parameters
(cherry picked from commit 97df5c3b9c)
2022-05-19 15:42:53 +02:00
DerLinkman
97df5c3b9c [DB] Update DB Version to remove pipemess parameters 2022-05-19 15:42:13 +02:00
DerLinkman
8d9102aa08 [Imapsync] Case sensitive PIPEMESS removal
(cherry picked from commit 33e5ad2b5c)
2022-05-19 15:40:39 +02:00
DerLinkman
33e5ad2b5c [Imapsync] Case sensitive PIPEMESS removal 2022-05-19 14:41:21 +02:00
DerLinkman
998cb642a9 [UI] Moved Password Change warning to top for user site 2022-05-19 10:43:06 +02:00
milkmaker
07ac195fea Translations update from Weblate (#4591)
* [Web] Updated lang.ru.json [CI SKIP]

Co-authored-by: DRago_Angel <alekseev.dmitriy.92@gmail.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.uk.json [CI SKIP]

[Web] Updated lang.uk.json [CI SKIP]

[Web] Added lang.uk.json [CI SKIP]

Co-authored-by: OGudzik <olegrpg@gmail.com>
Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.it.json [CI SKIP]

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

* Add Ukrainian language code in vars.inc.php

Co-authored-by: DRago_Angel <alekseev.dmitriy.92@gmail.com>
Co-authored-by: OGudzik <olegrpg@gmail.com>
Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: Stefano <stefano.vassena@gmail.com>
2022-05-18 18:20:03 +02:00
FreddleSpl0it
7d5990bf0f restrict webauthn-tfa-get-args sql query 2022-05-18 10:03:10 +02:00
FreddleSpl0it
4ec982163e restrict webauthn-tfa-get-args sql query 2022-05-18 09:39:50 +02:00
FreddleSpl0it
3c9502f241 add webauthn console log 2022-05-17 19:02:52 +02:00
Niklas Meyer
63cecb2fd8 Merge pull request #4484 from FreddleSpl0it/selection-tfa
[Web] change tfa flow
2022-05-17 15:26:43 +02:00
Niklas Meyer
3029a2d33d Change DB Date to newer Date than staging 2022-05-17 15:26:01 +02:00
Niklas Meyer
fa0d2a959d Merge branch 'feature/tfa-flow' into selection-tfa 2022-05-17 15:23:10 +02:00
Niklas Meyer
f79cac3292 Merge pull request #4590 from FreddleSpl0it/swagger-appPasswd 2022-05-17 08:53:57 +02:00
FreddleSpl0it
7a20a9941e Update swagger docs - add/app-passwd 2022-05-17 07:03:33 +02:00
Niklas Meyer
24cc960379 [Clamd] Update to ClamAV 0.105
Merge pull request #4589 from mailcow/feature/clamd-0.105
2022-05-16 19:51:18 +02:00
Niklas Meyer
353df6413f [UI] Increase Mailadmin loading performance
Merge pull request #4562 from marcojarjour/unblock_mailadmin_upstream
2022-05-16 19:30:50 +02:00
Andri Steiner
b68eae16e5 [Web] Swagger UI: explicitly define used OpenAPI specifications (#4587) 2022-05-13 10:40:22 +02:00
Niklas Meyer
9a812edee4 Mooay 2022 Update – The Tag Update | Revision B (2022-05b)
This PR adds some API Fixes and one UI Fix (improvement)
2022-05-12 11:52:06 +02:00
Peter
43d2a6e135 Update issue template 2022-05-10 21:16:08 +02:00
Peter
5839e22796 Update issue template 2022-05-10 21:14:51 +02:00
DerLinkman
ee844c81d2 Changed Base Docker Image to 0.105.0_base 2022-05-08 18:33:29 +02:00
Niklas Meyer
b6cb3b026c [ClamAV] Update to 0.105 2022-05-06 15:44:58 +02:00
Niklas Meyer
df33ebb2a0 Merge pull request #4575 from FreddleSpl0it/footable-override-css
[Web] change opacity of footable collapse toggle
2022-05-06 08:59:53 +02:00
Niklas Meyer
d2a6838958 Merge pull request #4574 from FreddleSpl0it/tag-fix
[Web] domain/mailbox tagging check for empty tags
2022-05-06 08:59:21 +02:00
FreddleSpl0it
96b8054e6b [Web] change opacity of footable collapse toggle 2022-05-06 08:52:44 +02:00
FreddleSpl0it
dfdd2dadb4 [Web] domain/mailbox tagging check for empty tags 2022-05-06 08:30:15 +02:00
Niklas Meyer
d0528b7883 Merge pull request #4573 from jkellerer/patch-1
Fix for /api/v1/get/mailbox/{email}
2022-05-06 08:24:41 +02:00
FreddleSpl0it
f40e682800 [Web] domain/mailbox tagging check for empty tags 2022-05-06 07:42:45 +02:00
jkellerer
f4dc01d1ec Ensure return type is consistent (list vs object) 2022-05-05 20:00:40 +02:00
jkellerer
187ddedf96 Fix for /api/v1/get/mailbox/{email} 2022-05-05 19:43:33 +02:00
Niklas Meyer
5613134fed Merge pull request #4572 from mailcow/staging
Readded .gitkeep in data/web/templates/cache
2022-05-05 17:30:21 +02:00
Niklas Meyer
e454ed4e39 Readded .gitkeep in data/web/templates/cache 2022-05-05 17:25:04 +02:00
Marco Jarjour
003a6342a5 Match also mobile id's 2022-04-27 17:43:40 +02:00
Marco Jarjour
fb10764167 Execute API calls only when needed 2022-04-27 15:57:53 +02:00
FreddleSpl0it
6d3798ad08 [Web] fix yubi otp 2022-03-19 20:18:31 +01:00
FreddleSpl0it
70921b8d15 [Web] tfa extra debugging 2022-03-18 08:45:02 +01:00
FreddleSpl0it
b185f83fc3 [Web] tfa extra debugging 2022-03-18 08:37:22 +01:00
FreddleSpl0it
60af295c0a Merge branch 'selection-tfa' of https://github.com/FreddleSpl0it/mailcow-dockerized into selection-tfa 2022-03-14 10:32:55 +01:00
FreddleSpl0it
e7fe52a625 [Web] increase mysql publicKey field length 2022-03-14 10:31:59 +01:00
FreddleSpl0it
49c506eed9 [Web] multiple tfa - user support 2022-03-14 10:31:59 +01:00
FreddleSpl0it
21fadf6df2 [Web] multiple tfa - domainadmin support 2022-03-14 10:31:58 +01:00
FreddleSpl0it
5fcccbc97d [Web] add verify selected tfa 2022-03-14 10:31:56 +01:00
FreddleSpl0it
3ef2b6cfa2 [Web] add verify selected tfa 2022-03-14 10:31:51 +01:00
FreddleSpl0it
84b4269c75 [Web] increase mysql publicKey field length 2022-03-14 09:29:07 +01:00
FreddleSpl0it
a2d57d43d1 [Web] multiple tfa - user support 2022-03-07 11:41:13 +01:00
FreddleSpl0it
df33f1a130 [Web] multiple tfa - domainadmin support 2022-02-22 09:38:06 +01:00
FreddleSpl0it
4c6a2055c2 [Web] add verify selected tfa 2022-02-21 14:10:12 +01:00
FreddleSpl0it
f09a3df870 [Web] add verify selected tfa 2022-02-21 10:46:24 +01:00
El-Virus
ea1a412749 Fix missing "lbuchs", after resolving last conflict
It seems that when solving the conflict in my pr when the latest staging branch was merged to master, I accidentally deleted "lbuchs", I added it back
2022-01-21 15:46:44 +01:00
El-Virus
db82327d9a Merge branch 'staging' into master 2022-01-21 15:40:37 +01:00
El-Virus
ea1a02bd7d Fix "The operation is insecure." when trying to register fido2 device.
navigator.credentials.create(); Doesn't accept a port in the "id" parameter. So, when trying to register a fido2 device via WebAuthn throws: "The operation is insecure." on firefox and "The relying party ID is not a registrable domain suffix of, nor equal to the current domain." on Chrome or Edge.
This commit replaces `$_SERVER['HTTP_HOST']` with `$_SERVER['SERVER_NAME']` when initializing `$WebAuthn` which excludes the port to formulate correct requests.
Now Mailcow allows the registration of fido2 devices when running in a non-standard port(eg. 443).
2021-12-26 17:11:06 +01:00
57 changed files with 2718 additions and 758 deletions

View File

@@ -1,120 +0,0 @@
---
kind: pipeline
name: integration-testing
platform:
os: linux
arch: amd64
clone:
disable: true
steps:
- name: prepare-tests
pull: default
image: timovibritannia/ansible
commands:
- git clone https://github.com/mailcow/mailcow-integration-tests.git --branch $(curl -sL https://api.github.com/repos/mailcow/mailcow-integration-tests/releases/latest | jq -r '.tag_name') --single-branch .
- chmod +x ci.sh
- chmod +x ci-ssh.sh
- chmod +x ci-piprequierments.sh
- ./ci.sh
- wget -O group_vars/all/secrets.yml $SECRETS_DOWNLOAD_URL --quiet
environment:
SECRETS_DOWNLOAD_URL:
from_secret: SECRETS_DOWNLOAD_URL
VAULT_PW:
from_secret: VAULT_PW
when:
branch:
- master
- staging
event:
- push
- name: lint
pull: default
image: timovibritannia/ansible
commands:
- ansible-lint ./
when:
branch:
- master
- staging
event:
- push
- name: create-server
pull: default
image: timovibritannia/ansible
commands:
- ./ci-piprequierments.sh
- ansible-playbook mailcow-start-server.yml --diff
- ./ci-ssh.sh
environment:
ANSIBLE_HOST_KEY_CHECKING: false
ANSIBLE_FORCE_COLOR: true
when:
branch:
- master
- staging
event:
- push
- name: setup-server
pull: default
image: timovibritannia/ansible
commands:
- sleep 120
- ./ci-piprequierments.sh
- ansible-playbook mailcow-setup-server.yml --private-key /drone/src/id_ssh_rsa --diff
environment:
ANSIBLE_HOST_KEY_CHECKING: false
ANSIBLE_FORCE_COLOR: true
when:
branch:
- master
- staging
event:
- push
- name: run-tests
pull: default
image: timovibritannia/ansible
commands:
- ./ci-piprequierments.sh
- ansible-playbook mailcow-integration-tests.yml --private-key /drone/src/id_ssh_rsa --diff
environment:
ANSIBLE_HOST_KEY_CHECKING: false
ANSIBLE_FORCE_COLOR: true
when:
branch:
- master
- staging
event:
- push
- name: delete-server
pull: default
image: timovibritannia/ansible
commands:
- ./ci-piprequierments.sh
- ansible-playbook mailcow-delete-server.yml --diff
environment:
ANSIBLE_HOST_KEY_CHECKING: false
ANSIBLE_FORCE_COLOR: true
when:
branch:
- master
- staging
event:
- push
status:
- failure
- success
---
kind: signature
hmac: f6619243fe2a27563291c9f2a46d93ffbc3b6dced9a05f23e64b555ce03a31e5
...

View File

@@ -54,10 +54,11 @@ body:
| --- | --- | | --- | --- |
| My operating system | I_DO_REPLY_HERE | | My operating system | I_DO_REPLY_HERE |
| Is Apparmor, SELinux or similar active? | I_DO_REPLY_HERE | | Is Apparmor, SELinux or similar active? | I_DO_REPLY_HERE |
| Virtualization technlogy (KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported** | I_DO_REPLY_HERE | | Virtualization technology (KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported** | I_DO_REPLY_HERE |
| Server/VM specifications (Memory, CPU Cores) | I_DO_REPLY_HERE | | Server/VM specifications (Memory, CPU Cores) | I_DO_REPLY_HERE |
| Docker Version (`docker version`) | I_DO_REPLY_HERE | | Docker version (`docker version`) | I_DO_REPLY_HERE |
| Docker-Compose Version (`docker-compose version`) | I_DO_REPLY_HERE | | docker-compose version (`docker-compose version`) | I_DO_REPLY_HERE |
| mailcow version (```git describe --tags `git rev-list --tags --max-count=1` ```) | I_DO_REPLY_HERE |
| Reverse proxy (custom solution) | I_DO_REPLY_HERE | | Reverse proxy (custom solution) | I_DO_REPLY_HERE |
Output of `git diff origin/master`, any other changes to the code? If so, **please post them**: Output of `git diff origin/master`, any other changes to the code? If so, **please post them**:

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Mark/Close Stale Issues and Pull Requests 🗑️ - name: Mark/Close Stale Issues and Pull Requests 🗑️
uses: actions/stale@v5.0.0 uses: actions/stale@v5.1.0
with: with:
repo-token: ${{ secrets.STALE_ACTION_PAT }} repo-token: ${{ secrets.STALE_ACTION_PAT }}
days-before-stale: 60 days-before-stale: 60
@@ -30,6 +30,7 @@ jobs:
stale-issue-label: "stale" stale-issue-label: "stale"
stale-pr-label: "stale" stale-pr-label: "stale"
exempt-draft-pr: "true" exempt-draft-pr: "true"
close-issue-reason: "not_planned"
operations-per-run: "250" operations-per-run: "250"
ascending: "true" ascending: "true"
#DRY-RUN #DRY-RUN

42
.github/workflows/image_builds.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Build mailcow Docker Images
on:
push:
branches: [ "master", "staging" ]
workflow_dispatch:
jobs:
docker_image_builds:
strategy:
matrix:
images:
- "acme-mailcow"
- "clamd-mailcow"
- "dockerapi-mailcow"
- "dovecot-mailcow"
- "netfilter-mailcow"
- "olefy-mailcow"
- "php-fpm-mailcow"
- "postfix-mailcow"
- "rspamd-mailcow"
- "sogo-mailcow"
- "solr-mailcow"
- "unbound-mailcow"
- "watchdog-mailcow"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Docker
run: |
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
sudo service docker start
sudo curl -L https://github.com/docker/compose/releases/download/v$(curl -Ls https://www.servercow.de/docker-compose/latest.php)/docker-compose-$(uname -s)-$(uname -m) > /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
- name: Prepair Image Builds
run: |
cp helper-scripts/docker-compose.override.yml.d/BUILD_FLAGS/docker-compose.override.yml docker-compose.override.yml
- name: Build Docker Images
run: |
docker-compose build ${image}
env:
image: ${{ matrix.images }}

60
.github/workflows/integration_tests.yml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: mailcow Integration Tests
on:
push:
branches: [ "master", "staging" ]
workflow_dispatch:
jobs:
integration_tests:
runs-on: ubuntu-latest
steps:
- name: Setup Ansible
run: |
export DEBIAN_FRONTEND=noninteractive
sudo apt-get update
sudo apt-get install python3 python3-pip git
sudo pip3 install ansible
- name: Prepair Test Environment
run: |
git clone https://github.com/mailcow/mailcow-integration-tests.git --branch $(curl -sL https://api.github.com/repos/mailcow/mailcow-integration-tests/releases/latest | jq -r '.tag_name') --single-branch .
./fork_check.sh
./ci.sh
./ci-pip-requirements.sh
env:
VAULT_PW: ${{ secrets.MAILCOW_TESTS_VAULT_PW }}
VAULT_FILE: ${{ secrets.MAILCOW_TESTS_VAULT_FILE }}
- name: Start Integration Test Server
run: |
./fork_check.sh
ansible-playbook mailcow-start-server.yml --diff
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
ANSIBLE_HOST_KEY_CHECKING: 'false'
- name: Setup Integration Test Server
run: |
./fork_check.sh
sleep 30
ansible-playbook mailcow-setup-server.yml --private-key id_ssh_rsa --diff
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
ANSIBLE_HOST_KEY_CHECKING: 'false'
- name: Run Integration Tests
run: |
./fork_check.sh
ansible-playbook mailcow-integration-tests.yml --private-key id_ssh_rsa --diff
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
ANSIBLE_HOST_KEY_CHECKING: 'false'
- name: Delete Integration Test Server
if: always()
run: |
./fork_check.sh
ansible-playbook mailcow-delete-server.yml --diff
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
ANSIBLE_HOST_KEY_CHECKING: 'false'

View File

@@ -0,0 +1,17 @@
name: "Tweet trigger release"
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Tweet-trigger-publish-release
uses: mugi111/tweet-trigger-release@v1.1
with:
consumer_key: ${{ secrets.CONSUMER_KEY }}
consumer_secret: ${{ secrets.CONSUMER_SECRET }}
access_token_key: ${{ secrets.ACCESS_TOKEN_KEY }}
access_token_secret: ${{ secrets.ACCESS_TOKEN_SECRET }}
tweet_body: 'A new mailcow-dockerized Release has been Released on GitHub! Checkout our GitHub Page for the latest Release: github.com/mailcow/mailcow-dockerized/releases/latest'

View File

@@ -1,16 +0,0 @@
sudo: required
services:
- docker
script:
- echo 'Europe/Berlin' | MAILCOW_HOSTNAME=build.mailcow ./generate_config.sh
- docker-compose pull --ignore-pull-failures --parallel
- docker-compose build
- docker login --username=$DOCKER_HUB_USERNAME --password=$DOCKER_HUB_PASSWORD
- docker-compose push
branches:
only:
- master_disabled
env:
global:
- secure: MpxpTwD7f0CNEVLitSpVmocK7O9r+BwFE1deEHK4AlQo/oc9cOlhGe1EL3mx9zbglPmjlDg/8kMUGv6vSirIabfBo9Szjps76bHckFr9lr2Ykkg0e29oC8pgPpSXD1eY/1ZIN/FvIkxpUFLETo1okS/j9q/A0DCGFmti0n3EoMORsgRz9CpNAiEh0zpSd6+euPAGHuczuCrDuO84my9bIOCjA/+aPunHNeXiuM8yIM2SxCSyGtIKT0+jvquIvLF58VxivysXBlRfhDn8fhB09nXA2Ru/derYQACfcmNSn9Pd4bDpebPJW5B9H/XA8xjb58uKinUlncbAMB/QnxoT75j9YRWJZRSQ+34XNYP6ZgK9soZ2TC6djQyEKTUu45Kp/1s+poSn42m9jytJJTmmK0KxsZTRcC8JD5nrjIMZWPUNNTwC5L4+I7ZRWg2WooK3LNyq1Ng8Hn6W77wSgsvAJw2HD3Lx58AprGUhHuBeaIZRuSN9aKwZrl9vKQJLqPnOp/nF2EC6kot5HYYtcotGtETXPUDih21gWD5ZM2BqVqYfQQnJnNMgeYmMdj6QQuTFqhuNJf7hXRIRkTnD3j1gDOLKQZazW0+N2JE8XWDFwi6fKScDsxT85lJti9HmzHa7+k4RVHmUYuDgRoPuzUgjWHvPsiz3/Z8WQ9JYpH84S8w=
- secure: fWzZisT6nGDNL4lf6tXB07eFG2drgBakHxzdF/NFVvzuP861RFR6omuL+ED0PgXrEHDJBxaBLv52je8irmUXrAH1CNr7T8DWiZo/h5h609Uzr+38T1NnIu4krL0Wo6/CDwlLKnzqTq9yBIZLQSHVJmo8AOpo1JPIi2ajodqj9ZfmAxDQTQl+G6zvQjtqIkYHsHY7A44Rto0f14ykn7w2S82Jn6Ry89VNI5V1WEO3sMpM/XekNP/HokNcRIuntL/0+kuLvTJ5akGoTjBQxSnSW95opzPeGky74HRU2obExJYqKvF0VfVJRNAqejwjIiFIbbjqV0Sk5391kFuhuBErQQDM1bOHGdxZ41HsJH29qNWIl7C33Yl10qERoqecgsJ1N/bS2ZEmWqm/zQh5GClCXPvYmzEqMYsMGM3vjbKdjDlc1Wh2w/eFclsXN9LSXh1mc35rtj46frcT6e5Kof87AIfC9hTgDvk9kAsyjaHMkSHSZthbZXCIcsD8qriNm5UqfFBYD79mPIP1S2YMQ2jscCsjHOZgYVrcm0kzDF21J1w6H0Lo7d1jw37LYlegBdtLQ9gYgqY2D5m+nxWuVoD5FZmpR+5JGtK+ootyLFF8aiFoHXd4op1JCxRLjgkmnZKXzw3kTQSpE7oa7CgzchtQmK2nqcqla1b5Qk7ilVcjooo=

View File

@@ -2,7 +2,8 @@
## We stand with 🇺🇦 ## We stand with 🇺🇦
[![master build status](https://img.shields.io/drone/build/mailcow/mailcow-dockerized/master?label=master%20build&server=https%3A%2F%2Fdrone.mailcow.email)](https://drone.mailcow.email/mailcow/mailcow-dockerized) [![staging build status](https://img.shields.io/drone/build/mailcow/mailcow-dockerized/staging?label=staging%20build&server=https%3A%2F%2Fdrone.mailcow.email)](https://drone.mailcow.email/mailcow/mailcow-dockerized) [![Translation status](https://translate.mailcow.email/widgets/mailcow-dockerized/-/translation/svg-badge.svg)](https://translate.mailcow.email/engage/mailcow-dockerized/) [![Mailcow Integration Tests](https://github.com/mailcow/mailcow-dockerized/actions/workflows/integration_tests.yml/badge.svg?branch=master)](https://github.com/mailcow/mailcow-dockerized/actions/workflows/integration_tests.yml)
[![Translation status](https://translate.mailcow.email/widgets/mailcow-dockerized/-/translation/svg-badge.svg)](https://translate.mailcow.email/engage/mailcow-dockerized/)
[![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/mailcow_email.svg?style=social&label=Follow%20%40mailcow_email)](https://twitter.com/mailcow_email) [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/mailcow_email.svg?style=social&label=Follow%20%40mailcow_email)](https://twitter.com/mailcow_email)
## Want to support mailcow? ## Want to support mailcow?

42
SECURITY.md Normal file
View File

@@ -0,0 +1,42 @@
# Security Policies and Procedures
This document outlines security procedures and general policies for the _mailcow: dockerized_ project as found on [mailcow-dockerized](https://github.com/mailcow/mailcow-dockerized).
* [Reporting a Vulnerability](#reporting-a-vulnerability)
* [Disclosure Policy](#disclosure-policy)
* [Comments on this Policy](#comments-on-this-policy)
## Reporting a Vulnerability
The mailcow team and community take all security vulnerabilities
seriously. Thank you for improving the security of our open source
software. We appreciate your efforts and responsible disclosure and will
make every effort to acknowledge your contributions.
Report security vulnerabilities by emailing the mailcow team at:
info at servercow.de
mailcow team will acknowledge your email as soon as possible, and will
send a more detailed response afterwards indicating the next steps in
handling your report. After the initial reply to your report, the mailcow
team will endeavor to keep you informed of the progress towards a fix and
full announcement, and may ask for additional information or guidance.
Report security vulnerabilities in third-party modules to the person or
team maintaining the module.
## Disclosure Policy
When the mailcow team receives a security bug report, they will assign it
to a primary handler. This person will coordinate the fix and release
process, involving the following steps:
* Confirm the problem and determine the affected versions.
* Audit code to find any potential similar problems.
* Prepare fixes for all releases still under maintenance.
## Comments on this Policy
If you have suggestions on how this process could be improved please submit a
pull request.

0
create_cold_standby.sh Normal file → Executable file
View File

View File

@@ -1,4 +1,4 @@
FROM alpine:3.15 FROM alpine:3.16
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"

View File

@@ -1,4 +1,4 @@
FROM clamav/clamav:0.104.2-2_base FROM clamav/clamav:0.105.1_base
LABEL maintainer "André Peters <andre.peters@servercow.de>" LABEL maintainer "André Peters <andre.peters@servercow.de>"
@@ -8,8 +8,14 @@ RUN apk upgrade --no-cache \
bind-tools \ bind-tools \
bash bash
COPY clamd.sh ./ # init
COPY clamd.sh /clamd.sh
RUN chmod +x /sbin/tini RUN chmod +x /sbin/tini
# healthcheck
COPY healthcheck.sh /healthcheck.sh
RUN chmod +x /healthcheck.sh
HEALTHCHECK --start-period=6m CMD "/healthcheck.sh"
ENTRYPOINT [] ENTRYPOINT []
CMD ["/sbin/tini", "-g", "--", "/clamd.sh"] CMD ["/sbin/tini", "-g", "--", "/clamd.sh"]

View File

@@ -0,0 +1,9 @@
#!/bin/bash
if [[ "${SKIP_CLAMD}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "SKIP_CLAMD=y, skipping ClamAV..."
exit 0
fi
# run clamd healthcheck
/usr/local/bin/clamdcheck.sh

View File

@@ -1,4 +1,4 @@
FROM alpine:3.15 FROM alpine:3.16
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"

View File

@@ -2,7 +2,7 @@ FROM debian:bullseye-slim
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG DOVECOT=2.3.18 ARG DOVECOT=2.3.19.1
ENV LC_ALL C ENV LC_ALL C
ENV GOSU_VERSION 1.14 ENV GOSU_VERSION 1.14

View File

@@ -1,4 +1,4 @@
FROM alpine:3.15 FROM alpine:3.16
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
ENV XTABLES_LIBDIR /usr/lib/xtables ENV XTABLES_LIBDIR /usr/lib/xtables

View File

@@ -1,4 +1,4 @@
FROM alpine:3.15 FROM alpine:3.16
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
WORKDIR /app WORKDIR /app

View File

@@ -1,12 +1,12 @@
FROM php:8.0-fpm-alpine3.14 FROM php:8.0-fpm-alpine3.16
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
ENV APCU_PECL 5.1.20 ENV APCU_PECL 5.1.21
ENV IMAGICK_PECL 3.5.1 ENV IMAGICK_PECL 3.7.0
# Mailparse is pulled from master branch # Mailparse is pulled from master branch
#ENV MAILPARSE_PECL 3.0.2 #ENV MAILPARSE_PECL 3.0.2
ENV MEMCACHED_PECL 3.1.5 ENV MEMCACHED_PECL 3.2.0
ENV REDIS_PECL 5.3.4 ENV REDIS_PECL 5.3.7
RUN apk add -U --no-cache autoconf \ RUN apk add -U --no-cache autoconf \
aspell-dev \ aspell-dev \

View File

@@ -1,4 +1,4 @@
FROM debian:buster-slim FROM debian:bullseye-slim
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
FROM alpine:3.15 FROM alpine:3.16
LABEL maintainer "Andre Peters <andre.peters@servercow.de>" LABEL maintainer "Andre Peters <andre.peters@servercow.de>"

View File

@@ -1,4 +1,4 @@
FROM alpine:3.15 FROM alpine:3.16
LABEL maintainer "André Peters <andre.peters@servercow.de>" LABEL maintainer "André Peters <andre.peters@servercow.de>"
# Installation # Installation

View File

@@ -18,6 +18,9 @@ symbols {
"ENCRYPTED_CHAT" { "ENCRYPTED_CHAT" {
score = -20.0; score = -20.0;
} }
"SOGO_CONTACT" {
score = -99.0;
}
} }
group "MX" { group "MX" {

View File

@@ -39,7 +39,7 @@
window.onload = function() { window.onload = function() {
// Begin Swagger UI call region // Begin Swagger UI call region
const ui = SwaggerUIBundle({ const ui = SwaggerUIBundle({
url: "/api/openapi.yaml", urls: [{url: "/api/openapi.yaml", name: "mailcow API"}],
dom_id: '#swagger-ui', dom_id: '#swagger-ui',
deepLinking: true, deepLinking: true,
presets: [ presets: [

View File

@@ -209,10 +209,17 @@ paths:
- app_passwd - app_passwd
- add - add
- active: "1" - active: "1"
app_name: emclient username: info@domain.tld
app_name: wordpress
app_passwd: keyleudecticidechothistishownsan31 app_passwd: keyleudecticidechothistishownsan31
app_passwd2: keyleudecticidechothistishownsan31 app_passwd2: keyleudecticidechothistishownsan31
username: hello@mailcow.email protocols:
- imap_access
- dav_access
- smtp_access
- eas_access
- pop3_access
- sieve_access
msg: app_passwd_added msg: app_passwd_added
type: success type: success
schema: schema:
@@ -249,6 +256,13 @@ paths:
app_name: wordpress app_name: wordpress
app_passwd: keyleudecticidechothistishownsan31 app_passwd: keyleudecticidechothistishownsan31
app_passwd2: keyleudecticidechothistishownsan31 app_passwd2: keyleudecticidechothistishownsan31
protocols:
- imap_access
- dav_access
- smtp_access
- eas_access
- pop3_access
- sieve_access
properties: properties:
active: active:
description: is alias active or not description: is alias active or not
@@ -504,21 +518,23 @@ paths:
- domain.tld - domain.tld
type: success type: success
schema: schema:
properties: type: array
log: items:
description: contains request object type: object
items: {} properties:
type: array log:
msg: description: contains request object
items: {} items: {}
type: array type: array
type: msg:
enum: items: {}
- success type: array
- danger type:
- error enum:
type: string - success
type: object - danger
- error
type: string
description: OK description: OK
headers: {} headers: {}
tags: tags:
@@ -565,6 +581,11 @@ paths:
domain: domain:
description: Fully qualified domain name description: Fully qualified domain name
type: string type: string
gal:
description: >-
is domain global address list active or not, it enables
shared contacts accross domain in SOGo webmail
type: boolean
mailboxes: mailboxes:
description: limit count of mailboxes associated with this domain description: limit count of mailboxes associated with this domain
type: number type: number
@@ -582,6 +603,9 @@ paths:
if not, them you have to create "dummy" mailbox for each if not, them you have to create "dummy" mailbox for each
address to relay address to relay
type: boolean type: boolean
relay_unknown_only:
description: Relay non-existing mailboxes only. Existing mailboxes will be delivered locally.
type: boolean
rl_frame: rl_frame:
enum: enum:
- s - s
@@ -592,6 +616,11 @@ paths:
rl_value: rl_value:
description: rate limit value description: rate limit value
type: number type: number
tags:
description: tags for this Domain
type: array
items:
type: string
type: object type: object
summary: Create domain summary: Create domain
/api/v1/add/domain-admin: /api/v1/add/domain-admin:
@@ -1938,21 +1967,23 @@ paths:
- domain2.tld - domain2.tld
type: success type: success
schema: schema:
properties: type: array
log: items:
description: contains request object type: object
items: {} properties:
type: array log:
msg: description: contains request object
items: {} items: {}
type: array type: array
type: msg:
enum: items: {}
- success type: array
- danger type:
- error enum:
type: string - success
type: object - danger
- error
type: string
description: OK description: OK
headers: {} headers: {}
tags: tags:
@@ -1963,14 +1994,15 @@ paths:
content: content:
application/json: application/json:
schema: schema:
type: object
example: example:
- domain.tld - domain.tld
- domain2.tld - domain2.tld
properties: properties:
items: items:
description: contains list of domains you want to delete type: array
type: object items:
type: object type: string
summary: Delete domain summary: Delete domain
/api/v1/delete/domain-admin: /api/v1/delete/domain-admin:
post: post:
@@ -2958,23 +2990,25 @@ paths:
$ref: "#/components/responses/Unauthorized" $ref: "#/components/responses/Unauthorized"
"200": "200":
content: content:
"*/*": application/json:
schema: schema:
properties: type: array
log: items:
description: contains request object type: object
items: {} properties:
type: array log:
msg: type: array
items: {} description: contains request object
type: array items: {}
type: msg:
enum: type: array
- success items: {}
- danger type:
- error enum:
type: string - success
type: object - danger
- error
type: string
description: OK description: OK
headers: {} headers: {}
tags: tags:
@@ -3042,13 +3076,33 @@ paths:
if not, them you have to create "dummy" mailbox for each if not, them you have to create "dummy" mailbox for each
address to relay address to relay
type: boolean type: boolean
relay_unknown_only:
description: Relay non-existing mailboxes only. Existing mailboxes will be delivered locally.
type: boolean
relayhost: relayhost:
description: id of relayhost description: id of relayhost
type: number type: number
rl_frame:
enum:
- s
- m
- h
- d
type: string
rl_value:
description: rate limit value
type: number
tags:
description: tags for this Domain
type: array
items:
type: string
type: object type: object
items: items:
description: contains list of domain names you want update description: contains list of domain names you want update
type: object type: array
items:
type: string
type: object type: object
summary: Update domain summary: Update domain
/api/v1/edit/fail2ban: /api/v1/edit/fail2ban:
@@ -3939,6 +3993,8 @@ paths:
in: query in: query
name: tags name: tags
required: false required: false
schema:
type: string
- description: e.g. api-key-string - description: e.g. api-key-string
example: api-key-string example: api-key-string
in: header in: header
@@ -4498,6 +4554,8 @@ paths:
in: query in: query
name: tags name: tags
required: false required: false
schema:
type: string
- description: e.g. api-key-string - description: e.g. api-key-string
example: api-key-string example: api-key-string
in: header in: header

View File

@@ -232,6 +232,9 @@ table.footable>tbody>tr.footable-empty>td {
font-style:italic; font-style:italic;
font-size: 1rem; font-size: 1rem;
} }
table>tbody>tr>td>span.footable-toggle {
opacity: 0.75;
}
.navbar-nav > li { .navbar-nav > li {
font-size: 1rem !important; font-size: 1rem !important;
} }
@@ -257,6 +260,17 @@ code {
margin-right: 5px; margin-right: 5px;
} }
.list-group-item.webauthn-authenticator-selection,
.list-group-item.totp-authenticator-selection,
.list-group-item.yubi_otp-authenticator-selection {
border-radius: 0px !important;
}
.pending-tfa-collapse {
padding: 10px;
background: #fbfbfb;
border: 1px solid #ededed;
}
.tag-box { .tag-box {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -292,4 +306,4 @@ code {
padding: 0 5px 0 5px; padding: 0 5px 0 5px;
align-items: center; align-items: center;
display: inline-flex; display: inline-flex;
} }

View File

@@ -2,5 +2,5 @@
session_start(); session_start();
unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_method']); unset($_SESSION['pending_tfa_methods']);
?> ?>

View File

@@ -23,6 +23,27 @@ if (is_array($alertbox_log_parser)) {
unset($_SESSION['return']); unset($_SESSION['return']);
} }
// map tfa details for twig
$pending_tfa_authmechs = [];
foreach($_SESSION['pending_tfa_methods'] as $authdata){
$pending_tfa_authmechs[$authdata['authmech']] = false;
}
if (isset($pending_tfa_authmechs['webauthn'])) {
$pending_tfa_authmechs['webauthn'] = true;
}
if (!isset($pending_tfa_authmechs['webauthn'])
&& isset($pending_tfa_authmechs['yubi_otp'])) {
$pending_tfa_authmechs['yubi_otp'] = true;
}
if (!isset($pending_tfa_authmechs['webauthn'])
&& !isset($pending_tfa_authmechs['yubi_otp'])
&& isset($pending_tfa_authmechs['totp'])) {
$pending_tfa_authmechs['totp'] = true;
}
if (isset($pending_tfa_authmechs['u2f'])) {
$pending_tfa_authmechs['u2f'] = true;
}
// globals // globals
$globalVariables = [ $globalVariables = [
'mailcow_info' => array( 'mailcow_info' => array(
@@ -30,7 +51,8 @@ $globalVariables = [
'git_project_url' => $GLOBALS['MAILCOW_GIT_URL'] 'git_project_url' => $GLOBALS['MAILCOW_GIT_URL']
), ),
'js_path' => '/cache/'.basename($JSPath), 'js_path' => '/cache/'.basename($JSPath),
'pending_tfa_method' => @$_SESSION['pending_tfa_method'], 'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'],
'pending_tfa_authmechs' => $pending_tfa_authmechs,
'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'], 'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'],
'lang_footer' => json_encode($lang['footer']), 'lang_footer' => json_encode($lang['footer']),
'lang_acl' => json_encode($lang['acl']), 'lang_acl' => json_encode($lang['acl']),

View File

@@ -197,7 +197,7 @@ function dkim($_action, $_data = null, $privkey = false) {
return false; return false;
} }
try { try {
dkim('delete', (array)$domain); dkim('delete', array('domains' => $domain));
$redis->hSet('DKIM_PUB_KEYS', $domain, $pem_public_key); $redis->hSet('DKIM_PUB_KEYS', $domain, $pem_public_key);
$redis->hSet('DKIM_SELECTORS', $domain, $dkim_selector); $redis->hSet('DKIM_SELECTORS', $domain, $dkim_selector);
$redis->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, $private_key_normalized); $redis->hSet('DKIM_PRIV_KEYS', $dkim_selector . '.' . $domain, $private_key_normalized);

View File

@@ -830,11 +830,15 @@ function check_login($user, $pass, $app_passwd_data = false) {
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) { foreach ($rows as $row) {
// verify password
if (verify_hash($row['password'], $pass)) { if (verify_hash($row['password'], $pass)) {
if (get_tfa($user)['name'] != "none") { // check for tfa authenticators
$authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
// active tfa authenticators found, set pending user login
$_SESSION['pending_mailcow_cc_username'] = $user; $_SESSION['pending_mailcow_cc_username'] = $user;
$_SESSION['pending_mailcow_cc_role'] = "admin"; $_SESSION['pending_mailcow_cc_role'] = "admin";
$_SESSION['pending_tfa_method'] = get_tfa($user)['name']; $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
unset($_SESSION['ldelay']); unset($_SESSION['ldelay']);
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'info', 'type' => 'info',
@@ -842,8 +846,7 @@ function check_login($user, $pass, $app_passwd_data = false) {
'msg' => 'awaiting_tfa_confirmation' 'msg' => 'awaiting_tfa_confirmation'
); );
return "pending"; return "pending";
} } else {
else {
unset($_SESSION['ldelay']); unset($_SESSION['ldelay']);
// 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");
@@ -866,11 +869,14 @@ function check_login($user, $pass, $app_passwd_data = false) {
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) { foreach ($rows as $row) {
// verify password
if (verify_hash($row['password'], $pass) !== false) { if (verify_hash($row['password'], $pass) !== false) {
if (get_tfa($user)['name'] != "none") { // check for tfa authenticators
$authenticators = get_tfa($user);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
$_SESSION['pending_mailcow_cc_username'] = $user; $_SESSION['pending_mailcow_cc_username'] = $user;
$_SESSION['pending_mailcow_cc_role'] = "domainadmin"; $_SESSION['pending_mailcow_cc_role'] = "domainadmin";
$_SESSION['pending_tfa_method'] = get_tfa($user)['name']; $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
unset($_SESSION['ldelay']); unset($_SESSION['ldelay']);
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'info', 'type' => 'info',
@@ -930,24 +936,39 @@ function check_login($user, $pass, $app_passwd_data = false) {
$rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC)); $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
} }
foreach ($rows as $row) { foreach ($rows as $row) {
// verify password
if (verify_hash($row['password'], $pass) !== false) { if (verify_hash($row['password'], $pass) !== false) {
unset($_SESSION['ldelay']); // check for tfa authenticators
$_SESSION['return'][] = array( $authenticators = get_tfa($user);
'type' => 'success', if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
'log' => array(__FUNCTION__, $user, '*'), $_SESSION['pending_mailcow_cc_username'] = $user;
'msg' => array('logged_in_as', $user) $_SESSION['pending_mailcow_cc_role'] = "user";
); $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
if ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { unset($_SESSION['ldelay']);
$service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV'; $_SESSION['return'][] = array(
$stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)"); 'type' => 'success',
$stmt->execute(array( 'log' => array(__FUNCTION__, $user, '*'),
':service' => $service, 'msg' => array('logged_in_as', $user)
':app_id' => $row['app_passwd_id'], );
':username' => $user, return "pending";
':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) } else {
)); if ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {
$service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV';
$stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)");
$stmt->execute(array(
':service' => $service,
':app_id' => $row['app_passwd_id'],
':username' => $user,
':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'])
));
}
unset($_SESSION['ldelay']);
// Reactivate TFA if it was set to "deactivate TFA for next login"
$stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user");
$stmt->execute(array(':user' => $user));
return "user";
} }
return "user";
} }
} }
@@ -1142,47 +1163,46 @@ function set_tfa($_data) {
global $yubi; global $yubi;
global $tfa; global $tfa;
$_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']; $username = $_SESSION['mailcow_cc_username'];
if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
$_SESSION['return'][] = array( // check for empty user and role
'type' => 'danger', if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied' // check admin confirm password
); if ($access_denied === null) {
return false; $stmt = $pdo->prepare("SELECT `password` FROM `admin`
} WHERE `username` = :username");
$stmt = $pdo->prepare("SELECT `password` FROM `admin` $stmt->execute(array(':username' => $username));
WHERE `username` = :username"); $row = $stmt->fetch(PDO::FETCH_ASSOC);
$stmt->execute(array(':username' => $username)); if ($row) {
$row = $stmt->fetch(PDO::FETCH_ASSOC); if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); else $access_denied = false;
if (!empty($num_results)) {
if (!verify_hash($row['password'], $_data["confirm_password"])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied'
);
return false;
}
}
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if (!empty($num_results)) {
if (!verify_hash($row['password'], $_data["confirm_password"])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied'
);
return false;
} }
} }
// check mailbox confirm password
if ($access_denied === null) {
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
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;
}
}
// set access_denied error
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"]) {
case "yubi_otp": case "yubi_otp":
@@ -1220,8 +1240,7 @@ function set_tfa($_data) {
$yubico_modhex_id = substr($_data["otp_token"], 0, 12); $yubico_modhex_id = substr($_data["otp_token"], 0, 12);
$stmt = $pdo->prepare("DELETE FROM `tfa` $stmt = $pdo->prepare("DELETE FROM `tfa`
WHERE `username` = :username WHERE `username` = :username
AND (`authmech` != 'yubi_otp') AND (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
OR (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)");
$stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id)); $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id));
$stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES $stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES
(:key_id, :username, 'yubi_otp', '1', :secret)"); (:key_id, :username, 'yubi_otp', '1', :secret)");
@@ -1265,9 +1284,6 @@ function set_tfa($_data) {
case "webauthn": case "webauthn":
$key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"]; $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"];
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `authmech` != 'webauthn'");
$stmt->execute(array(':username' => $username));
$stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`) $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`)
VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')"); VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')");
$stmt->execute(array( $stmt->execute(array(
@@ -1439,25 +1455,27 @@ function unset_tfa_key($_data) {
global $pdo; global $pdo;
global $lang; global $lang;
$_data_log = $_data; $_data_log = $_data;
$access_denied = null;
$id = intval($_data['unset_tfa_key']); $id = intval($_data['unset_tfa_key']);
$username = $_SESSION['mailcow_cc_username']; $username = $_SESSION['mailcow_cc_username'];
if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) {
$_SESSION['return'][] = array( // check for empty user and role
'type' => 'danger', if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true;
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied'
);
return false;
}
try { try {
if (!is_numeric($id)) { if (!is_numeric($id)) $access_denied = true;
$_SESSION['return'][] = array(
// set access_denied error
if ($access_denied){
$_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;
} }
// 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'");
$stmt->execute(array(':username' => $username)); $stmt->execute(array(':username' => $username));
@@ -1470,6 +1488,8 @@ function unset_tfa_key($_data) {
); );
return false; return false;
} }
// delete key
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id"); $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id");
$stmt->execute(array(':username' => $username, ':id' => $id)); $stmt->execute(array(':username' => $username, ':id' => $id));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -1487,7 +1507,7 @@ function unset_tfa_key($_data) {
return false; return false;
} }
} }
function get_tfa($username = null) { function get_tfa($username = null, $id = null) {
global $pdo; global $pdo;
if (isset($_SESSION['mailcow_cc_username'])) { if (isset($_SESSION['mailcow_cc_username'])) {
$username = $_SESSION['mailcow_cc_username']; $username = $_SESSION['mailcow_cc_username'];
@@ -1495,92 +1515,120 @@ function get_tfa($username = null) {
elseif (empty($username)) { elseif (empty($username)) {
return false; return false;
} }
$stmt = $pdo->prepare("SELECT * FROM `tfa`
WHERE `username` = :username AND `active` = '1'");
$stmt->execute(array(':username' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (isset($row["authmech"])) { if (!isset($id)){
switch ($row["authmech"]) { // fetch all tfa methods - just get information about possible authenticators
case "yubi_otp": $stmt = $pdo->prepare("SELECT `id`, `key_id`, `authmech` FROM `tfa`
$data['name'] = "yubi_otp"; WHERE `username` = :username AND `active` = '1'");
$data['pretty'] = "Yubico OTP"; $stmt->execute(array(':username' => $username));
$stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username"); $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmt->execute(array(
':username' => $username, // no tfa methods found
)); if (count($results) == 0) {
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $data['name'] = 'none';
while($row = array_shift($rows)) { $data['pretty'] = "-";
$data['additional'][] = $row; $data['additional'] = array();
return $data;
}
$data['additional'] = $results;
return $data;
} else {
// fetch specific authenticator details by id
$stmt = $pdo->prepare("SELECT * FROM `tfa`
WHERE `username` = :username AND `id` = :id AND `active` = '1'");
$stmt->execute(array(':username' => $username, ':id' => $id));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (isset($row["authmech"])) {
switch ($row["authmech"]) {
case "yubi_otp":
$data['name'] = "yubi_otp";
$data['pretty'] = "Yubico OTP";
$stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username AND `id` = :id");
$stmt->execute(array(
':username' => $username,
':id' => $id
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$data['additional'][] = $row;
}
return $data;
break;
// u2f - deprecated, should be removed
case "u2f":
$data['name'] = "u2f";
$data['pretty'] = "Fido U2F";
$stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username AND `id` = :id");
$stmt->execute(array(
':username' => $username,
':id' => $id
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$data['additional'][] = $row;
}
return $data;
break;
case "hotp":
$data['name'] = "hotp";
$data['pretty'] = "HMAC-based OTP";
return $data;
break;
case "totp":
$data['name'] = "totp";
$data['pretty'] = "Time-based OTP";
$stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username AND `id` = :id");
$stmt->execute(array(
':username' => $username,
':id' => $id
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$data['additional'][] = $row;
}
return $data;
break;
case "webauthn":
$data['name'] = "webauthn";
$data['pretty'] = "WebAuthn";
$stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username AND `id` = :id");
$stmt->execute(array(
':username' => $username,
':id' => $id
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$data['additional'][] = $row;
}
return $data;
break;
default:
$data['name'] = 'none';
$data['pretty'] = "-";
return $data;
break;
} }
return $data; }
break; else {
// u2f - deprecated, should be removed
case "u2f":
$data['name'] = "u2f";
$data['pretty'] = "Fido U2F";
$stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$data['additional'][] = $row;
}
return $data;
break;
case "hotp":
$data['name'] = "hotp";
$data['pretty'] = "HMAC-based OTP";
return $data;
break;
case "totp":
$data['name'] = "totp";
$data['pretty'] = "Time-based OTP";
$stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$data['additional'][] = $row;
}
return $data;
break;
case "webauthn":
$data['name'] = "webauthn";
$data['pretty'] = "WebAuthn";
$stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$data['additional'][] = $row;
}
return $data;
break;
default:
$data['name'] = 'none'; $data['name'] = 'none';
$data['pretty'] = "-"; $data['pretty'] = "-";
return $data; return $data;
break; }
} }
}
else {
$data['name'] = 'none';
$data['pretty'] = "-";
return $data;
}
} }
function verify_tfa_login($username, $_data, $WebAuthn) { function verify_tfa_login($username, $_data) {
global $pdo; global $pdo;
global $yubi; global $yubi;
global $u2f; global $u2f;
global $tfa; global $tfa;
global $WebAuthn;
if ($_data['tfa_method'] != 'u2f'){
$stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa`
WHERE `username` = :username AND `active` = '1'"); WHERE `username` = :username AND `id` = :id AND `active` = '1'");
$stmt->execute(array(':username' => $username)); $stmt->execute(array(':username' => $username, ':id' => $_data['id']));
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
switch ($row["authmech"]) { switch ($row["authmech"]) {
@@ -1597,9 +1645,10 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
$stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
WHERE `username` = :username WHERE `username` = :username
AND `authmech` = 'yubi_otp' AND `authmech` = 'yubi_otp'
AND `active`='1' AND `id` = :id
AND `active` = '1'
AND `secret` LIKE :modhex"); AND `secret` LIKE :modhex");
$stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id)); $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id, ':id' => $_data['id']));
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
$yubico_auth = explode(':', $row['secret']); $yubico_auth = explode(':', $row['secret']);
$yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]); $yubi = new Auth_Yubico($yubico_auth[0], $yubico_auth[1]);
@@ -1632,15 +1681,16 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
return false; return false;
break; break;
case "totp": case "totp":
try { try {
$stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa`
WHERE `username` = :username WHERE `username` = :username
AND `authmech` = 'totp' AND `authmech` = 'totp'
AND `id` = :id
AND `active`='1'"); AND `active`='1'");
$stmt->execute(array(':username' => $username)); $stmt->execute(array(':username' => $username, ':id' => $_data['id']));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) { foreach ($rows as $row) {
if ($tfa->verifyCode($row['secret'], $_data['token']) === true) { if ($tfa->verifyCode($row['secret'], $_data['token']) === true) {
$_SESSION['tfa_id'] = $row['id']; $_SESSION['tfa_id'] = $row['id'];
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
@@ -1648,7 +1698,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
'msg' => 'verified_totp_login' 'msg' => 'verified_totp_login'
); );
return true; return true;
} }
} }
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@@ -1656,23 +1706,16 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
'msg' => 'totp_verification_failed' 'msg' => 'totp_verification_failed'
); );
return false; return false;
} }
catch (PDOException $e) { catch (PDOException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'), 'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('mysql_error', $e) 'msg' => array('mysql_error', $e)
); );
return false; return false;
} }
break; break;
// u2f - deprecated, should be removed
case "u2f":
// delete old keys that used u2f
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = :authmech AND `username` = :username");
$stmt->execute(array(':authmech' => 'u2f', ':username' => $username));
return true;
case "webauthn": case "webauthn":
$tokenData = json_decode($_data['token']); $tokenData = json_decode($_data['token']);
$clientDataJSON = base64_decode($tokenData->clientDataJSON); $clientDataJSON = base64_decode($tokenData->clientDataJSON);
@@ -1681,13 +1724,20 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
$id = base64_decode($tokenData->id); $id = base64_decode($tokenData->id);
$challenge = $_SESSION['challenge']; $challenge = $_SESSION['challenge'];
$stmt = $pdo->prepare("SELECT `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `keyHandle` = :tokenId"); $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `id` = :id AND `active`='1'");
$stmt->execute(array(':tokenId' => $tokenData->id)); $stmt->execute(array(':id' => $_data['id']));
$process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC); $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($process_webauthn) || empty($process_webauthn['publicKey']) || empty($process_webauthn['username'])) return false; if (empty($process_webauthn)){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_verification_failed', 'authenticator not found')
);
return false;
}
if ($process_webauthn['publicKey'] === false) { if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'), 'log' => array(__FUNCTION__, $username, '*'),
@@ -1695,6 +1745,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
); );
return false; return false;
} }
try { try {
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']); $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
} }
@@ -1707,26 +1758,31 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
return false; return false;
} }
$stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username"); $stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username");
$stmt->execute(array(':username' => $process_webauthn['username'])); $stmt->execute(array(':username' => $process_webauthn['username']));
$obj_props = $stmt->fetch(PDO::FETCH_ASSOC); $obj_props = $stmt->fetch(PDO::FETCH_ASSOC);
if ($obj_props['superadmin'] === 1) { if ($obj_props['superadmin'] === 1) {
$_SESSION["mailcow_cc_role"] = "admin"; $_SESSION["mailcow_cc_role"] = "admin";
} }
elseif ($obj_props['superadmin'] === 0) { elseif ($obj_props['superadmin'] === 0) {
$_SESSION["mailcow_cc_role"] = "domainadmin"; $_SESSION["mailcow_cc_role"] = "domainadmin";
} }
else { else {
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username"); $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username");
$stmt->execute(array(':username' => $process_webauthn['username'])); $stmt->execute(array(':username' => $process_webauthn['username']));
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row['username'] == $process_webauthn['username']) { if (!empty($row['username'])) {
$_SESSION["mailcow_cc_role"] = "user"; $_SESSION["mailcow_cc_role"] = "user";
} } else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_verification_failed', 'could not determine user role')
);
return false;
}
} }
if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){ if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@@ -1736,9 +1792,8 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
return false; return false;
} }
$_SESSION["mailcow_cc_username"] = $process_webauthn['username']; $_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
$_SESSION['tfa_id'] = $process_webauthn['key_id']; $_SESSION['tfa_id'] = $process_webauthn['id'];
$_SESSION['authReq'] = null; $_SESSION['authReq'] = null;
unset($_SESSION["challenge"]); unset($_SESSION["challenge"]);
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -1759,6 +1814,17 @@ function verify_tfa_login($username, $_data, $WebAuthn) {
} }
return false; return false;
} else {
// delete old keys that used u2f
$stmt = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
$stmt->execute(array(':username' => $username));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($rows) == 0) return false;
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username");
$stmt->execute(array(':username' => $username));
return true;
}
} }
function admin_api($access, $action, $data = null) { function admin_api($access, $action, $data = null) {
global $pdo; global $pdo;

View File

@@ -336,9 +336,37 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$mins_interval = $_data['mins_interval']; $mins_interval = $_data['mins_interval'];
$enc1 = $_data['enc1']; $enc1 = $_data['enc1'];
$custom_params = (empty(trim($_data['custom_params']))) ? '' : trim($_data['custom_params']); $custom_params = (empty(trim($_data['custom_params']))) ? '' : trim($_data['custom_params']);
// Workaround, fixme
if (strpos($custom_params, 'pipemess')) { // validate custom params
$custom_params = ''; foreach (explode('-', $custom_params) as $param){
if(empty($param)) continue;
// extract option
if (str_contains($param, '=')) $param = explode('=', $param)[0];
else $param = rtrim($param, ' ');
// remove first char if first char is -
if ($param[0] == '-') $param = ltrim($param, $param[0]);
if (str_contains($param, ' ')) {
// bad char
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'bad character SPACE'
);
return false;
}
// check if param is whitelisted
if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){
// bad option
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'bad option '. $param
);
return false;
}
} }
if (empty($subfolder2)) { if (empty($subfolder2)) {
$subfolder2 = ""; $subfolder2 = "";
@@ -568,6 +596,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
)); ));
// save tags // save tags
foreach($tags as $index => $tag){ foreach($tags as $index => $tag){
if (empty($tag)) continue;
if ($index > $GLOBALS['TAGGING_LIMIT']) { if ($index > $GLOBALS['TAGGING_LIMIT']) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'warning', 'type' => 'warning',
@@ -598,7 +627,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
ratelimit('edit', 'domain', array('rl_value' => $_data['rl_value'], 'rl_frame' => $_data['rl_frame'], 'object' => $domain)); ratelimit('edit', 'domain', array('rl_value' => $_data['rl_value'], 'rl_frame' => $_data['rl_frame'], 'object' => $domain));
} }
if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) { if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) {
dkim('add', array('key_size' => $_data['key_size'], 'dkim_selector' => $_data['dkim_selector'], 'domains' => $domain)); if (!empty($redis->hGet('DKIM_SELECTORS', $domain))) {
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_add_dkim_available'
);
}
else {
dkim('add', array('key_size' => $_data['key_size'], 'dkim_selector' => $_data['dkim_selector'], 'domains' => $domain));
}
} }
if (!empty($restart_sogo)) { if (!empty($restart_sogo)) {
$restart_response = json_decode(docker('post', 'sogo-mailcow', 'restart'), true); $restart_response = json_decode(docker('post', 'sogo-mailcow', 'restart'), true);
@@ -928,7 +966,16 @@ 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'])) {
dkim('add', array('key_size' => $_data['key_size'], 'dkim_selector' => $_data['dkim_selector'], 'domains' => $alias_domain)); if (!empty($redis->hGet('DKIM_SELECTORS', $alias_domain))) {
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'domain_add_dkim_available'
);
}
else {
dkim('add', array('key_size' => $_data['key_size'], 'dkim_selector' => $_data['dkim_selector'], 'domains' => $alias_domain));
}
} }
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
@@ -1124,6 +1171,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
)); ));
// save tags // save tags
foreach($tags as $index => $tag){ foreach($tags as $index => $tag){
if (empty($tag)) continue;
if ($index > $GLOBALS['TAGGING_LIMIT']) { if ($index > $GLOBALS['TAGGING_LIMIT']) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'warning', 'type' => 'warning',
@@ -1744,8 +1792,37 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
continue; continue;
} }
if (strpos($custom_params, 'pipemess')) {
$custom_params = ''; // validate custom params
foreach (explode('-', $custom_params) as $param){
if(empty($param)) continue;
// extract option
if (str_contains($param, '=')) $param = explode('=', $param)[0];
else $param = rtrim($param, ' ');
// remove first char if first char is -
if ($param[0] == '-') $param = ltrim($param, $param[0]);
if (str_contains($param, ' ')) {
// bad char
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'bad character SPACE'
);
return false;
}
// check if param is whitelisted
if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){
// bad option
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'bad option '. $param
);
return false;
}
} }
if (empty($subfolder2)) { if (empty($subfolder2)) {
$subfolder2 = ""; $subfolder2 = "";
@@ -2201,8 +2278,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':gal' => $gal, ':gal' => $gal,
':domain' => $domain ':domain' => $domain
)); ));
// save tags, tag_name is unique // save tags
foreach($tags as $index => $tag){ foreach($tags as $index => $tag){
if (empty($tag)) continue;
if ($index > $GLOBALS['TAGGING_LIMIT']) { if ($index > $GLOBALS['TAGGING_LIMIT']) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'warning', 'type' => 'warning',
@@ -2368,8 +2446,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':description' => $description, ':description' => $description,
':domain' => $domain ':domain' => $domain
)); ));
// save tags, tag_name is unique // save tags
foreach($tags as $index => $tag){ foreach($tags as $index => $tag){
if (empty($tag)) continue;
if ($index > $GLOBALS['TAGGING_LIMIT']) { if ($index > $GLOBALS['TAGGING_LIMIT']) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'warning', 'type' => 'warning',
@@ -2712,6 +2791,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
)); ));
// save tags // save tags
foreach($tags as $index => $tag){ foreach($tags as $index => $tag){
if (empty($tag)) continue;
if ($index > $GLOBALS['TAGGING_LIMIT']) { if ($index > $GLOBALS['TAGGING_LIMIT']) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'warning', 'type' => 'warning',

View File

@@ -3,7 +3,7 @@ function init_db_schema() {
try { try {
global $pdo; global $pdo;
$db_version = "02052022_1500"; $db_version = "25072022_2300";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -440,7 +440,7 @@ function init_db_schema() {
"spam_score" => "TINYINT(1) NOT NULL DEFAULT '1'", "spam_score" => "TINYINT(1) NOT NULL DEFAULT '1'",
"spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'",
"delimiter_action" => "TINYINT(1) NOT NULL DEFAULT '1'", "delimiter_action" => "TINYINT(1) NOT NULL DEFAULT '1'",
"syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'", "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '0'",
"eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'", "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
"sogo_profile_reset" => "TINYINT(1) NOT NULL DEFAULT '0'", "sogo_profile_reset" => "TINYINT(1) NOT NULL DEFAULT '0'",
"pushover" => "TINYINT(1) NOT NULL DEFAULT '1'", "pushover" => "TINYINT(1) NOT NULL DEFAULT '1'",
@@ -738,8 +738,8 @@ function init_db_schema() {
"username" => "VARCHAR(255) NOT NULL", "username" => "VARCHAR(255) NOT NULL",
"authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')", "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')",
"secret" => "VARCHAR(255) DEFAULT NULL", "secret" => "VARCHAR(255) DEFAULT NULL",
"keyHandle" => "VARCHAR(255) DEFAULT NULL", "keyHandle" => "VARCHAR(1023) DEFAULT NULL",
"publicKey" => "VARCHAR(255) DEFAULT NULL", "publicKey" => "VARCHAR(4096) DEFAULT NULL",
"counter" => "INT NOT NULL DEFAULT '0'", "counter" => "INT NOT NULL DEFAULT '0'",
"certificate" => "TEXT", "certificate" => "TEXT",
"active" => "TINYINT(1) NOT NULL DEFAULT '0'" "active" => "TINYINT(1) NOT NULL DEFAULT '0'"
@@ -1227,8 +1227,16 @@ function init_db_schema() {
$pdo->query($create); $pdo->query($create);
} }
// Mitigate imapsync pipemess issue // Mitigate imapsync argument injection issue
$pdo->query("UPDATE `imapsync` SET `custom_params` = '' WHERE `custom_params` LIKE '%pipemess%';"); $pdo->query("UPDATE `imapsync` SET `custom_params` = ''
WHERE `custom_params` LIKE '%pipemess%'
OR custom_params LIKE '%skipmess%'
OR custom_params LIKE '%delete2foldersonly%'
OR custom_params LIKE '%delete2foldersbutnot%'
OR custom_params LIKE '%regexflag%'
OR custom_params LIKE '%pipemess%'
OR custom_params LIKE '%regextrans2%'
OR custom_params LIKE '%maxlinelengthcmd%';");
// Migrate webauthn tfa // Migrate webauthn tfa
$stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')"); $stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')");

View File

@@ -66,8 +66,9 @@ $qrprovider = new RobThree\Auth\Providers\Qr\QRServerProvider();
$tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider); $tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider);
// FIDO2 // FIDO2
$server_name = parse_url('https://' . $_SERVER['HTTP_HOST'], PHP_URL_HOST);
$formats = $GLOBALS['FIDO2_FORMATS']; $formats = $GLOBALS['FIDO2_FORMATS'];
$WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $_SERVER['HTTP_HOST'], $formats); $WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $server_name, $formats);
// only include root ca's when needed // only include root ca's when needed
if (getenv('WEBAUTHN_ONLY_TRUSTED_VENDORS') == 'y') $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates'); if (getenv('WEBAUTHN_ONLY_TRUSTED_VENDORS') == 'y') $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates');

View File

@@ -1,24 +1,24 @@
<?php <?php
if (isset($_POST["verify_tfa_login"])) { if (isset($_POST["verify_tfa_login"])) {
if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST, $WebAuthn)) { if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) {
$_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username']; $_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];
$_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role']; $_SESSION['mailcow_cc_role'] = $_SESSION['pending_mailcow_cc_role'];
unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_method']); unset($_SESSION['pending_tfa_methods']);
header("Location: /user"); header("Location: /user");
} else { } else {
unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_method']); unset($_SESSION['pending_tfa_methods']);
} }
} }
if (isset($_GET["cancel_tfa_login"])) { if (isset($_GET["cancel_tfa_login"])) {
unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_method']); unset($_SESSION['pending_tfa_methods']);
header("Location: /"); header("Location: /");
} }
@@ -34,6 +34,7 @@ if (isset($_POST["quick_delete"])) {
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
$login_user = strtolower(trim($_POST["login_user"])); $login_user = strtolower(trim($_POST["login_user"]));
$as = check_login($login_user, $_POST["pass_user"]); $as = check_login($login_user, $_POST["pass_user"]);
if ($as == "admin") { if ($as == "admin") {
$_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_username'] = $login_user;
$_SESSION['mailcow_cc_role'] = "admin"; $_SESSION['mailcow_cc_role'] = "admin";
@@ -47,22 +48,22 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
elseif ($as == "user") { elseif ($as == "user") {
$_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_username'] = $login_user;
$_SESSION['mailcow_cc_role'] = "user"; $_SESSION['mailcow_cc_role'] = "user";
$http_parameters = explode('&', $_SESSION['index_query_string']); $http_parameters = explode('&', $_SESSION['index_query_string']);
unset($_SESSION['index_query_string']); unset($_SESSION['index_query_string']);
if (in_array('mobileconfig', $http_parameters)) { if (in_array('mobileconfig', $http_parameters)) {
if (in_array('only_email', $http_parameters)) { if (in_array('only_email', $http_parameters)) {
header("Location: /mobileconfig.php?email_only"); header("Location: /mobileconfig.php?email_only");
die(); die();
} }
header("Location: /mobileconfig.php"); header("Location: /mobileconfig.php");
die(); die();
} }
header("Location: /user"); header("Location: /user");
} }
elseif ($as != "pending") { elseif ($as != "pending") {
unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_method']); unset($_SESSION['pending_tfa_methods']);
unset($_SESSION['mailcow_cc_username']); unset($_SESSION['mailcow_cc_username']);
unset($_SESSION['mailcow_cc_role']); unset($_SESSION['mailcow_cc_role']);
} }

View File

@@ -100,6 +100,7 @@ $AVAILABLE_LANGUAGES = array(
'ru' => 'Pусский (Russian)', 'ru' => 'Pусский (Russian)',
'sk' => 'Slovenčina (Slovak)', 'sk' => 'Slovenčina (Slovak)',
'sv' => 'Svenska (Swedish)', 'sv' => 'Svenska (Swedish)',
'uk' => 'Українська (Ukrainian)',
'zh' => '中文 (Chinese)' 'zh' => '中文 (Chinese)'
); );
@@ -227,3 +228,131 @@ $RSPAMD_MAPS = array(
'Monitoring Hosts' => 'monitoring_nolog.map' 'Monitoring Hosts' => 'monitoring_nolog.map'
) )
); );
$IMAPSYNC_OPTIONS = array(
'whitelist' => array(
'authmech1',
'authmech2',
'authuser1',
'authuser2',
'debugcontent',
'disarmreadreceipts',
'logdir',
'debugcrossduplicates',
'maxsize',
'minsize',
'minage',
'search',
'noabletosearch',
'pidfile',
'pidfilelocking',
'search1',
'search2',
'sslargs1',
'sslargs2',
'syncduplicates',
'usecache',
'synclabels',
'truncmess',
'domino2',
'expunge1',
'filterbuggyflags',
'justconnect',
'justfolders',
'maxlinelength',
'useheader',
'noabletosearch1',
'nolog',
'prefix1',
'prefix2',
'sep1',
'sep2',
'nofoldersizesatend',
'justfoldersizes',
'proxyauth1',
'skipemptyfolders',
'include',
'subfolder1',
'subscribed',
'subscribe',
'debug',
'debugimap2',
'domino1',
'exchange1',
'exchange2',
'justlogin',
'keepalive1',
'keepalive2',
'noabletosearch2',
'noexpunge2',
'noresyncflags',
'nossl1',
'nouidexpunge2',
'syncinternaldates',
'idatefromheader',
'useuid',
'debugflags',
'debugimap',
'delete1emptyfolders',
'delete2folders',
'gmail2',
'office1',
'testslive6',
'debugimap1',
'errorsmax',
'tests',
'gmail1',
'maxmessagespersecond',
'maxbytesafter',
'maxsleep',
'abort',
'resyncflags',
'resynclabels',
'syncacls',
'nosyncacls',
'nousecache',
'office2',
'testslive',
'debugmemory',
'exitwhenover',
'noid',
'noexpunge1',
'authmd51',
'logfile',
'proxyauth2',
'domain1',
'domain2',
'oauthaccesstoken1',
'oauthaccesstoken2',
'oauthdirect1',
'oauthdirect2',
'folder',
'folderrec',
'folderfirst',
'folderlast',
'nomixfolders',
'authmd52',
'debugfolders',
'nossl2',
'ssl2',
'tls2',
'notls2',
'debugssl',
'notls1',
'inet4',
'inet6',
'log',
'showpasswords'
),
'blacklist' => array(
'skipmess',
'delete2foldersonly',
'delete2foldersbutnot',
'regexflag',
'regexmess',
'pipemess',
'regextrans2',
'maxlinelengthcmd'
)
);

View File

@@ -290,6 +290,7 @@ $(document).ready(function() {
var tagValuesElem = $(tagboxElem).find(".tag-values")[0]; var tagValuesElem = $(tagboxElem).find(".tag-values")[0];
var tag = escapeHtml($(tagInputElem).val()); var tag = escapeHtml($(tagInputElem).val());
if (!tag) return;
var value_tags = []; var value_tags = [];
try { try {
value_tags = JSON.parse($(tagValuesElem).val()); value_tags = JSON.parse($(tagValuesElem).val());

View File

@@ -99,37 +99,6 @@ $(document).ready(function() {
}); });
auto_fill_quota($('#addSelectDomain').val()); auto_fill_quota($('#addSelectDomain').val());
// Read bcc local dests
// Using ajax to not be a blocking moo
$.get("/api/v1/get/bcc-destination-options", function(data){
// Domains
var optgroup = "<optgroup label='" + lang.domains + "'>";
$.each(data.domains, function(index, domain){
optgroup += "<option value='" + domain + "'>" + domain + "</option>"
});
optgroup += "</optgroup>"
$('#bcc-local-dest').append(optgroup);
// Alias domains
var optgroup = "<optgroup label='" + lang.domain_aliases + "'>";
$.each(data.alias_domains, function(index, alias_domain){
optgroup += "<option value='" + alias_domain + "'>" + alias_domain + "</option>"
});
optgroup += "</optgroup>"
$('#bcc-local-dest').append(optgroup);
// Mailboxes and aliases
$.each(data.mailboxes, function(mailbox, aliases){
var optgroup = "<optgroup label='" + mailbox + "'>";
$.each(aliases, function(index, alias){
optgroup += "<option value='" + alias + "'>" + alias + "</option>"
});
optgroup += "</optgroup>"
$('#bcc-local-dest').append(optgroup);
});
// Finish
$('#bcc-local-dest').find('option:selected').remove();
$('#bcc-local-dest').selectpicker('refresh');
});
$(".goto_checkbox").click(function( event ) { $(".goto_checkbox").click(function( event ) {
$("form[data-id='add_alias'] .goto_checkbox").not(this).prop('checked', false); $("form[data-id='add_alias'] .goto_checkbox").not(this).prop('checked', false);
if ($("form[data-id='add_alias'] .goto_checkbox:checked").length > 0) { if ($("form[data-id='add_alias'] .goto_checkbox:checked").length > 0) {
@@ -584,6 +553,7 @@ jQuery(function($){
'</div>'; '</div>';
item.chkbox = '<input type="checkbox" data-id="resource" name="multi_select" value="' + encodeURIComponent(item.name) + '" />'; item.chkbox = '<input type="checkbox" data-id="resource" name="multi_select" value="' + encodeURIComponent(item.name) + '" />';
item.name = escapeHtml(item.name); item.name = escapeHtml(item.name);
item.description = escapeHtml(item.description);
}); });
} }
}), }),
@@ -623,6 +593,37 @@ jQuery(function($){
}); });
} }
function draw_bcc_table() { function draw_bcc_table() {
// Read bcc local dests
// Using ajax to not be a blocking moo
$.get("/api/v1/get/bcc-destination-options", function(data){
// Domains
var optgroup = "<optgroup label='" + lang.domains + "'>";
$.each(data.domains, function(index, domain){
optgroup += "<option value='" + domain + "'>" + domain + "</option>"
});
optgroup += "</optgroup>"
$('#bcc-local-dest').append(optgroup);
// Alias domains
var optgroup = "<optgroup label='" + lang.domain_aliases + "'>";
$.each(data.alias_domains, function(index, alias_domain){
optgroup += "<option value='" + alias_domain + "'>" + alias_domain + "</option>"
});
optgroup += "</optgroup>"
$('#bcc-local-dest').append(optgroup);
// Mailboxes and aliases
$.each(data.mailboxes, function(mailbox, aliases){
var optgroup = "<optgroup label='" + mailbox + "'>";
$.each(aliases, function(index, alias){
optgroup += "<option value='" + alias + "'>" + alias + "</option>"
});
optgroup += "</optgroup>"
$('#bcc-local-dest').append(optgroup);
});
// Finish
$('#bcc-local-dest').find('option:selected').remove();
$('#bcc-local-dest').selectpicker('refresh');
});
ft_bcc_table = FooTable.init('#bcc_table', { ft_bcc_table = FooTable.init('#bcc_table', {
"columns": [ "columns": [
{"name":"chkbox","title":"","style":{"min-width":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"}, {"name":"chkbox","title":"","style":{"min-width":"60px","width":"60px"},"filterable": false,"sortable": false,"type":"html"},
@@ -1022,7 +1023,7 @@ jQuery(function($){
if (!item.exclude > 0) { if (!item.exclude > 0) {
item.exclude = '-'; item.exclude = '-';
} else { } else {
item.exclude = '<code>' + item.exclude + '</code>'; item.exclude = '<code>' + escapeHtml(item.exclude) + '</code>';
} }
item.server_w_port = escapeHtml(item.user1) + '@' + item.host1 + ':' + item.port1; item.server_w_port = escapeHtml(item.user1) + '@' + item.host1 + ':' + item.port1;
item.action = '<div class="btn-group footable-actions">' + item.action = '<div class="btn-group footable-actions">' +
@@ -1160,15 +1161,33 @@ jQuery(function($){
event.stopPropagation(); event.stopPropagation();
}) })
draw_domain_table(); // detect element visibility changes
draw_mailbox_table(); function onVisible(element, callback) {
draw_resource_table(); $(element).ready(function() {
draw_alias_table(); element_object = document.querySelector(element)
draw_aliasdomain_table(); new IntersectionObserver((entries, observer) => {
draw_sync_job_table(); entries.forEach(entry => {
draw_filter_table(); if(entry.intersectionRatio > 0) {
draw_bcc_table(); callback(element_object);
draw_recipient_map_table(); observer.disconnect();
draw_tls_policy_table(); }
});
}).observe(element_object);
});
}
// Load only if the tab is visible
onVisible("[id^=tab-domains]", () => draw_domain_table());
onVisible("[id^=tab-mailboxes]", () => draw_mailbox_table());
onVisible("[id^=tab-resources]", () => draw_resource_table());
onVisible("[id^=tab-mbox-aliases]", () => draw_alias_table());
onVisible("[id^=tab-domain-aliases]", () => draw_aliasdomain_table());
onVisible("[id^=tab-syncjobs]", () => draw_sync_job_table());
onVisible("[id^=tab-filters]", () => draw_filter_table());
onVisible("[id^=tab-bcc]", () => {
draw_bcc_table();
draw_recipient_map_table();
});
onVisible("[id^=tab-tls-policy]", () => draw_tls_policy_table());
}); });

View File

@@ -178,15 +178,22 @@ if (isset($_GET['query'])) {
// parse post data // parse post data
$post = trim(file_get_contents('php://input')); $post = trim(file_get_contents('php://input'));
if ($post) $post = json_decode($post); if ($post) $post = json_decode($post);
// decode base64 strings
$clientDataJSON = base64_decode($post->clientDataJSON);
$attestationObject = base64_decode($post->attestationObject);
// process registration data from authenticator // process registration data from authenticator
try { try {
// decode base64 strings
$clientDataJSON = base64_decode($post->clientDataJSON);
$attestationObject = base64_decode($post->attestationObject);
// processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true) // processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true)
$data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $_SESSION['challenge'], false, true); $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $_SESSION['challenge'], false, true);
// safe authenticator in mysql `tfa` table
$_data['tfa_method'] = $post->tfa_method;
$_data['key_id'] = $post->key_id;
$_data['confirm_password'] = $post->confirm_password;
$_data['registration'] = $data;
set_tfa($_data);
} }
catch (Throwable $ex) { catch (Throwable $ex) {
// err // err
@@ -197,11 +204,6 @@ if (isset($_GET['query'])) {
exit; exit;
} }
// safe authenticator in mysql `tfa` table
$_data['tfa_method'] = $post->tfa_method;
$_data['key_id'] = $post->key_id;
$_data['registration'] = $data;
set_tfa($_data);
// send response // send response
$return = new stdClass(); $return = new stdClass();
@@ -419,7 +421,7 @@ if (isset($_GET['query'])) {
// } // }
$ids = NULL; $ids = NULL;
$getArgs = $WebAuthn->getGetArgs($ids, 30, true, true, true, true, $GLOBALS['FIDO2_UV_FLAG_LOGIN']); $getArgs = $WebAuthn->getGetArgs($ids, 30, false, false, false, false, $GLOBALS['FIDO2_UV_FLAG_LOGIN']);
print(json_encode($getArgs)); print(json_encode($getArgs));
$_SESSION['challenge'] = $WebAuthn->getChallenge(); $_SESSION['challenge'] = $WebAuthn->getChallenge();
return; return;
@@ -428,8 +430,11 @@ if (isset($_GET['query'])) {
case "webauthn-tfa-registration": case "webauthn-tfa-registration":
if (isset($_SESSION["mailcow_cc_role"])) { if (isset($_SESSION["mailcow_cc_role"])) {
// Exclude existing CredentialIds, if any // Exclude existing CredentialIds, if any
$stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username"); $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username AND authmech = :authmech");
$stmt->execute(array(':username' => $_SESSION['mailcow_cc_username'])); $stmt->execute(array(
':username' => $_SESSION['mailcow_cc_username'],
':authmech' => 'webauthn'
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) { while($row = array_shift($rows)) {
$excludeCredentialIds[] = base64_decode($row['keyHandle']); $excludeCredentialIds[] = base64_decode($row['keyHandle']);
@@ -450,20 +455,24 @@ if (isset($_GET['query'])) {
} }
break; break;
case "webauthn-tfa-get-args": case "webauthn-tfa-get-args":
$stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username"); $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username AND authmech = :authmech");
$stmt->execute(array(':username' => $_SESSION['pending_mailcow_cc_username'])); $stmt->execute(array(
':username' => $_SESSION['pending_mailcow_cc_username'],
':authmech' => 'webauthn'
));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) { if (count($rows) == 0) {
$cids[] = base64_decode($row['keyHandle']);
}
if (count($cids) == 0) {
print(json_encode(array( print(json_encode(array(
'type' => 'error', 'type' => 'error',
'msg' => 'Cannot find matching credentialIds' 'msg' => 'Cannot find matching credentialIds'
))); )));
exit;
}
while($row = array_shift($rows)) {
$cids[] = base64_decode($row['keyHandle']);
} }
$getArgs = $WebAuthn->getGetArgs($cids, 30, true, true, true, true, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN']); $getArgs = $WebAuthn->getGetArgs($cids, 30, false, false, false, false, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN']);
$getArgs->publicKey->extensions = array('appid' => "https://".$getArgs->publicKey->rpId); $getArgs->publicKey->extensions = array('appid' => "https://".$getArgs->publicKey->rpId);
print(json_encode($getArgs)); print(json_encode($getArgs));
$_SESSION['challenge'] = $WebAuthn->getChallenge(); $_SESSION['challenge'] = $WebAuthn->getChallenge();
@@ -989,14 +998,19 @@ if (isset($_GET['query'])) {
if (isset($_GET['tags']) && $_GET['tags'] != '') if (isset($_GET['tags']) && $_GET['tags'] != '')
$tags = explode(',', $_GET['tags']); $tags = explode(',', $_GET['tags']);
$mailboxes = mailbox('get', 'mailboxes', $object, $tags); if ($tags === null) {
if (!empty($mailboxes)) { $data = mailbox('get', 'mailbox_details', $object);
foreach ($mailboxes as $mailbox) { process_get_return($data);
if ($details = mailbox('get', 'mailbox_details', $mailbox)) $data[] = $details; } else {
else continue; $mailboxes = mailbox('get', 'mailboxes', $object, $tags);
if (is_array($mailboxes)) {
foreach ($mailboxes as $mailbox) {
if ($details = mailbox('get', 'mailbox_details', $mailbox))
$data[] = $details;
}
} }
process_get_return($data, false);
} }
process_get_return($data);
break; break;
} }
break; break;

View File

@@ -106,7 +106,8 @@
"timeout2": "Timeout für Verbindung zum lokalen Host", "timeout2": "Timeout für Verbindung zum lokalen Host",
"username": "Benutzername", "username": "Benutzername",
"validate": "Validieren", "validate": "Validieren",
"validation_success": "Erfolgreich validiert" "validation_success": "Erfolgreich validiert",
"tags": "Tags"
}, },
"admin": { "admin": {
"access": "Zugang", "access": "Zugang",
@@ -920,6 +921,7 @@
"deleted_syncjob": "Sync-Jobs-ID %s gelöscht", "deleted_syncjob": "Sync-Jobs-ID %s gelöscht",
"deleted_syncjobs": "Sync-Jobs gelöscht: %s", "deleted_syncjobs": "Sync-Jobs gelöscht: %s",
"dkim_added": "DKIM-Key %s wurde hinzugefügt", "dkim_added": "DKIM-Key %s wurde hinzugefügt",
"domain_add_dkim_available": "Ein DKIM-Key existierte bereits",
"dkim_duplicated": "DKIM-Key der Domain %s wurde auf Domain %s kopiert", "dkim_duplicated": "DKIM-Key der Domain %s wurde auf Domain %s kopiert",
"dkim_removed": "DKIM-Key %s wurde entfernt", "dkim_removed": "DKIM-Key %s wurde entfernt",
"domain_added": "Domain %s wurde angelegt", "domain_added": "Domain %s wurde angelegt",

View File

@@ -928,6 +928,7 @@
"deleted_syncjob": "Deleted syncjob ID %s", "deleted_syncjob": "Deleted syncjob ID %s",
"deleted_syncjobs": "Deleted syncjobs: %s", "deleted_syncjobs": "Deleted syncjobs: %s",
"dkim_added": "DKIM key %s has been saved", "dkim_added": "DKIM key %s has been saved",
"domain_add_dkim_available": "A DKIM key did already exist",
"dkim_duplicated": "DKIM key for domain %s has been copied to %s", "dkim_duplicated": "DKIM key for domain %s has been copied to %s",
"dkim_removed": "DKIM key %s has been removed", "dkim_removed": "DKIM key %s has been removed",
"domain_added": "Added domain %s", "domain_added": "Added domain %s",

View File

@@ -19,7 +19,8 @@
"syncjobs": "Trabajos de sincronización", "syncjobs": "Trabajos de sincronización",
"tls_policy": "Póliza de TLS", "tls_policy": "Póliza de TLS",
"unlimited_quota": "Cuota ilimitada para buzones", "unlimited_quota": "Cuota ilimitada para buzones",
"app_passwds": "Gestionar las contraseñas de aplicaciones" "app_passwds": "Gestionar las contraseñas de aplicaciones",
"domain_desc": "Cambiar descripción del dominio"
}, },
"add": { "add": {
"activate_filter_warn": "Todos los demás filtros se desactivarán cuando este filtro se active.", "activate_filter_warn": "Todos los demás filtros se desactivarán cuando este filtro se active.",

View File

@@ -2,8 +2,8 @@
"acl": { "acl": {
"alias_domains": "Aggiungi alias di dominio", "alias_domains": "Aggiungi alias di dominio",
"app_passwds": "Gestisci le password delle app", "app_passwds": "Gestisci le password delle app",
"bcc_maps": "BCC maps", "bcc_maps": "Mappe CCN",
"delimiter_action": "Delimiter action", "delimiter_action": "Azione delimitatrice",
"domain_desc": "Modifica la descrizione del dominio", "domain_desc": "Modifica la descrizione del dominio",
"domain_relayhost": "Modifica relayhost per un dominio", "domain_relayhost": "Modifica relayhost per un dominio",
"eas_reset": "Ripristina i dispositivi EAS", "eas_reset": "Ripristina i dispositivi EAS",
@@ -106,7 +106,8 @@
"validate": "Convalida", "validate": "Convalida",
"validation_success": "Convalidato con successo", "validation_success": "Convalidato con successo",
"bcc_dest_format": "Il destinatario in copia nascosta deve essere un singolo indirizzo email.<br>Se si vuole spedire una copia del messaggio a più destinatari, bisogna creare un alias ed utilizzarlo per questa opzione.", "bcc_dest_format": "Il destinatario in copia nascosta deve essere un singolo indirizzo email.<br>Se si vuole spedire una copia del messaggio a più destinatari, bisogna creare un alias ed utilizzarlo per questa opzione.",
"app_passwd_protocols": "Protocolli consentiti per la password dell'app" "app_passwd_protocols": "Protocolli consentiti per la password dell'app",
"tags": "Tag"
}, },
"admin": { "admin": {
"access": "Accedi", "access": "Accedi",
@@ -983,7 +984,7 @@
"enter_qr_code": "Il codice TOTP se il tuo dispositivo non è in grado di acquisire i codici QR", "enter_qr_code": "Il codice TOTP se il tuo dispositivo non è in grado di acquisire i codici QR",
"error_code": "Codice di errore", "error_code": "Codice di errore",
"init_webauthn": "Inizializzazione, attendere prego...", "init_webauthn": "Inizializzazione, attendere prego...",
"key_id": "Identificatore per il tuo YubiKey", "key_id": "Identificatore per il tuo dispositivo",
"key_id_totp": "Identificatore per la tua chiave", "key_id_totp": "Identificatore per la tua chiave",
"none": "Disattivato", "none": "Disattivato",
"reload_retry": "- (ricaricare la pagina se l'errore persiste)", "reload_retry": "- (ricaricare la pagina se l'errore persiste)",
@@ -997,7 +998,9 @@
"waiting_usb_auth": "<i>In attesa del device USB...</i><br /><br />Tocca ora il pulsante sul dispositivo WebAuthn USB.", "waiting_usb_auth": "<i>In attesa del device USB...</i><br /><br />Tocca ora il pulsante sul dispositivo WebAuthn USB.",
"waiting_usb_register": "<i>In attesa del device USB...</i><br /><br />Inserisci la tua password qui sopra e conferma la tua registrazione WebAuthn toccando il pulsante del dispositivo WebAuthn USB.", "waiting_usb_register": "<i>In attesa del device USB...</i><br /><br />Inserisci la tua password qui sopra e conferma la tua registrazione WebAuthn toccando il pulsante del dispositivo WebAuthn USB.",
"yubi_otp": "Autenticazione Yubico OTP", "yubi_otp": "Autenticazione Yubico OTP",
"tfa_token_invalid": "Token TFA non valido" "tfa_token_invalid": "Token TFA non valido",
"u2f_deprecated": "Sembra che la tua chiave sia stata registrata utilizzando il metodo U2F deprecato. Disattiveremo Two-Factor-Authenticaiton per te e cancelleremo la tua chiave.",
"u2f_deprecated_important": "Registra la tua chiave nel pannello di amministrazione con il nuovo metodo WebAuthn."
}, },
"user": { "user": {
"action": "Azione", "action": "Azione",

View File

@@ -105,7 +105,9 @@
"timeout2": "Тайм-аут для подключения к локальному хосту", "timeout2": "Тайм-аут для подключения к локальному хосту",
"username": "Имя пользователя", "username": "Имя пользователя",
"validate": "Проверить", "validate": "Проверить",
"validation_success": "Проверка прошла успешно" "validation_success": "Проверка прошла успешно",
"tags": "Теги",
"app_passwd_protocols": "Разрешенные протоколы для пароля приложения"
}, },
"admin": { "admin": {
"access": "Настройки доступа", "access": "Настройки доступа",
@@ -190,7 +192,7 @@
"flush_queue": "Отправить все сообщения", "flush_queue": "Отправить все сообщения",
"forwarding_hosts": "Переадресация хостов", "forwarding_hosts": "Переадресация хостов",
"forwarding_hosts_add_hint": "Можно указывать: IPv4/IPv6 подсети в нотации CIDR, имена хостов (которые будут разрешаться в IP-адреса) или доменные имена (которые будут решаться с IP-адресами путем запроса SPF записей или, в случае их отсутствия - запросом MX записей).", "forwarding_hosts_add_hint": "Можно указывать: IPv4/IPv6 подсети в нотации CIDR, имена хостов (которые будут разрешаться в IP-адреса) или доменные имена (которые будут решаться с IP-адресами путем запроса SPF записей или, в случае их отсутствия - запросом MX записей).",
"forwarding_hosts_hint": "Входящие сообщения безоговорочно принимаются от любых хостов, перечисленных здесь. Эти хосты не проходят проверку DNSBL и graylisting. Спам, полученный от них, никогда не отклоняется, но при желании можно включить спам фильтр и письма с плохим рейтингом будут попадать в Junk. Наиболее распространенное использование - указать почтовые серверы, на которых вы установили правило, которое перенаправляет входящие электронные письма на ваш почтовый сервер.", "forwarding_hosts_hint": "Входящие сообщения безоговорочно принимаются от любых хостов, перечисленных здесь. Эти хосты не проходят проверку DNSBL и graylisting. Спам, полученный от них, никогда не отклоняется, но при желании можно включить спам фильтр и письма с плохим рейтингом будут попадать в Junk. Наиболее распространенное использование - указать почтовые серверы, на которых вы установили правило, которое перенаправляет входящие электронные письма на ваш почтовый сервер mailcow.",
"from": "От", "from": "От",
"generate": "сгенерировать", "generate": "сгенерировать",
"guid": "GUID - уникальный ID", "guid": "GUID - уникальный ID",
@@ -460,7 +462,8 @@
"unlimited_quota_acl": "Неограниченная квота запрещена политикой доступа", "unlimited_quota_acl": "Неограниченная квота запрещена политикой доступа",
"username_invalid": "Имя пользователя %s нельзя использовать", "username_invalid": "Имя пользователя %s нельзя использовать",
"validity_missing": "Пожалуйста, назначьте срок действия", "validity_missing": "Пожалуйста, назначьте срок действия",
"value_missing": "Пожалуйста заполните все поля" "value_missing": "Пожалуйста заполните все поля",
"yotp_verification_failed": "Ошибка валидации Yubico OTP: %s"
}, },
"debug": { "debug": {
"chart_this_server": "Диаграмма (текущий сервер)", "chart_this_server": "Диаграмма (текущий сервер)",
@@ -886,11 +889,11 @@
"type": "Тип" "type": "Тип"
}, },
"ratelimit": { "ratelimit": {
"disabled": "Отключен", "disabled": "Отключен",
"second": "сообщений / секунду", "second": "сообщений / секунду",
"minute": "сообщений / минуту", "minute": "сообщений / минуту",
"hour": "сообщений / час", "hour": "сообщений / час",
"day": "сообщений / день" "day": "сообщений / день"
}, },
"start": { "start": {
"help": "Справка", "help": "Справка",
@@ -985,7 +988,7 @@
"enter_qr_code": "Ваш код TOTP, если устройство не может отсканировать QR-код", "enter_qr_code": "Ваш код TOTP, если устройство не может отсканировать QR-код",
"error_code": "Код ошибки", "error_code": "Код ошибки",
"init_webauthn": "Инициализация, пожалуйста, подождите...", "init_webauthn": "Инициализация, пожалуйста, подождите...",
"key_id": "Идентификатор YubiKey ключа", "key_id": "Идентификатор вашего устройства",
"key_id_totp": "Идентификатор TOTP ключа", "key_id_totp": "Идентификатор TOTP ключа",
"none": "Отключить", "none": "Отключить",
"reload_retry": "- (перезагрузить страницу браузера или почистите кеш/cookies, если ошибка повторяется)", "reload_retry": "- (перезагрузить страницу браузера или почистите кеш/cookies, если ошибка повторяется)",
@@ -999,7 +1002,8 @@
"webauthn": "WebAuthn аутентификация", "webauthn": "WebAuthn аутентификация",
"waiting_usb_auth": "<i>Ожидание устройства USB...</i><br><br>Пожалуйста, нажмите кнопку на USB устройстве сейчас.", "waiting_usb_auth": "<i>Ожидание устройства USB...</i><br><br>Пожалуйста, нажмите кнопку на USB устройстве сейчас.",
"waiting_usb_register": "<i>Ожидание устройства USB...</i><br><br>Пожалуйста, введите пароль выше и подтвердите регистрацию, нажав кнопку на USB устройстве.", "waiting_usb_register": "<i>Ожидание устройства USB...</i><br><br>Пожалуйста, введите пароль выше и подтвердите регистрацию, нажав кнопку на USB устройстве.",
"yubi_otp": "Yubico OTP аутентификация" "yubi_otp": "Yubico OTP аутентификация",
"u2f_deprecated": "Похоже, что ваш ключ был зарегистрирован с использованием устаревшего метода U2F. Мы деактивируем для вас двухфакторную аутентификацию и удалим ваш ключ."
}, },
"user": { "user": {
"action": "Действия", "action": "Действия",

1187
data/web/lang/lang.uk.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -176,15 +176,62 @@ function recursiveBase64StrToArrayBuffer(obj) {
{% endfor %} {% endfor %}
// Confirm TFA modal // Confirm TFA modal
{% if pending_tfa_method %} {% if pending_tfa_methods %}
$('#ConfirmTFAModal').modal({ $('#ConfirmTFAModal').modal({
backdrop: 'static', backdrop: 'static',
keyboard: false keyboard: false
}); });
// validate Yubi OTP tfa
$("#pending_tfa_tab_yubi_otp").click(function(){
$(".totp-authenticator-selection").removeClass("active");
$(".webauthn-authenticator-selection").removeClass("active");
$("#collapseTotpTFA").collapse('hide');
$("#collapseWebAuthnTFA").collapse('hide');
});
$(".yubi-authenticator-selection").click(function(){
$(".yubi-authenticator-selection").removeClass("active");
$(this).addClass("active");
var id = $(this).children('input').first().val();
$("#yubi_selected_id").val(id);
$("#collapseYubiTFA").collapse('show');
});
// validate Time based OTP tfa
$("#pending_tfa_tab_totp").click(function(){
$(".yubi-authenticator-selection").removeClass("active");
$(".webauthn-authenticator-selection").removeClass("active");
$("#collapseYubiTFA").collapse('hide');
$("#collapseWebAuthnTFA").collapse('hide');
});
$(".totp-authenticator-selection").click(function(){
$(".totp-authenticator-selection").removeClass("active");
$(this).addClass("active");
var id = $(this).children('input').first().val();
$("#totp_selected_id").val(id);
$("#collapseTotpTFA").collapse('show');
});
// validate WebAuthn tfa // validate WebAuthn tfa
$('#start_webauthn_confirmation').click(function(){ $("#pending_tfa_tab_webauthn").click(function(){
$('#webauthn_status_auth').html('<p><i class="bi bi-arrow-repeat icon-spin"></i> ' + lang_tfa.init_webauthn + '</p>'); $(".totp-authenticator-selection").removeClass("active");
$(".yubi-authenticator-selection").removeClass("active");
$("#collapseTotpTFA").collapse('hide');
$("#collapseYubiTFA").collapse('hide');
});
$(".webauthn-authenticator-selection").click(function(){
$(".webauthn-authenticator-selection").removeClass("active");
$(this).addClass("active");
var id = $(this).children('input').first().val();
$("#webauthn_selected_id").val(id);
$("#collapseWebAuthnTFA").collapse('show');
$(this).find('input[name=token]').focus(); $(this).find('input[name=token]').focus();
if(document.getElementById("webauthn_auth_data") !== null) { if(document.getElementById("webauthn_auth_data") !== null) {
@@ -198,30 +245,32 @@ function recursiveBase64StrToArrayBuffer(obj) {
window.fetch("/api/v1/get/webauthn-tfa-get-args", {method:'GET',cache:'no-cache'}).then(response => { window.fetch("/api/v1/get/webauthn-tfa-get-args", {method:'GET',cache:'no-cache'}).then(response => {
return response.json(); return response.json();
}).then(json => { }).then(json => {
if (json.success === false) throw new Error(); console.log(json);
if (json.success === false) throw new Error();
if (json.type === "error") throw new Error(json.msg);
recursiveBase64StrToArrayBuffer(json); recursiveBase64StrToArrayBuffer(json);
return json; return json;
}).then(getCredentialArgs => { }).then(getCredentialArgs => {
// get credentials // get credentials
return navigator.credentials.get(getCredentialArgs); return navigator.credentials.get(getCredentialArgs);
}).then(cred => { }).then(cred => {
return { return {
id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null, id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null, authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
}; };
}).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) { }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
// send request by submit // send request by submit
var form = document.getElementById('webauthn_auth_form'); var form = document.getElementById('webauthn_auth_form');
var auth = document.getElementById('webauthn_auth_data'); var auth = document.getElementById('webauthn_auth_data');
auth.value = AuthenticatorAttestationResponse; auth.value = AuthenticatorAttestationResponse;
form.submit(); form.submit();
}).catch(function(err) { }).catch(function(err) {
var webauthn_return_code = document.getElementById('webauthn_return_code'); var webauthn_return_code = document.getElementById('webauthn_return_code');
webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null; webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null;
webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry; webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry;
}); });
} }
}); });
@@ -237,7 +286,9 @@ function recursiveBase64StrToArrayBuffer(obj) {
} }
}); });
}); });
{% endif %} {% endif %}
// Validate FIDO2 // Validate FIDO2
$("#fido2-login").click(function(){ $("#fido2-login").click(function(){
$('#fido2-alerts').html(); $('#fido2-alerts').html();
@@ -358,11 +409,13 @@ function recursiveBase64StrToArrayBuffer(obj) {
$("#start_webauthn_register").click(() => { $("#start_webauthn_register").click(() => {
var key_id = document.getElementsByName('key_id')[1].value; var key_id = document.getElementsByName('key_id')[1].value;
var confirm_password = document.getElementsByName('confirm_password')[1].value;
// fetch WebAuthn create args // fetch WebAuthn create args
window.fetch("/api/v1/get/webauthn-tfa-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(response => { window.fetch("/api/v1/get/webauthn-tfa-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(response => {
return response.json(); return response.json();
}).then(json => { }).then(json => {
console.log(json);
if (json.success === false) throw new Error(json.msg); if (json.success === false) throw new Error(json.msg);
recursiveBase64StrToArrayBuffer(json); recursiveBase64StrToArrayBuffer(json);
@@ -375,7 +428,8 @@ function recursiveBase64StrToArrayBuffer(obj) {
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null, attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null,
key_id: key_id, key_id: key_id,
tfa_method: "webauthn" tfa_method: "webauthn",
confirm_password: confirm_password
}; };
}).then(JSON.stringify).then(AuthenticatorAttestationResponse => { }).then(JSON.stringify).then(AuthenticatorAttestationResponse => {
// send request // send request

0
data/web/templates/cache/.gitkeep vendored Normal file
View File

View File

@@ -28,7 +28,7 @@
<div class="col-sm-9 col-xs-7"> <div class="col-sm-9 col-xs-7">
<select id="selectTFA" class="selectpicker" title="{{ lang.tfa.select }}"> <select id="selectTFA" class="selectpicker" title="{{ lang.tfa.select }}">
<option value="yubi_otp">{{ lang.tfa.yubi_otp }}</option> <option value="yubi_otp">{{ lang.tfa.yubi_otp }}</option>
<option value="u2f">{{ lang.tfa.u2f }}</option> <option value="webauthn">{{ lang.tfa.webauthn }}</option>
<option value="totp">{{ lang.tfa.totp }}</option> <option value="totp">{{ lang.tfa.totp }}</option>
<option value="none">{{ lang.tfa.none }}</option> <option value="none">{{ lang.tfa.none }}</option>
</select> </select>

View File

@@ -133,73 +133,174 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if pending_tfa_method %} {% if pending_tfa_methods %}
<div class="modal fade" id="ConfirmTFAModal" tabindex="-1" role="dialog" aria-labelledby="ConfirmTFAModalLabel"> <div class="modal fade" id="ConfirmTFAModal" tabindex="-1" role="dialog" aria-labelledby="ConfirmTFAModalLabel">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button> <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span></button>
<h3 class="modal-title">{{ lang.tfa[pending_tfa_method] }}</h3> <h3 class="modal-title">{{ lang.tfa.tfa }}</h3>
</div> </div>
<div class="modal-body">
{% if pending_tfa_method == 'yubi_otp' %} <ul class="nav nav-tabs" id="tabContent">
<form role="form" method="post"> {% if pending_tfa_authmechs["webauthn"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
<div class="form-group"> <li class="active"><a href="#tfa_tab_webauthn" data-toggle="tab" id="pending_tfa_tab_webauthn"><i class="bi bi-fingerprint"></i> WebAuthn</a></li>
<div class="input-group"> {% endif %}
<span class="input-group-addon" id="yubi-addon"><img alt="Yubicon Icon" src="/img/yubi.ico"></span>
<input type="text" name="token" class="form-control" autocomplete="off" placeholder="Touch Yubikey" aria-describedby="yubi-addon">
<input type="hidden" name="tfa_method" value="yubi_otp">
</div>
</div>
<button class="btn btn-sm visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-sm btn-default" type="submit" name="verify_tfa_login">{{ lang.login.login }}</button>
</form>
{% endif %}
{% if pending_tfa_method == 'totp' %}
<form role="form" method="post">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon" id="tfa-addon"><i class="bi bi-shield-lock-fill"></i></span>
<input type="number" min="000000" max="999999" name="token" class="form-control" placeholder="123456" autocomplete="one-time-code" aria-describedby="tfa-addon">
<input type="hidden" name="tfa_method" value="totp">
</div>
</div>
<button class="btn btn-sm visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-default" type="submit" name="verify_tfa_login">{{ lang.login.login }}</button>
</form>
{% endif %}
{% if pending_tfa_method == 'hotp' %}
<div class="empty"></div>
{% endif %}
{% if pending_tfa_method == 'webauthn' %} {% if pending_tfa_authmechs["yubi_otp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
<form role="form" method="post" id="webauthn_auth_form"> <li class="tab-pane {% if pending_tfa_authmechs["yubi_otp"] %}active{% endif %}">
<center> <a href="#tfa_tab_yubi_otp" data-toggle="tab" id="pending_tfa_tab_yubi_otp"><i class="bi bi-usb-drive"></i> Yubi OTP</a>
<div style="cursor:pointer" id="start_webauthn_confirmation"> </li>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24"> {% endif %}
<path d="M17.81 4.47c-.08 0-.16-.02-.23-.06C15.66 3.42 14 3 12.01 3c-1.98 0-3.86.47-5.57 1.41-.24.13-.54.04-.68-.2-.13-.24-.04-.55.2-.68C7.82 2.52 9.86 2 12.01 2c2.13 0 3.99.47 6.03 1.52.25.13.34.43.21.67-.09.18-.26.28-.44.28zM3.5 9.72c-.1 0-.2-.03-.29-.09-.23-.16-.28-.47-.12-.7.99-1.4 2.25-2.5 3.75-3.27C9.98 4.04 14 4.03 17.15 5.65c1.5.77 2.76 1.86 3.75 3.25.16.22.11.54-.12.7-.23.16-.54.11-.7-.12-.9-1.26-2.04-2.25-3.39-2.94-2.87-1.47-6.54-1.47-9.4.01-1.36.7-2.5 1.7-3.4 2.96-.08.14-.23.21-.39.21zm6.25 12.07c-.13 0-.26-.05-.35-.15-.87-.87-1.34-1.43-2.01-2.64-.69-1.23-1.05-2.73-1.05-4.34 0-2.97 2.54-5.39 5.66-5.39s5.66 2.42 5.66 5.39c0 .28-.22.5-.5.5s-.5-.22-.5-.5c0-2.42-2.09-4.39-4.66-4.39-2.57 0-4.66 1.97-4.66 4.39 0 1.44.32 2.77.93 3.85.64 1.15 1.08 1.64 1.85 2.42.19.2.19.51 0 .71-.11.1-.24.15-.37.15zm7.17-1.85c-1.19 0-2.24-.3-3.1-.89-1.49-1.01-2.38-2.65-2.38-4.39 0-.28.22-.5.5-.5s.5.22.5.5c0 1.41.72 2.74 1.94 3.56.71.48 1.54.71 2.54.71.24 0 .64-.03 1.04-.1.27-.05.53.13.58.41.05.27-.13.53-.41.58-.57.11-1.07.12-1.21.12zM14.91 22c-.04 0-.09-.01-.13-.02-1.59-.44-2.63-1.03-3.72-2.1-1.4-1.39-2.17-3.24-2.17-5.22 0-1.62 1.38-2.94 3.08-2.94 1.7 0 3.08 1.32 3.08 2.94 0 1.07.93 1.94 2.08 1.94s2.08-.87 2.08-1.94c0-3.77-3.25-6.83-7.25-6.83-2.84 0-5.44 1.58-6.61 4.03-.39.81-.59 1.76-.59 2.8 0 .78.07 2.01.67 3.61.1.26-.03.55-.29.64-.26.1-.55-.04-.64-.29-.49-1.31-.73-2.61-.73-3.96 0-1.2.23-2.29.68-3.24 1.33-2.79 4.28-4.6 7.51-4.6 4.55 0 8.25 3.51 8.25 7.83 0 1.62-1.38 2.94-3.08 2.94s-3.08-1.32-3.08-2.94c0-1.07-.93-1.94-2.08-1.94s-2.08.87-2.08 1.94c0 1.71.66 3.31 1.87 4.51.95.94 1.86 1.46 3.27 1.85.27.07.42.35.35.61-.05.23-.26.38-.47.38z"></path>
</svg> {% if pending_tfa_authmechs["totp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
<p>{{ lang.tfa.start_webauthn_validation }}</p> <li class="tab-pane {% if pending_tfa_authmechs["totp"] %}active{% endif %}">
<hr> <a href="#tfa_tab_totp" data-toggle="tab" id="pending_tfa_tab_totp"><i class="bi bi-clock-history"></i> Time based OTP</a>
</li>
{% endif %}
<!-- <li><a href="#tfa_tab_hotp" data-toggle="tab">HOTP</a></li> -->
{% if pending_tfa_authmechs["u2f"] is defined %}
<li class="active"><a href="#tfa_tab_u2f" data-toggle="tab"><i class="bi bi-x-octagon"></i> U2F</a></li>
{% endif %}
</ul>
<div class="tab-content">
{% if pending_tfa_authmechs["webauthn"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
<div role="tabpanel" class="tab-pane active" id="tfa_tab_webauthn">
<div class="panel panel-default" style="margin-bottom: 0px;">
<div class="panel-body">
<form role="form" method="post" id="webauthn_auth_form">
<legend>
<i class="bi bi-shield-fill-check"></i>
Authenticators
</legend>
<div class="list-group">
{% for authenticator in pending_tfa_methods %}
{% if authenticator["authmech"] == "webauthn" %}
<a href="#" class="list-group-item webauthn-authenticator-selection">
<i class="bi bi-key-fill" style="margin-right: 5px"></i>
<span>{{ authenticator["key_id"] }}</span>
<input type="hidden" value="{{ authenticator["id"] }}" /><br/>
</a>
{% endif %}
{% endfor %}
</div>
<div class="collapse pending-tfa-collapse" id="collapseWebAuthnTFA">
<p id="webauthn_status_auth"><p><i class="bi bi-arrow-repeat icon-spin"></i> {{ lang.tfa.init_webauthn }}</p></p>
<div class="alert alert-danger" style="display:none" id="webauthn_return_code"></div>
</div>
<input type="hidden" name="token" id="webauthn_auth_data"/>
<input type="hidden" name="tfa_method" value="webauthn">
<input type="hidden" name="verify_tfa_login"/><br/>
<input type="hidden" name="id" id="webauthn_selected_id" /><br/>
</form>
</div>
</div>
</div> </div>
</center> {% endif %}
<p id="webauthn_status_auth"></p> {% if pending_tfa_authmechs["yubi_otp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
<div class="alert alert-danger" style="display:none" id="webauthn_return_code"></div> <div role="tabpanel" class="tab-pane {% if pending_tfa_authmechs["yubi_otp"] %}active{% endif %}" id="tfa_tab_yubi_otp">
<input type="hidden" name="token" id="webauthn_auth_data"/> <div class="panel panel-default" style="margin-bottom: 0px;">
<input type="hidden" name="tfa_method" value="webauthn"> <div class="panel-body">
<input type="hidden" name="verify_tfa_login"/><br/> <form role="form" method="post">
</form> <legend>
{% endif %} <i class="bi bi-shield-fill-check"></i>
{# leave this here to inform users that u2f is deprecated #} Authenticators
{% if pending_tfa_method == 'u2f' %} </legend>
<form role="form" method="post" id="u2f_auth_form"> <div class="list-group">
<p>{{ lang.tfa.u2f_deprecated }}</p> {% for authenticator in pending_tfa_methods %}
<p><b>{{ lang.tfa.u2f_deprecated_important }}</b></p> {% if authenticator["authmech"] == "yubi_otp" %}
<input type="hidden" name="token" value="destroy" /> <a href="#" class="list-group-item yubi-authenticator-selection">
<input type="hidden" name="tfa_method" value="u2f"> <i class="bi bi-key-fill" style="margin-right: 5px"></i>
<input type="hidden" name="verify_tfa_login"/><br/> <span>{{ authenticator["key_id"] }}</span>
<button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button> <input type="hidden" value="{{ authenticator["id"] }}" />
</form> </a>
{% endif %} {% endif %}
</div> {% endfor %}
</div>
<div class="collapse pending-tfa-collapse" id="collapseYubiTFA">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon" id="yubi-addon"><img alt="Yubicon Icon" src="/img/yubi.ico"></span>
<input type="text" name="token" class="form-control" autocomplete="off" placeholder="Touch Yubikey" aria-describedby="yubi-addon">
<input type="hidden" name="tfa_method" value="yubi_otp">
<input type="hidden" name="id" id="yubi_selected_id" />
</div>
</div>
<button class="btn btn-sm visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-sm btn-default" type="submit" name="verify_tfa_login">{{ lang.login.login }}</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
{% if pending_tfa_authmechs["totp"] is defined and pending_tfa_authmechs["u2f"] is not defined %}
<div role="tabpanel" class="tab-pane {% if pending_tfa_authmechs["totp"] %}active{% endif %}" id="tfa_tab_totp">
<div class="panel panel-default" style="margin-bottom: 0px;">
<div class="panel-body">
<form role="form" method="post">
<legend>
<i class="bi bi-shield-fill-check"></i>
Authenticators
</legend>
<div class="list-group">
{% for authenticator in pending_tfa_methods %}
{% if authenticator["authmech"] == "totp" %}
<a href="#" class="list-group-item totp-authenticator-selection">
<i class="bi bi-key-fill" style="margin-right: 5px"></i>
<span>{{ authenticator["key_id"] }}</span>
<input type="hidden" value="{{ authenticator["id"] }}" />
</a>
{% endif %}
{% endfor %}
</div>
<div class="collapse pending-tfa-collapse" id="collapseTotpTFA">
<div class="form-group">
<div class="input-group">
<span class="input-group-addon" id="tfa-addon"><i class="bi bi-shield-lock-fill"></i></span>
<input type="number" min="000000" max="999999" name="token" class="form-control" placeholder="123456" autocomplete="one-time-code" aria-describedby="tfa-addon">
<input type="hidden" name="tfa_method" value="totp">
<input type="hidden" name="id" id="totp_selected_id" /><br/>
</div>
</div>
<button class="btn btn-sm visible-xs-block visible-sm-inline visible-md-inline visible-lg-inline btn-default" type="submit" name="verify_tfa_login">{{ lang.login.login }}</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
<!--
<div role="tabpanel" class="tab-pane" id="tfa_tab_hotp">
<div class="panel panel-default" style="margin-bottom: 0px;">
<div class="panel-body">
<div class="empty"></div>
</div>
</div>
</div>
-->
{% if pending_tfa_authmechs["u2f"] is defined %}
<div role="tabpanel" class="tab-pane active" id="tfa_tab_u2f">
<div class="panel panel-default" style="margin-bottom: 0px;">
<div class="panel-body">
{# leave this here to inform users that u2f is deprecated #}
<form role="form" method="post" id="u2f_auth_form">
<div>
<p>{{ lang.tfa.u2f_deprecated }}</p>
<p><b>{{ lang.tfa.u2f_deprecated_important }}</b></p>
<input type="hidden" name="token" value="destroy" />
<input type="hidden" name="tfa_method" value="u2f">
<input type="hidden" name="verify_tfa_login"/><br/>
<button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -435,11 +435,11 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p class="help-block">{{ lang.add.syncjob_hint }}</p> <p class="help-block">{{ lang.add.syncjob_hint }}</p>
<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_syncjob"> <form class="form-horizontal" data-cached-form="false" role="form" data-id="add_syncjob">
<div class="form-group"> <div class="form-group">
<label class="control-label col-sm-2" for="username">{{ lang.add.username }}</label> <label class="control-label col-sm-2" for="username">{{ lang.add.username }}</label>
<div class="col-sm-10"> <div class="col-sm-10">
<select data-live-search="true" name="username" required> <select data-live-search="true" name="username" title="{{ lang.add.select }}" required>
{% for mailbox in mailboxes %} {% for mailbox in mailboxes %}
<option>{{ mailbox }}</option> <option>{{ mailbox }}</option>
{% endfor %} {% endfor %}

View File

@@ -2,11 +2,14 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">{{ lang.user.mailbox_general }}</div> <div class="panel-heading">{{ lang.user.mailbox_general }}</div>
<div class="panel-body"> <div class="panel-body">
{% if mailboxdata.attributes.force_pw_update == '1' %}
<div class="alert alert-danger">{{ lang.user.force_pw_update|raw }}</div>
{% endif %}
{% if not skip_sogo %} {% if not skip_sogo %}
<div class="row"> <div class="row">
<div class="hidden-xs col-md-3 col-xs-5 text-right"></div> <div class="hidden-xs col-md-3 col-xs-5 text-right"></div>
<div class="col-md-3 col-xs-12"> <div class="col-md-3 col-xs-12">
{% if dual_login and allow_admin_email_login == 'n' %} {% if dual_login and allow_admin_email_login == 'n' or mailboxdata.attributes.force_pw_update == '1' %}
<button disabled class="btn btn-default btn-block btn-xs-lg"> <button disabled class="btn btn-default btn-block btn-xs-lg">
<i class="bi bi-inbox-fill"></i> {{ lang.user.open_webmail_sso }} <i class="bi bi-inbox-fill"></i> {{ lang.user.open_webmail_sso }}
</button> </button>
@@ -15,6 +18,10 @@
<i class="bi bi-inbox-fill"></i> {{ lang.user.open_webmail_sso }} <i class="bi bi-inbox-fill"></i> {{ lang.user.open_webmail_sso }}
</a> </a>
{% endif %} {% endif %}
<div>
<hr>
<p><a href="#pwChangeModal" data-toggle="modal"><i class="bi bi-pencil-fill"></i> {{ lang.user.change_password }}</a></p>
</div>
</div> </div>
</div> </div>
<hr> <hr>
@@ -40,8 +47,27 @@
</div> </div>
</div> </div>
<p>{{ mailboxdata.quota_used|formatBytes(2) }} / {% if mailboxdata.quota == 0 %}{% else %}{{ mailboxdata.quota|formatBytes(2) }}{% endif %}<br>{{ mailboxdata.messages }} {{ lang.user.messages }}</p> <p>{{ mailboxdata.quota_used|formatBytes(2) }} / {% if mailboxdata.quota == 0 %}{% else %}{{ mailboxdata.quota|formatBytes(2) }}{% endif %}<br>{{ mailboxdata.messages }} {{ lang.user.messages }}</p>
<hr> </div>
<p><a href="#pwChangeModal" data-toggle="modal"><i class="bi bi-pencil-fill"></i> {{ lang.user.change_password }}</a></p> </div>
<hr>
{# TFA #}
<div class="row">
<div class="col-sm-3 col-xs-5 text-right">{{ lang.tfa.tfa }}:</div>
<div class="col-sm-9 col-xs-7">
<p id="tfa_pretty">{{ tfa_data.pretty }}</p>
{% include 'tfa_keys.twig' %}
<br>
</div>
</div>
<div class="row">
<div class="col-sm-3 col-xs-5 text-right">{{ lang.tfa.set_tfa }}:</div>
<div class="col-sm-9 col-xs-7">
<select data-style="btn btn-sm dropdown-toggle bs-placeholder btn-default" data-width="fit" id="selectTFA" class="selectpicker" title="{{ lang.tfa.select }}">
<option value="yubi_otp">{{ lang.tfa.yubi_otp }}</option>
<option value="webauthn">{{ lang.tfa.webauthn }}</option>
<option value="totp">{{ lang.tfa.totp }}</option>
<option value="none">{{ lang.tfa.none }}</option>
</select>
</div> </div>
</div> </div>
<hr> <hr>
@@ -115,9 +141,6 @@
<hr> <hr>
<div class="row"> <div class="row">
<div class="col-sm-offset-3 col-sm-9"> <div class="col-sm-offset-3 col-sm-9">
{% if mailboxdata.attributes.force_pw_update == '1' %}
<div class="alert alert-danger">{{ lang.user.force_pw_update|raw }}</div>
{% endif %}
<p><a target="_blank" href="https://mailcow.github.io/mailcow-dockerized-docs/client/client/#{{ clientconfigstr }}">[{{ lang.user.client_configuration }}]</a></p> <p><a target="_blank" href="https://mailcow.github.io/mailcow-dockerized-docs/client/client/#{{ clientconfigstr }}">[{{ lang.user.client_configuration }}]</a></p>
<p><a href="#userFilterModal" data-toggle="modal">[{{ lang.user.show_sieve_filters }}]</a></p> <p><a href="#userFilterModal" data-toggle="modal">[{{ lang.user.show_sieve_filters }}]</a></p>
<hr> <hr>

View File

@@ -76,6 +76,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
'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,
'tfa_id' => @$_SESSION['tfa_id'],
'fido2_data' => $fido2_data, 'fido2_data' => $fido2_data,
'mailboxdata' => $mailboxdata, 'mailboxdata' => $mailboxdata,
'clientconfigstr' => $clientconfigstr, 'clientconfigstr' => $clientconfigstr,
@@ -90,8 +91,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == '
'number_of_app_passwords' => $number_of_app_passwords, 'number_of_app_passwords' => $number_of_app_passwords,
]; ];
} }
else {
if (!isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') {
header('Location: /'); header('Location: /');
exit(); exit();
} }

View File

@@ -2,7 +2,7 @@ version: '2.1'
services: services:
unbound-mailcow: unbound-mailcow:
image: mailcow/unbound:1.15 image: mailcow/unbound:1.16
environment: environment:
- TZ=${TZ} - TZ=${TZ}
volumes: volumes:
@@ -22,8 +22,8 @@ services:
- unbound-mailcow - unbound-mailcow
stop_grace_period: 45s stop_grace_period: 45s
volumes: volumes:
- mysql-vol-1:/var/lib/mysql/:Z - mysql-vol-1:/var/lib/mysql/
- mysql-socket-vol-1:/var/run/mysqld/:z - mysql-socket-vol-1:/var/run/mysqld/
- ./data/conf/mysql/:/etc/mysql/conf.d/:ro,Z - ./data/conf/mysql/:/etc/mysql/conf.d/:ro,Z
environment: environment:
- TZ=${TZ} - TZ=${TZ}
@@ -43,7 +43,7 @@ services:
redis-mailcow: redis-mailcow:
image: redis:6-alpine image: redis:6-alpine
volumes: volumes:
- redis-vol-1:/data/:Z - redis-vol-1:/data/
restart: always restart: always
ports: ports:
- "${REDIS_PORT:-127.0.0.1:7654}:6379" - "${REDIS_PORT:-127.0.0.1:7654}:6379"
@@ -58,8 +58,10 @@ services:
- redis - redis
clamd-mailcow: clamd-mailcow:
image: mailcow/clamd:1.51 image: mailcow/clamd:1.54
restart: always restart: always
depends_on:
- unbound-mailcow
dns: dns:
- ${IPV4_NETWORK:-172.22.1}.254 - ${IPV4_NETWORK:-172.22.1}.254
environment: environment:
@@ -67,7 +69,7 @@ services:
- SKIP_CLAMD=${SKIP_CLAMD:-n} - SKIP_CLAMD=${SKIP_CLAMD:-n}
volumes: volumes:
- ./data/conf/clamav/:/etc/clamav/:Z - ./data/conf/clamav/:/etc/clamav/:Z
- clamd-db-vol-1:/var/lib/clamav:z - clamd-db-vol-1:/var/lib/clamav
networks: networks:
mailcow-network: mailcow-network:
aliases: aliases:
@@ -93,7 +95,7 @@ services:
- ./data/conf/rspamd/lua/:/etc/rspamd/lua/:ro,Z - ./data/conf/rspamd/lua/:/etc/rspamd/lua/:ro,Z
- ./data/conf/rspamd/rspamd.conf.local:/etc/rspamd/rspamd.conf.local:Z - ./data/conf/rspamd/rspamd.conf.local:/etc/rspamd/rspamd.conf.local:Z
- ./data/conf/rspamd/rspamd.conf.override:/etc/rspamd/rspamd.conf.override:Z - ./data/conf/rspamd/rspamd.conf.override:/etc/rspamd/rspamd.conf.override:Z
- rspamd-vol-1:/var/lib/rspamd:z - rspamd-vol-1:/var/lib/rspamd
restart: always restart: always
hostname: rspamd hostname: rspamd
dns: dns:
@@ -104,7 +106,7 @@ services:
- rspamd - rspamd
php-fpm-mailcow: php-fpm-mailcow:
image: mailcow/phpfpm:1.78 image: mailcow/phpfpm:1.79
command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
depends_on: depends_on:
- redis-mailcow - redis-mailcow
@@ -113,8 +115,8 @@ services:
- ./data/web:/web:z - ./data/web:/web:z
- ./data/conf/rspamd/dynmaps:/dynmaps:ro,z - ./data/conf/rspamd/dynmaps:/dynmaps:ro,z
- ./data/conf/rspamd/custom/:/rspamd_custom_maps:z - ./data/conf/rspamd/custom/:/rspamd_custom_maps:z
- rspamd-vol-1:/var/lib/rspamd:z - rspamd-vol-1:/var/lib/rspamd
- mysql-socket-vol-1:/var/run/mysqld/:z - mysql-socket-vol-1:/var/run/mysqld/
- ./data/conf/sogo/:/etc/sogo/:z - ./data/conf/sogo/:/etc/sogo/:z
- ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z
- ./data/conf/phpfpm/sogo-sso/:/etc/sogo-sso/:z - ./data/conf/phpfpm/sogo-sso/:/etc/sogo-sso/:z
@@ -166,7 +168,7 @@ services:
- phpfpm - phpfpm
sogo-mailcow: sogo-mailcow:
image: mailcow/sogo:1.108 image: mailcow/sogo:1.109
environment: environment:
- DBNAME=${DBNAME} - DBNAME=${DBNAME}
- DBUSER=${DBUSER} - DBUSER=${DBUSER}
@@ -192,9 +194,9 @@ services:
- ./data/conf/sogo/custom-favicon.ico:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo.ico:z - ./data/conf/sogo/custom-favicon.ico:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo.ico:z
- ./data/conf/sogo/custom-theme.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/theme.js:z - ./data/conf/sogo/custom-theme.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/theme.js:z
- ./data/conf/sogo/custom-sogo.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/custom-sogo.js:z - ./data/conf/sogo/custom-sogo.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/custom-sogo.js:z
- mysql-socket-vol-1:/var/run/mysqld/:z - mysql-socket-vol-1:/var/run/mysqld/
- sogo-web-vol-1:/sogo_web:z - sogo-web-vol-1:/sogo_web
- sogo-userdata-backup-vol-1:/sogo_backup:Z - sogo-userdata-backup-vol-1:/sogo_backup
labels: labels:
ofelia.enabled: "true" ofelia.enabled: "true"
ofelia.job-exec.sogo_sessions.schedule: "@every 1m" ofelia.job-exec.sogo_sessions.schedule: "@every 1m"
@@ -213,7 +215,7 @@ services:
- sogo - sogo
dovecot-mailcow: dovecot-mailcow:
image: mailcow/dovecot:1.162 image: mailcow/dovecot:1.17
depends_on: depends_on:
- mysql-mailcow - mysql-mailcow
dns: dns:
@@ -226,13 +228,13 @@ services:
- ./data/assets/ssl:/etc/ssl/mail/:ro,z - ./data/assets/ssl:/etc/ssl/mail/:ro,z
- ./data/conf/sogo/:/etc/sogo/:z - ./data/conf/sogo/:/etc/sogo/:z
- ./data/conf/phpfpm/sogo-sso/:/etc/phpfpm/:z - ./data/conf/phpfpm/sogo-sso/:/etc/phpfpm/:z
- vmail-vol-1:/var/vmail:Z - vmail-vol-1:/var/vmail
- vmail-index-vol-1:/var/vmail_index:Z - vmail-index-vol-1:/var/vmail_index
- crypt-vol-1:/mail_crypt/:z - crypt-vol-1:/mail_crypt/
- ./data/conf/rspamd/custom/:/etc/rspamd/custom:z - ./data/conf/rspamd/custom/:/etc/rspamd/custom:z
- ./data/assets/templates:/templates:z - ./data/assets/templates:/templates:z
- rspamd-vol-1:/var/lib/rspamd:z - rspamd-vol-1:/var/lib/rspamd
- mysql-socket-vol-1:/var/run/mysqld/:z - mysql-socket-vol-1:/var/run/mysqld/
environment: environment:
- DOVECOT_MASTER_USER=${DOVECOT_MASTER_USER:-} - DOVECOT_MASTER_USER=${DOVECOT_MASTER_USER:-}
- DOVECOT_MASTER_PASS=${DOVECOT_MASTER_PASS:-} - DOVECOT_MASTER_PASS=${DOVECOT_MASTER_PASS:-}
@@ -293,17 +295,17 @@ services:
- dovecot - dovecot
postfix-mailcow: postfix-mailcow:
image: mailcow/postfix:1.66 image: mailcow/postfix:1.67
depends_on: depends_on:
- mysql-mailcow - mysql-mailcow
volumes: volumes:
- ./data/hooks/postfix:/hooks:Z - ./data/hooks/postfix:/hooks:Z
- ./data/conf/postfix:/opt/postfix/conf:z - ./data/conf/postfix:/opt/postfix/conf:z
- ./data/assets/ssl:/etc/ssl/mail/:ro,z - ./data/assets/ssl:/etc/ssl/mail/:ro,z
- postfix-vol-1:/var/spool/postfix:z - postfix-vol-1:/var/spool/postfix
- crypt-vol-1:/var/lib/zeyple:z - crypt-vol-1:/var/lib/zeyple
- rspamd-vol-1:/var/lib/rspamd:z - rspamd-vol-1:/var/lib/rspamd
- mysql-socket-vol-1:/var/run/mysqld/:z - mysql-socket-vol-1:/var/run/mysqld/
environment: environment:
- LOG_LINES=${LOG_LINES:-9999} - LOG_LINES=${LOG_LINES:-9999}
- TZ=${TZ} - TZ=${TZ}
@@ -373,10 +375,10 @@ services:
- ./data/assets/ssl/:/etc/ssl/mail/:ro,z - ./data/assets/ssl/:/etc/ssl/mail/:ro,z
- ./data/conf/nginx/:/etc/nginx/conf.d/:z - ./data/conf/nginx/:/etc/nginx/conf.d/:z
- ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z
- sogo-web-vol-1:/usr/lib/GNUstep/SOGo/:z - sogo-web-vol-1:/usr/lib/GNUstep/SOGo/
ports: ports:
- "${HTTPS_BIND:-:}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}" - "${HTTPS_BIND:-}:${HTTPS_PORT:-443}:${HTTPS_PORT:-443}"
- "${HTTP_BIND:-:}:${HTTP_PORT:-80}:${HTTP_PORT:-80}" - "${HTTP_BIND:-}:${HTTP_PORT:-80}:${HTTP_PORT:-80}"
restart: always restart: always
networks: networks:
mailcow-network: mailcow-network:
@@ -386,7 +388,7 @@ services:
acme-mailcow: acme-mailcow:
depends_on: depends_on:
- nginx-mailcow - nginx-mailcow
image: mailcow/acme:1.81 image: mailcow/acme:1.82
dns: dns:
- ${IPV4_NETWORK:-172.22.1}.254 - ${IPV4_NETWORK:-172.22.1}.254
environment: environment:
@@ -414,7 +416,7 @@ services:
- ./data/web/.well-known/acme-challenge:/var/www/acme:z - ./data/web/.well-known/acme-challenge:/var/www/acme:z
- ./data/assets/ssl:/var/lib/acme/:z - ./data/assets/ssl:/var/lib/acme/:z
- ./data/assets/ssl-example:/var/lib/ssl-example/:ro,Z - ./data/assets/ssl-example:/var/lib/ssl-example/:ro,Z
- mysql-socket-vol-1:/var/run/mysqld/:z - mysql-socket-vol-1:/var/run/mysqld/
restart: always restart: always
networks: networks:
mailcow-network: mailcow-network:
@@ -422,7 +424,7 @@ services:
- acme - acme
netfilter-mailcow: netfilter-mailcow:
image: mailcow/netfilter:1.47 image: mailcow/netfilter:1.48
stop_grace_period: 30s stop_grace_period: 30s
depends_on: depends_on:
- dovecot-mailcow - dovecot-mailcow
@@ -451,9 +453,9 @@ services:
tmpfs: tmpfs:
- /tmp - /tmp
volumes: volumes:
- rspamd-vol-1:/var/lib/rspamd:z - rspamd-vol-1:/var/lib/rspamd
- mysql-socket-vol-1:/var/run/mysqld/:z - mysql-socket-vol-1:/var/run/mysqld/
- postfix-vol-1:/var/spool/postfix:z - postfix-vol-1:/var/spool/postfix
- ./data/assets/ssl:/etc/ssl/mail/:ro,z - ./data/assets/ssl:/etc/ssl/mail/:ro,z
restart: always restart: always
environment: environment:
@@ -507,7 +509,7 @@ services:
- watchdog - watchdog
dockerapi-mailcow: dockerapi-mailcow:
image: mailcow/dockerapi:1.41 image: mailcow/dockerapi:1.42
security_opt: security_opt:
- label=disable - label=disable
restart: always restart: always
@@ -528,7 +530,7 @@ services:
image: mailcow/solr:1.8.1 image: mailcow/solr:1.8.1
restart: always restart: always
volumes: volumes:
- solr-vol-1:/opt/solr/server/solr/dovecot-fts/data:Z - solr-vol-1:/opt/solr/server/solr/dovecot-fts/data
ports: ports:
- "${SOLR_PORT:-127.0.0.1:18983}:8983" - "${SOLR_PORT:-127.0.0.1:18983}:8983"
environment: environment:
@@ -541,7 +543,7 @@ services:
- solr - solr
olefy-mailcow: olefy-mailcow:
image: mailcow/olefy:1.9 image: mailcow/olefy:1.10
restart: always restart: always
environment: environment:
- TZ=${TZ} - TZ=${TZ}

View File

@@ -25,7 +25,7 @@ if cp --help 2>&1 | grep -q -i "busybox"; then
exit 1 exit 1
fi fi
for bin in openssl curl docker-compose docker git awk sha1sum; do for bin in openssl curl docker docker-compose git awk sha1sum; do
if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
done done
@@ -144,7 +144,7 @@ DBROOT=$(LC_ALL=C </dev/urandom tr -dc A-Za-z0-9 | head -c 28)
# Do _not_ use IP:PORT in HTTP(S)_BIND or HTTP(S)_PORT # Do _not_ use IP:PORT in HTTP(S)_BIND or HTTP(S)_PORT
# IMPORTANT: Do not use port 8081, 9081 or 65510! # IMPORTANT: Do not use port 8081, 9081 or 65510!
# Example: HTTP_BIND=1.2.3.4 # Example: HTTP_BIND=1.2.3.4
# For IPv4 and IPv6 leave it empty: HTTP_BIND= & HTTPS_PORT= # For IPv4 leave it as it is: HTTP_BIND= & HTTPS_PORT=
# For IPv6 see https://mailcow.github.io/mailcow-dockerized-docs/post_installation/firststeps-ip_bindings/ # For IPv6 see https://mailcow.github.io/mailcow-dockerized-docs/post_installation/firststeps-ip_bindings/
HTTP_PORT=80 HTTP_PORT=80

View File

@@ -77,7 +77,7 @@ function preflight_local_checks() {
exit 1 exit 1
fi fi
for bin in rsync docker-compose docker grep cut; do for bin in rsync docker docker-compose grep cut; do
if [[ -z $(which ${bin}) ]]; then if [[ -z $(which ${bin}) ]]; then
>&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m" >&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
exit 1 exit 1
@@ -85,7 +85,7 @@ function preflight_local_checks() {
done done
if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then
>&2 echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m" echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m"
exit 1 exit 1
fi fi
} }
@@ -111,7 +111,7 @@ function preflight_remote_checks() {
exit 1 exit 1
fi fi
for bin in rsync docker-compose docker; do for bin in rsync docker docker-compose; do
if ! ssh -o StrictHostKeyChecking=no \ if ! ssh -o StrictHostKeyChecking=no \
-i "${REMOTE_SSH_KEY}" \ -i "${REMOTE_SSH_KEY}" \
${REMOTE_SSH_HOST} \ ${REMOTE_SSH_HOST} \
@@ -121,7 +121,6 @@ function preflight_remote_checks() {
exit 1 exit 1
fi fi
done done
} }
preflight_local_checks preflight_local_checks
@@ -252,16 +251,18 @@ if ! ssh -o StrictHostKeyChecking=no \
fi fi
echo "OK" echo "OK"
echo -e "\033[1mPulling images on remote...\033[0m" echo -e "\e[33mPulling images on remote...\e[0m"
if ! ssh -o StrictHostKeyChecking=no \ echo -e "\e[33mProcess is NOT stuck! Please wait...\e[0m"
-i "${REMOTE_SSH_KEY}" \
${REMOTE_SSH_HOST} \
-p ${REMOTE_SSH_PORT} \
docker-compose -f "${SCRIPT_DIR}/../docker-compose.yml" pull --no-parallel 2>&1 ; then
>&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote"
fi
echo -e "\033[1mForcing garbage cleanup on remote...\033[0m" if ! ssh -o StrictHostKeyChecking=no \
-i "${REMOTE_SSH_KEY}" \
${REMOTE_SSH_HOST} \
-p ${REMOTE_SSH_PORT} \
docker-compose -f "${SCRIPT_DIR}/../docker-compose.yml" pull --no-parallel --quiet 2>&1 ; then
>&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote"
fi
echo -e "\033[1mExecuting update script and forcing garbage cleanup on remote...\033[0m"
if ! ssh -o StrictHostKeyChecking=no \ if ! ssh -o StrictHostKeyChecking=no \
-i "${REMOTE_SSH_KEY}" \ -i "${REMOTE_SSH_KEY}" \
${REMOTE_SSH_HOST} \ ${REMOTE_SSH_HOST} \
@@ -270,4 +271,13 @@ if ! ssh -o StrictHostKeyChecking=no \
>&2 echo -e "\e[31m[ERR]\e[0m - Could not cleanup old images on remote" >&2 echo -e "\e[31m[ERR]\e[0m - Could not cleanup old images on remote"
fi fi
echo -e "\033[1mExecuting update script and checking for new docker-compose Version on remote...\033[0m"
if ! ssh -o StrictHostKeyChecking=no \
-i "${REMOTE_SSH_KEY}" \
${REMOTE_SSH_HOST} \
-p ${REMOTE_SSH_PORT} \
${SCRIPT_DIR}/../update.sh -f --update-compose ; then
>&2 echo -e "\e[31m[ERR]\e[0m - Could not fetch docker-compose on remote"
fi
echo -e "\e[32mDone\e[0m" echo -e "\e[32mDone\e[0m"

View File

@@ -76,11 +76,23 @@ else
CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]") CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd "[0-9A-Za-z-_]")
fi fi
if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then
>&2 echo -e "\e[31mBusyBox grep detected on local system, please install GNU grep\e[0m"
exit 1
fi
function backup() { function backup() {
DATE=$(date +"%Y-%m-%d-%H-%M-%S") DATE=$(date +"%Y-%m-%d-%H-%M-%S")
mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}" mkdir -p "${BACKUP_LOCATION}/mailcow-${DATE}"
chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}" chmod 755 "${BACKUP_LOCATION}/mailcow-${DATE}"
cp "${SCRIPT_DIR}/../mailcow.conf" "${BACKUP_LOCATION}/mailcow-${DATE}" cp "${SCRIPT_DIR}/../mailcow.conf" "${BACKUP_LOCATION}/mailcow-${DATE}"
for bin in docker; do
if [[ -z $(which ${bin}) ]]; then
>&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
exit 1
fi
done
while (( "$#" )); do while (( "$#" )); do
case "$1" in case "$1" in
vmail|all) vmail|all)
@@ -148,6 +160,12 @@ function backup() {
} }
function restore() { function restore() {
for bin in docker docker-compose; do
if [[ -z $(which ${bin}) ]]; then
>&2 echo -e "\e[31mCannot find ${bin} in local PATH, exiting...\e[0m"
exit 1
fi
done
echo echo
echo "Stopping watchdog-mailcow..." echo "Stopping watchdog-mailcow..."
docker stop $(docker ps -qf name=watchdog-mailcow) docker stop $(docker ps -qf name=watchdog-mailcow)
@@ -341,4 +359,4 @@ elif [[ ${1} == "restore" ]]; then
done done
echo "Restoring ${FILE_SELECTION[${input_sel}]} from ${RESTORE_POINT}..." echo "Restoring ${FILE_SELECTION[${input_sel}]} from ${RESTORE_POINT}..."
restore "${RESTORE_POINT}" ${FILE_SELECTION[${input_sel}]} restore "${RESTORE_POINT}" ${FILE_SELECTION[${input_sel}]}
fi fi

287
update.sh
View File

@@ -1,52 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Check permissions ############## Begin Function Section ##############
if [ "$(id -u)" -ne "0" ]; then
echo "You need to be root"
exit 1
fi
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# Run pre-update-hook
if [ -f "${SCRIPT_DIR}/pre_update_hook.sh" ]; then
bash "${SCRIPT_DIR}/pre_update_hook.sh"
fi
if [[ "$(uname -r)" =~ ^4\.15\.0-60 ]]; then
echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!";
echo "Please update to 5.x or use another distribution."
exit 1
fi
if [[ "$(uname -r)" =~ ^4\.4\. ]]; then
if grep -q Ubuntu <<< $(uname -a); then
echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"
echo "Please update to linux-generic-hwe-16.04 by running \"apt-get install --install-recommends linux-generic-hwe-16.04\""
exit 1
fi
echo "mailcow on a 4.4.x kernel is not supported. It may or may not work, please upgrade your kernel or continue at your own risk."
read -p "Press any key to continue..." < /dev/tty
fi
# Exit on error and pipefail
set -o pipefail
# Setting high dc timeout
export COMPOSE_HTTP_TIMEOUT=600
# Add /opt/bin to PATH
PATH=$PATH:/opt/bin
umask 0022
for bin in curl docker-compose docker git awk sha1sum; do
if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi
done
export LC_ALL=C
DATE=$(date +%Y-%m-%d_%H_%M_%S)
BRANCH=$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD)
check_online_status() { check_online_status() {
CHECK_ONLINE_IPS=(1.1.1.1 9.9.9.9 8.8.8.8) CHECK_ONLINE_IPS=(1.1.1.1 9.9.9.9 8.8.8.8)
@@ -197,6 +151,145 @@ migrate_docker_nat() {
fi fi
} }
remove_obsolete_nginx_ports() {
# Removing obsolete docker-compose.override.yml
for override in docker-compose.override.yml docker-compose.override.yaml; do
if [ -s $override ] ; then
if cat $override | grep nginx-mailcow > /dev/null 2>&1; then
if cat $override | grep -E '(\[::])' > /dev/null 2>&1; then
if cat $override | grep -w 80:80 > /dev/null 2>&1 && cat $override | grep -w 443:443 > /dev/null 2>&1 ; then
echo -e "\e[33mBacking up ${override} to preserve custom changes...\e[0m"
echo -e "\e[33m!!! Manual Merge needed (if other overrides are set) !!!\e[0m"
sleep 3
cp $override ${override}_backup
sed -i '/nginx-mailcow:$/,/^$/d' $override
echo -e "\e[33mRemoved obsolete NGINX IPv6 Bind from original override File.\e[0m"
if [[ "$(cat $override | sed '/^\s*$/d' | wc -l)" == "2" ]]; then
mv $override ${override}_empty
echo -e "\e[31m${override} is empty. Renamed it to ensure mailcow is startable.\e[0m"
fi
fi
fi
fi
fi
done
}
update_compose(){
if [[ ${NO_UPDATE_COMPOSE} == "y" ]]; then
echo -e "\e[33mNot fetching latest docker-compose, please check for updates manually!\e[0m"
return 0
elif [[ -e /etc/alpine-release ]]; then
echo -e "\e[33mNot fetching latest docker-compose, because you are using Alpine Linux without glibc support. Please update docker-compose via apk!\e[0m"
return 0
else
if [ ! $FORCE ]; then
read -r -p "Do you want to update your docker-compose Version? It will automatic upgrade your docker-compose installation (recommended)? [y/N] " updatecomposeresponse
if [[ ! "${updatecomposeresponse}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "OK, not updating docker-compose."
return 0
fi
fi
echo -e "\e[32mFetching new docker-compose version...\e[0m"
echo -e "\e[32mTrying to determine GLIBC version...\e[0m"
if ldd --version > /dev/null; then
GLIBC_V=$(ldd --version | grep -E '(GLIBC|GNU libc)' | rev | cut -d ' ' -f1 | rev | cut -d '.' -f2)
if [ ! -z "${GLIBC_V}" ] && [ ${GLIBC_V} -gt 27 ]; then
DC_DL_SUFFIX=
else
DC_DL_SUFFIX=legacy
fi
else
DC_DL_SUFFIX=legacy
fi
sleep 1
if [[ $(command -v pip 2>&1) && $(pip list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 || $(command -v pip3 2>&1) && $(pip3 list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 ]]; then
echo -e "\e[33mFound a docker-compose Version installed with pip!\e[0m"
echo -e "\e[31mPlease uninstall the pip Version of docker-compose since it doesn´t support Versions higher than 1.29.2.\e[0m"
sleep 2
echo -e "\e[33mExiting...\e[0m"
exit 1
#prevent breaking a working docker-compose installed with pip
elif [[ $(curl -sL -w "%{http_code}" https://www.servercow.de/docker-compose/latest.php?vers=${DC_DL_SUFFIX} -o /dev/null) == "200" ]]; then
LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php)
COMPOSE_VERSION=$(docker-compose version --short)
if [[ "$LATEST_COMPOSE" != "$COMPOSE_VERSION" ]]; then
COMPOSE_PATH=$(command -v docker-compose)
if [[ -w ${COMPOSE_PATH} ]]; then
curl -#L https://github.com/docker/compose/releases/download/v${LATEST_COMPOSE}/docker-compose-$(uname -s)-$(uname -m) > $COMPOSE_PATH
chmod +x $COMPOSE_PATH
else
echo -e "\e[33mWARNING: $COMPOSE_PATH is not writable, but new version $LATEST_COMPOSE is available (installed: $COMPOSE_VERSION)\e[0m"
return 1
fi
fi
else
echo -e "\e[33mCannot determine latest docker-compose version, skipping...\e[0m"
return 1
fi
fi
}
############## End Function Section ##############
# Check permissions
if [ "$(id -u)" -ne "0" ]; then
echo "You need to be root"
exit 1
fi
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# Run pre-update-hook
if [ -f "${SCRIPT_DIR}/pre_update_hook.sh" ]; then
bash "${SCRIPT_DIR}/pre_update_hook.sh"
fi
if [[ "$(uname -r)" =~ ^4\.15\.0-60 ]]; then
echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!";
echo "Please update to 5.x or use another distribution."
exit 1
fi
if [[ "$(uname -r)" =~ ^4\.4\. ]]; then
if grep -q Ubuntu <<< $(uname -a); then
echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"
echo "Please update to linux-generic-hwe-16.04 by running \"apt-get install --install-recommends linux-generic-hwe-16.04\""
exit 1
fi
echo "mailcow on a 4.4.x kernel is not supported. It may or may not work, please upgrade your kernel or continue at your own risk."
read -p "Press any key to continue..." < /dev/tty
fi
# Exit on error and pipefail
set -o pipefail
# Setting high dc timeout
export COMPOSE_HTTP_TIMEOUT=600
# Add /opt/bin to PATH
PATH=$PATH:/opt/bin
umask 0022
for bin in curl docker git awk sha1sum; do
if [[ -z $(command -v ${bin}) ]]; then
echo "Cannot find ${bin}, exiting..."
exit 1;
fi
done
if [[ -z $(command -v docker-compose) ]]; then
echo -e "\e[31mCannot find docker-compose Standalone.\e[0m"
echo -e "\e[31mPlease install it manually regarding to this doc site: https://mailcow.github.io/mailcow-dockerized-docs/i_u_m/i_u_m_install/\e[0m"
sleep 3
exit 1;
fi
export LC_ALL=C
DATE=$(date +%Y-%m-%d_%H_%M_%S)
BRANCH=$(cd ${SCRIPT_DIR}; git rev-parse --abbrev-ref HEAD)
while (($#)); do while (($#)); do
case "${1}" in case "${1}" in
--check|-c) --check|-c)
@@ -238,19 +331,33 @@ while (($#)); do
--no-update-compose) --no-update-compose)
NO_UPDATE_COMPOSE=y NO_UPDATE_COMPOSE=y
;; ;;
--update-compose)
LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php)
COMPOSE_VERSION=$(docker-compose version --short)
if [[ "$LATEST_COMPOSE" != "$COMPOSE_VERSION" ]]; then
echo -e "\e[33mA new docker-compose Version is available: $LATEST_COMPOSE\e[0m"
echo -e "\e[33mYour Version is: $COMPOSE_VERSION\e[0m"
update_compose
echo -e "\e[32mYour docker-compose Version is now up to date!\e[0m"
else
echo -e "\e[32mYour docker-compose Version is up to date! Not updating it...\e[0m"
fi
exit 0
;;
--skip-ping-check) --skip-ping-check)
SKIP_PING_CHECK=y SKIP_PING_CHECK=y
;; ;;
--help|-h) --help|-h)
echo './update.sh [-c|--check, --ours, --gc, --no-update-compose, --prefetch, --skip-start, --skip-ping-check, -f|--force, -h|--help] echo './update.sh [-c|--check, --ours, --gc, --no-update-compose, --update-compose, --prefetch, --skip-start, --skip-ping-check, -f|--force, -h|--help]
-c|--check - Check for updates and exit (exit codes => 0: update available, 3: no updates) -c|--check - Check for updates and exit (exit codes => 0: update available, 3: no updates)
--ours - Use merge strategy option "ours" to solve conflicts in favor of non-mailcow code (local changes over remote changes), not recommended! --ours - Use merge strategy option "ours" to solve conflicts in favor of non-mailcow code (local changes over remote changes), not recommended!
--gc - Run garbage collector to delete old image tags --gc - Run garbage collector to delete old image tags
--no-update-compose - Do not update docker-compose --no-update-compose - Skip the docker-compose Updates during the mailcow Update process
--update-compose - Only run the docker-compose Update process (don´t updates your mailcow itself)
--prefetch - Only prefetch new images and exit (useful to prepare updates) --prefetch - Only prefetch new images and exit (useful to prepare updates)
--skip-start - Do not start mailcow after update --skip-start - Do not start mailcow after update
--skip-ping-check - Skip ICMP Check to public DNS resolvers (Use it only if you´ve blocked any ICMP Connections to your mailcow machine). --skip-ping-check - Skip ICMP Check to public DNS resolvers (Use it only if you´ve blocked any ICMP Connections to your mailcow machine)
-f|--force - Force update, do not ask questions -f|--force - Force update, do not ask questions
' '
exit 1 exit 1
@@ -258,6 +365,21 @@ while (($#)); do
shift shift
done done
# Check if Docker-Compose is older then v2 before continuing
if ! docker-compose version --short | grep "^2." > /dev/null 2>&1; then
echo -e "\e[33mYour docker-compose Version is not up to date!\e[0m"
echo -e "\e[33mmailcow needs docker-compose > 2.X.X!\e[0m"
echo -e "\e[33mYour current installed Version: $(docker-compose version --short)\e[0m"
sleep 3
update_compose
if [[ ! "${updatecomposeresponse}" =~ ^([yY][eE][sS]|[yY])+$ ]] && [[ ! ${FORCE} ]]; then
echo -e "\e[31mmailcow does not work with docker-compose < 2.X.X anymore!\e[0m"
echo -e "\e[31mPlease update your docker-compose manually, to run mailcow.\e[0m"
echo -e "\e[31mExiting...\e[0m"
exit 1
fi
fi
[[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing"; exit 1;} [[ ! -f mailcow.conf ]] && { echo "mailcow.conf is missing"; exit 1;}
chmod 600 mailcow.conf chmod 600 mailcow.conf
source mailcow.conf source mailcow.conf
@@ -577,7 +699,21 @@ if [ ! $FORCE ]; then
migrate_docker_nat migrate_docker_nat
fi fi
LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php)
COMPOSE_VERSION=$(docker-compose version --short)
if [[ "$LATEST_COMPOSE" != "$COMPOSE_VERSION" ]]; then
echo -e "\e[33mA new docker-compose Version is available: $LATEST_COMPOSE\e[0m"
echo -e "\e[33mYour Version is: $COMPOSE_VERSION\e[0m"
update_compose
else
echo -e "\e[32mYour docker-compose Version is up to date! Not updating it...\e[0m"
fi
remove_obsolete_nginx_ports
echo -e "\e[32mValidating docker-compose stack configuration...\e[0m" echo -e "\e[32mValidating docker-compose stack configuration...\e[0m"
sed -i 's/HTTPS_BIND:-:/HTTPS_BIND:-/g' docker-compose.yml
sed -i 's/HTTP_BIND:-:/HTTP_BIND:-/g' docker-compose.yml
if ! docker-compose config -q; then if ! docker-compose config -q; then
echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m" echo -e "\e[31m\nOh no, something went wrong. Please check the error message above.\e[0m"
exit 1 exit 1
@@ -646,44 +782,6 @@ elif [[ ${MERGE_RETURN} != 0 ]]; then
exit 1 exit 1
fi fi
if [[ ${NO_UPDATE_COMPOSE} == "y" ]]; then
echo -e "\e[33mNot fetching latest docker-compose, please check for updates manually!\e[0m"
elif [[ -e /etc/alpine-release ]]; then
echo -e "\e[33mNot fetching latest docker-compose, because you are using Alpine Linux without glibc support. Please update docker-compose via apk!\e[0m"
else
echo -e "\e[32mFetching new docker-compose version...\e[0m"
echo -e "\e[32mTrying to determine GLIBC version...\e[0m"
if ldd --version > /dev/null; then
GLIBC_V=$(ldd --version | grep -E '(GLIBC|GNU libc)' | rev | cut -d ' ' -f1 | rev | cut -d '.' -f2)
if [ ! -z "${GLIBC_V}" ] && [ ${GLIBC_V} -gt 27 ]; then
DC_DL_SUFFIX=
else
DC_DL_SUFFIX=legacy
fi
else
DC_DL_SUFFIX=legacy
fi
sleep 1
if [[ ! -z $(which pip) && $(pip list --local 2>&1 | grep -v DEPRECATION | grep -c docker-compose) == 1 ]]; then
true
#prevent breaking a working docker-compose installed with pip
elif [[ $(curl -sL -w "%{http_code}" https://www.servercow.de/docker-compose/latest.php?vers=${DC_DL_SUFFIX} -o /dev/null) == "200" ]]; then
LATEST_COMPOSE=$(curl -#L https://www.servercow.de/docker-compose/latest.php)
COMPOSE_VERSION=$(docker-compose version --short)
if [[ "$LATEST_COMPOSE" != "$COMPOSE_VERSION" ]]; then
COMPOSE_PATH=$(which docker-compose)
if [[ -w ${COMPOSE_PATH} ]]; then
curl -#L https://github.com/docker/compose/releases/download/${LATEST_COMPOSE}/docker-compose-$(uname -s)-$(uname -m) > $COMPOSE_PATH
chmod +x $COMPOSE_PATH
else
echo -e "\e[33mWARNING: $COMPOSE_PATH is not writable, but new version $LATEST_COMPOSE is available (installed: $COMPOSE_VERSION)\e[0m"
fi
fi
else
echo -e "\e[33mCannot determine latest docker-compose version, skipping...\e[0m"
fi
fi
echo -e "\e[32mFetching new images, if any...\e[0m" echo -e "\e[32mFetching new images, if any...\e[0m"
sleep 2 sleep 2
docker-compose pull docker-compose pull
@@ -707,9 +805,6 @@ fi
# Checking for old project name bug # Checking for old project name bug
sed -i --follow-symlinks 's#COMPOSEPROJECT_NAME#COMPOSE_PROJECT_NAME#g' mailcow.conf sed -i --follow-symlinks 's#COMPOSEPROJECT_NAME#COMPOSE_PROJECT_NAME#g' mailcow.conf
# Checking old, wrong bindings
sed -i --follow-symlinks 's/HTTP_BIND=0.0.0.0/HTTP_BIND=/g' mailcow.conf
sed -i --follow-symlinks 's/HTTPS_BIND=0.0.0.0/HTTPS_BIND=/g' mailcow.conf
# Fix Rspamd maps # Fix Rspamd maps
if [ -f data/conf/rspamd/custom/global_from_blacklist.map ]; then if [ -f data/conf/rspamd/custom/global_from_blacklist.map ]; then
@@ -759,8 +854,8 @@ if [ -f "${SCRIPT_DIR}/post_update_hook.sh" ]; then
bash "${SCRIPT_DIR}/post_update_hook.sh" bash "${SCRIPT_DIR}/post_update_hook.sh"
fi fi
#echo "In case you encounter any problem, hard-reset to a state before updating mailcow:" # echo "In case you encounter any problem, hard-reset to a state before updating mailcow:"
#echo # echo
#git reflog --color=always | grep "Before update on " # git reflog --color=always | grep "Before update on "
#echo # echo
#echo "Use \"git reset --hard hash-on-the-left\" and run docker-compose up -d afterwards." # echo "Use \"git reset --hard hash-on-the-left\" and run docker-compose up -d afterwards."