Compare commits

...

145 Commits

Author SHA1 Message Date
DerLinkman
f09c8534f5 Merge branch 'master' into feature/fts-xapian 2023-03-15 16:13:51 +01:00
Niklas Meyer
d20df7d73e Merge pull request #5068 from mailcow/staging
2022-02a
2023-02-17 15:52:53 +01:00
Niklas Meyer
a8c61daeaf Merge pull request #5070 from mailcow/fix/snat
[Netfilter] Fix IPv4 Subrouting not added properly
2023-02-17 15:44:16 +01:00
Niklas Meyer
1a4f11209a Updated netfilter to 1.51 2023-02-17 13:22:23 +01:00
FreddleSpl0it
04403aaf70 [Netfilter] fix setting SNAT Rule if chain is empty 2023-02-17 13:15:44 +01:00
Niklas Meyer
7f0dd7d0d7 [Nextcloud] Added bzip2 as required package 2023-02-17 12:53:31 +01:00
FreddleSpl0it
cd29ad883e Merge branch 'staging' of https://github.com/mailcow/mailcow-dockerized into staging 2023-02-16 17:12:11 +01:00
FreddleSpl0it
e1cd719a17 [Web] fix mbox percentage sorting 2023-02-16 17:12:03 +01:00
Niklas Meyer
15bb331a7d Merge pull request #5048 from mailcow/renovate/composer-composer-2.x
Update dependency composer/composer to v2.5.4
2023-02-16 17:03:45 +01:00
Niklas Meyer
6f3179bb8d [web] Change FIDO2 login to independent button 2023-02-16 17:03:09 +01:00
Niklas Meyer
29e5b87207 Changed Language strings for clearer button meaning 2023-02-16 16:30:36 +01:00
Niklas Meyer
4403bc2d18 Merge pull request #5064 from mailcow/feat/clamav-1.0.1
[CLAMAV] Update to 1.0.1
2023-02-16 14:59:17 +01:00
Niklas Meyer
63e92e0897 [CLAMAV] Update to 1.0.1 2023-02-16 14:56:56 +01:00
renovate[bot]
aa4d8b1f47 Update dependency composer/composer to v2.5.4
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-02-15 13:51:12 +00:00
milkmaker
9054ca18be [Web] Updated lang.lv-lv.json (#5061)
Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
2023-02-14 19:33:12 +01:00
Niklas Meyer
38291d123f [DB] Fix espacing of special db names during upgrade 2023-02-14 10:11:55 +01:00
renovate[bot]
ca64ff2c0b Update devops-infra/action-pull-request action to v0.5.5 (#5060)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-02-12 23:56:14 +01:00
Tomy Hsieh
dc85f49961 feat: Change FIDO2 login to independent button 2023-02-11 21:49:21 +08:00
milkmaker
5dca4dac81 [Web] Updated lang.ru-ru.json (#5046)
Co-authored-by: Aleksandr Kliushenok <alex.1501@icloud.com>
2023-02-04 15:00:07 +01:00
Patrick Schult
df8775d4c9 Merge pull request #5040 from mailcow/staging
2023-02
2023-02-02 15:31:34 +01:00
Niklas Meyer
2bc663dcd5 Removed Twitter Action due to Twitter Paid API (soon). Thx Elon! 2023-02-02 14:55:44 +01:00
Patrick Schult
1071bb8230 Merge pull request #4967 from FELDSAM-INC/feldsam/sso
[Web] Implemented SSO for domain admins
2023-02-02 12:12:53 +01:00
Niklas Meyer
e437810eca Merge pull request #5038 from mailcow/fix/sogo-macos-fix
[Fix] SOGo Update Fix for 5.8.0 (macOS fix)
2023-02-02 11:32:35 +01:00
FreddleSpl0it
e8fd34d31f [Web] webauthn add lang strings 2023-02-02 11:28:51 +01:00
Niklas Meyer
6aebb8352e [Fix] SOGo Update Fix for 5.8.0 (macOS fix) 2023-02-02 11:03:51 +01:00
Patrick Schult
d684e0efc0 Merge pull request #5034 from mailcow/fix/skip-sogo
[Web] Skip update_sogo_static_view if sogo is disabled
2023-01-31 11:03:50 +01:00
FreddleSpl0it
64ac6a8891 [Web] Skip update_sogo_static_view if sogo is disabled 2023-01-31 10:54:16 +01:00
FreddleSpl0it
72e8180c6b [Web] datatable adjustment 2023-01-31 10:37:51 +01:00
FreddleSpl0it
d62c275004 [Web] match PAGINATION_SIZE to an existing datatable option 2023-01-31 09:49:18 +01:00
Patrick Schult
aa7f562761 Merge pull request #5011 from realizelol/staging
[BS5] Support for pagination_size + some minor improvements (to quarantine)
2023-01-31 09:43:51 +01:00
renovate[bot]
a1f033e4c1 Update docker/build-push-action action to v4 (#5032)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-30 19:58:17 +01:00
milkmaker
58ddc31db6 Translations update from Weblate (#5026)
* [Web] Updated lang.en-gb.json

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

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

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

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

Co-authored-by: Lukáš Matula <lukas@gbely.net>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: Lukáš Matula <lukas@gbely.net>
2023-01-26 20:09:52 +01:00
Kristian Feldsam
5bf62481d5 [Web] Implemented SSO for domain admins
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>

Revert "[Web] Implemented SSO for domain admins"

This reverts commit 6860dc8ebe2c8f53d77df5bca7787f7cb3bb4ee0.

Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-01-26 15:54:44 +01:00
realizelol
6ff3f3f044 [Web] Set pageLength to pagination_size + repect savedState...
Fix width in quarantine table.
2023-01-25 23:50:39 +01:00
Niklas Meyer
640f535e99 Merge pull request #5019 from mailcow/staging
2023-01a
2023-01-25 16:29:22 +01:00
Niklas Meyer
05d1a974eb Merge pull request #5003 from mailcow/feat/acme-skip-ip-check
[Acme] Implemented IP Check Bypass properly
2023-01-25 16:10:11 +01:00
Niklas Meyer
99e38d81b1 Removed Integration Tests 2023-01-25 16:09:15 +01:00
FreddleSpl0it
ed7b384e24 [Web] fix queue btn showing undefined 2023-01-25 09:34:12 +01:00
FreddleSpl0it
5439ea1010 Merge branch 'staging' of https://github.com/mailcow/mailcow-dockerized into staging 2023-01-25 09:32:27 +01:00
FreddleSpl0it
b719982504 partial rollback of dockerapi 2023-01-25 09:31:22 +01:00
milkmaker
8281d3fa55 [Web] Updated lang.da-dk.json (#5020)
Co-authored-by: osos <osos@openeyes.dk>

Co-authored-by: osos <osos@openeyes.dk>
2023-01-24 20:18:17 +01:00
FreddleSpl0it
9ba65a572e [Web] add missing template var for dadmins 2023-01-24 10:13:30 +01:00
FreddleSpl0it
afddcf7f3b replace nullnull.org with fuzzy.mailcow.email 2023-01-24 09:49:49 +01:00
Niklas Meyer
294569f5c9 Merge pull request #5015 from mailcow/feat/nc-install-fix
Fix nextcloud install
2023-01-22 16:17:18 +01:00
Peter
ef6452cf55 Fix installation of nextcloud 2023-01-22 15:06:36 +01:00
renovate[bot]
9af40eba10 Update dependency nextcloud/server to v25.0.3 (#4996)
Signed-off-by: milkmaker <milkmaker@mailcow.de>

Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-20 15:37:12 +01:00
renovate[bot]
1b3a13ca19 Update alpine Docker tag to v3.17 (#4997)
Signed-off-by: milkmaker <milkmaker@mailcow.de>

Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-20 15:36:52 +01:00
Patrick Schult
71cc607de6 Merge pull request #5006 from mailcow/staging
Revert Docker Compose detection commits
2023-01-19 16:04:50 +01:00
FreddleSpl0it
2ebd8345df Revert "[Generate] Refactor compose version detection using regex"
This reverts commit 4c6f8c4f60.
2023-01-19 15:58:22 +01:00
FreddleSpl0it
f5baeb31c1 Revert "[Update.sh] Implemented optimized Regex Compose Detection"
This reverts commit a76e6b32f7.
2023-01-19 15:57:49 +01:00
DerLinkman
5abda44bc6 Merge branch 'staging' 2023-01-19 14:07:55 +01:00
DerLinkman
520d070081 [Compose] Removed OOMKillDisabled from dockerapi 2023-01-19 14:04:55 +01:00
Niklas Meyer
86beba6f5a Merge pull request #4995 from mailcow/staging
2023-01
2023-01-19 12:25:57 +01:00
Niklas Meyer
f0d9948aee Merge pull request #4991 from mailcow/feat/dovecot-2.3.20
[Dovecot] Update to 2.3.20
2023-01-19 11:31:59 +01:00
DerLinkman
8e3d2f7010 [SOGo] Update to newer 5.8.0 (fix for macOS Caldav Bug) 2023-01-19 11:28:03 +01:00
Niklas Meyer
fc1c5a505d Merge pull request #4992 from mailcow/feat/phpfpm-renovate
Update composer and allow renovate for updating Dockerfiles
2023-01-19 10:54:02 +01:00
Niklas Meyer
18cb06fbc7 Merge pull request #4993 from mailcow/feat/renovate-docker-compose
Update renovate config
2023-01-18 21:22:15 +01:00
Peter
1af785a94f Enable dependencyDashboard
Add label for PRs
Add docker-compose manager
2023-01-18 19:37:09 +01:00
Peter
7626becb38 Add regex for matchstring line in Dockerfiles 2023-01-17 19:48:42 +01:00
Peter
5d5e959729 Add regex for matchstring line in Dockerfiles
Update composer to 2.5.1
2023-01-17 19:45:32 +01:00
Niklas Meyer
49bbdd064e Merge pull request #4989 from mailcow/feat/nextcloud-script-overhaul
[Nextcloud] Updated and improved script (implemented -u and more)
2023-01-17 16:34:58 +01:00
DerLinkman
9279ee2e76 [Dovecot] Update to 2.3.20 2023-01-17 16:23:31 +01:00
DerLinkman
a76e6b32f7 [Update.sh] Implemented optimized Regex Compose Detection 2023-01-16 16:02:56 +01:00
DerLinkman
4c6f8c4f60 [Generate] Refactor compose version detection using regex 2023-01-16 15:54:29 +01:00
FreddleSpl0it
826d32413b Merge branch 'staging' of https://github.com/mailcow/mailcow-dockerized into staging 2023-01-16 15:38:48 +01:00
DerLinkman
b6799d9fcb Feature: Add developer mode option to generate_config.sh 2023-01-16 15:38:42 +01:00
FreddleSpl0it
8782304e8d [Web] show fold/unfold action if child rows exists 2023-01-16 15:38:35 +01:00
DerLinkman
9c55d46bc6 [Nextcloud] Updated and improved script (implemented -u and more) 2023-01-16 14:35:15 +01:00
FreddleSpl0it
099db33e44 [Web] disable datatable default row click listener 2023-01-16 11:41:34 +01:00
DerLinkman
5c57df4669 [Acme] Implemented IP Check Bypass properly 2023-01-16 10:10:20 +01:00
FreddleSpl0it
152431a7d7 [Web] fix Spamfilter flag fwdhosts wrong naming 2023-01-16 09:24:10 +01:00
FreddleSpl0it
36fa5dc633 [Web] fix domain admins cant delete tags 2023-01-16 09:07:28 +01:00
renovate[bot]
814f4aed15 Update thollander/actions-comment-pull-request action to v2.3.1 (#4986)
Signed-off-by: milkmaker <milkmaker@mailcow.de>

Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-14 10:05:55 +01:00
milkmaker
e990856629 [Web] Updated lang.fr-fr.json (#4972)
Co-authored-by: Frederic Ollivier <fredol@me.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

Co-authored-by: Frederic Ollivier <fredol@me.com>
2023-01-09 18:03:41 +01:00
Niklas Meyer
c97afbfa0b Merge pull request #4943 from sivn/staging
[Web] added missing unban action
2023-01-09 12:41:32 +01:00
Niklas Meyer
93b3e0302a Merge pull request #4964 from mailcow/feat/renovate-gosu
Update gosu and allow renovate for updating Dockerfiles
2023-01-09 12:40:57 +01:00
Niklas Meyer
27c87de4ed Merge pull request #4966 from mailcow/fix/bs5
BS5 UI fixes
2023-01-09 12:40:14 +01:00
DerLinkman
028ad4ceb9 changed language string (de) 2023-01-09 10:43:42 +01:00
FreddleSpl0it
e501642b8e [Web] fix mailboxtable sort by quota 2023-01-09 08:04:16 +01:00
FreddleSpl0it
7966f010a2 [Web] switch table length + filter field positions 2023-01-06 15:03:04 +01:00
FreddleSpl0it
b22f74cb59 [Web] persist table settings + fix quarantine sort 2023-01-06 13:45:52 +01:00
FreddleSpl0it
c928948b15 [Web] use saved password policy for pwgen 2023-01-06 13:18:59 +01:00
FreddleSpl0it
606eaad8f7 [Web] set correct type for routing password input 2023-01-06 12:48:37 +01:00
FreddleSpl0it
c44281f62d [Web] set domain tab default active 2023-01-06 12:43:10 +01:00
FreddleSpl0it
1e98784eee [Web] Opt-In for third party ip_check 2023-01-06 12:09:15 +01:00
FreddleSpl0it
dd9296ffc2 [Web] fix extend_sender_acl issue for domainadmins 2023-01-06 11:07:44 +01:00
FreddleSpl0it
fc0e6b6efb [Web] fix quarantine darkmode style 2023-01-06 09:21:14 +01:00
FreddleSpl0it
68f5fbf65c [Web] remove remote Google fonts from lumen theme 2023-01-06 09:11:51 +01:00
FreddleSpl0it
9727e4084f [Web] load public ip on click and add curl timeout 2023-01-06 08:40:26 +01:00
milkmaker
5c2f48e94c [Web] Updated lang.zh-cn.json (#4965)
Co-authored-by: 雨 <luotianyi@luotianyi.me>

Co-authored-by: 雨 <luotianyi@luotianyi.me>
2023-01-05 17:40:36 +01:00
Peter
cb098df743 Update gosu to 1.16
Change ENV to ARG
Add matchstring line
2023-01-04 19:10:32 +01:00
Peter
b3c54ed07a Add regex for matchstring line in Dockerfiles 2023-01-04 19:09:23 +01:00
Peter
c601eca25d Update thollander/actions-comment-pull-request action to v2.3.0 2023-01-04 18:54:19 +01:00
Patrick Schult
48a13255f3 Merge pull request #4948 from tomudding/fix/sorting-mail-configuration-datatables
Fix sorting of mail configuration DataTables
2023-01-04 13:47:22 +01:00
milkmaker
08f93c7d58 Translations update from Weblate (#4960)
* [Web] Updated lang.zh-cn.json

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

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

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

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

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

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

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

Co-authored-by: 雨 <luotianyi@luotianyi.me>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: Stefano <stefano.vassena@gmail.com>
2023-01-03 18:12:18 +01:00
Niklas Meyer
e5c9752681 Merge pull request #4956 from mailcow/feat/nextcloud-renovate
Update nextcloud helperscript to use renovate
2023-01-02 14:36:29 +01:00
Peter
afa1ed1eff Add matchstring line for regex
Update nextcloud to 25.0.2
change download URLs
2022-12-31 17:13:38 +01:00
Peter
072cbe62de Enable regex as manager
Add regex for matchstring line
2022-12-31 17:11:16 +01:00
renovate[bot]
9fe8bfadf3 Update actions/stale action to v7 (#4953)
Signed-off-by: milkmaker <milkmaker@mailcow.de>

Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-31 16:49:21 +01:00
renovate[bot]
75e4953070 Update mugi111/tweet-trigger-release action to v1.2 (#4952)
Signed-off-by: milkmaker <milkmaker@mailcow.de>

Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-12-31 16:49:02 +01:00
Tom Udding
de30650dc7 Sort other mailbox DataTables also descending by ID
Also removes the extra non-usable sort option.
2022-12-30 16:38:02 +01:00
Tom Udding
690c34bc1d Sort sync jobs DataTable based on ID
By setting the default column to perform the sort on, the additional
sort option for the first (hidden) column is also removed.
2022-12-30 16:22:52 +01:00
Vincent Simon
4d2e32ee40 [Web] added missing unban action 2022-12-29 18:24:15 +01:00
FreddleSpl0it
02b2988beb [Web] fix typo in SASL table logs 2022-12-27 13:56:09 +01:00
DerLinkman
0408bbf57f Merge branch 'feature/bootstrap5' into feature/fts-xapian 2022-11-23 12:00:15 +01:00
DerLinkman
eb4a33dc27 Implemented current state of UI (BS5) 2022-10-19 13:41:33 +02:00
DerLinkman
a0ae59f8bf Added 1.20-Xapian Image 2022-10-07 17:19:15 +02:00
DerLinkman
60871fa7d0 Merge remote-tracking branch 'origin/staging' into feature/fts-xapian 2022-10-07 17:06:12 +02:00
DerLinkman
59a4031e24 Merge branch 'staging' into feature/fts-xapian 2022-10-07 17:03:28 +02:00
DerLinkman
04f55fc748 Updated newer Compenent Versions 2022-10-07 17:01:32 +02:00
DerLinkman
3ba3f1c2bd Updated Dovecot Configs to be up to date 2022-09-30 20:37:48 +02:00
DerLinkman
b0fd9787b5 Enabled Substring Search for flatcurve 2022-07-20 15:49:43 +02:00
DerLinkman
df3de09050 Multi Build Dovecot Building + Config Optimizations 2022-07-18 18:35:22 +02:00
DerLinkman
86079429b3 Using Stable Dovecot Builds for flatcurve 2022-07-18 14:24:46 +02:00
DerLinkman
ed476aae6b Removed Solr Leftovers + renamed flatcurve conf 2022-07-18 11:15:40 +02:00
moo
f0e27312f9 Restored manual Dovecot Build 2022-05-12 11:11:13 +02:00
moo
3425bcfbf0 Renamed, reconfigured needed Xapian files for Flatcurve 2022-04-29 14:35:54 +02:00
moo
bfa81b318d Changed Build process to fts-flatcurve Version of Xapian FTS 2022-04-29 14:28:49 +02:00
Niklas Meyer
8dba0ca7dd Merge branch 'staging' into feature/fts-xapian 2022-03-02 17:07:18 +01:00
Niklas Meyer
5bd3394ed9 Delete decode2text.sh 2022-02-23 16:12:15 +01:00
Niklas Meyer
c0e66254b9 Cleanup Dockerfile + Added new dependencies 2022-02-23 09:45:28 +01:00
Niklas Meyer
aec2dd1252 Merge pull request #4455 from DerLinkman/Dovecot-FTS-XAPIAN
[XAPIAN] Added Solr Replacement [BETA]
2022-02-07 08:39:17 +01:00
Niklas Meyer
d86e9a22f4 Fetch Staging from orig Repo (#3)
* [Web] add github version tag

* [Web] add github version tag

* [Web] add github version tag

* [Web] add github version tag

* [Web] add github version tag

* [Web] add github version tag error handling

* [Web] add github version tag error handling

* Passwordless SOGo auth: support for calendar invitations and calendar/contacts subscriptions

Inviting someone to a calendar event triggers a request to /SOGo/so/otheruser@example.com/freebusy.ifb/ajaxRead. Subscribing to someone's calendar/contacts triggers a request to /SOGo/so/otheruser@example.com/foldersSearch. The email address in the URL is different from the logged-in user, which needs to be handled appropriately by sogo-auth.php.

* [Web] add github version tag - adjust css

* [Compose] Update SOGo Autoreply Schedule to 5m

Based on the advice of inverse (SOGo developer). Thanks to https://github.com/jmber

Closes: https://github.com/mailcow/mailcow-dockerized/issues/4436

* [Web] add github version tag - move twig globals

* [Web] add github version tag - missing </div>

* Passwordless SOGo auth: improvements for when accessing other users

* [WebAuthn] fido2 passwordless auth - fix (#4440)

* [WebAuthn] fido2 revert

* [WebAuthn] set UV flags to 'discouraged'

* [WebAuthn] revert - set UV flags to 'discouraged'

* Update clamav to 0.104.2

* Update clamav to 0.104.2

* Update dovecot to 2.3.18

Update gosu to 1.14
Use debian bullseye as base

* [Web] Updated lang.es.json [CI SKIP] (#4453)

Co-authored-by: Fijxu <fijxu@zzls.xyz>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

Co-authored-by: Fijxu <fijxu@zzls.xyz>

Co-authored-by: FreddleSpl0it <patschul@posteo.de>
Co-authored-by: FreddleSpl0it <75116288+FreddleSpl0it@users.noreply.github.com>
Co-authored-by: Michael Kuron <mkuron@users.noreply.github.com>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: Fijxu <fijxu@zzls.xyz>
2022-02-07 08:28:10 +01:00
Niklas Meyer
db73f83c4e Merge pull request #2 from mailcow/master
Jan(moo)uary Update 2022 - Revision A (2022-01a) (#4445)
2022-02-07 08:21:31 +01:00
Niklas Meyer
28582c5842 [Compose] Changed Ofelia CMD for fts optimize 2022-02-01 14:50:19 +01:00
Niklas Meyer
3d637aca25 [Dovecot] Added Xapian Parameters from mailcow.conf 2022-02-01 14:48:46 +01:00
Niklas Meyer
d079ff49c6 Update syslog-ng.conf 2022-01-28 17:21:20 +01:00
Niklas Meyer
2d6ce926e1 Update syslog-ng-redis_slave.conf 2022-01-28 17:21:07 +01:00
Niklas Meyer
60ddfe3be2 [Dovecot] Added Xapian include 2022-01-28 17:07:12 +01:00
Niklas Meyer
30e2d944cd [Dovecot] Removed Xapian from dovecot.conf
This added a include try pointing on the file instead.
2022-01-28 16:51:33 +01:00
Niklas Meyer
99ea569288 [Dovecot] Added seperate XAPIAN Conf 2022-01-28 16:49:58 +01:00
Niklas Meyer
c98ef0d0c5 Delete FTS-Xapian.conf 2022-01-28 16:49:30 +01:00
Niklas Meyer
f09ca0a36a [Dovecot] Added seperate XAPIAN Conf 2022-01-28 16:48:51 +01:00
Niklas Meyer
cdce97bd59 [Dovecot] Changed Xapian default to 1024m instead of 2G 2022-01-28 15:03:23 +01:00
Niklas Meyer
ed8941440a [Dovecot] Added Xapian default config 2022-01-28 12:27:57 +01:00
Niklas Meyer
570170a5b1 [Compose] Remove solr from ipv6-nat dependencies 2022-01-28 12:24:51 +01:00
Niklas Meyer
df2c33d323 [Compose] Replace solr to Xapian (in dovecot)
First revision. Waiting on: https://github.com/grosjo/fts-xapian/issues/115
2022-01-28 11:19:38 +01:00
Niklas Meyer
f2e0e50f87 [Config] Readded default Value for Xapian Heap 2022-01-28 11:01:31 +01:00
Niklas Meyer
26c5ed73e2 [Config] Replace Solr with Xapian (Remove Solr Binds) 2022-01-28 10:45:33 +01:00
Niklas Meyer
148b511f9d [Update.sh] Replace Solr with Xapian 2022-01-28 10:44:08 +01:00
Niklas Meyer
311007700b [Dovecot] Add decode2text.sh 2022-01-27 15:36:03 +01:00
Niklas Meyer
3a9177bd4c Merge branch 'mailcow:master' into Dovecot-FTS-XAPIAN 2022-01-27 08:24:43 +01:00
Niklas Meyer
bca09e3afa [Dovecot] Rebase on Bullseye + Xapian Compile
This Push adds the bullseye rebase + the compilation of the XAPIAN Core + Plugin to run with Dovecot 2.3.17
2022-01-27 08:24:28 +01:00
Peter
cfba96f7e0 [GH-Actions][stale] Add neverstale label to exempt list 2022-01-22 17:41:46 +01:00
ntimo
c82f38a025 [API] Fix minor issue in api docs 2022-01-21 21:29:16 +00:00
105 changed files with 7106 additions and 6063 deletions

22
.github/renovate.json vendored
View File

@@ -1,13 +1,31 @@
{
"enabled": true,
"timezone": "Europe/Berlin",
"dependencyDashboard": false,
"dependencyDashboard": true,
"dependencyDashboardTitle": "Renovate Dashboard",
"commitBody": "Signed-off-by: milkmaker <milkmaker@mailcow.de>",
"rebaseWhen": "auto",
"labels": ["renovate"],
"assignees": [
"@magiccc"
],
"baseBranches": ["staging"],
"enabledManagers": ["github-actions"]
"enabledManagers": ["github-actions", "regex", "docker-compose"],
"ignorePaths": [
"data\/web\/inc\/lib\/vendor\/matthiasmullie\/minify\/**"
],
"regexManagers": [
{
"fileMatch": ["^helper-scripts\/nextcloud.sh$"],
"matchStrings": [
"#\\srenovate:\\sdatasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?( extractVersion=(?<extractVersion>.*?))?\\s.*?_VERSION=(?<currentValue>.*)"
]
},
{
"fileMatch": ["(^|/)Dockerfile[^/]*$"],
"matchStrings": [
"#\\srenovate:\\sdatasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?\\s(ENV|ARG) .*?_VERSION=(?<currentValue>.*)\\s"
]
}
]
}

View File

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

View File

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

View File

@@ -1,63 +0,0 @@
name: mailcow Integration Tests
on:
push:
branches: [ "master", "staging" ]
workflow_dispatch:
permissions:
contents: read
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

@@ -12,7 +12,7 @@ jobs:
with:
fetch-depth: 0
- name: Run the Action
uses: devops-infra/action-pull-request@v0.5.3
uses: devops-infra/action-pull-request@v0.5.5
with:
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}

View File

@@ -26,7 +26,7 @@ jobs:
password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
file: data/Dockerfiles/backup/Dockerfile

View File

@@ -1,20 +0,0 @@
name: "Tweet trigger release"
on:
release:
types: [published]
jobs:
tweet:
runs-on: ubuntu-latest
steps:
- name: "Get Release Tag"
run: |
RELEASE_TAG=$(curl https://api.github.com/repos/mailcow/mailcow-dockerized/releases/latest | jq -r '.tag_name')
- 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 update has just been released! Checkout the GitHub Page for changelog and more informations: https://github.com/mailcow/mailcow-dockerized/releases/latest'

1
.gitignore vendored
View File

@@ -13,6 +13,7 @@ data/conf/dovecot/acl_anyone
data/conf/dovecot/dovecot-master.passwd
data/conf/dovecot/dovecot-master.userdb
data/conf/dovecot/extra.conf
data/conf/dovecot/dovecot-fts-flatcurve.conf
data/conf/dovecot/global_sieve_*
data/conf/dovecot/last_login
data/conf/dovecot/lua

View File

@@ -1,6 +1,5 @@
# 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)

View File

@@ -213,11 +213,13 @@ while true; do
done
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
if [[ ${SKIP_IP_CHECK} != "y" ]]; then
# Start IP detection
log_f "Detecting IP addresses..."
IPV4=$(get_ipv4)
IPV6=$(get_ipv6)
log_f "OK: ${IPV4}, ${IPV6:-"0000:0000:0000:0000:0000:0000:0000:0000"}"
fi
#########################################
# IP and webroot challenge verification #

View File

@@ -1,4 +1,4 @@
FROM clamav/clamav:1.0_base
FROM clamav/clamav:1.0.1-1_base
LABEL maintainer "André Peters <andre.peters@servercow.de>"

View File

@@ -13,6 +13,7 @@ RUN apk add --update --no-cache python3 \
fastapi \
uvicorn \
aiodocker \
docker \
redis
COPY docker-entrypoint.sh /app/

View File

@@ -1,5 +1,6 @@
from fastapi import FastAPI, Response, Request
import aiodocker
import docker
import psutil
import sys
import re
@@ -9,11 +10,38 @@ import json
import asyncio
import redis
from datetime import datetime
import logging
from logging.config import dictConfig
log_config = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(levelprefix)s %(asctime)s %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"handlers": {
"default": {
"formatter": "default",
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
},
},
"loggers": {
"api-logger": {"handlers": ["default"], "level": "INFO"},
},
}
dictConfig(log_config)
containerIds_to_update = []
host_stats_isUpdating = False
app = FastAPI()
logger = logging.getLogger('api-logger')
@app.get("/host/stats")
@@ -21,18 +49,15 @@ async def get_host_update_stats():
global host_stats_isUpdating
if host_stats_isUpdating == False:
print("start host stats task")
asyncio.create_task(get_host_stats())
host_stats_isUpdating = True
while True:
if redis_client.exists('host_stats'):
break
print("wait for host_stats results")
await asyncio.sleep(1.5)
print("host stats pulled")
stats = json.loads(redis_client.get('host_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
@@ -106,14 +131,14 @@ async def post_containers(container_id : str, post_action : str, request: Reques
else:
api_call_method_name = '__'.join(['container_post', str(post_action) ])
docker_utils = DockerUtils(async_docker_client)
docker_utils = DockerUtils(sync_docker_client)
api_call_method = getattr(docker_utils, api_call_method_name, lambda container_id: Response(content=json.dumps({'type': 'danger', 'msg':'container_post - unknown api call' }, indent=4), media_type="application/json"))
print("api call: %s, container_id: %s" % (api_call_method_name, container_id))
return await api_call_method(container_id, request_json)
logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id))
return api_call_method(container_id, request_json)
except Exception as e:
print("error - container_post: %s" % str(e))
logger.error("error - container_post: %s" % str(e))
res = {
"type": "danger",
"msg": str(e)
@@ -152,398 +177,289 @@ class DockerUtils:
self.docker_client = docker_client
# api call: container_post - post_action: stop
async def container_post__stop(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
await container.stop()
res = {
'type': 'success',
'msg': 'command completed successfully'
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
def container_post__stop(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
container.stop()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: start
async def container_post__start(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
await container.start()
res = {
'type': 'success',
'msg': 'command completed successfully'
}
def container_post__start(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
container.start()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: restart
async def container_post__restart(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
await container.restart()
res = {
'type': 'success',
'msg': 'command completed successfully'
}
def container_post__restart(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
container.restart()
res = { 'type': 'success', 'msg': 'command completed successfully'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: top
async def container_post__top(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
ps_exec = await container.exec("ps")
async with ps_exec.start(detach=False) as stream:
ps_return = await stream.read_out()
exec_details = await ps_exec.inspect()
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
res = {
'type': 'success',
'msg': ps_return.data.decode('utf-8')
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
'type': 'danger',
'msg': ''
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
def container_post__top(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
res = { 'type': 'success', 'msg': container.top()}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: stats
def container_post__stats(self, container_id, request_json):
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
for stat in container.stats(decode=True, stream=True):
res = { 'type': 'success', 'msg': stat}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: mailq - task: delete
async def container_post__exec__mailq__delete(self, container_id, request_json):
def container_post__exec__mailq__delete(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-d %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids))
sanitized_string = str(' '.join(flagged_qids));
for container in self.docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return exec_run_handler('generic', postsuper_r)
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return await exec_run_handler('generic', postsuper_r_exec)
# api call: container_post - post_action: exec - cmd: mailq - task: hold
async def container_post__exec__mailq__hold(self, container_id, request_json):
def container_post__exec__mailq__hold(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-h %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids))
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return await exec_run_handler('generic', postsuper_r_exec)
sanitized_string = str(' '.join(flagged_qids));
for container in self.docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: mailq - task: cat
async def container_post__exec__mailq__cat(self, container_id, request_json):
def container_post__exec__mailq__cat(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
sanitized_string = str(' '.join(filtered_qids))
sanitized_string = str(' '.join(filtered_qids));
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
postcat_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
return await exec_run_handler('utf8_text_only', postcat_exec)
for container in self.docker_client.containers.list(filters={"id": container_id}):
postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
if not postcat_return:
postcat_return = 'err: invalid'
return exec_run_handler('utf8_text_only', postcat_return)
# api call: container_post - post_action: exec - cmd: mailq - task: unhold
async def container_post__exec__mailq__unhold(self, container_id, request_json):
def container_post__exec__mailq__unhold(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-H %s' % i for i in filtered_qids]
sanitized_string = str(' '.join(flagged_qids))
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return await exec_run_handler('generic', postsuper_r_exec)
sanitized_string = str(' '.join(flagged_qids));
for container in self.docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
return exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: mailq - task: deliver
async def container_post__exec__mailq__deliver(self, container_id, request_json):
def container_post__exec__mailq__deliver(self, container_id, request_json):
if 'items' in request_json:
r = re.compile("^[0-9a-fA-F]+$")
filtered_qids = filter(r.match, request_json['items'])
if filtered_qids:
flagged_qids = ['-i %s' % i for i in filtered_qids]
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
for i in flagged_qids:
postsuper_r_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
async with postsuper_r_exec.start(detach=False) as stream:
postsuper_r_return = await stream.read_out()
# todo: check each exit code
res = {
'type': 'success',
'msg': 'Scheduled immediate delivery'
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: mailq - task: list
async def container_post__exec__mailq__list(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
mailq_exec = await container.exec(["/usr/sbin/postqueue", "-j"], user='postfix')
return await exec_run_handler('utf8_text_only', mailq_exec)
# api call: container_post - post_action: exec - cmd: mailq - task: flush
async def container_post__exec__mailq__flush(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
postsuper_r_exec = await container.exec(["/usr/sbin/postqueue", "-f"], user='postfix')
return await exec_run_handler('generic', postsuper_r_exec)
# api call: container_post - post_action: exec - cmd: mailq - task: super_delete
async def container_post__exec__mailq__super_delete(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
postsuper_r_exec = await container.exec(["/usr/sbin/postsuper", "-d", "ALL"])
return await exec_run_handler('generic', postsuper_r_exec)
# api call: container_post - post_action: exec - cmd: system - task: fts_rescan
async def container_post__exec__system__fts_rescan(self, container_id, request_json):
if 'username' in request_json:
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
rescan_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail')
async with rescan_exec.start(detach=False) as stream:
rescan_return = await stream.read_out()
exec_details = await rescan_exec.inspect()
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
res = {
'type': 'success',
'msg': 'fts_rescan: rescan triggered'
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
'type': 'warning',
'msg': 'fts_rescan error'
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
if 'all' in request_json:
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
rescan_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
async with rescan_exec.start(detach=False) as stream:
rescan_return = await stream.read_out()
exec_details = await rescan_exec.inspect()
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
res = {
'type': 'success',
'msg': 'fts_rescan: rescan triggered'
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
'type': 'warning',
'msg': 'fts_rescan error'
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: system - task: df
async def container_post__exec__system__df(self, container_id, request_json):
if 'dir' in request_json:
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
df_exec = await container.exec(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
async with df_exec.start(detach=False) as stream:
df_return = await stream.read_out()
print(df_return)
print(await df_exec.inspect())
exec_details = await df_exec.inspect()
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
return df_return.data.decode('utf-8').rstrip()
else:
return "0,0,0,0,0,0"
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
async def container_post__exec__system__mysql_upgrade(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
sql_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
async with sql_exec.start(detach=False) as stream:
sql_return = await stream.read_out()
exec_details = await sql_exec.inspect()
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
matched = False
for line in sql_return.data.decode('utf-8').split("\n"):
if 'is already upgraded to' in line:
matched = True
if matched:
res = {
'type': 'success',
'msg': 'mysql_upgrade: already upgraded',
'text': sql_return.data.decode('utf-8')
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
await container.restart()
res = {
'type': 'warning',
'msg': 'mysql_upgrade: upgrade was applied',
'text': sql_return.data.decode('utf-8')
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
'type': 'error',
'msg': 'mysql_upgrade: error running command',
'text': sql_return.data.decode('utf-8')
}
for container in self.docker_client.containers.list(filters={"id": container_id}):
for i in flagged_qids:
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
# todo: check each exit code
res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
async def container_post__exec__system__mysql_tzinfo_to_sql(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
sql_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
async with sql_exec.start(detach=False) as stream:
sql_return = await stream.read_out()
exec_details = await sql_exec.inspect()
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
res = {
'type': 'info',
'msg': 'mysql_tzinfo_to_sql: command completed successfully',
'text': sql_return.data.decode('utf-8')
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
'type': 'error',
'msg': 'mysql_tzinfo_to_sql: error running command',
'text': sql_return.data.decode('utf-8')
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
async def container_post__exec__reload__dovecot(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
reload_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
return await exec_run_handler('generic', reload_exec)
# api call: container_post - post_action: exec - cmd: reload - task: postfix
async def container_post__exec__reload__postfix(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
reload_exec = await container.exec(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
return await exec_run_handler('generic', reload_exec)
# api call: container_post - post_action: exec - cmd: reload - task: nginx
async def container_post__exec__reload__nginx(self, container_id, request_json):
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
reload_exec = await container.exec(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
return await exec_run_handler('generic', reload_exec)
# api call: container_post - post_action: exec - cmd: sieve - task: list
async def container_post__exec__sieve__list(self, container_id, request_json):
if 'username' in request_json:
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
sieve_exec = await container.exec(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"])
return await exec_run_handler('utf8_text_only', sieve_exec)
# api call: container_post - post_action: exec - cmd: sieve - task: print
async def container_post__exec__sieve__print(self, container_id, request_json):
if 'username' in request_json and 'script_name' in request_json:
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
sieve_exec = await container.exec(cmd)
return await exec_run_handler('utf8_text_only', sieve_exec)
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
async def container_post__exec__maildir__cleanup(self, container_id, request_json):
if 'maildir' in request_json:
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
sane_name = re.sub(r'\W+', '', request_json['maildir'])
cmd = ["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request_json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request_json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"]
maildir_cleanup_exec = await container.exec(cmd, user='vmail')
return await exec_run_handler('generic', maildir_cleanup_exec)
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
async def container_post__exec__rspamd__worker_password(self, container_id, request_json):
if 'raw' in request_json:
for container in (await self.docker_client.containers.list()):
if container._id == container_id:
cmd = "./set_worker_password.sh '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
rspamd_password_exec = await container.exec(cmd, user='_rspamd')
async with rspamd_password_exec.start(detach=False) as stream:
rspamd_password_return = await stream.read_out()
matched = False
if "OK" in rspamd_password_return.data.decode('utf-8'):
# api call: container_post - post_action: exec - cmd: mailq - task: list
def container_post__exec__mailq__list(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
return exec_run_handler('utf8_text_only', mailq_return)
# api call: container_post - post_action: exec - cmd: mailq - task: flush
def container_post__exec__mailq__flush(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
return exec_run_handler('generic', postqueue_r)
# api call: container_post - post_action: exec - cmd: mailq - task: super_delete
def container_post__exec__mailq__super_delete(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
return exec_run_handler('generic', postsuper_r)
# api call: container_post - post_action: exec - cmd: system - task: fts_rescan
def container_post__exec__system__fts_rescan(self, container_id, request_json):
if 'username' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail')
if rescan_return.exit_code == 0:
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
if 'all' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
if rescan_return.exit_code == 0:
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: system - task: df
def container_post__exec__system__df(self, container_id, request_json):
if 'dir' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
if df_return.exit_code == 0:
return df_return.output.decode('utf-8').rstrip()
else:
return "0,0,0,0,0,0"
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
def container_post__exec__system__mysql_upgrade(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
if sql_return.exit_code == 0:
matched = False
for line in sql_return.output.decode('utf-8').split("\n"):
if 'is already upgraded to' in line:
matched = True
await container.restart()
if matched:
res = { 'type': 'success', 'msg':'mysql_upgrade: already upgraded', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
container.restart()
res = { 'type': 'warning', 'msg':'mysql_upgrade: upgrade was applied', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'error', 'msg': 'mysql_upgrade: error running command', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
def container_post__exec__system__mysql_tzinfo_to_sql(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
if sql_return.exit_code == 0:
res = { 'type': 'info', 'msg': 'mysql_tzinfo_to_sql: command completed successfully', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = { 'type': 'error', 'msg': 'mysql_tzinfo_to_sql: error running command', 'text': sql_return.output.decode('utf-8')}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
def container_post__exec__reload__dovecot(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
return exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: reload - task: postfix
def container_post__exec__reload__postfix(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
return exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: reload - task: nginx
def container_post__exec__reload__nginx(self, container_id, request_json):
for container in self.docker_client.containers.list(filters={"id": container_id}):
reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
return exec_run_handler('generic', reload_return)
# api call: container_post - post_action: exec - cmd: sieve - task: list
def container_post__exec__sieve__list(self, container_id, request_json):
if 'username' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"])
return exec_run_handler('utf8_text_only', sieve_return)
# api call: container_post - post_action: exec - cmd: sieve - task: print
def container_post__exec__sieve__print(self, container_id, request_json):
if 'username' in request.json and 'script_name' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
sieve_return = container.exec_run(cmd)
return exec_run_handler('utf8_text_only', sieve_return)
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
def container_post__exec__maildir__cleanup(self, container_id, request_json):
if 'maildir' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
sane_name = re.sub(r'\W+', '', request_json['maildir'])
cmd = ["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request_json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request_json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"]
maildir_cleanup = container.exec_run(cmd, user='vmail')
return exec_run_handler('generic', maildir_cleanup)
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
def container_post__exec__rspamd__worker_password(self, container_id, request_json):
if 'raw' in request_json:
for container in self.docker_client.containers.list(filters={"id": container_id}):
cmd = "/usr/bin/rspamadm pw -e -p '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
cmd_response = exec_cmd_container(container, cmd, user="_rspamd")
if matched:
res = {
'type': 'success',
'msg': 'command completed successfully'
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
'type': 'danger',
'msg': 'command did not complete'
}
return Response(content=json.dumps(res, indent=4), media_type="application/json")
matched = False
for line in cmd_response.split("\n"):
if '$2$' in line:
hash = line.strip()
hash_out = re.search('\$2\$.+$', hash).group(0)
rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc"
cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
cmd_response = exec_cmd_container(container, cmd, user="_rspamd")
if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response:
container.restart()
matched = True
if matched:
res = { 'type': 'success', 'msg': 'command completed successfully' }
logger.info('success changing Rspamd password')
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
logger.error('failed changing Rspamd password')
res = { 'type': 'danger', 'msg': 'command did not complete' }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
async def exec_run_handler(type, exec_obj):
async with exec_obj.start(detach=False) as stream:
exec_return = await stream.read_out()
def recv_socket_data(c_socket, timeout):
c_socket.setblocking(0)
total_data=[]
data=''
begin=time.time()
while True:
if total_data and time.time()-begin > timeout:
break
elif time.time()-begin > timeout*2:
break
try:
data = c_socket.recv(8192)
if data:
total_data.append(data.decode('utf-8'))
#change the beginning time for measurement
begin=time.time()
else:
#sleep for sometime to indicate a gap
time.sleep(0.1)
break
except:
pass
return ''.join(total_data)
if exec_return == None:
exec_return = ""
else:
exec_return = exec_return.data.decode('utf-8')
if type == 'generic':
exec_details = await exec_obj.inspect()
if exec_details["ExitCode"] == None or exec_details["ExitCode"] == 0:
res = {
"type": "success",
"msg": "command completed successfully"
}
try :
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
if not cmd.endswith("\n"):
cmd = cmd + "\n"
socket.send(cmd.encode('utf-8'))
data = recv_socket_data(socket, timeout)
socket.close()
return data
except Exception as e:
logger.error("error - exec_cmd_container: %s" % str(e))
traceback.print_exc(file=sys.stdout)
def exec_run_handler(type, output):
if type == 'generic':
if output.exit_code == 0:
res = { 'type': 'success', 'msg': 'command completed successfully' }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
else:
res = {
"type": "success",
"msg": "'command failed: " + exec_return
}
res = { 'type': 'danger', 'msg': 'command failed: ' + output.output.decode('utf-8') }
return Response(content=json.dumps(res, indent=4), media_type="application/json")
if type == 'utf8_text_only':
return Response(content=exec_return, media_type="text/plain")
return Response(content=output.output.decode('utf-8'), media_type="text/plain")
async def get_host_stats(wait=5):
global host_stats_isUpdating
@@ -570,12 +486,10 @@ async def get_host_stats(wait=5):
"type": "danger",
"msg": str(e)
}
print(json.dumps(res, indent=4))
await asyncio.sleep(wait)
host_stats_isUpdating = False
async def get_container_stats(container_id, wait=5, stop=False):
global containerIds_to_update
@@ -598,13 +512,11 @@ async def get_container_stats(container_id, wait=5, stop=False):
"type": "danger",
"msg": str(e)
}
print(json.dumps(res, indent=4))
else:
res = {
"type": "danger",
"msg": "no or invalid id defined"
}
print(json.dumps(res, indent=4))
await asyncio.sleep(wait)
if stop == True:
@@ -615,9 +527,13 @@ async def get_container_stats(container_id, wait=5, stop=False):
await get_container_stats(container_id, wait=0, stop=True)
if os.environ['REDIS_SLAVEOF_IP'] != "":
redis_client = redis.Redis(host=os.environ['REDIS_SLAVEOF_IP'], port=os.environ['REDIS_SLAVEOF_PORT'], db=0)
else:
redis_client = redis.Redis(host='redis-mailcow', port=6379, db=0)
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
logger.info('DockerApi started')

View File

@@ -1,21 +1,15 @@
FROM debian:bullseye-slim
FROM debian:bullseye-slim as build
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG DOVECOT=2.3.19.1
ARG FLATCURVE=v0.3.2
ARG XAPIAN=1.4.21
ENV LC_ALL C
ENV GOSU_VERSION 1.14
# Add groups and users before installing Dovecot to not break compatibility
RUN groupadd -g 5000 vmail \
&& groupadd -g 401 dovecot \
&& groupadd -g 402 dovenull \
&& groupadd -g 999 sogo \
&& usermod -a -G sogo nobody \
&& useradd -g vmail -u 5000 vmail -d /var/vmail \
&& useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \
&& useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull \
&& touch /etc/default/locale \
RUN touch /etc/default/locale \
&& apt-get update \
&& apt-get -y --no-install-recommends install \
apt-transport-https \
@@ -73,7 +67,13 @@ RUN groupadd -g 5000 vmail \
libunicode-string-perl \
liburi-perl \
libwww-perl \
libstemmer-dev \
libexttextcat-dev \
libldap-dev \
libghc-bzlib-dev \
lua-sql-mysql \
liblz4-dev \
libzstd-dev \
lua-socket \
mariadb-client \
procps \
@@ -84,30 +84,152 @@ RUN groupadd -g 5000 vmail \
syslog-ng-core \
syslog-ng-mod-redis \
wget \
git \
bison \
flex \
build-essential \
autoconf \
automake \
libtool \
make \
libxapian-dev \
default-libmysqlclient-dev \
libicu-dev \
zlib1g-dev \
pkg-config \
libsqlite3-dev \
liblua5.3-dev \
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \
&& chmod +x /usr/local/bin/gosu \
&& gosu nobody true \
&& apt-key adv --fetch-keys https://repo.dovecot.org/DOVECOT-REPO-GPG \
&& echo "deb https://repo.dovecot.org/ce-${DOVECOT}/debian/bullseye bullseye main" > /etc/apt/sources.list.d/dovecot.list \
&& apt-get update \
&& apt-get -y --no-install-recommends install \
dovecot-lua \
dovecot-managesieved \
dovecot-sieve \
dovecot-lmtpd \
dovecot-ldap \
dovecot-mysql \
dovecot-core \
dovecot-pop3d \
dovecot-imapd \
dovecot-solr \
&& pip3 install mysql-connector-python html2text jinja2 redis \
&& apt-get autoremove --purge -y \
&& apt-get autoclean \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /tmp/* /var/tmp/* /root/.cache/
&& gosu nobody true
# && apt-key adv --fetch-keys https://repo.dovecot.org/DOVECOT-REPO-GPG \
# && echo "deb https://repo.dovecot.org/ce-${DOVECOT}/debian/bullseye bullseye main" > /etc/apt/sources.list.d/dovecot.list \
# && apt-get update \
# && apt-get -y --no-install-recommends install \
# dovecot-lua \
# dovecot-managesieved \
# dovecot-sieve \
# dovecot-lmtpd \
# dovecot-ldap \
# dovecot-mysql \
# dovecot-core \
# dovecot-pop3d \
# dovecot-imapd \
# dovecot-dev
RUN cd /tmp && git clone --depth 1 --branch ${DOVECOT} https://github.com/dovecot/core.git dovecot/core && cd dovecot/core \
&& ./autogen.sh \
&& PANDOC=false ./configure --prefix=/usr --sysconfdir=/etc --localstatedir=/var --with-ssldir=/etc/ssl --enable-maintainer-mode --with-sql=yes --with-lua=yes --with-mysql --with-ldap --with-zstd --with-lz4 --with-ssl=openssl --with-notify=inotify --with-bzlib --with-zlib --enable-hardening --with-stemmer --with-textcat --with-icu \
&& make -j6 \
&& make install \
&& make clean
RUN cd /tmp && git clone --depth 1 --branch release-0.5 https://github.com/dovecot/pigeonhole dovecot/pigeonhole && cd dovecot/pigeonhole \
&& ./autogen.sh \
&& ./configure --with-dovecot=/usr/lib/dovecot --with-managesieve\
&& make -j6 \
&& make install \
&& make clean
RUN cd /tmp && wget https://oligarchy.co.uk/xapian/${XAPIAN}/xapian-core-${XAPIAN}.tar.xz && tar xf xapian-core-${XAPIAN}.tar.xz && cd xapian-core-${XAPIAN} \
&& ./configure --prefix=/usr/local/xapian \
&& make -j6 \
&& make install \
&& make clean
RUN cd /tmp && git clone --depth 1 --branch ${FLATCURVE} https://github.com/slusarz/dovecot-fts-flatcurve.git dovecot/flatcurve && cd dovecot/flatcurve \
&& ./autogen.sh \
&& ./configure --with-dovecot=/usr/lib/dovecot \
&& make -j6 \
&& make install \
&& make clean
FROM debian:bullseye-slim
RUN groupadd -g 5000 vmail \
&& groupadd -g 401 dovecot \
&& groupadd -g 402 dovenull \
&& groupadd -g 999 sogo \
&& usermod -a -G sogo nobody \
&& useradd -g vmail -u 5000 vmail -d /var/vmail \
&& useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \
&& useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull \
&& apt update && apt install lua-socket \
mariadb-client \
libstemmer-dev \
libexttextcat-dev \
libicu-dev \
libxapian-dev \
libsqlite3-dev \
liblua5.3-dev \
lua-sql-mysql \
libldap-dev \
procps \
python3-pip \
redis-server \
supervisor \
syslog-ng \
syslog-ng-core \
syslog-ng-mod-redis \
cpanminus \
curl \
libauthen-ntlm-perl \
libcgi-pm-perl \
libcrypt-openssl-rsa-perl \
libcrypt-ssleay-perl \
libdata-uniqid-perl \
libdbd-mysql-perl \
libdbi-perl \
libdigest-hmac-perl \
libdist-checkconflicts-perl \
libencode-imaputf7-perl \
libfile-copy-recursive-perl \
libfile-tail-perl \
libhtml-parser-perl \
libio-compress-perl \
libio-socket-inet6-perl \
libio-socket-ssl-perl \
libio-tee-perl \
libipc-run-perl \
libjson-webtoken-perl \
liblockfile-simple-perl \
libmail-imapclient-perl \
libmodule-implementation-perl \
libmodule-scandeps-perl \
libnet-ssleay-perl \
libpackage-stash-perl \
libpackage-stash-xs-perl \
libpar-packer-perl \
libparse-recdescent-perl \
libproc-processtable-perl \
libreadonly-perl \
libregexp-common-perl \
libsys-meminfo-perl \
libterm-readkey-perl \
libtest-deep-perl \
libtest-fatal-perl \
libtest-mock-guard-perl \
libtest-mockobject-perl \
libtest-nowarnings-perl \
libtest-pod-perl \
libtest-requires-perl \
libtest-simple-perl \
libtest-warn-perl \
libtry-tiny-perl \
libunicode-string-perl \
liburi-perl \
libwww-perl \
dnsutils \
gettext-base -y --no-install-recommends \
&& pip3 install mysql-connector-python html2text jinja2 redis
COPY --from=build /usr/lib/dovecot /usr/lib/dovecot
COPY --from=build /usr/bin/doveconf /usr/bin/doveconf
COPY --from=build /usr/bin/doveadm /usr/bin/doveadm
COPY --from=build /usr/bin/dovecot-sysreport /usr/bin/dovecot-sysreport
COPY --from=build /usr/sbin/dovecot /usr/sbin/dovecot
COPY --from=build /usr/libexec/dovecot/ /usr/libexec/dovecot/
COPY --from=build /usr/local/bin /usr/local/bin
COPY --from=build /usr/local/xapian/ /usr/local/xapian
COPY trim_logs.sh /usr/local/bin/trim_logs.sh
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf

View File

@@ -109,17 +109,19 @@ EOF
echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
if [[ "${SKIP_XAPIAN}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication' > /etc/dovecot/mail_plugins
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify listescape replication mail_log' > /etc/dovecot/mail_plugins_imap
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
else
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_solr listescape replication' > /etc/dovecot/mail_plugins
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_solr listescape replication' > /etc/dovecot/mail_plugins_imap
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_solr notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins
echo -n 'quota imap_quota imap_acl acl zlib imap_zlib imap_sieve mail_crypt mail_crypt_acl notify mail_log fts fts_flatcurve listescape replication' > /etc/dovecot/mail_plugins_imap
echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl fts fts_flatcurve notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
fi
chmod 644 /etc/dovecot/mail_plugins /etc/dovecot/mail_plugins_imap /etc/dovecot/mail_plugins_lmtp /templates/quarantine.tpl
sed -i 's/vsz_limit.*/vsz_limit = '${XAPIAN_HEAP}m/g /etc/dovecot/dovecot-fts-flatcurve.conf
cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql-userdb.conf
# Autogenerated by mailcow
driver = mysql

View File

@@ -359,21 +359,28 @@ def snat4(snat_target):
chain = iptc.Chain(table, 'POSTROUTING')
table.autocommit = False
new_rule = get_snat4_rule()
for position, rule in enumerate(chain.rules):
match = all((
new_rule.get_src() == rule.get_src(),
new_rule.get_dst() == rule.get_dst(),
new_rule.target.parameters == rule.target.parameters,
new_rule.target.name == rule.target.name
))
if position == 0:
if not match:
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
chain.insert_rule(new_rule)
else:
if match:
logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
chain.delete_rule(rule)
if not chain.rules:
# if there are no rules in the chain, insert the new rule directly
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
chain.insert_rule(new_rule)
else:
for position, rule in enumerate(chain.rules):
match = all((
new_rule.get_src() == rule.get_src(),
new_rule.get_dst() == rule.get_dst(),
new_rule.target.parameters == rule.target.parameters,
new_rule.target.name == rule.target.name
))
if position == 0:
if not match:
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
chain.insert_rule(new_rule)
else:
if match:
logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
chain.delete_rule(rule)
table.commit()
table.autocommit = True
except:

View File

@@ -1,12 +1,18 @@
FROM php:8.1-fpm-alpine3.17
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
ENV APCU_PECL 5.1.22
ENV IMAGICK_PECL 3.7.0
ENV MAILPARSE_PECL 3.1.4
ENV MEMCACHED_PECL 3.2.0
ENV REDIS_PECL 5.3.7
ENV COMPOSER 2.4.4
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced
ARG APCU_PECL_VERSION=5.1.22
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced
ARG IMAGICK_PECL_VERSION=3.7.0
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced
ARG MAILPARSE_PECL_VERSION=3.1.4
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced
ARG MEMCACHED_PECL_VERSION=3.2.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced
ARG REDIS_PECL_VERSION=5.3.7
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced
ARG COMPOSER_VERSION=2.5.4
RUN apk add -U --no-cache autoconf \
aspell-dev \
@@ -55,11 +61,11 @@ RUN apk add -U --no-cache autoconf \
samba-client \
zlib-dev \
tzdata \
&& pecl install mailparse-${MAILPARSE_PECL} \
&& pecl install redis-${REDIS_PECL} \
&& pecl install memcached-${MEMCACHED_PECL} \
&& pecl install APCu-${APCU_PECL} \
&& pecl install imagick-${IMAGICK_PECL} \
&& pecl install APCu-${APCU_PECL_VERSION} \
&& pecl install imagick-${IMAGICK_PECL_VERSION} \
&& pecl install mailparse-${MAILPARSE_PECL_VERSION} \
&& pecl install memcached-${MEMCACHED_PECL_VERSION} \
&& pecl install redis-${REDIS_PECL_VERSION} \
&& docker-php-ext-enable apcu imagick memcached mailparse redis \
&& pecl clear-cache \
&& docker-php-ext-configure intl \
@@ -72,7 +78,7 @@ RUN apk add -U --no-cache autoconf \
&& docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql pspell soap sockets zip bcmath gmp \
&& docker-php-ext-configure imap --with-imap --with-imap-ssl \
&& docker-php-ext-install -j 4 imap \
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER} \
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \
&& mv composer.phar /usr/local/bin/composer \
&& chmod +x /usr/local/bin/composer \
&& apk del --purge autoconf \
@@ -102,4 +108,4 @@ COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["php-fpm"]
CMD ["php-fpm"]

View File

@@ -3,8 +3,9 @@ LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG SOGO_DEBIAN_REPOSITORY=http://packages.sogo.nu/nightly/5/debian/
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
ARG GOSU_VERSION=1.16
ENV LC_ALL C
ENV GOSU_VERSION 1.14
# Prerequisites
RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \

View File

@@ -2,7 +2,8 @@ FROM solr:7.7-slim
USER root
ENV GOSU_VERSION 1.11
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
ARG GOSU_VERSION=1.16
COPY solr.sh /
COPY solr-config-7.7.0.xml /

View File

@@ -0,0 +1,23 @@
plugin {
fts = flatcurve
fts_autoindex = yes
fts_languages = en de
fts_tokenizers = generic email-address
fts_tokenizer_generic = algorithm=simple
# All of these are optional, and indicate the default values.
# They are listed here for documentation purposes; most people should
# not need to define/override in their config.
fts_flatcurve_commit_limit = 500
fts_flatcurve_max_term_size = 30
fts_flatcurve_min_term_size = 2
fts_flatcurve_optimize_limit = 10
fts_flatcurve_rotate_size = 5000
fts_flatcurve_rotate_time = 5000
fts_flatcurve_substring_search = yes
}
service indexer-worker {
vsz_limit = 1024m
}

View File

@@ -11,6 +11,7 @@ auth_mechanisms = plain login
#mail_debug = yes
#auth_debug = yes
log_path = syslog
log_debug = category=fts-flatcurve
disable_plaintext_auth = yes
# Uncomment on NFS share
#mmap_disable = yes
@@ -189,9 +190,6 @@ plugin {
acl_shared_dict = file:/var/vmail/shared-mailboxes.db
acl = vfile
acl_user = %u
fts = solr
fts_autoindex = yes
fts_solr = url=http://solr:8983/solr/dovecot-fts/
quota = dict:Userquota::proxy::sqlquota
quota_rule2 = Trash:storage=+100%%
sieve = /var/vmail/sieve/%u.sieve
@@ -242,6 +240,7 @@ plugin {
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
mail_log_fields = uid box msgid size
mail_log_cached_only = yes
}
service quota-warning {
executable = script /usr/local/bin/quota_notify.py
@@ -297,6 +296,7 @@ replication_dsync_parameters = -d -l 30 -U -n INBOX
!include_try /etc/dovecot/extra.conf
!include_try /etc/dovecot/sogo-sso.conf
!include_try /etc/dovecot/shared_namespace.conf
!include_try /etc/dovecot/dovecot-fts-flatcurve.conf
# </Includes>
default_client_limit = 10400
default_vsz_limit = 1024 M

View File

@@ -175,7 +175,7 @@ BAD_SUBJECT_00 {
type = "header";
header = "subject";
regexp = true;
map = "http://nullnull.org/bad-subject-regex.txt";
map = "http://fuzzy.mailcow.email/bad-subject-regex.txt";
score = 6.0;
symbols_set = ["BAD_SUBJECT_00"];
}

View File

@@ -103,6 +103,7 @@ $template_data = [
'rsettings' => $rsettings,
'rspamd_regex_maps' => $rspamd_regex_maps,
'logo_specs' => customize('get', 'main_logo_specs'),
'ip_check' => customize('get', 'ip_check'),
'password_complexity' => password_complexity('get'),
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
'lang_admin' => json_encode($lang['admin']),

View File

@@ -699,6 +699,38 @@ paths:
type: string
type: object
summary: Create Domain Admin user
/api/v1/add/sso/domain-admin:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
token: "591F6D-5C3DD2-7455CD-DAF1C1-AA4FCC"
description: OK
headers: { }
tags:
- Single Sign-On
description: >-
Using this endpoint you can issue a token for Domain Admin user. This token can be used for
autologin Domain Admin user by using query_string var sso_token={token}. Token expiration time is 30s
operationId: Issue Domain Admin SSO token
requestBody:
content:
application/json:
schema:
example:
username: testadmin
properties:
username:
description: the username for the admin user
type: object
type: object
summary: Issue Domain Admin SSO token
/api/v1/edit/da-acl:
post:
responses:
@@ -1999,7 +2031,7 @@ paths:
- domain.tld
- domain2.tld
properties:
items:
items:
type: array
items:
type: string
@@ -2993,7 +3025,7 @@ paths:
application/json:
schema:
type: array
items:
items:
type: object
properties:
log:
@@ -5586,6 +5618,8 @@ tags:
description: Manage DKIM keys
- name: Domain admin
description: Create or udpdate domain admin users
- name: Single Sign-On
description: Issue tokens for users
- name: Address Rewriting
description: Create BCC maps or recipient maps
- name: Outgoing TLS Policy Map Overrides

View File

@@ -77,4 +77,22 @@ li .dtr-data {
table.dataTable>tbody>tr.child span.dtr-title {
width: 30%;
max-width: 250px;
}
}
div.dataTables_wrapper div.dataTables_filter {
text-align: left;
}
div.dataTables_wrapper div.dataTables_length {
text-align: right;
}
.dataTables_paginate, .dataTables_length, .dataTables_filter {
margin: 10px 0!important;
}
td.dt-text-right {
text-align: end !important;
}
th.dt-text-right {
text-align: end !important;
}

View File

@@ -199,6 +199,13 @@
display: none !important;
}
div.dataTables_wrapper div.dataTables_length {
text-align: left;
}
.senders-mw220 {
max-width: 100% !important;
}
}
@media (max-width: 350px) {

View File

@@ -1,102 +1,104 @@
.pagination a {
text-decoration: none !important;
}
.panel.panel-default {
overflow: visible !important;
}
.table-responsive {
overflow: visible !important;
}
.table-responsive {
overflow-x: scroll !important;
}
.footer-add-item {
display: block;
text-align: center;
font-style: italic;
padding: 10px;
background: #F5F5F5;
}
@media (min-width: 992px) {
.container {
width: 100%;
}
}
@media (min-width: 1920px) {
.container {
width: 80%;
}
}
.mass-actions-quarantine {
user-select: none;
}
.inputMissingAttr {
border-color: #FF4136;
}
.modal#qidDetailModal p {
word-break: break-all;
}
span#qid_detail_score {
font-weight: 700;
margin-left: 5px;
}
span.rspamd-symbol {
display: inline-block;
margin: 2px 6px 2px 0;
border-radius: 4px;
padding: 0 7px;
}
span.rspamd-symbol.positive {
background: #4CAF50;
border: 1px solid #4CAF50;
color: white;
}
span.rspamd-symbol.negative {
background: #ff4136;
border: 1px solid #ff4136;
color: white;
}
span.rspamd-symbol.neutral {
background: #f5f5f5;
color: #333;
border: 1px solid #ccc;
}
span.rspamd-symbol span.score {
font-weight: 700;
}
span.mail-address-item {
background-color: #f5f5f5;
border-radius: 4px;
border: 1px solid #ccc;
padding: 2px 7px;
display: inline-block;
margin: 2px 6px 2px 0;
}
table tbody tr {
cursor: pointer;
}
table tbody tr td input[type="checkbox"] {
cursor: pointer;
}
.label-rspamd-action {
font-size:110%;
margin:20px;
}
.pagination a {
text-decoration: none !important;
}
.panel.panel-default {
overflow: visible !important;
}
.table-responsive {
overflow: visible !important;
}
.table-responsive {
overflow-x: scroll !important;
}
.footer-add-item {
display: block;
text-align: center;
font-style: italic;
padding: 10px;
background: #F5F5F5;
}
@media (min-width: 992px) {
.container {
width: 100%;
}
}
@media (min-width: 1920px) {
.container {
width: 80%;
}
}
.mass-actions-quarantine {
user-select: none;
}
.inputMissingAttr {
border-color: #FF4136;
}
.modal#qidDetailModal p {
word-break: break-all;
}
span#qid_detail_score {
font-weight: 700;
margin-left: 5px;
}
span.rspamd-symbol {
display: inline-block;
margin: 2px 6px 2px 0;
border-radius: 4px;
padding: 0 7px;
}
span.rspamd-symbol.positive {
background: #4CAF50;
border: 1px solid #4CAF50;
color: white;
}
span.rspamd-symbol.negative {
background: #ff4136;
border: 1px solid #ff4136;
color: white;
}
span.rspamd-symbol.neutral {
background: #f5f5f5;
color: #333;
border: 1px solid #ccc;
}
span.rspamd-symbol span.score {
font-weight: 700;
}
span.mail-address-item {
background-color: #f5f5f5;
border-radius: 4px;
border: 1px solid #ccc;
padding: 2px 7px;
display: inline-block;
margin: 2px 6px 2px 0;
}
table tbody tr {
cursor: pointer;
}
table tbody tr td input[type="checkbox"] {
cursor: pointer;
}
.label-rspamd-action {
font-size:110%;
margin:20px;
}
.senders-mw220 {
max-width: 220px;
}

View File

@@ -11,7 +11,86 @@
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
@import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,300;0,400;0,700;1,400&display=swap");
/* source-sans-pro-300 - latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
src: url('/fonts/source-sans-pro-v21-latin-300.eot'); /* IE9 Compat Modes */
src: local(''),
url('/fonts/source-sans-pro-v21-latin-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('/fonts/source-sans-pro-v21-latin-300.woff2') format('woff2'), /* Super Modern Browsers */
url('/fonts/source-sans-pro-v21-latin-300.woff') format('woff'), /* Modern Browsers */
url('/fonts/source-sans-pro-v21-latin-300.ttf') format('truetype'), /* Safari, Android, iOS */
url('/fonts/source-sans-pro-v21-latin-300.svg#SourceSansPro') format('svg'); /* Legacy iOS */
}
/* source-sans-pro-300italic - latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 300;
src: url('/fonts/source-sans-pro-v21-latin-300italic.eot'); /* IE9 Compat Modes */
src: local(''),
url('/fonts/source-sans-pro-v21-latin-300italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('/fonts/source-sans-pro-v21-latin-300italic.woff2') format('woff2'), /* Super Modern Browsers */
url('/fonts/source-sans-pro-v21-latin-300italic.woff') format('woff'), /* Modern Browsers */
url('/fonts/source-sans-pro-v21-latin-300italic.ttf') format('truetype'), /* Safari, Android, iOS */
url('/fonts/source-sans-pro-v21-latin-300italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
}
/* source-sans-pro-regular - latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
src: url('/fonts/source-sans-pro-v21-latin-regular.eot'); /* IE9 Compat Modes */
src: local(''),
url('/fonts/source-sans-pro-v21-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('/fonts/source-sans-pro-v21-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
url('/fonts/source-sans-pro-v21-latin-regular.woff') format('woff'), /* Modern Browsers */
url('/fonts/source-sans-pro-v21-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
url('/fonts/source-sans-pro-v21-latin-regular.svg#SourceSansPro') format('svg'); /* Legacy iOS */
}
/* source-sans-pro-italic - latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 400;
src: url('/fonts/source-sans-pro-v21-latin-italic.eot'); /* IE9 Compat Modes */
src: local(''),
url('/fonts/source-sans-pro-v21-latin-italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('/fonts/source-sans-pro-v21-latin-italic.woff2') format('woff2'), /* Super Modern Browsers */
url('/fonts/source-sans-pro-v21-latin-italic.woff') format('woff'), /* Modern Browsers */
url('/fonts/source-sans-pro-v21-latin-italic.ttf') format('truetype'), /* Safari, Android, iOS */
url('/fonts/source-sans-pro-v21-latin-italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
}
/* source-sans-pro-700 - latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
src: url('/fonts/source-sans-pro-v21-latin-700.eot'); /* IE9 Compat Modes */
src: local(''),
url('/fonts/source-sans-pro-v21-latin-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('/fonts/source-sans-pro-v21-latin-700.woff2') format('woff2'), /* Super Modern Browsers */
url('/fonts/source-sans-pro-v21-latin-700.woff') format('woff'), /* Modern Browsers */
url('/fonts/source-sans-pro-v21-latin-700.ttf') format('truetype'), /* Safari, Android, iOS */
url('/fonts/source-sans-pro-v21-latin-700.svg#SourceSansPro') format('svg'); /* Legacy iOS */
}
/* source-sans-pro-700italic - latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: italic;
font-weight: 700;
src: url('/fonts/source-sans-pro-v21-latin-700italic.eot'); /* IE9 Compat Modes */
src: local(''),
url('/fonts/source-sans-pro-v21-latin-700italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('/fonts/source-sans-pro-v21-latin-700italic.woff2') format('woff2'), /* Super Modern Browsers */
url('/fonts/source-sans-pro-v21-latin-700italic.woff') format('woff'), /* Modern Browsers */
url('/fonts/source-sans-pro-v21-latin-700italic.ttf') format('truetype'), /* Safari, Android, iOS */
url('/fonts/source-sans-pro-v21-latin-700italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
}
:root {
--bs-blue: #158cba;
--bs-indigo: #6610f2;

View File

@@ -358,3 +358,11 @@ table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
background: #333;
}
span.mail-address-item {
background-color: #333;
border-radius: 4px;
border: 1px solid #555;
padding: 2px 7px;
display: inline-block;
margin: 2px 6px 2px 0;
}

View File

@@ -65,6 +65,7 @@ $template_data = [
'solr_uptime' => round($solr_status['status']['dovecot-fts']['uptime'] / 1000 / 60 / 60),
'clamd_status' => $clamd_status,
'containers' => $containers,
'ip_check' => customize('get', 'ip_check'),
'lang_admin' => json_encode($lang['admin']),
'lang_debug' => json_encode($lang['debug']),
'lang_datatables' => json_encode($lang['datatables']),

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -160,6 +160,25 @@ function customize($_action, $_item, $_data = null) {
'msg' => 'ui_texts'
);
break;
case 'ip_check':
$ip_check = ($_data['ip_check_opt_in'] == "1") ? 1 : 0;
try {
$redis->set('IP_CHECK', $ip_check);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'ip_check_opt_in_modified'
);
break;
}
break;
case 'delete':
@@ -276,6 +295,20 @@ function customize($_action, $_item, $_data = null) {
return false;
}
break;
case 'ip_check':
try {
$ip_check = ($ip_check = $redis->get('IP_CHECK')) ? $ip_check : 0;
return $ip_check;
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
break;
}
break;
}

View File

@@ -1,407 +1,468 @@
<?php
function domain_admin($_action, $_data = null) {
global $pdo;
global $lang;
$_data_log = $_data;
!isset($_data_log['password']) ?: $_data_log['password'] = '*';
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
!isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
!isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
!isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
switch ($_action) {
case 'add':
$username = strtolower(trim($_data['username']));
$password = $_data['password'];
$password2 = $_data['password2'];
$domains = (array)$_data['domains'];
$active = intval($_data['active']);
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
if (empty($domains)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'domain_invalid'
);
return false;
}
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username) || $username == 'API') {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('username_invalid', $username)
);
return false;
}
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
$stmt = $pdo->prepare("SELECT `username` FROM `admin`
WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
foreach ($num_results as $num_results_each) {
if ($num_results_each != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('object_exists', htmlspecialchars($username))
);
return false;
}
}
if (password_check($password, $password2) !== true) {
continue;
}
$password_hashed = hash_password($password);
$valid_domains = 0;
foreach ($domains as $domain) {
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_invalid', htmlspecialchars($domain))
);
continue;
}
$valid_domains++;
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
VALUES (:username, :domain, :created, :active)");
$stmt->execute(array(
':username' => $username,
':domain' => $domain,
':created' => date('Y-m-d H:i:s'),
':active' => $active
));
}
if ($valid_domains != 0) {
$stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
VALUES (:username, :password_hashed, '0', :active)");
$stmt->execute(array(
':username' => $username,
':password_hashed' => $password_hashed,
':active' => $active
));
}
$stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)");
$stmt->execute(array(
':username' => $username
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_admin_added', htmlspecialchars($username))
);
break;
case 'edit':
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
// Administrator
if ($_SESSION['mailcow_cc_role'] == "admin") {
if (!is_array($_data['username'])) {
$usernames = array();
$usernames[] = $_data['username'];
}
else {
$usernames = $_data['username'];
}
foreach ($usernames as $username) {
$is_now = domain_admin('details', $username);
$domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null;
if (!empty($is_now)) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$domains = (!empty($domains)) ? $domains : $is_now['selected_domains'];
$username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
continue;
}
$password = $_data['password'];
$password2 = $_data['password2'];
if (!empty($domains)) {
foreach ($domains as $domain) {
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_invalid', htmlspecialchars($domain))
);
continue 2;
}
}
}
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('username_invalid', $username_new)
);
continue;
}
if ($username_new != $username) {
if (!empty(domain_admin('details', $username_new)['username'])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('username_invalid', $username_new)
);
continue;
}
}
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$stmt = $pdo->prepare("UPDATE `da_acl` SET `username` = :username_new WHERE `username` = :username");
$stmt->execute(array(
':username_new' => $username_new,
':username' => $username
));
if (!empty($domains)) {
foreach ($domains as $domain) {
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
VALUES (:username_new, :domain, :created, :active)");
$stmt->execute(array(
':username_new' => $username_new,
':domain' => $domain,
':created' => date('Y-m-d H:i:s'),
':active' => $active
));
}
}
if (!empty($password)) {
if (password_check($password, $password2) !== true) {
return false;
}
$password_hashed = hash_password($password);
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
$stmt->execute(array(
':password_hashed' => $password_hashed,
':username_new' => $username_new,
':username' => $username,
':active' => $active
));
if (isset($_data['disable_tfa'])) {
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
}
else {
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
}
}
else {
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
$stmt->execute(array(
':username_new' => $username_new,
':username' => $username,
':active' => $active
));
if (isset($_data['disable_tfa'])) {
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
}
else {
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
}
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_admin_modified', htmlspecialchars($username))
);
}
return true;
}
// Domain administrator
// Can only edit itself
elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
$username = $_SESSION['mailcow_cc_username'];
$password_old = $_data['user_old_pass'];
$password_new = $_data['user_new_pass'];
$password_new2 = $_data['user_new_pass2'];
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
WHERE `username` = :user");
$stmt->execute(array(':user' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!verify_hash($row['password'], $password_old)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
if (password_check($password_new, $password_new2) !== true) {
return false;
}
$password_hashed = hash_password($password_new);
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
$stmt->execute(array(
':password_hashed' => $password_hashed,
':username' => $username
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_admin_modified', htmlspecialchars($username))
);
}
break;
case 'delete':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$usernames = (array)$_data['username'];
foreach ($usernames as $username) {
if (empty(domain_admin('details', $username))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('username_invalid', $username)
);
continue;
}
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$stmt = $pdo->prepare("DELETE FROM `da_acl` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_admin_removed', htmlspecialchars($username))
);
}
break;
case 'get':
$domainadmins = array();
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$stmt = $pdo->query("SELECT DISTINCT
`username`
FROM `domain_admins`
WHERE `username` IN (
SELECT `username` FROM `admin`
WHERE `superadmin`!='1'
)");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) {
$domainadmins[] = $row['username'];
}
return $domainadmins;
break;
case 'details':
$domainadmindata = array();
if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) {
return false;
}
elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) {
return false;
}
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) {
return false;
}
$stmt = $pdo->prepare("SELECT
`tfa`.`active` AS `tfa_active`,
`domain_admins`.`username`,
`domain_admins`.`created`,
`domain_admins`.`active` AS `active`
FROM `domain_admins`
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
WHERE `domain_admins`.`username`= :domain_admin");
$stmt->execute(array(
':domain_admin' => $_data
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($row)) {
return false;
}
$domainadmindata['username'] = $row['username'];
$domainadmindata['tfa_active'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
$domainadmindata['tfa_active_int'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
$domainadmindata['active'] = $row['active'];
$domainadmindata['active_int'] = $row['active'];
$domainadmindata['created'] = $row['created'];
// GET SELECTED
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain` IN (
SELECT `domain` FROM `domain_admins`
WHERE `username`= :domain_admin)");
$stmt->execute(array(':domain_admin' => $_data));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$domainadmindata['selected_domains'][] = $row['domain'];
}
// GET UNSELECTED
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain` NOT IN (
SELECT `domain` FROM `domain_admins`
WHERE `username`= :domain_admin)");
$stmt->execute(array(':domain_admin' => $_data));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$domainadmindata['unselected_domains'][] = $row['domain'];
}
if (!isset($domainadmindata['unselected_domains'])) {
$domainadmindata['unselected_domains'] = "";
}
return $domainadmindata;
break;
}
}
<?php
function domain_admin($_action, $_data = null) {
global $pdo;
global $lang;
$_data_log = $_data;
!isset($_data_log['password']) ?: $_data_log['password'] = '*';
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
!isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
!isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
!isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
switch ($_action) {
case 'add':
$username = strtolower(trim($_data['username']));
$password = $_data['password'];
$password2 = $_data['password2'];
$domains = (array)$_data['domains'];
$active = intval($_data['active']);
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
if (empty($domains)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'domain_invalid'
);
return false;
}
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username) || $username == 'API') {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('username_invalid', $username)
);
return false;
}
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
$stmt = $pdo->prepare("SELECT `username` FROM `admin`
WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
foreach ($num_results as $num_results_each) {
if ($num_results_each != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('object_exists', htmlspecialchars($username))
);
return false;
}
}
if (password_check($password, $password2) !== true) {
continue;
}
$password_hashed = hash_password($password);
$valid_domains = 0;
foreach ($domains as $domain) {
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_invalid', htmlspecialchars($domain))
);
continue;
}
$valid_domains++;
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
VALUES (:username, :domain, :created, :active)");
$stmt->execute(array(
':username' => $username,
':domain' => $domain,
':created' => date('Y-m-d H:i:s'),
':active' => $active
));
}
if ($valid_domains != 0) {
$stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
VALUES (:username, :password_hashed, '0', :active)");
$stmt->execute(array(
':username' => $username,
':password_hashed' => $password_hashed,
':active' => $active
));
}
$stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)");
$stmt->execute(array(
':username' => $username
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_admin_added', htmlspecialchars($username))
);
break;
case 'edit':
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
// Administrator
if ($_SESSION['mailcow_cc_role'] == "admin") {
if (!is_array($_data['username'])) {
$usernames = array();
$usernames[] = $_data['username'];
}
else {
$usernames = $_data['username'];
}
foreach ($usernames as $username) {
$is_now = domain_admin('details', $username);
$domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null;
if (!empty($is_now)) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$domains = (!empty($domains)) ? $domains : $is_now['selected_domains'];
$username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
continue;
}
$password = $_data['password'];
$password2 = $_data['password2'];
if (!empty($domains)) {
foreach ($domains as $domain) {
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_invalid', htmlspecialchars($domain))
);
continue 2;
}
}
}
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('username_invalid', $username_new)
);
continue;
}
if ($username_new != $username) {
if (!empty(domain_admin('details', $username_new)['username'])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('username_invalid', $username_new)
);
continue;
}
}
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$stmt = $pdo->prepare("UPDATE `da_acl` SET `username` = :username_new WHERE `username` = :username");
$stmt->execute(array(
':username_new' => $username_new,
':username' => $username
));
if (!empty($domains)) {
foreach ($domains as $domain) {
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
VALUES (:username_new, :domain, :created, :active)");
$stmt->execute(array(
':username_new' => $username_new,
':domain' => $domain,
':created' => date('Y-m-d H:i:s'),
':active' => $active
));
}
}
if (!empty($password)) {
if (password_check($password, $password2) !== true) {
return false;
}
$password_hashed = hash_password($password);
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
$stmt->execute(array(
':password_hashed' => $password_hashed,
':username_new' => $username_new,
':username' => $username,
':active' => $active
));
if (isset($_data['disable_tfa'])) {
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
}
else {
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
}
}
else {
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
$stmt->execute(array(
':username_new' => $username_new,
':username' => $username,
':active' => $active
));
if (isset($_data['disable_tfa'])) {
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
}
else {
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
}
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_admin_modified', htmlspecialchars($username))
);
}
return true;
}
// Domain administrator
// Can only edit itself
elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
$username = $_SESSION['mailcow_cc_username'];
$password_old = $_data['user_old_pass'];
$password_new = $_data['user_new_pass'];
$password_new2 = $_data['user_new_pass2'];
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
WHERE `username` = :user");
$stmt->execute(array(':user' => $username));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!verify_hash($row['password'], $password_old)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
if (password_check($password_new, $password_new2) !== true) {
return false;
}
$password_hashed = hash_password($password_new);
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
$stmt->execute(array(
':password_hashed' => $password_hashed,
':username' => $username
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_admin_modified', htmlspecialchars($username))
);
}
break;
case 'delete':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$usernames = (array)$_data['username'];
foreach ($usernames as $username) {
if (empty(domain_admin('details', $username))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('username_invalid', $username)
);
continue;
}
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$stmt = $pdo->prepare("DELETE FROM `da_acl` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username");
$stmt->execute(array(
':username' => $username,
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('domain_admin_removed', htmlspecialchars($username))
);
}
break;
case 'get':
$domainadmins = array();
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$stmt = $pdo->query("SELECT DISTINCT
`username`
FROM `domain_admins`
WHERE `username` IN (
SELECT `username` FROM `admin`
WHERE `superadmin`!='1'
)");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while ($row = array_shift($rows)) {
$domainadmins[] = $row['username'];
}
return $domainadmins;
break;
case 'details':
$domainadmindata = array();
if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) {
return false;
}
elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) {
return false;
}
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) {
return false;
}
$stmt = $pdo->prepare("SELECT
`tfa`.`active` AS `tfa_active`,
`domain_admins`.`username`,
`domain_admins`.`created`,
`domain_admins`.`active` AS `active`
FROM `domain_admins`
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
WHERE `domain_admins`.`username`= :domain_admin");
$stmt->execute(array(
':domain_admin' => $_data
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($row)) {
return false;
}
$domainadmindata['username'] = $row['username'];
$domainadmindata['tfa_active'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
$domainadmindata['tfa_active_int'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
$domainadmindata['active'] = $row['active'];
$domainadmindata['active_int'] = $row['active'];
$domainadmindata['created'] = $row['created'];
// GET SELECTED
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain` IN (
SELECT `domain` FROM `domain_admins`
WHERE `username`= :domain_admin)");
$stmt->execute(array(':domain_admin' => $_data));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$domainadmindata['selected_domains'][] = $row['domain'];
}
// GET UNSELECTED
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
WHERE `domain` NOT IN (
SELECT `domain` FROM `domain_admins`
WHERE `username`= :domain_admin)");
$stmt->execute(array(':domain_admin' => $_data));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$domainadmindata['unselected_domains'][] = $row['domain'];
}
if (!isset($domainadmindata['unselected_domains'])) {
$domainadmindata['unselected_domains'] = "";
}
return $domainadmindata;
break;
}
}
function domain_admin_sso($_action, $_data) {
global $pdo;
switch ($_action) {
case 'check':
$token = $_data;
$stmt = $pdo->prepare("SELECT `t1`.`username` FROM `da_sso` AS `t1` JOIN `admin` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL '30' SECOND) AND `t2`.`active` = 1 AND `t2`.`superadmin` = 0;");
$stmt->execute(array(
':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token)
));
$return = $stmt->fetch(PDO::FETCH_ASSOC);
return empty($return['username']) ? false : $return['username'];
case 'issue':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => 'access_denied'
);
return false;
}
$username = $_data['username'];
$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results < 1) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data),
'msg' => array('object_doesnt_exist', htmlspecialchars($username))
);
return false;
}
$token = implode('-', array(
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3))),
strtoupper(bin2hex(random_bytes(3)))
));
$stmt = $pdo->prepare("INSERT INTO `da_sso` (`username`, `token`)
VALUES (:username, :token)");
$stmt->execute(array(
':username' => $username,
':token' => $token
));
// perform cleanup
$pdo->query("DELETE FROM `da_sso` WHERE created < DATE_SUB(NOW(), INTERVAL '30' SECOND);");
return ['token' => $token];
break;
}
}

View File

@@ -1739,7 +1739,7 @@ function verify_tfa_login($username, $_data) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_verification_failed', 'authenticator not found')
'msg' => array('webauthn_authenticator_failed')
);
return false;
}
@@ -1748,11 +1748,20 @@ function verify_tfa_login($username, $_data) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_verification_failed', 'publicKey not found')
'msg' => array('webauthn_publickey_failed')
);
return false;
}
if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_username_failed')
);
return false;
}
try {
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
}
@@ -1784,21 +1793,12 @@ function verify_tfa_login($username, $_data) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_verification_failed', 'could not determine user role')
'msg' => array('webauthn_role_failed')
);
return false;
}
}
if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $username, '*'),
'msg' => array('webauthn_verification_failed', 'user who requests does not match with sql entry')
);
return false;
}
$_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
$_SESSION['tfa_id'] = $process_webauthn['id'];
$_SESSION['authReq'] = null;

View File

@@ -2879,67 +2879,68 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
'msg' => 'extended_sender_acl_denied'
);
return false;
}
$extra_acls = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['extended_sender_acl']));
foreach ($extra_acls as $i => &$extra_acl) {
if (empty($extra_acl)) {
continue;
}
if (substr($extra_acl, 0, 1) === "@") {
$extra_acl = ltrim($extra_acl, '@');
}
if (!filter_var($extra_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name($extra_acl)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('extra_acl_invalid', htmlspecialchars($extra_acl))
);
unset($extra_acls[$i]);
continue;
}
$domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
if (filter_var($extra_acl, FILTER_VALIDATE_EMAIL)) {
$extra_acl_domain = idn_to_ascii(substr(strstr($extra_acl, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
if (in_array($extra_acl_domain, $domains)) {
else {
$extra_acls = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['extended_sender_acl']));
foreach ($extra_acls as $i => &$extra_acl) {
if (empty($extra_acl)) {
continue;
}
if (substr($extra_acl, 0, 1) === "@") {
$extra_acl = ltrim($extra_acl, '@');
}
if (!filter_var($extra_acl, FILTER_VALIDATE_EMAIL) && !is_valid_domain_name($extra_acl)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
'msg' => array('extra_acl_invalid', htmlspecialchars($extra_acl))
);
unset($extra_acls[$i]);
continue;
}
}
else {
if (in_array($extra_acl, $domains)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
);
unset($extra_acls[$i]);
continue;
$domains = array_merge(mailbox('get', 'domains'), mailbox('get', 'alias_domains'));
if (filter_var($extra_acl, FILTER_VALIDATE_EMAIL)) {
$extra_acl_domain = idn_to_ascii(substr(strstr($extra_acl, '@'), 1), 0, INTL_IDNA_VARIANT_UTS46);
if (in_array($extra_acl_domain, $domains)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
);
unset($extra_acls[$i]);
continue;
}
}
else {
if (in_array($extra_acl, $domains)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('extra_acl_invalid_domain', $extra_acl_domain)
);
unset($extra_acls[$i]);
continue;
}
$extra_acl = '@' . $extra_acl;
}
$extra_acl = '@' . $extra_acl;
}
}
$extra_acls = array_filter($extra_acls);
$extra_acls = array_values($extra_acls);
$extra_acls = array_unique($extra_acls);
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `logged_in_as` = :username");
$stmt->execute(array(
':username' => $username
));
foreach ($extra_acls as $sender_acl_external) {
$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`, `external`)
VALUES (:sender_acl, :username, 1)");
$extra_acls = array_filter($extra_acls);
$extra_acls = array_values($extra_acls);
$extra_acls = array_unique($extra_acls);
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `external` = 1 AND `logged_in_as` = :username");
$stmt->execute(array(
':sender_acl' => $sender_acl_external,
':username' => $username
));
foreach ($extra_acls as $sender_acl_external) {
$stmt = $pdo->prepare("INSERT INTO `sender_acl` (`send_as`, `logged_in_as`, `external`)
VALUES (:sender_acl, :username, 1)");
$stmt->execute(array(
':sender_acl' => $sender_acl_external,
':username' => $username
));
}
}
}
if (isset($_data['sender_acl'])) {
@@ -5170,15 +5171,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$tags = $_data['tags'];
if (!is_array($tags)) $tags = array();
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
$wasModified = false;
foreach ($domains as $domain) {
@@ -5190,7 +5182,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
continue;
}
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach($tags as $tag){
// delete tag
$wasModified = true;
@@ -5264,7 +5264,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
}
break;
}
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource'))) {
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource')) && getenv('SKIP_SOGO') != "y") {
update_sogo_static_view();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,140 +1,140 @@
<?php
// Start session
if (session_status() !== PHP_SESSION_ACTIVE) {
ini_set("session.cookie_httponly", 1);
ini_set('session.gc_maxlifetime', $SESSION_LIFETIME);
}
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == "https") {
if (session_status() !== PHP_SESSION_ACTIVE) {
ini_set("session.cookie_secure", 1);
}
$IS_HTTPS = true;
}
elseif (isset($_SERVER['HTTPS'])) {
if (session_status() !== PHP_SESSION_ACTIVE) {
ini_set("session.cookie_secure", 1);
}
$IS_HTTPS = true;
}
else {
$IS_HTTPS = false;
}
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
if (!isset($_SESSION['CSRF']['TOKEN'])) {
$_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32));
}
// Set session UA
if (!isset($_SESSION['SESS_REMOTE_UA'])) {
$_SESSION['SESS_REMOTE_UA'] = $_SERVER['HTTP_USER_AGENT'];
}
// Keep session active
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $SESSION_LIFETIME)) {
session_unset();
session_destroy();
}
$_SESSION['LAST_ACTIVITY'] = time();
// API
if (!empty($_SERVER['HTTP_X_API_KEY'])) {
$stmt = $pdo->prepare("SELECT * FROM `api` WHERE `api_key` = :api_key AND `active` = '1';");
$stmt->execute(array(
':api_key' => preg_replace('/[^a-zA-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY'])
));
$api_return = $stmt->fetch(PDO::FETCH_ASSOC);
if (!empty($api_return['api_key'])) {
$skip_ip_check = ($api_return['skip_ip_check'] == 1);
$remote = get_remote_ip(false);
$allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from']));
if ($skip_ip_check === true || ip_acl($remote, $allow_from)) {
$_SESSION['mailcow_cc_username'] = 'API';
$_SESSION['mailcow_cc_role'] = 'admin';
$_SESSION['mailcow_cc_api'] = true;
if ($api_return['access'] == 'rw') {
$_SESSION['mailcow_cc_api_access'] = 'rw';
}
else {
$_SESSION['mailcow_cc_api_access'] = 'ro';
}
}
else {
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
http_response_code(401);
echo json_encode(array(
'type' => 'error',
'msg' => 'api access denied for ip ' . $_SERVER['REMOTE_ADDR']
));
unset($_POST);
exit();
}
}
else {
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
http_response_code(401);
echo json_encode(array(
'type' => 'error',
'msg' => 'authentication failed'
));
unset($_POST);
exit();
}
}
// Handle logouts
if (isset($_POST["logout"])) {
if (isset($_SESSION["dual-login"])) {
$_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"];
$_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"];
unset($_SESSION["dual-login"]);
header("Location: /mailbox");
exit();
}
else {
session_regenerate_id(true);
session_unset();
session_destroy();
session_write_close();
header("Location: /");
}
}
// Check session
function session_check() {
if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) {
return true;
}
if (!isset($_SESSION['SESS_REMOTE_UA']) || ($_SESSION['SESS_REMOTE_UA'] != $_SERVER['HTTP_USER_AGENT'])) {
$_SESSION['return'][] = array(
'type' => 'warning',
'msg' => 'session_ua'
);
return false;
}
if (!empty($_POST)) {
if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) {
$_SESSION['return'][] = array(
'type' => 'warning',
'msg' => 'session_token'
);
return false;
}
unset($_POST['csrf_token']);
$_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32));
$_SESSION['CSRF']['TIME'] = time();
}
return true;
}
if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) {
$_POST = array();
$_FILES = array();
}
<?php
// Start session
if (session_status() !== PHP_SESSION_ACTIVE) {
ini_set("session.cookie_httponly", 1);
ini_set('session.gc_maxlifetime', $SESSION_LIFETIME);
}
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == "https") {
if (session_status() !== PHP_SESSION_ACTIVE) {
ini_set("session.cookie_secure", 1);
}
$IS_HTTPS = true;
}
elseif (isset($_SERVER['HTTPS'])) {
if (session_status() !== PHP_SESSION_ACTIVE) {
ini_set("session.cookie_secure", 1);
}
$IS_HTTPS = true;
}
else {
$IS_HTTPS = false;
}
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
if (!isset($_SESSION['CSRF']['TOKEN'])) {
$_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32));
}
// Set session UA
if (!isset($_SESSION['SESS_REMOTE_UA'])) {
$_SESSION['SESS_REMOTE_UA'] = $_SERVER['HTTP_USER_AGENT'];
}
// Keep session active
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $SESSION_LIFETIME)) {
session_unset();
session_destroy();
}
$_SESSION['LAST_ACTIVITY'] = time();
// API
if (!empty($_SERVER['HTTP_X_API_KEY'])) {
$stmt = $pdo->prepare("SELECT * FROM `api` WHERE `api_key` = :api_key AND `active` = '1';");
$stmt->execute(array(
':api_key' => preg_replace('/[^a-zA-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY'])
));
$api_return = $stmt->fetch(PDO::FETCH_ASSOC);
if (!empty($api_return['api_key'])) {
$skip_ip_check = ($api_return['skip_ip_check'] == 1);
$remote = get_remote_ip(false);
$allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from']));
if ($skip_ip_check === true || ip_acl($remote, $allow_from)) {
$_SESSION['mailcow_cc_username'] = 'API';
$_SESSION['mailcow_cc_role'] = 'admin';
$_SESSION['mailcow_cc_api'] = true;
if ($api_return['access'] == 'rw') {
$_SESSION['mailcow_cc_api_access'] = 'rw';
}
else {
$_SESSION['mailcow_cc_api_access'] = 'ro';
}
}
else {
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
http_response_code(401);
echo json_encode(array(
'type' => 'error',
'msg' => 'api access denied for ip ' . $_SERVER['REMOTE_ADDR']
));
unset($_POST);
exit();
}
}
else {
$redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']);
error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']);
http_response_code(401);
echo json_encode(array(
'type' => 'error',
'msg' => 'authentication failed'
));
unset($_POST);
exit();
}
}
// Handle logouts
if (isset($_POST["logout"])) {
if (isset($_SESSION["dual-login"])) {
$_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"];
$_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"];
unset($_SESSION["dual-login"]);
header("Location: /mailbox");
exit();
}
else {
session_regenerate_id(true);
session_unset();
session_destroy();
session_write_close();
header("Location: /");
}
}
// Check session
function session_check() {
if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) {
return true;
}
if (!isset($_SESSION['SESS_REMOTE_UA']) || ($_SESSION['SESS_REMOTE_UA'] != $_SERVER['HTTP_USER_AGENT'])) {
$_SESSION['return'][] = array(
'type' => 'warning',
'msg' => 'session_ua'
);
return false;
}
if (!empty($_POST)) {
if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) {
$_SESSION['return'][] = array(
'type' => 'warning',
'msg' => 'session_token'
);
return false;
}
unset($_POST['csrf_token']);
$_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32));
$_SESSION['CSRF']['TIME'] = time();
}
return true;
}
if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) {
$_POST = array();
$_FILES = array();
}

View File

@@ -1,4 +1,15 @@
<?php
// SSO Domain Admin
if (!empty($_GET['sso_token'])) {
$username = domain_admin_sso('check', $_GET['sso_token']);
if ($username !== false) {
$_SESSION['mailcow_cc_username'] = $username;
$_SESSION['mailcow_cc_role'] = 'domainadmin';
header('Location: /mailbox');
}
}
if (isset($_POST["verify_tfa_login"])) {
if (verify_tfa_login($_SESSION['pending_mailcow_cc_username'], $_POST)) {
$_SESSION['mailcow_cc_username'] = $_SESSION['pending_mailcow_cc_username'];
@@ -6,7 +17,7 @@ if (isset($_POST["verify_tfa_login"])) {
unset($_SESSION['pending_mailcow_cc_username']);
unset($_SESSION['pending_mailcow_cc_role']);
unset($_SESSION['pending_tfa_methods']);
header("Location: /user");
} else {
unset($_SESSION['pending_mailcow_cc_username']);
@@ -34,7 +45,7 @@ if (isset($_POST["quick_delete"])) {
if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) {
$login_user = strtolower(trim($_POST["login_user"]));
$as = check_login($login_user, $_POST["pass_user"]);
if ($as == "admin") {
$_SESSION['mailcow_cc_username'] = $login_user;
$_SESSION['mailcow_cc_role'] = "admin";

View File

@@ -124,7 +124,7 @@ $MAILCOW_APPS = array(
);
// Rows until pagination begins
$PAGINATION_SIZE = 20;
$PAGINATION_SIZE = 25;
// Default number of rows/lines to display (log table)
$LOG_LINES = 1000;

View File

@@ -12,14 +12,22 @@ $(document).ready(function() {
$.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}});
}
$(".generate_password").click(function( event ) {
$(".generate_password").click(async function( event ) {
try {
var password_policy = await window.fetch("/api/v1/get/passwordpolicy", { method:'GET', cache:'no-cache' });
var password_policy = await password_policy.json();
random_passwd_length = password_policy.length;
} catch(err) {
var random_passwd_length = 8;
}
event.preventDefault();
$('[data-hibp]').trigger('input');
if (typeof($(this).closest("form").data('pwgen-length')) == "number") {
var random_passwd = GPW.pronounceable($(this).closest("form").data('pwgen-length'))
}
else {
var random_passwd = GPW.pronounceable(8)
var random_passwd = GPW.pronounceable(random_passwd_length)
}
$(this).closest("form").find('[data-pwgen-field]').attr('type', 'text');
$(this).closest("form").find('[data-pwgen-field]').val(random_passwd);
@@ -278,6 +286,8 @@ $(document).ready(function() {
$.extend($.fn.dataTable.defaults, {
responsive: true
});
// disable default datatable click listener
$(document).off('click', 'tbody>tr');
// tag boxes
$('.tag-box .tag-add').click(function(){

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,7 @@ $(document).ready(function() {
});
// set update loop container list
containersToUpdate = {}
containersToUpdate = {};
// set default ChartJs Font Color
Chart.defaults.color = '#999';
// create host cpu and mem charts
@@ -44,14 +44,46 @@ $(document).ready(function() {
check_update(mailcow_info.version_tag, mailcow_info.project_url);
}
$("#maiclow_version").click(function(){
if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin" ||
mailcow_info.branch !== "master")
if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin" || mailcow_info.branch !== "master")
return;
showVersionModal("Version " + mailcow_info.version_tag, mailcow_info.version_tag);
})
// get public ips
get_public_ips();
$("#host_show_ip").click(function(){
$("#host_show_ip").find(".text").addClass("d-none");
$("#host_show_ip").find(".spinner-border").removeClass("d-none");
window.fetch("/api/v1/get/status/host/ip", { method:'GET', cache:'no-cache' }).then(function(response) {
return response.json();
}).then(function(data) {
console.log(data);
// display host ips
if (data.ipv4)
$("#host_ipv4").text(data.ipv4);
if (data.ipv6)
$("#host_ipv6").text(data.ipv6);
$("#host_show_ip").addClass("d-none");
$("#host_show_ip").find(".text").removeClass("d-none");
$("#host_show_ip").find(".spinner-border").addClass("d-none");
$("#host_ipv4").removeClass("d-none");
$("#host_ipv6").removeClass("d-none");
$("#host_ipv6").removeClass("text-danger");
$("#host_ipv4").addClass("d-block");
$("#host_ipv6").addClass("d-block");
}).catch(function(error){
console.log(error);
$("#host_ipv6").removeClass("d-none");
$("#host_ipv6").addClass("d-block");
$("#host_ipv6").addClass("text-danger");
$("#host_ipv6").text(lang_debug.error_show_ip);
$("#host_show_ip").find(".text").removeClass("d-none");
$("#host_show_ip").find(".spinner-border").addClass("d-none");
});
});
update_container_stats();
});
jQuery(function($){
@@ -85,11 +117,20 @@ jQuery(function($){
return;
}
$('#autodiscover_log').DataTable({
var table = $('#autodiscover_log').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order: [[0, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-autodiscover-logs', '#autodiscover_log');
},
ajax: {
type: "GET",
url: "/api/v1/get/logs/autodiscover/100",
@@ -134,6 +175,10 @@ jQuery(function($){
}
]
});
table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-autodiscover-logs', '#autodiscover_log');
});
}
function draw_postfix_logs() {
// just recalc width if instance already exists
@@ -142,11 +187,20 @@ jQuery(function($){
return;
}
$('#postfix_log').DataTable({
var table = $('#postfix_log').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order: [[0, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-postfix-logs', '#postfix_log');
},
ajax: {
type: "GET",
url: "/api/v1/get/logs/postfix",
@@ -176,6 +230,10 @@ jQuery(function($){
}
]
});
table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-postfix-logs', '#postfix_log');
});
}
function draw_watchdog_logs() {
// just recalc width if instance already exists
@@ -184,11 +242,20 @@ jQuery(function($){
return;
}
$('#watchdog_log').DataTable({
var table = $('#watchdog_log').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order: [[0, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-watchdog-logs', '#watchdog_log');
},
ajax: {
type: "GET",
url: "/api/v1/get/logs/watchdog",
@@ -222,6 +289,10 @@ jQuery(function($){
}
]
});
table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-watchdog-logs', '#watchdog_log');
});
}
function draw_api_logs() {
// just recalc width if instance already exists
@@ -230,11 +301,20 @@ jQuery(function($){
return;
}
$('#api_log').DataTable({
var table = $('#api_log').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order: [[0, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-api-logs', '#api_log');
},
ajax: {
type: "GET",
url: "/api/v1/get/logs/api",
@@ -275,6 +355,10 @@ jQuery(function($){
}
]
});
table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-api-logs', '#api_log');
});
}
function draw_rl_logs() {
// just recalc width if instance already exists
@@ -283,11 +367,20 @@ jQuery(function($){
return;
}
$('#rl_log').DataTable({
var table = $('#rl_log').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order: [[0, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-rl-logs', '#rl_log');
},
ajax: {
type: "GET",
url: "/api/v1/get/logs/ratelimited",
@@ -366,6 +459,10 @@ jQuery(function($){
}
]
});
table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-rl-logs', '#rl_log');
});
}
function draw_ui_logs() {
// just recalc width if instance already exists
@@ -374,11 +471,20 @@ jQuery(function($){
return;
}
$('#ui_logs').DataTable({
var table = $('#ui_logs').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order: [[0, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-ui-logs', '#ui_logs');
},
ajax: {
type: "GET",
url: "/api/v1/get/logs/ui",
@@ -437,6 +543,10 @@ jQuery(function($){
}
]
});
table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-ui-logs', '#ui_log');
});
}
function draw_sasl_logs() {
// just recalc width if instance already exists
@@ -445,11 +555,20 @@ jQuery(function($){
return;
}
$('#sasl_logs').DataTable({
var table = $('#sasl_logs').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order: [[0, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-sasl-logs', '#sasl_logs');
},
ajax: {
type: "GET",
url: "/api/v1/get/logs/sasl",
@@ -479,12 +598,16 @@ jQuery(function($){
data: 'datetime',
defaultContent: '',
createdCell: function(td, cellData) {
cellData = Math.floor((new Date(data.replace(/-/g, "/"))).getTime() / 1000);
cellData = Math.floor((new Date(cellData.replace(/-/g, "/"))).getTime() / 1000);
createSortableDate(td, cellData)
}
}
]
});
table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-sasl-logs', '#sasl_logs');
});
}
function draw_acme_logs() {
// just recalc width if instance already exists
@@ -493,11 +616,20 @@ jQuery(function($){
return;
}
$('#acme_log').DataTable({
var table = $('#acme_log').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order: [[0, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-acme-logs', '#acme_log');
},
ajax: {
type: "GET",
url: "/api/v1/get/logs/acme",
@@ -522,6 +654,10 @@ jQuery(function($){
}
]
});
table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-acme-logs', '#acme_log');
});
}
function draw_netfilter_logs() {
// just recalc width if instance already exists
@@ -530,11 +666,20 @@ jQuery(function($){
return;
}
$('#netfilter_log').DataTable({
var table = $('#netfilter_log').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order: [[0, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-netfilter-logs', '#netfilter_log');
},
ajax: {
type: "GET",
url: "/api/v1/get/logs/netfilter",
@@ -564,6 +709,10 @@ jQuery(function($){
}
]
});
table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-netfilter-logs', '#netfilter_log');
});
}
function draw_sogo_logs() {
// just recalc width if instance already exists
@@ -572,11 +721,20 @@ jQuery(function($){
return;
}
$('#sogo_log').DataTable({
var table = $('#sogo_log').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order: [[0, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-sogo-logs', '#sogo_log');
},
ajax: {
type: "GET",
url: "/api/v1/get/logs/sogo",
@@ -606,6 +764,10 @@ jQuery(function($){
}
]
});
table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-sogo-logs', '#sogo_log');
});
}
function draw_dovecot_logs() {
// just recalc width if instance already exists
@@ -614,11 +776,20 @@ jQuery(function($){
return;
}
$('#dovecot_log').DataTable({
var table = $('#dovecot_log').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order: [[0, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-dovecot-logs', '#dovecot_log');
},
ajax: {
type: "GET",
url: "/api/v1/get/logs/dovecot",
@@ -648,6 +819,10 @@ jQuery(function($){
}
]
});
table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-dovecot-logs', '#dovecot_log');
});
}
function rspamd_pie_graph() {
$.ajax({
@@ -717,11 +892,20 @@ jQuery(function($){
return;
}
$('#rspamd_history').DataTable({
var table = $('#rspamd_history').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: log_pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
order: [[0, 'desc']],
initComplete: function(){
hideTableExpandCollapseBtn('#tab-rspamd-logs', '#rspamd_history');
},
ajax: {
type: "GET",
url: "/api/v1/get/logs/rspamd-history",
@@ -810,6 +994,10 @@ jQuery(function($){
}
]
});
table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#tab-rspamd-history', '#rspamd_history');
});
}
function process_table_data(data, table) {
if (table == 'rspamd_history') {
@@ -821,31 +1009,31 @@ jQuery(function($){
item.rcpt = escapeHtml(item.rcpt_smtp.join(", "));
}
item.symbols = Object.keys(item.symbols).sort(function (a, b) {
if (item.symbols[a].score === 0) return 1
if (item.symbols[b].score === 0) return -1
if (item.symbols[a].score === 0) return 1;
if (item.symbols[b].score === 0) return -1;
if (item.symbols[b].score < 0 && item.symbols[a].score < 0) {
return item.symbols[a].score - item.symbols[b].score
return item.symbols[a].score - item.symbols[b].score;
}
if (item.symbols[b].score > 0 && item.symbols[a].score > 0) {
return item.symbols[b].score - item.symbols[a].score
return item.symbols[b].score - item.symbols[a].score;
}
return item.symbols[b].score - item.symbols[a].score
return item.symbols[b].score - item.symbols[a].score;
}).map(function(key) {
var sym = item.symbols[key];
if (sym.score < 0) {
sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)'
sym.score_formatted = '(<span class="text-success"><b>' + sym.score + '</b></span>)';
}
else if (sym.score === 0) {
sym.score_formatted = '(<span><b>' + sym.score + '</b></span>)'
sym.score_formatted = '(<span><b>' + sym.score + '</b></span>)';
}
else {
sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)'
sym.score_formatted = '(<span class="text-danger"><b>' + sym.score + '</b></span>)';
}
var str = '<strong>' + key + '</strong> ' + sym.score_formatted;
if (sym.options) {
str += ' [' + escapeHtml(sym.options.join(", ")) + "]";
}
return str
return str;
}).join('<br>\n');
item.subject = escapeHtml(item.subject);
var scan_time = item.time_real.toFixed(3);
@@ -978,14 +1166,14 @@ jQuery(function($){
}
});
}
return data
return data;
};
$('.add_log_lines').on('click', function (e) {
e.preventDefault();
var log_table= $(this).data("table")
var new_nrows = $(this).data("nrows")
var post_process = $(this).data("post-process")
var log_url = $(this).data("log-url")
var log_table= $(this).data("table");
var new_nrows = $(this).data("nrows");
var post_process = $(this).data("post-process");
var log_url = $(this).data("log-url");
if (log_table === undefined || new_nrows === undefined || post_process === undefined || log_url === undefined) {
console.log("no data-table or data-nrows or log_url or data-post-process attr found");
return;
@@ -1005,6 +1193,12 @@ jQuery(function($){
});
}
})
function hideTableExpandCollapseBtn(tab, table){
if ($(table).hasClass('collapsed'))
$(tab).find(".table_collapse_option").show();
else
$(tab).find(".table_collapse_option").hide();
}
// detect element visibility changes
function onVisible(element, callback) {
@@ -1037,7 +1231,6 @@ jQuery(function($){
onVisible("[id^=rspamd_donut]", () => rspamd_pie_graph());
// start polling host stats if tab is active
onVisible("[id^=tab-containers]", () => update_stats());
// start polling container stats if collapse is active
@@ -1120,9 +1313,9 @@ function update_stats(timeout=5){
if (mem_chart.data.labels.length > 30) mem_chart.data.labels.shift();
cpu_chart.data.datasets[0].data.push(data.cpu.usage);
if (cpu_chart.data.datasets[0].data.length > 30) cpu_chart.data.datasets[0].data.shift();
if (cpu_chart.data.datasets[0].data.length > 30) cpu_chart.data.datasets[0].data.shift();
mem_chart.data.datasets[0].data.push(data.memory.usage);
if (mem_chart.data.datasets[0].data.length > 30) mem_chart.data.datasets[0].data.shift();
if (mem_chart.data.datasets[0].data.length > 30) mem_chart.data.datasets[0].data.shift();
cpu_chart.update();
mem_chart.update();
@@ -1224,20 +1417,6 @@ function update_container_stats(timeout=5){
// run again in n seconds
setTimeout(update_container_stats, timeout * 1000);
}
// get public ips
function get_public_ips(){
window.fetch("/api/v1/get/status/host/ip", {method:'GET',cache:'no-cache'}).then(function(response) {
return response.json();
}).then(function(data) {
console.log(data);
// display host ips
if (data.ipv4)
$("#host_ipv4").text(data.ipv4);
if (data.ipv6)
$("#host_ipv6").text(data.ipv6);
});
}
// format hosts uptime seconds to readable string
function formatUptime(seconds){
seconds = Number(seconds);
@@ -1295,23 +1474,23 @@ function createReadWriteChart(chart_id, read_lable, write_lable){
};
var optionsNet = {
interaction: {
mode: 'index'
mode: 'index'
},
scales: {
yAxis: {
min: 0,
grid: {
display: false
display: false
},
ticks: {
callback: function(i, index, ticks) {
return formatBytes(i);
return formatBytes(i);
}
}
},
xAxis: {
grid: {
display: false
display: false
}
}
}
@@ -1359,13 +1538,13 @@ function createHostCpuAndMemChart(){
};
var optionsCpu = {
interaction: {
mode: 'index'
mode: 'index'
},
scales: {
yAxis: {
min: 0,
grid: {
display: false
display: false
},
ticks: {
callback: function(i, index, ticks) {
@@ -1375,7 +1554,7 @@ function createHostCpuAndMemChart(){
},
xAxis: {
grid: {
display: false
display: false
}
}
}
@@ -1397,13 +1576,13 @@ function createHostCpuAndMemChart(){
};
var optionsMem = {
interaction: {
mode: 'index'
mode: 'index'
},
scales: {
yAxis: {
min: 0,
grid: {
display: false
display: false
},
ticks: {
callback: function(i, index, ticks) {
@@ -1413,7 +1592,7 @@ function createHostCpuAndMemChart(){
},
xAxis: {
grid: {
display: false
display: false
}
}
}
@@ -1509,22 +1688,22 @@ function parseGithubMarkdownLinks(inputText) {
replacePattern1 = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
replacedText = inputText.replace(replacePattern1, (matched, index, original, input_string) => {
if (matched.includes('github.com')){
// return short link if it's github link
last_uri_path = matched.split('/');
last_uri_path = last_uri_path[last_uri_path.length - 1];
if (matched.includes('github.com')){
// return short link if it's github link
last_uri_path = matched.split('/');
last_uri_path = last_uri_path[last_uri_path.length - 1];
// adjust Full Changelog link to match last git version and new git version, if link is a compare link
if (matched.includes('/compare/') && mailcow_info.last_version_tag !== ''){
matched = matched.replace(last_uri_path, mailcow_info.last_version_tag + '...' + mailcow_info.version_tag);
last_uri_path = mailcow_info.last_version_tag + '...' + mailcow_info.version_tag;
}
// adjust Full Changelog link to match last git version and new git version, if link is a compare link
if (matched.includes('/compare/') && mailcow_info.last_version_tag !== ''){
matched = matched.replace(last_uri_path, mailcow_info.last_version_tag + '...' + mailcow_info.version_tag);
last_uri_path = mailcow_info.last_version_tag + '...' + mailcow_info.version_tag;
}
return '<a href="' + matched + '" target="_blank">' + last_uri_path + '</a><br>';
};
return '<a href="' + matched + '" target="_blank">' + last_uri_path + '</a><br>';
};
// if it's not a github link, return complete link
return '<a href="' + matched + '" target="_blank">' + matched + '</a>';
// if it's not a github link, return complete link
return '<a href="' + matched + '" target="_blank">' + matched + '</a>';
});
return replacedText;

View File

@@ -1,210 +1,222 @@
$(document).ready(function() {
$(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); });
$("#pushover_delete").click(function() { return confirm(lang.delete_ays); });
$(".goto_checkbox").click(function( event ) {
$("form[data-id='editalias'] .goto_checkbox").not(this).prop('checked', false);
if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
$('#textarea_alias_goto').prop('disabled', true);
}
else {
$("#textarea_alias_goto").removeAttr('disabled');
}
});
$("#disable_sender_check").click(function( event ) {
if ($("form[data-id='editmailbox'] #disable_sender_check:checked").length > 0) {
$('#editSelectSenderACL').prop('disabled', true);
$('#editSelectSenderACL').selectpicker('refresh');
}
else {
$('#editSelectSenderACL').prop('disabled', false);
$('#editSelectSenderACL').selectpicker('refresh');
}
});
if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
$('#textarea_alias_goto').prop('disabled', true);
}
$("#mailbox-password-warning-close").click(function( event ) {
$('#mailbox-passwd-hidden-info').addClass('hidden');
$('#mailbox-passwd-form-groups').removeClass('hidden');
});
// Sender ACL
if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
$("#sender_acl_disabled").show();
}
$('#editSelectSenderACL').change(function() {
if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
$("#sender_acl_disabled").show();
}
else {
$("#sender_acl_disabled").hide();
}
});
// Resources
if ($("#editSelectMultipleBookings").val() == "custom") {
$("#multiple_bookings_custom_div").show();
$('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
}
$("#editSelectMultipleBookings").change(function() {
$('input[name=multiple_bookings]').val($("#editSelectMultipleBookings").val());
if ($('input[name=multiple_bookings]').val() == "custom") {
$("#multiple_bookings_custom_div").show();
}
else {
$("#multiple_bookings_custom_div").hide();
}
});
$("#multiple_bookings_custom").bind("change keypress keyup blur", function() {
$('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
});
// load tags
if ($('#tags').length){
var tagsEl = $('#tags').parent().find('.tag-values')[0];
console.log($(tagsEl).val())
var tags = JSON.parse($(tagsEl).val());
$(tagsEl).val("");
for (var i = 0; i < tags.length; i++)
addTag($('#tags'), tags[i]);
}
});
jQuery(function($){
// http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
function validateEmail(email) {
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
function draw_wl_policy_domain_table() {
$('#wl_policy_domain_table').DataTable({
processing: true,
serverSide: false,
language: lang_datatables,
ajax: {
type: "GET",
url: '/api/v1/get/policy_wl_domain/' + table_for_domain,
dataSrc: function(data){
$.each(data, function (i, item) {
if (!validateEmail(item.object)) {
item.chkbox = '<input type="checkbox" data-id="policy_wl_domain" name="multi_select" value="' + item.prefid + '" />';
}
else {
item.chkbox = '<input type="checkbox" disabled title="' + lang_user.spamfilter_table_domain_policy + '" />';
}
});
return data;
}
},
columns: [
{
// placeholder, so checkbox will not block child row toggle
title: '',
data: null,
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: '',
data: 'chkbox',
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: 'ID',
data: 'prefid',
defaultContent: ''
},
{
title: lang_user.spamfilter_table_rule,
data: 'value',
defaultContent: ''
},
{
title: 'Scope',
data: 'object',
defaultContent: ''
}
]
});
}
function draw_bl_policy_domain_table() {
$('#bl_policy_domain_table').DataTable({
processing: true,
serverSide: false,
language: lang_datatables,
ajax: {
type: "GET",
url: '/api/v1/get/policy_bl_domain/' + table_for_domain,
dataSrc: function(data){
$.each(data, function (i, item) {
if (!validateEmail(item.object)) {
item.chkbox = '<input type="checkbox" data-id="policy_bl_domain" name="multi_select" value="' + item.prefid + '" />';
}
else {
item.chkbox = '<input type="checkbox" disabled tooltip="' + lang_user.spamfilter_table_domain_policy + '" />';
}
});
return data;
}
},
columns: [
{
// placeholder, so checkbox will not block child row toggle
title: '',
data: null,
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: '',
data: 'chkbox',
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: 'ID',
data: 'prefid',
defaultContent: ''
},
{
title: lang_user.spamfilter_table_rule,
data: 'value',
defaultContent: ''
},
{
title: 'Scope',
data: 'object',
defaultContent: ''
}
]
});
}
// detect element visibility changes
function onVisible(element, callback) {
$(document).ready(function() {
element_object = document.querySelector(element);
if (element_object === null) return;
new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if(entry.intersectionRatio > 0) {
callback(element_object);
observer.disconnect();
}
});
}).observe(element_object);
});
}
// Draw Table if tab is active
onVisible("[id^=wl_policy_domain_table]", () => draw_wl_policy_domain_table());
onVisible("[id^=bl_policy_domain_table]", () => draw_bl_policy_domain_table());
});
$(document).ready(function() {
$(".arrow-toggle").on('click', function(e) { e.preventDefault(); $(this).find('.arrow').toggleClass("animation"); });
$("#pushover_delete").click(function() { return confirm(lang.delete_ays); });
$(".goto_checkbox").click(function( event ) {
$("form[data-id='editalias'] .goto_checkbox").not(this).prop('checked', false);
if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
$('#textarea_alias_goto').prop('disabled', true);
}
else {
$("#textarea_alias_goto").removeAttr('disabled');
}
});
$("#disable_sender_check").click(function( event ) {
if ($("form[data-id='editmailbox'] #disable_sender_check:checked").length > 0) {
$('#editSelectSenderACL').prop('disabled', true);
$('#editSelectSenderACL').selectpicker('refresh');
}
else {
$('#editSelectSenderACL').prop('disabled', false);
$('#editSelectSenderACL').selectpicker('refresh');
}
});
if ($("form[data-id='editalias'] .goto_checkbox:checked").length > 0) {
$('#textarea_alias_goto').prop('disabled', true);
}
$("#mailbox-password-warning-close").click(function( event ) {
$('#mailbox-passwd-hidden-info').addClass('hidden');
$('#mailbox-passwd-form-groups').removeClass('hidden');
});
// Sender ACL
if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
$("#sender_acl_disabled").show();
}
$('#editSelectSenderACL').change(function() {
if ($("#editSelectSenderACL option[value='\*']:selected").length > 0){
$("#sender_acl_disabled").show();
}
else {
$("#sender_acl_disabled").hide();
}
});
// Resources
if ($("#editSelectMultipleBookings").val() == "custom") {
$("#multiple_bookings_custom_div").show();
$('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
}
$("#editSelectMultipleBookings").change(function() {
$('input[name=multiple_bookings]').val($("#editSelectMultipleBookings").val());
if ($('input[name=multiple_bookings]').val() == "custom") {
$("#multiple_bookings_custom_div").show();
}
else {
$("#multiple_bookings_custom_div").hide();
}
});
$("#multiple_bookings_custom").bind("change keypress keyup blur", function() {
$('input[name=multiple_bookings]').val($("#multiple_bookings_custom").val());
});
// load tags
if ($('#tags').length){
var tagsEl = $('#tags').parent().find('.tag-values')[0];
console.log($(tagsEl).val())
var tags = JSON.parse($(tagsEl).val());
$(tagsEl).val("");
for (var i = 0; i < tags.length; i++)
addTag($('#tags'), tags[i]);
}
});
jQuery(function($){
// http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
function validateEmail(email) {
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
function draw_wl_policy_domain_table() {
$('#wl_policy_domain_table').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
ajax: {
type: "GET",
url: '/api/v1/get/policy_wl_domain/' + table_for_domain,
dataSrc: function(data){
$.each(data, function (i, item) {
if (!validateEmail(item.object)) {
item.chkbox = '<input type="checkbox" data-id="policy_wl_domain" name="multi_select" value="' + item.prefid + '" />';
}
else {
item.chkbox = '<input type="checkbox" disabled title="' + lang_user.spamfilter_table_domain_policy + '" />';
}
});
return data;
}
},
columns: [
{
// placeholder, so checkbox will not block child row toggle
title: '',
data: null,
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: '',
data: 'chkbox',
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: 'ID',
data: 'prefid',
defaultContent: ''
},
{
title: lang_user.spamfilter_table_rule,
data: 'value',
defaultContent: ''
},
{
title: 'Scope',
data: 'object',
defaultContent: ''
}
]
});
}
function draw_bl_policy_domain_table() {
$('#bl_policy_domain_table').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
ajax: {
type: "GET",
url: '/api/v1/get/policy_bl_domain/' + table_for_domain,
dataSrc: function(data){
$.each(data, function (i, item) {
if (!validateEmail(item.object)) {
item.chkbox = '<input type="checkbox" data-id="policy_bl_domain" name="multi_select" value="' + item.prefid + '" />';
}
else {
item.chkbox = '<input type="checkbox" disabled tooltip="' + lang_user.spamfilter_table_domain_policy + '" />';
}
});
return data;
}
},
columns: [
{
// placeholder, so checkbox will not block child row toggle
title: '',
data: null,
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: '',
data: 'chkbox',
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: 'ID',
data: 'prefid',
defaultContent: ''
},
{
title: lang_user.spamfilter_table_rule,
data: 'value',
defaultContent: ''
},
{
title: 'Scope',
data: 'object',
defaultContent: ''
}
]
});
}
// detect element visibility changes
function onVisible(element, callback) {
$(document).ready(function() {
element_object = document.querySelector(element);
if (element_object === null) return;
new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if(entry.intersectionRatio > 0) {
callback(element_object);
observer.disconnect();
}
});
}).observe(element_object);
});
}
// Draw Table if tab is active
onVisible("[id^=wl_policy_domain_table]", () => draw_wl_policy_domain_table());
onVisible("[id^=bl_policy_domain_table]", () => draw_bl_policy_domain_table());
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,71 +1,71 @@
jQuery(function($){
var qitem = $('legend').data('hash');
var qError = $("#qid_error");
$.ajax({
url: '/inc/ajax/qitem_details.php',
data: { hash: qitem },
dataType: 'json',
success: function(data){
$('[data-id="qitems_single"]').each(function(index) {
$(this).attr("data-item", qitem);
});
$('#qid_detail_subj').text(data.subject);
$('#qid_detail_hfrom').text(data.header_from);
$('#qid_detail_efrom').text(data.env_from);
$('#qid_detail_score').html('');
$('#qid_detail_symbols').html('');
$('#qid_detail_recipients').html('');
$('#qid_detail_fuzzy').html('');
if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
$.each(data.fuzzy_hashes, function (index, value) {
$('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
});
} else {
$('#qid_detail_fuzzy').append('-');
}
if (typeof data.symbols !== 'undefined') {
data.symbols.sort(function (a, b) {
if (a.score === 0) return 1
if (b.score === 0) return -1
if (b.score < 0 && a.score < 0) {
return a.score - b.score
}
if (b.score > 0 && a.score > 0) {
return b.score - a.score
}
return b.score - a.score
})
$.each(data.symbols, function (index, value) {
var highlightClass = ''
if (value.score > 0) highlightClass = 'negative'
else if (value.score < 0) highlightClass = 'positive'
else highlightClass = 'neutral'
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
});
$('[data-bs-toggle="tooltip"]').tooltip()
}
if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
if (data.action === "add header") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.junk_folder + '</span>');
} else if (data.action === "reject") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + data.score + '</b> - ' + lang.rejected + '</span>');
} else if (data.action === "rewrite subject") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
}
}
if (typeof data.recipients !== 'undefined') {
$.each(data.recipients, function(index, value) {
var elem = $('<span class="mail-address-item"></span>');
elem.text(value.address + ' (' + value.type.toUpperCase() + ')');
$('#qid_detail_recipients').append(elem);
});
}
},
error: function(data){
if (typeof data.error !== 'undefined') {
qError.text("Error loading quarantine item");
qError.show();
}
}
});
});
jQuery(function($){
var qitem = $('legend').data('hash');
var qError = $("#qid_error");
$.ajax({
url: '/inc/ajax/qitem_details.php',
data: { hash: qitem },
dataType: 'json',
success: function(data){
$('[data-id="qitems_single"]').each(function(index) {
$(this).attr("data-item", qitem);
});
$('#qid_detail_subj').text(data.subject);
$('#qid_detail_hfrom').text(data.header_from);
$('#qid_detail_efrom').text(data.env_from);
$('#qid_detail_score').html('');
$('#qid_detail_symbols').html('');
$('#qid_detail_recipients').html('');
$('#qid_detail_fuzzy').html('');
if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
$.each(data.fuzzy_hashes, function (index, value) {
$('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
});
} else {
$('#qid_detail_fuzzy').append('-');
}
if (typeof data.symbols !== 'undefined') {
data.symbols.sort(function (a, b) {
if (a.score === 0) return 1;
if (b.score === 0) return -1;
if (b.score < 0 && a.score < 0) {
return a.score - b.score;
}
if (b.score > 0 && a.score > 0) {
return b.score - a.score;
}
return b.score - a.score;
})
$.each(data.symbols, function (index, value) {
var highlightClass = '';
if (value.score > 0) highlightClass = 'negative';
else if (value.score < 0) highlightClass = 'positive';
else highlightClass = 'neutral';
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
});
$('[data-bs-toggle="tooltip"]').tooltip();
}
if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
if (data.action === "add header") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.junk_folder + '</span>');
} else if (data.action === "reject") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + data.score + '</b> - ' + lang.rejected + '</span>');
} else if (data.action === "rewrite subject") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
}
}
if (typeof data.recipients !== 'undefined') {
$.each(data.recipients, function(index, value) {
var elem = $('<span class="mail-address-item"></span>');
elem.text(value.address + ' (' + value.type.toUpperCase() + ')');
$('#qid_detail_recipients').append(elem);
});
}
},
error: function(data){
if (typeof data.error !== 'undefined') {
qError.text("Error loading quarantine item");
qError.show();
}
}
});
});

View File

@@ -1,260 +1,297 @@
// Base64 functions
var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(r){var t,e,o,a,h,n,c,d="",C=0;for(r=Base64._utf8_encode(r);C<r.length;)a=(t=r.charCodeAt(C++))>>2,h=(3&t)<<4|(e=r.charCodeAt(C++))>>4,n=(15&e)<<2|(o=r.charCodeAt(C++))>>6,c=63&o,isNaN(e)?n=c=64:isNaN(o)&&(c=64),d=d+this._keyStr.charAt(a)+this._keyStr.charAt(h)+this._keyStr.charAt(n)+this._keyStr.charAt(c);return d},decode:function(r){var t,e,o,a,h,n,c="",d=0;for(r=r.replace(/[^A-Za-z0-9\+\/\=]/g,"");d<r.length;)t=this._keyStr.indexOf(r.charAt(d++))<<2|(a=this._keyStr.indexOf(r.charAt(d++)))>>4,e=(15&a)<<4|(h=this._keyStr.indexOf(r.charAt(d++)))>>2,o=(3&h)<<6|(n=this._keyStr.indexOf(r.charAt(d++))),c+=String.fromCharCode(t),64!=h&&(c+=String.fromCharCode(e)),64!=n&&(c+=String.fromCharCode(o));return c=Base64._utf8_decode(c)},_utf8_encode:function(r){r=r.replace(/\r\n/g,"\n");for(var t="",e=0;e<r.length;e++){var o=r.charCodeAt(e);o<128?t+=String.fromCharCode(o):o>127&&o<2048?(t+=String.fromCharCode(o>>6|192),t+=String.fromCharCode(63&o|128)):(t+=String.fromCharCode(o>>12|224),t+=String.fromCharCode(o>>6&63|128),t+=String.fromCharCode(63&o|128))}return t},_utf8_decode:function(r){for(var t="",e=0,o=c1=c2=0;e<r.length;)(o=r.charCodeAt(e))<128?(t+=String.fromCharCode(o),e++):o>191&&o<224?(c2=r.charCodeAt(e+1),t+=String.fromCharCode((31&o)<<6|63&c2),e+=2):(c2=r.charCodeAt(e+1),c3=r.charCodeAt(e+2),t+=String.fromCharCode((15&o)<<12|(63&c2)<<6|63&c3),e+=3);return t}};
jQuery(function($){
acl_data = JSON.parse(acl);
// http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
$(".refresh_table").on('click', function(e) {
e.preventDefault();
var table_name = $(this).data('table');
$('#' + table_name).DataTable().ajax.reload();
});
function draw_quarantine_table() {
$('#quarantinetable').DataTable({
processing: true,
serverSide: false,
language: lang_datatables,
ajax: {
type: "GET",
url: "/api/v1/get/quarantine/all",
dataSrc: function(data){
$.each(data, function (i, item) {
if (item.subject === null) {
item.subject = '';
} else {
item.subject = escapeHtml(item.subject);
}
if (item.score === null) {
item.score = '-';
}
if (item.virus_flag > 0) {
item.virus = '<span class="badge fs-6 bg-danger">' + lang.high_danger + '</span>';
} else {
item.virus = '<span class="badge fs-6 bg-secondary">' + lang.neutral_danger + '</span>';
}
if (item.action === "reject") {
item.rspamdaction = '<span class="badge fs-6 bg-danger">' + lang.rejected + '</span>';
} else if (item.action === "add header") {
item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.junk_folder + '</span>';
} else if (item.action === "rewrite subject") {
item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.rewrite_subject + '</span>';
}
if(item.notified > 0) {
item.notified = '&#10004;';
} else {
item.notified = '&#10006;';
}
if (acl_data.login_as === 1) {
item.action = '<div class="btn-group">' +
'<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-half btn-info show_qid_info"><i class="bi bi-box-arrow-up-right"></i> ' + lang.show_item + '</a>' +
'<a href="#" data-action="delete_selected" data-id="del-single-qitem" data-api-url="delete/qitem" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
'</div>';
}
else {
item.action = '<div class="btn-group">' +
'<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-info show_qid_info"><i class="bi bi-file-earmark-text"></i> ' + lang.show_item + '</a>' +
'</div>';
}
item.chkbox = '<input type="checkbox" data-id="qitems" name="multi_select" value="' + item.id + '" />';
});
return data;
}
},
columns: [
{
// placeholder, so checkbox will not block child row toggle
title: '',
data: null,
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: '',
data: 'chkbox',
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: 'ID',
data: 'id',
defaultContent: ''
},
{
title: lang.qid,
data: 'qid',
defaultContent: ''
},
{
title: lang.sender,
data: 'sender',
defaultContent: ''
},
{
title: lang.subj,
data: 'subject',
defaultContent: ''
},
{
title: lang.rspamd_result,
data: 'rspamdaction',
defaultContent: ''
},
{
title: lang.rcpt,
data: 'rcpt',
defaultContent: ''
},
{
title: lang.danger,
data: 'virus',
defaultContent: ''
},
{
title: lang.spam_score,
data: 'score',
defaultContent: ''
},
{
title: lang.notified,
data: 'notified',
defaultContent: ''
},
{
title: lang.received,
data: 'created',
defaultContent: '',
render: function (data,type) {
var date = new Date(data ? data * 1000 : 0);
return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
}
},
{
title: lang.action,
data: 'action',
className: 'text-md-end dt-sm-head-hidden dt-body-right',
defaultContent: ''
},
]
});
}
$('body').on('click', '.show_qid_info', function (e) {
e.preventDefault();
var qitem = $(this).attr('data-item');
var qError = $("#qid_error");
$('#qidDetailModal').modal('show');
qError.hide();
$.ajax({
url: '/inc/ajax/qitem_details.php',
data: { id: qitem },
dataType: 'json',
success: function(data){
$('[data-id="qitems_single"]').each(function(index) {
$(this).attr("data-item", qitem);
});
$("#quick_download_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&eml', '_blank')");
$("#quick_release_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_release', '_blank')");
$("#quick_delete_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_delete', '_blank')");
$('#qid_detail_subj').text(data.subject);
$('#qid_detail_hfrom').text(data.header_from);
$('#qid_detail_efrom').text(data.env_from);
$('#qid_detail_score').html('');
$('#qid_detail_recipients').html('');
$('#qid_detail_symbols').html('');
$('#qid_detail_fuzzy').html('');
if (typeof data.symbols !== 'undefined') {
data.symbols.sort(function (a, b) {
if (a.score === 0) return 1
if (b.score === 0) return -1
if (b.score < 0 && a.score < 0) {
return a.score - b.score
}
if (b.score > 0 && a.score > 0) {
return b.score - a.score
}
return b.score - a.score
})
$.each(data.symbols, function (index, value) {
var highlightClass = ''
if (value.score > 0) highlightClass = 'negative'
else if (value.score < 0) highlightClass = 'positive'
else highlightClass = 'neutral'
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
});
$('[data-bs-toggle="tooltip"]').tooltip()
}
if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
$.each(data.fuzzy_hashes, function (index, value) {
$('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
});
} else {
$('#qid_detail_fuzzy').append('-');
}
if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
if (data.action == "add header") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.junk_folder + '</span>');
} else if (data.action == "reject") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + data.score + '</b> - ' + lang.rejected + '</span>');
} else if (data.action == "rewrite subject") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
}
}
if (typeof data.recipients !== 'undefined') {
$.each(data.recipients, function(index, value) {
var elem = $('<span class="mail-address-item"></span>');
elem.text(value.address + ' (' + value.type.toUpperCase() + ')');
$('#qid_detail_recipients').append(elem);
});
}
$('#qid_detail_text').text(data.text_plain);
$('#qid_detail_text_from_html').text(data.text_html);
var qAtts = $("#qid_detail_atts");
if (typeof data.attachments !== 'undefined') {
qAtts.text('');
$.each(data.attachments, function(index, value) {
qAtts.append(
'<p><a href="/inc/ajax/qitem_details.php?id=' + qitem + '&att=' + index + '" target="_blank">' + value[0] + '</a> (' + value[1] + ')' +
' - <small><a href="' + value[3] + '" target="_blank">' + lang.check_hash + '</a></small></p>'
);
});
}
else {
qAtts.text('-');
}
},
error: function(data){
if (typeof data.error !== 'undefined') {
$('#qid_detail_subj').text('-');
$('#qid_detail_hfrom').text('-');
$('#qid_detail_efrom').text('-');
$('#qid_detail_score').html('-');
$('#qid_detail_recipients').html('-');
$('#qid_detail_symbols').html('-');
$('#qid_detail_fuzzy').html('-');
$('#qid_detail_text').text('-');
$('#qid_detail_text_from_html').text('-');
qError.text("Error loading quarantine item");
qError.show();
}
}
});
});
$('body').on('click', 'span.footable-toggle', function () {
event.stopPropagation();
})
// Initial table drawings
draw_quarantine_table();
});
// Base64 functions
var Base64={_keyStr:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",encode:function(r){var t,e,o,a,h,n,c,d="",C=0;for(r=Base64._utf8_encode(r);C<r.length;)a=(t=r.charCodeAt(C++))>>2,h=(3&t)<<4|(e=r.charCodeAt(C++))>>4,n=(15&e)<<2|(o=r.charCodeAt(C++))>>6,c=63&o,isNaN(e)?n=c=64:isNaN(o)&&(c=64),d=d+this._keyStr.charAt(a)+this._keyStr.charAt(h)+this._keyStr.charAt(n)+this._keyStr.charAt(c);return d},decode:function(r){var t,e,o,a,h,n,c="",d=0;for(r=r.replace(/[^A-Za-z0-9\+\/\=]/g,"");d<r.length;)t=this._keyStr.indexOf(r.charAt(d++))<<2|(a=this._keyStr.indexOf(r.charAt(d++)))>>4,e=(15&a)<<4|(h=this._keyStr.indexOf(r.charAt(d++)))>>2,o=(3&h)<<6|(n=this._keyStr.indexOf(r.charAt(d++))),c+=String.fromCharCode(t),64!=h&&(c+=String.fromCharCode(e)),64!=n&&(c+=String.fromCharCode(o));return c=Base64._utf8_decode(c)},_utf8_encode:function(r){r=r.replace(/\r\n/g,"\n");for(var t="",e=0;e<r.length;e++){var o=r.charCodeAt(e);o<128?t+=String.fromCharCode(o):o>127&&o<2048?(t+=String.fromCharCode(o>>6|192),t+=String.fromCharCode(63&o|128)):(t+=String.fromCharCode(o>>12|224),t+=String.fromCharCode(o>>6&63|128),t+=String.fromCharCode(63&o|128))}return t},_utf8_decode:function(r){for(var t="",e=0,o=c1=c2=0;e<r.length;)(o=r.charCodeAt(e))<128?(t+=String.fromCharCode(o),e++):o>191&&o<224?(c2=r.charCodeAt(e+1),t+=String.fromCharCode((31&o)<<6|63&c2),e+=2):(c2=r.charCodeAt(e+1),c3=r.charCodeAt(e+2),t+=String.fromCharCode((15&o)<<12|(63&c2)<<6|63&c3),e+=3);return t}};
jQuery(function($){
acl_data = JSON.parse(acl);
// http://stackoverflow.com/questions/24816/escaping-html-strings-with-jquery
var entityMap={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#x2F;","`":"&#x60;","=":"&#x3D;"};
function escapeHtml(n){return String(n).replace(/[&<>"'`=\/]/g,function(n){return entityMap[n]})}
function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
$(".refresh_table").on('click', function(e) {
e.preventDefault();
var table_name = $(this).data('table');
$('#' + table_name).DataTable().ajax.reload();
});
function draw_quarantine_table() {
var table = $('#quarantinetable').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
order: [[2, 'desc']],
lengthMenu: [
[10, 25, 50, 100, -1],
[10, 25, 50, 100, 'all']
],
pagingType: 'first_last_numbers',
aColumns: [
{ sWidth: '8.25%' },
{ sClass: 'classDataTable' }
],
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
initComplete: function(){
hideTableExpandCollapseBtn('#quarantinetable');
},
ajax: {
type: "GET",
url: "/api/v1/get/quarantine/all",
dataSrc: function(data){
$.each(data, function (i, item) {
if (item.subject === null) {
item.subject = '';
} else {
item.subject = escapeHtml(item.subject);
}
if (item.score === null) {
item.score = '-';
}
if (item.virus_flag > 0) {
item.virus = '<span class="badge fs-6 bg-danger">' + lang.high_danger + '</span>';
} else {
item.virus = '<span class="badge fs-6 bg-secondary">' + lang.neutral_danger + '</span>';
}
if (item.action === "reject") {
item.rspamdaction = '<span class="badge fs-6 bg-danger">' + lang.rejected + '</span>';
} else if (item.action === "add header") {
item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.junk_folder + '</span>';
} else if (item.action === "rewrite subject") {
item.rspamdaction = '<span class="badge fs-6 bg-warning">' + lang.rewrite_subject + '</span>';
}
if(item.notified > 0) {
item.notified = '&#10004;';
} else {
item.notified = '&#10006;';
}
if (acl_data.login_as === 1) {
item.action = '<div class="btn-group">' +
'<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-half btn-info show_qid_info"><i class="bi bi-box-arrow-up-right"></i> ' + lang.show_item + '</a>' +
'<a href="#" data-action="delete_selected" data-id="del-single-qitem" data-api-url="delete/qitem" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
'</div>';
}
else {
item.action = '<div class="btn-group">' +
'<a href="#" data-item="' + encodeURI(item.id) + '" class="btn btn-xs btn-info show_qid_info"><i class="bi bi-file-earmark-text"></i> ' + lang.show_item + '</a>' +
'</div>';
}
item.chkbox = '<input type="checkbox" data-id="qitems" name="multi_select" value="' + item.id + '" />';
});
return data;
}
},
columns: [
{
// placeholder, so checkbox will not block child row toggle
title: '',
data: null,
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: '',
data: 'chkbox',
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: 'ID',
data: 'id',
defaultContent: ''
},
{
title: lang.qid,
data: 'qid',
defaultContent: ''
},
{
title: lang.sender,
data: 'sender',
className: 'senders-mw220',
defaultContent: ''
},
{
title: lang.subj,
data: 'subject',
defaultContent: ''
},
{
title: lang.rspamd_result,
data: 'rspamdaction',
defaultContent: ''
},
{
title: lang.rcpt,
data: 'rcpt',
defaultContent: ''
},
{
title: lang.danger,
data: 'virus',
defaultContent: ''
},
{
title: lang.spam_score,
data: 'score',
defaultContent: ''
},
{
title: lang.notified,
data: 'notified',
defaultContent: ''
},
{
title: lang.received,
data: 'created',
defaultContent: '',
createdCell: function(td, cellData) {
$(td).attr({
"data-order": cellData,
"data-sort": cellData
});
var date = new Date(cellData ? cellData * 1000 : 0);
var dateString = date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
$(td).html(dateString);
}
},
{
title: lang.action,
data: 'action',
className: 'dt-text-right dt-sm-head-hidden',
defaultContent: ''
},
]
});
table.on('responsive-resize', function (e, datatable, columns){
hideTableExpandCollapseBtn('#quarantinetable');
});
}
$('body').on('click', '.show_qid_info', function (e) {
e.preventDefault();
var qitem = $(this).attr('data-item');
var qError = $("#qid_error");
$('#qidDetailModal').modal('show');
qError.hide();
$.ajax({
url: '/inc/ajax/qitem_details.php',
data: { id: qitem },
dataType: 'json',
success: function(data){
$('[data-id="qitems_single"]').each(function(index) {
$(this).attr("data-item", qitem);
});
$("#quick_download_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&eml', '_blank')");
$("#quick_release_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_release', '_blank')");
$("#quick_delete_link").attr("onclick", "window.open('/inc/ajax/qitem_details.php?id=" + qitem + "&quick_delete', '_blank')");
$('#qid_detail_subj').text(data.subject);
$('#qid_detail_hfrom').text(data.header_from);
$('#qid_detail_efrom').text(data.env_from);
$('#qid_detail_score').html('');
$('#qid_detail_recipients').html('');
$('#qid_detail_symbols').html('');
$('#qid_detail_fuzzy').html('');
if (typeof data.symbols !== 'undefined') {
data.symbols.sort(function (a, b) {
if (a.score === 0) return 1;
if (b.score === 0) return -1;
if (b.score < 0 && a.score < 0) {
return a.score - b.score;
}
if (b.score > 0 && a.score > 0) {
return b.score - a.score;
}
return b.score - a.score;
})
$.each(data.symbols, function (index, value) {
var highlightClass = '';
if (value.score > 0) highlightClass = 'negative';
else if (value.score < 0) highlightClass = 'positive';
else highlightClass = 'neutral';
$('#qid_detail_symbols').append('<span data-bs-toggle="tooltip" class="rspamd-symbol ' + highlightClass + '" title="' + (value.options ? value.options.join(', ') : '') + '">' + value.name + ' (<span class="score">' + value.score + '</span>)</span>');
});
$('[data-bs-toggle="tooltip"]').tooltip();
}
if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) {
$.each(data.fuzzy_hashes, function (index, value) {
$('#qid_detail_fuzzy').append('<p style="font-family:monospace">' + value + '</p>');
});
} else {
$('#qid_detail_fuzzy').append('-');
}
if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') {
if (data.action == "add header") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.junk_folder + '</span>');
} else if (data.action == "reject") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-danger"><b>' + data.score + '</b> - ' + lang.rejected + '</span>');
} else if (data.action == "rewrite subject") {
$('#qid_detail_score').append('<span class="label-rspamd-action badge fs-6 bg-warning"><b>' + data.score + '</b> - ' + lang.rewrite_subject + '</span>');
}
}
if (typeof data.recipients !== 'undefined') {
$.each(data.recipients, function(index, value) {
var elem = $('<span class="mail-address-item"></span>');
elem.text(value.address + ' (' + value.type.toUpperCase() + ')');
$('#qid_detail_recipients').append(elem);
});
}
$('#qid_detail_text').text(data.text_plain);
$('#qid_detail_text_from_html').text(data.text_html);
var qAtts = $("#qid_detail_atts");
if (typeof data.attachments !== 'undefined') {
qAtts.text('');
$.each(data.attachments, function(index, value) {
qAtts.append(
'<p><a href="/inc/ajax/qitem_details.php?id=' + qitem + '&att=' + index + '" target="_blank">' + value[0] + '</a> (' + value[1] + ')' +
' - <small><a href="' + value[3] + '" target="_blank">' + lang.check_hash + '</a></small></p>'
);
});
}
else {
qAtts.text('-');
}
},
error: function(data){
if (typeof data.error !== 'undefined') {
$('#qid_detail_subj').text('-');
$('#qid_detail_hfrom').text('-');
$('#qid_detail_efrom').text('-');
$('#qid_detail_score').html('-');
$('#qid_detail_recipients').html('-');
$('#qid_detail_symbols').html('-');
$('#qid_detail_fuzzy').html('-');
$('#qid_detail_text').text('-');
$('#qid_detail_text_from_html').text('-');
qError.text("Error loading quarantine item");
qError.show();
}
}
});
});
$('body').on('click', 'span.footable-toggle', function () {
event.stopPropagation();
})
// Initial table drawings
draw_quarantine_table();
function hideTableExpandCollapseBtn(table){
if ($(table).hasClass('collapsed'))
$(".table_collapse_option").show();
else
$(".table_collapse_option").hide();
}
});

View File

@@ -1,123 +1,128 @@
jQuery(function($){
$(".refresh_table").on('click', function(e) {
e.preventDefault();
var table_name = $(this).data('table');
$('#' + table_name).DataTable().ajax.reload();
});
$(".refresh_table").on('click', function(e) {
e.preventDefault();
var table_name = $(this).data('table');
$('#' + table_name).DataTable().ajax.reload();
});
function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
function humanFileSize(i){if(Math.abs(i)<1024)return i+" B";var B=["KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"],e=-1;do{i/=1024,++e}while(Math.abs(i)>=1024&&e<B.length-1);return i.toFixed(1)+" "+B[e]}
// Queue item
$('#showQueuedMsg').on('show.bs.modal', function (e) {
$('#queue_msg_content').text(lang.loading);
button = $(e.relatedTarget)
if (button != null) {
$('#queue_id').text(button.data('queue-id'));
}
$.ajax({
type: 'GET',
url: '/api/v1/get/postcat/' + button.data('queue-id'),
dataType: 'text',
complete: function (data) {
console.log(data);
$('#queue_msg_content').text(data.responseText);
}
});
})
function draw_queue() {
// just recalc width if instance already exists
if ($.fn.DataTable.isDataTable('#queuetable') ) {
$('#queuetable').DataTable().columns.adjust().responsive.recalc();
return;
// Queue item
$('#showQueuedMsg').on('show.bs.modal', function (e) {
$('#queue_msg_content').text(lang.loading);
button = $(e.relatedTarget)
if (button != null) {
$('#queue_id').text(button.data('queue-id'));
}
$.ajax({
type: 'GET',
url: '/api/v1/get/postcat/' + button.data('queue-id'),
dataType: 'text',
complete: function (data) {
$('#queue_msg_content').text(data.responseText);
}
});
})
$('#queuetable').DataTable({
processing: true,
serverSide: false,
language: lang_datatables,
ajax: {
type: "GET",
url: "/api/v1/get/mailq/all",
dataSrc: function(data){
$.each(data, function (i, item) {
item.chkbox = '<input type="checkbox" data-id="mailqitems" name="multi_select" value="' + item.queue_id + '" />';
rcpts = $.map(item.recipients, function(i) {
return escapeHtml(i);
});
item.recipients = rcpts.join('<hr style="margin:1px!important">');
item.action = '<div class="btn-group">' +
'<a href="#" data-bs-toggle="modal" data-bs-target="#showQueuedMsg" data-queue-id="' + encodeURI(item.queue_id) + '" class="btn btn-xs btn-secondary">' + lang.queue_show_message + '</a>' +
function draw_queue() {
// just recalc width if instance already exists
if ($.fn.DataTable.isDataTable('#queuetable') ) {
$('#queuetable').DataTable().columns.adjust().responsive.recalc();
return;
}
$('#queuetable').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" +
"tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
ajax: {
type: "GET",
url: "/api/v1/get/mailq/all",
dataSrc: function(data){
$.each(data, function (i, item) {
item.chkbox = '<input type="checkbox" data-id="mailqitems" name="multi_select" value="' + item.queue_id + '" />';
rcpts = $.map(item.recipients, function(i) {
return escapeHtml(i);
});
item.recipients = rcpts.join('<hr style="margin:1px!important">');
item.action = '<div class="btn-group">' +
'<a href="#" data-bs-toggle="modal" data-bs-target="#showQueuedMsg" data-queue-id="' + encodeURI(item.queue_id) + '" class="btn btn-xs btn-secondary">' + lang.show_message + '</a>' +
'</div>';
});
return data;
}
},
columns: [
{
// placeholder, so checkbox will not block child row toggle
title: '',
data: null,
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: '',
data: 'chkbox',
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: 'QID',
data: 'queue_id',
defaultContent: ''
},
{
title: 'Queue',
data: 'queue_name',
defaultContent: ''
},
{
title: lang_admin.arrival_time,
data: 'arrival_time',
defaultContent: '',
render: function (data, type){
var date = new Date(data ? data * 1000 : 0);
return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
}
},
{
title: lang_admin.message_size,
data: 'message_size',
defaultContent: '',
render: function (data, type){
return humanFileSize(data);
}
},
{
title: lang_admin.sender,
data: 'sender',
defaultContent: ''
},
{
title: lang_admin.recipients,
data: 'recipients',
defaultContent: ''
},
{
title: lang_admin.action,
data: 'action',
className: 'text-md-end dt-sm-head-hidden dt-body-right',
defaultContent: ''
},
{
// placeholder, so checkbox will not block child row toggle
title: '',
data: null,
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: '',
data: 'chkbox',
searchable: false,
orderable: false,
defaultContent: ''
},
{
title: 'QID',
data: 'queue_id',
defaultContent: ''
},
{
title: 'Queue',
data: 'queue_name',
defaultContent: ''
},
{
title: lang_admin.arrival_time,
data: 'arrival_time',
defaultContent: '',
render: function (data, type){
var date = new Date(data ? data * 1000 : 0);
return date.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"});
}
},
{
title: lang_admin.message_size,
data: 'message_size',
defaultContent: '',
render: function (data, type){
return humanFileSize(data);
}
},
{
title: lang_admin.sender,
data: 'sender',
defaultContent: ''
},
{
title: lang_admin.recipients,
data: 'recipients',
defaultContent: ''
},
{
title: lang_admin.action,
data: 'action',
className: 'dt-sm-head-hidden dt-text-right',
defaultContent: ''
},
]
});
}
draw_queue();
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -288,6 +288,18 @@ if (isset($_GET['query'])) {
case "domain-admin":
process_add_return(domain_admin('add', $attr));
break;
case "sso":
switch ($object) {
case "domain-admin":
$data = domain_admin_sso('issue', $attr);
if($data) {
echo json_encode($data);
exit(0);
}
process_add_return($data);
break;
}
break;
case "admin":
process_add_return(admin('add', $attr));
break;
@@ -561,6 +573,15 @@ if (isset($_GET['query'])) {
echo '{}';
}
break;
default:
$password_complexity_rules = password_complexity('get');
if ($password_complexity_rules !== false) {
process_get_return($password_complexity_rules);
}
else {
echo '{}';
}
break;
}
break;
@@ -1544,14 +1565,15 @@ if (isset($_GET['query'])) {
}
else if ($extra == "ip") {
// get public ips
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, 'http://ipv4.mailcow.email');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 0);
curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($curl, CURLOPT_TIMEOUT, 15);
curl_setopt($curl, CURLOPT_URL, 'http://ipv4.mailcow.email');
$ipv4 = curl_exec($curl);
curl_setopt($curl, CURLOPT_URL, 'http://ipv6.mailcow.email');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_POST, 0);
$ipv6 = curl_exec($curl);
$ips = array(
"ipv4" => $ipv4,
@@ -1913,6 +1935,9 @@ if (isset($_GET['query'])) {
case "ui_texts":
process_edit_return(customize('edit', 'ui_texts', $attr));
break;
case "ip_check":
process_edit_return(customize('edit', 'ip_check', $attr));
break;
case "self":
if ($_SESSION['mailcow_cc_role'] == "domainadmin") {
process_edit_return(domain_admin('edit', $attr));

View File

@@ -650,7 +650,7 @@
},
"login": {
"delayed": "Přihlášení zpožděno o %s sekund.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Přihlásit",
"mobileconfig_info": "Ke stažení profilového souboru se přihlaste jako uživatel schránky.",
"other_logins": "Přihlášení klíčem",

View File

@@ -1,6 +1,6 @@
{
"acl": {
"alias_domains": "Tilføj kældenavn domæner",
"alias_domains": "Tilføj domænealias",
"app_passwds": "Administrer app-adgangskoder",
"bcc_maps": "BCC kort",
"delimiter_action": "Afgrænsning handling",
@@ -22,9 +22,9 @@
"spam_alias": "Midlertidige aliasser",
"spam_policy": "Sortliste / hvidliste",
"spam_score": "Spam-score",
"syncjobs": "Synkroniser job",
"syncjobs": "Synkroniserings job",
"tls_policy": "TLS politik",
"unlimited_quota": "Ubegrænset quote for mailbokse",
"unlimited_quota": "Ubegrænset plads for mailbokse",
"domain_desc": "Skift domæne beskrivelse"
},
"add": {
@@ -33,7 +33,7 @@
"add": "Tilføj",
"add_domain_only": "Tilføj kun domæne",
"add_domain_restart": "Tilføj domæne og genstart SOGo",
"alias_address": "Alias adresse (r)",
"alias_address": "Alias adresse(r)",
"alias_address_info": "<small>Fuld e-mail-adresse eller @ eksempel.com for at fange alle beskeder til et domæne (kommasepareret). <b> kun mailcow-domæner</b>.</small>",
"alias_domain": "Alias-domæne",
"alias_domain_info": "<small>Kun gyldige domænenavne (kommasepareret).</small>",
@@ -586,7 +586,7 @@
},
"login": {
"delayed": "Login blev forsinket med% s sekunder.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Login",
"mobileconfig_info": "Log ind som postkassebruger for at downloade den anmodede Apple-forbindelsesprofil.",
"other_logins": "Nøgle login",

View File

@@ -204,6 +204,9 @@
"include_exclude": "Ein- und Ausschlüsse",
"include_exclude_info": "Ohne Auswahl werden <b>alle Mailboxen</b> adressiert.",
"includes": "Diese Empfänger einschließen",
"ip_check": "IP Check",
"ip_check_disabled": "IP check ist deaktiviert. Unter dem angegebenen Pfad kann es aktiviert werden<br> <strong>System > Konfiguration > Einstellungen > UI-Anpassung</strong>",
"ip_check_opt_in": "Opt-In für die Nutzung der Drittanbieter-Dienste <strong>ipv4.mailcow.email</strong> und <strong>ipv6.mailcow.email</strong> zur Auflösung externer IP-Adressen.",
"is_mx_based": "MX-basiert",
"last_applied": "Zuletzt angewendet",
"license_info": "Eine Lizenz ist nicht erforderlich, hilft jedoch der Entwicklung mailcows.<br><a href=\"https://www.servercow.de/mailcow#sal\" target=\"_blank\" alt=\"SAL Bestellung\">Hier kann die mailcow-GUID registriert werden.</a> Alternativ ist <a href=\"https://www.servercow.de/mailcow#support\" target=\"_blank\" alt=\"SAL Bestellung\">die Bestellung von Support-Paketen möglich</a>.",
@@ -336,7 +339,8 @@
"oauth2_add_client": "Füge OAuth2 Client hinzu",
"api_read_only": "Schreibgeschützter Zugriff",
"api_read_write": "Lese-Schreib-Zugriff",
"oauth2_apps": "OAuth2 Apps"
"oauth2_apps": "OAuth2 Apps",
"queue_unban": "entsperren"
},
"danger": {
"access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten",
@@ -363,6 +367,7 @@
"domain_not_empty": "Domain %s ist nicht leer",
"domain_not_found": "Domain %s nicht gefunden",
"domain_quota_m_in_use": "Domain-Speicherplatzlimit muss größer oder gleich %d MiB sein",
"extended_sender_acl_denied": "Keine Rechte zum Setzen von externen Absenderadressen",
"extra_acl_invalid": "Externe Absenderadresse \"%s\" ist ungültig",
"extra_acl_invalid_domain": "Externe Absenderadresse \"%s\" verwendet eine ungültige Domain",
"fido2_verification_failed": "FIDO2-Verifizierung fehlgeschlagen: %s",
@@ -450,39 +455,45 @@
"totp_verification_failed": "TOTP-Verifizierung fehlgeschlagen",
"transport_dest_exists": "Transport-Maps-Ziel \"%s\" existiert bereits",
"webauthn_verification_failed": "WebAuthn-Verifizierung fehlgeschlagen: %s",
"webauthn_authenticator_failed": "Der ausgewählte Authenticator wurde nicht gefunden",
"webauthn_publickey_failed": "Zu dem ausgewählten Authenticator wurde kein Publickey hinterlegt",
"webauthn_username_failed": "Der ausgewählte Authenticator gehört zu einem anderen Konto",
"unknown": "Ein unbekannter Fehler trat auf",
"unknown_tfa_method": "Unbekannte TFA-Methode",
"unlimited_quota_acl": "Unendliche Quota untersagt durch ACL",
"username_invalid": "Benutzername %s kann nicht verwendet werden",
"validity_missing": "Bitte geben Sie eine Gültigkeitsdauer an",
"value_missing": "Bitte alle Felder ausfüllen",
"yotp_verification_failed": "Yubico OTP-Verifizierung fehlgeschlagen: %s"
"yotp_verification_failed": "Yubico OTP-Verifizierung fehlgeschlagen: %s",
"template_exists": "Vorlage %s existiert bereits",
"template_id_invalid": "Vorlagen-ID %s ungültig",
"template_name_invalid": "Name der Vorlage ungültig"
},
"datatables": {
"collapse_all": "Alle Einklappen",
"decimal": "",
"emptyTable": "Keine Daten in der Tabelle vorhanden",
"expand_all": "Alle Ausklappen",
"info": "_START_ bis _END_ von _TOTAL_ Einträgen",
"infoEmpty": "0 bis 0 von 0 Einträgen",
"infoFiltered": "(gefiltert von _MAX_ Einträgen)",
"infoPostFix": "",
"thousands": ".",
"lengthMenu": "_MENU_ Einträge anzeigen",
"loadingRecords": "Wird geladen...",
"processing": "Bitte warten...",
"search": "Suchen",
"zeroRecords": "Keine Einträge vorhanden.",
"paginate": {
"first": "Erste",
"previous": "Zurück",
"next": "Nächste",
"last": "Letzte"
},
"aria": {
"sortAscending": ": aktivieren, um Spalte aufsteigend zu sortieren",
"sortDescending": ": aktivieren, um Spalte absteigend zu sortieren"
}
"collapse_all": "Alle Einklappen",
"decimal": ",",
"emptyTable": "Keine Daten in der Tabelle vorhanden",
"expand_all": "Alle Ausklappen",
"info": "_START_ bis _END_ von _TOTAL_ Einträgen",
"infoEmpty": "0 bis 0 von 0 Einträgen",
"infoFiltered": "(gefiltert von _MAX_ Einträgen)",
"infoPostFix": "",
"thousands": ".",
"lengthMenu": "_MENU_ Einträge anzeigen",
"loadingRecords": "Wird geladen...",
"processing": "Bitte warten...",
"search": "Suchen",
"zeroRecords": "Keine Einträge vorhanden.",
"paginate": {
"first": "Erste",
"previous": "Zurück",
"next": "Nächste",
"last": "Letzte"
},
"aria": {
"sortAscending": ": aktivieren, um Spalte aufsteigend zu sortieren",
"sortDescending": ": aktivieren, um Spalte absteigend zu sortieren"
}
},
"debug": {
"chart_this_server": "Chart (dieser Server)",
@@ -494,6 +505,7 @@
"current_time": "Systemzeit",
"disk_usage": "Festplattennutzung",
"docs": "Dokumente",
"error_show_ip": "Konnte die öffentlichen IP Adressen nicht auflösen",
"external_logs": "Externe Logs",
"history_all_servers": "History (alle Server)",
"in_memory_logs": "In-memory Logs",
@@ -506,6 +518,7 @@
"online_users": "Benutzer online",
"restart_container": "Neustart",
"service": "Dienst",
"show_ip": "Zeige öffentliche IP",
"size": "Größe",
"solr_dead": "Solr startet, ist deaktiviert oder temporär nicht erreichbar.",
"solr_status": "Solr Status",
@@ -645,7 +658,8 @@
"title": "Objekt bearbeiten",
"unchanged_if_empty": "Unverändert, wenn leer",
"username": "Benutzername",
"validate_save": "Validieren und speichern"
"validate_save": "Validieren und speichern",
"pushover_sound": "Ton"
},
"fido2": {
"confirm": "Bestätigen",
@@ -686,7 +700,8 @@
"quarantine": "Quarantäne",
"restart_netfilter": "Netfilter neustarten",
"restart_sogo": "SOGo neustarten",
"user_settings": "Benutzereinstellungen"
"user_settings": "Benutzereinstellungen",
"mailcow_system": "System"
},
"info": {
"awaiting_tfa_confirmation": "Warte auf TFA-Verifizierung",
@@ -695,7 +710,7 @@
},
"login": {
"delayed": "Login wurde zur Sicherheit um %s Sekunde/n verzögert.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Anmelden",
"mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.",
"other_logins": "Key Login",
@@ -942,7 +957,7 @@
"queue": {
"delete": "Queue löschen",
"flush": "Queue flushen",
"info" : "In der Mailqueue befinden sich alle E-Mails, welche auf eine Zustellung warten. Sollte eine E-Mail eine längere Zeit innerhalb der Mailqueue stecken wird diese automatisch vom System gelöscht.<br>Die Fehlermeldung der jeweiligen Mail gibt aufschluss darüber, warum diese nicht zugestellt werden konnte",
"info": "In der Mailqueue befinden sich alle E-Mails, welche auf eine Zustellung warten. Sollte eine E-Mail eine längere Zeit innerhalb der Mailqueue stecken wird diese automatisch vom System gelöscht.<br>Die Fehlermeldung der jeweiligen Mail gibt aufschluss darüber, warum diese nicht zugestellt werden konnte",
"legend": "Funktionen der Mailqueue Aktionen:",
"ays": "Soll die derzeitige Queue wirklich komplett bereinigt werden?",
"deliver_mail": "Ausliefern",
@@ -1001,6 +1016,7 @@
"forwarding_host_removed": "Weiterleitungs-Host %s wurde entfernt",
"global_filter_written": "Filterdatei wurde erfolgreich geschrieben",
"hash_deleted": "Hash wurde gelöscht",
"ip_check_opt_in_modified": "IP Check wurde erfolgreich gespeichert",
"item_deleted": "Objekt %s wurde entfernt",
"item_released": "Objekt %s freigegeben",
"items_deleted": "Objekt(e) %s wurde(n) erfolgreich entfernt",
@@ -1229,7 +1245,8 @@
"syncjob_EXIT_CONNECTION_FAILURE": "Verbindungsproblem",
"syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung",
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem",
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort"
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort",
"pushover_sound": "Ton"
},
"warning": {
"cannot_delete_self": "Kann derzeit eingeloggten Benutzer nicht entfernen",

View File

@@ -206,6 +206,9 @@
"include_exclude": "Include/Exclude",
"include_exclude_info": "By default - with no selection - <b>all mailboxes</b> are addressed",
"includes": "Include these recipients",
"ip_check": "IP Check",
"ip_check_disabled": "IP check is disabled. You can enable it under<br> <strong>System > Configuration > Options > Customize</strong>",
"ip_check_opt_in": "Opt-In for using third party service <strong>ipv4.mailcow.email</strong> and <strong>ipv6.mailcow.email</strong> to resolve external IP addresses.",
"is_mx_based": "MX based",
"last_applied": "Last applied",
"license_info": "A license is not required but helps further development.<br><a href=\"https://www.servercow.de/mailcow?lang=en#sal\" target=\"_blank\" alt=\"SAL order\">Register your GUID here</a> or <a href=\"https://www.servercow.de/mailcow?lang=en#support\" target=\"_blank\" alt=\"Support order\">buy support for your mailcow installation.</a>",
@@ -264,6 +267,7 @@
"quota_notifications": "Quota notifications",
"quota_notifications_info": "Quota notifications are sent to users once when crossing 80% and once when crossing 95% usage.",
"quota_notifications_vars": "{{percent}} equals the current quota of the user<br>{{username}} is the mailbox name",
"queue_unban": "unban",
"r_active": "Active restrictions",
"r_inactive": "Inactive restrictions",
"r_info": "Greyed out/disabled elements on the list of active restrictions are not known as valid restrictions to mailcow and cannot be moved. Unknown restrictions will be set in order of appearance anyway. <br>You can add new elements in <code>inc/vars.local.inc.php</code> to be able to toggle them.",
@@ -363,6 +367,7 @@
"domain_not_empty": "Cannot remove non-empty domain %s",
"domain_not_found": "Domain %s not found",
"domain_quota_m_in_use": "Domain quota must be greater or equal to %s MiB",
"extended_sender_acl_denied": "missing ACL to set external sender addresses",
"extra_acl_invalid": "External sender address \"%s\" is invalid",
"extra_acl_invalid_domain": "External sender \"%s\" uses an invalid domain",
"fido2_verification_failed": "FIDO2 verification failed: %s",
@@ -453,6 +458,9 @@
"totp_verification_failed": "TOTP verification failed",
"transport_dest_exists": "Transport destination \"%s\" exists",
"webauthn_verification_failed": "WebAuthn verification failed: %s",
"webauthn_authenticator_failed": "The selected authenticator was not found",
"webauthn_publickey_failed": "No public key was stored for the selected authenticator",
"webauthn_username_failed": "The selected authenticator belongs to another account",
"unknown": "An unknown error occurred",
"unknown_tfa_method": "Unknown TFA method",
"unlimited_quota_acl": "Unlimited quota prohibited by ACL",
@@ -462,30 +470,30 @@
"yotp_verification_failed": "Yubico OTP verification failed: %s"
},
"datatables": {
"collapse_all": "Collapse All",
"decimal": "",
"emptyTable": "No data available in table",
"expand_all": "Expand All",
"info": "Showing _START_ to _END_ of _TOTAL_ entries",
"infoEmpty": "Showing 0 to 0 of 0 entries",
"infoFiltered": "(filtered from _MAX_ total entries)",
"infoPostFix": "",
"thousands": ",",
"lengthMenu": "Show _MENU_ entries",
"loadingRecords": "Loading...",
"processing": "Please wait...",
"search": "Search:",
"zeroRecords": "No matching records found",
"paginate": {
"first": "First",
"last": "Last",
"next": "Next",
"previous": "Previous"
},
"aria": {
"sortAscending": ": activate to sort column ascending",
"sortDescending": ": activate to sort column descending"
}
"collapse_all": "Collapse All",
"decimal": ".",
"emptyTable": "No data available in table",
"expand_all": "Expand All",
"info": "Showing _START_ to _END_ of _TOTAL_ entries",
"infoEmpty": "Showing 0 to 0 of 0 entries",
"infoFiltered": "(filtered from _MAX_ total entries)",
"infoPostFix": "",
"thousands": ",",
"lengthMenu": "Show _MENU_ entries",
"loadingRecords": "Loading...",
"processing": "Please wait...",
"search": "Search:",
"zeroRecords": "No matching records found",
"paginate": {
"first": "First",
"last": "Last",
"next": "Next",
"previous": "Previous"
},
"aria": {
"sortAscending": ": activate to sort column ascending",
"sortDescending": ": activate to sort column descending"
}
},
"debug": {
"chart_this_server": "Chart (this server)",
@@ -497,6 +505,7 @@
"current_time": "System Time",
"disk_usage": "Disk usage",
"docs": "Docs",
"error_show_ip": "Could not resolve the public IP addresses",
"external_logs": "External logs",
"history_all_servers": "History (all servers)",
"in_memory_logs": "In-memory logs",
@@ -509,6 +518,7 @@
"online_users": "Users online",
"restart_container": "Restart",
"service": "Service",
"show_ip": "Show public IP",
"size": "Size",
"solr_dead": "Solr is starting, disabled or died.",
"solr_status": "Solr status",
@@ -700,7 +710,7 @@
},
"login": {
"delayed": "Login was delayed by %s seconds.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Login",
"mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.",
"other_logins": "Key login",
@@ -947,7 +957,7 @@
"queue": {
"delete": "Delete all",
"flush": "Flush queue",
"info" : "The mail queue contains all e-mails that are waiting for delivery. If an email is stuck in the mail queue for a long time, it is automatically deleted by the system.<br>The error message of the respective mail gives information about why the mail could not be delivered.",
"info": "The mail queue contains all e-mails that are waiting for delivery. If an email is stuck in the mail queue for a long time, it is automatically deleted by the system.<br>The error message of the respective mail gives information about why the mail could not be delivered.",
"legend": "Mail queue actions functions:",
"ays": "Please confirm you want to delete all items from the current queue.",
"deliver_mail": "Deliver",
@@ -961,11 +971,11 @@
"unhold_mail_legend": "Releases selected mails for delivery. (Requires prior hold)"
},
"ratelimit": {
"disabled": "Disabled",
"second": "msgs / second",
"minute": "msgs / minute",
"hour": "msgs / hour",
"day": "msgs / day"
"disabled": "Disabled",
"second": "msgs / second",
"minute": "msgs / minute",
"hour": "msgs / hour",
"day": "msgs / day"
},
"start": {
"help": "Show/Hide help panel",
@@ -1013,6 +1023,7 @@
"forwarding_host_removed": "Forwarding host %s has been removed",
"global_filter_written": "Filter was successfully written to file",
"hash_deleted": "Hash deleted",
"ip_check_opt_in_modified": "IP check was saved successfully",
"item_deleted": "Item %s successfully deleted",
"item_released": "Item %s released",
"items_deleted": "Item %s successfully deleted",

View File

@@ -105,7 +105,8 @@
"username": "Nom d'utilisateur",
"validate": "Valider",
"validation_success": "Validation réussie",
"bcc_dest_format": "La destination Cci doit être une seule adresse e-mail valide.<br>Si vous avez besoin d'envoyer une copie à plusieurs adresses, créez un alias et utilisez-le ici."
"bcc_dest_format": "La destination Cci doit être une seule adresse e-mail valide.<br>Si vous avez besoin d'envoyer une copie à plusieurs adresses, créez un alias et utilisez-le ici.",
"tags": "Etiquettes"
},
"admin": {
"access": "Accès",
@@ -318,7 +319,9 @@
"oauth2_add_client": "Ajouter un client OAuth2",
"password_policy": "Politique de mots de passe",
"admins": "Administrateurs",
"api_read_only": "Accès lecture-seule"
"api_read_only": "Accès lecture-seule",
"password_policy_lowerupper": "Doit contenir des caractères minuscules et majuscules",
"password_policy_numbers": "Doit contenir au moins un chiffre"
},
"danger": {
"access_denied": "Accès refusé ou données de formulaire non valides",
@@ -609,7 +612,7 @@
},
"login": {
"delayed": "La connexion a été retardée de %s secondes.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Connexion",
"mobileconfig_info": "Veuillez vous connecter en tant quutilisateur de la boîte pour télécharger le profil de connexion Apple demandé.",
"other_logins": "Clé d'authentification",

View File

@@ -43,7 +43,7 @@
"app_name": "Nome app",
"app_password": "Aggiungi la password dell'app",
"automap": "Prova a mappare automaticamente le cartelle (\"Sent items\", \"Sent\" => \"Posta inviata\" ecc.)",
"backup_mx_options": "Relay options",
"backup_mx_options": "Opzioni di inoltro",
"comment_info": "Un commento privato non è visibile all'utente, mentre un commento pubblico viene mostrato come suggerimento quando si passa con il mouse nella panoramica di un utente",
"custom_params": "Parametri personalizzati",
"custom_params_hint": "Corretto: --param=xy, errato: --param xy",
@@ -303,7 +303,7 @@
"spamfilter": "Filtri spam",
"subject": "Oggetto",
"success": "Successo",
"sys_mails": "System mails",
"sys_mails": "Mail di sistema",
"text": "Testo",
"time": "Orario",
"title": "Titolo",
@@ -335,7 +335,8 @@
"api_read_write": "Accesso in lettura-scrittura",
"oauth2_apps": "App OAuth2",
"oauth2_add_client": "Aggiungere il client OAuth2",
"rsettings_preset_4": "Disattivare Rspamd per un dominio"
"rsettings_preset_4": "Disattivare Rspamd per un dominio",
"options": "Opzioni"
},
"danger": {
"access_denied": "Accesso negato o form di login non corretto",
@@ -364,7 +365,7 @@
"extra_acl_invalid": "External sender address \"%s\" is invalid",
"extra_acl_invalid_domain": "External sender \"%s\" uses an invalid domain",
"fido2_verification_failed": "FIDO2 verification failed: %s",
"file_open_error": "File cannot be opened for writing",
"file_open_error": "Il file non può essere aperto per la scrittura",
"filter_type": "Wrong filter type",
"from_invalid": "Il mittente non può essere vuoto",
"global_filter_write_error": "Could not write filter file: %s",
@@ -397,7 +398,7 @@
"mailbox_quota_exceeds_domain_quota": "Lo spazio massimo supera la spazio del dominio",
"mailbox_quota_left_exceeded": "Non c'è abbastanza spazio libero (space left: %d MiB)",
"mailboxes_in_use": "Lo spazio massimo della casella deve essere maggiore o uguale a %d",
"malformed_username": "Malformed username",
"malformed_username": "Nome utente non valido",
"map_content_empty": "Map content cannot be empty",
"max_alias_exceeded": "Numero massimo di alias superato",
"max_mailbox_exceeded": "Numero massimo di caselle superato (%d of %d)",
@@ -429,18 +430,18 @@
"resource_invalid": "Il nome della risorsa non è valido",
"rl_timeframe": "Rate limit time frame is incorrect",
"rspamd_ui_pw_length": "Rspamd UI password should be at least 6 chars long",
"script_empty": "Script cannot be empty",
"script_empty": "Lo script non può essere vuoto",
"sender_acl_invalid": "Il valore di Sender ACL non è valido",
"set_acl_failed": "Failed to set ACL",
"settings_map_invalid": "Settings map ID %s invalid",
"sieve_error": "Sieve parser error: %s",
"spam_learn_error": "Spam learn error: %s",
"subject_empty": "Subject must not be empty",
"subject_empty": "L'oggetto non deve essere vuoto",
"target_domain_invalid": "Goto domain non è valido",
"targetd_not_found": "Il target del dominio non è stato trovato",
"targetd_relay_domain": "Target domain %s is a relay domain",
"temp_error": "Temporary error",
"text_empty": "Text must not be empty",
"temp_error": "Errore temporaneo",
"text_empty": "Il testo non deve essere vuoto",
"tfa_token_invalid": "TFA token invalid",
"tls_policy_map_dest_invalid": "Policy destination is invalid",
"tls_policy_map_entry_exists": "A TLS policy map entry \"%s\" exists",
@@ -448,40 +449,54 @@
"totp_verification_failed": "TOTP verification failed",
"transport_dest_exists": "Transport destination \"%s\" exists",
"webauthn_verification_failed": "WebAuthn verification failed: %s",
"unknown": "An unknown error occurred",
"unknown": "Si è verificato un errore sconosciuto",
"unknown_tfa_method": "Unknown TFA method",
"unlimited_quota_acl": "Unlimited quota prohibited by ACL",
"username_invalid": "Username %s non può essere utilizzato",
"username_invalid": "Il nome utente %s non può essere utilizzato",
"validity_missing": "Assegnare un periodo di validità",
"value_missing": "Si prega di fornire tutti i valori",
"yotp_verification_failed": "Verifica OTP Yubico fallita: %s"
"yotp_verification_failed": "Verifica OTP Yubico fallita: %s",
"demo_mode_enabled": "La modalità demo è abilitata",
"template_name_invalid": "Nome template non valido",
"template_exists": "Il template %s esiste già",
"template_id_invalid": "Il template con ID %s non è valido"
},
"debug": {
"chart_this_server": "Grafico (questo server)",
"containers_info": "Container information",
"containers_info": "Informazioni sul container",
"disk_usage": "Uso del disco",
"docs": "Docs",
"external_logs": "External logs",
"history_all_servers": "History (all servers)",
"external_logs": "Log esterni",
"history_all_servers": "Cronologia (tutti i server)",
"in_memory_logs": "In-memory logs",
"jvm_memory_solr": "JVM memory usage",
"last_modified": "Ultima modifica",
"log_info": "<p>mailcow <b>in-memory logs</b> are collected in Redis lists and trimmed to LOG_LINES (%d) every minute to reduce hammering.\r\n <br>In-memory logs are not meant to be persistent. All applications that log in-memory, also log to the Docker daemon and therefore to the default logging driver.\r\n <br>The in-memory log type should be used for debugging minor issues with containers.</p>\r\n <p><b>External logs</b> are collected via API of the given application.</p>\r\n <p><b>Static logs</b> are mostly activity logs, that are not logged to the Dockerd but still need to be persistent (except for API logs).</p>",
"login_time": "Time",
"login_time": "Orario",
"logs": "Logs",
"online_users": "Users online",
"online_users": "Utenti online",
"restart_container": "Riavvio",
"service": "Servizio",
"size": "Size",
"solr_dead": "Solr is starting, disabled or died.",
"size": "Dimensione",
"solr_dead": "Solr sta partendo, è disabilitato o morto.",
"solr_status": "Stato Solr",
"started_at": "Started at",
"started_on": "Started on",
"static_logs": "Static logs",
"started_at": "Iniziato alle",
"started_on": "Iniziato",
"static_logs": "Log statici",
"success": "Successo",
"system_containers": "System & Containers",
"system_containers": "Sistema & Containers",
"uptime": "Tempo di attività",
"username": "Username"
"username": "Nome utente",
"container_disabled": "Container arrestato o disattivato",
"update_available": "È disponibile un aggiornamento",
"container_running": "In esecuzione",
"container_stopped": "Arrestato",
"cores": "Cores",
"current_time": "Orario di sistema",
"memory": "Memoria",
"timezone": "Fuso orario",
"no_update_available": "Il sistema è aggiornato all'ultima versione",
"update_failed": "Impossibile verificare la presenza di un aggiornamento"
},
"diagnostics": {
"cname_from_a": "Valore letto dal record A/AAAA. Questo è supportato finché il record punta alla risorsa corretta.",
@@ -514,7 +529,7 @@
"delete1": "Elimina dalla sorgente al termine",
"delete2": "Delete messages on destination that are not on source",
"delete2duplicates": "Elimina duplicati nella destinazione",
"delete_ays": "Please confirm the deletion process.",
"delete_ays": "Si prega di confermare il processo di eliminazione.",
"description": "Descrizione",
"disable_login": "Disabilita l'accesso (la posta in arrivo viene correttamente recapitata)",
"domain": "Modifica dominio",
@@ -527,12 +542,12 @@
"exclude": "Escludi oggetti (regex)",
"extended_sender_acl": "External sender addresses",
"extended_sender_acl_info": "A DKIM domain key should be imported, if available.<br>\r\n Remember to add this server to the corresponding SPF TXT record.<br>\r\n Whenever a domain or alias domain is added to this server, that overlaps with an external address, the external address is removed.<br>\r\n Use @domain.tld to allow to send as *@domain.tld.",
"force_pw_update": "Force password update at next login",
"force_pw_update": "Forza l'aggiornamento della password al prossimo accesso",
"force_pw_update_info": "Questo utente potrà accedere solo a %s.",
"full_name": "Nome completo",
"gal": "Global Address List",
"gal_info": "The GAL contains all objects of a domain and cannot be edited by any user. Free/busy information in SOGo is missing, if disabled! <b>Restart SOGo to apply changes.</b>",
"generate": "generate",
"generate": "crea",
"grant_types": "Grant types",
"hostname": "Hostname",
"inactive": "Inattivo",
@@ -549,7 +564,7 @@
"mbox_rl_info": "This rate limit is applied on the SASL login name, it matches any \"from\" address used by the logged-in user. A mailbox rate limit overrides a domain-wide rate limit.",
"mins_interval": "Intervallo (min)",
"multiple_bookings": "Prenotazioni multiple",
"nexthop": "Next hop",
"nexthop": "Prossimo hop",
"password": "Password",
"password_repeat": "Conferma password (riscrivi)",
"previous": "Pagina precedente",
@@ -561,9 +576,9 @@
"pushover_sender_array": "Only consider the following sender email addresses <small>(comma-separated)</small>",
"pushover_sender_regex": "Consider the following sender regex",
"pushover_text": "Notification text",
"pushover_title": "Notification title",
"pushover_title": "Titolo della notifica",
"pushover_vars": "When no sender filter is defined, all mails will be considered.<br>Regex filters as well as exact sender checks can be defined individually and will be considered sequentially. They do not depend on each other.<br>Useable variables for text and title (please take note of data protection policies)",
"pushover_verify": "Verify credentials",
"pushover_verify": "Verifica credenziali",
"quota_mb": "Spazio (MiB)",
"quota_warning_bcc": "Quota warning BCC",
"quota_warning_bcc_info": "Warnings will be sent as separate copies to the following recipients. The subject will be suffixed by the corresponding username in brackets, for example: <code>Quota warning (user@example.com)</code>.",
@@ -582,42 +597,44 @@
"sender_acl": "Consenti di inviare come",
"sender_acl_disabled": "<span class=\"badge fs-6 bg-danger\">Sender check is disabled</span>",
"sender_acl_info": "If mailbox user A is allowed to send as mailbox user B, the sender address is not automatically displayed as selectable \"from\" field in SOGo.<br>\r\n Mailbox user B needs to create a delegation in SOGo to allow mailbox user A to select their address as sender. To delegate a mailbox in SOGo, use the menu (three dots) to the right of your mailbox name in the upper left while in mail view. This behaviour does not apply to alias addresses.",
"sieve_desc": "Short description",
"sieve_desc": "Breve descrizione",
"sieve_type": "Filter type",
"skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)",
"sogo_visible": "Alias is visible in SOGo",
"sogo_visible": "L'alias è visibile in SOGo",
"sogo_visible_info": "This option only affects objects, that can be displayed in SOGo (shared or non-shared alias addresses pointing to at least one local mailbox). If hidden, an alias will not appear as selectable sender in SOGo.",
"spam_alias": "Create or change time limited alias addresses",
"spam_filter": "Spam filter",
"spam_policy": "Add or remove items to white-/blacklist",
"spam_score": "Set a custom spam score",
"spam_policy": "Aggiungi o rimuovi elementi dalla whitelist/blacklist",
"spam_score": "Imposta un punteggio spam personalizzato",
"subfolder2": "Sincronizza in una sottocartella<br /><small>(vuoto = non sincronizzare in sottocartella)</small>",
"syncjob": "Modifica sincronizzazione",
"target_address": "Vai all'indirizzo/i <small>(separato da virgola)</small>",
"target_domain": "Target dominio",
"timeout1": "Timeout for connection to remote host",
"timeout2": "Timeout for connection to local host",
"timeout1": "Timeout per la connessione all'host remoto",
"timeout2": "Timeout per la connessione all'host remoto",
"title": "Modifica oggetto",
"unchanged_if_empty": "Se immutato lasciare vuoto",
"username": "Username",
"username": "Nome utente",
"validate_save": "Convalida e salva",
"pushover": "Pushover",
"sogo_access_info": "Il single-sign-on dall'interno dell'interfaccia di posta rimane funzionante. Questa impostazione non influisce sull'accesso a tutti gli altri servizi né cancella o modifica il profilo SOGo esistente dell'utente.",
"none_inherit": "Nessuno / Eredita",
"sogo_access": "Concedere l'accesso diretto a SOGo",
"acl": "ACL (autorizzazione)",
"app_passwd_protocols": "Protocolli consentiti per la password dell'app"
"app_passwd_protocols": "Protocolli consentiti per la password dell'app",
"last_modified": "Ultima modifica",
"pushover_sound": "Suono"
},
"fido2": {
"confirm": "Confirm",
"confirm": "Conferma",
"fido2_auth": "Login with FIDO2",
"fido2_success": "Device successfully registered",
"fido2_validation_failed": "Validation failed",
"fn": "Friendly name",
"known_ids": "Known IDs",
"none": "Disabled",
"register_status": "Registration status",
"rename": "Rename",
"fido2_success": "Dispositivo registrato con successo",
"fido2_validation_failed": "Validazione fallita",
"fn": "Nome descrittivo",
"known_ids": "ID conosciuti",
"none": "Disabilitato",
"register_status": "Stato di registrazione",
"rename": "Rinominare",
"set_fido2": "Register FIDO2 device",
"set_fn": "Set friendly name",
"start_fido2_validation": "Start FIDO2 validation",
@@ -641,13 +658,14 @@
"header": {
"administration": "Amministrazione",
"apps": "App",
"debug": "Informazioni di sistema",
"debug": "Informazioni",
"email": "E-Mail",
"mailcow_config": "Configurazione",
"quarantine": "Quarantena",
"restart_netfilter": "Riavvia netfilter",
"restart_sogo": "Riavvia SOGo",
"user_settings": "Impostazioni utente"
"user_settings": "Impostazioni utente",
"mailcow_system": "Sistema"
},
"info": {
"awaiting_tfa_confirmation": "In attesa di conferma TFA",
@@ -656,12 +674,12 @@
},
"login": {
"delayed": "L'accesso è stato ritardato di %s secondi.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Login",
"mobileconfig_info": "Please login as mailbox user to download the requested Apple connection profile.",
"other_logins": "Key login",
"password": "Password",
"username": "Username"
"username": "Nome utente"
},
"mailbox": {
"action": "Azione",
@@ -733,7 +751,7 @@
"inactive": "Inattivo",
"insert_preset": "Insert example preset \"%s\"",
"kind": "Tipo",
"last_mail_login": "Last mail login",
"last_mail_login": "Ultimo accesso alla posta",
"last_modified": "Ultima modifica",
"last_pw_change": "Ultima modifica della password",
"last_run": "Ultima esecuzione",
@@ -828,7 +846,15 @@
"sender": "Mittente",
"all_domains": "Tutti i domini",
"recipient": "Destinatario",
"syncjob_EX_OK": "Successo"
"syncjob_EX_OK": "Successo",
"add_template": "Aggiungi template",
"force_pw_update": "Forza il cambio della password al prossimo accesso",
"relay_unknown": "Inoltra a caselle di posta sconosciute",
"mailbox_templates": "Template della mailbox",
"domain_templates": "Template di dominio",
"gal": "Elenco indirizzi globale",
"templates": "Template",
"template": "Template"
},
"oauth2": {
"access_denied": "Effettua il login alla casella di posta per garantire l'accesso tramite OAuth2.",
@@ -847,7 +873,7 @@
"confirm_delete": "Conferma l'eliminazione di questo elemento.",
"danger": "Pericolo",
"deliver_inbox": "Consegna nella posta in arrivo",
"disabled_by_config": "The current system configuration disables the quarantine functionality. Please set \"retentions per mailbox\" and a \"maximum size\" for quarantine elements.",
"disabled_by_config": "L'attuale configurazione del sistema disabilita la funzionalità di quarantena. Imposta \"conservazioni per casella di posta\" e \"dimensione massima\" per gli elementi di quarantena.",
"download_eml": "Download (.eml)",
"empty": "Nessun risultato",
"high_danger": "Alto",
@@ -893,7 +919,18 @@
"type": "Tipologia"
},
"queue": {
"queue_manager": "Gestore code"
"queue_manager": "Gestore code",
"delete": "Cancella tutto",
"ays": "Conferma che desideri eliminare tutti gli elementi dalla coda corrente.",
"info": "La coda di posta contiene tutte le e-mail in attesa di consegna. Se un'e-mail rimane a lungo nella coda di posta, viene automaticamente cancellata dal sistema.<br>Il messaggio di errore della rispettiva e-mail fornisce informazioni sul motivo per cui non è stato possibile consegnarla.",
"deliver_mail_legend": "Tenta di riconsegnare i messaggi selezionati.",
"hold_mail": "Blocca",
"flush": "Svuota la coda",
"deliver_mail": "Consegna",
"show_message": "Mostra messaggio",
"unhold_mail": "Sblocca",
"hold_mail_legend": "Blocca le mail selezionate. (Previene ulteriori tentativi di consegna)",
"legend": "Funzioni delle azioni della coda di posta:"
},
"start": {
"help": "Mostra/Nascondi pannello di aiuto",
@@ -978,7 +1015,10 @@
"verified_totp_login": "Verified TOTP login",
"verified_webauthn_login": "Verified WebAuthn login",
"verified_yotp_login": "Verified Yubico OTP login",
"domain_add_dkim_available": "Esisteva già una chiave DKIM"
"domain_add_dkim_available": "Esisteva già una chiave DKIM",
"template_added": "Aggiunto template %s",
"template_modified": "Le modifiche al template %s sono state salvate",
"template_removed": "Il template con ID %s è stato cancellato"
},
"tfa": {
"api_register": "%s usa le API Yubico Cloud. Richiedi una chiave API <a href=\"https://upgrade.yubico.com/getapikey/\" target=\"_blank\">qui</a>",
@@ -1142,7 +1182,7 @@
"tls_enforce_in": "Imponi TLS in ingresso",
"tls_enforce_out": "Imponi TLS in uscita",
"tls_policy": "Politica di crittografia",
"tls_policy_warning": "<strong>Attenzione:</strong> If you decide to enforce encrypted mail transfer, you may lose emails.<br />Messages to not satisfy the policy will be bounced with a hard fail by the mail system.<br />This option applies to your primary email address (login name), all addresses derived from alias domains as well as alias addresses <b>with only this single mailbox</b> as target.",
"tls_policy_warning": "<strong>Attenzione:</strong> Se decidi di applicare il trasferimento di posta crittografato, potresti perdere le email.<br />I messaggi che non soddisfano la politica verranno respinti con un hard fail dal sistema di posta.<br />This option applies to your primary email address (login name), all addresses derived from alias domains as well as alias addresses <b>with only this single mailbox</b> as target.",
"user_settings": "Impostazioni utente",
"username": "Nome utente",
"verify": "Verifica",
@@ -1166,7 +1206,8 @@
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Impossibile connettersi al server remoto",
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nome utente o password errati",
"with_app_password": "con password dell'app",
"direct_protocol_access": "Questo utente della mailbox ha <b>accesso diretto ed esterno</b> ai seguenti protocolli e applicazioni. Questa impostazione è controllata dal tuo amministratore. Le password delle applicazioni possono essere create per garantire l'accesso ai singoli protocolli e applicazioni.<br>Il pulsante \"Accedi alla webmail\" fornisce un singolo accesso a SOGo ed è sempre disponibile."
"direct_protocol_access": "Questo utente della mailbox ha <b>accesso diretto ed esterno</b> ai seguenti protocolli e applicazioni. Questa impostazione è controllata dal tuo amministratore. Le password delle applicazioni possono essere create per garantire l'accesso ai singoli protocolli e applicazioni.<br>Il pulsante \"Accedi alla webmail\" fornisce un singolo accesso a SOGo ed è sempre disponibile.",
"pushover_sound": "Suono"
},
"warning": {
"cannot_delete_self": "Cannot delete logged in user",
@@ -1187,5 +1228,29 @@
"second": "messaggi / secondo",
"hour": "messaggi / ora",
"day": "messaggi / giorno"
},
"datatables": {
"infoFiltered": "(filtrato da _MAX_ voci totali)",
"collapse_all": "Comprimi tutto",
"emptyTable": "Nessun dato disponibile nella tabella",
"expand_all": "Espandi tutto",
"info": "Visualizzazione da _START_ a _END_ di _TOTAL_ voci",
"infoEmpty": "Visualizzazione da 0 a 0 di 0 voci",
"thousands": ".",
"loadingRecords": "Caricamento...",
"processing": "Attendere prego...",
"search": "Ricerca:",
"zeroRecords": "Nessuna corrispondenza trovata",
"paginate": {
"first": "Prima",
"last": "Ultima",
"next": "Prossima",
"previous": "Precedente"
},
"lengthMenu": "Mostra _MENU_ voci",
"aria": {
"sortAscending": ": attivare l'ordinamento crescente delle colonne",
"sortDescending": ": attivare l'ordinamento decrescente delle colonne"
}
}
}

View File

@@ -3,7 +3,8 @@
"bcc_maps": "BCC kartes",
"filters": "Filtri",
"recipient_maps": "Saņēmēja kartes",
"syncjobs": "Sinhronizācijas uzdevumi"
"syncjobs": "Sinhronizācijas uzdevumi",
"spam_score": "Mēstules novērtējums"
},
"add": {
"activate_filter_warn": "Visi pārējie filtri tiks deaktivizēti, kad aktīvs ir atzīmēts.",
@@ -104,10 +105,10 @@
"host": "Hosts",
"import": "Importēt",
"import_private_key": "Importēt privātu atslēgu",
"in_use_by": "Tiek lietots ar",
"in_use_by": "Izmanto",
"inactive": "Neaktīvs",
"link": "Saite",
"loading": "Lūdzu uzgaidiet...",
"loading": "Lūgums uzgaidīt...",
"logo_info": "Jūsu attēls augšējā navigācijas joslā tiks palielināts līdz 40 pikseļiem un maks. sākumlapas platums par 250 pikseļi. Ir ļoti ieteicama pielāgojama grafikaYour image will be scaled to a height of 40px for the top navigation bar and a max. width of 250px for the start page. Ir ļoti ieteicama pielāgojamā grafika",
"main_name": "\"mailcow UI\" nosaukums",
"merged_vars_hint": "Pelēkās rindas tika apvienotas <code>vars.(local.)inc.php</code> un nevar tikt modificētas.",
@@ -144,7 +145,10 @@
"ui_texts": "UI etiķetes un teksti",
"unchanged_if_empty": "Ja nav veiktas izmaiņas, atstājiet tukšu",
"upload": "Augšupielādēt",
"username": "Lietotājvārds"
"username": "Lietotājvārds",
"generate": "izveidot",
"message": "Ziņojums",
"last_applied": "Pēdējoreiz pielietots"
},
"danger": {
"access_denied": "Piekļuve liegta, vai nepareizi dati",
@@ -170,7 +174,7 @@
"is_alias": "%s jau ir zināms alias",
"is_alias_or_mailbox": "%s jau ir zināms alias, pastkastes vai alias addrese izvērsta no alias domēna.",
"is_spam_alias": "%s ir jau zināms spam alias",
"last_key": "Pēdējā atslēga nevar būt dzēsta",
"last_key": "Pēdējo atslēgu nevar izdzēst, tā vietā jāatspējo divpakāpju pārbaude.",
"login_failed": "Ielogošanās neveiksmīga",
"mailbox_invalid": "Pastkastes vārds ir nederīgs",
"mailbox_quota_exceeded": "Kvota pārsniedz domēna limitu (max. %d MiB)",
@@ -262,7 +266,8 @@
"title": "Labot priekšmetu",
"unchanged_if_empty": "Ja neizmainīts atstājiet tukšu",
"username": "Lietotājvārds",
"validate_save": "Apstiprināt un saglabāt"
"validate_save": "Apstiprināt un saglabāt",
"last_modified": "Pēdējoreiz mainīts"
},
"footer": {
"cancel": "Atcelt",
@@ -314,21 +319,21 @@
"bcc_destinations": "BCC galamērķi/s",
"bcc_info": "BCC kartes tiek izmantotas, lai klusu pārsūtītu visu ziņojumu kopijas uz citu adresi. Saņēmēja kartes tipa ieraksts tiek izmantots, kad vietējais galamērķis darbojas kā pasta adresāts. Sūtītāja kartes atbilst vienam un tam pašam principam. <br/>\r\n   Vietējais galamērķis netiks informēts par piegādes neveiksmi. ",
"bcc_local_dest": "Vietējais galamērķis",
"bcc_map_type": "BCC tips",
"bcc_map_type": "BCC veids",
"bcc_maps": "BCC kartes",
"bcc_rcpt_map": "saņēmēja karte",
"bcc_sender_map": "Sūtītāja karte",
"bcc_to_rcpt": "Pārslēdzieties uz adresāta kartes tipu",
"bcc_to_sender": "Pārslēgties uz sūtītāja kartes tipu",
"bcc_type": "BCC tips",
"deactivate": "Deaktivizēt",
"deactivate": "Deaktivēt",
"description": "Apraksts",
"dkim_key_length": "DKIM atslēgas garums (bits)",
"domain": "Domēns",
"domain_admins": "Domēna administratori",
"domain_aliases": "Domēna aliases",
"domain_quota": "Kvota",
"domain_quota_total": "Kopējā domēna kvota",
"domain_quota_total": "Kopējais domēna ierobežojums",
"domains": "Domēns",
"edit": "Labot",
"empty": "Nav rezultātu",
@@ -341,7 +346,7 @@
"inactive": "Neaktīvs",
"kind": "Veids",
"last_run": "Pēdējā norise",
"last_run_reset": "Nākamais grafiks",
"last_run_reset": "Ievietot sarakstā kā nākamo",
"mailbox_quota": "Maks. pastkastes izmērs",
"mailboxes": "Pastkaste",
"max_aliases": "Maks. iespejamās aliases",
@@ -374,7 +379,13 @@
"tls_enforce_out": "Piespiest TLS izejošajiem",
"toggle_all": "Pārslēgt visu",
"username": "Lietotājvārds",
"waiting": "Gaidīšana"
"waiting": "Gaidīšana",
"last_modified": "Pēdējoreiz mainīts",
"booking_0_short": "Vienmēŗ bezmaksas",
"daily": "Ik dienu",
"hourly": "Ik stundu",
"last_mail_login": "Pēdējā pieteikšanās pastkastē",
"mailbox": "Pastkaste"
},
"quarantine": {
"action": "Darbības",
@@ -547,5 +558,14 @@
"waiting": "Waiting",
"week": "Nedēļa",
"weeks": "Nedēļas"
},
"datatables": {
"paginate": {
"first": "Pirmā",
"last": "Pēdējā"
}
},
"debug": {
"last_modified": "Pēdējoreiz mainīts"
}
}

View File

@@ -598,7 +598,7 @@
},
"login": {
"delayed": "Aanmelding vertraagd met %s seconden.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Aanmelden",
"mobileconfig_info": "Log in als mailboxgebruiker om het Apple-verbindingsprofiel te downloaden.",
"other_logins": "Meld aan met key",

View File

@@ -656,7 +656,7 @@
},
"login": {
"delayed": "Conectarea a fost întârziată cu %s secunde.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Autentificare",
"mobileconfig_info": "Autentificați-vă cu adresa de email pentru a descărca profilul de conexiune Apple.",
"other_logins": "Autentificare cu cheie",

View File

@@ -37,7 +37,7 @@
"add_domain_only": "Только добавить домен",
"add_domain_restart": "Добавить домен и перезапустить SOGo",
"alias_address": "Псевдоним/ы",
"alias_address_info": "<small>Укажите почтовые адреса разделенные запятыми или, если хотите пересылать все сообщения для домена владельцам псевдонима то: <code>@example.com</code>. <b>Только домены mailcow разрешены</b>.</small>",
"alias_address_info": "<small>Адрес(а) электронной почты (через запятую) или @example.com (для перехвата всех писем для домена). <b>только домены mailcow</b>.</small>",
"alias_domain": "Псевдоним домена",
"alias_domain_info": "<small>Действительные имена доменов, раздёленные запятыми.</small>",
"app_name": "Название приложения",
@@ -335,7 +335,8 @@
"username": "Имя пользователя",
"validate_license_now": "Получить лицензию на основе GUID с сервера лицензий",
"verify": "Проверить",
"yes": "&#10003;"
"yes": "&#10003;",
"queue_unban": "разблокировать"
},
"danger": {
"access_denied": "Доступ запрещён, или указаны неверные данные",
@@ -654,7 +655,7 @@
},
"login": {
"delayed": "Вход был отложен на %s секунд.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Войти",
"mobileconfig_info": "Пожалуйста, войдите в систему как пользователь почтового аккаунта для загрузки профиля подключения Apple.",
"other_logins": "Вход с помощью ключа",

View File

@@ -106,7 +106,8 @@
"username": "Používateľské meno",
"validate": "Overiť",
"validation_success": "Úspešne overené",
"app_passwd_protocols": "Povolené protokoly k heslu aplikácie"
"app_passwd_protocols": "Povolené protokoly k heslu aplikácie",
"tags": "Štítky"
},
"admin": {
"access": "Prístup",
@@ -656,7 +657,7 @@
},
"login": {
"delayed": "Prihlásenie bolo oneskorené o %s sekúnd.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Prihlásenie",
"mobileconfig_info": "Prosím, prihláste sa ako mailový používateľ pre stiahnutie požadovaného Apple profilu.",
"other_logins": "Prihlásenie kľúčom",

View File

@@ -618,7 +618,7 @@
},
"login": {
"delayed": "Av säkerhetsskäl har inloggning inaktiverats i %s sekunder.",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Logga in",
"mobileconfig_info": "Logga in som en användare av brevlåda för att ladda ner den begärda Apple-anslutningsprofilen.",
"other_logins": "Loggain med nyckel",

View File

@@ -656,7 +656,7 @@
"awaiting_tfa_confirmation": "В очікуванні підтвердження TFA"
},
"login": {
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "Увійти",
"other_logins": "Вхід за допомогою ключа",
"password": "Пароль",

View File

@@ -31,7 +31,7 @@
"unlimited_quota": "无限邮箱容量配额"
},
"add": {
"activate_filter_warn": "当 \"启用\" 选项被勾选后,所有其他过滤器都会被禁用",
"activate_filter_warn": "当“启用”选项被勾选后,其它所有的过滤器都会被禁用",
"active": "启用",
"add": "添加",
"add_domain_only": "只添加域名",
@@ -208,7 +208,7 @@
"includes": "包括这些收件人",
"is_mx_based": "基于 MX 记录",
"last_applied": "最后应用的条目",
"license_info": "你不需要获取证书便可以使用此项目,但是获取证书可以帮助此项目进一步发展。<br>在这里<a href=\"https://www.servercow.de/mailcow?lang=en#sal\" target=\"_blank\" alt=\"SAL order\">注册</a>你的 GUID或者为你的 Mailcow 安装<a href=\"https://www.servercow.de/mailcow?lang=en#support\" target=\"_blank\" alt=\"Support order\">购买</a>支持服务。",
"license_info": "使用并不需要许可证,但获得许可证能够帮助此项目进一步发展。<br><a href=\"https://www.servercow.de/mailcow?lang=en#sal\" target=\"_blank\" alt=\"订购 SAL\">在这里注册你的 GUID </a>或者<a href=\"https://www.servercow.de/mailcow?lang=en#support\" target=\"_blank\" alt=\"订购支持服务\">为你的 Mailcow 安装购买支持服务。</a>",
"link": "链接",
"loading": "请等待...",
"login_time": "登录时间",
@@ -335,7 +335,8 @@
"username": "用户名",
"validate_license_now": "通过证书服务器验证 GUID",
"verify": "验证",
"yes": "&#10003;"
"yes": "&#10003;",
"options": "选项"
},
"danger": {
"access_denied": "访问被拒绝或者表单数据无效",
@@ -402,7 +403,7 @@
"max_alias_exceeded": "超出最大别名数",
"max_mailbox_exceeded": "超出最大邮箱数 (%d / %d)",
"max_quota_in_use": "邮箱数必须大于等于 %d MiB",
"maxquota_empty": "每个邮箱的最大配额必须不为0",
"maxquota_empty": "每个邮箱的最大配额必须不为 0 。",
"mysql_error": "MySQL 错误: %s",
"network_host_invalid": "网络或主机无效: %s",
"next_hop_interferes": "%s 与下一跳 %s 冲突",
@@ -454,7 +455,8 @@
"username_invalid": "用户名 %s 无法使用",
"validity_missing": "请设置有效期",
"value_missing": "请填入所有值",
"yotp_verification_failed": "Yubico OTP 认证失败: %s"
"yotp_verification_failed": "Yubico OTP 认证失败: %s",
"template_exists": "模板 %s 已存在"
},
"debug": {
"chart_this_server": "图表 (此服务器)",
@@ -473,7 +475,7 @@
"restart_container": "重启",
"service": "服务",
"size": "大小",
"solr_dead": "Solr 在启动中、已关闭或已停止",
"solr_dead": "Solr 在启动中、已关闭或已停止",
"solr_status": "Solr 状态",
"started_at": "开始于",
"started_on": "启动于",
@@ -481,10 +483,14 @@
"success": "成功",
"system_containers": "系统和容器",
"uptime": "运行时间",
"username": "用户名"
"username": "用户名",
"container_disabled": "容器已被停止或禁用",
"container_running": "运行中",
"cores": "核心数",
"memory": "内存"
},
"diagnostics": {
"cname_from_a": "虽然此记录为 A/AAAA 类型,但只要记录指向正确的资源便可以被支持",
"cname_from_a": "来自 A/AAAA 记录的值。但只要记录指向正确的资源即可。",
"dns_records": "DNS 记录",
"dns_records_24hours": "请注意 DNS 记录的更改可能需要24小时才可以使此页面的当前状态显示正确。此页面为你提供了一个可以便捷查询如何配置 DNS 记录以及检查你的 DNS 记录是否正确的方式。",
"dns_records_data": "正确数据",
@@ -501,7 +507,7 @@
"advanced_settings": "高级设置",
"alias": "编辑别名",
"allow_from_smtp": "只允许这些 IP 使用 <b>SMTP</b>",
"allow_from_smtp_info": "留空以允许所有发送者。<br>IPv4/IPv6地址网络",
"allow_from_smtp_info": "留空以允许所有发送者。<br>IPv4/IPv6 地址网络",
"allowed_protocols": "允许的协议",
"app_name": "应用名称",
"app_passwd": "应用密码",
@@ -629,7 +635,7 @@
"delete_these_items": "请确认对以下对象 ID 的更改",
"hibp_check": "使用 haveibeenpwned.com 网站检查密码",
"hibp_nok": "匹配到密码!存在潜在的使用危险!",
"hibp_ok": "未匹配到密码",
"hibp_ok": "未找到匹配的记录。",
"loading": "请等待...",
"nothing_selected": "未选择",
"restart_container": "重启容器",
@@ -655,7 +661,7 @@
},
"login": {
"delayed": "请在 %s 秒后重新登录。",
"fido2_webauthn": "使用 FIDO2/WebAuthn 登录",
"fido2_webauthn": "使用 FIDO2/WebAuthn Login 登录",
"login": "登录",
"mobileconfig_info": "请使用邮箱用户登录以下载 Apple 连接描述文件。",
"other_logins": "Key 登录",
@@ -685,7 +691,7 @@
"aliases": "别名",
"all_domains": "全部域名",
"allow_from_smtp": "只允许这些 IP 使用 <b>SMTP</b>",
"allow_from_smtp_info": "留空以允许所有发送者<br>IPv4/IPv6地址或网络",
"allow_from_smtp_info": "留空以允许所有发送者<br>IPv4/IPv6 地址或网络",
"allowed_protocols": "允许用户直接访问的协议 (不会影响应用的密码协议)",
"backup_mx": "中继域名",
"bcc": "BCC",
@@ -739,7 +745,7 @@
"last_run_reset": "下一次运行",
"mailbox": "邮箱",
"mailbox_defaults": "默认设置",
"mailbox_defaults_info": "配置新邮箱的默认设置",
"mailbox_defaults_info": "配置新邮箱的默认设置",
"mailbox_defquota": "默认邮箱大小",
"mailbox_quota": "最大邮箱大小",
"mailboxes": "邮箱",
@@ -820,7 +826,12 @@
"username": "用户名",
"waiting": "等待中",
"weekly": "每周",
"yes": "&#10003;"
"yes": "&#10003;",
"domain_templates": "域名模板",
"mailbox_templates": "邮箱模板",
"gal": "全局地址列表",
"max_aliases": "最大别名数",
"max_mailboxes": "最大可能的邮箱数"
},
"oauth2": {
"access_denied": "请作为邮箱所有者登录以使用 OAuth2 授权",
@@ -865,7 +876,7 @@
"refresh": "刷新",
"rejected": "已拒绝",
"release": "移除",
"release_body": "我们已在此消息中将你的消息作为 eml 附件文件",
"release_body": "我们已将你的消息作为 eml 文件附在此消息中。",
"release_subject": "存在潜在危险的隔离文件 %s",
"remove": "删除",
"rewrite_subject": "重写主题",
@@ -885,7 +896,8 @@
"type": "类型"
},
"queue": {
"queue_manager": "队列管理器"
"queue_manager": "队列管理器",
"delete": "全部删除"
},
"ratelimit": {
"disabled": "禁用",
@@ -1179,5 +1191,18 @@
"quota_exceeded_scope": "域名配额超标: 此域名下现在只能创建无限容量的邮箱。",
"session_token": "表单字段无效: Token 不匹配",
"session_ua": "表单字段无效: User-Agent 校验错误"
},
"datatables": {
"info": "正从 _TOTAL_ 个条目中显示 _START_ 到 _END_ 条目",
"collapse_all": "全部折叠",
"expand_all": "全部展开",
"infoEmpty": "正从共 0 个条目中显示从 0 到 0 条目",
"processing": "请稍等...",
"search": "搜索:",
"paginate": {
"first": "第一页",
"last": "最后一页",
"previous": "上一页"
}
}
}

View File

@@ -655,7 +655,7 @@
},
"login": {
"delayed": "請在 %s 秒後重新登入。",
"fido2_webauthn": "FIDO2/WebAuthn",
"fido2_webauthn": "FIDO2/WebAuthn Login",
"login": "登入",
"mobileconfig_info": "請使用信箱使用者登入以下載 Apple 連接描述檔案。",
"other_logins": "金鑰登入",

View File

@@ -57,7 +57,7 @@
</div>
</div> <!-- /col-md-12 -->
</div> <!-- /row -->
</div>
</div>
{% include 'modals/admin.twig' %}
@@ -66,7 +66,7 @@ var lang = {{ lang_admin|raw }};
var lang_datatables = {{ lang_datatables|raw }};
var admin_username = '{{ mailcow_cc_username }}';
var csrf_token = '{{ csrf_token }}';
var pagination_size = '{{ pagination_size }}';
var log_pagination_size = '{{ log_pagination_size }}';
var pagination_size = Math.trunc('{{ pagination_size }}');
var log_pagination_size = Math.trunc('{{ log_pagination_size }}');
</script>
{% endblock %}

View File

@@ -33,6 +33,20 @@
</div>
</div>
{% endif %}
<legend style="padding-top:20px" unselectable="on">{{ lang.admin.ip_check }}</legend><hr />
<div id="ip_check">
<form class="form" data-id="ip_check" role="form" method="post">
<div class="mb-4">
<input class="form-check-input" type="checkbox" value="1" name="ip_check_opt_in" id="ip_check_opt_in" {% if ip_check == 1 %}checked{% endif %}>
<label class="form-check-label" for="ip_check_opt_in">
{{ lang.admin.ip_check_opt_in|raw }}
</label>
</div>
<p><div class="btn-group">
<button class="btn btn-sm btn-xs-half d-block d-sm-inline btn-success" data-action="edit_selected" data-item="admin" data-id="ip_check" data-reload="no" data-api-url='edit/ip_check' data-api-attr='{}' href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
</div></p>
</form>
</div>
<legend>{{ lang.admin.app_links }}</legend><hr />
<p class="text-muted">{{ lang.admin.merged_vars_hint|raw }}</p>
<form class="form-inline" data-id="app_links" role="form" method="post">

View File

@@ -36,7 +36,7 @@
</div>
<div class="mb-4">
<label for="rlyhost_password">{{ lang.admin.password }}</label>
<input class="form-control" id="rlyhost_password" name="password">
<input class="form-control" id="rlyhost_password" name="password" type="password">
</div>
<button class="btn btn-sm d-block d-sm-inline btn-success" data-action="add_item" data-id="rlyhost" data-api-url='add/relayhost' data-api-attr='{}' href="#"><i class="bi bi-plus-lg"></i> {{ lang.admin.add }}</button>
</form>
@@ -86,7 +86,7 @@
</div>
<div class="mb-4">
<label for="transport_password">{{ lang.admin.password }}</label>
<input class="form-control" id="transport_password" name="password">
<input class="form-control" id="transport_password" name="password" type="password">
</div>
<div class="mb-2">
<label>

File diff suppressed because one or more lines are too long

View File

@@ -46,7 +46,7 @@
<div class="col-sm-3 col-5 text-end">{{ lang.fido2.known_ids }}:</div>
<div class="col-sm-9 col-7">
<div class="table-responsive">
<table class="table table-striped table-hover table-condensed" id="fido2_keys">
<table class="table table-striped table-hover table-condensed w-100" id="fido2_keys">
<tr>
<th>ID</th>
<th style="min-width:240px;text-align: right">{{ lang.admin.action }}</th>

View File

@@ -26,7 +26,7 @@
var lang_user = {{ lang_user|raw }};
var lang_datatables = {{ lang_datatables|raw }};
var csrf_token = '{{ csrf_token }}';
var pagination_size = '{{ pagination_size }}';
var pagination_size = Math.trunc('{{ pagination_size }}');
var table_for_domain = '{{ domain }}';
</script>
{% endblock %}

View File

@@ -200,8 +200,10 @@
{% if sender_acl_handles.external_sender_aliases %}
{% set ext_sender_acl = sender_acl_handles.external_sender_aliases|join(', ') %}
{% endif %}
<input type="text" class="form-control" name="extended_sender_acl" value="{{ ext_sender_acl }}" placeholder="user1@example.com, user2@example.org, @example.com, ...">
<small class="text-muted">{{ lang.edit.extended_sender_acl_info|raw }}</small>
{% if acl.extend_sender_acl and acl.extend_sender_acl == 1 %}
<input type="text" class="form-control" name="extended_sender_acl" value="{{ ext_sender_acl }}" placeholder="user1@example.com, user2@example.org, @example.com, ...">
<small class="text-muted">{{ lang.edit.extended_sender_acl_info|raw }}</small>
{% endif %}
</div>
</div>
<div class="row">

View File

@@ -38,15 +38,8 @@
</div>
</div>
<div class="d-flex mt-4" style="position: relative">
<div class="btn-group">
<div class="btn-group">
<button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button>
<button type="button" class="btn btn-xs-lg btn-success dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</a></li>
</ul>
</div>
</div>
<button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button>
<button type="button" class="btn btn-xs-lg btn-success ms-2" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</button>
{% if not oauth2_request %}
<button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="btn btn-xs-lg btn-secondary ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>

View File

@@ -4,18 +4,18 @@
<div id="mail-content" class="responsive-tabs">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item dropdown" role="presentation">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.domains }}</a>
<ul class="dropdown-menu">
<li><button class="dropdown-item" aria-selected="false" aria-controls="tab-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-domains">{{ lang.mailbox.domains }}</button></li>
<li><button class="dropdown-item {% if mailcow_cc_role != 'admin' %} d-none{% endif %}" aria-selected="false" aria-controls="tab-templates-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-templates-domains">{{ lang.mailbox.templates }}</button></li>
</ul>
<a class="nav-link dropdown-toggle active" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.domains }}</a>
<ul class="dropdown-menu">
<li><button class="dropdown-item" aria-selected="false" aria-controls="tab-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-domains">{{ lang.mailbox.domains }}</button></li>
<li><button class="dropdown-item {% if mailcow_cc_role != 'admin' %} d-none{% endif %}" aria-selected="false" aria-controls="tab-templates-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-templates-domains">{{ lang.mailbox.templates }}</button></li>
</ul>
</li>
<li class="nav-item dropdown" role="presentation">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.mailboxes }}</a>
<ul class="dropdown-menu">
<li><button class="dropdown-item" aria-selected="false" aria-controls="tab-mailboxes" role="tab" data-bs-toggle="tab" data-bs-target="#tab-mailboxes">{{ lang.mailbox.mailboxes }}</button></li>
<li><button class="dropdown-item {% if mailcow_cc_role != 'admin' %} d-none{% endif %}" aria-selected="false" aria-controls="tab-templates-mbox" role="tab" data-bs-toggle="tab" data-bs-target="#tab-templates-mbox">{{ lang.mailbox.templates }}</button></li>
</ul>
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.mailboxes }}</a>
<ul class="dropdown-menu">
<li><button class="dropdown-item" aria-selected="false" aria-controls="tab-mailboxes" role="tab" data-bs-toggle="tab" data-bs-target="#tab-mailboxes">{{ lang.mailbox.mailboxes }}</button></li>
<li><button class="dropdown-item {% if mailcow_cc_role != 'admin' %} d-none{% endif %}" aria-selected="false" aria-controls="tab-templates-mbox" role="tab" data-bs-toggle="tab" data-bs-target="#tab-templates-mbox">{{ lang.mailbox.templates }}</button></li>
</ul>
</li>
<li class="nav-item" role="presentation"><button class="nav-link" aria-controls="tab-resources" role="tab" data-bs-toggle="tab" data-bs-target="#tab-resources">{{ lang.mailbox.resources }}</button></li>
<li class="nav-item dropdown">
@@ -58,7 +58,7 @@
var lang_rl = {{ lang_rl|raw }};
var lang_datatables = {{ lang_datatables|raw }};
var csrf_token = '{{ csrf_token }}';
var pagination_size = '{{ pagination_size }}';
var pagination_size = Math.trunc('{{ pagination_size }}');
var role = '{{ role }}';
var is_dual = {{ is_dual }};
var ALLOW_ADMIN_EMAIL_LOGIN = {{ allow_admin_email_login }};

View File

@@ -23,9 +23,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="bcc" data-api-url='edit/bcc' data-api-attr='{"type":"rcpt"}' href="#">{{ lang.mailbox.bcc_to_rcpt }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="bcc" data-api-url='delete/bcc' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="bcc_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="bcc_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="bcc_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="bcc_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addBCCModalAdmin"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_bcc_entry }}</a>
</div>
@@ -44,9 +44,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="bcc" data-api-url='edit/bcc' data-api-attr='{"type":"rcpt"}' href="#">{{ lang.mailbox.bcc_to_rcpt }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="bcc" data-api-url='delete/bcc' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="bcc_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="bcc_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="bcc_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="bcc_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addBCCModalAdmin"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_bcc_entry }}</a>
</div>
@@ -74,9 +74,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="recipient_map" data-api-url='edit/recipient_map' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="recipient_map" data-api-url='delete/recipient_map' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="recipient_map_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="recipient_map_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="recipient_map_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="recipient_map_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addRecipientMapModalAdmin"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_recipient_map_entry }}</a>
</div>
@@ -92,9 +92,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="recipient_map" data-api-url='edit/recipient_map' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="recipient_map" data-api-url='delete/recipient_map' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="recipient_map_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="recipient_map_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="recipient_map_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="recipient_map_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addRecipientMapModalAdmin"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_recipient_map_entry }}</a>
</div>

View File

@@ -1,7 +1,7 @@
<div role="tabpanel" class="tab-pane fade" id="tab-domain-aliases" role="tabpanel" aria-labelledby="tab-domain-aliases">
<div class="card mb-4">
<div class="card-header d-flex fs-5">
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-domain-aliases" data-bs-toggle="collapse" aria-controls="ollapse-tab-domain-aliases">
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-domain-aliases" data-bs-toggle="collapse" aria-controls="collapse-tab-domain-aliases">
{{ lang.mailbox.domain_aliases }} <span class="badge bg-info table-lines"></span>
</button>
<span class="d-none d-md-block">{{ lang.mailbox.domain_aliases }} <span class="badge bg-info table-lines"></span></span>
@@ -20,9 +20,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="alias-domain" data-api-url='edit/alias-domain' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="alias-domain" data-api-url='delete/alias-domain' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="aliasdomain_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="aliasdomain_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="aliasdomain_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="aliasdomain_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-acl="{{ acl.alias_domains }}" data-bs-toggle="modal" data-bs-target="#addAliasDomainModal"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_domain_alias }}</a>
</div>
@@ -37,9 +37,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="alias-domain" data-api-url='edit/alias-domain' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="alias-domain" data-api-url='delete/alias-domain' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="aliasdomain_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="aliasdomain_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="aliasdomain_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="aliasdomain_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-acl="{{ acl.alias_domains }}" data-bs-toggle="modal" data-bs-target="#addAliasDomainModal"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_domain_alias }}</a>
</div>

View File

@@ -22,10 +22,10 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="domain" data-api-url='edit/domain' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="domain" data-api-url='delete/domain' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="domain_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="domain_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
{% endif %}
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="domain_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="domain_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
{% if mailcow_cc_role == 'admin' %}
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addDomainModal"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_domain }}</a>
@@ -43,10 +43,10 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="domain" data-api-url='edit/domain' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="domain" data-api-url='delete/domain' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
{% endif %}
<li><a class="dropdown-item" data-datatables-expand="domain_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="domain_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="domain_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="domain_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
{% if mailcow_cc_role == 'admin' %}
<button class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addDomainModal"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_domain }}</button>

View File

@@ -23,9 +23,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"filter_type":"postfilter"}' href="#">{{ lang.mailbox.set_postfilter }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-text="{{ lang.user.eas_reset }}?" data-id="filter_item" data-api-url='delete/filter' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="filter_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="filter_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="filter_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="filter_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addFilterModalAdmin"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_filter }}</a>
</div>
@@ -44,9 +44,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="filter_item" data-api-url='edit/filter' data-api-attr='{"filter_type":"postfilter"}' href="#">{{ lang.mailbox.set_postfilter }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-text="{{ lang.user.eas_reset }}?" data-id="filter_item" data-api-url='delete/filter' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="filter_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="filter_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="filter_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="filter_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addFilterModalAdmin"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_filter }}</a>
</div>

View File

@@ -16,9 +16,9 @@
<a class="btn btn-sm btn-xs-half btn-secondary" id="toggle_multi_select_all" data-id="mailbox" href="#"><i class="bi bi-check-all"></i> {{ lang.mailbox.toggle_all }}</a>
<a class="btn btn-sm btn-xs-half btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-datatables-expand="mailbox_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="mailbox_table">{{ lang.datatables.collapse_all }}</a></li>
<li><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="mailbox_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="mailbox_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="dropdown-header">{{ lang.mailbox.mailbox }}</li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="mailbox" data-api-url='edit/mailbox' data-api-attr='{"active":"1"}' href="#">{{ lang.mailbox.activate }}</a></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="mailbox" data-api-url='edit/mailbox' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
@@ -64,8 +64,8 @@
<a class="btn btn-sm btn-secondary" id="toggle_multi_select_all" data-id="mailbox" href="#"><i class="bi bi-check-all"></i> {{ lang.mailbox.toggle_all }}</a>
<a class="btn btn-sm btn-xs-half btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-datatables-expand="mailbox_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="mailbox_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="mailbox_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="mailbox_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<div class="btn-group">
<a class="btn btn-sm btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.mailbox }}</a>
@@ -130,9 +130,9 @@
<a class="btn btn-sm btn-xs-half btn-secondary" id="toggle_multi_select_all" data-id="mailbox" href="#"><i class="bi bi-check-all"></i> {{ lang.mailbox.toggle_all }}</a>
<a class="btn btn-sm btn-xs-half btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-datatables-expand="mailbox_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="mailbox_table">{{ lang.datatables.collapse_all }}</a></li>
<li><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="mailbox_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="mailbox_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="dropdown-header">{{ lang.mailbox.mailbox }}</li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="mailbox" data-api-url='edit/mailbox' data-api-attr='{"active":"1"}' href="#">{{ lang.mailbox.activate }}</a></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="mailbox" data-api-url='edit/mailbox' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
@@ -178,8 +178,8 @@
<a class="btn btn-sm btn-secondary" id="toggle_multi_select_all" data-id="mailbox" href="#"><i class="bi bi-check-all"></i> {{ lang.mailbox.toggle_all }}</a>
<a class="btn btn-sm btn-xs-half btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-datatables-expand="mailbox_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="mailbox_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="mailbox_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="mailbox_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<div class="btn-group">
<a class="btn btn-sm btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.mailbox }}</a>

View File

@@ -20,9 +20,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="alias" data-api-url='edit/alias' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="alias" data-api-url='delete/alias' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="alias_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="alias_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="alias_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="alias_table">{{ lang.datatables.collapse_all }}</a></li>
{% if not skip_sogo %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="alias" data-api-url='edit/alias' data-api-attr='{"sogo_visible":"1"}' href="#">{{ lang.mailbox.sogo_visible_y }}</a></li>
@@ -44,9 +44,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="alias" data-api-url='edit/alias' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="alias" data-api-url='delete/alias' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="alias_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="alias_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="alias_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="alias_table">{{ lang.datatables.collapse_all }}</a></li>
{% if not skip_sogo %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="alias" data-api-url='edit/alias' data-api-attr='{"sogo_visible":"1"}' href="#">{{ lang.mailbox.sogo_visible_y }}</a></li>

View File

@@ -20,9 +20,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="resource" data-api-url='edit/resource' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="resource" data-api-url='delete/resource' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="resource_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="resource_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="resource_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="resource_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addResourceModal"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_resource }}</a>
</div>
@@ -41,9 +41,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="resource" data-api-url='edit/resource' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="resource" data-api-url='delete/resource' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="resource_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="resource_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="resource_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="resource_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addResourceModal"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_resource }}</a>
</div>

View File

@@ -22,9 +22,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="syncjob" data-api-url='delete/syncjob' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="sync_job_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="sync_job_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="sync_job_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="sync_job_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addSyncJobModalAdmin"><i class="bi bi-plus-lg"></i> {{ lang.user.create_syncjob }}</a>
</div>
@@ -41,9 +41,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="syncjob" data-api-url='edit/syncjob' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="syncjob" data-api-url='delete/syncjob' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="sync_job_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="sync_job_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="sync_job_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="sync_job_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addSyncJobModalAdmin"><i class="bi bi-plus-lg"></i> {{ lang.user.create_syncjob }}</a>
</div>

View File

@@ -18,9 +18,9 @@
<ul class="dropdown-menu">
{% if mailcow_cc_role == 'admin' %}
<li><a class="dropdown-item" data-action="delete_selected" data-id="domain_template" data-api-url='delete/domain/template' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="templates_domain_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="templates_domain_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="templates_domain_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="templates_domain_table">{{ lang.datatables.collapse_all }}</a></li>
{% endif %}
</ul>
{% if mailcow_cc_role == 'admin' %}
@@ -36,9 +36,9 @@
<ul class="dropdown-menu">
{% if mailcow_cc_role == 'admin' %}
<li><a class="dropdown-item" data-action="delete_selected" data-id="domain_template" data-api-url='delete/domain/template' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="templates_domain_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="templates_domain_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="templates_domain_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="templates_domain_table">{{ lang.datatables.collapse_all }}</a></li>
{% endif %}
</ul>
{% if mailcow_cc_role == 'admin' %}

View File

@@ -18,9 +18,9 @@
<ul class="dropdown-menu">
{% if mailcow_cc_role == 'admin' %}
<li><a class="dropdown-item" data-action="delete_selected" data-id="mailbox_template" data-api-url='delete/mailbox/template' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="templates_mbox_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="templates_mbox_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="templates_mbox_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="templates_mbox_table">{{ lang.datatables.collapse_all }}</a></li>
{% endif %}
</ul>
{% if mailcow_cc_role == 'admin' %}
@@ -36,9 +36,9 @@
<ul class="dropdown-menu">
{% if mailcow_cc_role == 'admin' %}
<li><a class="dropdown-item" data-action="delete_selected" data-id="mailbox_template" data-api-url='delete/mailbox/template' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="templates_mbox_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="templates_mbox_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="templates_mbox_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="templates_mbox_table">{{ lang.datatables.collapse_all }}</a></li>
{% endif %}
</ul>
{% if mailcow_cc_role == 'admin' %}

View File

@@ -20,9 +20,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="tls-policy-map" data-api-url='edit/tls-policy-map' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="tls-policy-map" data-api-url='delete/tls-policy-map' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="tls_policy_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="tls_policy_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="tls_policy_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="tls_policy_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addTLSPolicyMapAdmin"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_tls_policy_map }}</a>
</div>
@@ -38,9 +38,9 @@
<li><a class="dropdown-item" data-action="edit_selected" data-id="tls-policy-map" data-api-url='edit/tls-policy-map' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="tls-policy-map" data-api-url='delete/tls-policy-map' href="#">{{ lang.mailbox.remove }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-datatables-expand="tls_policy_table">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="tls_policy_table">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="tls_policy_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="tls_policy_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addTLSPolicyMapAdmin"><i class="bi bi-plus-lg"></i> {{ lang.mailbox.add_tls_policy_map }}</a>
</div>

View File

@@ -16,9 +16,9 @@
<a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary" id="toggle_multi_select_all" data-id="qitems" href="#"><i class="bi bi-check-all"></i> {{ lang.quarantine.toggle_all }}</a>
<a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.quarantine.quick_actions }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-datatables-expand="quarantinetable" data-table="quarantinetable" href="#">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="quarantinetable" data-table="quarantinetable" href="#">{{ lang.datatables.collapse_all }}</a></li>
<li><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="quarantinetable" data-table="quarantinetable" href="#">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="quarantinetable" data-table="quarantinetable" href="#">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="qitems" data-api-url='edit/qitem' data-api-attr='{"action":"release"}' href="#">{{ lang.quarantine.deliver_inbox }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="qitems" data-api-url='edit/qitem' data-api-attr='{"action":"learnspam"}' href="#">{{ lang.quarantine.learn_spam_delete }}</a></li>
@@ -37,15 +37,15 @@
</p>
{% endif %}
</p>
<table id="quarantinetable" class="table table-striped"></table>
<table id="quarantinetable" class="table table-striped w-100"></table>
<div class="mass-actions-quarantine mt-4">
<div class="btn-group" data-acl="{{ acl.quarantine }}">
<a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary" id="toggle_multi_select_all" data-id="qitems" href="#"><i class="bi bi-check-all"></i> {{ lang.quarantine.toggle_all }}</a>
<a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.quarantine.quick_actions }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-datatables-expand="quarantinetable" data-table="quarantinetable" href="#">{{ lang.datatables.expand_all }}</a></li>
<li><a class="dropdown-item" data-datatables-collapse="quarantinetable" data-table="quarantinetable" href="#">{{ lang.datatables.collapse_all }}</a></li>
<li><hr class="dropdown-divider"></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="quarantinetable" data-table="quarantinetable" href="#">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="quarantinetable" data-table="quarantinetable" href="#">{{ lang.datatables.collapse_all }}</a></li>
<li class="table_collapse_option"><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="qitems" data-api-url='edit/qitem' data-api-attr='{"action":"release"}' href="#">{{ lang.quarantine.deliver_inbox }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="qitems" data-api-url='edit/qitem' data-api-attr='{"action":"learnspam"}' href="#">{{ lang.quarantine.learn_spam_delete }}</a></li>
@@ -66,7 +66,7 @@ var acl = '{{ acl_json|raw }}';
var lang = {{ lang_quarantine|raw }};
var lang_datatables = {{ lang_datatables|raw }};
var csrf_token = '{{ csrf_token }}';
var pagination_size = '{{ pagination_size }}';
var pagination_size = Math.trunc('{{ pagination_size }}');
var role = '{{ role }}';
</script>
{% endblock %}

View File

@@ -55,7 +55,7 @@
var lang = {{ lang_queue|raw }};
var lang_datatables = {{ lang_datatables|raw }};
var csrf_token = '{{ csrf_token }}';
var pagination_size = '{{ pagination_size }}';
var pagination_size = Math.trunc('{{ pagination_size }}');
var table_for_domain = '{{ domain }}';
</script>
{% endblock %}

View File

@@ -4,7 +4,7 @@
var acl = '{{ acl_json|raw }}';
var lang = {{ lang_user|raw }};
var csrf_token = '{{ csrf_token }}';
var pagination_size = '{{ pagination_size }}';
var pagination_size = Math.trunc('{{ pagination_size }}');
var mailcow_cc_username = '{{ mailcow_cc_username }}';
var user_spam_score = [{{ user_spam_score }}];
var lang_datatables = {{ lang_datatables|raw }};

View File

@@ -20,6 +20,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma
'tfa_data' => $tfa_data,
'fido2_data' => $fido2_data,
'lang_user' => json_encode($lang['user']),
'lang_datatables' => json_encode($lang['datatables']),
];
}
elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') {

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