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

Compare commits

...

343 Commits

Author SHA1 Message Date
Niklas Meyer 4d688c5500 2024-11a (#6160)
* update.sh: precaution ask for deletion of dns_blocklists.cf if old format (#6154)

* [Web] Updated lang.zh-cn.json (#6151)

[Web] Updated lang.zh-cn.json

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

* compose: bump sogo version to include 5.11.2 (#6156)

* php: use correct php image + workaround of #6149 (#6159)

* compose: bump php-fpm container to correctly use patched c-ares

* [Web] check $containers_info contains required fields

---------

Co-authored-by: FreddleSpl0it <patschul@posteo.de>

---------

Co-authored-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: Easton Man <me@eastonman.com>
Co-authored-by: FreddleSpl0it <patschul@posteo.de>
2024-11-12 15:57:17 +01:00
FreddleSpl0it 0a58aa293a Merge pull request #6141 from mailcow/staging
2024-11
2024-11-07 11:41:45 +01:00
milkmaker be79f320d2 Translations update from Weblate (#6140)
* [Web] Updated lang.lv-lv.json

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

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

Co-authored-by: Furkan <furkan43500@gmail.com>

---------

Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
Co-authored-by: Furkan <furkan43500@gmail.com>
2024-11-06 19:08:53 +01:00
Niklas Meyer 6ec1e357c3 fix: broken sogo cron notifications (for appointments etc.) (#6128) 2024-11-05 16:21:14 +01:00
milkmaker 8b2f71f97e update postscreen_access.cidr (#6129) 2024-11-05 16:20:57 +01:00
renovate[bot] 93cf99cc9e chore(deps): update thollander/actions-comment-pull-request action to v3.0.1 (#6130)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-02 20:38:18 +01:00
FreddleSpl0it d8c8e4ab1b [DockerApi] Fix IMAP ACL migration issue when renaming mailbox 2024-10-31 11:00:03 +01:00
FreddleSpl0it 2d76ffc88c Merge pull request #6045 from mailcow/feat/rename-mbox
[Web][DockerApi] Add Feature to Rename Email Addresses
2024-10-25 10:49:58 +02:00
FreddleSpl0it 672bb345fd Fix mailbox_rename de-de translation 2024-10-25 10:47:53 +02:00
milkmaker 5c88030b5a Translations update from Weblate (#6123)
* [Web] Updated lang.lv-lv.json

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

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

[Web] Updated lang.zh-tw.json

Co-authored-by: SamWang8891 <g348.8891@gmail.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

---------

Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
Co-authored-by: SamWang8891 <g348.8891@gmail.com>
2024-10-22 21:52:42 +02:00
Niklas Meyer b106945c73 Feat/rspamd 3.10.2 (#6122)
* rspamd: update to 3.10.2

* rspamd: fix broken archive_extension gz
2024-10-21 16:03:51 +02:00
milkmaker 502a7100ca [Web] Updated lang.zh-cn.json (#6120)
Co-authored-by: SamWang8891 <g348.8891@gmail.com>
2024-10-19 22:24:45 +02:00
Niklas Meyer ee2791d93a rspamd: update to 3.10.1 (#6115)
* rspamd: upgrade to 3.10.1

* rspamd: adapt 30s task timeout per default now
2024-10-18 15:50:45 +02:00
SamWang8891 399630cf34 Update lang.zh-tw.json (#6114) 2024-10-17 14:50:05 +02:00
Patrik Kernstock fce93609dd Update mime_types.conf configuration (#6013)
In the last months and years, the default `mime_types.conf` of rspamd has changed and it might be also useful to make some adjustments to the weight of certain file extensions.

This PR is removing all file extensions from `mime_types.conf` which are already in rspamd's default configuration at [rspamd/src/plugins/lua/mime_types.lua](https://github.com/rspamd/rspamd/blob/master/src/plugins/lua/mime_types.lua). If file extension is not present or has a different score compared to rspamd default, it is still in the list.

There are also a few major differences to certain file extensions, which might be useful to discuss and carefully adjust. For example, `.exe` files are rated very 'badly' due to high chance of being malicious, so are other extensions like `bat`, `cmd`, etc.

Current suggestion:
```lua
# Extensions that are treated as 'bad'
# Number is score multiply factor
bad_extensions = {
  apk = 4,
  appx = 4,
  appxbundle = 4,
  bat = 8,
  cab = 20,
  cmd = 8,
  com = 20,
  diagcfg = 4,
  diagpack = 4,
  dmg = 8,
  ex = 20,
  ex_ = 20,
  exe = 20,
  img = 4,
  jar = 8,
  jnlp = 8,
  js = 8,
  jse = 8,
  lnk = 20,
  mjs = 8,
  msi = 4,
  msix = 4,
  msixbundle = 4,
  ps1 = 8,
  scr = 20,
  sct = 20,
  vb = 20,
  vbe = 20,
  vbs = 20,
  vhd = 4,
  py = 4,
  reg = 8,
  scf = 8,
  vhdx = 4,
};

# Extensions that are particularly penalized for archives
bad_archive_extensions = {
  pptx = 0.5,
  docx = 0.5,
  xlsx = 0.5,
  pdf = 1.0,
  jar = 12,
  jnlp = 12,
  bat = 12,
  cmd = 12,
};

# Used to detect another archive in archive
archive_extensions = {
  tar = 1,
  ['tar.gz'] = 1,
};
```

**As a important reminder**: For all remaining and additional file extensions and score weights, please check above default rspamd configuration!
2024-10-17 09:11:55 +02:00
Niklas Meyer 38907b5032 dovecot: activate lazy_expunge plugin per default (unconfigured) (#6112) 2024-10-16 15:56:40 +02:00
Peter 5a0f20b9ea Update dependency twig/twig to v3.14.0 (#6071) 2024-10-16 15:29:16 +02:00
Niklas Meyer 8dcaffe925 php: upgrade to alpine 3.20 (base os) (#6106) 2024-10-16 10:35:54 +02:00
Niklas Meyer c53bf85480 postfix: add X-Original-To header per default (#6110) 2024-10-16 10:35:39 +02:00
Niklas Meyer 982e823c71 sogo: upgrade to 5.11.1 (#6109) 2024-10-15 16:13:51 +02:00
renovate[bot] 382056ec18 chore(deps): update dependency krakjoe/apcu to v5.1.24 (#6087)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 11:24:26 +02:00
renovate[bot] 4c9690e87c chore(deps): update dependency php/pecl-mail-mailparse to v3.1.8 (#6096)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 11:09:23 +02:00
renovate[bot] 9a58e5e35a chore(deps): update dependency phpredis/phpredis to v6.1.0 (#6098)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2024-10-15 10:45:32 +02:00
renovate[bot] 932cf453de chore(deps): update dependency nextcloud/server to v28.0.11 (#6101)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 10:34:57 +02:00
milkmaker 1538fda71c update postscreen_access.cidr (#6093) 2024-10-15 10:34:39 +02:00
renovate[bot] 54a0d53deb chore(deps): update thollander/actions-comment-pull-request action to v3 (#6102)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 10:34:19 +02:00
Niklas Meyer fda95301ba fix: added tls1.0/1.1 patch for openssl when using older tls versions in override (#6105) 2024-10-15 10:32:08 +02:00
FreddleSpl0it 1528e8766a [DockerApi] correctly escape user input 2024-09-06 15:59:52 +02:00
Hassan A Hashim 220fdbb168 Add missing Russian translation (#6065) 2024-09-06 07:14:34 +02:00
milkmaker fe3d08515e [Web] Language file updated by 'Cleanup translation files' addon (#6064) 2024-09-06 07:13:59 +02:00
airon-assustadus 22f7f61ac9 feat/brazilian-translations (#6048)
# What
- Adding some brazilian translations that were missing

Co-authored-by: Airon Teixeira <airon@ymail.com>
2024-09-05 15:09:49 +02:00
FreddleSpl0it 29d8cfe2ba [Web] Set min-width and text-align for last login badges 2024-09-05 14:02:04 +02:00
FreddleSpl0it f2e35dff68 [Web] rename user in sender_acl table 2024-09-05 12:40:30 +02:00
FreddleSpl0it b1368d29d1 Merge pull request #5724 from q16marvin/master
show last sso login in mailbox table
2024-09-05 12:02:16 +02:00
FreddleSpl0it 0d704a57f5 Merge pull request #6057 from mailcow/fix/sogo-auto-reply
[SOGo] Fix vacation auto reply date shifting
2024-09-05 11:19:40 +02:00
FreddleSpl0it 462137ede7 Merge pull request #6044 from mailcow/feat/redis-session-store
[PHP-FPM] Use redis as session store
2024-09-05 10:55:07 +02:00
Niklas Meyer bb6f405841 compose: added clamd as depends_on to rspamd (#6062) 2024-09-04 14:42:30 +02:00
renovate[bot] 8b2d67169b chore(deps): update peter-evans/create-pull-request action to v7 (#6059)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 19:42:10 +02:00
Finn Hoffhenke 710cec996c feat: Added check for newer version tags on remote (#6054) 2024-09-02 15:40:29 +02:00
Niklas Meyer 0129f84a32 Merge pull request #6056 from mailcow/update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2024-09-02 15:37:24 +02:00
FreddleSpl0it ae3653a925 [SOGo] vacation auto reply date shifting #5394 2024-09-02 10:22:51 +02:00
milkmaker af0c61b90a update postscreen_access.cidr 2024-09-01 00:19:09 +00:00
milkmaker 7203735532 [Web] Updated lang.it-it.json (#6053)
Co-authored-by: Stefano <stefano.vassena@gmail.com>
2024-08-29 20:27:23 +02:00
FreddleSpl0it 4f9e37c0c3 [Web] rename user in bcc_maps, recipient_maps and imapsync table 2024-08-28 11:16:29 +02:00
FreddleSpl0it d21c1bfa72 [Web] add error handling for get_acl call 2024-08-28 10:48:44 +02:00
FreddleSpl0it 822d9a7de6 [Web] rename goto in alias table 2024-08-27 10:07:07 +02:00
DerLinkman 37beed6ad9 update FUNDING.yml 2024-08-26 09:56:49 +02:00
milkmaker 0066040bdc Translations update from Weblate (#6049)
* [Web] Updated lang.cs-cz.json

Co-authored-by: Kristian Feldsam <feldsam@gmail.com>

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

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

---------

Co-authored-by: Kristian Feldsam <feldsam@gmail.com>
Co-authored-by: Samuel F <20537389+samuelfranzini@users.noreply.github.com>
2024-08-24 14:09:28 +02:00
DerLinkman 75f18df143 Revert "Before update on 2024-08-20_14_22_10"
This reverts commit 89398c4726.
2024-08-23 09:54:10 +02:00
FreddleSpl0it 8e7b27aae4 [DockerApi] rework doveadm__get_acl function 2024-08-23 09:30:23 +02:00
FreddleSpl0it c62b467ac4 [PHP-FPM] Use redis as session store 2024-08-22 11:16:01 +02:00
FreddleSpl0it be5a181be5 [Web][DockerApi] migrate imap acl on mbox rename 2024-08-22 10:10:05 +02:00
FreddleSpl0it 10dfd0a443 [Web][DockerApi] Add the ability to rename the local part of a mailbox 2024-08-21 10:10:34 +02:00
milkmaker cc5138da13 Translations update from Weblate (#6039)
* [Web] Updated lang.fr-fr.json

[Web] Updated lang.fr-fr.json

Co-authored-by: GeistFighter <lorentzjohan1@gmail.com>
Co-authored-by: Samuel F <20537389+samuelfranzini@users.noreply.github.com>

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

Co-authored-by: Berttas <mika@tarh.fi>

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

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

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

Co-authored-by: DRago_Angel <dragoangel@users.noreply.translate.mailcow.email>

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

Co-authored-by: xmacaba <lixo@macaba.com.br>

---------

Co-authored-by: GeistFighter <lorentzjohan1@gmail.com>
Co-authored-by: Samuel F <20537389+samuelfranzini@users.noreply.github.com>
Co-authored-by: Berttas <mika@tarh.fi>
Co-authored-by: Habetdin <15926758+Habetdin@users.noreply.github.com>
Co-authored-by: DRago_Angel <dragoangel@users.noreply.translate.mailcow.email>
Co-authored-by: xmacaba <lixo@macaba.com.br>
2024-08-20 21:34:04 +02:00
DerLinkman 89398c4726 Before update on 2024-08-20_14_22_10 2024-08-20 14:22:55 +02:00
DerLinkman 8971b11c49 Merge branch 'staging' 2024-08-20 14:08:57 +02:00
Hassan A Hashim bb7fd483f7 Fix: Escape a ' character in update.sh (#6034) 2024-08-20 14:08:08 +02:00
Niklas Meyer 439a936fd8 Merge pull request #6033 from mailcow/staging
2024-08a
2024-08-20 13:44:51 +02:00
Délano 567ebbc324 Pushover/Quarantine utf 8 fix - fixes #6028 (#6031)
* Decode rspamd-subject for pushover notifications

Fixes #6028

* Apply iconv_mime_decode to the quarantine function as well
This might contain utf-8 encoded text as well

* Moved the iconv_mime_decode "fix" back to pipe.php
2024-08-20 13:39:20 +02:00
Hassan A Hashim f9a7712025 Replace weird character to the correct ' (#6029)
* Replace weird character to the correct `'`

* Replace final weird character, just found.
2024-08-20 08:08:34 +02:00
Hassan A Hashim 3d62869664 Fix: bash variables are not quoted (#6022)
* Fix: Double quote variables to prevent word splitting

* Fix `update.sh`: Double quote to prevent word splitting

* Refactor: Remove unnecessary white-spaces.
2024-08-19 15:47:55 +02:00
Niklas Meyer b70bcd36fb containers: use mariadb-admin instead of deprecated mysqladmin (#6026)
* dockerfiles: use mariadb-admin instead of deprecated mysqladmin command

* compose: bump compose tags
2024-08-19 11:33:28 +02:00
Niklas Meyer cb50d08605 dovecot: added timeout option when sa-rules cannot be downloaded (#6025)
* dovecot: added timeout option when sa-rules cannot be downloaded

* dovecot: changed sa-rules exit code to 0 to allow dovecot to start afterwards
2024-08-19 11:08:13 +02:00
Hassan A Hashim f3da8bb85f Refactor/Change Dockerfiles cmd from shell to exec form (#6019)
* Update `dockerapi/Dockerfile` CMD from shell to exec format

* Update `postfix/Dockerfile` CMD from shell to exec format

* Update `sogo/Dockerfile` CMD from shell to exec format

* Update `unbound/Dockerfile` CMD from shell to exec format

* Update `watchdog/Dockerfile` CMD from shell to exec format
2024-08-19 10:42:11 +02:00
Niklas Meyer 12e4d639f0 Merge pull request #6016 from jkrgr0/fix/ParseDockerVersion 2024-08-16 10:50:04 +02:00
Janek eb3f88fc91 fix: 🚑 Fixed version parsing of docker
Only the first result (the major version) is relevant

Closes #6015
2024-08-16 08:47:03 +02:00
Niklas Meyer 9a729d89bf Merge pull request #6012 from mailcow/staging
2024-08
2024-08-15 14:46:50 +02:00
Niklas Meyer 74b4097ee0 Merge pull request #6011 from mailcow/gh/add_pull_request_template
.github: Add pull_request_template.md
2024-08-15 11:51:37 +02:00
DerLinkman e00d0d5f8d Updated contributing.md 2024-08-15 11:32:28 +02:00
DerLinkman c5e399ebc2 .github: Add pull_request_template.md 2024-08-15 11:09:37 +02:00
FreddleSpl0it cb9ca772b1 Merge pull request #6009 from mailcow/feat/pw-reset
[Web] Add a forgot password flow
2024-08-15 11:06:30 +02:00
Niklas Meyer 162f05ccda Merge pull request #6007 from mailcow/revert-5945-master
Revert "Don't expose SMTP/IMAP if announced "not provided" via SRV"
2024-08-15 09:51:19 +02:00
Niklas Meyer 6c97c4f372 Revert "Don't expose SMTP/IMAP if announced "not provided" via SRV" 2024-08-15 09:50:36 +02:00
Niklas Meyer 6d4fcacd83 Merge pull request #6006 from mailcow/fix/issue-5986
flatcurve-fts: limit tokenizers size in e-mail adress
2024-08-14 10:06:17 +02:00
DerLinkman 1994f706c0 dovecot: optimized dockerfile syntax 2024-08-14 10:03:42 +02:00
DerLinkman e34afd3fdd flatcurve-fts: limit tokenizers for email adresses 2024-08-14 10:02:59 +02:00
DerLinkman a6f71faf46 github-actions: compacted auto nightly pr 2024-08-13 16:07:09 +02:00
Niklas Meyer b26ccc2019 unbound: fix healthcheck logging + added fail tolerance to checks (#6004)
* unbound: fix healthcheck logging to stdout + rewrote healthcheck logic

* compose: bump unbound tag

* unbound: fixed healthcheck logic
2024-08-13 15:59:57 +02:00
Niklas Meyer b1c1e403d2 sogo: update to 5.11.0 + Rebase on Bookworm (#6002)
* sogo: update to 5.11.0

* compose: bump sogo compose tag
2024-08-13 09:43:59 +02:00
Dmitriy Alekseev 8753ea2be6 [Rspamd] Fix bayes config (#6000)
* [Rspamd] Fix bayes config

Add hint about classifier name, and add missing learn_condition

* Update statistic.conf
2024-08-12 10:05:08 +02:00
milkmaker 9fee568082 Translations update from Weblate (#5999)
* [Web] Updated lang.ru-ru.json

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

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

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

---------

Co-authored-by: Oleksii Kruhlenko <a.kruglenko@gmail.com>
2024-08-10 20:44:40 +02:00
DerLinkman 294a406b91 fix: disabled api call to solr in ui when mailbox deleted but using flatcurve 2024-08-08 09:25:52 +02:00
Niklas Meyer 8b933f1967 Merge pull request #5934 from muhlba91/fix/restore-docker 2024-08-08 08:47:12 +02:00
Kitof 824a473fea ofelia: limit scope to mailcow project (#5776)
* Filter to limit ofelia scope

See https://github.com/mailcow/mailcow-dockerized/issues/5775

* compose: added ${COMPOSE_PROJECT_NAME} ENV to ofelia container
2024-08-08 08:42:50 +02:00
milkmaker 7f790c5360 [Web] Updated lang.si-si.json (#5995)
Co-authored-by: gomiunik <boris@gomiunik.net>
2024-08-07 18:39:38 +02:00
DerLinkman 52431a3942 compose: bump watchdog image 2024-08-07 14:50:12 +02:00
Niklas Meyer 8017394e9d Merge pull request #5773 from mrclschstr/staging
[Fix] Watchdog: escape subject and body for webhooks
2024-08-07 14:48:11 +02:00
Niklas Meyer 76194be7dd Merge pull request #5991 from h3ssan/refactor/update-script-help-exit
Refactor: `update.sh` script with `--help` should exit with status code 0
2024-08-07 14:03:32 +02:00
Niklas Meyer 3b23afa0ff Merge pull request #5661 from mailcow/feat/rspamd-3.8
rspamd: upgrade to rspamd 3.9.1
2024-08-07 14:01:39 +02:00
DerLinkman 6e00d653ce compose: bumped rspamd tag 2024-08-07 14:00:04 +02:00
DerLinkman b6c036496d rspamd: fixed dqs rbl insertion handling 2024-08-07 14:00:04 +02:00
DerLinkman 5d7c9b20bc rspamd: upgrade to 3.9.1 + upgrade to bookworm 2024-08-07 14:00:04 +02:00
DerLinkman 4b400eadb1 rspamd: Added DQS RBLs when key is set 2024-08-07 13:59:26 +02:00
Niklas Meyer ab2abda8cc Merge pull request #5967 from Doozy134/fix/curl-hostname
fix: change internal urls for containers using curl on alpine
2024-08-07 13:58:11 +02:00
Hassan A Hashim 2fe21e9641 Refactor: update.sh script with --help should exit with status code 0 2024-08-07 14:57:36 +03:00
Niklas Meyer b7ed6982d8 Merge pull request #5945 from SailReal/master
Don't expose SMTP/IMAP if announced "not provided" via SRV
2024-08-07 13:51:10 +02:00
Niklas Meyer fd927853cb Merge pull request #5990 from h3ssan/fix/dockerfile-label-fix
Fix `LABEL` in Dockerfile, should be key=value
2024-08-07 13:49:07 +02:00
Niklas Meyer c48f4f4ab8 Merge pull request #5989 from h3ssan/fix/update-script-procceding-typo
Fix typo in `update.sh`: word Proceeding
2024-08-07 13:47:57 +02:00
DerLinkman a4c006828e compose: bump container tags 2024-08-07 09:51:47 +02:00
DerLinkman b56291f62b adapt scheme to affected curl containers (dirty way... but workaround) 2024-08-07 09:50:57 +02:00
Kasim 0cdf7647c4 Include COMPOSE_PROJECT_NAME in Nginx url 2024-08-07 09:40:08 +02:00
Kasim 8fe1cc4961 change nginx address
#5962
2024-08-07 09:40:04 +02:00
Niklas Meyer bf050f17c4 Merge pull request #5987 from h3ssan/fix/validate-mailcow-conf-before-source
Bug Fix: Check `mailcow.conf` exists before work with it
2024-08-07 09:33:16 +02:00
Hassan A Hashim edd85dea8d Fix LABEL in Dockerfile, should be key=value
Refering to the [Official Docker Docs](`https://docs.docker.com/reference/dockerfile/#label`), clearly said the format of LABEL is `LABEL <key>=<value> <key>=<value> <key>=<value> ...`.
2024-08-06 22:44:59 +03:00
Hassan A Hashim 3bf90c1f73 Fix typo for word Potential in update.sh file. 2024-08-06 21:22:30 +03:00
Hassan A Hashim 292306b191 Fix typos and English grammar in update.sh
German is different in using upper-case than English lol
2024-08-06 21:12:20 +03:00
Hassan A Hashim b3e0a66222 Fix typo: receiving updates from an unsupported branch 2024-08-06 21:03:17 +03:00
Hassan A Hashim e994cf4d05 Fix typo in update.sh: Proceeding 2024-08-06 20:38:18 +03:00
Hassan A Hashim cc0dc2eae0 Add color-coded error message for missing mailcow.conf 2024-08-06 17:51:46 +03:00
DerLinkman a001a0584f update.sh: fix text for min. docker ver 2024-08-06 16:21:28 +02:00
DerLinkman 926af87cfb scripts: adding docker version check to align to docs (24.X) 2024-08-06 16:20:28 +02:00
Hassan A Hashim b0339372b5 Check mailcow.conf exists before source it 2024-08-06 17:12:54 +03:00
Niklas Meyer e398cb91e9 Merge pull request #5985 from mailcow/feat/improve-sieve-parser
ui: added enotify and mime as valid options for ui
2024-08-06 15:36:00 +02:00
DerLinkman 6ee0303b0f ui: added enotify and mime as valid options for ui 2024-08-06 15:33:40 +02:00
Niklas Meyer 68616c2d57 Merge pull request #5972 from rallisf1/dovecot-folders-greek
Greek names of dovecot folders
2024-08-06 12:28:23 +02:00
Niklas Meyer f8de520d29 Merge pull request #5983 from mailcow/fix/sieve-compiling
dovecot: fix precompiling of sieve scripts
2024-08-06 12:27:41 +02:00
Niklas Meyer 10077ece31 Merge pull request #5804 from Ayowel/feat/unattended-install
Allow prompt-less install on low-resource systems
2024-08-06 12:26:51 +02:00
DerLinkman c918726143 dovecot: fix precompiling of sieve scripts 2024-08-06 12:04:04 +02:00
milkmaker 3885b07a99 [Web] Updated lang.nb-no.json (#5980)
Co-authored-by: Christer Solstrand Johannessen <csjoh@users.noreply.translate.mailcow.email>
2024-08-05 19:36:55 +02:00
FreddleSpl0it fcf27d640d Merge pull request #5979 from mailcow/staging
2024-07
2024-08-05 08:55:59 +02:00
Marcel Schuster 82fde23cc1 Bump watchdog to v2.03 2024-08-01 19:14:29 +02:00
FreddleSpl0it cbca306fc1 Merge pull request #5976 from mailcow/fix/get-tfa
2024-07 fixes
2024-08-01 11:04:04 +02:00
Niklas Meyer 6a8986fe4f Merge pull request #5974 from mailcow:update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2024-08-01 09:06:42 +02:00
milkmaker ff34eb12e2 update postscreen_access.cidr 2024-08-01 00:16:46 +00:00
FreddleSpl0it fbecd60e56 [Web] add new pw_reset acl to mailbox templates 2024-07-31 09:23:53 +02:00
FreddleSpl0it c37bf0bb32 [Web] improve error handling for user password resets 2024-07-31 09:22:52 +02:00
FreddleSpl0it 2208d7e6fb [Web] add function to reset user passwords 2024-07-30 14:46:08 +02:00
John Rallis e426c3a7e7 Greek names of dovecot folders
Names taken from MSO 2016
2024-07-29 16:46:03 +03:00
Niklas Meyer 03fccb28e9 Merge pull request #5971 from mailcow/dragoangel-patch-1
Do not add MAILCOW_WHITE on failed DMARC
2024-07-29 09:51:16 +02:00
Dmitriy Alekseev 8fbfd99dd6 Update composites.conf 2024-07-28 13:20:24 +02:00
Dmitriy Alekseev 7f7a869678 Do not add MAILCOW_WHITE on failed DMARC 2024-07-28 13:19:03 +02:00
DerLinkman 73257151c4 postfix: remove forced helo restrictions from master.cf 2024-07-24 15:29:28 +02:00
FreddleSpl0it efb2572f0f [Web] escapeHtml in relayhosts table 2024-07-22 15:05:43 +02:00
FreddleSpl0it 66aa28b5de [Web] escapeHtml in api_log table 2024-07-22 15:04:29 +02:00
Niklas Meyer 987a027339 Merge pull request #5957 from mailcow/staging
2024-06c
2024-07-12 16:25:01 +02:00
Niklas Meyer eea81e21f6 Revert "php: Rebase on Debian 12" (#5956)
* Revert "php: Rebase on Debian 12 (#5951)"

This reverts commit 9b478b3859.

* Revert all before "the storm" in php world
2024-07-12 16:21:53 +02:00
Niklas Meyer a689109f44 Merge pull request #5955 from mailcow/revert-5875-staging_cml
Revert "Update debug.twig to include a link to the git project URL for the mailcow version tag"
2024-07-12 16:05:01 +02:00
Niklas Meyer 58c0a46459 Revert "Update debug.twig to include a link to the git project URL for the mailcow version tag" 2024-07-12 16:04:19 +02:00
Niklas Meyer 2dbe8bf4ca Merge pull request #5952 from mailcow/staging
2024-06b
2024-07-12 10:17:46 +02:00
Niklas Meyer ef7ec06947 Merge pull request #5930 from mailcow/update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2024-07-12 10:16:44 +02:00
DerLinkman fc7ea7a247 web: remove WIP notice for ARM64 from ui 2024-07-12 10:15:06 +02:00
Niklas Meyer 9b478b3859 php: Rebase on Debian 12 (#5951)
* php: rebuild on debian 12

* Restored one build dockerfile

* cleanup Dockerfile
2024-07-12 09:40:10 +02:00
Julian Raufelder 384e5a2e64 Don't expose SMTP/IMAP if announced "not provided" via SRV
Fixes #5944
2024-07-09 19:57:32 +02:00
Niklas Meyer aadeeb0df3 Merge pull request #5634 from torzech/proper-threads-regex
Enhanced regular expression for THREADS parameter
2024-07-09 10:10:42 +02:00
FreddleSpl0it f33d82ffc1 [Web] use correct user to fetch TFA authenticators 2024-07-03 15:50:17 +02:00
Daniel Muehlbachler-Pietrzykowski ffeeb179e1 restore: remove tty requirement from restore process to allow for automated restores 2024-07-03 10:53:37 +02:00
milkmaker 8e2d3a6db5 update postscreen_access.cidr 2024-07-01 00:16:56 +00:00
Niklas Meyer 70126e1f0c Merge pull request #5926 from mailcow/staging
🌙🐄 Moone Update 2024 | Revision A
2024-06-27 18:07:19 +02:00
Niklas Meyer b9ae174a6a Merge pull request #5925 from mailcow/revert-5912-weblate-translated
Revert "Translations update from Weblate"
2024-06-27 18:04:41 +02:00
Niklas Meyer 9715c57314 Revert "Translations update from Weblate (#5912)"
This reverts commit 1af9c21a50.
2024-06-27 18:03:01 +02:00
Niklas Meyer b9f8959d92 Update CONTRIBUTING.md
Added language terms
2024-06-27 13:11:19 +02:00
Niklas Meyer 9c814cc182 Merge pull request #5922 from mailcow/staging
2024-06
2024-06-27 11:15:53 +02:00
Niklas Meyer cf6594220c dovecot: add Flatcurve FTS Engine as EXPERIMENTAL (#5920)
* dovecot: experimental added flatcurve backend + switch

* dovecot: bump docker image
2024-06-26 11:28:18 +02:00
Niklas Meyer 2cf952eb36 [Postfix] Upgrade to Deb12 + PF to 3.7.10 & Drop TLS 1.0/1.1 per default (#5635)
* postfix: removed TLS1.0/1.1 support (natively)

* postfix: upgrade to deb12 + pf to 3.7.9

* compose: increased postfix tag

* postfix: shortened TLS syntax with new format of 3.6+
2024-06-26 10:44:07 +02:00
DerLinkman 6fc86dd7d3 acme: corrected acme-tiny download path 2024-06-24 10:00:30 +02:00
DerLinkman bf13af9691 increased rspamd image tag 2024-06-24 10:00:16 +02:00
milkmaker 1af9c21a50 Translations update from Weblate (#5912)
* [Web] Updated lang.ca-es.json

[Web] Updated lang.ca-es.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.cs-cz.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.da-dk.json

[Web] Updated lang.da-dk.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.es-es.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.fi-fi.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.fr-fr.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.hu-hu.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.ro-ro.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.it-it.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.lv-lv.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.ko-kr.json

[Web] Updated lang.ko-kr.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.nl-nl.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.pl-pl.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.ru-ru.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.sk-sk.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

* [Web] Updated lang.sv-se.json

[Web] Updated lang.sv-se.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.zh-cn.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

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

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

[Web] Updated lang.zh-tw.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.pt-pt.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.tr-tr.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.si-si.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

[Web] Updated lang.gr-gr.json

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>

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

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

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

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

---------

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Peter <magic@kthx.at>
2024-06-24 09:07:46 +02:00
realizelol 443941e687 [Rspamd] Delete overriding obsolete rspamd plugin (#5900)
* [Dockerfiles] rspamd: Delete COPY of metadata_exporter.lua plugin

* [Dockerfiles] rspamd: Delete metadata_exporter.lua plugin file

* Dockerfile: changed way of installing rspamd (granular version)

---------

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
2024-06-24 09:07:12 +02:00
renovate[bot] 527577b438 chore(deps): update docker/build-push-action action to v6 (#5910)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-17 17:38:21 +02:00
milkmaker 9daf2d80c0 Translations update from Weblate (#5908)
* [Web] Updated lang.fr-fr.json

Co-authored-by: Paul FERA <paulfera17@gmail.com>

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

[Web] Updated lang.lv-lv.json

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

---------

Co-authored-by: Paul FERA <paulfera17@gmail.com>
Co-authored-by: Deniss <mailcow@rigaden.me>
Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
2024-06-16 19:21:46 +02:00
Daniel 38b0641742 Remove unnecessary log lines in Postfix's log (#5817)
* Update main.cf

In order to avoid unnecessary log lines, changed:

smtpd_discard_ehlo_keywords = chunking
to this one:

# The non-logging alternative:
smtpd_discard_ehlo_keywords = chunking, silent-discard

Update main.cf to remove unnecessary log lines in Postfix log
2024-06-10 14:51:55 +02:00
Niklas Meyer f675af5bb0 Merge pull request #5902 from mailcow/feat/nextcloud-deprecation
nextcloud: add deprecation notice once script start
2024-06-10 14:33:53 +02:00
DerLinkman 533c4e7956 nextcloud: add deprecation notice once script start 2024-06-10 14:21:13 +02:00
Niklas Meyer 1b2c2c0037 Merge pull request #5690 from mailcow:renovate/nextcloud-server-28.x
chore(deps): update dependency nextcloud/server to v28.0.6
2024-06-10 13:57:09 +02:00
Niklas Meyer 97768494e1 Merge pull request #5880 from PierrePlt:fix/blocking-last-logins
Fix blocking last logins fetching
2024-06-10 12:40:43 +02:00
Lasagne 4a052da289 Add switch to skip fetching certificates auto{config,discover} subdomains (#5838)
* Add ACME_DONT_FETCH_CERTS_FOR_HTTP_SUBDOMAINS to acme.sh

* Add ACME_DONT_FETCH_CERTS_FOR_HTTP_SUBDOMAINS to docker-compose.yml

* Add ACME_DONT_FETCH_CERTS_FOR_HTTP_SUBDOMAINS to generate_config.sh

* Add ACME_DONT_FETCH_CERTS_FOR_HTTP_SUBDOMAINS to update.sh

* AUTODISCOVER_SAN instead of long string

default on,
default is fetching certs for auto{discover,conf}

* AUTODISCOVER_SAN instead of long string

also flipped

* AUTODISCOVER_SAN instead of long string

flipped default meaning

* fix explanation for AUTODISCOVER_SAN

* AUTODISCOVER_SAN instead of long string

and flipped meaning of the bool

* fix AUTODISCOVER_SAN explanation

* Merge branch 'mailcow:staging' into staging

* update.sh: corrected syntax for mailcow.conf insertion
2024-06-10 12:33:02 +02:00
Niklas Meyer 18d7a55b15 Merge pull request #5901 from mailcow:sorbs
Remove discontinued SORBS DNSBL
2024-06-10 12:18:43 +02:00
Michael Kuron 9ca2fb7ccf Remove discontinued SORBS DNSBL 2024-06-08 12:29:08 +02:00
Niklas Meyer b4e8355827 Merge pull request #5845 from iamspido:patch-1
remove version from docker-compose.yml
2024-06-06 15:30:15 +02:00
DerLinkman e0bde1c459 compose: removed all versions declarations (DEPRECATED) 2024-06-06 15:29:34 +02:00
Niklas Meyer 27c007ebd3 Merge pull request #5750 from DocFraggle:staging
Fix unbound healthcheck.sh to log all messages to logfile
2024-06-06 15:27:40 +02:00
Niklas Meyer 8f3ea09732 Merge pull request #5893 from mailcow/feat/base-os
os: updated all Alpine containers to 3.20
2024-06-05 13:10:10 +02:00
DerLinkman af626d98d3 dovecot: fixed sa-rules download 2024-06-05 13:07:12 +02:00
Niklas Meyer 34b0574e56 Merge pull request #5886 from Thomas2500:patch-1
Switch IP2Country lookup backend to shortened version
2024-06-05 12:37:31 +02:00
Niklas Meyer 49d738809b Merge pull request #5863 from mailcow:update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2024-06-05 12:32:56 +02:00
Niklas Meyer 2fa3a22eca Merge pull request #5875 from CallMeLeon167:staging_cml
Update debug.twig to include a link to the git project URL for the mailcow version tag
2024-06-05 12:31:33 +02:00
Niklas Meyer dc5eb6f92e Merge pull request #5883 from mailcow:renovate/alpine-3.x
chore(deps): update alpine docker tag to v3.20
2024-06-05 12:27:28 +02:00
DerLinkman ba8902f0b1 os: updated all Alpine containers to 3.20 2024-06-05 11:52:48 +02:00
milkmaker 11e9a77840 update postscreen_access.cidr 2024-06-01 00:15:03 +00:00
Thomas Bella 64cd7e74c5 Switch IP2Country lookup backend to shortened version
Improves performance of #5880
2024-05-28 20:29:05 +02:00
renovate[bot] cac65d081e chore(deps): update dependency nextcloud/server to v28.0.6
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2024-05-28 12:54:13 +00:00
renovate[bot] e5ada994be Update alpine Docker tag to v3.20
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2024-05-22 18:17:51 +00:00
Pierre Pelletier 6ba2459645 Fixed blocking last connection fetching 2024-05-18 11:38:41 +00:00
DerLinkman 58f63aad08 [UI] Corrected Sieve Preset 1 (Fixed Regex) 2024-05-13 15:02:41 +02:00
milkmaker 8a8687a63c [Web] Updated lang.zh-cn.json (#5876)
Co-authored-by: Koala Ng <tonghoil@hotmail.com>
2024-05-10 22:38:08 +02:00
Leon Schmidt f7f93c360d fix formatting of the mailcow version tag link 2024-05-10 19:56:31 +02:00
Leon c160e1f68e Update debug.twig 2024-05-10 15:57:36 +02:00
Leon Schmidt 47c08ab8d2 Update debug.twig target="_blank" attribute for the mailcow version tag link 2024-05-10 15:17:49 +02:00
Leon Schmidt cd83ffbaa2 Update debug.twig to include a link to the git project URL for the mailcow version tag 2024-05-10 15:09:27 +02:00
milkmaker e12981a821 [Web] Updated lang.zh-cn.json (#5873)
Co-authored-by: Koala Ng <tonghoil@hotmail.com>
2024-05-07 17:44:37 +02:00
Ramis 47fd1bb894 Update lang.ru-ru.json (#5865)
Update lang

Co-authored-by: Patrick Schult <75116288+FreddleSpl0it@users.noreply.github.com>
2024-05-03 14:05:43 +02:00
milkmaker 20582b6353 [Web] Updated lang.lv-lv.json (#5862)
Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
2024-04-30 18:28:09 +02:00
IamSpido c8ff5387c0 remove version from docker-compose.yml
With docker version 25.05 the version 2.1 in docker-compose.yml will be obsolete.
docker-compose.yml: `version` is obsolete
2024-04-16 14:10:44 +02:00
Mitchell van Bijleveld 7cb138d515 Improve Dutch translation (#5840) 2024-04-11 21:36:45 +02:00
milkmaker 3dd4c45fab Translations update from Weblate (#5839)
* [Web] Updated lang.hu-hu.json

Co-authored-by: David Csillag <csillag.david.istvan@gmail.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

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

---------

Co-authored-by: David Csillag <csillag.david.istvan@gmail.com>
Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
2024-04-10 21:55:31 +02:00
polido 549539bec9 Update lang.pt-pt.json (#5832)
Co-authored-by: Patrick Schult <75116288+FreddleSpl0it@users.noreply.github.com>
2024-04-08 17:48:41 +02:00
milkmaker e449cac464 Translations update from Weblate (#5835)
* [Web] Updated lang.fr-fr.json

Co-authored-by: Quiwy <github@quiwy.ninja>

* [Web] Updated lang.sv-se.json

Co-authored-by: André J <aj@nadox.se>

---------

Co-authored-by: Quiwy <github@quiwy.ninja>
Co-authored-by: André J <aj@nadox.se>
2024-04-08 17:47:43 +02:00
milkmaker 62e458f39b [Web] Updated lang.fr-fr.json (#5824)
Co-authored-by: Quiwy <github@quiwy.ninja>
2024-04-04 19:23:02 +02:00
FreddleSpl0it b37caaf9e5 [Web] secure container_ctrl.php 2024-04-04 16:30:35 +02:00
FreddleSpl0it 7660ca89ae [Web] break loop if rspamd_map is valid 2024-04-04 16:29:58 +02:00
Patrick Schult 36b5cccd18 Merge pull request #5819 from mailcow/staging
2024-04
2024-04-04 08:50:58 +02:00
Patrick Schult 9decfa9c31 Merge pull request #5818 from mailcow/fix/web
[Web] fix exception handler and rspamd_maps function
2024-04-04 08:19:58 +02:00
FreddleSpl0it 3aee2b6cf5 [Web] use SEC_FETCH_DEST header instead of Referer to block api requests 2024-04-03 11:43:48 +02:00
Patrick Schult 17d797cee4 Merge pull request #5751 from mailcow/fix/rspamd-rewrite-ct
[Rspamd] milter update Content-Type and Content-Transfer-Encoding header
2024-04-03 10:49:21 +02:00
Patrick Schult 75550eeea3 Merge pull request #5812 from mailcow/limit-local-addrs
[Rspamd] Set local_addrs lo mailcow networks
2024-04-03 10:48:46 +02:00
FreddleSpl0it 0d09c86c12 [Web] fix invalid rspamd map check 2024-04-03 10:08:18 +02:00
FreddleSpl0it 2db8f482db [Web] escape html of alert messages 2024-04-03 10:07:36 +02:00
FreddleSpl0it 00d4b32a1b [Web] deny api calls from sogo 2024-04-03 10:06:43 +02:00
milkmaker 8a82bab1f3 [Web] Updated lang.tr-tr.json (#5815)
Co-authored-by: Uğurcan Albayrak <canalbayrakugur@gmail.com>
2024-04-02 18:04:30 +02:00
milkmaker 237a25e6b0 update postscreen_access.cidr (#5811) 2024-04-02 02:20:31 +02:00
milkmaker 5dc836671d [Web] Updated lang.tr-tr.json (#5813)
[Web] Updated lang.tr-tr.json

Co-authored-by: Uğurcan Albayrak <canalbayrakugur@gmail.com>
Co-authored-by: evrenkoksal <evrenkoksal@gmail.com>
2024-04-01 21:57:15 +02:00
Dmitriy Alekseev 26be1cb602 Set local_addrs in Rspamd 2024-04-01 11:28:06 +03:00
yvan-algoo dc7a48cbf9 Update French translation (#5805)
* Fix some typo in French translation

* Fix typo error introduced in last commit

* Fixed another typo introduced in my first commit
2024-03-30 01:10:12 +01:00
milkmaker 52455be815 Translations update from Weblate (#5810)
* [Web] Updated lang.lt-lt.json

[Web] Updated lang.lt-lt.json

[Web] Updated lang.lt-lt.json

[Web] Updated lang.lt-lt.json

[Web] Updated lang.lt-lt.json

[Web] Updated lang.lt-lt.json

[Web] Added lang.lt-lt.json

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

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

[Web] Updated lang.lv-lv.json

[Web] Updated lang.lv-lv.json

[Web] Updated lang.lv-lv.json

Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
Co-authored-by: Edgars Počs <edgars.pocs@dna.lv>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

[Web] Updated lang.tr-tr.json

[Web] Updated lang.tr-tr.json

[Web] Updated lang.tr-tr.json

[Web] Updated lang.tr-tr.json

[Web] Updated lang.tr-tr.json

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

* Add lt-lt in vars.inc.php

---------

Co-authored-by: Ari Archer <ari@ari.lt>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
Co-authored-by: Edgars Počs <edgars.pocs@dna.lv>
Co-authored-by: evrenkoksal <evrenkoksal@gmail.com>
2024-03-30 01:09:22 +01:00
Ayowel 5c851f2935 Allow prompt-less install on low-resource systems 2024-03-26 08:19:24 +01:00
Niklas Meyer bbbdcfb625 Merge pull request #5743 from mailcow/fix-5742
Remove one GmbH in Dockerfiles
2024-03-20 09:37:45 +01:00
Niklas Meyer b054a57e16 Merge pull request #5770 from mailcow/update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2024-03-19 10:19:54 +01:00
aaadddfgh fd73b3ad88 Update lang.zh-cn.json (#5789)
Change a better translation
2024-03-13 15:53:37 +01:00
milkmaker 8c0637b556 [Web] Updated lang.lv-lv.json (#5777)
Co-authored-by: Edgars Andersons <Edgars+Mailcow+Weblate@gaitenis.id.lv>
2024-03-05 17:57:55 +01:00
Marcel Schuster 914a8204d4 Watchdog: escape subject and body for webhooks 2024-03-01 23:07:05 +01:00
DerLinkman d92ffe8fc7 helper: remove old SOGo repo to not break builds on ARM64 2024-03-01 11:41:11 +01:00
milkmaker e0eb3a4f13 update postscreen_access.cidr 2024-03-01 00:14:54 +00:00
Niklas Meyer 1fb0060a73 Merge pull request #5765 from mailcow/feat/sogo-5.10
sogo: upgrade to 5.10.0
2024-02-27 08:22:19 +01:00
DerLinkman d7430bf516 sogo: add new options to sogo.conf for update 5.10.0 2024-02-26 17:17:34 +01:00
DerLinkman 35f039a119 sogo: update to 5.10.0 2024-02-26 16:55:13 +01:00
milkmaker 79432a40d7 Translations update from Weblate (#5762)
* [Web] Updated lang.es-es.json

Co-authored-by: Fernando Dilland <fernandodilland@gmail.com>

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

Co-authored-by: Christer Solstrand Johannessen <csjoh@users.noreply.translate.mailcow.email>

---------

Co-authored-by: Fernando Dilland <fernandodilland@gmail.com>
Co-authored-by: Christer Solstrand Johannessen <csjoh@users.noreply.translate.mailcow.email>
2024-02-25 19:51:57 +01:00
FreddleSpl0it 98cdb95bc0 [Rspamd] milter update Content-Type and Content-Transfer-Encoding header after need_rewrite_ct 2024-02-19 11:20:19 +01:00
Hailer, Christian 02a55ce9db Fix unbound healthcheck.sh to log all messages to logfile 2024-02-19 09:26:29 +01:00
renovate[bot] 6f4720e1ea chore(deps): update thollander/actions-comment-pull-request action to v2.5.0 (#5747) 2024-02-17 11:42:30 +01:00
Peter 6a807b7799 Remove one GmbH 2024-02-15 17:43:01 +01:00
Patrick Schult 8d4ef147d2 Merge pull request #5741 from mailcow/staging
2024-02
2024-02-15 11:27:09 +01:00
milkmaker 8ed6217d1c Translations update from Weblate (#5740)
* [Web] Language file updated by 'Cleanup translation files' addon

[Web] Updated lang.it-it.json

ui: fixed broken Links to docs

ui: fix wrong docs links

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
Co-authored-by: Michele Caputo <michele@caputoweb.xyz>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

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

---------

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
Co-authored-by: Michele Caputo <michele@caputoweb.xyz>
2024-02-14 20:11:51 +01:00
milkmaker 7dae4a976d Translations update from Weblate (#5732)
* ui: fix wrong docs links

* ui: fixed broken Links to docs

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

[Web] Updated lang.nb-no.json

[Web] Updated lang.nb-no.json

[Web] Added lang.nb-no.json

Co-authored-by: Christer Solstrand Johannessen <csjoh@users.noreply.translate.mailcow.email>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

[Web] Updated lang.fr-fr.json

Co-authored-by: Alix ANNERAUD <alix.anneraud@outlook.fr>
Co-authored-by: William Blondel <contact@williamblondel.fr>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

Co-authored-by: Michele Caputo <michele@caputoweb.xyz>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

Co-authored-by: BallBill <BallBill@users.noreply.translate.mailcow.email>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

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

[Web] Updated lang.pt-br.json

[Web] Updated lang.pt-br.json

[Web] Updated lang.pt-br.json

Co-authored-by: Abner Santana <abnerss@outlook.com>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* Add Norwegian in vars.inc.php

---------

Co-authored-by: DerLinkman <niklas.meyer@servercow.de>
Co-authored-by: Christer Solstrand Johannessen <csjoh@users.noreply.translate.mailcow.email>
Co-authored-by: Peter <magic@kthx.at>
Co-authored-by: Alix ANNERAUD <alix.anneraud@outlook.fr>
Co-authored-by: William Blondel <contact@williamblondel.fr>
Co-authored-by: Michele Caputo <michele@caputoweb.xyz>
Co-authored-by: BallBill <BallBill@users.noreply.translate.mailcow.email>
Co-authored-by: Abner Santana <abnerss@outlook.com>
2024-02-14 20:09:10 +01:00
FreddleSpl0it 3b83949ba3 [Netfilter] Update to 1.58 2024-02-14 13:58:07 +01:00
Patrick Schult d8baadb991 Merge pull request #5679 from Habetdin/staging
[Netfilter] respect ban time limits
2024-02-14 11:37:24 +01:00
Patrick Schult 7d3f9fa407 Merge pull request #5727 from mailcow/fix/domain-wide-footer
[Rspamd] apply domain wide footer to alias domains
2024-02-14 09:43:04 +01:00
Patrick Schult 705d144a85 Merge pull request #5729 from mailcow/feat/readable-domainnames
[Web] display human readable domainnames instead of punycode
2024-02-14 09:36:20 +01:00
Patrick Schult ff05cff36c Merge pull request #5730 from mailcow/fix/add-domain-gal
[Web] fix setting unchecked checkboxes on domain adding
2024-02-14 09:34:21 +01:00
Patrick Schult 861fa7b145 Merge pull request #5728 from mailcow/fix/debug-tz
[Web] fix blank /debug page with invalid timezone
2024-02-14 09:32:17 +01:00
FreddleSpl0it d65a0bba44 [ClamAV] Update to 1.2.2 2024-02-13 09:16:38 +01:00
FreddleSpl0it dac1bd88dc [Web] fix setting unchecked checkboxes 2024-02-09 15:17:02 +01:00
FreddleSpl0it 288dbfa37c [Web] display human readable domainnames instead of punycode 2024-02-09 15:13:45 +01:00
FreddleSpl0it a0e55cb9b1 [Web] fix blank /debug page with invalid timezone 2024-02-09 15:08:21 +01:00
FreddleSpl0it 86ba019ca0 [Rspamd] apply domain wide footer to alias domains 2024-02-09 14:59:14 +01:00
q16marvin 19deda31bc Update functions.mailbox.inc.php 2024-02-09 11:23:47 +01:00
q16marvin 4f47534824 Update mailbox.js 2024-02-09 11:23:09 +01:00
DerLinkman 3cb9c2ece5 ui: fix wrong docs links
ui: fixed broken Links to docs
2024-02-09 08:11:20 +01:00
Habetdin 1787c53d98 [Netfilter] respect ban time limits 2024-02-09 01:57:09 +03:00
Niklas Meyer 8ae762a8c8 Merge pull request #5717 from mailcow/staging
2024-01e
2024-02-08 15:58:47 +01:00
DerLinkman 63426c3cd0 unbound: remove netcat check & package 2024-02-08 15:55:26 +01:00
DerLinkman e184713c67 added action for support label in issues 2024-02-08 13:06:02 +01:00
Niklas Meyer 1926625297 Merge pull request #5711 from amorfo77/master
[Netfilter] set IP check more relaxed on NFTables.py
2024-02-08 12:36:03 +01:00
DerLinkman 63bb8e8cef unbound: increase check interval to 30s 2024-02-08 12:23:46 +01:00
DerLinkman 583c5b48a0 dovecot: bump to docker image 1.28.1 2024-02-07 17:29:36 +01:00
DerLinkman d08ccbce78 dovecot: fix wrong timestamps inside logs 2024-02-07 17:28:49 +01:00
DerLinkman 5a9702771c [SOGo] Fixed SOGo crash on older kernels < 5.10.0-X 2024-02-07 17:18:20 +01:00
vicente eb91d9905b fix typpo in chain order message 2024-02-07 15:48:49 +01:00
vicente 38cc85fa4c set strict=False 2024-02-07 15:36:04 +01:00
FreddleSpl0it 77e6ef218c [Netfilter] Update to 1.57 2024-02-05 09:54:16 +01:00
FreddleSpl0it 464b6f2e93 [Netfilter] fix redis logs 2024-02-05 09:47:19 +01:00
Niklas Meyer 20c90642f9 Merge pull request #5700 from mailcow/staging
[Netfilter] fix mailcow isolation rule for iptables
2024-02-02 17:49:49 +01:00
FreddleSpl0it 57e67ea8f7 [Netfilter] fix mailcow isolation rule for iptables 2024-02-02 17:40:44 +01:00
Niklas Meyer c9e9628383 Merge pull request #5699 from mailcow/staging
2024-01d
2024-02-02 17:08:45 +01:00
DerLinkman 909f07939e dovecot: bump version for repl fix 2024-02-02 17:06:31 +01:00
FreddleSpl0it a310493485 [Dovecot] fix repl_health.sh 2024-02-02 16:52:41 +01:00
Niklas Meyer 1e09df20b6 Merge pull request #5689 from mailcow/staging
2024-01c
2024-02-02 15:52:33 +01:00
Patrick Schult 087481ac12 Merge pull request #5696 from mailcow/fix/netfilter
[Netfilter] add mailcow isolation rule to MAILCOW chain
2024-02-02 14:33:01 +01:00
FreddleSpl0it c941e802d4 [Netfilter] only perform cleanup at exit if SIGTERM was recieved 2024-02-02 12:57:21 +01:00
FreddleSpl0it 39589bd441 [Netfilter] only perform cleanup at exit if SIGTERM was recieved 2024-02-02 12:46:50 +01:00
DerLinkman 2e57325dde docker-compose.yml: Bump dovecot + netfilter version 2024-02-02 11:27:46 +01:00
FreddleSpl0it 2072301d89 [Netfilter] only perform cleanup at exit if SIGTERM was recieved 2024-02-02 11:08:44 +01:00
FreddleSpl0it b236fd3ac6 [Netfilter] add mailcow isolation rule to MAILCOW chain
[Netfilter] add mailcow rule to docker-user chain

[Netfilter] add mailcow isolation rule to MAILCOW chain

[Netfilter] add mailcow isolation rule to MAILCOW chain

[Netfilter] set mailcow isolation rule before redis

[Netfilter] clear bans in redis after connecting

[Netfilter] simplify mailcow isolation rule for compatibility with iptables-nft

[Netfilter] stop container after mariadb, redis, dovecot, solr

[Netfilter] simplify mailcow isolation rule for compatibility with iptables-nft

[Netfilter] add exception for mailcow isolation rule for HA setups

[Netfilter] add exception for mailcow isolation rule for HA setups

[Netfilter] add DISABLE_NETFILTER_ISOLATION_RULE

[Netfilter] fix wrong var name

[Netfilter] add DISABLE_NETFILTER_ISOLATION_RULE to update and generate_config sh
2024-02-02 10:10:11 +01:00
Niklas Meyer b968695e31 Merge pull request #5686 from mailcow/update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2024-02-01 08:58:35 +01:00
Niklas Meyer 694f1d1623 Merge pull request #5688 from mailcow/fix/sogo-authenticated-users
sogo: fix ACL allow authenticated users + rebuild on Bookworm
2024-02-01 08:42:53 +01:00
DerLinkman 93e4d58606 sogo: fix ACL allow authenticated users + rebuild on Bookworm 2024-02-01 08:41:11 +01:00
milkmaker cc77caad67 update postscreen_access.cidr 2024-02-01 00:13:56 +00:00
renovate[bot] f74573f5d0 chore(deps): update peter-evans/create-pull-request action to v6 (#5683)
Signed-off-by: milkmaker <milkmaker@mailcow.de>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-31 16:14:42 +01:00
DerLinkman deb6f0babc issue: added architecture as dropdown 2024-01-23 08:46:06 +01:00
Niklas Meyer cb978136bd Merge pull request #5663 from mailcow/staging
2024-01b
2024-01-22 11:50:41 +01:00
Niklas Meyer 1159450cc4 Merge pull request #5662 from mailcow/fix/rollback-curl-bug
fix: rollback curl bug
2024-01-22 11:39:27 +01:00
DerLinkman a0613e4b10 fix: rollback of Alpine 3.19 were possible 2024-01-22 11:26:26 +01:00
Niklas Meyer 68989f0a45 Merge pull request #5647 from Candinya/patch-1
fix: watchdog webhook body variables injector
2024-01-22 10:34:06 +01:00
DerLinkman 7da5e3697e compose: bump watchdog version 2024-01-22 10:32:01 +01:00
Nya Candy 6e7a0eb662 fix: watchdog webhook body variables injector 2024-01-22 10:32:01 +01:00
Niklas Meyer b25ac855ca Merge pull request #5660 from luminem/openrc-support
Test for openrc configuration file instead of alpine
2024-01-22 10:27:29 +01:00
Niklas Meyer 3e02dcbb95 Merge pull request #5652 from KagurazakaNyaa/master
Allow user skip unbound healthcheck
2024-01-22 10:25:50 +01:00
DerLinkman 53be119e39 compose: bump unbound version 2024-01-22 10:22:24 +01:00
Luca Barbato 25bdc4c9ed Test for openrc configuration file instead of alpine
This way other distro using openrc can be supported.
2024-01-22 09:50:24 +01:00
KagurazakaNyaa 9d4055fc4d add parameter SKIP_UNBOUND_HEALTHCHECK to old installations 2024-01-19 00:07:51 +08:00
KagurazakaNyaa d2edf359ac update config comment 2024-01-18 23:53:08 +08:00
KagurazakaNyaa aa1d92dfbb add SKIP_UNBOUND_HEALTHCHECK to docker-compose.yml 2024-01-18 23:50:26 +08:00
KagurazakaNyaa b89d71e6e4 change variable name 2024-01-18 23:48:59 +08:00
KagurazakaNyaa ed493f9c3a Allow user skip unbound healthcheck 2024-01-18 23:28:03 +08:00
Niklas Meyer 76f8a5b7de Merge pull request #5650 from mailcow/staging
unbound: increased healthcheck timeout
2024-01-18 11:56:09 +01:00
DerLinkman cb3bc207b9 unbound: increased healthcheck timeout 2024-01-18 11:55:01 +01:00
Niklas Meyer b5db5dd0b4 Merge pull request #5642 from mailcow/staging
2024-01
2024-01-17 13:51:40 +01:00
FreddleSpl0it 90a7cff2c9 [Rspamd] check if footer.skip_replies is not 0 2024-01-17 12:05:51 +01:00
FreddleSpl0it cc3adbe78c [Web] fix datatables ssp queries 2024-01-17 12:04:01 +01:00
Niklas Meyer bd6a7210b7 Merge pull request #5523 from FELDSAM-INC/feldsam/datatables-ssp
Implemented Server Side processing for domains and mailboxes datatables
2024-01-17 10:23:05 +01:00
Niklas Meyer 905a202873 Merge pull request #5587 from mailcow/feat/arm64
mailcow Multiarch (x86 and ARM64) support
2024-01-17 10:18:06 +01:00
DerLinkman accedf0280 Updated mailcow Components to be ARM64 compatible 2024-01-17 10:14:36 +01:00
FreddleSpl0it 99d9a2eacd [Web] fix mailbox and domain creation 2024-01-17 09:52:43 +01:00
Kristian Feldsam ac4f131fa8 Domains and Mailboxes datatable - server side processing - filtering by tags
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2024-01-16 15:03:28 +01:00
FreddleSpl0it 7f6f7e0e9f [Web] limit logo file upload 2024-01-15 16:34:47 +01:00
Niklas Meyer 43bb26f28c Merge pull request #5639 from mailcow/feat/unbound-healthcheck-rewrite
unbound: rewrote of healthcheck
2024-01-15 15:57:18 +01:00
DerLinkman b29dc37991 unbound: rewrote healthcheck to be more detailed
unbound: added comments to rewritten healthcheck
2024-01-15 15:17:28 +01:00
DerLinkman cf9f02adbb ui: fix alignment secondary 2024-01-10 14:43:59 +01:00
Tomasz Orzechowski 6dc0bdbfa3 Proper number of threads regex. 2024-01-09 22:03:24 +01:00
DerLinkman b5a1a18b04 lang: fixed totp langs 2024-01-09 12:20:30 +01:00
Niklas Meyer b4eeb0ffae Merge pull request #5522 from mailcow/renovate/krakjoe-apcu-5.x
chore(deps): update dependency krakjoe/apcu to v5.1.23
2024-01-09 12:06:12 +01:00
Niklas Meyer 48549ead7f Merge pull request #5549 from mailcow/renovate/phpredis-phpredis-6.x
chore(deps): update dependency phpredis/phpredis to v6.0.2
2024-01-09 12:04:41 +01:00
Niklas Meyer 01b0ad0fd9 Merge pull request #5550 from mailcow/renovate/tianon-gosu-1.x
chore(deps): update dependency tianon/gosu to v1.17
2024-01-09 12:04:21 +01:00
Niklas Meyer 2b21501450 Merge pull request #5581 from mailcow/renovate/composer-composer-2.x
chore(deps): update dependency composer/composer to v2.6.6
2024-01-09 12:03:08 +01:00
Niklas Meyer b491f6af9b Merge pull request #5615 from mailcow/fix/default-values
[Web] use template for default values in mbox and domain creation
2024-01-09 12:01:24 +01:00
Niklas Meyer 942ef7c254 Merge pull request #5592 from mailcow/feat/alpine-3.19
Update Dockerfiles to Alpine 3.19
2024-01-09 11:57:34 +01:00
DerLinkman 1ee3bb42f3 compose: updated image tags 2024-01-09 11:55:32 +01:00
DerLinkman 25007b1963 dockerapi: implemented lifespan function 2024-01-09 11:50:22 +01:00
DerLinkman f442378377 dockerfiles: updated maintainer 2024-01-09 11:18:55 +01:00
DerLinkman 333b7ebc0c Fix Alpine 3.19 dependencies 2024-01-09 11:17:52 +01:00
Peter 5896766fc3 Update to Alpine 3.19 2024-01-09 11:17:51 +01:00
Niklas Meyer 89540aec28 Merge pull request #5612 from mailcow/feat/domain-wide-footer
[Rspamd] add option to skip domain wide footer on reply e-mails
2024-01-09 11:10:35 +01:00
DerLinkman b960143045 translation: update de-de.json 2024-01-09 11:09:35 +01:00
DerLinkman 6ab45cf668 db: bumped version to newer timestamp 2024-01-08 14:43:25 +01:00
Niklas Meyer fd206a7ef6 Merge pull request #5621 from mailcow/align-ehlo-keywords-to-fuctions
[Postfix] Remove pipeling from ehlo keywords as we block it in data
2024-01-08 09:52:28 +01:00
Niklas Meyer 1c7347d38d Merge pull request #5616 from FELDSAM-INC/feldsam/fix-form-dark-mode
Fixed bg color of form elements in dark mode
2024-01-08 09:51:48 +01:00
Niklas Meyer 7f58c422f2 Merge pull request #5625 from mailcow/update/postscreen_access.cidr
[Postfix] update postscreen_access.cidr
2024-01-08 09:51:27 +01:00
Niklas Meyer 0a0e2b5e93 Merge pull request #5624 from mthld/patch-2
Add new SOGoMailHideInlineAttachments option to sogo.conf
2024-01-08 09:47:50 +01:00
milkmaker de00c424f4 update postscreen_access.cidr 2024-01-01 00:15:27 +00:00
Mathilde a249e2028d Add new SOGoMailHideInlineAttachments option to sogo.conf
SOGoMailHideInlineAttachments = YES; will allow to hide inline (body and footer) images being shown as attachments.
2023-12-30 10:16:25 +01:00
Dmitriy Alekseev 68036eeccf Update main.cf 2023-12-29 22:06:18 +02:00
Dmitriy Alekseev b4bb11320f Update main.cf 2023-12-29 16:04:52 +02:00
Dmitriy Alekseev c61938db23 [Postfix] Remove pipeling from ehlo keywords as we block it in data restrictions 2023-12-29 15:59:16 +02:00
Kristian Feldsam c24543fea0 [Web] Fixed form fields bg color in dark mode
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-12-27 17:33:12 +01:00
FreddleSpl0it 38497b04ac [Web] use template for default values in mbox and domain creation 2023-12-27 14:57:27 +01:00
FreddleSpl0it efab11720d add option to skip footer on reply e-mails 2023-12-22 10:39:07 +01:00
renovate[bot] 40fdf99a55 Update dependency composer/composer to v2.6.6
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-12-08 20:07:11 +00:00
Kristian Feldsam efcca61f5a Mailboxes datatable - server side processing ordering
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-12-04 14:52:17 +01:00
Kristian Feldsam 4dad0002cd Domains datatable - server side processing ordering
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-12-04 14:15:57 +01:00
renovate[bot] d4dd1e37ce Update dependency tianon/gosu to v1.17
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-11-21 09:03:09 +00:00
renovate[bot] a8dfa95126 Update dependency phpredis/phpredis to v6.0.2
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-11-21 09:03:02 +00:00
renovate[bot] 4f109c1a94 Update dependency krakjoe/apcu to v5.1.23
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2023-11-12 17:28:57 +00:00
Kristian Feldsam 28cec99699 Mailboxes datatable - server side processing
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-11-12 10:35:26 +01:00
Kristian Feldsam 3e194c7906 Domains datatable - server side processing
Signed-off-by: Kristian Feldsam <feldsam@gmail.com>
2023-11-12 10:35:22 +01:00
337 changed files with 15020 additions and 5880 deletions
+1
View File
@@ -1 +1,2 @@
github: mailcow
custom: ["https://www.servercow.de/mailcow?lang=en#sal"] custom: ["https://www.servercow.de/mailcow?lang=en#sal"]
+10
View File
@@ -62,6 +62,16 @@ body:
- nightly - nightly
validations: validations:
required: true required: true
- type: dropdown
attributes:
label: "Which architecture are you using?"
description: "#### `uname -m`"
multiple: false
options:
- x86
- ARM64 (aarch64)
validations:
required: true
- type: input - type: input
attributes: attributes:
label: "Operating System:" label: "Operating System:"
@@ -1,13 +1,3 @@
## :memo: Brief description
<!-- Diff summary - START -->
<!-- Diff summary - END -->
## :computer: Commits
<!-- Diff commits - START -->
<!-- Diff commits - END -->
## :file_folder: Modified files ## :file_folder: Modified files
<!-- Diff files - START --> <!-- Diff files - START -->
<!-- Diff files - END --> <!-- Diff files - END -->
+38
View File
@@ -0,0 +1,38 @@
<!-- _Please make sure to review and check all of these items, otherwise we might refuse your PR:_ -->
## Contribution Guidelines
* [ ] I've read the [contribution guidelines](https://github.com/mailcow/mailcow-dockerized/blob/master/CONTRIBUTING.md) and wholeheartedly agree them
<!-- _NOTE: this tickbox is needed to fullfil on order to get your PR reviewed._ -->
## What does this PR include?
### Short Description
<!-- Please write a short description, what your PR does here. -->
### Affected Containers
<!-- Please list all affected Docker containers here, which you commited changes to -->
<!--
Please list them like this:
- container1
- container2
- container3
etc.
-->
## Did you run tests?
### What did you tested?
<!-- Please write shortly, what you've tested (which components etc.). -->
### What were the final results? (Awaited, got)
<!-- Please write shortly, what your final tests results were. What did you awaited? Was the outcome the awaited one? -->
@@ -0,0 +1,37 @@
name: Check if labeled support, if so send message and close issue
on:
issues:
types:
- labeled
jobs:
add-comment:
if: github.event.label.name == 'support'
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Add comment
run: gh issue comment "$NUMBER" --body "$BODY"
env:
GH_TOKEN: ${{ secrets.SUPPORTISSUES_ACTION_PAT }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
BODY: |
**THIS IS A AUTOMATED MESSAGE!**
It seems your issue is not a bug.
Therefore we highly advise you to get support!
You can get support either by:
- ordering a paid [support contract at Servercow](https://www.servercow.de/mailcow?lang=en#support/) (Directly from the developers) or
- using the [community forum](https://community.mailcow.email) (**Based on volunteers! NO guaranteed answer**) or
- using the [Telegram support channel](https://t.me/mailcow) (**Based on volunteers! NO guaranteed answer**)
This issue will be closed. If you think your reported issue is not a support case feel free to comment above and if so the issue will reopened.
- name: Close issue
env:
GH_TOKEN: ${{ secrets.SUPPORTISSUES_ACTION_PAT }}
GH_REPO: ${{ github.repository }}
NUMBER: ${{ github.event.issue.number }}
run: gh issue close "$NUMBER" -r "not planned"
@@ -10,7 +10,7 @@ jobs:
if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging if: github.event.pull_request.base.ref != 'staging' #check if the target branch is not staging
steps: steps:
- name: Send message - name: Send message
uses: thollander/actions-comment-pull-request@v2.4.3 uses: thollander/actions-comment-pull-request@v3.0.1
with: with:
GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }} GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }}
message: | message: |
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }} password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
@@ -22,7 +22,7 @@ jobs:
bash helper-scripts/update_postscreen_whitelist.sh bash helper-scripts/update_postscreen_whitelist.sh
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@v5 uses: peter-evans/create-pull-request@v7
with: with:
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }} token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
commit-message: update postscreen_access.cidr commit-message: update postscreen_access.cidr
+2
View File
@@ -13,6 +13,7 @@ data/conf/dovecot/acl_anyone
data/conf/dovecot/dovecot-master.passwd data/conf/dovecot/dovecot-master.passwd
data/conf/dovecot/dovecot-master.userdb data/conf/dovecot/dovecot-master.userdb
data/conf/dovecot/extra.conf data/conf/dovecot/extra.conf
data/conf/dovecot/mail_replica.conf
data/conf/dovecot/global_sieve_* data/conf/dovecot/global_sieve_*
data/conf/dovecot/last_login data/conf/dovecot/last_login
data/conf/dovecot/lua data/conf/dovecot/lua
@@ -44,6 +45,7 @@ data/conf/rspamd/override.d/*
data/conf/sogo/custom-theme.js data/conf/sogo/custom-theme.js
data/conf/sogo/plist_ldap data/conf/sogo/plist_ldap
data/conf/sogo/sieve.creds data/conf/sogo/sieve.creds
data/conf/sogo/cron.creds
data/conf/sogo/sogo-full.svg data/conf/sogo/sogo-full.svg
data/gitea/ data/gitea/
data/gogs/ data/gogs/
+34 -15
View File
@@ -1,33 +1,52 @@
# Contribution Guidelines (Last modified on 18th December 2023) # Contribution Guidelines
**_Last modified on 15th August 2024_**
First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow! First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow!
## Pull Requests (Last modified on 18th December 2023) As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly.
**PLEASE NOTE, THAT WE MIGHT CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULLFIL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request.
## Topics
- [Pull Requests](#pull-requests)
- [Issue Reporting](#issue-reporting)
- [Guidelines](#issue-reporting-guidelines)
- [Issue Report Guide](#issue-report-guide)
## Pull Requests
**_Last modified on 15th August 2024_**
However, please note the following regarding pull requests: However, please note the following regarding pull requests:
1. **ALWAYS** create your PR using the staging branch of your locally cloned mailcow instance, as the pull request will end up in said staging branch of mailcow once approved. Ideally, you should simply create a new branch for your pull request that is named after the type of your PR (e.g. `feat/` for function updates or `fix/` for bug fixes) and the actual content (e.g. `sogo-6.0.0` for an update from SOGo to version 6 or `html-escape` for a fix that includes escaping HTML in mailcow). 1. **ALWAYS** create your PR using the staging branch of your locally cloned mailcow instance, as the pull request will end up in said staging branch of mailcow once approved. Ideally, you should simply create a new branch for your pull request that is named after the type of your PR (e.g. `feat/` for function updates or `fix/` for bug fixes) and the actual content (e.g. `sogo-6.0.0` for an update from SOGo to version 6 or `html-escape` for a fix that includes escaping HTML in mailcow).
2. Please **keep** this pull request branch **clean** and free of commits that have nothing to do with the changes you have made (e.g. commits from other users from other branches). *If you make changes to the `update.sh` script or other scripts that trigger a commit, there is usually a developer mode for clean working in this case. 2. **ALWAYS** report/request issues/features in the english language, even though mailcow is a german based company. This is done to allow other GitHub users to reply to your issues/requests too which did not speak german or other languages besides english.
3. **Test your changes before you commit them as a pull request.** <ins>If possible</ins>, write a small **test log** or demonstrate the functionality with a **screenshot or GIF**. *We will of course also test your pull request ourselves, but proof from you will save us the question of whether you have tested your own changes yourself.* 3. Please **keep** this pull request branch **clean** and free of commits that have nothing to do with the changes you have made (e.g. commits from other users from other branches). *If you make changes to the `update.sh` script or other scripts that trigger a commit, there is usually a developer mode for clean working in this case.*
4. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.* 4. **Test your changes before you commit them as a pull request.** <ins>If possible</ins>, write a small **test log** or demonstrate the functionality with a **screenshot or GIF**. *We will of course also test your pull request ourselves, but proof from you will save us the question of whether you have tested your own changes yourself.*
5. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project. 5. **Please use** the pull request template we provide once creating a pull request. *HINT: During editing you encounter comments which looks like: `<!-- CONTENT -->`. These can be removed or kept, as they will not rendered later on GitHub! Please only create actual content without the said comments.*
6. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort! 6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.*
7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project.
8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort!
--- ---
## Issue Reporting (Last modified on 18th December 2023) ## Issue Reporting
**_Last modified on 15th August 2024_**
If you plan to report a issue within mailcow please read and understand the following rules: If you plan to report a issue within mailcow please read and understand the following rules:
### Issue Reporting Guidelines
1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support). 1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support).
2. **ONLY** report an error if you have the **necessary know-how (at least the basics)** for the administration of an e-mail server and the usage of Docker. mailcow is a complex and fully-fledged e-mail server including groupware components on a Docker basement and it requires a bit of technical know-how for debugging and operating. 2. **ONLY** report an error if you have the **necessary know-how (at least the basics)** for the administration of an e-mail server and the usage of Docker. mailcow is a complex and fully-fledged e-mail server including groupware components on a Docker basement and it requires a bit of technical know-how for debugging and operating.
3. **ONLY** report bugs that are contained in the latest mailcow release series. *The definition of the latest release series includes the last major patch (e.g. 2023-12) and all minor patches (revisions) below it (e.g. 2023-12a, b, c etc.).* New issue reports published starting from January 1, 2024 must meet this criterion, as versions below the latest releases are no longer supported by us. 3. **ALWAYS** report/request issues/features in the english language, even though mailcow is a german based company. This is done to allow other GitHub users to reply to your issues/requests too which did not speak german or other languages besides english.
4. When reporting a problem, please be as detailed as possible and include even the smallest changes to your mailcow installation. Simply fill out the corresponding bug report form in detail and accurately to minimize possible questions. 4. **ONLY** report bugs that are contained in the latest mailcow release series. *The definition of the latest release series includes the last major patch (e.g. 2023-12) and all minor patches (revisions) below it (e.g. 2023-12a, b, c etc.).* New issue reports published starting from January 1, 2024 must meet this criterion, as versions below the latest releases are no longer supported by us.
5. **Before you open an issue/feature request**, please first check whether a similar request already exists in the mailcow tracker on GitHub. If so, please include yourself in this request. 5. When reporting a problem, please be as detailed as possible and include even the smallest changes to your mailcow installation. Simply fill out the corresponding bug report form in detail and accurately to minimize possible questions.
6. When you create a issue/feature request: Please note that the creation does <ins>**not guarantee an instant implementation or fix by the mailcow team or the community**</ins>. 6. **Before you open an issue/feature request**, please first check whether a similar request already exists in the mailcow tracker on GitHub. If so, please include yourself in this request.
7. Please **ALWAYS** anonymize any sensitive information in your bug report or feature request before submitting it. 7. When you create a issue/feature request: Please note that the creation does <ins>**not guarantee an instant implementation or fix by the mailcow team or the community**</ins>.
8. Please **ALWAYS** anonymize any sensitive information in your bug report or feature request before submitting it.
### Quick guide to reporting problems: ### Issue Report Guide
1. Read your logs; follow them to see what the reason for your problem is. 1. Read your logs; follow them to see what the reason for your problem is.
2. Follow the leads given to you in your logfiles and start investigating. 2. Follow the leads given to you in your logfiles and start investigating.
3. Restarting the troubled service or the whole stack to see if the problem persists. 3. Restarting the troubled service or the whole stack to see if the problem persists.
@@ -36,4 +55,4 @@ If you plan to report a issue within mailcow please read and understand the foll
6. [Create an issue](https://github.com/mailcow/mailcow-dockerized/issues/new/choose) over at our GitHub repository if you think your problem might be a bug or a missing feature you badly need. But please make sure, that you include **all the logs** and a full description to your problem. 6. [Create an issue](https://github.com/mailcow/mailcow-dockerized/issues/new/choose) over at our GitHub repository if you think your problem might be a bug or a missing feature you badly need. But please make sure, that you include **all the logs** and a full description to your problem.
7. Ask your questions in our community-driven [support channels](https://docs.mailcow.email/#community-support-and-chat). 7. Ask your questions in our community-driven [support channels](https://docs.mailcow.email/#community-support-and-chat).
## When creating an issue/feature request or a pull request, you will be asked to confirm these guidelines. ## When creating an issue/feature request or a pull request, you will be asked to confirm these guidelines.
+4 -5
View File
@@ -1,6 +1,7 @@
FROM alpine:3.17 FROM alpine:3.20
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
RUN apk upgrade --no-cache \ RUN apk upgrade --no-cache \
&& apk add --update --no-cache \ && apk add --update --no-cache \
@@ -14,9 +15,7 @@ RUN apk upgrade --no-cache \
tini \ tini \
tzdata \ tzdata \
python3 \ python3 \
py3-pip \ acme-tiny --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/
&& pip3 install --upgrade pip \
&& pip3 install acme-tiny
COPY acme.sh /srv/acme.sh COPY acme.sh /srv/acme.sh
COPY functions.sh /srv/functions.sh COPY functions.sh /srv/functions.sh
+11 -3
View File
@@ -33,6 +33,10 @@ if [[ "${ONLY_MAILCOW_HOSTNAME}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
ONLY_MAILCOW_HOSTNAME=y ONLY_MAILCOW_HOSTNAME=y
fi fi
if [[ "${AUTODISCOVER_SAN}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
AUTODISCOVER_SAN=y
fi
# Request individual certificate for every domain # Request individual certificate for every domain
if [[ "${ENABLE_SSL_SNI}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then if [[ "${ENABLE_SSL_SNI}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
ENABLE_SSL_SNI=y ENABLE_SSL_SNI=y
@@ -113,13 +117,13 @@ fi
chmod 600 ${ACME_BASE}/key.pem chmod 600 ${ACME_BASE}/key.pem
log_f "Waiting for database..." log_f "Waiting for database..."
while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent > /dev/null; do while ! /usr/bin/mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent > /dev/null; do
sleep 2 sleep 2
done done
log_f "Database OK" log_f "Database OK"
log_f "Waiting for Nginx..." log_f "Waiting for Nginx..."
until $(curl --output /dev/null --silent --head --fail http://nginx:8081); do until $(curl --output /dev/null --silent --head --fail http://nginx.${COMPOSE_PROJECT_NAME}_mailcow-network:8081); do
sleep 2 sleep 2
done done
log_f "Nginx OK" log_f "Nginx OK"
@@ -133,7 +137,7 @@ log_f "Resolver OK"
# Waiting for domain table # Waiting for domain table
log_f "Waiting for domain table..." log_f "Waiting for domain table..."
while [[ -z ${DOMAIN_TABLE} ]]; do while [[ -z ${DOMAIN_TABLE} ]]; do
curl --silent http://nginx/ >/dev/null 2>&1 curl --silent http://nginx.${COMPOSE_PROJECT_NAME}_mailcow-network/ >/dev/null 2>&1
DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs) DOMAIN_TABLE=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SHOW TABLES LIKE 'domain'" -Bs)
[[ -z ${DOMAIN_TABLE} ]] && sleep 10 [[ -z ${DOMAIN_TABLE} ]] && sleep 10
done done
@@ -211,7 +215,11 @@ while true; do
ADDITIONAL_SAN_ARR+=($i) ADDITIONAL_SAN_ARR+=($i)
fi fi
done done
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
# Fetch certs for autoconfig and autodiscover subdomains
ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig') ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig')
fi
if [[ ${SKIP_IP_CHECK} != "y" ]]; then if [[ ${SKIP_IP_CHECK} != "y" ]]; then
# Start IP detection # Start IP detection
@@ -2,32 +2,32 @@
# Reading container IDs # Reading container IDs
# Wrapping as array to ensure trimmed content when calling $NGINX etc. # Wrapping as array to ensure trimmed content when calling $NGINX etc.
NGINX=($(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"nginx-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) NGINX=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"nginx-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
DOVECOT=($(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"dovecot-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) DOVECOT=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"dovecot-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
POSTFIX=($(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) POSTFIX=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " "))
reload_nginx(){ reload_nginx(){
echo "Reloading Nginx..." echo "Reloading Nginx..."
NGINX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi/containers/${NGINX}/exec -d '{"cmd":"reload", "task":"nginx"}' --silent -H 'Content-type: application/json' | jq -r .type) NGINX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${NGINX}/exec -d '{"cmd":"reload", "task":"nginx"}' --silent -H 'Content-type: application/json' | jq -r .type)
[[ ${NGINX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Nginx, restarting container..."; restart_container ${NGINX} ; } [[ ${NGINX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Nginx, restarting container..."; restart_container ${NGINX} ; }
} }
reload_dovecot(){ reload_dovecot(){
echo "Reloading Dovecot..." echo "Reloading Dovecot..."
DOVECOT_RELOAD_RET=$(curl -X POST --insecure https://dockerapi/containers/${DOVECOT}/exec -d '{"cmd":"reload", "task":"dovecot"}' --silent -H 'Content-type: application/json' | jq -r .type) DOVECOT_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${DOVECOT}/exec -d '{"cmd":"reload", "task":"dovecot"}' --silent -H 'Content-type: application/json' | jq -r .type)
[[ ${DOVECOT_RELOAD_RET} != 'success' ]] && { echo "Could not reload Dovecot, restarting container..."; restart_container ${DOVECOT} ; } [[ ${DOVECOT_RELOAD_RET} != 'success' ]] && { echo "Could not reload Dovecot, restarting container..."; restart_container ${DOVECOT} ; }
} }
reload_postfix(){ reload_postfix(){
echo "Reloading Postfix..." echo "Reloading Postfix..."
POSTFIX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi/containers/${POSTFIX}/exec -d '{"cmd":"reload", "task":"postfix"}' --silent -H 'Content-type: application/json' | jq -r .type) POSTFIX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/exec -d '{"cmd":"reload", "task":"postfix"}' --silent -H 'Content-type: application/json' | jq -r .type)
[[ ${POSTFIX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Postfix, restarting container..."; restart_container ${POSTFIX} ; } [[ ${POSTFIX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Postfix, restarting container..."; restart_container ${POSTFIX} ; }
} }
restart_container(){ restart_container(){
for container in $*; do for container in $*; do
echo "Restarting ${container}..." echo "Restarting ${container}..."
C_REST_OUT=$(curl -X POST --insecure https://dockerapi/containers/${container}/restart --silent | jq -r '.msg') C_REST_OUT=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${container}/restart --silent | jq -r '.msg')
echo "${C_REST_OUT}" echo "${C_REST_OUT}"
done done
} }
+1 -1
View File
@@ -1,3 +1,3 @@
FROM debian:bullseye-slim FROM debian:bookworm-slim
RUN apt update && apt install pigz RUN apt update && apt install pigz
+7 -3
View File
@@ -1,12 +1,14 @@
FROM clamav/clamav:1.0.3_base FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
RUN apk upgrade --no-cache \ RUN apk upgrade --no-cache \
&& apk add --update --no-cache \ && apk add --update --no-cache \
rsync \ rsync \
clamav \
bind-tools \ bind-tools \
bash bash \
tini
# init # init
COPY clamd.sh /clamd.sh COPY clamd.sh /clamd.sh
@@ -14,7 +16,9 @@ RUN chmod +x /sbin/tini
# healthcheck # healthcheck
COPY healthcheck.sh /healthcheck.sh COPY healthcheck.sh /healthcheck.sh
COPY clamdcheck.sh /usr/local/bin
RUN chmod +x /healthcheck.sh RUN chmod +x /healthcheck.sh
RUN chmod +x /usr/local/bin/clamdcheck.sh
HEALTHCHECK --start-period=6m CMD "/healthcheck.sh" HEALTHCHECK --start-period=6m CMD "/healthcheck.sh"
ENTRYPOINT [] ENTRYPOINT []
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
set -eu
if [ "${CLAMAV_NO_CLAMD:-}" != "false" ]; then
if [ "$(echo "PING" | nc localhost 3310)" != "PONG" ]; then
echo "ERROR: Unable to contact server"
exit 1
fi
echo "Clamd is up"
fi
exit 0
+7 -5
View File
@@ -1,7 +1,8 @@
FROM alpine:3.17 FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
ARG PIP_BREAK_SYSTEM_PACKAGES=1
WORKDIR /app WORKDIR /app
RUN apk add --update --no-cache python3 \ RUN apk add --update --no-cache python3 \
@@ -9,12 +10,13 @@ RUN apk add --update --no-cache python3 \
openssl \ openssl \
tzdata \ tzdata \
py3-psutil \ py3-psutil \
py3-redis \
py3-async-timeout \
&& pip3 install --upgrade pip \ && pip3 install --upgrade pip \
fastapi \ fastapi \
uvicorn \ uvicorn \
aiodocker \ aiodocker \
docker \ docker
aioredis
RUN mkdir /app/modules RUN mkdir /app/modules
COPY docker-entrypoint.sh /app/ COPY docker-entrypoint.sh /app/
@@ -22,4 +24,4 @@ COPY main.py /app/main.py
COPY modules/ /app/modules/ COPY modules/ /app/modules/
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"] ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]
CMD exec python main.py CMD ["python", "main.py"]
+52 -51
View File
@@ -5,16 +5,63 @@ import json
import uuid import uuid
import async_timeout import async_timeout
import asyncio import asyncio
import aioredis
import aiodocker import aiodocker
import docker import docker
import logging import logging
from logging.config import dictConfig from logging.config import dictConfig
from fastapi import FastAPI, Response, Request from fastapi import FastAPI, Response, Request
from modules.DockerApi import DockerApi from modules.DockerApi import DockerApi
from redis import asyncio as aioredis
from contextlib import asynccontextmanager
dockerapi = None dockerapi = None
app = FastAPI()
@asynccontextmanager
async def lifespan(app: FastAPI):
global dockerapi
# Initialize a custom logger
logger = logging.getLogger("dockerapi")
logger.setLevel(logging.INFO)
# Configure the logger to output logs to the terminal
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
formatter = logging.Formatter("%(levelname)s: %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info("Init APP")
# Init redis client
if os.environ['REDIS_SLAVEOF_IP'] != "":
redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0")
else:
redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0")
# Init docker clients
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
logger.info("Subscribe to redis channel")
# Subscribe to redis channel
dockerapi.pubsub = redis.pubsub()
await dockerapi.pubsub.subscribe("MC_CHANNEL")
asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
yield
# Close docker connections
dockerapi.sync_docker_client.close()
await dockerapi.async_docker_client.close()
# Close redis
await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
await dockerapi.redis_client.close()
app = FastAPI(lifespan=lifespan)
# Define Routes # Define Routes
@app.get("/host/stats") @app.get("/host/stats")
@@ -43,7 +90,7 @@ async def get_container(container_id : str):
if container._id == container_id: if container._id == container_id:
container_info = await container.show() container_info = await container.show()
return Response(content=json.dumps(container_info, indent=4), media_type="application/json") return Response(content=json.dumps(container_info, indent=4), media_type="application/json")
res = { res = {
"type": "danger", "type": "danger",
"msg": "no container found" "msg": "no container found"
@@ -83,7 +130,7 @@ async def get_containers():
async def post_containers(container_id : str, post_action : str, request: Request): async def post_containers(container_id : str, post_action : str, request: Request):
global dockerapi global dockerapi
try : try:
request_json = await request.json() request_json = await request.json()
except Exception as err: except Exception as err:
request_json = {} request_json = {}
@@ -145,52 +192,6 @@ async def post_container_update_stats(container_id : str):
stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats')) stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats'))
return Response(content=json.dumps(stats, indent=4), media_type="application/json") return Response(content=json.dumps(stats, indent=4), media_type="application/json")
# Events
@app.on_event("startup")
async def startup_event():
global dockerapi
# Initialize a custom logger
logger = logging.getLogger("dockerapi")
logger.setLevel(logging.INFO)
# Configure the logger to output logs to the terminal
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
formatter = logging.Formatter("%(levelname)s: %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info("Init APP")
# Init redis client
if os.environ['REDIS_SLAVEOF_IP'] != "":
redis_client = redis = await aioredis.from_url(f"redis://{os.environ['REDIS_SLAVEOF_IP']}:{os.environ['REDIS_SLAVEOF_PORT']}/0")
else:
redis_client = redis = await aioredis.from_url("redis://redis-mailcow:6379/0")
# Init docker clients
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
dockerapi = DockerApi(redis_client, sync_docker_client, async_docker_client, logger)
logger.info("Subscribe to redis channel")
# Subscribe to redis channel
dockerapi.pubsub = redis.pubsub()
await dockerapi.pubsub.subscribe("MC_CHANNEL")
asyncio.create_task(handle_pubsub_messages(dockerapi.pubsub))
@app.on_event("shutdown")
async def shutdown_event():
global dockerapi
# Close docker connections
dockerapi.sync_docker_client.close()
await dockerapi.async_docker_client.close()
# Close redis
await dockerapi.pubsub.unsubscribe("MC_CHANNEL")
await dockerapi.redis_client.close()
# PubSub Handler # PubSub Handler
async def handle_pubsub_messages(channel: aioredis.client.PubSub): async def handle_pubsub_messages(channel: aioredis.client.PubSub):
@@ -243,7 +244,7 @@ async def handle_pubsub_messages(channel: aioredis.client.PubSub):
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json)) dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
else: else:
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json)) dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
await asyncio.sleep(0.0) await asyncio.sleep(0.0)
except asyncio.TimeoutError: except asyncio.TimeoutError:
pass pass
+144 -5
View File
@@ -159,7 +159,7 @@ class DockerApi:
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix') postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
# todo: check each exit code # todo: check each exit code
res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'} res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'}
return Response(content=json.dumps(res, indent=4), media_type="application/json") return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: mailq - task: list # api call: container_post - post_action: exec - cmd: mailq - task: list
def container_post__exec__mailq__list(self, request_json, **kwargs): def container_post__exec__mailq__list(self, request_json, **kwargs):
if 'container_id' in kwargs: if 'container_id' in kwargs:
@@ -318,7 +318,7 @@ class DockerApi:
if 'username' in request_json and 'script_name' in request_json: if 'username' in request_json and 'script_name' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters): for container in self.sync_docker_client.containers.list(filters=filters):
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"] 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) sieve_return = container.exec_run(cmd)
return self.exec_run_handler('utf8_text_only', sieve_return) return self.exec_run_handler('utf8_text_only', sieve_return)
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup # api call: container_post - post_action: exec - cmd: maildir - task: cleanup
@@ -342,6 +342,30 @@ class DockerApi:
cmd = ["/bin/bash", "-c", cmd_vmail] cmd = ["/bin/bash", "-c", cmd_vmail]
maildir_cleanup = container.exec_run(cmd, user='vmail') maildir_cleanup = container.exec_run(cmd, user='vmail')
return self.exec_run_handler('generic', maildir_cleanup) return self.exec_run_handler('generic', maildir_cleanup)
# api call: container_post - post_action: exec - cmd: maildir - task: move
def container_post__exec__maildir__move(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'old_maildir' in request_json and 'new_maildir' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
vmail_name = request_json['old_maildir'].replace("'", "'\\''")
new_vmail_name = request_json['new_maildir'].replace("'", "'\\''")
cmd_vmail = f"if [[ -d '/var/vmail/{vmail_name}' ]]; then /bin/mv '/var/vmail/{vmail_name}' '/var/vmail/{new_vmail_name}'; fi"
index_name = request_json['old_maildir'].split("/")
new_index_name = request_json['new_maildir'].split("/")
if len(index_name) > 1 and len(new_index_name) > 1:
index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].replace("'", "'\\''")
new_index_name = new_index_name[1].replace("'", "'\\''") + "@" + new_index_name[0].replace("'", "'\\''")
cmd_vmail_index = f"if [[ -d '/var/vmail_index/{index_name}' ]]; then /bin/mv '/var/vmail_index/{index_name}' '/var/vmail_index/{new_index_name}_index'; fi"
cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index]
else:
cmd = ["/bin/bash", "-c", cmd_vmail]
maildir_move = container.exec_run(cmd, user='vmail')
return self.exec_run_handler('generic', maildir_move)
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password # api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
def container_post__exec__rspamd__worker_password(self, request_json, **kwargs): def container_post__exec__rspamd__worker_password(self, request_json, **kwargs):
if 'container_id' in kwargs: if 'container_id' in kwargs:
@@ -358,8 +382,8 @@ class DockerApi:
for line in cmd_response.split("\n"): for line in cmd_response.split("\n"):
if '$2$' in line: if '$2$' in line:
hash = line.strip() hash = line.strip()
hash_out = re.search('\$2\$.+$', hash).group(0) hash_out = re.search(r'\$2\$.+$', hash).group(0)
rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip()) rspamd_passphrase_hash = re.sub(r'[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc" 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 = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd") cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")
@@ -374,6 +398,121 @@ class DockerApi:
self.logger.error('failed changing Rspamd password') self.logger.error('failed changing Rspamd password')
res = { 'type': 'danger', 'msg': 'command did not complete' } res = { 'type': 'danger', 'msg': 'command did not complete' }
return Response(content=json.dumps(res, indent=4), media_type="application/json") return Response(content=json.dumps(res, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: sogo - task: rename
def container_post__exec__sogo__rename_user(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
if 'old_username' in request_json and 'new_username' in request_json:
for container in self.sync_docker_client.containers.list(filters=filters):
old_username = request_json['old_username'].replace("'", "'\\''")
new_username = request_json['new_username'].replace("'", "'\\''")
sogo_return = container.exec_run(["/bin/bash", "-c", f"sogo-tool rename-user '{old_username}' '{new_username}'"], user='sogo')
return self.exec_run_handler('generic', sogo_return)
# api call: container_post - post_action: exec - cmd: doveadm - task: get_acl
def container_post__exec__doveadm__get_acl(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
id = request_json['id'].replace("'", "'\\''")
shared_folders = container.exec_run(["/bin/bash", "-c", f"doveadm mailbox list -u '{id}'"])
shared_folders = shared_folders.output.decode('utf-8')
shared_folders = shared_folders.splitlines()
formatted_acls = []
mailbox_seen = []
for shared_folder in shared_folders:
if "Shared" not in shared_folder:
mailbox = shared_folder.replace("'", "'\\''")
if mailbox in mailbox_seen:
continue
acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u '{id}' '{mailbox}'"])
acls = acls.output.decode('utf-8').strip().splitlines()
if len(acls) >= 2:
for acl in acls[1:]:
user_id, rights = acl.split(maxsplit=1)
user_id = user_id.split('=')[1]
mailbox_seen.append(mailbox)
formatted_acls.append({ 'user': id, 'id': user_id, 'mailbox': mailbox, 'rights': rights.split() })
elif "Shared" in shared_folder and "/" in shared_folder:
shared_folder = shared_folder.split("/")
if len(shared_folder) < 3:
continue
user = shared_folder[1].replace("'", "'\\''")
mailbox = '/'.join(shared_folder[2:]).replace("'", "'\\''")
if mailbox in mailbox_seen:
continue
acls = container.exec_run(["/bin/bash", "-c", f"doveadm acl get -u '{user}' '{mailbox}'"])
acls = acls.output.decode('utf-8').strip().splitlines()
if len(acls) >= 2:
for acl in acls[1:]:
user_id, rights = acl.split(maxsplit=1)
user_id = user_id.split('=')[1].replace("'", "'\\''")
if user_id == id and mailbox not in mailbox_seen:
mailbox_seen.append(mailbox)
formatted_acls.append({ 'user': user, 'id': id, 'mailbox': mailbox, 'rights': rights.split() })
return Response(content=json.dumps(formatted_acls, indent=4), media_type="application/json")
# api call: container_post - post_action: exec - cmd: doveadm - task: delete_acl
def container_post__exec__doveadm__delete_acl(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
user = request_json['user'].replace("'", "'\\''")
mailbox = request_json['mailbox'].replace("'", "'\\''")
id = request_json['id'].replace("'", "'\\''")
if user and mailbox and id:
acl_delete_return = container.exec_run(["/bin/bash", "-c", f"doveadm acl delete -u '{user}' '{mailbox}' 'user={id}'"])
return self.exec_run_handler('generic', acl_delete_return)
# api call: container_post - post_action: exec - cmd: doveadm - task: set_acl
def container_post__exec__doveadm__set_acl(self, request_json, **kwargs):
if 'container_id' in kwargs:
filters = {"id": kwargs['container_id']}
elif 'container_name' in kwargs:
filters = {"name": kwargs['container_name']}
for container in self.sync_docker_client.containers.list(filters=filters):
user = request_json['user'].replace("'", "'\\''")
mailbox = request_json['mailbox'].replace("'", "'\\''")
id = request_json['id'].replace("'", "'\\''")
rights = ""
available_rights = [
"admin",
"create",
"delete",
"expunge",
"insert",
"lookup",
"post",
"read",
"write",
"write-deleted",
"write-seen"
]
for right in request_json['rights']:
right = right.replace("'", "'\\''").lower()
if right in available_rights:
rights += right + " "
if user and mailbox and id and rights:
acl_set_return = container.exec_run(["/bin/bash", "-c", f"doveadm acl set -u '{user}' '{mailbox}' 'user={id}' {rights}"])
return self.exec_run_handler('generic', acl_set_return)
# Collect host stats # Collect host stats
async def get_host_stats(self, wait=5): async def get_host_stats(self, wait=5):
@@ -462,7 +601,7 @@ class DockerApi:
except: except:
pass pass
return ''.join(total_data) return ''.join(total_data)
try : try :
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
if not cmd.endswith("\n"): if not cmd.endswith("\n"):
+98 -100
View File
@@ -1,119 +1,116 @@
FROM debian:bullseye-slim FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-tags depName=dovecot/core versioning=semver-coerced extractVersion=(?<version>.*)$
ARG DOVECOT=2.3.21 # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=(?<version>.*)$
ARG GOSU_VERSION=1.16 ARG GOSU_VERSION=1.16
ENV LC_ALL C
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
# Add groups and users before installing Dovecot to not break compatibility # Add groups and users before installing Dovecot to not break compatibility
RUN groupadd -g 5000 vmail \ RUN addgroup -g 5000 vmail \
&& groupadd -g 401 dovecot \ && addgroup -g 401 dovecot \
&& groupadd -g 402 dovenull \ && addgroup -g 402 dovenull \
&& groupadd -g 999 sogo \ && sed -i "s/999/99/" /etc/group \
&& usermod -a -G sogo nobody \ && addgroup -g 999 sogo \
&& useradd -g vmail -u 5000 vmail -d /var/vmail \ && addgroup nobody sogo \
&& useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \ && adduser -D -u 5000 -G vmail -h /var/vmail vmail \
&& useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull \ && adduser -D -G dovecot -u 401 -h /dev/null -s /sbin/nologin dovecot \
&& touch /etc/default/locale \ && adduser -D -G dovenull -u 402 -h /dev/null -s /sbin/nologin dovenull \
&& apt-get update \ && apk add --no-cache --update \
&& apt-get -y --no-install-recommends install \ bash \
build-essential \ bind-tools \
apt-transport-https \ findutils \
envsubst \
ca-certificates \ ca-certificates \
cpanminus \
curl \ curl \
dnsutils \ coreutils \
dirmngr \
gettext \
gnupg2 \
jq \ jq \
libauthen-ntlm-perl \ lua \
libcgi-pm-perl \ lua-cjson \
libcrypt-openssl-rsa-perl \
libcrypt-ssleay-perl \
libdata-uniqid-perl \
libdbd-mysql-perl \
libdbi-perl \
libdigest-hmac-perl \
libdist-checkconflicts-perl \
libencode-imaputf7-perl \
libfile-copy-recursive-perl \
libfile-tail-perl \
libhtml-parser-perl \
libio-compress-perl \
libio-socket-inet6-perl \
libio-socket-ssl-perl \
libio-tee-perl \
libipc-run-perl \
libjson-webtoken-perl \
liblockfile-simple-perl \
libmail-imapclient-perl \
libmodule-implementation-perl \
libmodule-scandeps-perl \
libnet-ssleay-perl \
libpackage-stash-perl \
libpackage-stash-xs-perl \
libpar-packer-perl \
libparse-recdescent-perl \
libproc-processtable-perl \
libreadonly-perl \
libregexp-common-perl \
libssl-dev \
libsys-meminfo-perl \
libterm-readkey-perl \
libtest-deep-perl \
libtest-fatal-perl \
libtest-mock-guard-perl \
libtest-mockobject-perl \
libtest-nowarnings-perl \
libtest-pod-perl \
libtest-requires-perl \
libtest-simple-perl \
libtest-warn-perl \
libtry-tiny-perl \
libunicode-string-perl \
liburi-perl \
libwww-perl \
lua-sql-mysql \
lua-socket \ lua-socket \
lua-sql-mysql \
lua5.3-sql-mysql \
icu-data-full \
mariadb-connector-c \
gcompat \
mariadb-client \ mariadb-client \
perl \
perl-ntlm \
perl-cgi \
perl-crypt-openssl-rsa \
perl-utils \
perl-crypt-ssleay \
perl-data-uniqid \
perl-dbd-mysql \
perl-dbi \
perl-digest-hmac \
perl-dist-checkconflicts \
perl-encode-imaputf7 \
perl-file-copy-recursive \
perl-file-tail \
perl-io-socket-inet6 \
perl-io-gzip \
perl-io-socket-ssl \
perl-io-tee \
perl-ipc-run \
perl-json-webtoken \
perl-mail-imapclient \
perl-module-implementation \
perl-module-scandeps \
perl-net-ssleay \
perl-package-stash \
perl-package-stash-xs \
perl-par-packer \
perl-parse-recdescent \
perl-lockfile-simple \
libproc \
perl-readonly \
perl-regexp-common \
perl-sys-meminfo \
perl-term-readkey \
perl-test-deep \
perl-test-fatal \
perl-test-mockobject \
perl-test-mock-guard \
perl-test-pod \
perl-test-requires \
perl-test-simple \
perl-test-warn \
perl-try-tiny \
perl-unicode-string \
perl-proc-processtable \
perl-app-cpanminus \
procps \ procps \
python3-pip \ python3 \
redis-server \ py3-mysqlclient \
supervisor \ py3-html2text \
py3-jinja2 \
py3-redis \
redis \
syslog-ng \ syslog-ng \
syslog-ng-core \ syslog-ng-redis \
syslog-ng-mod-redis \ syslog-ng-json \
supervisor \
tzdata \
wget \ wget \
&& dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \ dovecot \
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \ dovecot-dev \
&& chmod +x /usr/local/bin/gosu \
&& gosu nobody true \
&& apt-key adv --fetch-keys https://repo.dovecot.org/DOVECOT-REPO-GPG \
&& echo "deb https://repo.dovecot.org/ce-${DOVECOT}/debian/bullseye bullseye main" > /etc/apt/sources.list.d/dovecot.list \
&& apt-get update \
&& apt-get -y --no-install-recommends install \
dovecot-lua \
dovecot-managesieved \
dovecot-sieve \
dovecot-lmtpd \ dovecot-lmtpd \
dovecot-lua \
dovecot-ldap \ dovecot-ldap \
dovecot-mysql \ dovecot-mysql \
dovecot-core \ dovecot-sql \
dovecot-submissiond \
dovecot-pigeonhole-plugin \
dovecot-pop3d \ dovecot-pop3d \
dovecot-imapd \ dovecot-fts-solr \
dovecot-solr \ dovecot-fts-flatcurve \
&& pip3 install mysql-connector-python html2text jinja2 redis \ && arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
&& apt-get autoremove --purge -y \ && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$arch" \
&& apt-get autoclean \ && chmod +x /usr/local/bin/gosu \
&& rm -rf /var/lib/apt/lists/* \ && gosu nobody true
&& rm -rf /tmp/* /var/tmp/* /root/.cache/
# imapsync dependencies
RUN cpan Crypt::OpenSSL::PKCS12
COPY trim_logs.sh /usr/local/bin/trim_logs.sh COPY trim_logs.sh /usr/local/bin/trim_logs.sh
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
@@ -133,6 +130,7 @@ COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
COPY quarantine_notify.py /usr/local/bin/quarantine_notify.py COPY quarantine_notify.py /usr/local/bin/quarantine_notify.py
COPY quota_notify.py /usr/local/bin/quota_notify.py COPY quota_notify.py /usr/local/bin/quota_notify.py
COPY repl_health.sh /usr/local/bin/repl_health.sh COPY repl_health.sh /usr/local/bin/repl_health.sh
COPY optimize-fts.sh /usr/local/bin/optimize-fts.sh
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
+84 -13
View File
@@ -2,7 +2,7 @@
set -e set -e
# Wait for MySQL to warm-up # Wait for MySQL to warm-up
while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
echo "Waiting for database to come up..." echo "Waiting for database to come up..."
sleep 2 sleep 2
done done
@@ -29,6 +29,7 @@ ${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
# Create missing directories # Create missing directories
[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/ [[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/
[[ ! -d /etc/dovecot/lua/ ]] && mkdir -p /etc/dovecot/lua/ [[ ! -d /etc/dovecot/lua/ ]] && mkdir -p /etc/dovecot/lua/
[[ ! -d /etc/dovecot/conf.d/ ]] && mkdir -p /etc/dovecot/conf.d/
[[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage [[ ! -d /var/vmail/_garbage ]] && mkdir -p /var/vmail/_garbage
[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve [[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve
[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo [[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo
@@ -109,12 +110,19 @@ EOF
echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone echo -n ${ACL_ANYONE} > /etc/dovecot/acl_anyone
if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then if [[ "${FLATCURVE_EXPERIMENTAL}" =~ ^([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 -e "\e[33mActivating Flatcurve as FTS Backend...\e[0m"
echo -e "\e[33mDepending on your previous setup a full reindex might be needed... \e[0m"
echo -e "\e[34mVisit https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-fts/#fts-related-dovecot-commands to learn how to reindex\e[0m"
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_flatcurve listescape replication lazy_expunge' > /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
elif [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo -n 'quota acl zlib mail_crypt mail_crypt_acl mail_log notify listescape replication lazy_expunge' > /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 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 echo -n 'quota sieve acl zlib mail_crypt mail_crypt_acl notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
else 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 acl zlib mail_crypt mail_crypt_acl mail_log notify fts fts_solr listescape replication lazy_expunge' > /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 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 sieve acl zlib mail_crypt mail_crypt_acl fts fts_solr notify listescape replication' > /etc/dovecot/mail_plugins_lmtp
fi fi
@@ -239,6 +247,51 @@ function script_deinit()
end end
EOF EOF
# Temporarily set FTS depending on user choice inside mailcow.conf. Will be removed as soon as Solr is dropped
if [[ "${FLATCURVE_EXPERIMENTAL}" =~ ^([yY][eE][sS]|[yY])$ ]]; then
cat <<EOF > /etc/dovecot/conf.d/fts.conf
# Autogenerated by mailcow
plugin {
fts_autoindex = yes
fts_autoindex_exclude = \Junk
fts_autoindex_exclude2 = \Trash
fts = flatcurve
# Maximum term length can be set via the 'maxlen' argument (maxlen is
# specified in bytes, not number of UTF-8 characters)
fts_tokenizer_email_address = maxlen=100
fts_tokenizer_generic = algorithm=simple maxlen=30
# These are not flatcurve settings, but required for Dovecot FTS. See
# Dovecot FTS Configuration link above for further information.
fts_languages = en es de
fts_tokenizers = generic email-address
# OPTIONAL: Recommended default FTS core configuration
fts_filters = normalizer-icu snowball stopwords
fts_filters_en = lowercase snowball english-possessive stopwords
}
EOF
elif [[ ! "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])$ ]]; then
cat <<EOF > /etc/dovecot/conf.d/fts.conf
# Autogenerated by mailcow
plugin {
fts = solr
fts_autoindex = yes
fts_autoindex_exclude = \Junk
fts_autoindex_exclude2 = \Trash
fts_solr = url=http://solr:8983/solr/dovecot-fts/
fts_tokenizers = generic email-address
fts_tokenizer_generic = algorithm=simple
fts_filters = normalizer-icu snowball stopwords
fts_filters_en = lowercase snowball english-possessive stopwords
}
EOF
fi
# Replace patterns in app-passdb.lua # Replace patterns in app-passdb.lua
sed -i "s/__DBUSER__/${DBUSER}/g" /etc/dovecot/lua/passwd-verify.lua sed -i "s/__DBUSER__/${DBUSER}/g" /etc/dovecot/lua/passwd-verify.lua
sed -i "s/__DBPASS__/${DBPASS}/g" /etc/dovecot/lua/passwd-verify.lua sed -i "s/__DBPASS__/${DBPASS}/g" /etc/dovecot/lua/passwd-verify.lua
@@ -318,6 +371,8 @@ EOF
# Create random master Password for SOGo SSO # Create random master Password for SOGo SSO
RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1) RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1)
echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass
# Creating additional creds file for SOGo notify crons (calendars, etc)
echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds
cat <<EOF > /etc/dovecot/sogo-sso.conf cat <<EOF > /etc/dovecot/sogo-sso.conf
# Autogenerated by mailcow # Autogenerated by mailcow
passdb { passdb {
@@ -335,6 +390,14 @@ sys.exit()
EOF EOF
fi fi
# Set mail_replica for HA setups
if [[ -n ${MAILCOW_REPLICA_IP} && -n ${DOVEADM_REPLICA_PORT} ]]; then
cat <<EOF > /etc/dovecot/mail_replica.conf
# Autogenerated by mailcow
mail_replica = tcp:${MAILCOW_REPLICA_IP}:${DOVEADM_REPLICA_PORT}
EOF
fi
# 401 is user dovecot # 401 is user dovecot
if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then if [[ ! -s /mail_crypt/ecprivkey.pem || ! -s /mail_crypt/ecpubkey.pem ]]; then
openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem
@@ -344,20 +407,23 @@ else
chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem
fi fi
# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
if grep -qE 'ssl_min_protocol\s*=\s*(TLSv1|TLSv1\.1)\s*$' /etc/dovecot/dovecot.conf /etc/dovecot/extra.conf; then
sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf
echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf
echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf
echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
fi
# Compile sieve scripts # Compile sieve scripts
sievec /var/vmail/sieve/global_sieve_before.sieve sievec /var/vmail/sieve/global_sieve_before.sieve
sievec /var/vmail/sieve/global_sieve_after.sieve sievec /var/vmail/sieve/global_sieve_after.sieve
sievec /usr/lib/dovecot/sieve/report-spam.sieve sievec /usr/lib/dovecot/sieve/report-spam.sieve
sievec /usr/lib/dovecot/sieve/report-ham.sieve sievec /usr/lib/dovecot/sieve/report-ham.sieve
for file in /var/vmail/*/*/sieve/*.sieve ; do
if [[ "$file" == "/var/vmail/*/*/sieve/*.sieve" ]]; then
continue
fi
sievec "$file" "$(dirname "$file")/../.dovecot.svbin"
chown vmail:vmail "$(dirname "$file")/../.dovecot.svbin"
done
# Fix permissions # Fix permissions
chown root:root /etc/dovecot/sql/*.conf chown root:root /etc/dovecot/sql/*.conf
chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua
@@ -378,7 +444,8 @@ chmod +x /usr/lib/dovecot/sieve/rspamd-pipe-ham \
/usr/local/bin/maildir_gc.sh \ /usr/local/bin/maildir_gc.sh \
/usr/local/sbin/stop-supervisor.sh \ /usr/local/sbin/stop-supervisor.sh \
/usr/local/bin/quota_notify.py \ /usr/local/bin/quota_notify.py \
/usr/local/bin/repl_health.sh /usr/local/bin/repl_health.sh \
/usr/local/bin/optimize-fts.sh
# Prepare environment file for cronjobs # Prepare environment file for cronjobs
printenv | sed 's/^\(.*\)$/export \1/g' > /source_env.sh printenv | sed 's/^\(.*\)$/export \1/g' > /source_env.sh
@@ -432,4 +499,8 @@ done
# May be related to something inside Docker, I seriously don't know # May be related to something inside Docker, I seriously don't know
touch /etc/dovecot/lua/passwd-verify.lua touch /etc/dovecot/lua/passwd-verify.lua
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
fi
exec "$@" exec "$@"
+7
View File
@@ -0,0 +1,7 @@
#!/bin/bash
if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ && ! "${FLATCURVE_EXPERIMENTAL}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
exit 0
else
doveadm fts optimize -A
fi
@@ -3,11 +3,10 @@
import smtplib import smtplib
import os import os
import sys import sys
import mysql.connector import MySQLdb
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.utils import COMMASPACE, formatdate from email.utils import COMMASPACE, formatdate
import cgi
import jinja2 import jinja2
from jinja2 import Template from jinja2 import Template
import json import json
@@ -50,7 +49,7 @@ try:
def query_mysql(query, headers = True, update = False): def query_mysql(query, headers = True, update = False):
while True: while True:
try: try:
cnx = mysql.connector.connect(unix_socket = '/var/run/mysqld/mysqld.sock', user=os.environ.get('DBUSER'), passwd=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci") cnx = MySQLdb.connect(user=os.environ.get('DBUSER'), password=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci")
except Exception as ex: except Exception as ex:
print('%s - trying again...' % (ex)) print('%s - trying again...' % (ex))
time.sleep(3) time.sleep(3)
+1 -1
View File
@@ -55,7 +55,7 @@ try:
msg.attach(text_part) msg.attach(text_part)
msg.attach(html_part) msg.attach(html_part)
msg['To'] = username msg['To'] = username
p = Popen(['/usr/lib/dovecot/dovecot-lda', '-d', username, '-o', '"plugin/quota=maildir:User quota:noenforcing"'], stdout=PIPE, stdin=PIPE, stderr=STDOUT) p = Popen(['/usr/libexec/dovecot/dovecot-lda', '-d', username, '-o', '"plugin/quota=maildir:User quota:noenforcing"'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)
p.communicate(input=bytes(msg.as_string(), 'utf-8')) p.communicate(input=bytes(msg.as_string(), 'utf-8'))
domain = username.split("@")[-1] domain = username.split("@")[-1]
+1 -1
View File
@@ -11,7 +11,7 @@ fi
# Is replication active? # Is replication active?
# grep on file is less expensive than doveconf # grep on file is less expensive than doveconf
if ! grep -qi mail_replica /etc/dovecot/dovecot.conf; then if [ -n ${MAILCOW_REPLICA_IP} ]; then
${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null ${REDIS_CMDLINE} SET DOVECOT_REPL_HEALTH 1 > /dev/null
exit exit
fi fi
+3 -3
View File
@@ -3,8 +3,8 @@ FILE=/tmp/mail$$
cat > $FILE cat > $FILE
trap "/bin/rm -f $FILE" 0 1 2 3 13 15 trap "/bin/rm -f $FILE" 0 1 2 3 13 15
cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzydel cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzydel
cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnham cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/learnham
cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzyadd
exit 0 exit 0
+3 -3
View File
@@ -3,8 +3,8 @@ FILE=/tmp/mail$$
cat > $FILE cat > $FILE
trap "/bin/rm -f $FILE" 0 1 2 3 13 15 trap "/bin/rm -f $FILE" 0 1 2 3 13 15
cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzydel cat ${FILE} | /usr/bin/curl -H "Flag: 13" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzydel
cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/learnspam cat ${FILE} | /usr/bin/curl -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/learnspam
cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/fuzzyadd cat ${FILE} | /usr/bin/curl -H "Flag: 11" -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/fuzzyadd
exit 0 exit 0
+10 -6
View File
@@ -11,21 +11,25 @@ else
fi fi
# Deploy # Deploy
curl --connect-timeout 15 --retry 10 --max-time 30 http://www.spamassassin.heinlein-support.de/$(dig txt 1.4.3.spamassassin.heinlein-support.de +short | tr -d '"' | tr -dc '0-9').tar.gz --output /tmp/sa-rules-heinlein.tar.gz if curl --connect-timeout 15 --retry 10 --max-time 30 https://www.spamassassin.heinlein-support.de/$(dig txt 1.4.3.spamassassin.heinlein-support.de +short | tr -d '"' | tr -dc '0-9').tar.gz --output /tmp/sa-rules-heinlein.tar.gz; then
if gzip -t /tmp/sa-rules-heinlein.tar.gz; then if gzip -t /tmp/sa-rules-heinlein.tar.gz; then
tar xfvz /tmp/sa-rules-heinlein.tar.gz -C /tmp/sa-rules-heinlein tar xfvz /tmp/sa-rules-heinlein.tar.gz -C /tmp/sa-rules-heinlein
cat /tmp/sa-rules-heinlein/*cf > /etc/rspamd/custom/sa-rules cat /tmp/sa-rules-heinlein/*cf > /etc/rspamd/custom/sa-rules
fi
else
echo "Failed to download SA rules. Exiting."
exit 0 # Must be 0 otherwise dovecot would not start at all
fi fi
sed -i -e 's/\([^\\]\)\$\([^\/]\)/\1\\$\2/g' /etc/rspamd/custom/sa-rules sed -i -e 's/\([^\\]\)\$\([^\/]\)/\1\\$\2/g' /etc/rspamd/custom/sa-rules
if [[ "$(cat /etc/rspamd/custom/sa-rules | md5sum | cut -d' ' -f1)" != "${HASH_SA_RULES}" ]]; then if [[ "$(cat /etc/rspamd/custom/sa-rules | md5sum | cut -d' ' -f1)" != "${HASH_SA_RULES}" ]]; then
CONTAINER_NAME=rspamd-mailcow CONTAINER_NAME=rspamd-mailcow
CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | \ CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | \
jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | \ jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | \
jq -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id") jq -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")
if [[ ! -z ${CONTAINER_ID} ]]; then if [[ ! -z ${CONTAINER_ID} ]]; then
curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://dockerapi/containers/${CONTAINER_ID}/restart curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
fi fi
fi fi
@@ -13,6 +13,10 @@ autostart=true
[program:dovecot] [program:dovecot]
command=/usr/sbin/dovecot -F command=/usr/sbin/dovecot -F
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true autorestart=true
[eventlistener:processes] [eventlistener:processes]
@@ -1,4 +1,4 @@
@version: 3.28 @version: 4.5
@include "scl.conf" @include "scl.conf"
options { options {
chain_hostnames(off); chain_hostnames(off);
@@ -6,11 +6,12 @@ options {
use_dns(no); use_dns(no);
use_fqdn(no); use_fqdn(no);
owner("root"); group("adm"); perm(0640); owner("root"); group("adm"); perm(0640);
stats_freq(0); stats(freq(0));
keep_timestamp(no);
bad_hostname("^gconfd$"); bad_hostname("^gconfd$");
}; };
source s_src { source s_dgram {
unix-stream("/dev/log"); unix-dgram("/dev/log");
internal(); internal();
}; };
destination d_stdout { pipe("/dev/stdout"); }; destination d_stdout { pipe("/dev/stdout"); };
@@ -36,7 +37,7 @@ filter f_replica {
not match("Error: sync: Unknown user in remote" value("MESSAGE")); not match("Error: sync: Unknown user in remote" value("MESSAGE"));
}; };
log { log {
source(s_src); source(s_dgram);
filter(f_replica); filter(f_replica);
destination(d_stdout); destination(d_stdout);
filter(f_mail); filter(f_mail);
+6 -5
View File
@@ -1,4 +1,4 @@
@version: 3.28 @version: 4.5
@include "scl.conf" @include "scl.conf"
options { options {
chain_hostnames(off); chain_hostnames(off);
@@ -6,11 +6,12 @@ options {
use_dns(no); use_dns(no);
use_fqdn(no); use_fqdn(no);
owner("root"); group("adm"); perm(0640); owner("root"); group("adm"); perm(0640);
stats_freq(0); stats(freq(0));
keep_timestamp(no);
bad_hostname("^gconfd$"); bad_hostname("^gconfd$");
}; };
source s_src { source s_dgram {
unix-stream("/dev/log"); unix-dgram("/dev/log");
internal(); internal();
}; };
destination d_stdout { pipe("/dev/stdout"); }; destination d_stdout { pipe("/dev/stdout"); };
@@ -36,7 +37,7 @@ filter f_replica {
not match("Error: sync: Unknown user in remote" value("MESSAGE")); not match("Error: sync: Unknown user in remote" value("MESSAGE"));
}; };
log { log {
source(s_src); source(s_dgram);
filter(f_replica); filter(f_replica);
destination(d_stdout); destination(d_stdout);
filter(f_mail); filter(f_mail);
+5 -2
View File
@@ -1,8 +1,10 @@
FROM alpine:3.17 FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
WORKDIR /app WORKDIR /app
ARG PIP_BREAK_SYSTEM_PACKAGES=1
ENV XTABLES_LIBDIR /usr/lib/xtables ENV XTABLES_LIBDIR /usr/lib/xtables
ENV PYTHON_IPTABLES_XTABLES_VERSION 12 ENV PYTHON_IPTABLES_XTABLES_VERSION 12
ENV IPTABLES_LIBDIR /usr/lib ENV IPTABLES_LIBDIR /usr/lib
@@ -14,6 +16,7 @@ RUN apk add --virtual .build-deps \
openssl-dev \ openssl-dev \
&& apk add -U python3 \ && apk add -U python3 \
iptables \ iptables \
iptables-dev \
ip6tables \ ip6tables \
xtables-addons \ xtables-addons \
nftables \ nftables \
+99 -65
View File
@@ -21,28 +21,6 @@ from modules.IPTables import IPTables
from modules.NFTables import NFTables from modules.NFTables import NFTables
# connect to redis
while True:
try:
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
if "".__eq__(redis_slaveof_ip):
r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
else:
r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)
r.ping()
except Exception as ex:
print('%s - trying again in 3 seconds' % (ex))
time.sleep(3)
else:
break
pubsub = r.pubsub()
# rename fail2ban to netfilter
if r.exists('F2B_LOG'):
r.rename('F2B_LOG', 'NETFILTER_LOG')
# globals # globals
WHITELIST = [] WHITELIST = []
BLACKLIST= [] BLACKLIST= []
@@ -50,18 +28,10 @@ bans = {}
quit_now = False quit_now = False
exit_code = 0 exit_code = 0
lock = Lock() lock = Lock()
chain_name = "MAILCOW"
r = None
# init Logger pubsub = None
logger = Logger(r) clear_before_quit = False
# init backend
backend = sys.argv[1]
if backend == "nftables":
logger.logInfo('Using NFTables backend')
tables = NFTables("MAILCOW", logger)
else:
logger.logInfo('Using IPTables backend')
tables = IPTables("MAILCOW", logger)
def refreshF2boptions(): def refreshF2boptions():
@@ -110,16 +80,16 @@ def refreshF2bregex():
global exit_code global exit_code
if not r.get('F2B_REGEX'): if not r.get('F2B_REGEX'):
f2bregex = {} f2bregex = {}
f2bregex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)' f2bregex[1] = r'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
f2bregex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)' f2bregex[2] = r'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+' f2bregex[3] = r'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+' f2bregex[4] = r'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+' f2bregex[5] = r'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),' f2bregex[6] = r'-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
f2bregex[7] = '-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' f2bregex[7] = r'-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
f2bregex[8] = '-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' f2bregex[8] = r'-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+'
f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked' f2bregex[9] = r'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+' f2bregex[10] = r'([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False)) r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
else: else:
try: try:
@@ -144,8 +114,6 @@ def ban(address):
global lock global lock
refreshF2boptions() refreshF2boptions()
BAN_TIME = int(f2boptions['ban_time'])
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
MAX_ATTEMPTS = int(f2boptions['max_attempts']) MAX_ATTEMPTS = int(f2boptions['max_attempts'])
RETRY_WINDOW = int(f2boptions['retry_window']) RETRY_WINDOW = int(f2boptions['retry_window'])
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4']) NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
@@ -180,7 +148,7 @@ def ban(address):
if bans[net]['attempts'] >= MAX_ATTEMPTS: if bans[net]['attempts'] >= MAX_ATTEMPTS:
cur_time = int(round(time.time())) cur_time = int(round(time.time()))
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter'] NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter'])
logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 )) logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1: if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1:
with lock: with lock:
@@ -250,17 +218,21 @@ def clear():
with lock: with lock:
tables.clearIPv4Table() tables.clearIPv4Table()
tables.clearIPv6Table() tables.clearIPv6Table()
r.delete('F2B_ACTIVE_BANS') try:
r.delete('F2B_PERM_BANS') if r is not None:
pubsub.unsubscribe() r.delete('F2B_ACTIVE_BANS')
r.delete('F2B_PERM_BANS')
except Exception as ex:
logger.logWarn('Error clearing redis keys F2B_ACTIVE_BANS and F2B_PERM_BANS: %s' % ex)
def watch(): def watch():
logger.logInfo('Watching Redis channel F2B_CHANNEL') global pubsub
pubsub.subscribe('F2B_CHANNEL')
global quit_now global quit_now
global exit_code global exit_code
logger.logInfo('Watching Redis channel F2B_CHANNEL')
pubsub.subscribe('F2B_CHANNEL')
while not quit_now: while not quit_now:
try: try:
for item in pubsub.listen(): for item in pubsub.listen():
@@ -280,6 +252,7 @@ def watch():
ban(addr) ban(addr)
except Exception as ex: except Exception as ex:
logger.logWarn('Error reading log line from pubsub: %s' % ex) logger.logWarn('Error reading log line from pubsub: %s' % ex)
pubsub = None
quit_now = True quit_now = True
exit_code = 2 exit_code = 2
@@ -302,12 +275,11 @@ def snat6(snat_target):
tables.snat6(snat_target, os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64')) tables.snat6(snat_target, os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64'))
def autopurge(): def autopurge():
global f2boptions
while not quit_now: while not quit_now:
time.sleep(10) time.sleep(10)
refreshF2boptions() refreshF2boptions()
BAN_TIME = int(f2boptions['ban_time'])
MAX_BAN_TIME = int(f2boptions['max_ban_time'])
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
MAX_ATTEMPTS = int(f2boptions['max_attempts']) MAX_ATTEMPTS = int(f2boptions['max_attempts'])
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN') QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
if QUEUE_UNBAN: if QUEUE_UNBAN:
@@ -315,9 +287,9 @@ def autopurge():
unban(str(net)) unban(str(net))
for net in bans.copy(): for net in bans.copy():
if bans[net]['attempts'] >= MAX_ATTEMPTS: if bans[net]['attempts'] >= MAX_ATTEMPTS:
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter'] NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter'])
TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt'] TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt']
if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME or TIME_SINCE_LAST_ATTEMPT > MAX_BAN_TIME: if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME:
unban(net) unban(net)
def mailcowChainOrder(): def mailcowChainOrder():
@@ -331,6 +303,16 @@ def mailcowChainOrder():
if quit_now: return if quit_now: return
quit_now, exit_code = tables.checkIPv6ChainOrder() quit_now, exit_code = tables.checkIPv6ChainOrder()
def calcNetBanTime(ban_counter):
global f2boptions
BAN_TIME = int(f2boptions['ban_time'])
MAX_BAN_TIME = int(f2boptions['max_ban_time'])
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** ban_counter
NET_BAN_TIME = max([BAN_TIME, min([NET_BAN_TIME, MAX_BAN_TIME])])
return NET_BAN_TIME
def isIpNetwork(address): def isIpNetwork(address):
try: try:
ipaddress.ip_network(address, False) ipaddress.ip_network(address, False)
@@ -403,21 +385,76 @@ def blacklistUpdate():
permBan(net=net, unban=True) permBan(net=net, unban=True)
time.sleep(60.0 - ((time.time() - start_time) % 60.0)) time.sleep(60.0 - ((time.time() - start_time) % 60.0))
def quit(signum, frame): def sigterm_quit(signum, frame):
global quit_now global clear_before_quit
quit_now = True clear_before_quit = True
sys.exit(exit_code)
def berfore_quit():
if clear_before_quit:
clear()
if pubsub is not None:
pubsub.unsubscribe()
if __name__ == '__main__': if __name__ == '__main__':
refreshF2boptions() atexit.register(berfore_quit)
signal.signal(signal.SIGTERM, sigterm_quit)
# init Logger
logger = Logger()
# init backend
backend = sys.argv[1]
if backend == "nftables":
logger.logInfo('Using NFTables backend')
tables = NFTables(chain_name, logger)
else:
logger.logInfo('Using IPTables backend')
tables = IPTables(chain_name, logger)
# In case a previous session was killed without cleanup # In case a previous session was killed without cleanup
clear() clear()
# Reinit MAILCOW chain # Reinit MAILCOW chain
# Is called before threads start, no locking # Is called before threads start, no locking
logger.logInfo("Initializing mailcow netfilter chain") logger.logInfo("Initializing mailcow netfilter chain")
tables.initChainIPv4() tables.initChainIPv4()
tables.initChainIPv6() tables.initChainIPv6()
if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE").lower() in ("y", "yes"):
logger.logInfo(f"Skipping {chain_name} isolation")
else:
logger.logInfo(f"Setting {chain_name} isolation")
tables.create_mailcow_isolation_rule("br-mailcow", [3306, 6379, 8983, 12345], os.getenv("MAILCOW_REPLICA_IP"))
# connect to redis
while True:
try:
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
if "".__eq__(redis_slaveof_ip):
r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0)
else:
r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0)
r.ping()
pubsub = r.pubsub()
except Exception as ex:
print('%s - trying again in 3 seconds' % (ex))
time.sleep(3)
else:
break
logger.set_redis(r)
# rename fail2ban to netfilter
if r.exists('F2B_LOG'):
r.rename('F2B_LOG', 'NETFILTER_LOG')
# clear bans in redis
r.delete('F2B_ACTIVE_BANS')
r.delete('F2B_PERM_BANS')
refreshF2boptions()
watch_thread = Thread(target=watch) watch_thread = Thread(target=watch)
watch_thread.daemon = True watch_thread.daemon = True
watch_thread.start() watch_thread.start()
@@ -460,9 +497,6 @@ if __name__ == '__main__':
whitelistupdate_thread.daemon = True whitelistupdate_thread.daemon = True
whitelistupdate_thread.start() whitelistupdate_thread.start()
signal.signal(signal.SIGTERM, quit)
atexit.register(clear)
while not quit_now: while not quit_now:
time.sleep(0.5) time.sleep(0.5)
@@ -1,5 +1,6 @@
import iptc import iptc
import time import time
import os
class IPTables: class IPTables:
def __init__(self, chain_name, logger): def __init__(self, chain_name, logger):
@@ -211,3 +212,41 @@ class IPTables:
target = rule.create_target("SNAT") target = rule.create_target("SNAT")
target.to_source = snat_target target.to_source = snat_target
return rule return rule
def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""):
try:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name)
# insert mailcow isolation rule
rule = iptc.Rule()
rule.in_interface = f'!{_interface}'
rule.out_interface = _interface
rule.protocol = 'tcp'
rule.create_target("DROP")
match = rule.create_match("multiport")
match.dports = ','.join(map(str, _dports))
if rule in chain.rules:
chain.delete_rule(rule)
chain.insert_rule(rule, position=0)
# insert mailcow isolation exception rule
if _allow != "":
rule = iptc.Rule()
rule.src = _allow
rule.in_interface = f'!{_interface}'
rule.out_interface = _interface
rule.protocol = 'tcp'
rule.create_target("ACCEPT")
match = rule.create_match("multiport")
match.dports = ','.join(map(str, _dports))
if rule in chain.rules:
chain.delete_rule(rule)
chain.insert_rule(rule, position=0)
return True
except Exception as e:
self.logger.logCrit(f"Error adding {self.chain_name} isolation: {e}")
return False
+9 -2
View File
@@ -2,7 +2,10 @@ import time
import json import json
class Logger: class Logger:
def __init__(self, redis): def __init__(self):
self.r = None
def set_redis(self, redis):
self.r = redis self.r = redis
def log(self, priority, message): def log(self, priority, message):
@@ -10,8 +13,12 @@ class Logger:
tolog['time'] = int(round(time.time())) tolog['time'] = int(round(time.time()))
tolog['priority'] = priority tolog['priority'] = priority
tolog['message'] = message tolog['message'] = message
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
print(message) print(message)
if self.r is not None:
try:
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
except Exception as ex:
print('Failed logging to redis: %s' % (ex))
def logWarn(self, message): def logWarn(self, message):
self.log('warn', message) self.log('warn', message)
+173 -9
View File
@@ -1,5 +1,6 @@
import nftables import nftables
import ipaddress import ipaddress
import os
class NFTables: class NFTables:
def __init__(self, chain_name, logger): def __init__(self, chain_name, logger):
@@ -40,6 +41,7 @@ class NFTables:
exit_code = 2 exit_code = 2
if chain_position > 0: if chain_position > 0:
chain_position += 1
self.logger.logCrit(f'MAILCOW target is in position {chain_position} in the {filter_table} {chain} table, restarting container to fix it...') self.logger.logCrit(f'MAILCOW target is in position {chain_position} in the {filter_table} {chain} table, restarting container to fix it...')
err = True err = True
exit_code = 2 exit_code = 2
@@ -266,6 +268,17 @@ class NFTables:
return self.nft_exec_dict(delete_command) return self.nft_exec_dict(delete_command)
def delete_filter_rule(self, _family:str, _chain: str, _handle:str):
delete_command = self.get_base_dict()
_rule_opts = {'family': _family,
'table': 'filter',
'chain': _chain,
'handle': _handle }
_delete = {'delete': {'rule': _rule_opts} }
delete_command["nftables"].append(_delete)
return self.nft_exec_dict(delete_command)
def snat_rule(self, _family: str, snat_target: str, source_address: str): def snat_rule(self, _family: str, snat_target: str, source_address: str):
chain_name = self.nft_chain_names[_family]['nat']['postrouting'] chain_name = self.nft_chain_names[_family]['nat']['postrouting']
@@ -297,8 +310,8 @@ class NFTables:
rule_handle = rule["handle"] rule_handle = rule["handle"]
break break
dest_net = ipaddress.ip_network(source_address) dest_net = ipaddress.ip_network(source_address, strict=False)
target_net = ipaddress.ip_network(snat_target) target_net = ipaddress.ip_network(snat_target, strict=False)
if rule_found: if rule_found:
saddr_ip = rule["expr"][0]["match"]["right"]["prefix"]["addr"] saddr_ip = rule["expr"][0]["match"]["right"]["prefix"]["addr"]
@@ -309,9 +322,9 @@ class NFTables:
target_ip = rule["expr"][3]["snat"]["addr"] target_ip = rule["expr"][3]["snat"]["addr"]
saddr_net = ipaddress.ip_network(saddr_ip + '/' + str(saddr_len)) saddr_net = ipaddress.ip_network(saddr_ip + '/' + str(saddr_len), strict=False)
daddr_net = ipaddress.ip_network(daddr_ip + '/' + str(daddr_len)) daddr_net = ipaddress.ip_network(daddr_ip + '/' + str(daddr_len), strict=False)
current_target_net = ipaddress.ip_network(target_ip) current_target_net = ipaddress.ip_network(target_ip, strict=False)
match = all(( match = all((
dest_net == saddr_net, dest_net == saddr_net,
@@ -381,7 +394,7 @@ class NFTables:
break break
return chain_handle return chain_handle
def get_rules_handle(self, _family: str, _table: str, chain_name: str): def get_rules_handle(self, _family: str, _table: str, chain_name: str, _comment_filter = "mailcow"):
rule_handle = [] rule_handle = []
# Command: 'nft list chain {family} {table} {chain_name}' # Command: 'nft list chain {family} {table} {chain_name}'
_chain_opts = {'family': _family, 'table': _table, 'name': chain_name} _chain_opts = {'family': _family, 'table': _table, 'name': chain_name}
@@ -397,7 +410,7 @@ class NFTables:
rule = _object["rule"] rule = _object["rule"]
if rule["family"] == _family and rule["table"] == _table and rule["chain"] == chain_name: if rule["family"] == _family and rule["table"] == _table and rule["chain"] == chain_name:
if rule.get("comment") and rule["comment"] == "mailcow": if rule.get("comment") and rule["comment"] == _comment_filter:
rule_handle.append(rule["handle"]) rule_handle.append(rule["handle"])
return rule_handle return rule_handle
@@ -405,7 +418,7 @@ class NFTables:
json_command = self.get_base_dict() json_command = self.get_base_dict()
expr_opt = [] expr_opt = []
ipaddr_net = ipaddress.ip_network(ipaddr) ipaddr_net = ipaddress.ip_network(ipaddr, strict=False)
right_dict = {'prefix': {'addr': str(ipaddr_net.network_address), 'len': int(ipaddr_net.prefixlen) } } right_dict = {'prefix': {'addr': str(ipaddr_net.network_address), 'len': int(ipaddr_net.prefixlen) } }
left_dict = {'payload': {'protocol': _family, 'field': 'saddr'} } left_dict = {'payload': {'protocol': _family, 'field': 'saddr'} }
@@ -439,6 +452,8 @@ class NFTables:
continue continue
rule = _object["rule"]["expr"][0]["match"] rule = _object["rule"]["expr"][0]["match"]
if not "payload" in rule["left"]:
continue
left_opt = rule["left"]["payload"] left_opt = rule["left"]["payload"]
if not left_opt["protocol"] == _family: if not left_opt["protocol"] == _family:
continue continue
@@ -454,7 +469,7 @@ class NFTables:
current_rule_net = ipaddress.ip_network(current_rule_ip) current_rule_net = ipaddress.ip_network(current_rule_ip)
# ip to ban # ip to ban
candidate_net = ipaddress.ip_network(ipaddr) candidate_net = ipaddress.ip_network(ipaddr, strict=False)
if current_rule_net == candidate_net: if current_rule_net == candidate_net:
rule_handle = _object["rule"]["handle"] rule_handle = _object["rule"]["handle"]
@@ -493,3 +508,152 @@ class NFTables:
position+=1 position+=1
return position if rule_found else False return position if rule_found else False
def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""):
family = "ip"
table = "filter"
comment_filter_drop = "mailcow isolation"
comment_filter_allow = "mailcow isolation allow"
json_command = self.get_base_dict()
# Delete old mailcow isolation rules
handles = self.get_rules_handle(family, table, self.chain_name, comment_filter_drop)
for handle in handles:
self.delete_filter_rule(family, self.chain_name, handle)
handles = self.get_rules_handle(family, table, self.chain_name, comment_filter_allow)
for handle in handles:
self.delete_filter_rule(family, self.chain_name, handle)
# insert mailcow isolation rule
_match_dict_drop = [
{
"match": {
"op": "!=",
"left": {
"meta": {
"key": "iifname"
}
},
"right": _interface
}
},
{
"match": {
"op": "==",
"left": {
"meta": {
"key": "oifname"
}
},
"right": _interface
}
},
{
"match": {
"op": "==",
"left": {
"payload": {
"protocol": "tcp",
"field": "dport"
}
},
"right": {
"set": _dports
}
}
},
{
"counter": {
"packets": 0,
"bytes": 0
}
},
{
"drop": None
}
]
rule_drop = { "insert": { "rule": {
"family": family,
"table": table,
"chain": self.chain_name,
"comment": comment_filter_drop,
"expr": _match_dict_drop
}}}
json_command["nftables"].append(rule_drop)
# insert mailcow isolation allow rule
if _allow != "":
_match_dict_allow = [
{
"match": {
"op": "==",
"left": {
"payload": {
"protocol": "ip",
"field": "saddr"
}
},
"right": _allow
}
},
{
"match": {
"op": "!=",
"left": {
"meta": {
"key": "iifname"
}
},
"right": _interface
}
},
{
"match": {
"op": "==",
"left": {
"meta": {
"key": "oifname"
}
},
"right": _interface
}
},
{
"match": {
"op": "==",
"left": {
"payload": {
"protocol": "tcp",
"field": "dport"
}
},
"right": {
"set": _dports
}
}
},
{
"counter": {
"packets": 0,
"bytes": 0
}
},
{
"accept": None
}
]
rule_allow = { "insert": { "rule": {
"family": family,
"table": table,
"chain": self.chain_name,
"comment": comment_filter_allow,
"expr": _match_dict_allow
}}}
json_command["nftables"].append(rule_allow)
success = self.nft_exec_dict(json_command)
if success == False:
self.logger.logCrit(f"Error adding {self.chain_name} isolation")
return False
return True
+4 -2
View File
@@ -1,6 +1,8 @@
FROM alpine:3.17 FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
ARG PIP_BREAK_SYSTEM_PACKAGES=1
WORKDIR /app WORKDIR /app
#RUN addgroup -S olefy && adduser -S olefy -G olefy \ #RUN addgroup -S olefy && adduser -S olefy -G olefy \
+7 -6
View File
@@ -1,18 +1,19 @@
FROM php:8.2-fpm-alpine3.17 FROM php:8.2-fpm-alpine3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG APCU_PECL_VERSION=5.1.22 ARG APCU_PECL_VERSION=5.1.24
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$ # renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
ARG IMAGICK_PECL_VERSION=3.7.0 ARG IMAGICK_PECL_VERSION=3.7.0
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MAILPARSE_PECL_VERSION=3.1.6 ARG MAILPARSE_PECL_VERSION=3.1.8
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$ # renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MEMCACHED_PECL_VERSION=3.2.0 ARG MEMCACHED_PECL_VERSION=3.2.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$ # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
ARG REDIS_PECL_VERSION=6.0.1 ARG REDIS_PECL_VERSION=6.1.0
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$ # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
ARG COMPOSER_VERSION=2.6.5 ARG COMPOSER_VERSION=2.6.6
RUN apk add -U --no-cache autoconf \ RUN apk add -U --no-cache autoconf \
aspell-dev \ aspell-dev \
+19 -9
View File
@@ -3,27 +3,37 @@
function array_by_comma { local IFS=","; echo "$*"; } function array_by_comma { local IFS=","; echo "$*"; }
# Wait for containers # Wait for containers
while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
echo "Waiting for SQL..." echo "Waiting for SQL..."
sleep 2 sleep 2
done done
# Do not attempt to write to slave # Do not attempt to write to slave
if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT}" REDIS_HOST=$REDIS_SLAVEOF_IP
REDIS_PORT=$REDIS_SLAVEOF_PORT
else else
REDIS_CMDLINE="redis-cli -h redis -p 6379" REDIS_HOST="redis"
REDIS_PORT="6379"
fi fi
REDIS_CMDLINE="redis-cli -h ${REDIS_HOST} -p ${REDIS_PORT}"
until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do
echo "Waiting for Redis..." echo "Waiting for Redis..."
sleep 2 sleep 2
done done
# Set redis session store
echo -n '
session.save_handler = redis
session.save_path = "tcp://'${REDIS_HOST}':'${REDIS_PORT}'"
' > /usr/local/etc/php/conf.d/session_store.ini
# Check mysql_upgrade (master and slave) # Check mysql_upgrade (master and slave)
CONTAINER_ID= CONTAINER_ID=
until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do
CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null) CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
echo "Could not get mysql-mailcow container id... trying again"
sleep 2 sleep 2
done done
echo "MySQL @ ${CONTAINER_ID}" echo "MySQL @ ${CONTAINER_ID}"
@@ -34,7 +44,7 @@ until [[ ${SQL_UPGRADE_STATUS} == 'success' ]]; do
echo "Tried to upgrade MySQL and failed, giving up after ${SQL_LOOP_C} retries and starting container (oops, not good)" echo "Tried to upgrade MySQL and failed, giving up after ${SQL_LOOP_C} retries and starting container (oops, not good)"
break break
fi fi
SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json') SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json')
SQL_UPGRADE_STATUS=$(echo ${SQL_FULL_UPGRADE_RETURN} | jq -r .type) SQL_UPGRADE_STATUS=$(echo ${SQL_FULL_UPGRADE_RETURN} | jq -r .type)
SQL_LOOP_C=$((SQL_LOOP_C+1)) SQL_LOOP_C=$((SQL_LOOP_C+1))
echo "SQL upgrade iteration #${SQL_LOOP_C}" echo "SQL upgrade iteration #${SQL_LOOP_C}"
@@ -43,7 +53,7 @@ until [[ ${SQL_UPGRADE_STATUS} == 'success' ]]; do
echo "MySQL applied an upgrade, debug output:" echo "MySQL applied an upgrade, debug output:"
echo ${SQL_FULL_UPGRADE_RETURN} echo ${SQL_FULL_UPGRADE_RETURN}
sleep 3 sleep 3
while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
echo "Waiting for SQL to return, please wait" echo "Waiting for SQL to return, please wait"
sleep 2 sleep 2
done done
@@ -59,12 +69,12 @@ done
# doing post-installation stuff, if SQL was upgraded (master and slave) # doing post-installation stuff, if SQL was upgraded (master and slave)
if [ ${SQL_CHANGED} -eq 1 ]; then if [ ${SQL_CHANGED} -eq 1 ]; then
POSTFIX=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null) POSTFIX=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null)
if [[ -z "${POSTFIX}" ]] || ! [[ "${POSTFIX}" =~ ^[[:alnum:]]*$ ]]; then if [[ -z "${POSTFIX}" ]] || ! [[ "${POSTFIX}" =~ ^[[:alnum:]]*$ ]]; then
echo "Could not determine Postfix container ID, skipping Postfix restart." echo "Could not determine Postfix container ID, skipping Postfix restart."
else else
echo "Restarting Postfix" echo "Restarting Postfix"
curl -X POST --silent --insecure https://dockerapi/containers/${POSTFIX}/restart | jq -r '.msg' curl -X POST --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/restart | jq -r '.msg'
echo "Sleeping 5 seconds..." echo "Sleeping 5 seconds..."
sleep 5 sleep 5
fi fi
@@ -73,7 +83,7 @@ fi
# Check mysql tz import (master and slave) # Check mysql tz import (master and slave)
TZ_CHECK=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null) TZ_CHECK=$(mysql --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null)
if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then
SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json') SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json')
echo "MySQL mysql_tzinfo_to_sql - debug output:" echo "MySQL mysql_tzinfo_to_sql - debug output:"
echo ${SQL_FULL_TZINFO_IMPORT_RETURN} echo ${SQL_FULL_TZINFO_IMPORT_RETURN}
fi fi
+4 -3
View File
@@ -1,5 +1,6 @@
FROM debian:bullseye-slim FROM debian:bookworm-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ENV LC_ALL C ENV LC_ALL C
@@ -59,4 +60,4 @@ EXPOSE 588
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
@@ -12,4 +12,15 @@ if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
fi fi
# Fix OpenSSL 3.X TLS1.0, 1.1 support (https://community.mailcow.email/d/4062-hi-all/20)
if grep -qE '\!SSLv2|\!SSLv3|>=TLSv1(\.[0-1])?$' /opt/postfix/conf/main.cf /opt/postfix/conf/extra.cf; then
sed -i '/\[openssl_init\]/a ssl_conf = ssl_configuration' /etc/ssl/openssl.cnf
echo "[ssl_configuration]" >> /etc/ssl/openssl.cnf
echo "system_default = tls_system_default" >> /etc/ssl/openssl.cnf
echo "[tls_system_default]" >> /etc/ssl/openssl.cnf
echo "MinProtocol = TLSv1" >> /etc/ssl/openssl.cnf
echo "CipherString = DEFAULT@SECLEVEL=0" >> /etc/ssl/openssl.cnf
fi
exec "$@" exec "$@"
+1 -7
View File
@@ -5,7 +5,7 @@ trap "postfix stop" EXIT
[[ ! -d /opt/postfix/conf/sql/ ]] && mkdir -p /opt/postfix/conf/sql/ [[ ! -d /opt/postfix/conf/sql/ ]] && mkdir -p /opt/postfix/conf/sql/
# Wait for MySQL to warm-up # Wait for MySQL to warm-up
while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
echo "Waiting for database to come up..." echo "Waiting for database to come up..."
sleep 2 sleep 2
done done
@@ -415,12 +415,6 @@ postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
b.barracudacentral.org=127.0.0.2*7 b.barracudacentral.org=127.0.0.2*7
bl.mailspike.net=127.0.0.2*5 bl.mailspike.net=127.0.0.2*5
bl.mailspike.net=127.0.0.[10;11;12]*4 bl.mailspike.net=127.0.0.[10;11;12]*4
dnsbl.sorbs.net=127.0.0.10*8
dnsbl.sorbs.net=127.0.0.5*6
dnsbl.sorbs.net=127.0.0.7*3
dnsbl.sorbs.net=127.0.0.8*2
dnsbl.sorbs.net=127.0.0.6*2
dnsbl.sorbs.net=127.0.0.9*2
EOF EOF
fi fi
DNSBL_CONFIG=$(grep -v '^#' /opt/postfix/conf/dns_blocklists.cf | grep '\S') DNSBL_CONFIG=$(grep -v '^#' /opt/postfix/conf/dns_blocklists.cf | grep '\S')
@@ -1,4 +1,4 @@
@version: 3.28 @version: 3.38
@include "scl.conf" @include "scl.conf"
options { options {
chain_hostnames(off); chain_hostnames(off);
+1 -1
View File
@@ -1,4 +1,4 @@
@version: 3.28 @version: 3.38
@include "scl.conf" @include "scl.conf"
options { options {
chain_hostnames(off); chain_hostnames(off);
+15 -11
View File
@@ -1,9 +1,10 @@
FROM debian:bullseye-slim FROM debian:bookworm-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG CODENAME=bullseye ARG RSPAMD_VER=rspamd_3.10.2-1~b8a232043
ENV LC_ALL C ARG CODENAME=bookworm
ENV LC_ALL=C
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
tzdata \ tzdata \
@@ -11,12 +12,16 @@ RUN apt-get update && apt-get install -y \
gnupg2 \ gnupg2 \
apt-transport-https \ apt-transport-https \
dnsutils \ dnsutils \
netcat \ netcat-traditional \
&& apt-key adv --fetch-keys https://rspamd.com/apt-stable/gpg.key \ wget \
&& echo "deb [arch=amd64] https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \ redis-tools \
&& apt-get update \ procps \
&& apt-get --no-install-recommends -y install rspamd redis-tools procps nano \ nano \
&& rm -rf /var/lib/apt/lists/* \ lua-cjson \
&& arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
&& wget -P /tmp https://rspamd.com/apt-stable/pool/main/r/rspamd/${RSPAMD_VER}~${CODENAME}_${arch}.deb\
&& apt install -y /tmp/${RSPAMD_VER}~${CODENAME}_${arch}.deb \
&& rm -rf /var/lib/apt/lists/* /tmp/*\
&& apt-get autoremove --purge \ && apt-get autoremove --purge \
&& apt-get clean \ && apt-get clean \
&& mkdir -p /run/rspamd \ && mkdir -p /run/rspamd \
@@ -25,7 +30,6 @@ RUN apt-get update && apt-get install -y \
&& sed -i 's/#analysis_keyword_table > 0/analysis_cat_table.macro_exist == "M"/g' /usr/share/rspamd/lualib/lua_scanners/oletools.lua && sed -i 's/#analysis_keyword_table > 0/analysis_cat_table.macro_exist == "M"/g' /usr/share/rspamd/lualib/lua_scanners/oletools.lua
COPY settings.conf /etc/rspamd/settings.conf COPY settings.conf /etc/rspamd/settings.conf
COPY metadata_exporter.lua /usr/share/rspamd/plugins/metadata_exporter.lua
COPY set_worker_password.sh /set_worker_password.sh COPY set_worker_password.sh /set_worker_password.sh
COPY docker-entrypoint.sh /docker-entrypoint.sh COPY docker-entrypoint.sh /docker-entrypoint.sh
@@ -124,4 +124,190 @@ for file in /hooks/*; do
fi fi
done done
# If DQS KEY is set in mailcow.conf add Spamhaus DQS RBLs
if [[ ! -z ${SPAMHAUS_DQS_KEY} ]]; then
cat <<EOF > /etc/rspamd/custom/dqs-rbl.conf
# Autogenerated by mailcow. DO NOT TOUCH!
spamhaus {
rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net";
from = false;
}
spamhaus_from {
from = true;
received = false;
rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net";
returncodes {
SPAMHAUS_ZEN = [ "127.0.0.2", "127.0.0.3", "127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7", "127.0.0.9", "127.0.0.10", "127.0.0.11" ];
}
}
spamhaus_authbl_received {
# Check if the sender client is listed in AuthBL (AuthBL is *not* part of ZEN)
rbl = "${SPAMHAUS_DQS_KEY}.authbl.dq.spamhaus.net";
from = false;
received = true;
ipv6 = true;
returncodes {
SH_AUTHBL_RECEIVED = "127.0.0.20"
}
}
spamhaus_dbl {
# Add checks on the HELO string
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
helo = true;
rdns = true;
dkim = true;
disable_monitoring = true;
returncodes {
RBL_DBL_SPAM = "127.0.1.2";
RBL_DBL_PHISH = "127.0.1.4";
RBL_DBL_MALWARE = "127.0.1.5";
RBL_DBL_BOTNET = "127.0.1.6";
RBL_DBL_ABUSED_SPAM = "127.0.1.102";
RBL_DBL_ABUSED_PHISH = "127.0.1.104";
RBL_DBL_ABUSED_MALWARE = "127.0.1.105";
RBL_DBL_ABUSED_BOTNET = "127.0.1.106";
RBL_DBL_DONT_QUERY_IPS = "127.0.1.255";
}
}
spamhaus_dbl_fullurls {
ignore_defaults = true;
no_ip = true;
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
selector = 'urls:get_host'
disable_monitoring = true;
returncodes {
DBLABUSED_SPAM_FULLURLS = "127.0.1.102";
DBLABUSED_PHISH_FULLURLS = "127.0.1.104";
DBLABUSED_MALWARE_FULLURLS = "127.0.1.105";
DBLABUSED_BOTNET_FULLURLS = "127.0.1.106";
}
}
spamhaus_zrd {
# Add checks on the HELO string also for DQS
rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net";
helo = true;
rdns = true;
dkim = true;
disable_monitoring = true;
returncodes {
RBL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"];
RBL_ZRD_FRESH_DOMAIN = [
"127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"
];
RBL_ZRD_DONT_QUERY_IPS = "127.0.2.255";
}
}
"SPAMHAUS_ZEN_URIBL" {
enabled = true;
rbl = "${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net";
resolve_ip = true;
checks = ['urls'];
replyto = true;
emails = true;
ipv4 = true;
ipv6 = true;
emails_domainonly = true;
returncodes {
URIBL_SBL = "127.0.0.2";
URIBL_SBL_CSS = "127.0.0.3";
URIBL_XBL = ["127.0.0.4", "127.0.0.5", "127.0.0.6", "127.0.0.7"];
URIBL_PBL = ["127.0.0.10", "127.0.0.11"];
URIBL_DROP = "127.0.0.9";
}
}
SH_EMAIL_DBL {
ignore_defaults = true;
replyto = true;
emails_domainonly = true;
disable_monitoring = true;
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
returncodes = {
SH_EMAIL_DBL = [
"127.0.1.2",
"127.0.1.4",
"127.0.1.5",
"127.0.1.6"
];
SH_EMAIL_DBL_ABUSED = [
"127.0.1.102",
"127.0.1.104",
"127.0.1.105",
"127.0.1.106"
];
SH_EMAIL_DBL_DONT_QUERY_IPS = [ "127.0.1.255" ];
}
}
SH_EMAIL_ZRD {
ignore_defaults = true;
replyto = true;
emails_domainonly = true;
disable_monitoring = true;
rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net";
returncodes = {
SH_EMAIL_ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"];
SH_EMAIL_ZRD_FRESH_DOMAIN = [
"127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"
];
SH_EMAIL_ZRD_DONT_QUERY_IPS = [ "127.0.2.255" ];
}
}
"DBL" {
# override the defaults for DBL defined in modules.d/rbl.conf
rbl = "${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net";
disable_monitoring = true;
}
"ZRD" {
ignore_defaults = true;
rbl = "${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net";
no_ip = true;
dkim = true;
emails = true;
emails_domainonly = true;
urls = true;
returncodes = {
ZRD_VERY_FRESH_DOMAIN = ["127.0.2.2", "127.0.2.3", "127.0.2.4"];
ZRD_FRESH_DOMAIN = ["127.0.2.5", "127.0.2.6", "127.0.2.7", "127.0.2.8", "127.0.2.9", "127.0.2.10", "127.0.2.11", "127.0.2.12", "127.0.2.13", "127.0.2.14", "127.0.2.15", "127.0.2.16", "127.0.2.17", "127.0.2.18", "127.0.2.19", "127.0.2.20", "127.0.2.21", "127.0.2.22", "127.0.2.23", "127.0.2.24"];
}
}
spamhaus_sbl_url {
ignore_defaults = true
rbl = "${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net";
checks = ['urls'];
disable_monitoring = true;
returncodes {
SPAMHAUS_SBL_URL = "127.0.0.2";
}
}
SH_HBL_EMAIL {
ignore_defaults = true;
rbl = "_email.${SPAMHAUS_DQS_KEY}.hbl.dq.spamhaus.net";
emails_domainonly = false;
selector = "from('smtp').lower;from('mime').lower";
ignore_whitelist = true;
checks = ['emails', 'replyto'];
hash = "sha1";
returncodes = {
SH_HBL_EMAIL = [
"127.0.3.2"
];
}
}
spamhaus_dqs_hbl {
symbol = "HBL_FILE_UNKNOWN";
rbl = "_file.${SPAMHAUS_DQS_KEY}.hbl.dq.spamhaus.net.";
selector = "attachments('rbase32', 'sha256')";
ignore_whitelist = true;
ignore_defaults = true;
returncodes {
SH_HBL_FILE_MALICIOUS = "127.0.3.10";
SH_HBL_FILE_SUSPICIOUS = "127.0.3.15";
}
}
EOF
else
rm -rf /etc/rspamd/custom/dqs-rbl.conf
fi
exec "$@" exec "$@"
@@ -1,632 +0,0 @@
--[[
Copyright (c) 2016, Andrew Lewis <nerf@judo.za.org>
Copyright (c) 2016, Vsevolod Stakhov <vsevolod@highsecure.ru>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
]]--
if confighelp then
return
end
-- A plugin that pushes metadata (or whole messages) to external services
local redis_params
local lua_util = require "lua_util"
local rspamd_http = require "rspamd_http"
local rspamd_util = require "rspamd_util"
local rspamd_logger = require "rspamd_logger"
local ucl = require "ucl"
local E = {}
local N = 'metadata_exporter'
local settings = {
pusher_enabled = {},
pusher_format = {},
pusher_select = {},
mime_type = 'text/plain',
defer = false,
mail_from = '',
mail_to = 'postmaster@localhost',
helo = 'rspamd',
email_template = [[From: "Rspamd" <$mail_from>
To: $mail_to
Subject: Spam alert
Date: $date
MIME-Version: 1.0
Message-ID: <$our_message_id>
Content-type: text/plain; charset=utf-8
Content-Transfer-Encoding: 8bit
Authenticated username: $user
IP: $ip
Queue ID: $qid
SMTP FROM: $from
SMTP RCPT: $rcpt
MIME From: $header_from
MIME To: $header_to
MIME Date: $header_date
Subject: $header_subject
Message-ID: $message_id
Action: $action
Score: $score
Symbols: $symbols]],
}
local function get_general_metadata(task, flatten, no_content)
local r = {}
local ip = task:get_from_ip()
if ip and ip:is_valid() then
r.ip = tostring(ip)
else
r.ip = 'unknown'
end
r.user = task:get_user() or 'unknown'
r.qid = task:get_queue_id() or 'unknown'
r.subject = task:get_subject() or 'unknown'
r.action = task:get_metric_action('default')
local s = task:get_metric_score('default')[1]
r.score = flatten and string.format('%.2f', s) or s
local fuzzy = task:get_mempool():get_variable("fuzzy_hashes", "fstrings")
if fuzzy and #fuzzy > 0 then
local fz = {}
for _,h in ipairs(fuzzy) do
table.insert(fz, h)
end
if not flatten then
r.fuzzy = fz
else
r.fuzzy = table.concat(fz, ', ')
end
else
r.fuzzy = 'unknown'
end
local rcpt = task:get_recipients('smtp')
if rcpt then
local l = {}
for _, a in ipairs(rcpt) do
table.insert(l, a['addr'])
end
if not flatten then
r.rcpt = l
else
r.rcpt = table.concat(l, ', ')
end
else
r.rcpt = 'unknown'
end
local from = task:get_from('smtp')
if ((from or E)[1] or E).addr then
r.from = from[1].addr
else
r.from = 'unknown'
end
local syminf = task:get_symbols_all()
if flatten then
local l = {}
for _, sym in ipairs(syminf) do
local txt
if sym.options then
local topt = table.concat(sym.options, ', ')
txt = sym.name .. '(' .. string.format('%.2f', sym.score) .. ')' .. ' [' .. topt .. ']'
else
txt = sym.name .. '(' .. string.format('%.2f', sym.score) .. ')'
end
table.insert(l, txt)
end
r.symbols = table.concat(l, '\n\t')
else
r.symbols = syminf
end
local function process_header(name)
local hdr = task:get_header_full(name)
if hdr then
local l = {}
for _, h in ipairs(hdr) do
table.insert(l, h.decoded)
end
if not flatten then
return l
else
return table.concat(l, '\n')
end
else
return 'unknown'
end
end
if not no_content then
r.header_from = process_header('from')
r.header_to = process_header('to')
r.header_subject = process_header('subject')
r.header_date = process_header('date')
r.message_id = task:get_message_id()
end
return r
end
local formatters = {
default = function(task)
return task:get_content(), {}
end,
email_alert = function(task, rule, extra)
local meta = get_general_metadata(task, true)
local display_emails = {}
local mail_targets = {}
meta.mail_from = rule.mail_from or settings.mail_from
local mail_rcpt = rule.mail_to or settings.mail_to
if type(mail_rcpt) ~= 'table' then
table.insert(display_emails, string.format('<%s>', mail_rcpt))
table.insert(mail_targets, mail_rcpt)
else
for _, e in ipairs(mail_rcpt) do
table.insert(display_emails, string.format('<%s>', e))
table.insert(mail_targets, mail_rcpt)
end
end
if rule.email_alert_sender then
local x = task:get_from('smtp')
if x and string.len(x[1].addr) > 0 then
table.insert(mail_targets, x)
table.insert(display_emails, string.format('<%s>', x[1].addr))
end
end
if rule.email_alert_user then
local x = task:get_user()
if x then
table.insert(mail_targets, x)
table.insert(display_emails, string.format('<%s>', x))
end
end
if rule.email_alert_recipients then
local x = task:get_recipients('smtp')
if x then
for _, e in ipairs(x) do
if string.len(e.addr) > 0 then
table.insert(mail_targets, e.addr)
table.insert(display_emails, string.format('<%s>', e.addr))
end
end
end
end
meta.mail_to = table.concat(display_emails, ', ')
meta.our_message_id = rspamd_util.random_hex(12) .. '@rspamd'
meta.date = rspamd_util.time_to_string(rspamd_util.get_time())
return lua_util.template(rule.email_template or settings.email_template, meta), { mail_targets = mail_targets}
end,
json = function(task)
return ucl.to_format(get_general_metadata(task), 'json-compact')
end
}
local function is_spam(action)
return (action == 'reject' or action == 'add header' or action == 'rewrite subject')
end
local selectors = {
default = function(task)
return true
end,
is_spam = function(task)
local action = task:get_metric_action('default')
return is_spam(action)
end,
is_spam_authed = function(task)
if not task:get_user() then
return false
end
local action = task:get_metric_action('default')
return is_spam(action)
end,
is_reject = function(task)
local action = task:get_metric_action('default')
return (action == 'reject')
end,
is_reject_authed = function(task)
if not task:get_user() then
return false
end
local action = task:get_metric_action('default')
return (action == 'reject')
end,
}
local function maybe_defer(task, rule)
if rule.defer then
rspamd_logger.warnx(task, 'deferring message')
task:set_pre_result('soft reject', 'deferred', N)
end
end
local pushers = {
redis_pubsub = function(task, formatted, rule)
local _,ret,upstream
local function redis_pub_cb(err)
if err then
rspamd_logger.errx(task, 'got error %s when publishing on server %s',
err, upstream:get_addr())
return maybe_defer(task, rule)
end
return true
end
ret,_,upstream = rspamd_redis_make_request(task,
redis_params, -- connect params
nil, -- hash key
true, -- is write
redis_pub_cb, --callback
'PUBLISH', -- command
{rule.channel, formatted} -- arguments
)
if not ret then
rspamd_logger.errx(task, 'error connecting to redis')
maybe_defer(task, rule)
end
end,
http = function(task, formatted, rule)
local function http_callback(err, code)
if err then
rspamd_logger.errx(task, 'got error %s in http callback', err)
return maybe_defer(task, rule)
end
if code ~= 200 then
rspamd_logger.errx(task, 'got unexpected http status: %s', code)
return maybe_defer(task, rule)
end
return true
end
local hdrs = {}
if rule.meta_headers then
local gm = get_general_metadata(task, false, true)
local pfx = rule.meta_header_prefix or 'X-Rspamd-'
for k, v in pairs(gm) do
if type(v) == 'table' then
hdrs[pfx .. k] = ucl.to_format(v, 'json-compact')
else
hdrs[pfx .. k] = v
end
end
end
rspamd_http.request({
task=task,
url=rule.url,
body=formatted,
callback=http_callback,
mime_type=rule.mime_type or settings.mime_type,
headers=hdrs,
})
end,
send_mail = function(task, formatted, rule, extra)
local lua_smtp = require "lua_smtp"
local function sendmail_cb(ret, err)
if not ret then
rspamd_logger.errx(task, 'SMTP export error: %s', err)
maybe_defer(task, rule)
end
end
lua_smtp.sendmail({
task = task,
host = rule.smtp,
port = rule.smtp_port or settings.smtp_port or 25,
from = rule.mail_from or settings.mail_from,
recipients = extra.mail_targets or rule.mail_to or settings.mail_to,
helo = rule.helo or settings.helo,
timeout = rule.timeout or settings.timeout,
}, formatted, sendmail_cb)
end,
}
local opts = rspamd_config:get_all_opt(N)
if not opts then return end
local process_settings = {
select = function(val)
selectors.custom = assert(load(val))()
end,
format = function(val)
formatters.custom = assert(load(val))()
end,
push = function(val)
pushers.custom = assert(load(val))()
end,
custom_push = function(val)
if type(val) == 'table' then
for k, v in pairs(val) do
pushers[k] = assert(load(v))()
end
end
end,
custom_select = function(val)
if type(val) == 'table' then
for k, v in pairs(val) do
selectors[k] = assert(load(v))()
end
end
end,
custom_format = function(val)
if type(val) == 'table' then
for k, v in pairs(val) do
formatters[k] = assert(load(v))()
end
end
end,
pusher_enabled = function(val)
if type(val) == 'string' then
if pushers[val] then
settings.pusher_enabled[val] = true
else
rspamd_logger.errx(rspamd_config, 'Pusher type: %s is invalid', val)
end
elseif type(val) == 'table' then
for _, v in ipairs(val) do
if pushers[v] then
settings.pusher_enabled[v] = true
else
rspamd_logger.errx(rspamd_config, 'Pusher type: %s is invalid', val)
end
end
end
end,
}
for k, v in pairs(opts) do
local f = process_settings[k]
if f then
f(opts[k])
else
settings[k] = v
end
end
if type(settings.rules) ~= 'table' then
-- Legacy config
settings.rules = {}
if not next(settings.pusher_enabled) then
if pushers.custom then
rspamd_logger.infox(rspamd_config, 'Custom pusher implicitly enabled')
settings.pusher_enabled.custom = true
else
-- Check legacy options
if settings.url then
rspamd_logger.warnx(rspamd_config, 'HTTP pusher implicitly enabled')
settings.pusher_enabled.http = true
end
if settings.channel then
rspamd_logger.warnx(rspamd_config, 'Redis Pubsub pusher implicitly enabled')
settings.pusher_enabled.redis_pubsub = true
end
if settings.smtp and settings.mail_to then
rspamd_logger.warnx(rspamd_config, 'SMTP pusher implicitly enabled')
settings.pusher_enabled.send_mail = true
end
end
end
if not next(settings.pusher_enabled) then
rspamd_logger.errx(rspamd_config, 'No push backend enabled')
return
end
if settings.formatter then
settings.format = formatters[settings.formatter]
if not settings.format then
rspamd_logger.errx(rspamd_config, 'No such formatter: %s', settings.formatter)
return
end
end
if settings.selector then
settings.select = selectors[settings.selector]
if not settings.select then
rspamd_logger.errx(rspamd_config, 'No such selector: %s', settings.selector)
return
end
end
for k in pairs(settings.pusher_enabled) do
local formatter = settings.pusher_format[k]
local selector = settings.pusher_select[k]
if not formatter then
settings.pusher_format[k] = settings.formatter or 'default'
rspamd_logger.infox(rspamd_config, 'Using default formatter for %s pusher', k)
else
if not formatters[formatter] then
rspamd_logger.errx(rspamd_config, 'No such formatter: %s - disabling %s', formatter, k)
settings.pusher_enabled.k = nil
end
end
if not selector then
settings.pusher_select[k] = settings.selector or 'default'
rspamd_logger.infox(rspamd_config, 'Using default selector for %s pusher', k)
else
if not selectors[selector] then
rspamd_logger.errx(rspamd_config, 'No such selector: %s - disabling %s', selector, k)
settings.pusher_enabled.k = nil
end
end
end
if settings.pusher_enabled.redis_pubsub then
redis_params = rspamd_parse_redis_server(N)
if not redis_params then
rspamd_logger.errx(rspamd_config, 'No redis servers are specified')
settings.pusher_enabled.redis_pubsub = nil
else
local r = {}
r.backend = 'redis_pubsub'
r.channel = settings.channel
r.defer = settings.defer
r.selector = settings.pusher_select.redis_pubsub
r.formatter = settings.pusher_format.redis_pubsub
settings.rules[r.backend:upper()] = r
end
end
if settings.pusher_enabled.http then
if not settings.url then
rspamd_logger.errx(rspamd_config, 'No URL is specified')
settings.pusher_enabled.http = nil
else
local r = {}
r.backend = 'http'
r.url = settings.url
r.mime_type = settings.mime_type
r.defer = settings.defer
r.selector = settings.pusher_select.http
r.formatter = settings.pusher_format.http
settings.rules[r.backend:upper()] = r
end
end
if settings.pusher_enabled.send_mail then
if not (settings.mail_to and settings.smtp) then
rspamd_logger.errx(rspamd_config, 'No mail_to and/or smtp setting is specified')
settings.pusher_enabled.send_mail = nil
else
local r = {}
r.backend = 'send_mail'
r.mail_to = settings.mail_to
r.mail_from = settings.mail_from
r.helo = settings.hello
r.smtp = settings.smtp
r.smtp_port = settings.smtp_port
r.email_template = settings.email_template
r.defer = settings.defer
r.selector = settings.pusher_select.send_mail
r.formatter = settings.pusher_format.send_mail
settings.rules[r.backend:upper()] = r
end
end
if not next(settings.pusher_enabled) then
rspamd_logger.errx(rspamd_config, 'No push backend enabled')
return
end
elseif not next(settings.rules) then
lua_util.debugm(N, rspamd_config, 'No rules enabled')
return
end
if not settings.rules or not next(settings.rules) then
rspamd_logger.errx(rspamd_config, 'No rules enabled')
return
end
local backend_required_elements = {
http = {
'url',
},
smtp = {
'mail_to',
'smtp',
},
redis_pubsub = {
'channel',
},
}
local check_element = {
selector = function(k, v)
if not selectors[v] then
rspamd_logger.errx(rspamd_config, 'Rule %s has invalid selector %s', k, v)
return false
else
return true
end
end,
formatter = function(k, v)
if not formatters[v] then
rspamd_logger.errx(rspamd_config, 'Rule %s has invalid formatter %s', k, v)
return false
else
return true
end
end,
}
local backend_check = {
default = function(k, rule)
local reqset = backend_required_elements[rule.backend]
if reqset then
for _, e in ipairs(reqset) do
if not rule[e] then
rspamd_logger.errx(rspamd_config, 'Rule %s misses required setting %s', k, e)
settings.rules[k] = nil
end
end
end
for sett, v in pairs(rule) do
local f = check_element[sett]
if f then
if not f(sett, v) then
settings.rules[k] = nil
end
end
end
end,
}
backend_check.redis_pubsub = function(k, rule)
if not redis_params then
redis_params = rspamd_parse_redis_server(N)
end
if not redis_params then
rspamd_logger.errx(rspamd_config, 'No redis servers are specified')
settings.rules[k] = nil
else
backend_check.default(k, rule)
end
end
setmetatable(backend_check, {
__index = function()
return backend_check.default
end,
})
for k, v in pairs(settings.rules) do
if type(v) == 'table' then
local backend = v.backend
if not backend then
rspamd_logger.errx(rspamd_config, 'Rule %s has no backend', k)
settings.rules[k] = nil
elseif not pushers[backend] then
rspamd_logger.errx(rspamd_config, 'Rule %s has invalid backend %s', k, backend)
settings.rules[k] = nil
else
local f = backend_check[backend]
f(k, v)
end
else
rspamd_logger.errx(rspamd_config, 'Rule %s has bad type: %s', k, type(v))
settings.rules[k] = nil
end
end
local function gen_exporter(rule)
return function (task)
if task:has_flag('skip') then return end
local selector = rule.selector or 'default'
local selected = selectors[selector](task)
if selected then
lua_util.debugm(N, task, 'Message selected for processing')
local formatter = rule.formatter or 'default'
local formatted, extra = formatters[formatter](task, rule)
if formatted then
pushers[rule.backend](task, formatted, rule, extra)
else
lua_util.debugm(N, task, 'Formatter [%s] returned non-truthy value [%s]', formatter, formatted)
end
else
lua_util.debugm(N, task, 'Selector [%s] returned non-truthy value [%s]', selector, selected)
end
end
end
if not next(settings.rules) then
rspamd_logger.errx(rspamd_config, 'No rules enabled')
lua_util.disable_module(N, "config")
end
for k, r in pairs(settings.rules) do
rspamd_config:register_symbol({
name = 'EXPORT_METADATA_' .. k,
type = 'idempotent',
callback = gen_exporter(r),
priority = 10,
flags = 'empty,explicit_disable,ignore_passthrough',
})
end
+13 -10
View File
@@ -1,11 +1,13 @@
FROM debian:bullseye-slim FROM debian:bookworm-slim
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG SOGO_DEBIAN_REPOSITORY=http://packages.sogo.nu/nightly/5/debian/ ARG DEBIAN_VERSION=bookworm
ARG SOGO_DEBIAN_REPOSITORY=http://www.axis.cz/linux/debian
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$ # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.16 ARG GOSU_VERSION=1.17
ENV LC_ALL C ENV LC_ALL=C
# Prerequisites # Prerequisites
RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
@@ -21,7 +23,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
syslog-ng-core \ syslog-ng-core \
syslog-ng-mod-redis \ syslog-ng-mod-redis \
dirmngr \ dirmngr \
netcat \ netcat-traditional \
psmisc \ psmisc \
wget \ wget \
patch \ patch \
@@ -31,13 +33,14 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
&& gosu nobody true \ && gosu nobody true \
&& mkdir /usr/share/doc/sogo \ && mkdir /usr/share/doc/sogo \
&& touch /usr/share/doc/sogo/empty.sh \ && touch /usr/share/doc/sogo/empty.sh \
&& apt-key adv --keyserver keys.openpgp.org --recv-key 74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 \ && wget http://www.axis.cz/linux/debian/axis-archive-keyring.deb -O /tmp/axis-archive-keyring.deb \
&& echo "deb ${SOGO_DEBIAN_REPOSITORY} bullseye bullseye" > /etc/apt/sources.list.d/sogo.list \ && apt install -y /tmp/axis-archive-keyring.deb \
&& echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} sogo-v5" > /etc/apt/sources.list.d/sogo.list \
&& apt-get update && apt-get install -y --no-install-recommends \ && apt-get update && apt-get install -y --no-install-recommends \
sogo \ sogo \
sogo-activesync \ sogo-activesync \
&& apt-get autoclean \ && apt-get autoclean \
&& rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/sogo.list \ && rm -rf /var/lib/apt/lists/* \
&& touch /etc/default/locale && touch /etc/default/locale
COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh
@@ -53,4 +56,4 @@ RUN chmod +x /bootstrap-sogo.sh \
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]
CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
+3 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# Wait for MySQL to warm-up # Wait for MySQL to warm-up
while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
echo "Waiting for database to come up..." echo "Waiting for database to come up..."
sleep 2 sleep 2
done done
@@ -150,6 +150,8 @@ cat <<EOF > /var/lib/sogo/GNUstep/Defaults/sogod.plist
<string>YES</string> <string>YES</string>
<key>SOGoEncryptionKey</key> <key>SOGoEncryptionKey</key>
<string>${RAND_PASS}</string> <string>${RAND_PASS}</string>
<key>OCSAdminURL</key>
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_admin</string>
<key>OCSCacheFolderURL</key> <key>OCSCacheFolderURL</key>
<string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_cache_folder</string> <string>mysql://${DBUSER}:${DBPASS}@%2Fvar%2Frun%2Fmysqld%2Fmysqld.sock/${DBNAME}/sogo_cache_folder</string>
<key>OCSEMailAlarmsFolderURL</key> <key>OCSEMailAlarmsFolderURL</key>
@@ -10,6 +10,8 @@ if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then
cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf
fi fi
echo "$TZ" > /etc/timezone
# Run hooks # Run hooks
for file in /hooks/*; do for file in /hooks/*; do
if [ -x "${file}" ]; then if [ -x "${file}" ]; then
@@ -1,4 +1,4 @@
@version: 3.28 @version: 3.38
@include "scl.conf" @include "scl.conf"
options { options {
chain_hostnames(off); chain_hostnames(off);
+1 -1
View File
@@ -1,4 +1,4 @@
@version: 3.28 @version: 3.38
@include "scl.conf" @include "scl.conf"
options { options {
chain_hostnames(off); chain_hostnames(off);
+1 -1
View File
@@ -3,7 +3,7 @@ FROM solr:7.7-slim
USER root USER root
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=(?<version>.*)$ # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=(?<version>.*)$
ARG GOSU_VERSION=1.16 ARG GOSU_VERSION=1.17
COPY solr.sh / COPY solr.sh /
COPY solr-config-7.7.0.xml / COPY solr-config-7.7.0.xml /
+15 -1
View File
@@ -1,7 +1,15 @@
#!/bin/bash #!/bin/bash
if [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then if [[ "${FLATCURVE_EXPERIMENTAL}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "FLATCURVE_EXPERIMENTAL=y, skipping Solr but enabling Flatcurve as FTS for Dovecot!"
echo "Solr will be removed in the future!"
sleep 365d
exit 0
elif [[ "${SKIP_SOLR}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
echo "SKIP_SOLR=y, skipping Solr..." echo "SKIP_SOLR=y, skipping Solr..."
echo "HINT: You could try the newer FTS Backend Flatcurve, which is currently in experimental state..."
echo "Simply set FLATCURVE_EXPERIMENTAL=y inside your mailcow.conf and restart the stack afterwards!"
echo "Solr will be removed in the future!"
sleep 365d sleep 365d
exit 0 exit 0
fi fi
@@ -57,5 +65,11 @@ if [[ "${1}" == "--bootstrap" ]]; then
exit 0 exit 0
fi fi
echo "Starting up Solr..."
echo -e "\e[31mSolr is deprecated! You can try the new FTS System now by enabling FLATCURVE_EXPERIMENTAL=y inside mailcow.conf and restarting the stack\e[0m"
echo -e "\e[31mSolr will be removed completely soon!\e[0m"
sleep 15
exec gosu solr solr-foreground exec gosu solr solr-foreground
+15 -7
View File
@@ -1,28 +1,36 @@
FROM alpine:3.17 FROM alpine:3.20
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
RUN apk add --update --no-cache \ RUN apk add --update --no-cache \
curl \ curl \
bind-tools \
coreutils \
unbound \ unbound \
bash \ bash \
openssl \ openssl \
drill \ drill \
tzdata \ tzdata \
syslog-ng \
supervisor \
&& curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache \ && curl -o /etc/unbound/root.hints https://www.internic.net/domain/named.cache \
&& chown root:unbound /etc/unbound \ && chown root:unbound /etc/unbound \
&& adduser unbound tty \ && adduser unbound tty \
&& chmod 775 /etc/unbound && chmod 775 /etc/unbound
EXPOSE 53/udp 53/tcp EXPOSE 53/udp 53/tcp
COPY docker-entrypoint.sh /docker-entrypoint.sh COPY docker-entrypoint.sh /docker-entrypoint.sh
# healthcheck (nslookup) # healthcheck (dig, ping)
COPY healthcheck.sh /healthcheck.sh COPY healthcheck.sh /healthcheck.sh
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
COPY supervisord.conf /etc/supervisor/supervisord.conf
COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh
RUN chmod +x /healthcheck.sh RUN chmod +x /healthcheck.sh
HEALTHCHECK --interval=30s --timeout=10s CMD [ "/healthcheck.sh" ] HEALTHCHECK --interval=30s --timeout=10s \
CMD sh -c '[ -f /tmp/healthcheck_status ] && [ "$(cat /tmp/healthcheck_status)" -eq 0 ] || exit 1'
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
CMD ["/usr/sbin/unbound"]
+98 -8
View File
@@ -1,12 +1,102 @@
#!/bin/bash #!/bin/bash
nslookup mailcow.email 127.0.0.1 1> /dev/null STATUS_FILE="/tmp/healthcheck_status"
RUNS=0
if [ $? == 0 ]; then # Declare log function for logfile to stdout
echo "DNS resolution is working!" function log_to_stdout() {
exit 0 echo "$(date +"%Y-%m-%d %H:%M:%S"): $1"
else }
echo "DNS resolution is not working correctly..."
echo "Maybe check your outbound firewall, as it needs to resolve DNS over TCP AND UDP!" # General Ping function to check general pingability
exit 1 function check_ping() {
declare -a ipstoping=("1.1.1.1" "8.8.8.8" "9.9.9.9")
local fail_tolerance=1
local failures=0
for ip in "${ipstoping[@]}" ; do
success=false
for ((i=1; i<=3; i++)); do
ping -q -c 3 -w 5 "$ip" > /dev/null
if [ $? -eq 0 ]; then
success=true
break
else
log_to_stdout "Healthcheck: Failed to ping $ip on attempt $i. Trying again..."
fi
done
if [ "$success" = false ]; then
log_to_stdout "Healthcheck: Couldn't ping $ip after 3 attempts. Marking this IP as failed."
((failures++))
fi
done
if [ $failures -gt $fail_tolerance ]; then
log_to_stdout "Healthcheck: Too many ping failures ($fail_tolerance failures allowed, you got $failures failures), marking Healthcheck as unhealthy..."
return 1
fi fi
return 0
}
# General DNS Resolve Check against Unbound Resolver himself
function check_dns() {
declare -a domains=("fuzzy.mailcow.email" "github.com" "hub.docker.com")
local fail_tolerance=1
local failures=0
for domain in "${domains[@]}" ; do
success=false
for ((i=1; i<=3; i++)); do
dig_output=$(dig +short +timeout=2 +tries=1 "$domain" @127.0.0.1 2>/dev/null)
dig_rc=$?
if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then
log_to_stdout "Healthcheck: DNS Resolution Failed on attempt $i for $domain! Trying again..."
else
success=true
break
fi
done
if [ "$success" = false ]; then
log_to_stdout "Healthcheck: DNS Resolution not possible after 3 attempts for $domain... Gave up!"
((failures++))
fi
done
if [ $failures -gt $fail_tolerance ]; then
log_to_stdout "Healthcheck: Too many DNS failures ($fail_tolerance failures allowed, you got $failures failures), marking Healthcheck as unhealthy..."
return 1
fi
return 0
}
while true; do
if [[ ${SKIP_UNBOUND_HEALTHCHECK} == "y" ]]; then
log_to_stdout "Healthcheck: ALL CHECKS WERE SKIPPED! Unbound is healthy!"
echo "0" > $STATUS_FILE
sleep 365d
fi
# run checks, if check is not returning 0 (return value if check is ok), healthcheck will exit with 1 (marked in docker as unhealthy)
check_ping
PING_STATUS=$?
check_dns
DNS_STATUS=$?
if [ $PING_STATUS -ne 0 ] || [ $DNS_STATUS -ne 0 ]; then
echo "1" > $STATUS_FILE
else
echo "0" > $STATUS_FILE
fi
sleep 30
done
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
printf "READY\n";
while read line; do
echo "Processing Event: $line" >&2;
kill -3 $(cat "/var/run/supervisord.pid")
done < /dev/stdin
rm -rf /tmp/healthcheck_status
+32
View File
@@ -0,0 +1,32 @@
[supervisord]
nodaemon=true
user=root
pidfile=/var/run/supervisord.pid
[program:syslog-ng]
command=/usr/sbin/syslog-ng --foreground --no-caps
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autostart=true
[program:unbound]
command=/usr/sbin/unbound
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
[program:unbound-healthcheck]
command=/bin/bash /healthcheck.sh
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
[eventlistener:processes]
command=/usr/local/sbin/stop-supervisor.sh
events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL
+21
View File
@@ -0,0 +1,21 @@
@version: 4.5
@include "scl.conf"
options {
chain_hostnames(off);
flush_lines(0);
use_dns(no);
use_fqdn(no);
owner("root"); group("adm"); perm(0640);
stats(freq(0));
keep_timestamp(no);
bad_hostname("^gconfd$");
};
source s_dgram {
unix-dgram("/dev/log");
internal();
};
destination d_stdout { pipe("/dev/stdout"); };
log {
source(s_dgram);
destination(d_stdout);
};
+4 -3
View File
@@ -1,5 +1,6 @@
FROM alpine:3.17 FROM alpine:3.20
LABEL maintainer "André Peters <andre.peters@servercow.de>"
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
# Installation # Installation
RUN apk add --update \ RUN apk add --update \
@@ -36,4 +37,4 @@ RUN apk add --update \
COPY watchdog.sh /watchdog.sh COPY watchdog.sh /watchdog.sh
COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh
CMD /watchdog.sh CMD ["/watchdog.sh"]
+15 -11
View File
@@ -33,7 +33,7 @@ if [[ ! -p /tmp/com_pipe ]]; then
fi fi
# Wait for containers # Wait for containers
while ! mysqladmin status --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do
echo "Waiting for SQL..." echo "Waiting for SQL..."
sleep 2 sleep 2
done done
@@ -169,9 +169,13 @@ function notify_error() {
return 1 return 1
fi fi
# Escape subject and body (https://stackoverflow.com/a/2705678)
ESCAPED_SUBJECT=$(echo ${SUBJECT} | sed -e 's/[\/&]/\\&/g')
ESCAPED_BODY=$(echo ${BODY} | sed -e 's/[\/&]/\\&/g')
# Replace subject and body placeholders # Replace subject and body placeholders
WEBHOOK_BODY=$(echo ${WATCHDOG_NOTIFY_WEBHOOK_BODY} | sed "s|\$SUBJECT\|\${SUBJECT}|$SUBJECT|g" | sed "s|\$BODY\|\${BODY}|$BODY|") WEBHOOK_BODY=$(echo ${WATCHDOG_NOTIFY_WEBHOOK_BODY} | sed -e "s/\$SUBJECT\|\${SUBJECT}/$ESCAPED_SUBJECT/g" -e "s/\$BODY\|\${BODY}/$ESCAPED_BODY/g")
# POST to webhook # POST to webhook
curl -X POST -H "Content-Type: application/json" ${CURL_VERBOSE} -d "${WEBHOOK_BODY}" ${WATCHDOG_NOTIFY_WEBHOOK} curl -X POST -H "Content-Type: application/json" ${CURL_VERBOSE} -d "${WEBHOOK_BODY}" ${WATCHDOG_NOTIFY_WEBHOOK}
@@ -191,12 +195,12 @@ get_container_ip() {
else else
sleep 0.5 sleep 0.5
# get long container id for exact match # get long container id for exact match
CONTAINER_ID=($(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")) CONTAINER_ID=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id"))
# returned id can have multiple elements (if scaled), shuffle for random test # returned id can have multiple elements (if scaled), shuffle for random test
CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf)) CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf))
if [[ ! -z ${CONTAINER_ID} ]]; then if [[ ! -z ${CONTAINER_ID} ]]; then
for matched_container in "${CONTAINER_ID[@]}"; do for matched_container in "${CONTAINER_ID[@]}"; do
CONTAINER_IPS=($(curl --silent --insecure https://dockerapi/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress')) CONTAINER_IPS=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress'))
for ip_match in "${CONTAINER_IPS[@]}"; do for ip_match in "${CONTAINER_IPS[@]}"; do
# grep will do nothing if one of these vars is empty # grep will do nothing if one of these vars is empty
[[ -z ${ip_match} ]] && continue [[ -z ${ip_match} ]] && continue
@@ -716,8 +720,8 @@ rspamd_checks() {
From: watchdog@localhost From: watchdog@localhost
Empty Empty
' | usr/bin/curl --max-time 10 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd/scan | jq -rc .default.required_score) ' | usr/bin/curl --max-time 10 -s --data-binary @- --unix-socket /var/lib/rspamd/rspamd.sock http://rspamd.${COMPOSE_PROJECT_NAME}_mailcow-network/scan | jq -rc .default.required_score | sed 's/\..*//' )
if [[ ${SCORE} != "9999" ]]; then if [[ ${SCORE} -ne 9999 ]]; then
echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2 echo "Rspamd settings check failed, score returned: ${SCORE}" 2>> /tmp/rspamd-mailcow 1>&2
err_count=$(( ${err_count} + 1)) err_count=$(( ${err_count} + 1))
else else
@@ -1095,12 +1099,12 @@ while true; do
elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
kill -STOP ${BACKGROUND_TASKS[*]} kill -STOP ${BACKGROUND_TASKS[*]}
sleep 10 sleep 10
CONTAINER_ID=$(curl --silent --insecure https://dockerapi/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id") CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")
if [[ ! -z ${CONTAINER_ID} ]]; then if [[ ! -z ${CONTAINER_ID} ]]; then
if [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then if [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then
HAS_INITDB=$(curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true) HAS_INITDB=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true)
fi fi
S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://dockerapi/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d))) S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d)))
if [ ${S_RUNNING} -lt 360 ]; then if [ ${S_RUNNING} -lt 360 ]; then
log_msg "Container is running for less than 360 seconds, skipping action..." log_msg "Container is running for less than 360 seconds, skipping action..."
elif [[ ! -z ${HAS_INITDB} ]]; then elif [[ ! -z ${HAS_INITDB} ]]; then
@@ -1108,7 +1112,7 @@ while true; do
sleep 60 sleep 60
else else
log_msg "Sending restart command to ${CONTAINER_ID}..." log_msg "Sending restart command to ${CONTAINER_ID}..."
curl --silent --insecure -XPOST https://dockerapi/containers/${CONTAINER_ID}/restart curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart
notify_error "${com_pipe_answer}" notify_error "${com_pipe_answer}"
log_msg "Wait for restarted container to settle and continue watching..." log_msg "Wait for restarted container to settle and continue watching..."
sleep 35 sleep 35
+29
View File
@@ -0,0 +1,29 @@
<html>
<head>
<meta name="x-apple-disable-message-reformatting" />
<style>
body {
font-family: Helvetica, Arial, Sans-Serif;
}
/* mobile devices */
@media all and (max-width: 480px) {
.mob {
display: none;
}
}
</style>
</head>
<body>
Hello {{username2}},<br><br>
Somebody requested a new password for the {{hostname}} account associated with {{username}}.<br>
<small>Date of the password reset request: {{date}}</small><br><br>
You can reset your password by clicking the link below:<br>
<a href="{{link}}">{{link}}</a><br><br>
The link will be valid for the next {{token_lifetime}} minutes.<br><br>
If you did not request a new password, please ignore this email.<br>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
Hello {{username2}},
Somebody requested a new password for the {{hostname}} account associated with {{username}}.
Date of the password reset request: {{date}}
You can reset your password by clicking the link below:
{{link}}
The link will be valid for the next {{token_lifetime}} minutes.
If you did not request a new password, please ignore this email.
+5 -3
View File
@@ -10,6 +10,7 @@
auth_mechanisms = plain login auth_mechanisms = plain login
#mail_debug = yes #mail_debug = yes
#auth_debug = yes #auth_debug = yes
#log_debug = category=fts-flatcurve # Activate Logging for Flatcurve FTS Searchings
log_path = syslog log_path = syslog
disable_plaintext_auth = yes disable_plaintext_auth = yes
# Uncomment on NFS share # Uncomment on NFS share
@@ -194,9 +195,6 @@ plugin {
acl_shared_dict = file:/var/vmail/shared-mailboxes.db acl_shared_dict = file:/var/vmail/shared-mailboxes.db
acl = vfile acl = vfile
acl_user = %u acl_user = %u
fts = solr
fts_autoindex = yes
fts_solr = url=http://solr:8983/solr/dovecot-fts/
quota = dict:Userquota::proxy::sqlquota quota = dict:Userquota::proxy::sqlquota
quota_rule2 = Trash:storage=+100%% quota_rule2 = Trash:storage=+100%%
sieve = /var/vmail/sieve/%u.sieve sieve = /var/vmail/sieve/%u.sieve
@@ -247,6 +245,9 @@ plugin {
mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename
mail_log_fields = uid box msgid size mail_log_fields = uid box msgid size
mail_log_cached_only = yes mail_log_cached_only = yes
# Try set mail_replica
!include_try /etc/dovecot/mail_replica.conf
} }
service quota-warning { service quota-warning {
executable = script /usr/local/bin/quota_notify.py executable = script /usr/local/bin/quota_notify.py
@@ -302,6 +303,7 @@ replication_dsync_parameters = -d -l 30 -U -n INBOX
!include_try /etc/dovecot/extra.conf !include_try /etc/dovecot/extra.conf
!include_try /etc/dovecot/sogo-sso.conf !include_try /etc/dovecot/sogo-sso.conf
!include_try /etc/dovecot/shared_namespace.conf !include_try /etc/dovecot/shared_namespace.conf
!include_try /etc/dovecot/conf.d/fts.conf
# </Includes> # </Includes>
default_client_limit = 10400 default_client_limit = 10400
default_vsz_limit = 1024 M default_vsz_limit = 1024 M
+16 -1
View File
@@ -289,5 +289,20 @@ namespace inbox {
mailbox "Kladde" { mailbox "Kladde" {
special_use = \Drafts special_use = \Drafts
} }
mailbox "Πρόχειρα" {
special_use = \Drafts
}
mailbox "Απεσταλμένα" {
special_use = \Sent
}
mailbox "Κάδος απορριμάτων" {
special_use = \Trash
}
mailbox "Ανεπιθύμητα" {
special_use = \Junk
}
mailbox "Αρχειοθετημένα" {
special_use = \Archive
}
prefix = prefix =
} }
+1 -1
View File
@@ -1,6 +1,6 @@
if /^\s*Received:.*Authenticated sender.*\(Postcow\)/ if /^\s*Received:.*Authenticated sender.*\(Postcow\)/
#/^Received: from .*? \([\w-.]* \[.*?\]\)\s+\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (E?SMTPS?A?) id ([A-F0-9]+).+;.*?/ #/^Received: from .*? \([\w-.]* \[.*?\]\)\s+\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (E?SMTPS?A?) id ([A-F0-9]+).+;.*?/
/^Received: from .*? \([\w-.]* \[.*?\]\)(.*|\n.*)\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (.*)/ /^Received: from .*? \([\w\-.]* \[.*?\]\)(.*|\n.*)\(Authenticated sender: (.+)\)\s+by.+\(Postcow\) with (.*)/
REPLACE Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with $3 REPLACE Received: from [127.0.0.1] (localhost [127.0.0.1]) by localhost (Mailerdaemon) with $3
endif endif
if /^\s*Received: from.* \(.*dovecot-mailcow.*mailcow-network.*\).*\(Postcow\)/ if /^\s*Received: from.* \(.*dovecot-mailcow.*mailcow-network.*\).*\(Postcow\)/
+14 -10
View File
@@ -85,6 +85,7 @@ smtp_tls_security_level = dane
smtpd_data_restrictions = reject_unauth_pipelining, permit smtpd_data_restrictions = reject_unauth_pipelining, permit
smtpd_delay_reject = yes smtpd_delay_reject = yes
smtpd_error_sleep_time = 10s smtpd_error_sleep_time = 10s
smtpd_forbid_bare_newline = yes
smtpd_hard_error_limit = ${stress?1}${stress:5} smtpd_hard_error_limit = ${stress?1}${stress:5}
smtpd_helo_required = yes smtpd_helo_required = yes
smtpd_proxy_timeout = 600s smtpd_proxy_timeout = 600s
@@ -113,14 +114,14 @@ smtpd_tls_loglevel = 1
# Mandatory protocols and ciphers are used when a connections is enforced to use TLS # Mandatory protocols and ciphers are used when a connections is enforced to use TLS
# Does _not_ apply to enforced incoming TLS settings per mailbox # Does _not_ apply to enforced incoming TLS settings per mailbox
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtp_tls_mandatory_protocols = >=TLSv1.2
lmtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 lmtp_tls_mandatory_protocols = >=TLSv1.2
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtpd_tls_mandatory_protocols = >=TLSv1.2
smtpd_tls_mandatory_ciphers = high smtpd_tls_mandatory_ciphers = high
smtp_tls_protocols = !SSLv2, !SSLv3 smtp_tls_protocols = >=TLSv1.2
lmtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 lmtp_tls_protocols = >=TLSv1.2
smtpd_tls_protocols = !SSLv2, !SSLv3 smtpd_tls_protocols = >=TLSv1.2
smtpd_tls_security_level = may smtpd_tls_security_level = may
tls_preempt_cipherlist = yes tls_preempt_cipherlist = yes
@@ -161,13 +162,16 @@ transport_maps = pcre:/opt/postfix/conf/custom_transport.pcre,
proxy:mysql:/opt/postfix/conf/sql/mysql_relay_ne.cf, proxy:mysql:/opt/postfix/conf/sql/mysql_relay_ne.cf,
proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf proxy:mysql:/opt/postfix/conf/sql/mysql_transport_maps.cf
smtp_sasl_auth_soft_bounce = no smtp_sasl_auth_soft_bounce = no
postscreen_discard_ehlo_keywords = silent-discard, dsn postscreen_discard_ehlo_keywords = silent-discard, dsn, chunking
compatibility_level = 2 smtpd_discard_ehlo_keywords = chunking, silent-discard
compatibility_level = 3.7
smtputf8_enable = no smtputf8_enable = no
# Define protocols for SMTPS and submission service # Define protocols for SMTPS and submission service
submission_smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 submission_smtpd_tls_mandatory_protocols = >=TLSv1.2
smtps_smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 smtps_smtpd_tls_mandatory_protocols = >=TLSv1.2
parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,qmqpd_authorized_clients
# This Option is added to correctly set the X-Original-To Header when mails are send to lmtp (dovecot)
lmtp_destination_recipient_limit=1
# DO NOT EDIT ANYTHING BELOW # # DO NOT EDIT ANYTHING BELOW #
# Overrides # # Overrides #
+1 -2
View File
@@ -4,7 +4,6 @@ smtp inet n - n - 1 postscreen
-o postscreen_upstream_proxy_protocol=haproxy -o postscreen_upstream_proxy_protocol=haproxy
-o syslog_name=haproxy -o syslog_name=haproxy
smtpd pass - - n - - smtpd smtpd pass - - n - - smtpd
-o smtpd_helo_restrictions=permit_mynetworks,reject_non_fqdn_helo_hostname
-o smtpd_sasl_auth_enable=no -o smtpd_sasl_auth_enable=no
-o smtpd_sender_restrictions=permit_mynetworks,reject_unlisted_sender,reject_unknown_sender_domain -o smtpd_sender_restrictions=permit_mynetworks,reject_unlisted_sender,reject_unknown_sender_domain
@@ -106,7 +105,7 @@ retry unix - - n - - error
discard unix - - n - - discard discard unix - - n - - discard
local unix - n n - - local local unix - n n - - local
virtual unix - n n - - virtual virtual unix - n n - - virtual
lmtp unix - - n - - lmtp lmtp unix - - n - - lmtp flags=O
anvil unix - - n - 1 anvil anvil unix - - n - 1 anvil
scache unix - - n - 1 scache scache unix - - n - 1 scache
maildrop unix - n n - - pipe flags=DRhu maildrop unix - n n - - pipe flags=DRhu
File diff suppressed because it is too large Load Diff
+24 -2
View File
@@ -49,27 +49,49 @@ $from = $headers['From'];
$empty_footer = json_encode(array( $empty_footer = json_encode(array(
'html' => '', 'html' => '',
'plain' => '', 'plain' => '',
'skip_replies' => 0,
'vars' => array() 'vars' => array()
)); ));
error_log("FOOTER: checking for domain " . $domain . ", user " . $username . " and address " . $from . PHP_EOL); error_log("FOOTER: checking for domain " . $domain . ", user " . $username . " and address " . $from . PHP_EOL);
try { try {
$stmt = $pdo->prepare("SELECT `plain`, `html`, `mbox_exclude` FROM `domain_wide_footer` // try get $target_domain if $domain is an alias_domain
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain`
WHERE `alias_domain` = :alias_domain");
$stmt->execute(array(
':alias_domain' => $domain
));
$alias_domain = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$alias_domain) {
$target_domain = $domain;
} else {
$target_domain = $alias_domain['target_domain'];
}
// get footer associated with the domain
$stmt = $pdo->prepare("SELECT `plain`, `html`, `mbox_exclude`, `alias_domain_exclude`, `skip_replies` FROM `domain_wide_footer`
WHERE `domain` = :domain"); WHERE `domain` = :domain");
$stmt->execute(array( $stmt->execute(array(
':domain' => $domain ':domain' => $target_domain
)); ));
$footer = $stmt->fetch(PDO::FETCH_ASSOC); $footer = $stmt->fetch(PDO::FETCH_ASSOC);
// check if the sender is excluded
if (in_array($from, json_decode($footer['mbox_exclude']))){ if (in_array($from, json_decode($footer['mbox_exclude']))){
$footer = false; $footer = false;
} }
if (in_array($domain, json_decode($footer['alias_domain_exclude']))){
$footer = false;
}
if (empty($footer)){ if (empty($footer)){
echo $empty_footer; echo $empty_footer;
exit; exit;
} }
error_log("FOOTER: " . json_encode($footer) . PHP_EOL); error_log("FOOTER: " . json_encode($footer) . PHP_EOL);
// footer will be applied
// get custom mailbox attributes to insert into the footer
$stmt = $pdo->prepare("SELECT `custom_attributes` FROM `mailbox` WHERE `username` = :username"); $stmt = $pdo->prepare("SELECT `custom_attributes` FROM `mailbox` WHERE `username` = :username");
$stmt->execute(array( $stmt->execute(array(
':username' => $username ':username' => $username
+5 -1
View File
@@ -21,6 +21,10 @@ FREEMAIL_TO_UNDISC_RCPT {
SOGO_CONTACT_EXCLUDE { SOGO_CONTACT_EXCLUDE {
expression = "(-WHITELISTED_FWD_HOST | -g+:policies) & ^SOGO_CONTACT & !DMARC_POLICY_ALLOW"; expression = "(-WHITELISTED_FWD_HOST | -g+:policies) & ^SOGO_CONTACT & !DMARC_POLICY_ALLOW";
} }
# Remove MAILCOW_WHITE symbol for senders with broken policy recieved not from fwd hosts
MAILCOW_WHITE_EXCLUDE {
expression = "^MAILCOW_WHITE & (-DMARC_POLICY_REJECT | -DMARC_POLICY_QUARANTINE | -R_SPF_PERMFAIL) & !WHITELISTED_FWD_HOST";
}
# Spoofed header from and broken policy (excluding sieve host, rspamd host, whitelisted senders, authenticated senders and forward hosts) # Spoofed header from and broken policy (excluding sieve host, rspamd host, whitelisted senders, authenticated senders and forward hosts)
SPOOFED_UNAUTH { SPOOFED_UNAUTH {
expression = "!MAILCOW_AUTH & !MAILCOW_WHITE & !RSPAMD_HOST & !SIEVE_HOST & MAILCOW_DOMAIN_HEADER_FROM & !WHITELISTED_FWD_HOST & -g+:policies"; expression = "!MAILCOW_AUTH & !MAILCOW_WHITE & !RSPAMD_HOST & !SIEVE_HOST & MAILCOW_DOMAIN_HEADER_FROM & !WHITELISTED_FWD_HOST & -g+:policies";
@@ -103,4 +107,4 @@ CLAMD_JS_MALWARE {
expression = "CLAM_SECI_JS & !MAILCOW_WHITE"; expression = "CLAM_SECI_JS & !MAILCOW_WHITE";
description = "JS malware found, Securite JS malware Flag set through ClamAV"; description = "JS malware found, Securite JS malware Flag set through ClamAV";
score = 8; score = 8;
} }
+44 -30
View File
@@ -1,27 +1,45 @@
###############################################################################
# This list is added/merged with defined defaults in LUA module:
# https://github.com/rspamd/rspamd/blob/master/src/plugins/lua/mime_types.lua
###############################################################################
# Extensions that are treated as 'bad' # Extensions that are treated as 'bad'
# Number is score multiply factor # Number is score multiply factor
bad_extensions = { bad_extensions = {
scr = 20, apk = 4,
lnk = 20, appx = 4,
exe = 20, appxbundle = 4,
msi = 1, bat = 8,
msp = 1,
msu = 1,
jar = 2,
com = 20,
bat = 4,
cmd = 4,
ps1 = 4,
ace = 4,
arj = 4,
cab = 20, cab = 20,
cmd = 8,
com = 20,
diagcfg = 4,
diagpack = 4,
dmg = 8,
ex = 20,
ex_ = 20,
exe = 20,
img = 4,
jar = 8,
jnlp = 8,
js = 8,
jse = 8,
lnk = 20,
mjs = 8,
msi = 4,
msix = 4,
msixbundle = 4,
ps1 = 8,
scr = 20,
sct = 20,
vb = 20,
vbe = 20,
vbs = 20, vbs = 20,
hta = 4, vhd = 4,
shs = 4, py = 4,
wsc = 4, reg = 8,
wsf = 4, scf = 8,
iso = 8, vhdx = 4,
img = 8
}; };
# Extensions that are particularly penalized for archives # Extensions that are particularly penalized for archives
@@ -30,18 +48,14 @@ bad_archive_extensions = {
docx = 0.5, docx = 0.5,
xlsx = 0.5, xlsx = 0.5,
pdf = 1.0, pdf = 1.0,
jar = 3, jar = 12,
js = 0.5, jnlp = 12,
vbs = 20, bat = 12,
exe = 20 cmd = 12,
}; };
# Used to detect another archive in archive # Used to detect another archive in archive
archive_extensions = { archive_extensions = {
zip = 1, tar = 1,
arj = 1, gz = 1,
rar = 1, };
ace = 1,
7z = 1,
cab = 1
};
+2
View File
@@ -2,7 +2,9 @@ dns {
enable_dnssec = true; enable_dnssec = true;
} }
map_watch_interval = 30s; map_watch_interval = 30s;
task_timeout = 30s;
disable_monitoring = true; disable_monitoring = true;
# In case a task times out (like DNS lookup), soft reject the message # In case a task times out (like DNS lookup), soft reject the message
# instead of silently accepting the message without further processing. # instead of silently accepting the message without further processing.
soft_reject_on_timeout = true; soft_reject_on_timeout = true;
local_addrs = /etc/rspamd/custom/mailcow_networks.map;
+5 -17
View File
@@ -1,23 +1,8 @@
rbls { rbls {
sorbs {
symbol = "RBL_SORBS";
rbl = "dnsbl.sorbs.net";
returncodes {
# http:// www.sorbs.net/general/using.shtml
RBL_SORBS_HTTP = "127.0.0.2";
RBL_SORBS_SOCKS = "127.0.0.3";
RBL_SORBS_MISC = "127.0.0.4";
RBL_SORBS_SMTP = "127.0.0.5";
RBL_SORBS_RECENT = "127.0.0.6";
RBL_SORBS_WEB = "127.0.0.7";
RBL_SORBS_DUL = "127.0.0.10";
RBL_SORBS_BLOCK = "127.0.0.8";
RBL_SORBS_ZOMBIE = "127.0.0.9";
}
}
interserver_ip { interserver_ip {
symbol = "RBL_INTERSERVER_IP"; symbol = "RBL_INTERSERVER_IP";
rbl = "rbl.interserver.net"; rbl = "rbl.interserver.net";
from = true;
ipv6 = false; ipv6 = false;
returncodes { returncodes {
RBL_INTERSERVER_BAD_IP = "127.0.0.2"; RBL_INTERSERVER_BAD_IP = "127.0.0.2";
@@ -35,4 +20,7 @@ rbls {
RBL_INTERSERVER_BAD_URI = "127.0.0.2"; RBL_INTERSERVER_BAD_URI = "127.0.0.2";
} }
} }
}
.include(try=true,override=true,priority=5) "$LOCAL_CONFDIR/custom/dqs-rbl.conf"
}
+257 -40
View File
@@ -5,46 +5,6 @@ symbols = {
"RBL_UCEPROTECT_LEVEL2" { "RBL_UCEPROTECT_LEVEL2" {
score = 1.5; score = 1.5;
} }
"RBL_SORBS" {
score = 0.0;
description = "Unrecognised result from SORBS RBL";
}
"RBL_SORBS_HTTP" {
score = 2.5;
description = "List of Open HTTP Proxy Servers.";
}
"RBL_SORBS_SOCKS" {
score = 2.5;
description = "List of Open SOCKS Proxy Servers.";
}
"RBL_SORBS_MISC" {
score = 1.0;
description = "List of open Proxy Servers not listed in the SOCKS or HTTP lists.";
}
"RBL_SORBS_SMTP" {
score = 4.0;
description = "List of Open SMTP relay servers.";
}
"RBL_SORBS_RECENT" {
score = 2.0;
description = "List of hosts that have been noted as sending spam/UCE/UBE to the admins of SORBS within the last 28 days (includes new.spam.dnsbl.sorbs.net).";
}
"RBL_SORBS_WEB" {
score = 2.0;
description = "List of web (WWW) servers which have spammer abusable vulnerabilities (e.g. FormMail scripts)";
}
"RBL_SORBS_DUL" {
score = 2.0;
description = "Dynamic IP Address ranges (NOT a Dial Up list!)";
}
"RBL_SORBS_BLOCK" {
score = 0.5;
description = "List of hosts demanding that they never be tested by SORBS.";
}
"RBL_SORBS_ZOMBIE" {
score = 2.0;
description = "List of networks hijacked from their original owners, some of which have already used for spamming.";
}
"RECEIVED_SPAMHAUS_XBL" { "RECEIVED_SPAMHAUS_XBL" {
weight = 0.0; weight = 0.0;
description = "Received address is listed in ZEN XBL"; description = "Received address is listed in ZEN XBL";
@@ -57,4 +17,261 @@ symbols = {
score = 4.0; score = 4.0;
description = "Listed on Interserver RBL"; description = "Listed on Interserver RBL";
} }
"SPAMHAUS_ZEN" {
weight = 7.0;
}
"SH_AUTHBL_RECEIVED" {
weight = 4.0;
}
"RBL_DBL_SPAM" {
weight = 7.0;
}
"RBL_DBL_PHISH" {
weight = 7.0;
}
"RBL_DBL_MALWARE" {
weight = 7.0;
}
"RBL_DBL_BOTNET" {
weight = 7.0;
}
"RBL_DBL_ABUSED_SPAM" {
weight = 3.0;
}
"RBL_DBL_ABUSED_PHISH" {
weight = 3.0;
}
"RBL_DBL_ABUSED_MALWARE" {
weight = 3.0;
}
"RBL_DBL_ABUSED_BOTNET" {
weight = 3.0;
}
"RBL_ZRD_VERY_FRESH_DOMAIN" {
weight = 7.0;
}
"RBL_ZRD_FRESH_DOMAIN" {
weight = 4.0;
}
"ZRD_VERY_FRESH_DOMAIN" {
weight = 7.0;
}
"ZRD_FRESH_DOMAIN" {
weight = 4.0;
}
"SH_EMAIL_DBL" {
weight = 7.0;
}
"SH_EMAIL_DBL_ABUSED" {
weight = 7.0;
}
"SH_EMAIL_ZRD_VERY_FRESH_DOMAIN" {
weight = 7.0;
}
"SH_EMAIL_ZRD_FRESH_DOMAIN" {
weight = 4.0;
}
"RBL_DBL_DONT_QUERY_IPS" {
weight = 0.0;
}
"RBL_ZRD_DONT_QUERY_IPS" {
weight = 0.0;
}
"SH_EMAIL_ZRD_DONT_QUERY_IPS" {
weight = 0.0;
}
"SH_EMAIL_DBL_DONT_QUERY_IPS" {
weight = 0.0;
}
"DBL" {
weight = 0.0;
description = "DBL unknown result";
groups = ["spamhaus"];
}
"DBL_SPAM" {
weight = 7;
description = "DBL uribl spam";
groups = ["spamhaus"];
}
"DBL_PHISH" {
weight = 7;
description = "DBL uribl phishing";
groups = ["spamhaus"];
}
"DBL_MALWARE" {
weight = 7;
description = "DBL uribl malware";
groups = ["spamhaus"];
}
"DBL_BOTNET" {
weight = 7;
description = "DBL uribl botnet C&C domain";
groups = ["spamhaus"];
}
"DBLABUSED_SPAM_FULLURLS" {
weight = 5.5;
description = "DBL uribl abused legit spam";
groups = ["spamhaus"];
}
"DBLABUSED_PHISH_FULLURLS" {
weight = 5.5;
description = "DBL uribl abused legit phish";
groups = ["spamhaus"];
}
"DBLABUSED_MALWARE_FULLURLS" {
weight = 5.5;
description = "DBL uribl abused legit malware";
groups = ["spamhaus"];
}
"DBLABUSED_BOTNET_FULLURLS" {
weight = 5.5;
description = "DBL uribl abused legit botnet";
groups = ["spamhaus"];
}
"DBL_ABUSE" {
weight = 5.5;
description = "DBL uribl abused legit spam";
groups = ["spamhaus"];
}
"DBL_ABUSE_REDIR" {
weight = 1.5;
description = "DBL uribl abused spammed redirector domain";
groups = ["spamhaus"];
}
"DBL_ABUSE_PHISH" {
weight = 5.5;
description = "DBL uribl abused legit phish";
groups = ["spamhaus"];
}
"DBL_ABUSE_MALWARE" {
weight = 5.5;
description = "DBL uribl abused legit malware";
groups = ["spamhaus"];
}
"DBL_ABUSE_BOTNET" {
weight = 5.5;
description = "DBL uribl abused legit botnet C&C";
groups = ["spamhaus"];
}
"DBL_PROHIBIT" {
weight = 0.0;
description = "DBL uribl IP queries prohibited!";
groups = ["spamhaus"];
}
"DBL_BLOCKED_OPENRESOLVER" {
weight = 0.0;
description = "You are querying Spamhaus from an open resolver, please see https://www.spamhaus.org/returnc/pub/";
groups = ["spamhaus"];
}
"DBL_BLOCKED" {
weight = 0.0;
description = "You are exceeding the query limit, please see https://www.spamhaus.org/returnc/vol/";
groups = ["spamhaus"];
}
"SPAMHAUS_ZEN_URIBL" {
weight = 0.0;
description = "Spamhaus ZEN URIBL: Filtered result";
groups = ["spamhaus"];
}
"URIBL_SBL" {
weight = 6.5;
description = "A domain in the message body resolves to an IP listed in Spamhaus SBL";
one_shot = true;
groups = ["spamhaus"];
}
"URIBL_SBL_CSS" {
weight = 6.5;
description = "A domain in the message body resolves to an IP listed in Spamhaus SBL CSS";
one_shot = true;
groups = ["spamhaus"];
}
"URIBL_PBL" {
weight = 0.01;
description = "A domain in the message body resolves to an IP listed in Spamhaus PBL";
one_shot = true;
groups = ["spamhaus"];
}
"URIBL_DROP" {
weight = 6.5;
description = "A domain in the message body resolves to an IP listed in Spamhaus DROP";
one_shot = true;
groups = ["spamhaus"];
}
"URIBL_XBL" {
weight = 5.0;
description = "A domain in the message body resolves to an IP listed in Spamhaus XBL";
one_shot = true;
groups = ["spamhaus"];
}
"SPAMHAUS_SBL_URL" {
weight = 6.5;
description = "A numeric URL in the message body is listed in Spamhaus SBL";
one_shot = true;
groups = ["spamhaus"];
}
"SH_HBL_EMAIL" {
weight = 7;
description = "Email listed in HBL";
groups = ["spamhaus"];
}
"SH_HBL_FILE_MALICIOUS" {
weight = 7;
description = "An attachment hash is listed in Spamhaus HBL as malicious";
groups = ["spamhaus"];
}
"SH_HBL_FILE_SUSPICIOUS" {
weight = 5;
description = "An attachment hash is listed in Spamhaus HBL as suspicious";
groups = ["spamhaus"];
}
"RBL_SPAMHAUS_CW_BTC" {
score = 7;
description = "Bitcoin found in Spamhaus cryptowallet list";
groups = ["spamhaus"];
}
"RBL_SPAMHAUS_CW_ETH" {
score = 7;
description = "Ethereum found in Spamhaus cryptowallet list";
groups = ["spamhaus"];
}
"RBL_SPAMHAUS_CW_BCH" {
score = 7;
description = "Bitcoinhash found in Spamhaus cryptowallet list";
groups = ["spamhaus"];
}
"RBL_SPAMHAUS_CW_XMR" {
score = 7;
description = "Monero found in Spamhaus cryptowallet list";
groups = ["spamhaus"];
}
"RBL_SPAMHAUS_CW_LTC" {
score = 7;
description = "Litecoin found in Spamhaus cryptowallet list";
groups = ["spamhaus"];
}
"RBL_SPAMHAUS_CW_XRP" {
score = 7;
description = "Ripple found in Spamhaus cryptowallet list";
groups = ["spamhaus"];
}
"RBL_SPAMHAUS_HBL_URL" {
score = 7;
description = "URL found in spamhaus HBL blocklist";
groups = ["spamhaus"];
}
} }
+4 -2
View File
@@ -1,12 +1,14 @@
classifier "bayes" { classifier "bayes" {
# name = "custom"; # 'name' parameter must be set if multiple classifiers are defined
learn_condition = 'return require("lua_bayes_learn").can_learn';
new_schema = true;
tokenizer { tokenizer {
name = "osb"; name = "osb";
} }
backend = "redis"; backend = "redis";
min_tokens = 11; min_tokens = 11;
min_learns = 5; min_learns = 5;
new_schema = true; expire = 7776000;
expire = 2592000;
statfile { statfile {
symbol = "BAYES_HAM"; symbol = "BAYES_HAM";
spam = false; spam = false;
+22
View File
@@ -567,6 +567,14 @@ rspamd_config:register_symbol({
if footer and type(footer) == "table" and (footer.html and footer.html ~= "" or footer.plain and footer.plain ~= "") then if footer and type(footer) == "table" and (footer.html and footer.html ~= "" or footer.plain and footer.plain ~= "") then
rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s, vars=%s", uname, footer.html, footer.plain, footer.vars) rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s, vars=%s", uname, footer.html, footer.plain, footer.vars)
if footer.skip_replies ~= 0 then
in_reply_to = task:get_header_raw('in-reply-to')
if in_reply_to then
rspamd_logger.infox(rspamd_config, "mail is a reply - skip footer")
return
end
end
local envfrom_mime = task:get_from(2) local envfrom_mime = task:get_from(2)
local from_name = "" local from_name = ""
if envfrom_mime and envfrom_mime[1].name then if envfrom_mime and envfrom_mime[1].name then
@@ -613,10 +621,24 @@ rspamd_config:register_symbol({
local nct = string.format('%s: %s/%s; charset=utf-8', local nct = string.format('%s: %s/%s; charset=utf-8',
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype) 'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
out[#out + 1] = nct out[#out + 1] = nct
-- update Content-Type header
task:set_milter_reply({
remove_headers = {['Content-Type'] = 0},
})
task:set_milter_reply({
add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8', rewrite.new_ct.type, rewrite.new_ct.subtype)}
})
return return
elseif name:lower() == 'content-transfer-encoding' then elseif name:lower() == 'content-transfer-encoding' then
out[#out + 1] = string.format('%s: %s', out[#out + 1] = string.format('%s: %s',
'Content-Transfer-Encoding', 'quoted-printable') 'Content-Transfer-Encoding', 'quoted-printable')
-- update Content-Transfer-Encoding header
task:set_milter_reply({
remove_headers = {['Content-Transfer-Encoding'] = 0},
})
task:set_milter_reply({
add_headers = {['Content-Transfer-Encoding'] = 'quoted-printable'}
})
seen_cte = true seen_cte = true
return return
end end
+1 -1
View File
@@ -52,7 +52,7 @@ $headers = getallheaders();
$qid = $headers['X-Rspamd-Qid']; $qid = $headers['X-Rspamd-Qid'];
$fuzzy = $headers['X-Rspamd-Fuzzy']; $fuzzy = $headers['X-Rspamd-Fuzzy'];
$subject = $headers['X-Rspamd-Subject']; $subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
$score = $headers['X-Rspamd-Score']; $score = $headers['X-Rspamd-Score'];
$rcpts = $headers['X-Rspamd-Rcpt']; $rcpts = $headers['X-Rspamd-Rcpt'];
$user = $headers['X-Rspamd-User']; $user = $headers['X-Rspamd-User'];
+1 -1
View File
@@ -53,7 +53,7 @@ $qid = $headers['X-Rspamd-Qid'];
$rcpts = $headers['X-Rspamd-Rcpt']; $rcpts = $headers['X-Rspamd-Rcpt'];
$sender = $headers['X-Rspamd-From']; $sender = $headers['X-Rspamd-From'];
$ip = $headers['X-Rspamd-Ip']; $ip = $headers['X-Rspamd-Ip'];
$subject = $headers['X-Rspamd-Subject']; $subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
$messageid= $json_body->message_id; $messageid= $json_body->message_id;
$priority = 0; $priority = 0;
+5
View File
@@ -12,9 +12,13 @@
SOGoJunkFolderName= "Junk"; SOGoJunkFolderName= "Junk";
SOGoMailDomain = "sogo.local"; SOGoMailDomain = "sogo.local";
SOGoEnableEMailAlarms = YES; SOGoEnableEMailAlarms = YES;
SOGoMailHideInlineAttachments = YES;
SOGoFoldersSendEMailNotifications = YES; SOGoFoldersSendEMailNotifications = YES;
SOGoForwardEnabled = YES; SOGoForwardEnabled = YES;
// Option to set Users as admin to globally manage calendar permissions etc. Disabled by default
// SOGoSuperUsernames = ("moo@example.com");
SOGoUIAdditionalJSFiles = ( SOGoUIAdditionalJSFiles = (
js/theme.js, js/theme.js,
js/custom-sogo.js js/custom-sogo.js
@@ -37,6 +41,7 @@
SOGoLanguage = English; SOGoLanguage = English;
SOGoMailAuxiliaryUserAccountsEnabled = YES; SOGoMailAuxiliaryUserAccountsEnabled = YES;
// SOGoCreateIdentitiesDisabled = NO;
SOGoMailCustomFromEnabled = YES; SOGoMailCustomFromEnabled = YES;
SOGoMailingMechanism = smtp; SOGoMailingMechanism = smtp;
SOGoSMTPAuthenticationType = plain; SOGoSMTPAuthenticationType = plain;
+1
View File
@@ -107,6 +107,7 @@ $template_data = [
'f2b_banlist_url' => getBaseUrl() . "/api/v1/get/fail2ban/banlist/" . $f2b_data['banlist_id'], 'f2b_banlist_url' => getBaseUrl() . "/api/v1/get/fail2ban/banlist/" . $f2b_data['banlist_id'],
'q_data' => quarantine('settings'), 'q_data' => quarantine('settings'),
'qn_data' => quota_notification('get'), 'qn_data' => quota_notification('get'),
'pw_reset_data' => reset_password('get_notification'),
'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'), 'rsettings_map' => file_get_contents('http://nginx:8081/settings.php'),
'rsettings' => $rsettings, 'rsettings' => $rsettings,
'rspamd_regex_maps' => $rspamd_regex_maps, 'rspamd_regex_maps' => $rspamd_regex_maps,
+1 -1
View File
@@ -228,8 +228,8 @@ legend {
margin-top: 20px; margin-top: 20px;
} }
.slave-info { .slave-info {
padding: 15px 0px 15px 15px;
font-weight: bold; font-weight: bold;
color: orange;
} }
.alert-hr { .alert-hr {
margin:3px 0px; margin:3px 0px;
+3
View File
@@ -175,6 +175,9 @@ pre {
background-color: #282828; background-color: #282828;
border: 1px solid #555; border: 1px solid #555;
} }
.form-control {
background-color: transparent;
}
input.form-control, textarea.form-control { input.form-control, textarea.form-control {
color: #e2e2e2 !important; color: #e2e2e2 !important;
background-color: #424242 !important; background-color: #424242 !important;
+18 -10
View File
@@ -23,11 +23,15 @@ $exec_fields = array('cmd' => 'system', 'task' => 'df', 'dir' => '/var/vmail');
$vmail_df = explode(',', (string)json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true)); $vmail_df = explode(',', (string)json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true));
// containers // containers
$containers = (array) docker('info'); $containers_info = (array) docker('info');
if ($clamd_status === false) unset($containers['clamd-mailcow']); if ($clamd_status === false) unset($containers_info['clamd-mailcow']);
if ($solr_status === false) unset($containers['solr-mailcow']); if ($solr_status === false) unset($containers_info['solr-mailcow']);
ksort($containers); ksort($containers_info);
foreach ($containers as $container => $container_info) { $containers = array();
foreach ($containers_info as $container => $container_info) {
if (!isset($container_info['State']) || !is_array($container_info['State']) || !isset($container_info['State']['StartedAt'])){
continue;
}
date_default_timezone_set('UTC'); date_default_timezone_set('UTC');
$StartedAt = date_parse($container_info['State']['StartedAt']); $StartedAt = date_parse($container_info['State']['StartedAt']);
if ($StartedAt['hour'] !== false) { if ($StartedAt['hour'] !== false) {
@@ -39,14 +43,18 @@ foreach ($containers as $container => $container_info) {
$StartedAt['month'], $StartedAt['month'],
$StartedAt['day'], $StartedAt['day'],
$StartedAt['year'])); $StartedAt['year']));
$user_tz = new DateTimeZone(getenv('TZ')); try {
$date->setTimezone($user_tz); $user_tz = new DateTimeZone(getenv('TZ'));
$started = $date->format('r'); $date->setTimezone($user_tz);
$container_info['State']['StartedAtHR'] = $date->format('r');
} catch(Exception $e) {
$container_info['State']['StartedAtHR'] = '?';
}
} }
else { else {
$started = '?'; $container_info['State']['StartedAtHR'] = '?';
} }
$containers[$container]['State']['StartedAtHR'] = $started; $containers[$container] = $container_info;
} }
// get mailcow data // get mailcow data
+2 -1
View File
@@ -59,7 +59,8 @@ if (isset($_SESSION['mailcow_cc_role'])) {
'domain_details' => $result, 'domain_details' => $result,
'domain_footer' => $domain_footer, 'domain_footer' => $domain_footer,
'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]), 'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]),
'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address') 'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address'),
'alias_domains' => mailbox('get', 'alias_domains', $_GET["domain"])
]; ];
} }
} }
+7
View File
@@ -1,4 +1,11 @@
<?php <?php
// Block requests by checking the 'Sec-Fetch-Dest' header.
if (isset($_SERVER['HTTP_SEC_FETCH_DEST']) && $_SERVER['HTTP_SEC_FETCH_DEST'] !== 'empty') {
header('HTTP/1.1 403 Forbidden');
exit;
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != 'admin') { if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != 'admin') {
exit(); exit();
+2 -1
View File
@@ -12,7 +12,8 @@ $alertbox_log_parser = alertbox_log_parser($_SESSION);
$alerts = []; $alerts = [];
if (is_array($alertbox_log_parser)) { if (is_array($alertbox_log_parser)) {
foreach ($alertbox_log_parser as $log) { foreach ($alertbox_log_parser as $log) {
$message = strtr($log['msg'], ["\n" => '', "\r" => '', "\t" => '<br>']); $message = htmlspecialchars($log['msg'], ENT_QUOTES);
$message = strtr($message, ["\n" => '', "\r" => '', "\t" => '<br>']);
$alerts[trim($log['type'], '"')][] = trim($message, '"'); $alerts[trim($log['type'], '"')][] = trim($message, '"');
} }
$alert = array_filter(array_unique($alerts)); $alert = array_filter(array_unique($alerts));
+18
View File
@@ -2,6 +2,7 @@
function customize($_action, $_item, $_data = null) { function customize($_action, $_item, $_data = null) {
global $redis; global $redis;
global $lang; global $lang;
global $LOGO_LIMITS;
switch ($_action) { switch ($_action) {
case 'add': case 'add':
@@ -35,6 +36,23 @@ function customize($_action, $_item, $_data = null) {
); );
return false; return false;
} }
if ($_data[$_item]['size'] > $LOGO_LIMITS['max_size']) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'img_size_exceeded'
);
return false;
}
list($width, $height) = getimagesize($_data[$_item]['tmp_name']);
if ($width > $LOGO_LIMITS['max_width'] || $height > $LOGO_LIMITS['max_height']) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'img_dimensions_exceeded'
);
return false;
}
$image = new Imagick($_data[$_item]['tmp_name']); $image = new Imagick($_data[$_item]['tmp_name']);
if ($image->valid() !== true) { if ($image->valid() !== true) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
+450 -41
View File
@@ -284,17 +284,17 @@ function last_login($action, $username, $sasl_limit_days = 7, $ui_offset = 1) {
} }
if (!$sasl[$k]['location']) { if (!$sasl[$k]['location']) {
$curl = curl_init(); $curl = curl_init();
curl_setopt($curl, CURLOPT_URL,"https://dfdata.bella.network/lookup/" . $sasl[$k]['real_rip']); curl_setopt($curl, CURLOPT_URL,"https://dfdata.bella.network/country/" . $sasl[$k]['real_rip']);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_USERAGENT, 'Moocow'); curl_setopt($curl, CURLOPT_USERAGENT, 'Moocow');
curl_setopt($curl, CURLOPT_TIMEOUT, 5); curl_setopt($curl, CURLOPT_TIMEOUT, 5);
$ip_data = curl_exec($curl); $ip_data = curl_exec($curl);
if (!curl_errno($curl)) { if (!curl_errno($curl)) {
$ip_data_array = json_decode($ip_data, true); $ip_data_array = json_decode($ip_data, true);
if ($ip_data_array !== false and !empty($ip_data_array['location']['shortcountry'])) { if ($ip_data_array !== false and !empty($ip_data_array['shortcountry'])) {
$sasl[$k]['location'] = $ip_data_array['location']['shortcountry']; $sasl[$k]['location'] = $ip_data_array['shortcountry'];
try { try {
$redis->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['location']['shortcountry']); $redis->hSet('IP_SHORTCOUNTRY', $sasl[$k]['real_rip'], $ip_data_array['shortcountry']);
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -939,10 +939,10 @@ function check_login($user, $pass, $app_passwd_data = false) {
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
$rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC)); $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC));
} }
foreach ($rows as $row) { foreach ($rows as $row) {
// verify password // verify password
if (verify_hash($row['password'], $pass) !== false) { if (verify_hash($row['password'], $pass) !== false) {
if (!array_key_exists("app_passwd_id", $row)){ if (!array_key_exists("app_passwd_id", $row)){
// password is not a app password // password is not a app password
// check for tfa authenticators // check for tfa authenticators
$authenticators = get_tfa($user); $authenticators = get_tfa($user);
@@ -953,11 +953,6 @@ function check_login($user, $pass, $app_passwd_data = false) {
$_SESSION['pending_mailcow_cc_role'] = "user"; $_SESSION['pending_mailcow_cc_role'] = "user";
$_SESSION['pending_tfa_methods'] = $authenticators['additional']; $_SESSION['pending_tfa_methods'] = $authenticators['additional'];
unset($_SESSION['ldelay']); unset($_SESSION['ldelay']);
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
return "pending"; return "pending";
} else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) { } else if (!isset($authenticators['additional']) || !is_array($authenticators['additional']) || count($authenticators['additional']) == 0) {
// no authenticators found, login successfull // no authenticators found, login successfull
@@ -966,6 +961,11 @@ function check_login($user, $pass, $app_passwd_data = false) {
$stmt->execute(array(':user' => $user)); $stmt->execute(array(':user' => $user));
unset($_SESSION['ldelay']); unset($_SESSION['ldelay']);
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => array('logged_in_as', $user)
);
return "user"; return "user";
} }
} elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { } elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) {
@@ -1028,7 +1028,7 @@ function update_sogo_static_view($mailbox = null) {
// Check if the mailbox exists // Check if the mailbox exists
$stmt = $pdo->prepare("SELECT username FROM mailbox WHERE username = :mailbox AND active = '1'"); $stmt = $pdo->prepare("SELECT username FROM mailbox WHERE username = :mailbox AND active = '1'");
$stmt->execute(array(':mailbox' => $mailbox)); $stmt->execute(array(':mailbox' => $mailbox));
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row){ if ($row){
$mailbox_exists = true; $mailbox_exists = true;
} }
@@ -1056,7 +1056,7 @@ function update_sogo_static_view($mailbox = null) {
LEFT OUTER JOIN grouped_sender_acl_external external_acl ON external_acl.username = mailbox.username LEFT OUTER JOIN grouped_sender_acl_external external_acl ON external_acl.username = mailbox.username
WHERE WHERE
mailbox.active = '1'"; mailbox.active = '1'";
if ($mailbox_exists) { if ($mailbox_exists) {
$query .= " AND mailbox.username = :mailbox"; $query .= " AND mailbox.username = :mailbox";
$stmt = $pdo->prepare($query); $stmt = $pdo->prepare($query);
@@ -1065,21 +1065,25 @@ function update_sogo_static_view($mailbox = null) {
$query .= " GROUP BY mailbox.username"; $query .= " GROUP BY mailbox.username";
$stmt = $pdo->query($query); $stmt = $pdo->query($query);
} }
$stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');"); $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
flush_memcached(); flush_memcached();
} }
function edit_user_account($_data) { function edit_user_account($_data) {
global $lang; global $lang;
global $pdo; global $pdo;
$_data_log = $_data; $_data_log = $_data;
!isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*'; !isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
!isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*'; !isset($_data_log['user_new_pass2']) ?: $_data_log['user_new_pass2'] = '*';
!isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*'; !isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
$username = $_SESSION['mailcow_cc_username']; $username = $_SESSION['mailcow_cc_username'];
$role = $_SESSION['mailcow_cc_role']; $role = $_SESSION['mailcow_cc_role'];
$password_old = $_data['user_old_pass']; $password_old = $_data['user_old_pass'];
$pw_recovery_email = $_data['pw_recovery_email'];
if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') { if (filter_var($username, FILTER_VALIDATE_EMAIL === false) || $role != 'user') {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@@ -1088,20 +1092,24 @@ function edit_user_account($_data) {
); );
return false; return false;
} }
$stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
WHERE `kind` NOT REGEXP 'location|thing|group' // edit password
AND `username` = :user"); if (!empty($password_old) && !empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) {
$stmt->execute(array(':user' => $username)); $stmt = $pdo->prepare("SELECT `password` FROM `mailbox`
$row = $stmt->fetch(PDO::FETCH_ASSOC); WHERE `kind` NOT REGEXP 'location|thing|group'
if (!verify_hash($row['password'], $password_old)) { AND `username` = :user");
$_SESSION['return'][] = array( $stmt->execute(array(':user' => $username));
'type' => 'danger', $row = $stmt->fetch(PDO::FETCH_ASSOC);
'log' => array(__FUNCTION__, $_data_log),
'msg' => 'access_denied' if (!verify_hash($row['password'], $password_old)) {
); $_SESSION['return'][] = array(
return false; 'type' => 'danger',
} 'log' => array(__FUNCTION__, $_data_log),
if (!empty($_data['user_new_pass']) && !empty($_data['user_new_pass2'])) { 'msg' => 'access_denied'
);
return false;
}
$password_new = $_data['user_new_pass']; $password_new = $_data['user_new_pass'];
$password_new2 = $_data['user_new_pass2']; $password_new2 = $_data['user_new_pass2'];
if (password_check($password_new, $password_new2) !== true) { if (password_check($password_new, $password_new2) !== true) {
@@ -1116,8 +1124,29 @@ function edit_user_account($_data) {
':password_hashed' => $password_hashed, ':password_hashed' => $password_hashed,
':username' => $username ':username' => $username
)); ));
update_sogo_static_view();
} }
update_sogo_static_view(); // edit password recovery email
elseif (isset($pw_recovery_email)) {
if (!isset($_SESSION['acl']['pw_reset']) || $_SESSION['acl']['pw_reset'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
$pw_recovery_email = (!filter_var($pw_recovery_email, FILTER_VALIDATE_EMAIL)) ? '' : $pw_recovery_email;
$stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email)
WHERE `username` = :username");
$stmt->execute(array(
':recovery_email' => $pw_recovery_email,
':username' => $username
));
}
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_data_log), 'log' => array(__FUNCTION__, $_data_log),
@@ -1345,7 +1374,7 @@ function set_tfa($_data) {
$_data['registration']->certificate, $_data['registration']->certificate,
0 0
)); ));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_data_log), 'log' => array(__FUNCTION__, $_data_log),
@@ -1515,7 +1544,7 @@ function unset_tfa_key($_data) {
try { try {
if (!is_numeric($id)) $access_denied = true; if (!is_numeric($id)) $access_denied = true;
// set access_denied error // set access_denied error
if ($access_denied){ if ($access_denied){
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -1524,7 +1553,7 @@ function unset_tfa_key($_data) {
'msg' => 'access_denied' 'msg' => 'access_denied'
); );
return false; return false;
} }
// check if it's last key // check if it's last key
$stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa` $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa`
@@ -1560,7 +1589,7 @@ function unset_tfa_key($_data) {
} }
function get_tfa($username = null, $id = null) { function get_tfa($username = null, $id = null) {
global $pdo; global $pdo;
if (isset($_SESSION['mailcow_cc_username'])) { if (empty($username) && isset($_SESSION['mailcow_cc_username'])) {
$username = $_SESSION['mailcow_cc_username']; $username = $_SESSION['mailcow_cc_username'];
} }
elseif (empty($username)) { elseif (empty($username)) {
@@ -1573,7 +1602,7 @@ function get_tfa($username = null, $id = null) {
WHERE `username` = :username AND `active` = '1'"); WHERE `username` = :username AND `active` = '1'");
$stmt->execute(array(':username' => $username)); $stmt->execute(array(':username' => $username));
$results = $stmt->fetchAll(PDO::FETCH_ASSOC); $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
// no tfa methods found // no tfa methods found
if (count($results) == 0) { if (count($results) == 0) {
$data['name'] = 'none'; $data['name'] = 'none';
@@ -1781,8 +1810,8 @@ function verify_tfa_login($username, $_data) {
'msg' => array('webauthn_authenticator_failed') 'msg' => array('webauthn_authenticator_failed')
); );
return false; return false;
} }
if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) { if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@@ -2144,7 +2173,7 @@ function cors($action, $data = null) {
'msg' => 'access_denied' 'msg' => 'access_denied'
); );
return false; return false;
} }
$allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']); $allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']);
$allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins; $allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins;
@@ -2177,7 +2206,7 @@ function cors($action, $data = null) {
$redis->hMSet('CORS_SETTINGS', array( $redis->hMSet('CORS_SETTINGS', array(
'allowed_origins' => implode(', ', $allowed_origins), 'allowed_origins' => implode(', ', $allowed_origins),
'allowed_methods' => implode(', ', $allowed_methods) 'allowed_methods' => implode(', ', $allowed_methods)
)); ));
} catch (RedisException $e) { } catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@@ -2229,10 +2258,10 @@ function cors($action, $data = null) {
header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin'); header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin');
// Access-Control settings requested, this is just a preflight request // Access-Control settings requested, this is just a preflight request
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' && if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' &&
isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) && isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) &&
isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) { isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
$allowed_methods = explode(', ', $cors_settings["allowed_methods"]); $allowed_methods = explode(', ', $cors_settings["allowed_methods"]);
if (in_array($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'], $allowed_methods, true)) if (in_array($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'], $allowed_methods, true))
// method allowed send 200 OK // method allowed send 200 OK
@@ -2261,6 +2290,386 @@ function uuid4() {
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
} }
function reset_password($action, $data = null) {
global $pdo;
global $redis;
global $mailcow_hostname;
global $PW_RESET_TOKEN_LIMIT;
global $PW_RESET_TOKEN_LIFETIME;
$_data_log = $data;
if (isset($_data_log['new_password'])) $_data_log['new_password'] = '*';
if (isset($_data_log['new_password2'])) $_data_log['new_password2'] = '*';
switch ($action) {
case 'check':
$token = $data;
$stmt = $pdo->prepare("SELECT `t1`.`username` FROM `reset_password` AS `t1` JOIN `mailbox` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL :lifetime MINUTE) AND `t2`.`active` = 1;");
$stmt->execute(array(
':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token),
':lifetime' => $PW_RESET_TOKEN_LIFETIME
));
$return = $stmt->fetch(PDO::FETCH_ASSOC);
return empty($return['username']) ? false : $return['username'];
break;
case 'issue':
$username = $data;
// perform cleanup
$stmt = $pdo->prepare("DELETE FROM `reset_password` WHERE created < DATE_SUB(NOW(), INTERVAL :lifetime MINUTE);");
$stmt->execute(array(':lifetime' => $PW_RESET_TOKEN_LIFETIME));
if (filter_var($username, FILTER_VALIDATE_EMAIL) === false) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
$pw_reset_notification = reset_password('get_notification', 'raw');
if (!$pw_reset_notification) return false;
if (empty($pw_reset_notification['from']) || empty($pw_reset_notification['subject'])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'password_reset_na'
);
return false;
}
$stmt = $pdo->prepare("SELECT * FROM `mailbox`
WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$mailbox_data = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($mailbox_data)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'password_reset_invalid_user'
);
return false;
}
$mailbox_attr = json_decode($mailbox_data['attributes'], true);
if (empty($mailbox_attr['recovery_email']) || filter_var($mailbox_attr['recovery_email'], FILTER_VALIDATE_EMAIL) === false) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => "password_reset_invalid_user"
);
return false;
}
$stmt = $pdo->prepare("SELECT * FROM `reset_password`
WHERE `username` = :username");
$stmt->execute(array(':username' => $username));
$generated_token_count = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($generated_token_count >= $PW_RESET_TOKEN_LIMIT) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => "reset_token_limit_exceeded"
);
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 `reset_password` (`username`, `token`)
VALUES (:username, :token)");
$stmt->execute(array(
':username' => $username,
':token' => $token
));
$reset_link = getBaseURL() . "/reset-password?token=" . $token;
$request_date = new DateTime();
$locale_date = locale_get_default();
$date_formatter = new IntlDateFormatter(
$locale_date,
IntlDateFormatter::FULL,
IntlDateFormatter::FULL
);
$formatted_request_date = $date_formatter->format($request_date);
// set template vars
// subject
$pw_reset_notification['subject'] = str_replace('{{hostname}}', $mailcow_hostname, $pw_reset_notification['subject']);
$pw_reset_notification['subject'] = str_replace('{{link}}', $reset_link, $pw_reset_notification['subject']);
$pw_reset_notification['subject'] = str_replace('{{username}}', $username, $pw_reset_notification['subject']);
$pw_reset_notification['subject'] = str_replace('{{username2}}', $mailbox_attr['recovery_email'], $pw_reset_notification['subject']);
$pw_reset_notification['subject'] = str_replace('{{date}}', $formatted_request_date, $pw_reset_notification['subject']);
$pw_reset_notification['subject'] = str_replace('{{token_lifetime}}', $PW_RESET_TOKEN_LIFETIME, $pw_reset_notification['subject']);
// text
$pw_reset_notification['text_tmpl'] = str_replace('{{hostname}}', $mailcow_hostname, $pw_reset_notification['text_tmpl']);
$pw_reset_notification['text_tmpl'] = str_replace('{{link}}', $reset_link, $pw_reset_notification['text_tmpl']);
$pw_reset_notification['text_tmpl'] = str_replace('{{username}}', $username, $pw_reset_notification['text_tmpl']);
$pw_reset_notification['text_tmpl'] = str_replace('{{username2}}', $mailbox_attr['recovery_email'], $pw_reset_notification['text_tmpl']);
$pw_reset_notification['text_tmpl'] = str_replace('{{date}}', $formatted_request_date, $pw_reset_notification['text_tmpl']);
$pw_reset_notification['text_tmpl'] = str_replace('{{token_lifetime}}', $PW_RESET_TOKEN_LIFETIME, $pw_reset_notification['text_tmpl']);
// html
$pw_reset_notification['html_tmpl'] = str_replace('{{hostname}}', $mailcow_hostname, $pw_reset_notification['html_tmpl']);
$pw_reset_notification['html_tmpl'] = str_replace('{{link}}', $reset_link, $pw_reset_notification['html_tmpl']);
$pw_reset_notification['html_tmpl'] = str_replace('{{username}}', $username, $pw_reset_notification['html_tmpl']);
$pw_reset_notification['html_tmpl'] = str_replace('{{username2}}', $mailbox_attr['recovery_email'], $pw_reset_notification['html_tmpl']);
$pw_reset_notification['html_tmpl'] = str_replace('{{date}}', $formatted_request_date, $pw_reset_notification['html_tmpl']);
$pw_reset_notification['html_tmpl'] = str_replace('{{token_lifetime}}', $PW_RESET_TOKEN_LIFETIME, $pw_reset_notification['html_tmpl']);
$email_sent = reset_password('send_mail', array(
"from" => $pw_reset_notification['from'],
"to" => $mailbox_attr['recovery_email'],
"subject" => $pw_reset_notification['subject'],
"text" => $pw_reset_notification['text_tmpl'],
"html" => $pw_reset_notification['html_tmpl']
));
if (!$email_sent){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => "recovery_email_failed"
);
return false;
}
list($localPart, $domainPart) = explode('@', $mailbox_attr['recovery_email']);
if (strlen($localPart) > 1) {
$maskedLocalPart = $localPart[0] . str_repeat('*', strlen($localPart) - 1);
} else {
$maskedLocalPart = "*";
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => array("recovery_email_sent", $maskedLocalPart . '@' . $domainPart)
);
return array(
"username" => $username,
"issue" => "success"
);
break;
case 'reset':
$token = $data['token'];
$new_password = $data['new_password'];
$new_password2 = $data['new_password2'];
$username = $data['username'];
$check_tfa = $data['check_tfa'];
if (!$username || !$token) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'invalid_reset_token'
);
return false;
}
# check new password
if (!password_check($new_password, $new_password2)) {
return false;
}
if ($check_tfa){
// check for tfa authenticators
$authenticators = get_tfa($username);
if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) {
$_SESSION['pending_mailcow_cc_username'] = $username;
$_SESSION['pending_pw_reset_token'] = $token;
$_SESSION['pending_pw_new_password'] = $new_password;
$_SESSION['pending_tfa_methods'] = $authenticators['additional'];
$_SESSION['return'][] = array(
'type' => 'info',
'log' => array(__FUNCTION__, $user, '*'),
'msg' => 'awaiting_tfa_confirmation'
);
return false;
}
}
# set new password
$password_hashed = hash_password($new_password);
$stmt = $pdo->prepare("UPDATE `mailbox` SET
`password` = :password_hashed,
`attributes` = JSON_SET(`attributes`, '$.passwd_update', NOW())
WHERE `username` = :username");
$stmt->execute(array(
':password_hashed' => $password_hashed,
':username' => $username
));
// perform cleanup
$stmt = $pdo->prepare("DELETE FROM `reset_password` WHERE `username` = :username;");
$stmt->execute(array(
':username' => $username
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'password_changed_success'
);
return true;
break;
case 'get_notification':
$type = $data;
try {
$settings['from'] = $redis->Get('PW_RESET_FROM');
$settings['subject'] = $redis->Get('PW_RESET_SUBJ');
$settings['html_tmpl'] = $redis->Get('PW_RESET_HTML');
$settings['text_tmpl'] = $redis->Get('PW_RESET_TEXT');
if (empty($settings['html_tmpl']) && empty($settings['text_tmpl'])) {
$settings['html_tmpl'] = file_get_contents("/tpls/pw_reset_html.tpl");
$settings['text_tmpl'] = file_get_contents("/tpls/pw_reset_text.tpl");
}
if ($type != "raw") {
$settings['html_tmpl'] = htmlspecialchars($settings['html_tmpl']);
$settings['text_tmpl'] = htmlspecialchars($settings['text_tmpl']);
}
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => array('redis_error', $e)
);
return false;
}
return $settings;
break;
case 'send_mail':
$from = $data['from'];
$to = $data['to'];
$text = $data['text'];
$html = $data['html'];
$subject = $data['subject'];
if (!filter_var($from, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'from_invalid'
);
return false;
}
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'to_invalid'
);
return false;
}
if (empty($subject)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'subject_empty'
);
return false;
}
if (empty($text)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'text_empty'
);
return false;
}
ini_set('max_execution_time', 0);
ini_set('max_input_time', 0);
$mail = new PHPMailer;
$mail->Timeout = 10;
$mail->SMTPOptions = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true
)
);
$mail->isSMTP();
$mail->Host = 'postfix-mailcow';
$mail->SMTPAuth = false;
$mail->Port = 25;
$mail->setFrom($from);
$mail->Subject = $subject;
$mail->CharSet ="UTF-8";
if (!empty($html)) {
$mail->Body = $html;
$mail->AltBody = $text;
}
else {
$mail->Body = $text;
}
$mail->XMailer = 'MooMail';
$mail->AddAddress($to);
if (!$mail->send()) {
return false;
}
$mail->ClearAllRecipients();
return true;
break;
}
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'access_denied'
);
return false;
}
switch ($action) {
case 'edit_notification':
$subject = $data['subject'];
$from = preg_replace('/[\x00-\x1F\x80-\xFF]/', '', $data['from']);
$from = (!filter_var($from, FILTER_VALIDATE_EMAIL)) ? "" : $from;
$subject = (empty($subject)) ? "" : $subject;
$text = (empty($data['text_tmpl'])) ? "" : $data['text_tmpl'];
$html = (empty($data['html_tmpl'])) ? "" : $data['html_tmpl'];
try {
$redis->Set('PW_RESET_FROM', $from);
$redis->Set('PW_RESET_SUBJ', $subject);
$redis->Set('PW_RESET_HTML', $html);
$redis->Set('PW_RESET_TEXT', $text);
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => array('redis_error', $e)
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $action, $_data_log),
'msg' => 'saved_settings'
);
break;
}
}
function get_logs($application, $lines = false) { function get_logs($application, $lines = false) {
if ($lines === false) { if ($lines === false) {
+425 -156
View File
@@ -184,6 +184,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'msg' => 'global_filter_written' 'msg' => 'global_filter_written'
); );
return true; return true;
break;
case 'filter': case 'filter':
$sieve = new Sieve\SieveParser(); $sieve = new Sieve\SieveParser();
if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) { if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
@@ -478,16 +479,24 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
return false; return false;
} }
$DOMAIN_DEFAULT_ATTRIBUTES = null;
if ($_data['template']){
$DOMAIN_DEFAULT_ATTRIBUTES = mailbox('get', 'domain_templates', $_data['template'])['attributes'];
}
if (empty($DOMAIN_DEFAULT_ATTRIBUTES)) {
$DOMAIN_DEFAULT_ATTRIBUTES = mailbox('get', 'domain_templates')[0]['attributes'];
}
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46); $domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
$description = $_data['description']; $description = $_data['description'];
if (empty($description)) $description = $domain; if (empty($description)) $description = $domain;
$tags = (array)$_data['tags']; $tags = (isset($_data['tags'])) ? (array)$_data['tags'] : $DOMAIN_DEFAULT_ATTRIBUTES['tags'];
$aliases = (int)$_data['aliases']; $aliases = (isset($_data['aliases'])) ? (int)$_data['aliases'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_num_aliases_for_domain'];
$mailboxes = (int)$_data['mailboxes']; $mailboxes = (isset($_data['mailboxes'])) ? (int)$_data['mailboxes'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_num_mboxes_for_domain'];
$defquota = (int)$_data['defquota']; $defquota = (isset($_data['defquota'])) ? (int)$_data['defquota'] : $DOMAIN_DEFAULT_ATTRIBUTES['def_quota_for_mbox'] / 1024 ** 2;
$maxquota = (int)$_data['maxquota']; $maxquota = (isset($_data['maxquota'])) ? (int)$_data['maxquota'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_quota_for_mbox'] / 1024 ** 2;
$restart_sogo = (int)$_data['restart_sogo']; $restart_sogo = (int)$_data['restart_sogo'];
$quota = (int)$_data['quota']; $quota = (isset($_data['quota'])) ? (int)$_data['quota'] : $DOMAIN_DEFAULT_ATTRIBUTES['max_quota_for_domain'] / 1024 ** 2;
if ($defquota > $maxquota) { if ($defquota > $maxquota) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@@ -520,11 +529,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
return false; return false;
} }
$active = intval($_data['active']); $active = (isset($_data['active'])) ? intval($_data['active']) : $DOMAIN_DEFAULT_ATTRIBUTES['active'];
$relay_all_recipients = intval($_data['relay_all_recipients']); $relay_all_recipients = (isset($_data['relay_all_recipients'])) ? intval($_data['relay_all_recipients']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_all_recipients'];
$relay_unknown_only = intval($_data['relay_unknown_only']); $relay_unknown_only = (isset($_data['relay_unknown_only'])) ? intval($_data['relay_unknown_only']) : $DOMAIN_DEFAULT_ATTRIBUTES['relay_unknown_only'];
$backupmx = intval($_data['backupmx']); $backupmx = (isset($_data['backupmx'])) ? intval($_data['backupmx']) : $DOMAIN_DEFAULT_ATTRIBUTES['backupmx'];
$gal = intval($_data['gal']); $gal = (isset($_data['gal'])) ? intval($_data['gal']) : $DOMAIN_DEFAULT_ATTRIBUTES['gal'];
if ($relay_all_recipients == 1) { if ($relay_all_recipients == 1) {
$backupmx = '1'; $backupmx = '1';
} }
@@ -625,9 +634,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
return false; return false;
} }
if (!empty(intval($_data['rl_value']))) { $_data['rl_value'] = (isset($_data['rl_value'])) ? intval($_data['rl_value']) : $DOMAIN_DEFAULT_ATTRIBUTES['rl_value'];
$_data['rl_frame'] = (isset($_data['rl_frame'])) ? $_data['rl_frame'] : $DOMAIN_DEFAULT_ATTRIBUTES['rl_frame'];
if (!empty($_data['rl_value']) && !empty($_data['rl_frame'])){
ratelimit('edit', 'domain', array('rl_value' => $_data['rl_value'], 'rl_frame' => $_data['rl_frame'], 'object' => $domain)); ratelimit('edit', 'domain', array('rl_value' => $_data['rl_value'], 'rl_frame' => $_data['rl_frame'], 'object' => $domain));
} }
$_data['key_size'] = (isset($_data['key_size'])) ? intval($_data['key_size']) : $DOMAIN_DEFAULT_ATTRIBUTES['key_size'];
$_data['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : $DOMAIN_DEFAULT_ATTRIBUTES['dkim_selector'];
if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) { if (!empty($_data['key_size']) && !empty($_data['dkim_selector'])) {
if (!empty($redis->hGet('DKIM_SELECTORS', $domain))) { if (!empty($redis->hGet('DKIM_SELECTORS', $domain))) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -1006,11 +1019,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
return false; return false;
} }
if (empty($name)) {
$name = $local_part;
}
$template_attr = null;
if ($_data['template']){
$template_attr = mailbox('get', 'mailbox_templates', $_data['template'])['attributes'];
}
if (empty($template_attr)) {
$template_attr = mailbox('get', 'mailbox_templates')[0]['attributes'];
}
$MAILBOX_DEFAULT_ATTRIBUTES = array_merge($MAILBOX_DEFAULT_ATTRIBUTES, $template_attr);
$password = $_data['password']; $password = $_data['password'];
$password2 = $_data['password2']; $password2 = $_data['password2'];
$name = ltrim(rtrim($_data['name'], '>'), '<'); $name = ltrim(rtrim($_data['name'], '>'), '<');
$tags = $_data['tags']; $tags = (isset($_data['tags'])) ? $_data['tags'] : $MAILBOX_DEFAULT_ATTRIBUTES['tags'];
$quota_m = intval($_data['quota']); $quota_m = (isset($_data['quota'])) ? intval($_data['quota']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['quota']) / 1024 ** 2;
if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) { if ((!isset($_SESSION['acl']['unlimited_quota']) || $_SESSION['acl']['unlimited_quota'] != "1") && $quota_m === 0) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@@ -1019,9 +1044,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
return false; return false;
} }
if (empty($name)) {
$name = $local_part;
}
if (isset($_data['protocol_access'])) { if (isset($_data['protocol_access'])) {
$_data['protocol_access'] = (array)$_data['protocol_access']; $_data['protocol_access'] = (array)$_data['protocol_access'];
$_data['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0; $_data['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
@@ -1029,7 +1052,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $_data['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
} }
$active = intval($_data['active']); $active = (isset($_data['active'])) ? intval($_data['active']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['active']);
$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']); $force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update']);
$tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']); $tls_enforce_in = (isset($_data['tls_enforce_in'])) ? intval($_data['tls_enforce_in']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_in']);
$tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']); $tls_enforce_out = (isset($_data['tls_enforce_out'])) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
@@ -1210,7 +1233,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':active' => $active ':active' => $active
)); ));
if (isset($_data['acl'])) { if (isset($_data['acl'])) {
$_data['acl'] = (array)$_data['acl']; $_data['acl'] = (array)$_data['acl'];
$_data['spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0; $_data['spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0;
@@ -1227,12 +1250,31 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0; $_data['quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
$_data['quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0; $_data['quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
$_data['app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0; $_data['app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
$_data['pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0;
} else {
$_data['spam_alias'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_alias']);
$_data['tls_policy'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_tls_policy']);
$_data['spam_score'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_score']);
$_data['spam_policy'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_spam_policy']);
$_data['delimiter_action'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_delimiter_action']);
$_data['syncjobs'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_syncjobs']);
$_data['eas_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_eas_reset']);
$_data['sogo_profile_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_sogo_profile_reset']);
$_data['pushover'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pushover']);
$_data['quarantine'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine']);
$_data['quarantine_attachments'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_attachments']);
$_data['quarantine_notification'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_notification']);
$_data['quarantine_category'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_quarantine_category']);
$_data['app_passwds'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_app_passwds']);
$_data['pw_reset'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['acl_pw_reset']);
}
$stmt = $pdo->prepare("INSERT INTO `user_acl` try {
$stmt = $pdo->prepare("INSERT INTO `user_acl`
(`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`, (`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`,
`pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`) `pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`, `pw_reset`)
VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset, VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset,
:pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds) "); :pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds, :pw_reset) ");
$stmt->execute(array( $stmt->execute(array(
':username' => $username, ':username' => $username,
':spam_alias' => $_data['spam_alias'], ':spam_alias' => $_data['spam_alias'],
@@ -1248,34 +1290,21 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':quarantine_attachments' => $_data['quarantine_attachments'], ':quarantine_attachments' => $_data['quarantine_attachments'],
':quarantine_notification' => $_data['quarantine_notification'], ':quarantine_notification' => $_data['quarantine_notification'],
':quarantine_category' => $_data['quarantine_category'], ':quarantine_category' => $_data['quarantine_category'],
':app_passwds' => $_data['app_passwds'] ':app_passwds' => $_data['app_passwds'],
':pw_reset' => $_data['pw_reset']
)); ));
} }
else { catch (PDOException $e) {
$stmt = $pdo->prepare("INSERT INTO `user_acl` $_SESSION['return'][] = array(
(`username`, `spam_alias`, `tls_policy`, `spam_score`, `spam_policy`, `delimiter_action`, `syncjobs`, `eas_reset`, `sogo_profile_reset`, 'type' => 'danger',
`pushover`, `quarantine`, `quarantine_attachments`, `quarantine_notification`, `quarantine_category`, `app_passwds`) 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
VALUES (:username, :spam_alias, :tls_policy, :spam_score, :spam_policy, :delimiter_action, :syncjobs, :eas_reset, :sogo_profile_reset, 'msg' => $e->getMessage()
:pushover, :quarantine, :quarantine_attachments, :quarantine_notification, :quarantine_category, :app_passwds) "); );
$stmt->execute(array( return false;
':username' => $username,
':spam_alias' => 0,
':tls_policy' => 0,
':spam_score' => 0,
':spam_policy' => 0,
':delimiter_action' => 0,
':syncjobs' => 0,
':eas_reset' => 0,
':sogo_profile_reset' => 0,
':pushover' => 0,
':quarantine' => 0,
':quarantine_attachments' => 0,
':quarantine_notification' => 0,
':quarantine_category' => 0,
':app_passwds' => 0
));
} }
$_data['rl_frame'] = (isset($_data['rl_frame'])) ? $_data['rl_frame'] : $MAILBOX_DEFAULT_ATTRIBUTES['rl_frame'];
$_data['rl_value'] = (isset($_data['rl_value'])) ? $_data['rl_value'] : $MAILBOX_DEFAULT_ATTRIBUTES['rl_value'];
if (isset($_data['rl_frame']) && isset($_data['rl_value'])){ if (isset($_data['rl_frame']) && isset($_data['rl_value'])){
ratelimit('edit', 'mailbox', array( ratelimit('edit', 'mailbox', array(
'object' => $username, 'object' => $username,
@@ -1438,7 +1467,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
return false; return false;
} }
// check attributes // check attributes
$attr = array(); $attr = array();
$attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array(); $attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array();
@@ -1524,17 +1553,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr["tls_enforce_out"] = isset($_data['tls_enforce_out']) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']); $attr["tls_enforce_out"] = isset($_data['tls_enforce_out']) ? intval($_data['tls_enforce_out']) : intval($MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out']);
if (isset($_data['protocol_access'])) { if (isset($_data['protocol_access'])) {
$_data['protocol_access'] = (array)$_data['protocol_access']; $_data['protocol_access'] = (array)$_data['protocol_access'];
$attr['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']); $attr['imap_access'] = (in_array('imap', $_data['protocol_access'])) ? 1 : 0;
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']); $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']); $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']); $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
} }
else { else {
$attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']); $attr['imap_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['imap_access']);
$attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']); $attr['pop3_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['pop3_access']);
$attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']); $attr['smtp_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['smtp_access']);
$attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']); $attr['sieve_access'] = intval($MAILBOX_DEFAULT_ATTRIBUTES['sieve_access']);
} }
if (isset($_data['acl'])) { if (isset($_data['acl'])) {
$_data['acl'] = (array)$_data['acl']; $_data['acl'] = (array)$_data['acl'];
$attr['acl_spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0; $attr['acl_spam_alias'] = (in_array('spam_alias', $_data['acl'])) ? 1 : 0;
@@ -1551,6 +1580,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['acl_quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0; $attr['acl_quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
$attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0; $attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
$attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0; $attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
$attr['acl_pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0;
} else { } else {
$_data['acl'] = (array)$_data['acl']; $_data['acl'] = (array)$_data['acl'];
$attr['acl_spam_alias'] = 0; $attr['acl_spam_alias'] = 0;
@@ -2079,7 +2109,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
return false; return false;
} }
// check if param is whitelisted // check if param is whitelisted
if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){ if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){
// bad option // bad option
@@ -2772,11 +2802,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
// check name // check name
if ($is_now["template"] == "Default" && $is_now["template"] != $_data["template"]){ if ($is_now["template"] == "Default" && $is_now["template"] != $_data["template"]){
// keep template name of Default template // keep template name of Default template
$_data["template"] = $is_now["template"]; $_data["template"] = $is_now["template"];
} }
else { else {
$_data["template"] = (isset($_data["template"])) ? $_data["template"] : $is_now["template"]; $_data["template"] = (isset($_data["template"])) ? $_data["template"] : $is_now["template"];
} }
// check attributes // check attributes
$attr = array(); $attr = array();
$attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array(); $attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array();
@@ -2803,10 +2833,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
":id" => $id , ":id" => $id ,
":template" => $_data["template"] , ":template" => $_data["template"] ,
":attributes" => json_encode($attr) ":attributes" => json_encode($attr)
)); ));
} }
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -2840,21 +2870,22 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $_data['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
} }
if (!empty($is_now)) { if (!empty($is_now)) {
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
(int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']); (int)$force_pw_update = (isset($_data['force_pw_update'])) ? intval($_data['force_pw_update']) : intval($is_now['attributes']['force_pw_update']);
(int)$sogo_access = (isset($_data['sogo_access']) && isset($_SESSION['acl']['sogo_access']) && $_SESSION['acl']['sogo_access'] == "1") ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']); (int)$sogo_access = (isset($_data['sogo_access']) && isset($_SESSION['acl']['sogo_access']) && $_SESSION['acl']['sogo_access'] == "1") ? intval($_data['sogo_access']) : intval($is_now['attributes']['sogo_access']);
(int)$imap_access = (isset($_data['imap_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']); (int)$imap_access = (isset($_data['imap_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['imap_access']) : intval($is_now['attributes']['imap_access']);
(int)$pop3_access = (isset($_data['pop3_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']); (int)$pop3_access = (isset($_data['pop3_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['pop3_access']) : intval($is_now['attributes']['pop3_access']);
(int)$smtp_access = (isset($_data['smtp_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']); (int)$smtp_access = (isset($_data['smtp_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['smtp_access']) : intval($is_now['attributes']['smtp_access']);
(int)$sieve_access = (isset($_data['sieve_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']); (int)$sieve_access = (isset($_data['sieve_access']) && isset($_SESSION['acl']['protocol_access']) && $_SESSION['acl']['protocol_access'] == "1") ? intval($_data['sieve_access']) : intval($is_now['attributes']['sieve_access']);
(int)$relayhost = (isset($_data['relayhost']) && isset($_SESSION['acl']['mailbox_relayhost']) && $_SESSION['acl']['mailbox_relayhost'] == "1") ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']); (int)$relayhost = (isset($_data['relayhost']) && isset($_SESSION['acl']['mailbox_relayhost']) && $_SESSION['acl']['mailbox_relayhost'] == "1") ? intval($_data['relayhost']) : intval($is_now['attributes']['relayhost']);
(int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576); (int)$quota_m = (isset_has_content($_data['quota'])) ? intval($_data['quota']) : ($is_now['quota'] / 1048576);
$name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name']; $name = (!empty($_data['name'])) ? ltrim(rtrim($_data['name'], '>'), '<') : $is_now['name'];
$domain = $is_now['domain']; $domain = $is_now['domain'];
$quota_b = $quota_m * 1048576; $quota_b = $quota_m * 1048576;
$password = (!empty($_data['password'])) ? $_data['password'] : null; $password = (!empty($_data['password'])) ? $_data['password'] : null;
$password2 = (!empty($_data['password2'])) ? $_data['password2'] : null; $password2 = (!empty($_data['password2'])) ? $_data['password2'] : null;
$tags = (is_array($_data['tags']) ? $_data['tags'] : array()); $pw_recovery_email = (isset($_data['pw_recovery_email'])) ? $_data['pw_recovery_email'] : $is_now['attributes']['recovery_email'];
$tags = (is_array($_data['tags']) ? $_data['tags'] : array());
} }
else { else {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -3107,31 +3138,43 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':address' => $username, ':address' => $username,
':active' => $active ':active' => $active
)); ));
$stmt = $pdo->prepare("UPDATE `mailbox` SET try {
`active` = :active, $stmt = $pdo->prepare("UPDATE `mailbox` SET
`name`= :name, `active` = :active,
`quota` = :quota_b, `name`= :name,
`attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update), `quota` = :quota_b,
`attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access), `attributes` = JSON_SET(`attributes`, '$.force_pw_update', :force_pw_update),
`attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access), `attributes` = JSON_SET(`attributes`, '$.sogo_access', :sogo_access),
`attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access), `attributes` = JSON_SET(`attributes`, '$.imap_access', :imap_access),
`attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access), `attributes` = JSON_SET(`attributes`, '$.sieve_access', :sieve_access),
`attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost), `attributes` = JSON_SET(`attributes`, '$.pop3_access', :pop3_access),
`attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access) `attributes` = JSON_SET(`attributes`, '$.relayhost', :relayhost),
WHERE `username` = :username"); `attributes` = JSON_SET(`attributes`, '$.smtp_access', :smtp_access),
$stmt->execute(array( `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email)
':active' => $active, WHERE `username` = :username");
':name' => $name, $stmt->execute(array(
':quota_b' => $quota_b, ':active' => $active,
':force_pw_update' => $force_pw_update, ':name' => $name,
':sogo_access' => $sogo_access, ':quota_b' => $quota_b,
':imap_access' => $imap_access, ':force_pw_update' => $force_pw_update,
':pop3_access' => $pop3_access, ':sogo_access' => $sogo_access,
':sieve_access' => $sieve_access, ':imap_access' => $imap_access,
':smtp_access' => $smtp_access, ':pop3_access' => $pop3_access,
':relayhost' => $relayhost, ':sieve_access' => $sieve_access,
':username' => $username ':smtp_access' => $smtp_access,
)); ':recovery_email' => $pw_recovery_email,
':relayhost' => $relayhost,
':username' => $username
));
}
catch (PDOException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => $e->getMessage()
);
return false;
}
// save tags // save tags
foreach($tags as $index => $tag){ foreach($tags as $index => $tag){
if (empty($tag)) continue; if (empty($tag)) continue;
@@ -3149,7 +3192,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
':tag_name' => $tag, ':tag_name' => $tag,
)); ));
} }
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
@@ -3160,6 +3203,197 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
return true; return true;
break; break;
case 'mailbox_rename':
$domain = $_data['domain'];
$old_local_part = $_data['old_local_part'];
$old_username = $old_local_part . "@" . $domain;
$new_local_part = $_data['new_local_part'];
$new_username = $new_local_part . "@" . $domain;
$create_alias = intval($_data['create_alias']);
if (!filter_var($old_username, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('username_invalid', $old_username)
);
return false;
}
if (!filter_var($new_username, FILTER_VALIDATE_EMAIL)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('username_invalid', $new_username)
);
return false;
}
$is_now = mailbox('get', 'mailbox_details', $old_username);
if (empty($is_now)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $is_now['domain'])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
// get imap acls
try {
$exec_fields = array(
'cmd' => 'doveadm',
'task' => 'get_acl',
'id' => $old_username
);
$imap_acls = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true);
// delete imap acls
foreach ($imap_acls as $imap_acl) {
$exec_fields = array(
'cmd' => 'doveadm',
'task' => 'delete_acl',
'user' => $imap_acl['user'],
'mailbox' => $imap_acl['mailbox'],
'id' => $imap_acl['id']
);
docker('post', 'dovecot-mailcow', 'exec', $exec_fields);
}
} catch (Exception $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => $e->getMessage()
);
return false;
}
// rename username in sql
try {
$pdo->beginTransaction();
$pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
// Update username in mailbox table
$pdo->prepare('UPDATE mailbox SET username = :new_username, local_part = :new_local_part WHERE username = :old_username')
->execute([
':new_username' => $new_username,
':new_local_part' => $new_local_part,
':old_username' => $old_username
]);
$pdo->prepare("UPDATE alias SET address = :new_username, goto = :new_username2 WHERE address = :old_username")
->execute([
':new_username' => $new_username,
':new_username2' => $new_username,
':old_username' => $old_username
]);
// Update the username in all related tables
$tables = [
'tags_mailbox' => ['username'],
'sieve_filters' => ['username'],
'app_passwd' => ['mailbox'],
'user_acl' => ['username'],
'da_acl' => ['username'],
'quota2' => ['username'],
'quota2replica' => ['username'],
'pushover' => ['username'],
'alias' => ['goto'],
"imapsync" => ['user2'],
'bcc_maps' => ['local_dest', 'bcc_dest'],
'recipient_maps' => ['old_dest', 'new_dest'],
'sender_acl' => ['logged_in_as', 'send_as']
];
foreach ($tables as $table => $columns) {
foreach ($columns as $column) {
$stmt = $pdo->prepare("UPDATE $table SET $column = :new_username WHERE $column = :old_username")
->execute([
':new_username' => $new_username,
':old_username' => $old_username
]);
}
}
// Update c_uid, c_name and mail in _sogo_static_view table
$pdo->prepare("UPDATE _sogo_static_view SET c_uid = :new_username, c_name = :new_username2, mail = :new_username3 WHERE c_uid = :old_username")
->execute([
':new_username' => $new_username,
':new_username2' => $new_username,
':new_username3' => $new_username,
':old_username' => $old_username
]);
// Re-enable foreign key checks
$pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
$pdo->commit();
} catch (PDOException $e) {
// Rollback the transaction if something goes wrong
$pdo->rollBack();
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => $e->getMessage()
);
return false;
}
// move maildir
$exec_fields = array(
'cmd' => 'maildir',
'task' => 'move',
'old_maildir' => $domain . '/' . $old_local_part,
'new_maildir' => $domain . '/' . $new_local_part
);
docker('post', 'dovecot-mailcow', 'exec', $exec_fields);
// rename username in sogo
$exec_fields = array(
'cmd' => 'sogo',
'task' => 'rename_user',
'old_username' => $old_username,
'new_username' => $new_username
);
docker('post', 'sogo-mailcow', 'exec', $exec_fields);
// set imap acls
foreach ($imap_acls as $imap_acl) {
$user_id = ($imap_acl['id'] == $old_username) ? $new_username : $imap_acl['id'];
$user = ($imap_acl['user'] == $old_username) ? $new_username : $imap_acl['user'];
$exec_fields = array(
'cmd' => 'doveadm',
'task' => 'set_acl',
'user' => $user,
'mailbox' => $imap_acl['mailbox'],
'id' => $user_id,
'rights' => $imap_acl['rights']
);
docker('post', 'dovecot-mailcow', 'exec', $exec_fields);
}
// create alias
if ($create_alias == 1) {
mailbox("add", "alias", array(
"address" => $old_username,
"goto" => $new_username,
"active" => 1,
"sogo_visible" => 1,
"private_comment" => sprintf($lang['success']['mailbox_renamed'], $old_username, $new_username)
));
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_renamed', $old_username, $new_username)
);
break;
case 'mailbox_templates': case 'mailbox_templates':
if ($_SESSION['mailcow_cc_role'] != "admin") { if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -3192,11 +3426,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
// check name // check name
if ($is_now["template"] == "Default" && $is_now["template"] != $_data["template"]){ if ($is_now["template"] == "Default" && $is_now["template"] != $_data["template"]){
// keep template name of Default template // keep template name of Default template
$_data["template"] = $is_now["template"]; $_data["template"] = $is_now["template"];
} }
else { else {
$_data["template"] = (isset($_data["template"])) ? $_data["template"] : $is_now["template"]; $_data["template"] = (isset($_data["template"])) ? $_data["template"] : $is_now["template"];
} }
// check attributes // check attributes
$attr = array(); $attr = array();
$attr["quota"] = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0; $attr["quota"] = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0;
@@ -3216,11 +3450,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0; $attr['pop3_access'] = (in_array('pop3', $_data['protocol_access'])) ? 1 : 0;
$attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0; $attr['smtp_access'] = (in_array('smtp', $_data['protocol_access'])) ? 1 : 0;
$attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0; $attr['sieve_access'] = (in_array('sieve', $_data['protocol_access'])) ? 1 : 0;
} }
else { else {
foreach ($is_now as $key => $value){ foreach ($is_now as $key => $value){
$attr[$key] = $is_now[$key]; $attr[$key] = $is_now[$key];
} }
} }
if (isset($_data['acl'])) { if (isset($_data['acl'])) {
$_data['acl'] = (array)$_data['acl']; $_data['acl'] = (array)$_data['acl'];
@@ -3238,10 +3472,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr['acl_quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0; $attr['acl_quarantine_notification'] = (in_array('quarantine_notification', $_data['acl'])) ? 1 : 0;
$attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0; $attr['acl_quarantine_category'] = (in_array('quarantine_category', $_data['acl'])) ? 1 : 0;
$attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0; $attr['acl_app_passwds'] = (in_array('app_passwds', $_data['acl'])) ? 1 : 0;
} else { $attr['acl_pw_reset'] = (in_array('pw_reset', $_data['acl'])) ? 1 : 0;
} else {
foreach ($is_now as $key => $value){ foreach ($is_now as $key => $value){
$attr[$key] = $is_now[$key]; $attr[$key] = $is_now[$key];
} }
} }
@@ -3253,7 +3488,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
":id" => $id , ":id" => $id ,
":template" => $_data["template"] , ":template" => $_data["template"] ,
":attributes" => json_encode($attr) ":attributes" => json_encode($attr)
)); ));
} }
@@ -3282,7 +3517,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
continue; continue;
} }
$is_now = mailbox('get', 'mailbox_details', $mailbox); $is_now = mailbox('get', 'mailbox_details', $mailbox);
if(!empty($is_now)){ if(!empty($is_now)){
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $is_now['domain'])) { if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $is_now['domain'])) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -3309,15 +3544,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array( $stmt->execute(array(
":username" => $mailbox, ":username" => $mailbox,
":custom_attributes" => json_encode($attributes) ":custom_attributes" => json_encode($attributes)
)); ));
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $mailbox) 'msg' => array('mailbox_modified', $mailbox)
); );
} }
return true; return true;
break; break;
case 'resource': case 'resource':
@@ -3399,7 +3634,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
} }
break; break;
case 'domain_wide_footer': case 'domain_wide_footer':
if (!is_array($_data['domains'])) { if (!is_array($_data['domains'])) {
$domains = array(); $domains = array();
$domains[] = $_data['domains']; $domains[] = $_data['domains'];
@@ -3411,31 +3646,56 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$footers = array(); $footers = array();
$footers['html'] = isset($_data['html']) ? $_data['html'] : ''; $footers['html'] = isset($_data['html']) ? $_data['html'] : '';
$footers['plain'] = isset($_data['plain']) ? $_data['plain'] : ''; $footers['plain'] = isset($_data['plain']) ? $_data['plain'] : '';
$footers['skip_replies'] = isset($_data['skip_replies']) ? (int)$_data['skip_replies'] : 0;
$footers['mbox_exclude'] = array(); $footers['mbox_exclude'] = array();
if (isset($_data["mbox_exclude"])){ $footers['alias_domain_exclude'] = array();
if (!is_array($_data["mbox_exclude"])) { if (isset($_data["exclude"])){
$_data["mbox_exclude"] = array($_data["mbox_exclude"]); if (!is_array($_data["exclude"])) {
$_data["exclude"] = array($_data["exclude"]);
} }
foreach ($_data["mbox_exclude"] as $mailbox) { foreach ($_data["exclude"] as $exclude) {
if (!filter_var($mailbox, FILTER_VALIDATE_EMAIL)) { if (filter_var($exclude, FILTER_VALIDATE_EMAIL)) {
$stmt = $pdo->prepare("SELECT `address` FROM `alias` WHERE `address` = :address
UNION
SELECT `username` FROM `mailbox` WHERE `username` = :username");
$stmt->execute(array(
':address' => $exclude,
':username' => $exclude,
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if(!$row){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('username_invalid', $exclude)
);
continue;
}
array_push($footers['mbox_exclude'], $exclude);
}
elseif (is_valid_domain_name($exclude)) {
$stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `alias_domain` = :alias_domain");
$stmt->execute(array(
':alias_domain' => $exclude,
));
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if(!$row){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('username_invalid', $exclude)
);
continue;
}
array_push($footers['alias_domain_exclude'], $exclude);
}
else {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('username_invalid', $mailbox) 'msg' => array('username_invalid', $exclude)
); );
continue;
} }
$is_now = mailbox('get', 'mailbox_details', $mailbox);
if(empty($is_now)){
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('username_invalid', $mailbox)
);
continue;
}
array_push($footers['mbox_exclude'], $mailbox);
} }
} }
foreach ($domains as $domain) { foreach ($domains as $domain) {
@@ -3460,12 +3720,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
try { try {
$stmt = $pdo->prepare("DELETE FROM `domain_wide_footer` WHERE `domain`= :domain"); $stmt = $pdo->prepare("DELETE FROM `domain_wide_footer` WHERE `domain`= :domain");
$stmt->execute(array(':domain' => $domain)); $stmt->execute(array(':domain' => $domain));
$stmt = $pdo->prepare("INSERT INTO `domain_wide_footer` (`domain`, `html`, `plain`, `mbox_exclude`) VALUES (:domain, :html, :plain, :mbox_exclude)"); $stmt = $pdo->prepare("INSERT INTO `domain_wide_footer` (`domain`, `html`, `plain`, `mbox_exclude`, `alias_domain_exclude`, `skip_replies`) VALUES (:domain, :html, :plain, :mbox_exclude, :alias_domain_exclude, :skip_replies)");
$stmt->execute(array( $stmt->execute(array(
':domain' => $domain, ':domain' => $domain,
':html' => $footers['html'], ':html' => $footers['html'],
':plain' => $footers['plain'], ':plain' => $footers['plain'],
':mbox_exclude' => json_encode($footers['mbox_exclude']), ':mbox_exclude' => json_encode($footers['mbox_exclude']),
':alias_domain_exclude' => json_encode($footers['alias_domain_exclude']),
':skip_replies' => $footers['skip_replies'],
)); ));
} }
catch (PDOException $e) { catch (PDOException $e) {
@@ -3625,7 +3887,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
// prepend domain to array // prepend domain to array
$params = array(); $params = array();
foreach ($tags as $key => $val){ foreach ($tags as $key => $val){
array_push($params, '%'.$_data.'%'); array_push($params, '%'.$_data.'%');
array_push($params, '%'.$val.'%'); array_push($params, '%'.$val.'%');
} }
@@ -3634,7 +3896,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) { while($row = array_shift($rows)) {
if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], explode('@', $row['username'])[1])) if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], explode('@', $row['username'])[1]))
$mailboxes[] = $row['username']; $mailboxes[] = $row['username'];
} }
} }
@@ -4189,7 +4451,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
while($row = array_shift($rows)) { while($row = array_shift($rows)) {
if ($_SESSION['mailcow_cc_role'] == "admin") if ($_SESSION['mailcow_cc_role'] == "admin")
$domains[] = $row['domain']; $domains[] = $row['domain'];
elseif (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['domain'])) elseif (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $row['domain']))
$domains[] = $row['domain']; $domains[] = $row['domain'];
} }
} else { } else {
@@ -4289,6 +4551,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$domaindata['mboxes_in_domain'] = $MailboxDataDomain['count']; $domaindata['mboxes_in_domain'] = $MailboxDataDomain['count'];
$domaindata['mboxes_left'] = $row['mailboxes'] - $MailboxDataDomain['count']; $domaindata['mboxes_left'] = $row['mailboxes'] - $MailboxDataDomain['count'];
$domaindata['domain_name'] = $row['domain']; $domaindata['domain_name'] = $row['domain'];
$domaindata['domain_h_name'] = idn_to_utf8($row['domain']);
$domaindata['description'] = $row['description']; $domaindata['description'] = $row['description'];
$domaindata['max_num_aliases_for_domain'] = $row['aliases']; $domaindata['max_num_aliases_for_domain'] = $row['aliases'];
$domaindata['max_num_mboxes_for_domain'] = $row['mailboxes']; $domaindata['max_num_mboxes_for_domain'] = $row['mailboxes'];
@@ -4348,19 +4611,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
$_data = (isset($_data)) ? intval($_data) : null; $_data = (isset($_data)) ? intval($_data) : null;
if (isset($_data)){ if (isset($_data)){
$stmt = $pdo->prepare("SELECT * FROM `templates` $stmt = $pdo->prepare("SELECT * FROM `templates`
WHERE `id` = :id AND type = :type"); WHERE `id` = :id AND type = :type");
$stmt->execute(array( $stmt->execute(array(
":id" => $_data, ":id" => $_data,
":type" => "domain" ":type" => "domain"
)); ));
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($row)){ if (empty($row)){
return false; return false;
} }
$row["attributes"] = json_decode($row["attributes"], true); $row["attributes"] = json_decode($row["attributes"], true);
return $row; return $row;
} }
@@ -4368,11 +4631,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt = $pdo->prepare("SELECT * FROM `templates` WHERE `type` = 'domain'"); $stmt = $pdo->prepare("SELECT * FROM `templates` WHERE `type` = 'domain'");
$stmt->execute(); $stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($rows)){ if (empty($rows)){
return false; return false;
} }
foreach($rows as $key => $row){ foreach($rows as $key => $row){
$rows[$key]["attributes"] = json_decode($row["attributes"], true); $rows[$key]["attributes"] = json_decode($row["attributes"], true);
} }
@@ -4435,7 +4698,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$mailboxdata['active'] = $row['active']; $mailboxdata['active'] = $row['active'];
$mailboxdata['active_int'] = $row['active']; $mailboxdata['active_int'] = $row['active'];
$mailboxdata['domain'] = $row['domain']; $mailboxdata['domain'] = $row['domain'];
$mailboxdata['relayhost'] = $row['relayhost'];
$mailboxdata['name'] = $row['name']; $mailboxdata['name'] = $row['name'];
$mailboxdata['local_part'] = $row['local_part']; $mailboxdata['local_part'] = $row['local_part'];
$mailboxdata['quota'] = $row['quota']; $mailboxdata['quota'] = $row['quota'];
@@ -4475,6 +4737,9 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
else if ($SaslLogs['service'] == 'pop3') { else if ($SaslLogs['service'] == 'pop3') {
$last_pop3_login = strtotime($SaslLogs['datetime']); $last_pop3_login = strtotime($SaslLogs['datetime']);
}
else if ($SaslLogs['service'] == 'SSO') {
$last_sso_login = strtotime($SaslLogs['datetime']);
} }
} }
if (!isset($last_imap_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) { if (!isset($last_imap_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
@@ -4485,10 +4750,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
if (!isset($last_pop3_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) { if (!isset($last_pop3_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
$last_pop3_login = 0; $last_pop3_login = 0;
}
if (!isset($last_sso_login) || $GLOBALS['SHOW_LAST_LOGIN'] === false) {
$last_sso_login = 0;
} }
$mailboxdata['last_imap_login'] = $last_imap_login; $mailboxdata['last_imap_login'] = $last_imap_login;
$mailboxdata['last_smtp_login'] = $last_smtp_login; $mailboxdata['last_smtp_login'] = $last_smtp_login;
$mailboxdata['last_pop3_login'] = $last_pop3_login; $mailboxdata['last_pop3_login'] = $last_pop3_login;
$mailboxdata['last_sso_login'] = $last_sso_login;
if (!isset($_extra) || $_extra != 'reduced') { if (!isset($_extra) || $_extra != 'reduced') {
$rl = ratelimit('get', 'mailbox', $_data); $rl = ratelimit('get', 'mailbox', $_data);
@@ -4539,19 +4808,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
$_data = (isset($_data)) ? intval($_data) : null; $_data = (isset($_data)) ? intval($_data) : null;
if (isset($_data)){ if (isset($_data)){
$stmt = $pdo->prepare("SELECT * FROM `templates` $stmt = $pdo->prepare("SELECT * FROM `templates`
WHERE `id` = :id AND type = :type"); WHERE `id` = :id AND type = :type");
$stmt->execute(array( $stmt->execute(array(
":id" => $_data, ":id" => $_data,
":type" => "mailbox" ":type" => "mailbox"
)); ));
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
if (empty($row)){ if (empty($row)){
return false; return false;
} }
$row["attributes"] = json_decode($row["attributes"], true); $row["attributes"] = json_decode($row["attributes"], true);
return $row; return $row;
} }
@@ -4622,7 +4891,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
} }
try { try {
$stmt = $pdo->prepare("SELECT `html`, `plain`, `mbox_exclude` FROM `domain_wide_footer` $stmt = $pdo->prepare("SELECT `html`, `plain`, `mbox_exclude`, `alias_domain_exclude`, `skip_replies` FROM `domain_wide_footer`
WHERE `domain` = :domain"); WHERE `domain` = :domain");
$stmt->execute(array( $stmt->execute(array(
':domain' => $domain ':domain' => $domain
@@ -4993,7 +5262,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$ids = $_data['ids']; $ids = $_data['ids'];
} }
foreach ($ids as $id) { foreach ($ids as $id) {
// delete template // delete template
$stmt = $pdo->prepare("DELETE FROM `templates` $stmt = $pdo->prepare("DELETE FROM `templates`
@@ -5160,7 +5429,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'msg' => 'Could not move maildir to garbage collector: variables local_part and/or domain empty' 'msg' => 'Could not move maildir to garbage collector: variables local_part and/or domain empty'
); );
} }
if (strtolower(getenv('SKIP_SOLR')) == 'n') { if (strtolower(getenv('SKIP_SOLR')) == 'n' && strtolower(getenv('FLATCURVE_EXPERIMENTAL')) != 'y') {
$curl = curl_init(); $curl = curl_init();
curl_setopt($curl, CURLOPT_URL, 'http://solr:8983/solr/dovecot-fts/update?commit=true'); curl_setopt($curl, CURLOPT_URL, 'http://solr:8983/solr/dovecot-fts/update?commit=true');
curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: text/xml')); curl_setopt($curl, CURLOPT_HTTPHEADER,array('Content-Type: text/xml'));
@@ -5306,7 +5575,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
continue; continue;
} }
update_sogo_static_view($username); update_sogo_static_view($username);
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
@@ -5333,7 +5602,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$ids = $_data['ids']; $ids = $_data['ids'];
} }
foreach ($ids as $id) { foreach ($ids as $id) {
// delete template // delete template
$stmt = $pdo->prepare("DELETE FROM `templates` $stmt = $pdo->prepare("DELETE FROM `templates`
@@ -5342,7 +5611,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
":id" => $id, ":id" => $id,
":type" => "mailbox", ":type" => "mailbox",
":template" => "Default" ":template" => "Default"
)); ));
} }
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -5416,7 +5685,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
} }
break; break;
case 'tags_domain': case 'tags_domain':
if (!is_array($_data['domain'])) { if (!is_array($_data['domain'])) {
$domains = array(); $domains = array();
$domains[] = $_data['domain']; $domains[] = $_data['domain'];
@@ -5429,7 +5698,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$wasModified = false; $wasModified = false;
foreach ($domains as $domain) { foreach ($domains as $domain) {
if (!is_valid_domain_name($domain)) { if (!is_valid_domain_name($domain)) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
@@ -5446,7 +5715,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
); );
return false; return false;
} }
foreach($tags as $tag){ foreach($tags as $tag){
// delete tag // delete tag
$wasModified = true; $wasModified = true;
@@ -5501,7 +5770,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
// delete tags // delete tags
foreach($tags as $tag){ foreach($tags as $tag){
$wasModified = true; $wasModified = true;
$stmt = $pdo->prepare("DELETE FROM `tags_mailbox` WHERE `username` = :username AND `tag_name` = :tag_name"); $stmt = $pdo->prepare("DELETE FROM `tags_mailbox` WHERE `username` = :username AND `tag_name` = :tag_name");
$stmt->execute(array( $stmt->execute(array(
':username' => $username, ':username' => $username,
+16 -7
View File
@@ -143,17 +143,26 @@ function rspamd_maps($_action, $_data = null) {
return false; return false;
} }
$maps = (array)$_data['map']; $maps = (array)$_data['map'];
$valid_maps = array();
foreach ($maps as $map) { foreach ($maps as $map) {
$is_valid = false;
foreach ($RSPAMD_MAPS as $rspamd_map_type) { foreach ($RSPAMD_MAPS as $rspamd_map_type) {
if (!in_array($map, $rspamd_map_type)) { if (in_array($map, $rspamd_map_type)) {
$_SESSION['return'][] = array( $is_valid = true;
'type' => 'danger', break;
'log' => array(__FUNCTION__, $_action, '-'),
'msg' => array('global_map_invalid', $map)
);
continue;
} }
} }
if ($is_valid) {
array_push($valid_maps, $map);
} else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, '-'),
'msg' => array('global_map_invalid', $map)
);
}
}
foreach ($valid_maps as $map) {
try { try {
if (file_exists('/rspamd_custom_maps/' . $map)) { if (file_exists('/rspamd_custom_maps/' . $map)) {
$map_content = trim($_data['rspamd_map_data']); $map_content = trim($_data['rspamd_map_data']);
-1
View File
@@ -49,7 +49,6 @@ $globalVariables = [
'app_links' => customize('get', 'app_links'), 'app_links' => customize('get', 'app_links'),
'is_root_uri' => (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) == '/'), 'is_root_uri' => (parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) == '/'),
'uri' => $_SERVER['REQUEST_URI'], 'uri' => $_SERVER['REQUEST_URI'],
'last_login' => last_login('get', $_SESSION['mailcow_cc_username'], 7, 0)['ui']['time']
]; ];
foreach ($globalVariables as $globalVariableName => $globalVariableValue) { foreach ($globalVariables as $globalVariableName => $globalVariableValue) {
+29 -1
View File
@@ -3,7 +3,7 @@ function init_db_schema() {
try { try {
global $pdo; global $pdo;
$db_version = "21112023_1644"; $db_version = "29072024_1000";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -273,6 +273,8 @@ function init_db_schema() {
"html" => "LONGTEXT", "html" => "LONGTEXT",
"plain" => "LONGTEXT", "plain" => "LONGTEXT",
"mbox_exclude" => "JSON NOT NULL DEFAULT ('[]')", "mbox_exclude" => "JSON NOT NULL DEFAULT ('[]')",
"alias_domain_exclude" => "JSON NOT NULL DEFAULT ('[]')",
"skip_replies" => "TINYINT(1) NOT NULL DEFAULT '0'"
), ),
"keys" => array( "keys" => array(
"primary" => array( "primary" => array(
@@ -481,6 +483,7 @@ function init_db_schema() {
"quarantine_notification" => "TINYINT(1) NOT NULL DEFAULT '1'", "quarantine_notification" => "TINYINT(1) NOT NULL DEFAULT '1'",
"quarantine_category" => "TINYINT(1) NOT NULL DEFAULT '1'", "quarantine_category" => "TINYINT(1) NOT NULL DEFAULT '1'",
"app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'", "app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'",
"pw_reset" => "TINYINT(1) NOT NULL DEFAULT '1'",
), ),
"keys" => array( "keys" => array(
"primary" => array( "primary" => array(
@@ -692,6 +695,19 @@ function init_db_schema() {
), ),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
), ),
"reset_password" => array(
"cols" => array(
"username" => "VARCHAR(255) NOT NULL",
"token" => "VARCHAR(255) NOT NULL",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
),
"keys" => array(
"primary" => array(
"" => array("token", "created")
),
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"imapsync" => array( "imapsync" => array(
"cols" => array( "cols" => array(
"id" => "INT NOT NULL AUTO_INCREMENT", "id" => "INT NOT NULL AUTO_INCREMENT",
@@ -977,6 +993,18 @@ function init_db_schema() {
), ),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
), ),
"sogo_admin" => array(
"cols" => array(
"c_key" => "VARCHAR(255) NOT NULL DEFAULT ''",
"c_content" => "mediumtext NOT NULL",
),
"keys" => array(
"primary" => array(
"" => array("c_key")
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"pushover" => array( "pushover" => array(
"cols" => array( "cols" => array(
"username" => "VARCHAR(255) NOT NULL", "username" => "VARCHAR(255) NOT NULL",
+162 -16
View File
@@ -1039,6 +1039,73 @@
}, },
"time": "2017-04-19T22:01:50+00:00" "time": "2017-04-19T22:01:50+00:00"
}, },
{
"name": "symfony/deprecation-contracts",
"version": "v3.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-04-18T09:32:20+00:00"
},
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.24.0", "version": "v1.24.0",
@@ -1287,6 +1354,82 @@
], ],
"time": "2021-09-13T13:58:33+00:00" "time": "2021-09-13T13:58:33+00:00"
}, },
{
"name": "symfony/polyfill-php81",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php81\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-09T11:45:10+00:00"
},
{ {
"name": "symfony/translation", "name": "symfony/translation",
"version": "v6.0.5", "version": "v6.0.5",
@@ -1604,34 +1747,37 @@
}, },
{ {
"name": "twig/twig", "name": "twig/twig",
"version": "v3.4.3", "version": "v3.14.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/twigphp/Twig.git", "url": "https://github.com/twigphp/Twig.git",
"reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58" "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/c38fd6b0b7f370c198db91ffd02e23b517426b58", "url": "https://api.github.com/repos/twigphp/Twig/zipball/126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"reference": "c38fd6b0b7f370c198db91ffd02e23b517426b58", "reference": "126b2c97818dbff0cdf3fbfc881aedb3d40aae72",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": ">=7.2.5", "php": ">=8.0.2",
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/polyfill-ctype": "^1.8", "symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3" "symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php81": "^1.29"
}, },
"require-dev": { "require-dev": {
"psr/container": "^1.0", "psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0"
}, },
"type": "library", "type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
}
},
"autoload": { "autoload": {
"files": [
"src/Resources/core.php",
"src/Resources/debug.php",
"src/Resources/escaper.php",
"src/Resources/string_loader.php"
],
"psr-4": { "psr-4": {
"Twig\\": "src/" "Twig\\": "src/"
} }
@@ -1664,7 +1810,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/twigphp/Twig/issues", "issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.4.3" "source": "https://github.com/twigphp/Twig/tree/v3.14.0"
}, },
"funding": [ "funding": [
{ {
@@ -1676,7 +1822,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2022-09-28T08:42:51+00:00" "time": "2024-09-09T17:55:12+00:00"
}, },
{ {
"name": "yubico/u2flib-server", "name": "yubico/u2flib-server",
@@ -1728,5 +1874,5 @@
"prefer-lowest": false, "prefer-lowest": false,
"platform": [], "platform": [],
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.3.0" "plugin-api-version": "2.6.0"
} }
@@ -0,0 +1,33 @@
<?xml version='1.0' standalone='yes'?>
<extension name="enotify">
<command name="notify">
<parameter type="tag" name="from" occurrence="optional">
<parameter type="string" name="from-address" />
</parameter>
<parameter type="tag" name="importance" regex="(1|2|3)" occurrence="optional" />
<parameter type="tag" name="options" occurrence="optional">
<parameter type="stringlist" name="option-strings" />
</parameter>
<parameter type="tag" name="message" occurrence="optional">
<parameter type="string" name="message-text" />
</parameter>
<parameter type="string" name="method" />
</command>
<test name="valid_notify_method">
<parameter type="stringlist" name="notification-uris" />
</test>
<test name="notify_method_capability">
<parameter type="string" name="notification-uri" />
<parameter type="string" name="notification-capability" />
<parameter type="stringlist" name="key-list" />
</test>
<modifier name="encodeurl" />
</extension>
@@ -0,0 +1,58 @@
<?xml version='1.0' standalone='yes'?>
<extension name="mime">
<command name="foreverypart">
<parameter type="string" name="name" occurrence="optional" />
<block />
</command>
<command name="break">
<parameter type="string" name="name" occurrence="optional" />
</command>
<tagged-argument extends="(header|address|exists)">
<parameter type="tag" name="mime" regex="mime" occurrence="optional" />
</tagged-argument>
<tagged-argument extends="(header|address|exists)">
<parameter type="tag" name="anychild" regex="anychild" occurrence="optional" />
</tagged-argument>
<tagged-argument extends="(header)">
<parameter type="tag" name="type" occurrence="optional" />
</tagged-argument>
<tagged-argument extends="(header)">
<parameter type="tag" name="subtype" occurrence="optional" />
</tagged-argument>
<tagged-argument extends="(header)">
<parameter type="tag" name="contenttype" occurrence="optional" />
</tagged-argument>
<tagged-argument extends="(header)">
<parameter type="tag" name="param" regex="param" occurrence="optional">
<parameter type="stringlist" name="param-list" />
</parameter>
</tagged-argument>
<tagged-argument extends="(header|address|exists)">
<parameter type="stringlist" name="header-names" />
</tagged-argument>
<tagged-argument extends="(header)">
<parameter type="stringlist" name="key-list" />
</tagged-argument>
<action name="replace">
<parameter type="tag" name="mime" regex="mime" occurrence="optional" />
<parameter type="string" name="subject" occurrence="optional" />
<parameter type="string" name="from" occurrence="optional" />
<parameter type="string" name="replacement" />
</action>
<action name="enclose">
<parameter type="string" name="subject" occurrence="optional" />
<parameter type="stringlist" name="headers" occurrence="optional" />
<parameter type="string" name="text" />
</action>
<action name="extracttext">
<parameter type="tag" name="first" regex="first" occurrence="optional" />
<parameter type="number" name="number" occurrence="optional" />
<parameter type="string" name="varname" />
</action>
</extension>
+622
View File
@@ -0,0 +1,622 @@
<?php
/*
* Helper functions for building a DataTables server-side processing SQL query
*
* The static functions in this class are just helper functions to help build
* the SQL used in the DataTables demo server-side processing scripts. These
* functions obviously do not represent all that can be done with server-side
* processing, they are intentionally simple to show how it works. More complex
* server-side processing operations will likely require a custom script.
*
* See https://datatables.net/usage/server-side for full details on the server-
* side processing requirements of DataTables.
*
* @license MIT - https://datatables.net/license_mit
*/
class SSP {
/**
* Create the data output array for the DataTables rows
*
* @param array $columns Column information array
* @param array $data Data from the SQL get
* @return array Formatted data in a row based format
*/
static function data_output ( $columns, $data )
{
$out = array();
for ( $i=0, $ien=count($data) ; $i<$ien ; $i++ ) {
$row = array();
for ( $j=0, $jen=count($columns) ; $j<$jen ; $j++ ) {
$column = $columns[$j];
// Is there a formatter?
if ( isset( $column['formatter'] ) ) {
if(empty($column['db'])){
$row[ $column['dt'] ] = $column['formatter']( $data[$i] );
}
else{
$row[ $column['dt'] ] = $column['formatter']( $data[$i][ $column['db'] ], $data[$i] );
}
}
else {
if(!empty($column['db']) && (!isset($column['dummy']) || $column['dummy'] !== true)){
$row[ $column['dt'] ] = $data[$i][ $columns[$j]['db'] ];
}
else{
$row[ $column['dt'] ] = "";
}
}
}
$out[] = $row;
}
return $out;
}
/**
* Database connection
*
* Obtain an PHP PDO connection from a connection details array
*
* @param array $conn SQL connection details. The array should have
* the following properties
* * host - host name
* * db - database name
* * user - user name
* * pass - user password
* * Optional: `'charset' => 'utf8'` - you might need this depending on your PHP / MySQL config
* @return resource PDO connection
*/
static function db ( $conn )
{
if ( is_array( $conn ) ) {
return self::sql_connect( $conn );
}
return $conn;
}
/**
* Paging
*
* Construct the LIMIT clause for server-side processing SQL query
*
* @param array $request Data sent to server by DataTables
* @param array $columns Column information array
* @return string SQL limit clause
*/
static function limit ( $request, $columns )
{
$limit = '';
if ( isset($request['start']) && $request['length'] != -1 ) {
$limit = "LIMIT ".intval($request['start']).", ".intval($request['length']);
}
return $limit;
}
/**
* Ordering
*
* Construct the ORDER BY clause for server-side processing SQL query
*
* @param array $request Data sent to server by DataTables
* @param array $columns Column information array
* @return string SQL order by clause
*/
static function order ( $tableAS, $request, $columns )
{
$select = '';
$order = '';
if ( isset($request['order']) && count($request['order']) ) {
$selects = [];
$orderBy = [];
$dtColumns = self::pluck( $columns, 'dt' );
for ( $i=0, $ien=count($request['order']) ; $i<$ien ; $i++ ) {
// Convert the column index into the column data property
$columnIdx = intval($request['order'][$i]['column']);
$requestColumn = $request['columns'][$columnIdx];
$columnIdx = array_search( $columnIdx, $dtColumns );
$column = $columns[ $columnIdx ];
if ( $requestColumn['orderable'] == 'true' ) {
$dir = $request['order'][$i]['dir'] === 'asc' ?
'ASC' :
'DESC';
if(isset($column['order_subquery'])) {
$selects[] = '('.$column['order_subquery'].') AS `'.$column['db'].'_count`';
$orderBy[] = '`'.$column['db'].'_count` '.$dir;
} else {
$orderBy[] = '`'.$tableAS.'`.`'.$column['db'].'` '.$dir;
}
}
}
if ( count( $selects ) ) {
$select = ', '.implode(', ', $selects);
}
if ( count( $orderBy ) ) {
$order = 'ORDER BY '.implode(', ', $orderBy);
}
}
return [$select, $order];
}
/**
* Searching / Filtering
*
* Construct the WHERE clause for server-side processing SQL query.
*
* NOTE this does not match the built-in DataTables filtering which does it
* word by word on any field. It's possible to do here performance on large
* databases would be very poor
*
* @param array $request Data sent to server by DataTables
* @param array $columns Column information array
* @param array $bindings Array of values for PDO bindings, used in the
* sql_exec() function
* @return string SQL where clause
*/
static function filter ( $tablesAS, $request, $columns, &$bindings )
{
$globalSearch = array();
$columnSearch = array();
$joins = array();
$dtColumns = self::pluck( $columns, 'dt' );
if ( isset($request['search']) && $request['search']['value'] != '' ) {
$str = $request['search']['value'];
for ( $i=0, $ien=count($request['columns']) ; $i<$ien ; $i++ ) {
$requestColumn = $request['columns'][$i];
$columnIdx = array_search( $i, $dtColumns );
$column = $columns[ $columnIdx ];
if ( $requestColumn['searchable'] == 'true' ) {
if(!empty($column['db'])){
$binding = self::bind( $bindings, '%'.$str.'%', PDO::PARAM_STR );
if(isset($column['search']['join'])) {
$joins[] = $column['search']['join'];
$globalSearch[] = $column['search']['where_column'].' LIKE '.$binding;
} else {
$globalSearch[] = "`".$tablesAS."`.`".$column['db']."` LIKE ".$binding;
}
}
}
}
}
// Individual column filtering
if ( isset( $request['columns'] ) ) {
for ( $i=0, $ien=count($request['columns']) ; $i<$ien ; $i++ ) {
$requestColumn = $request['columns'][$i];
$columnIdx = array_search( $requestColumn['data'], $dtColumns );
$column = $columns[ $columnIdx ];
$str = $requestColumn['search']['value'];
if ( $requestColumn['searchable'] == 'true' &&
$str != '' ) {
if(!empty($column['db'])){
$binding = self::bind( $bindings, '%'.$str.'%', PDO::PARAM_STR );
$columnSearch[] = "`".$tablesAS."`.`".$column['db']."` LIKE ".$binding;
}
}
}
}
// Combine the filters into a single string
$where = '';
if ( count( $globalSearch ) ) {
$where = '('.implode(' OR ', $globalSearch).')';
}
if ( count( $columnSearch ) ) {
$where = $where === '' ?
implode(' AND ', $columnSearch) :
$where .' AND '. implode(' AND ', $columnSearch);
}
$join = '';
if( count($joins) ) {
$join = implode(' ', $joins);
}
if ( $where !== '' ) {
$where = 'WHERE '.$where;
}
return [$join, $where];
}
/**
* Perform the SQL queries needed for an server-side processing requested,
* utilising the helper functions of this class, limit(), order() and
* filter() among others. The returned array is ready to be encoded as JSON
* in response to an SSP request, or can be modified if needed before
* sending back to the client.
*
* @param array $request Data sent to server by DataTables
* @param array|PDO $conn PDO connection resource or connection parameters array
* @param string $table SQL table to query
* @param string $primaryKey Primary key of the table
* @param array $columns Column information array
* @return array Server-side processing response array
*/
static function simple ( $request, $conn, $table, $primaryKey, $columns )
{
$bindings = array();
$db = self::db( $conn );
// Allow for a JSON string to be passed in
if (isset($request['json'])) {
$request = json_decode($request['json'], true);
}
// table AS
$tablesAS = null;
if(is_array($table)) {
$tablesAS = $table[1];
$table = $table[0];
}
// Build the SQL query string from the request
list($select, $order) = self::order( $tablesAS, $request, $columns );
$limit = self::limit( $request, $columns );
list($join, $where) = self::filter( $tablesAS, $request, $columns, $bindings );
// Main query to actually get the data
$data = self::sql_exec( $db, $bindings,
"SELECT `$tablesAS`.`".implode("`, `$tablesAS`.`", self::pluck($columns, 'db'))."`
$select
FROM `$table` AS `$tablesAS`
$join
$where
GROUP BY `{$tablesAS}`.`{$primaryKey}`
$order
$limit"
);
// Data set length after filtering
$resFilterLength = self::sql_exec( $db, $bindings,
"SELECT COUNT(DISTINCT `{$tablesAS}`.`{$primaryKey}`)
FROM `$table` AS `$tablesAS`
$join
$where"
);
$recordsFiltered = $resFilterLength[0][0];
// Total data set length
$resTotalLength = self::sql_exec( $db,
"SELECT COUNT(`{$tablesAS}`.`{$primaryKey}`)
FROM `$table` AS `$tablesAS`"
);
$recordsTotal = $resTotalLength[0][0];
/*
* Output
*/
return array(
"draw" => isset ( $request['draw'] ) ?
intval( $request['draw'] ) :
0,
"recordsTotal" => intval( $recordsTotal ),
"recordsFiltered" => intval( $recordsFiltered ),
"data" => self::data_output( $columns, $data )
);
}
/**
* The difference between this method and the `simple` one, is that you can
* apply additional `where` conditions to the SQL queries. These can be in
* one of two forms:
*
* * 'Result condition' - This is applied to the result set, but not the
* overall paging information query - i.e. it will not effect the number
* of records that a user sees they can have access to. This should be
* used when you want apply a filtering condition that the user has sent.
* * 'All condition' - This is applied to all queries that are made and
* reduces the number of records that the user can access. This should be
* used in conditions where you don't want the user to ever have access to
* particular records (for example, restricting by a login id).
*
* In both cases the extra condition can be added as a simple string, or if
* you are using external values, as an assoc. array with `condition` and
* `bindings` parameters. The `condition` is a string with the SQL WHERE
* condition and `bindings` is an assoc. array of the binding names and
* values.
*
* @param array $request Data sent to server by DataTables
* @param array|PDO $conn PDO connection resource or connection parameters array
* @param string|array $table SQL table to query, if array second key is AS
* @param string $primaryKey Primary key of the table
* @param array $columns Column information array
* @param string $join JOIN sql string
* @param string|array $whereResult WHERE condition to apply to the result set
* @return array Server-side processing response array
*/
static function complex (
$request,
$conn,
$table,
$primaryKey,
$columns,
$join=null,
$whereResult=null
) {
$bindings = array();
$db = self::db( $conn );
// table AS
$tablesAS = null;
if(is_array($table)) {
$tablesAS = $table[1];
$table = $table[0];
}
// Build the SQL query string from the request
list($select, $order) = self::order( $tablesAS, $request, $columns );
$limit = self::limit( $request, $columns );
list($join_filter, $where) = self::filter( $tablesAS, $request, $columns, $bindings );
// whereResult can be a simple string, or an assoc. array with a
// condition and bindings
if ( $whereResult ) {
$str = $whereResult;
if ( is_array($whereResult) ) {
$str = $whereResult['condition'];
if ( isset($whereResult['bindings']) ) {
self::add_bindings($bindings, $whereResult);
}
}
$where = $where ?
$where .' AND '.$str :
'WHERE '.$str;
}
// Main query to actually get the data
$data = self::sql_exec( $db, $bindings,
"SELECT `$tablesAS`.`".implode("`, `$tablesAS`.`", self::pluck($columns, 'db'))."`
$select
FROM `$table` AS `$tablesAS`
$join
$join_filter
$where
GROUP BY `{$tablesAS}`.`{$primaryKey}`
$order
$limit"
);
// Data set length after filtering
$resFilterLength = self::sql_exec( $db, $bindings,
"SELECT COUNT(DISTINCT `{$tablesAS}`.`{$primaryKey}`)
FROM `$table` AS `$tablesAS`
$join
$join_filter
$where"
);
$recordsFiltered = (isset($resFilterLength[0])) ? $resFilterLength[0][0] : 0;
// Total data set length
$resTotalLength = self::sql_exec( $db, $bindings,
"SELECT COUNT(`{$tablesAS}`.`{$primaryKey}`)
FROM `$table` AS `$tablesAS`
$join
$join_filter
$where"
);
$recordsTotal = (isset($resTotalLength[0])) ? $resTotalLength[0][0] : 0;
/*
* Output
*/
return array(
"draw" => isset ( $request['draw'] ) ?
intval( $request['draw'] ) :
0,
"recordsTotal" => intval( $recordsTotal ),
"recordsFiltered" => intval( $recordsFiltered ),
"data" => self::data_output( $columns, $data )
);
}
/**
* Connect to the database
*
* @param array $sql_details SQL server connection details array, with the
* properties:
* * host - host name
* * db - database name
* * user - user name
* * pass - user password
* @return resource Database connection handle
*/
static function sql_connect ( $sql_details )
{
try {
$db = @new PDO(
"mysql:host={$sql_details['host']};dbname={$sql_details['db']}",
$sql_details['user'],
$sql_details['pass'],
array( PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION )
);
}
catch (PDOException $e) {
self::fatal(
"An error occurred while connecting to the database. ".
"The error reported by the server was: ".$e->getMessage()
);
}
return $db;
}
/**
* Execute an SQL query on the database
*
* @param resource $db Database handler
* @param array $bindings Array of PDO binding values from bind() to be
* used for safely escaping strings. Note that this can be given as the
* SQL query string if no bindings are required.
* @param string $sql SQL query to execute.
* @return array Result from the query (all rows)
*/
static function sql_exec ( $db, $bindings, $sql=null )
{
// Argument shifting
if ( $sql === null ) {
$sql = $bindings;
}
$stmt = $db->prepare( $sql );
// Bind parameters
if ( is_array( $bindings ) ) {
for ( $i=0, $ien=count($bindings) ; $i<$ien ; $i++ ) {
$binding = $bindings[$i];
$stmt->bindValue( $binding['key'], $binding['val'], $binding['type'] );
}
}
// Execute
try {
$stmt->execute();
}
catch (PDOException $e) {
self::fatal( "An SQL error occurred: ".$e->getMessage() );
}
// Return all
return $stmt->fetchAll( PDO::FETCH_BOTH );
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Internal methods
*/
/**
* Throw a fatal error.
*
* This writes out an error message in a JSON string which DataTables will
* see and show to the user in the browser.
*
* @param string $msg Message to send to the client
*/
static function fatal ( $msg )
{
echo json_encode( array(
"error" => $msg
) );
exit(0);
}
/**
* Create a PDO binding key which can be used for escaping variables safely
* when executing a query with sql_exec()
*
* @param array &$a Array of bindings
* @param * $val Value to bind
* @param int $type PDO field type
* @return string Bound key to be used in the SQL where this parameter
* would be used.
*/
static function bind ( &$a, $val, $type )
{
$key = ':binding_'.count( $a );
$a[] = array(
'key' => $key,
'val' => $val,
'type' => $type
);
return $key;
}
static function add_bindings(&$bindings, $vals)
{
foreach($vals['bindings'] as $key => $value) {
$bindings[] = array(
'key' => $key,
'val' => $value,
'type' => PDO::PARAM_STR
);
}
}
/**
* Pull a particular property from each assoc. array in a numeric array,
* returning and array of the property values from each item.
*
* @param array $a Array to get data from
* @param string $prop Property to read
* @return array Array of property values
*/
static function pluck ( $a, $prop )
{
$out = array();
for ( $i=0, $len=count($a) ; $i<$len ; $i++ ) {
if ( empty($a[$i][$prop]) && $a[$i][$prop] !== 0 ) {
continue;
}
if ( $prop == 'db' && isset($a[$i]['dummy']) && $a[$i]['dummy'] === true ) {
continue;
}
//removing the $out array index confuses the filter method in doing proper binding,
//adding it ensures that the array data are mapped correctly
$out[$i] = $a[$i][$prop];
}
return $out;
}
/**
* Return a string from an array or a string
*
* @param array|string $a Array to join
* @param string $join Glue for the concatenation
* @return string Joined string
*/
static function _flatten ( $a, $join = ' AND ' )
{
if ( ! $a ) {
return '';
}
else if ( $a && is_array($a) ) {
return implode( $join, $a );
}
return $a;
}
}
+15 -2
View File
@@ -3,8 +3,21 @@
// autoload.php @generated by Composer // autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) { if (PHP_VERSION_ID < 50600) {
echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; if (!headers_sent()) {
exit(1); header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
} }
require_once __DIR__ . '/composer/autoload_real.php'; require_once __DIR__ . '/composer/autoload_real.php';
+72 -65
View File
@@ -42,35 +42,37 @@ namespace Composer\Autoload;
*/ */
class ClassLoader class ClassLoader
{ {
/** @var ?string */ /** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir; private $vendorDir;
// PSR-4 // PSR-4
/** /**
* @var array[] * @var array<string, array<string, int>>
* @psalm-var array<string, array<string, int>>
*/ */
private $prefixLengthsPsr4 = array(); private $prefixLengthsPsr4 = array();
/** /**
* @var array[] * @var array<string, list<string>>
* @psalm-var array<string, array<int, string>>
*/ */
private $prefixDirsPsr4 = array(); private $prefixDirsPsr4 = array();
/** /**
* @var array[] * @var list<string>
* @psalm-var array<string, string>
*/ */
private $fallbackDirsPsr4 = array(); private $fallbackDirsPsr4 = array();
// PSR-0 // PSR-0
/** /**
* @var array[] * List of PSR-0 prefixes
* @psalm-var array<string, array<string, string[]>> *
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/ */
private $prefixesPsr0 = array(); private $prefixesPsr0 = array();
/** /**
* @var array[] * @var list<string>
* @psalm-var array<string, string>
*/ */
private $fallbackDirsPsr0 = array(); private $fallbackDirsPsr0 = array();
@@ -78,8 +80,7 @@ class ClassLoader
private $useIncludePath = false; private $useIncludePath = false;
/** /**
* @var string[] * @var array<string, string>
* @psalm-var array<string, string>
*/ */
private $classMap = array(); private $classMap = array();
@@ -87,29 +88,29 @@ class ClassLoader
private $classMapAuthoritative = false; private $classMapAuthoritative = false;
/** /**
* @var bool[] * @var array<string, bool>
* @psalm-var array<string, bool>
*/ */
private $missingClasses = array(); private $missingClasses = array();
/** @var ?string */ /** @var string|null */
private $apcuPrefix; private $apcuPrefix;
/** /**
* @var self[] * @var array<string, self>
*/ */
private static $registeredLoaders = array(); private static $registeredLoaders = array();
/** /**
* @param ?string $vendorDir * @param string|null $vendorDir
*/ */
public function __construct($vendorDir = null) public function __construct($vendorDir = null)
{ {
$this->vendorDir = $vendorDir; $this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
} }
/** /**
* @return string[] * @return array<string, list<string>>
*/ */
public function getPrefixes() public function getPrefixes()
{ {
@@ -121,8 +122,7 @@ class ClassLoader
} }
/** /**
* @return array[] * @return array<string, list<string>>
* @psalm-return array<string, array<int, string>>
*/ */
public function getPrefixesPsr4() public function getPrefixesPsr4()
{ {
@@ -130,8 +130,7 @@ class ClassLoader
} }
/** /**
* @return array[] * @return list<string>
* @psalm-return array<string, string>
*/ */
public function getFallbackDirs() public function getFallbackDirs()
{ {
@@ -139,8 +138,7 @@ class ClassLoader
} }
/** /**
* @return array[] * @return list<string>
* @psalm-return array<string, string>
*/ */
public function getFallbackDirsPsr4() public function getFallbackDirsPsr4()
{ {
@@ -148,8 +146,7 @@ class ClassLoader
} }
/** /**
* @return string[] Array of classname => path * @return array<string, string> Array of classname => path
* @psalm-return array<string, string>
*/ */
public function getClassMap() public function getClassMap()
{ {
@@ -157,8 +154,7 @@ class ClassLoader
} }
/** /**
* @param string[] $classMap Class to filename map * @param array<string, string> $classMap Class to filename map
* @psalm-param array<string, string> $classMap
* *
* @return void * @return void
*/ */
@@ -175,24 +171,25 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix, either * Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix. * appending or prepending to the ones previously set for this prefix.
* *
* @param string $prefix The prefix * @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 root directories * @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories * @param bool $prepend Whether to prepend the directories
* *
* @return void * @return void
*/ */
public function add($prefix, $paths, $prepend = false) public function add($prefix, $paths, $prepend = false)
{ {
$paths = (array) $paths;
if (!$prefix) { if (!$prefix) {
if ($prepend) { if ($prepend) {
$this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0 = array_merge(
(array) $paths, $paths,
$this->fallbackDirsPsr0 $this->fallbackDirsPsr0
); );
} else { } else {
$this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0, $this->fallbackDirsPsr0,
(array) $paths $paths
); );
} }
@@ -201,19 +198,19 @@ class ClassLoader
$first = $prefix[0]; $first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) { if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths; $this->prefixesPsr0[$first][$prefix] = $paths;
return; return;
} }
if ($prepend) { if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths, $paths,
$this->prefixesPsr0[$first][$prefix] $this->prefixesPsr0[$first][$prefix]
); );
} else { } else {
$this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix], $this->prefixesPsr0[$first][$prefix],
(array) $paths $paths
); );
} }
} }
@@ -222,9 +219,9 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace, either * Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace. * appending or prepending to the ones previously set for this namespace.
* *
* @param string $prefix The prefix/namespace, with trailing '\\' * @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories * @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories * @param bool $prepend Whether to prepend the directories
* *
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* *
@@ -232,17 +229,18 @@ class ClassLoader
*/ */
public function addPsr4($prefix, $paths, $prepend = false) public function addPsr4($prefix, $paths, $prepend = false)
{ {
$paths = (array) $paths;
if (!$prefix) { if (!$prefix) {
// Register directories for the root namespace. // Register directories for the root namespace.
if ($prepend) { if ($prepend) {
$this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4 = array_merge(
(array) $paths, $paths,
$this->fallbackDirsPsr4 $this->fallbackDirsPsr4
); );
} else { } else {
$this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4, $this->fallbackDirsPsr4,
(array) $paths $paths
); );
} }
} elseif (!isset($this->prefixDirsPsr4[$prefix])) { } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
@@ -252,18 +250,18 @@ class ClassLoader
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
} }
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths; $this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) { } elseif ($prepend) {
// Prepend directories for an already registered namespace. // Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths, $paths,
$this->prefixDirsPsr4[$prefix] $this->prefixDirsPsr4[$prefix]
); );
} else { } else {
// Append directories for an already registered namespace. // Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix], $this->prefixDirsPsr4[$prefix],
(array) $paths $paths
); );
} }
} }
@@ -272,8 +270,8 @@ class ClassLoader
* Registers a set of PSR-0 directories for a given prefix, * Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix. * replacing any others previously set for this prefix.
* *
* @param string $prefix The prefix * @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 base directories * @param list<string>|string $paths The PSR-0 base directories
* *
* @return void * @return void
*/ */
@@ -290,8 +288,8 @@ class ClassLoader
* Registers a set of PSR-4 directories for a given namespace, * Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace. * replacing any others previously set for this namespace.
* *
* @param string $prefix The prefix/namespace, with trailing '\\' * @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories * @param list<string>|string $paths The PSR-4 base directories
* *
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
* *
@@ -425,7 +423,8 @@ class ClassLoader
public function loadClass($class) public function loadClass($class)
{ {
if ($file = $this->findFile($class)) { if ($file = $this->findFile($class)) {
includeFile($file); $includeFile = self::$includeFile;
$includeFile($file);
return true; return true;
} }
@@ -476,9 +475,9 @@ class ClassLoader
} }
/** /**
* Returns the currently registered loaders indexed by their corresponding vendor directories. * Returns the currently registered loaders keyed by their corresponding vendor directories.
* *
* @return self[] * @return array<string, self>
*/ */
public static function getRegisteredLoaders() public static function getRegisteredLoaders()
{ {
@@ -555,18 +554,26 @@ class ClassLoader
return false; return false;
} }
}
/** /**
* Scope isolated include. * @return void
* */
* Prevents access to $this/self from included files. private static function initializeIncludeClosure()
* {
* @param string $file if (self::$includeFile !== null) {
* @return void return;
* @private }
*/
function includeFile($file) /**
{ * Scope isolated include.
include $file; *
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
} }
+12 -5
View File
@@ -98,7 +98,7 @@ class InstalledVersions
{ {
foreach (self::getInstalled() as $installed) { foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) { if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
} }
} }
@@ -119,7 +119,7 @@ class InstalledVersions
*/ */
public static function satisfies(VersionParser $parser, $packageName, $constraint) public static function satisfies(VersionParser $parser, $packageName, $constraint)
{ {
$constraint = $parser->parseConstraints($constraint); $constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName)); $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint); return $provided->matches($constraint);
@@ -328,7 +328,9 @@ class InstalledVersions
if (isset(self::$installedByVendor[$vendorDir])) { if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir]; $installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) { } elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1]; self::$installed = $installed[count($installed) - 1];
} }
@@ -340,12 +342,17 @@ class InstalledVersions
// only require the installed.php file if this file is loaded from its dumped location, // only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') { if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = require __DIR__ . '/installed.php'; /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else { } else {
self::$installed = array(); self::$installed = array();
} }
} }
$installed[] = self::$installed;
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed; return $installed;
} }

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