mirror of
https://github.com/mailcow/mailcow-dockerized.git
synced 2026-06-13 01:50:34 +00:00
Compare commits
553 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 76f8a5b7de | |||
| cb3bc207b9 | |||
| b5db5dd0b4 | |||
| 90a7cff2c9 | |||
| cc3adbe78c | |||
| bd6a7210b7 | |||
| 905a202873 | |||
| accedf0280 | |||
| 99d9a2eacd | |||
| ac4f131fa8 | |||
| 7f6f7e0e9f | |||
| 43bb26f28c | |||
| b29dc37991 | |||
| cf9f02adbb | |||
| b5a1a18b04 | |||
| b4eeb0ffae | |||
| 48549ead7f | |||
| 01b0ad0fd9 | |||
| 2b21501450 | |||
| b491f6af9b | |||
| 942ef7c254 | |||
| 1ee3bb42f3 | |||
| 25007b1963 | |||
| f442378377 | |||
| 333b7ebc0c | |||
| 5896766fc3 | |||
| 89540aec28 | |||
| b960143045 | |||
| 6ab45cf668 | |||
| fd206a7ef6 | |||
| 1c7347d38d | |||
| 7f58c422f2 | |||
| 0a0e2b5e93 | |||
| de00c424f4 | |||
| a249e2028d | |||
| 68036eeccf | |||
| cb0b0235f0 | |||
| 6ff6f7a28d | |||
| 0b628fb22d | |||
| b4bb11320f | |||
| c61938db23 | |||
| acf9d5480c | |||
| a1cb7fd778 | |||
| c24543fea0 | |||
| 100e8ab00d | |||
| 38497b04ac | |||
| 7bd27b920a | |||
| efab11720d | |||
| 121f0120f0 | |||
| 515b85bb2f | |||
| f27e41d19c | |||
| 603d451fc9 | |||
| 89adaabb64 | |||
| 987ca68ca6 | |||
| 71defbf2f9 | |||
| 5c35b42844 | |||
| 904b37c4be | |||
| 4e252f8243 | |||
| dc3e52a900 | |||
| 06ad5f6652 | |||
| c3b5474cbf | |||
| 69e3b830ed | |||
| 96a5891ce7 | |||
| 66b9245b28 | |||
| f38ec68695 | |||
| 996772a27d | |||
| 7f4e9c1ad4 | |||
| 218ba69501 | |||
| c2e5dfd933 | |||
| 3e40bbc603 | |||
| 3498d4b9c5 | |||
| f4b838cad8 | |||
| 86fa8634ee | |||
| 8882006700 | |||
| 40fdf99a55 | |||
| 0257736c64 | |||
| 2024cda560 | |||
| 03aaf4ad76 | |||
| 550b88861f | |||
| 02ae5fa007 | |||
| d81f105ed7 | |||
| d3ed225675 | |||
| efcca61f5a | |||
| 4dad0002cd | |||
| 9ffc83f0f6 | |||
| 981c7d5974 | |||
| 5da089ccd7 | |||
| 91e00f7d97 | |||
| 3a675fb541 | |||
| 9a5d8d2d22 | |||
| de812221ef | |||
| 340980bdd0 | |||
| f68a28fa2b | |||
| 7b7798e8c4 | |||
| b3ac94115e | |||
| b1a172cad9 | |||
| f2e21c68d0 | |||
| 8b784c0eb1 | |||
| bc59f32b96 | |||
| a4fa8a4fae | |||
| f730192c98 | |||
| f994501296 | |||
| 9c3e73606c | |||
| 5619e16b70 | |||
| d2e3867893 | |||
| 979f5475c3 | |||
| 5a10f2dd7c | |||
| a80b5b7dd0 | |||
| 392967d664 | |||
| d4dd1e37ce | |||
| a8dfa95126 | |||
| 3b3c2b7141 | |||
| f55c3c0887 | |||
| f423ad77f3 | |||
| 8ba1e1ba9e | |||
| 55576084fc | |||
| 03311b06c9 | |||
| b5c3d01834 | |||
| f398ecbe39 | |||
| 8f1ae0f099 | |||
| c8bee57732 | |||
| 85641794c3 | |||
| 849decaa59 | |||
| 6e88550f92 | |||
| 7c52483887 | |||
| 0aa520c030 | |||
| 548999f163 | |||
| 63df547306 | |||
| 547d2ca308 | |||
| 46b995f9e3 | |||
| 4f109c1a94 | |||
| 1fdf704cb4 | |||
| 5ec9c4c750 | |||
| 28cec99699 | |||
| 3e194c7906 | |||
| afed94cc0e | |||
| 6f48c5ace0 | |||
| 9a7e1c2b5a | |||
| 2ef7539d55 | |||
| 4e52542e33 | |||
| a1895ad924 | |||
| d5a2c96887 | |||
| 3f30fe3113 | |||
| d89f24a1a3 | |||
| 413354ff29 | |||
| a28ba5bebb | |||
| b93375b671 | |||
| f39005b72d | |||
| b568a33581 | |||
| b05ef8edac | |||
| 015f9b663f | |||
| b6167257c9 | |||
| 687fe044b2 | |||
| cfa47eb873 | |||
| 7079000ee0 | |||
| f60c4f39ee | |||
| 473713219f | |||
| 03ed81dc3f | |||
| 53543ccf26 | |||
| 3b183933e3 | |||
| 6c6fde8e2e | |||
| 61e23b6b81 | |||
| 6c649debc9 | |||
| 87b0683f77 | |||
| 59c1e7a18a | |||
| 4f9dad5dd3 | |||
| adc6a0054c | |||
| 5425cca47e | |||
| 8a70cdb48b | |||
| bb4bc11383 | |||
| a366494c34 | |||
| 99de302ec9 | |||
| 907912046f | |||
| 2c0d379dc5 | |||
| 5b8efeb2ba | |||
| f1c93fa337 | |||
| a94a29a6ac | |||
| 7e3d736ee1 | |||
| 437534556e | |||
| ce4b9c98dc | |||
| c134078d60 | |||
| a8bc6aff2e | |||
| 0b627017e0 | |||
| eb3be80286 | |||
| 1fda71e4fa | |||
| a02bd4beff | |||
| d7f3ee16aa | |||
| 87e3c91c26 | |||
| 33a38e6fde | |||
| 3d8f45db43 | |||
| 40df25dcf0 | |||
| 5de151a966 | |||
| 115d0681a7 | |||
| 1c403a6d60 | |||
| e67ba60863 | |||
| 0c0ec7be58 | |||
| a72b3689b0 | |||
| c4c76e0945 | |||
| 1a793e0b7e | |||
| d0562ddbd9 | |||
| 3851a48ea0 | |||
| 40dcf86846 | |||
| 257e104d2b | |||
| 3f2a9b6973 | |||
| ed365c35e7 | |||
| 24ff70759a | |||
| c55c38f77b | |||
| 934bc15fae | |||
| c2c994bfbb | |||
| b1c2ffba6e | |||
| b4a56052c5 | |||
| 69d15df221 | |||
| e5752755d1 | |||
| d98cfe0fc7 | |||
| 1a1955c1c2 | |||
| 0303dbc1d2 | |||
| acee742822 | |||
| 8d792fbd62 | |||
| d132a51a4d | |||
| 2111115a73 | |||
| 160c9caee3 | |||
| 33de788453 | |||
| f86f5657d9 | |||
| e02a92a0d0 | |||
| 5ae9605e77 | |||
| 88fbec1e53 | |||
| d098e7b9e6 | |||
| a8930e8060 | |||
| e26501261e | |||
| 89bc11ce0f | |||
| 4b096962a9 | |||
| c64fdf9aa3 | |||
| 9caaaa6498 | |||
| 105a7a4c74 | |||
| 09782e5b47 | |||
| 8d75b570c8 | |||
| 21121f9827 | |||
| 8e87e76dcf | |||
| 2629f3d865 | |||
| 8e5cd90707 | |||
| 9ffa810054 | |||
| db9562e843 | |||
| 3540075b61 | |||
| d0ba061f7a | |||
| 871ae5d7d2 | |||
| 633ebe5e8d | |||
| 1b7cc830ca | |||
| d48193fd0e | |||
| bb69f39976 | |||
| f059db54d0 | |||
| e4e8abb1b9 | |||
| 1a207f4d88 | |||
| 25d6e0bbd0 | |||
| 8e5323023a | |||
| 6d9805109a | |||
| 1822d56efb | |||
| 1e3766e2f1 | |||
| 718dcb69be | |||
| 372b1c7bbc | |||
| 9ba5c13702 | |||
| 30e241babe | |||
| 956b170674 | |||
| 2c52753adb | |||
| 095d59c01b | |||
| 1a2f145b28 | |||
| 930473a980 | |||
| 1db8990271 | |||
| 025fd03310 | |||
| e468c59dfc | |||
| 340ef866d2 | |||
| 533bd36572 | |||
| 5bf29e6ac1 | |||
| d6c3c58f42 | |||
| b050cb9864 | |||
| e176724775 | |||
| 8f9ed9e0df | |||
| 003eecf131 | |||
| 180b9fc8d2 | |||
| 5d3491c801 | |||
| c45684b986 | |||
| 5c886d2f4e | |||
| 9f39af46aa | |||
| 7cda9f063f | |||
| 5e7583c5e6 | |||
| a1fb962215 | |||
| 57d849a51b | |||
| 3000da6b88 | |||
| db75cbbcb0 | |||
| 22acbb6b57 | |||
| 31cb0f7db1 | |||
| 6d17b9f504 | |||
| 0f337971ff | |||
| 6cf2775e7e | |||
| dabf9104ed | |||
| 952ddb18fd | |||
| 34d990a800 | |||
| 020cb21b35 | |||
| 525364ba65 | |||
| 731fabef58 | |||
| c10be77a1b | |||
| a8bc4e3f37 | |||
| 815572f200 | |||
| 23fc54f2cf | |||
| 11407973b1 | |||
| b9867e3fe0 | |||
| 3814c3294f | |||
| 9c44b5e546 | |||
| cd635ec813 | |||
| 03831149f8 | |||
| d8fd023cdb | |||
| 521120a448 | |||
| ec8d298c36 | |||
| db2759b7d1 | |||
| 3c3b9575a2 | |||
| 03580cbf39 | |||
| 2b009c71c1 | |||
| b903cf3888 | |||
| 987cfd5dae | |||
| 1537fb39c0 | |||
| 65cbc478b8 | |||
| e2e8fbe313 | |||
| cf239dd6b2 | |||
| a0723f60d2 | |||
| da8e496430 | |||
| 722134e474 | |||
| cb1a11e551 | |||
| 8984509f58 | |||
| 0f0d43b253 | |||
| 0f6956572e | |||
| 29892dc694 | |||
| 14265f3de8 | |||
| 0863bffdd2 | |||
| 3b748a30cc | |||
| 5619175108 | |||
| 6e9c024b3c | |||
| 8cd4ae1e34 | |||
| 689856b186 | |||
| 7b645303d6 | |||
| 408381bddb | |||
| 380cdab6fc | |||
| 03b7a8d639 | |||
| bf6a61fa2d | |||
| 1de47072f8 | |||
| c0c46b7cf5 | |||
| 42a91af7ac | |||
| 6e1ee638ff | |||
| 61c8afa088 | |||
| c873a14127 | |||
| 06cce79806 | |||
| 0927c5df57 | |||
| e691d2c782 | |||
| 67510adb9e | |||
| 490d553dfc | |||
| 70aab7568e | |||
| f82aba3e26 | |||
| f80940efdc | |||
| 6f875398c0 | |||
| 7a582afbdc | |||
| 38cd376228 | |||
| 74bcec45f1 | |||
| 9700b3251f | |||
| 88b8d50cd5 | |||
| 55b0191050 | |||
| 33c97fb318 | |||
| 23d33ad5a8 | |||
| bd6c98047a | |||
| 73d6a29ae1 | |||
| 173e39c859 | |||
| c0745c5cde | |||
| 1a6f93327e | |||
| 3c68a53170 | |||
| e38c27ed67 | |||
| 8eaf8bbbde | |||
| e015c7dbca | |||
| 58452abcdf | |||
| 2cbf0da137 | |||
| f295b8cd91 | |||
| 97a492b891 | |||
| aabcd10539 | |||
| ee607dc3cc | |||
| 1265302a8e | |||
| b5acf56e20 | |||
| 9752313d24 | |||
| fe4a418af4 | |||
| e5f03e8526 | |||
| fb60c4a150 | |||
| 6b82284a41 | |||
| 192f67cd41 | |||
| fd203abd47 | |||
| 6b65f0fc74 | |||
| 856b3b62f2 | |||
| 0372a2150d | |||
| f3322c0577 | |||
| c2bcc4e086 | |||
| 6e79c48640 | |||
| d7dfa95e1b | |||
| cf1cc24e33 | |||
| 6824a5650f | |||
| 73570cc8b5 | |||
| 959dcb9980 | |||
| 8f28666916 | |||
| 3eaa5a626c | |||
| 8c79056a94 | |||
| ed076dc23e | |||
| be2286c11c | |||
| 0e24c3d300 | |||
| e1d8df6580 | |||
| 04a08a7d69 | |||
| 3c0c8aa01f | |||
| 026b278357 | |||
| 4121509ceb | |||
| 00ac61f0a4 | |||
| 4bb0dbb2f7 | |||
| 13b6df74af | |||
| 5c025bf865 | |||
| 20fc9eaf84 | |||
| 22a0479fab | |||
| 3510d5617d | |||
| 236d627fbd | |||
| 99739eada0 | |||
| 7bfef57894 | |||
| d9dfe15253 | |||
| 3fe8aaa719 | |||
| 78a8fac6af | |||
| 6986e7758f | |||
| b4a9df76b8 | |||
| d9d958356a | |||
| 96f954a4e2 | |||
| 44585e1c15 | |||
| c737ff4180 | |||
| 025279009d | |||
| a9dc13d567 | |||
| c3ed01c9b5 | |||
| bd0b4a521e | |||
| 800a0ace71 | |||
| db97869472 | |||
| f681fcf154 | |||
| db1b5956fc | |||
| bdb07061ed | |||
| 428b917579 | |||
| 469f959e96 | |||
| b68e189d97 | |||
| 028ef22878 | |||
| 80dacc015a | |||
| 0194c39bd5 | |||
| f53ca24bb0 | |||
| ae46a877d3 | |||
| 400939faf6 | |||
| fd0205aafd | |||
| e367a8ce24 | |||
| 096e2a41e9 | |||
| e010f08143 | |||
| 3d2483ca37 | |||
| 535dd23509 | |||
| 4336a99c6a | |||
| 4cd5f93cdf | |||
| 67955779b0 | |||
| 26c34b484a | |||
| 4021613059 | |||
| e891bf8411 | |||
| f7798d1aac | |||
| d11f00261b | |||
| 22cd12f37b | |||
| db2fb12837 | |||
| e808e595eb | |||
| ce6742c676 | |||
| cf3dc584d0 | |||
| 62f3603588 | |||
| 9fd4aa93e9 | |||
| 5bc3d93545 | |||
| c28a6b89f0 | |||
| 1233613bea | |||
| 0206e0886c | |||
| f6d135fbad | |||
| f7da314dcf | |||
| e6ce5e88f7 | |||
| e5e6418be8 | |||
| 6507b53bbb | |||
| 2eafd89412 | |||
| 0f59d4952b | |||
| 7225bd2f55 | |||
| deb2b80352 | |||
| ad9dee92be | |||
| f36bc16ca7 | |||
| bda5f0ed4a | |||
| cbe1c97a82 | |||
| 81fcbdd104 | |||
| 1a9294b58f | |||
| 310c01aac2 | |||
| 229303c1f8 | |||
| fc075bc6b7 | |||
| d04f0257c2 | |||
| d11d356803 | |||
| c54750ef8b | |||
| 510ef5196b | |||
| 04e46f9f5b | |||
| 6c0a5028c0 | |||
| 791bbeeb39 | |||
| a5b8f1b7f7 | |||
| af267ff706 | |||
| 46cc022590 | |||
| f77c65411d | |||
| 1052e13af8 | |||
| 11e1502b12 | |||
| 02afc45a15 | |||
| 3e1cfe0d08 | |||
| a3c5f785e9 | |||
| d20df7d73e | |||
| a8c61daeaf | |||
| 1a4f11209a | |||
| 04403aaf70 | |||
| 7f0dd7d0d7 | |||
| cd29ad883e | |||
| e1cd719a17 | |||
| 15bb331a7d | |||
| 6f3179bb8d | |||
| 29e5b87207 | |||
| 4403bc2d18 | |||
| 63e92e0897 | |||
| aa4d8b1f47 | |||
| 9054ca18be | |||
| 38291d123f | |||
| ca64ff2c0b | |||
| dc85f49961 | |||
| 5dca4dac81 | |||
| df8775d4c9 | |||
| 2bc663dcd5 | |||
| 1071bb8230 | |||
| e437810eca | |||
| e8fd34d31f | |||
| 6aebb8352e | |||
| d684e0efc0 | |||
| 64ac6a8891 | |||
| 72e8180c6b | |||
| d62c275004 | |||
| aa7f562761 | |||
| a1f033e4c1 | |||
| 58ddc31db6 | |||
| 5bf62481d5 | |||
| 6ff3f3f044 | |||
| 7877215d59 | |||
| e4347792b8 | |||
| 50fde60899 | |||
| 38f5e293b0 | |||
| b6b399a590 | |||
| b83841d253 | |||
| 3e69304f0f | |||
| fe8131f743 | |||
| 9ef14a20d1 | |||
| 5897b97065 | |||
| 1606658cb1 | |||
| 54ba66733e | |||
| f6847e6f8c |
@@ -1,8 +1,11 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: ❓ Community-driven support
|
- name: ❓ Community-driven support (Free)
|
||||||
url: https://mailcow.github.io/mailcow-dockerized-docs/#get-support
|
url: https://docs.mailcow.email/#get-support
|
||||||
about: Please use the community forum for questions or assistance
|
about: Please use the community forum for questions or assistance
|
||||||
|
- name: 🔥 Premium Support (Paid)
|
||||||
|
url: https://www.servercow.de/mailcow?lang=en#support
|
||||||
|
about: Buy a support subscription for any critical issues and get assisted by the mailcow Team. See conditions!
|
||||||
- name: 🚨 Report a security vulnerability
|
- name: 🚨 Report a security vulnerability
|
||||||
url: https://www.servercow.de/anfrage?lang=en
|
url: "mailto:info@servercow.de?subject=mailcow: dockerized Security Vulnerability"
|
||||||
about: Please give us appropriate time to verify, respond and fix before disclosure.
|
about: Please give us appropriate time to verify, respond and fix before disclosure.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"baseBranches": ["staging"],
|
"baseBranches": ["staging"],
|
||||||
"enabledManagers": ["github-actions", "regex", "docker-compose"],
|
"enabledManagers": ["github-actions", "regex", "docker-compose"],
|
||||||
"ignorePaths": [
|
"ignorePaths": [
|
||||||
"data\/web\/inc\/lib\/vendor\/matthiasmullie\/minify\/**"
|
"data\/web\/inc\/lib\/vendor\/**"
|
||||||
],
|
],
|
||||||
"regexManagers": [
|
"regexManagers": [
|
||||||
{
|
{
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
{
|
{
|
||||||
"fileMatch": ["(^|/)Dockerfile[^/]*$"],
|
"fileMatch": ["(^|/)Dockerfile[^/]*$"],
|
||||||
"matchStrings": [
|
"matchStrings": [
|
||||||
"#\\srenovate:\\sdatasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?\\s(ENV|ARG) .*?_VERSION=(?<currentValue>.*)\\s"
|
"#\\srenovate:\\sdatasource=(?<datasource>.*?) depName=(?<depName>.*?)( versioning=(?<versioning>.*?))?( extractVersion=(?<extractVersion>.*?))?\\s(ENV|ARG) .*?_VERSION=(?<currentValue>.*)\\s"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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.3.1
|
uses: thollander/actions-comment-pull-request@v2.4.3
|
||||||
with:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }}
|
GITHUB_TOKEN: ${{ secrets.CHECKIFPRISSTAGING_ACTION_PAT }}
|
||||||
message: |
|
message: |
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
- name: Mark/Close Stale Issues and Pull Requests 🗑️
|
||||||
uses: actions/stale@v7.0.0
|
uses: actions/stale@v9.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
repo-token: ${{ secrets.STALE_ACTION_PAT }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- "watchdog-mailcow"
|
- "watchdog-mailcow"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Docker
|
- name: Setup Docker
|
||||||
run: |
|
run: |
|
||||||
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
|
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Run the Action
|
- name: Run the Action
|
||||||
uses: devops-infra/action-pull-request@v0.5.3
|
uses: devops-infra/action-pull-request@v0.5.5
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
|
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
|
||||||
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}
|
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}
|
||||||
|
|||||||
@@ -11,24 +11,25 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_USERNAME }}
|
username: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_USERNAME }}
|
||||||
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@v3
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
file: data/Dockerfiles/backup/Dockerfile
|
file: data/Dockerfiles/backup/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
tags: mailcow/backup:latest
|
tags: mailcow/backup:latest
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
name: "Tweet trigger release"
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
tweet:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: "Get Release Tag"
|
|
||||||
run: |
|
|
||||||
RELEASE_TAG=$(curl https://api.github.com/repos/mailcow/mailcow-dockerized/releases/latest | jq -r '.tag_name')
|
|
||||||
- name: Tweet-trigger-publish-release
|
|
||||||
uses: mugi111/tweet-trigger-release@v1.2
|
|
||||||
with:
|
|
||||||
consumer_key: ${{ secrets.CONSUMER_KEY }}
|
|
||||||
consumer_secret: ${{ secrets.CONSUMER_SECRET }}
|
|
||||||
access_token_key: ${{ secrets.ACCESS_TOKEN_KEY }}
|
|
||||||
access_token_secret: ${{ secrets.ACCESS_TOKEN_SECRET }}
|
|
||||||
tweet_body: 'A new mailcow update has just been released! Checkout the GitHub Page for changelog and more informations: https://github.com/mailcow/mailcow-dockerized/releases/latest'
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
name: Update postscreen_access.cidr
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Monthly
|
||||||
|
- cron: "0 0 1 * *"
|
||||||
|
workflow_dispatch: # Allow to run workflow manually
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Update-postscreen_access_cidr:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Generate postscreen_access.cidr
|
||||||
|
run: |
|
||||||
|
bash helper-scripts/update_postscreen_whitelist.sh
|
||||||
|
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@v5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.mailcow_action_Update_postscreen_access_cidr_pat }}
|
||||||
|
commit-message: update postscreen_access.cidr
|
||||||
|
committer: milkmaker <milkmaker@mailcow.de>
|
||||||
|
author: milkmaker <milkmaker@mailcow.de>
|
||||||
|
signoff: false
|
||||||
|
branch: update/postscreen_access.cidr
|
||||||
|
base: staging
|
||||||
|
delete-branch: true
|
||||||
|
add-paths: |
|
||||||
|
data/conf/postfix/postscreen_access.cidr
|
||||||
|
title: '[Postfix] update postscreen_access.cidr'
|
||||||
|
body: |
|
||||||
|
This PR updates the postscreen_access.cidr using GitHub Actions and [helper-scripts/update_postscreen_whitelist.sh](https://github.com/mailcow/mailcow-dockerized/blob/master/helper-scripts/update_postscreen_whitelist.sh)
|
||||||
@@ -36,6 +36,8 @@ data/conf/postfix/extra.cf
|
|||||||
data/conf/postfix/sni.map
|
data/conf/postfix/sni.map
|
||||||
data/conf/postfix/sni.map.db
|
data/conf/postfix/sni.map.db
|
||||||
data/conf/postfix/sql
|
data/conf/postfix/sql
|
||||||
|
data/conf/postfix/dns_blocklists.cf
|
||||||
|
data/conf/postfix/dnsbl_reply.map
|
||||||
data/conf/rspamd/custom/*
|
data/conf/rspamd/custom/*
|
||||||
data/conf/rspamd/local.d/*
|
data/conf/rspamd/local.d/*
|
||||||
data/conf/rspamd/override.d/*
|
data/conf/rspamd/override.d/*
|
||||||
|
|||||||
+33
-3
@@ -1,9 +1,39 @@
|
|||||||
When a problem occurs, then always for a reason! What you want to do in such a case is:
|
# Contribution Guidelines (Last modified on 18th December 2023)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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).
|
||||||
|
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.
|
||||||
|
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.*
|
||||||
|
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.*
|
||||||
|
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.
|
||||||
|
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!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue Reporting (Last modified on 18th December 2023)
|
||||||
|
|
||||||
|
If you plan to report a issue within mailcow please read and understand the following rules:
|
||||||
|
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
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>.
|
||||||
|
7. Please **ALWAYS** anonymize any sensitive information in your bug report or feature request before submitting it.
|
||||||
|
|
||||||
|
### Quick guide to reporting problems:
|
||||||
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.
|
||||||
4. Read the [documentation](https://mailcow.github.io/mailcow-dockerized-docs/) of the troubled service and search its bugtracker for your problem.
|
4. Read the [documentation](https://docs.mailcow.email/) of the troubled service and search its bugtracker for your problem.
|
||||||
5. Search our [issues](https://github.com/mailcow/mailcow-dockerized/issues) for your problem.
|
5. Search our [issues](https://github.com/mailcow/mailcow-dockerized/issues) for 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.
|
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://mailcow.github.io/mailcow-dockerized-docs/#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.
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
[](https://translate.mailcow.email/engage/mailcow-dockerized/)
|
[](https://translate.mailcow.email/engage/mailcow-dockerized/)
|
||||||
[](https://twitter.com/mailcow_email)
|
[](https://twitter.com/mailcow_email)
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
## Want to support mailcow?
|
## Want to support mailcow?
|
||||||
|
|
||||||
@@ -13,7 +15,7 @@ Or just spread the word: moo.
|
|||||||
|
|
||||||
## Info, documentation and support
|
## Info, documentation and support
|
||||||
|
|
||||||
Please see [the official documentation](https://mailcow.github.io/mailcow-dockerized-docs/) for installation and support instructions. 🐄
|
Please see [the official documentation](https://docs.mailcow.email/) for installation and support instructions. 🐄
|
||||||
|
|
||||||
🐛 **If you found a critical security issue, please mail us to [info at servercow.de](mailto:info@servercow.de).**
|
🐛 **If you found a critical security issue, please mail us to [info at servercow.de](mailto:info@servercow.de).**
|
||||||
|
|
||||||
@@ -25,7 +27,9 @@ Please see [the official documentation](https://mailcow.github.io/mailcow-docker
|
|||||||
|
|
||||||
[Telegram mailcow Off-Topic channel](https://t.me/mailcowOfftopic)
|
[Telegram mailcow Off-Topic channel](https://t.me/mailcowOfftopic)
|
||||||
|
|
||||||
[Official Twitter Account](https://twitter.com/mailcow_email)
|
[Official 𝕏 (Twitter) Account](https://twitter.com/mailcow_email)
|
||||||
|
|
||||||
|
[Official Mastodon Account](https://mailcow.social/@doncow)
|
||||||
|
|
||||||
Telegram desktop clients are available for [multiple platforms](https://desktop.telegram.org). You can search the groups history for keywords.
|
Telegram desktop clients are available for [multiple platforms](https://desktop.telegram.org). You can search the groups history for keywords.
|
||||||
|
|
||||||
@@ -38,4 +42,4 @@ mailcow is a registered word mark of The Infrastructure Company GmbH, Parkstr. 4
|
|||||||
|
|
||||||
The project is managed and maintained by The Infrastructure Company GmbH.
|
The project is managed and maintained by The Infrastructure Company GmbH.
|
||||||
|
|
||||||
Originated from @andryyy (André)
|
Originated from @andryyy (André)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
FROM alpine:3.17
|
FROM alpine:3.19
|
||||||
|
|
||||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
ARG PIP_BREAK_SYSTEM_PACKAGES=1
|
||||||
RUN apk upgrade --no-cache \
|
RUN apk upgrade --no-cache \
|
||||||
&& apk add --update --no-cache \
|
&& apk add --update --no-cache \
|
||||||
bash \
|
bash \
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
FROM clamav/clamav:1.0_base
|
FROM alpine:3.19
|
||||||
|
|
||||||
LABEL maintainer "André Peters <andre.peters@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH 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 []
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
FROM alpine:3.17
|
FROM alpine:3.19
|
||||||
|
|
||||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH 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,14 +10,18 @@ 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
|
||||||
redis
|
RUN mkdir /app/modules
|
||||||
|
|
||||||
COPY docker-entrypoint.sh /app/
|
COPY docker-entrypoint.sh /app/
|
||||||
COPY dockerapi.py /app/
|
COPY main.py /app/main.py
|
||||||
|
COPY modules/ /app/modules/
|
||||||
|
|
||||||
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]
|
ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"]
|
||||||
|
CMD exec python main.py
|
||||||
@@ -6,4 +6,4 @@
|
|||||||
-subj /CN=dockerapi/O=mailcow \
|
-subj /CN=dockerapi/O=mailcow \
|
||||||
-addext subjectAltName=DNS:dockerapi`
|
-addext subjectAltName=DNS:dockerapi`
|
||||||
|
|
||||||
`uvicorn --host 0.0.0.0 --port 443 --ssl-certfile=/app/dockerapi_cert.pem --ssl-keyfile=/app/dockerapi_key.pem dockerapi:app`
|
exec "$@"
|
||||||
|
|||||||
@@ -1,539 +0,0 @@
|
|||||||
from fastapi import FastAPI, Response, Request
|
|
||||||
import aiodocker
|
|
||||||
import docker
|
|
||||||
import psutil
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import asyncio
|
|
||||||
import redis
|
|
||||||
from datetime import datetime
|
|
||||||
import logging
|
|
||||||
from logging.config import dictConfig
|
|
||||||
|
|
||||||
|
|
||||||
log_config = {
|
|
||||||
"version": 1,
|
|
||||||
"disable_existing_loggers": False,
|
|
||||||
"formatters": {
|
|
||||||
"default": {
|
|
||||||
"()": "uvicorn.logging.DefaultFormatter",
|
|
||||||
"fmt": "%(levelprefix)s %(asctime)s %(message)s",
|
|
||||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
|
||||||
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"handlers": {
|
|
||||||
"default": {
|
|
||||||
"formatter": "default",
|
|
||||||
"class": "logging.StreamHandler",
|
|
||||||
"stream": "ext://sys.stderr",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"loggers": {
|
|
||||||
"api-logger": {"handlers": ["default"], "level": "INFO"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
dictConfig(log_config)
|
|
||||||
|
|
||||||
containerIds_to_update = []
|
|
||||||
host_stats_isUpdating = False
|
|
||||||
app = FastAPI()
|
|
||||||
logger = logging.getLogger('api-logger')
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/host/stats")
|
|
||||||
async def get_host_update_stats():
|
|
||||||
global host_stats_isUpdating
|
|
||||||
|
|
||||||
if host_stats_isUpdating == False:
|
|
||||||
asyncio.create_task(get_host_stats())
|
|
||||||
host_stats_isUpdating = True
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if redis_client.exists('host_stats'):
|
|
||||||
break
|
|
||||||
await asyncio.sleep(1.5)
|
|
||||||
|
|
||||||
|
|
||||||
stats = json.loads(redis_client.get('host_stats'))
|
|
||||||
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
@app.get("/containers/{container_id}/json")
|
|
||||||
async def get_container(container_id : str):
|
|
||||||
if container_id and container_id.isalnum():
|
|
||||||
try:
|
|
||||||
for container in (await async_docker_client.containers.list()):
|
|
||||||
if container._id == container_id:
|
|
||||||
container_info = await container.show()
|
|
||||||
return Response(content=json.dumps(container_info, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": "no container found"
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
except Exception as e:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": str(e)
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
else:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": "no or invalid id defined"
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
@app.get("/containers/json")
|
|
||||||
async def get_containers():
|
|
||||||
containers = {}
|
|
||||||
try:
|
|
||||||
for container in (await async_docker_client.containers.list()):
|
|
||||||
container_info = await container.show()
|
|
||||||
containers.update({container_info['Id']: container_info})
|
|
||||||
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
|
||||||
except Exception as e:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": str(e)
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
@app.post("/containers/{container_id}/{post_action}")
|
|
||||||
async def post_containers(container_id : str, post_action : str, request: Request):
|
|
||||||
try :
|
|
||||||
request_json = await request.json()
|
|
||||||
except Exception as err:
|
|
||||||
request_json = {}
|
|
||||||
|
|
||||||
if container_id and container_id.isalnum() and post_action:
|
|
||||||
try:
|
|
||||||
"""Dispatch container_post api call"""
|
|
||||||
if post_action == 'exec':
|
|
||||||
if not request_json or not 'cmd' in request_json:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": "cmd is missing"
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
if not request_json or not 'task' in request_json:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": "task is missing"
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
api_call_method_name = '__'.join(['container_post', str(post_action), str(request_json['cmd']), str(request_json['task']) ])
|
|
||||||
else:
|
|
||||||
api_call_method_name = '__'.join(['container_post', str(post_action) ])
|
|
||||||
|
|
||||||
docker_utils = DockerUtils(sync_docker_client)
|
|
||||||
api_call_method = getattr(docker_utils, api_call_method_name, lambda container_id: Response(content=json.dumps({'type': 'danger', 'msg':'container_post - unknown api call' }, indent=4), media_type="application/json"))
|
|
||||||
|
|
||||||
|
|
||||||
logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id))
|
|
||||||
return api_call_method(container_id, request_json)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("error - container_post: %s" % str(e))
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": str(e)
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
else:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": "invalid container id or missing action"
|
|
||||||
}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
@app.post("/container/{container_id}/stats/update")
|
|
||||||
async def post_container_update_stats(container_id : str):
|
|
||||||
global containerIds_to_update
|
|
||||||
|
|
||||||
# start update task for container if no task is running
|
|
||||||
if container_id not in containerIds_to_update:
|
|
||||||
asyncio.create_task(get_container_stats(container_id))
|
|
||||||
containerIds_to_update.append(container_id)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if redis_client.exists(container_id + '_stats'):
|
|
||||||
break
|
|
||||||
await asyncio.sleep(1.5)
|
|
||||||
|
|
||||||
stats = json.loads(redis_client.get(container_id + '_stats'))
|
|
||||||
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DockerUtils:
|
|
||||||
def __init__(self, docker_client):
|
|
||||||
self.docker_client = docker_client
|
|
||||||
|
|
||||||
# api call: container_post - post_action: stop
|
|
||||||
def container_post__stop(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
|
|
||||||
container.stop()
|
|
||||||
|
|
||||||
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
# api call: container_post - post_action: start
|
|
||||||
def container_post__start(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
|
|
||||||
container.start()
|
|
||||||
|
|
||||||
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
# api call: container_post - post_action: restart
|
|
||||||
def container_post__restart(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
|
|
||||||
container.restart()
|
|
||||||
|
|
||||||
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
# api call: container_post - post_action: top
|
|
||||||
def container_post__top(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
|
|
||||||
res = { 'type': 'success', 'msg': container.top()}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
# api call: container_post - post_action: stats
|
|
||||||
def container_post__stats(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(all=True, filters={"id": container_id}):
|
|
||||||
for stat in container.stats(decode=True, stream=True):
|
|
||||||
res = { 'type': 'success', 'msg': stat}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
# api call: container_post - post_action: exec - cmd: mailq - task: delete
|
|
||||||
def container_post__exec__mailq__delete(self, container_id, request_json):
|
|
||||||
if 'items' in request_json:
|
|
||||||
r = re.compile("^[0-9a-fA-F]+$")
|
|
||||||
filtered_qids = filter(r.match, request_json['items'])
|
|
||||||
if filtered_qids:
|
|
||||||
flagged_qids = ['-d %s' % i for i in filtered_qids]
|
|
||||||
sanitized_string = str(' '.join(flagged_qids));
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
|
||||||
return exec_run_handler('generic', postsuper_r)
|
|
||||||
|
|
||||||
|
|
||||||
# api call: container_post - post_action: exec - cmd: mailq - task: hold
|
|
||||||
def container_post__exec__mailq__hold(self, container_id, request_json):
|
|
||||||
if 'items' in request_json:
|
|
||||||
r = re.compile("^[0-9a-fA-F]+$")
|
|
||||||
filtered_qids = filter(r.match, request_json['items'])
|
|
||||||
if filtered_qids:
|
|
||||||
flagged_qids = ['-h %s' % i for i in filtered_qids]
|
|
||||||
sanitized_string = str(' '.join(flagged_qids));
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
|
||||||
return exec_run_handler('generic', postsuper_r)
|
|
||||||
|
|
||||||
# api call: container_post - post_action: exec - cmd: mailq - task: cat
|
|
||||||
def container_post__exec__mailq__cat(self, container_id, request_json):
|
|
||||||
if 'items' in request_json:
|
|
||||||
r = re.compile("^[0-9a-fA-F]+$")
|
|
||||||
filtered_qids = filter(r.match, request_json['items'])
|
|
||||||
if filtered_qids:
|
|
||||||
sanitized_string = str(' '.join(filtered_qids));
|
|
||||||
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
|
|
||||||
if not postcat_return:
|
|
||||||
postcat_return = 'err: invalid'
|
|
||||||
return exec_run_handler('utf8_text_only', postcat_return)
|
|
||||||
|
|
||||||
# api call: container_post - post_action: exec - cmd: mailq - task: unhold
|
|
||||||
def container_post__exec__mailq__unhold(self, container_id, request_json):
|
|
||||||
if 'items' in request_json:
|
|
||||||
r = re.compile("^[0-9a-fA-F]+$")
|
|
||||||
filtered_qids = filter(r.match, request_json['items'])
|
|
||||||
if filtered_qids:
|
|
||||||
flagged_qids = ['-H %s' % i for i in filtered_qids]
|
|
||||||
sanitized_string = str(' '.join(flagged_qids));
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
|
||||||
return exec_run_handler('generic', postsuper_r)
|
|
||||||
|
|
||||||
# api call: container_post - post_action: exec - cmd: mailq - task: deliver
|
|
||||||
def container_post__exec__mailq__deliver(self, container_id, request_json):
|
|
||||||
if 'items' in request_json:
|
|
||||||
r = re.compile("^[0-9a-fA-F]+$")
|
|
||||||
filtered_qids = filter(r.match, request_json['items'])
|
|
||||||
if filtered_qids:
|
|
||||||
flagged_qids = ['-i %s' % i for i in filtered_qids]
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
for i in flagged_qids:
|
|
||||||
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
|
|
||||||
# todo: check each exit code
|
|
||||||
res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
# api call: container_post - post_action: exec - cmd: mailq - task: list
|
|
||||||
def container_post__exec__mailq__list(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
|
|
||||||
return exec_run_handler('utf8_text_only', mailq_return)
|
|
||||||
# api call: container_post - post_action: exec - cmd: mailq - task: flush
|
|
||||||
def container_post__exec__mailq__flush(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
|
|
||||||
return exec_run_handler('generic', postqueue_r)
|
|
||||||
# api call: container_post - post_action: exec - cmd: mailq - task: super_delete
|
|
||||||
def container_post__exec__mailq__super_delete(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
|
|
||||||
return exec_run_handler('generic', postsuper_r)
|
|
||||||
# api call: container_post - post_action: exec - cmd: system - task: fts_rescan
|
|
||||||
def container_post__exec__system__fts_rescan(self, container_id, request_json):
|
|
||||||
if 'username' in request_json:
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail')
|
|
||||||
if rescan_return.exit_code == 0:
|
|
||||||
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
else:
|
|
||||||
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
if 'all' in request_json:
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
|
|
||||||
if rescan_return.exit_code == 0:
|
|
||||||
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
else:
|
|
||||||
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
# api call: container_post - post_action: exec - cmd: system - task: df
|
|
||||||
def container_post__exec__system__df(self, container_id, request_json):
|
|
||||||
if 'dir' in request_json:
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
|
|
||||||
if df_return.exit_code == 0:
|
|
||||||
return df_return.output.decode('utf-8').rstrip()
|
|
||||||
else:
|
|
||||||
return "0,0,0,0,0,0"
|
|
||||||
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
|
|
||||||
def container_post__exec__system__mysql_upgrade(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
|
|
||||||
if sql_return.exit_code == 0:
|
|
||||||
matched = False
|
|
||||||
for line in sql_return.output.decode('utf-8').split("\n"):
|
|
||||||
if 'is already upgraded to' in line:
|
|
||||||
matched = True
|
|
||||||
if matched:
|
|
||||||
res = { 'type': 'success', 'msg':'mysql_upgrade: already upgraded', 'text': sql_return.output.decode('utf-8')}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
else:
|
|
||||||
container.restart()
|
|
||||||
res = { 'type': 'warning', 'msg':'mysql_upgrade: upgrade was applied', 'text': sql_return.output.decode('utf-8')}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
else:
|
|
||||||
res = { 'type': 'error', 'msg': 'mysql_upgrade: error running command', 'text': sql_return.output.decode('utf-8')}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
|
|
||||||
def container_post__exec__system__mysql_tzinfo_to_sql(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
|
|
||||||
if sql_return.exit_code == 0:
|
|
||||||
res = { 'type': 'info', 'msg': 'mysql_tzinfo_to_sql: command completed successfully', 'text': sql_return.output.decode('utf-8')}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
else:
|
|
||||||
res = { 'type': 'error', 'msg': 'mysql_tzinfo_to_sql: error running command', 'text': sql_return.output.decode('utf-8')}
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
|
|
||||||
def container_post__exec__reload__dovecot(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
|
|
||||||
return exec_run_handler('generic', reload_return)
|
|
||||||
# api call: container_post - post_action: exec - cmd: reload - task: postfix
|
|
||||||
def container_post__exec__reload__postfix(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
|
|
||||||
return exec_run_handler('generic', reload_return)
|
|
||||||
# api call: container_post - post_action: exec - cmd: reload - task: nginx
|
|
||||||
def container_post__exec__reload__nginx(self, container_id, request_json):
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
|
|
||||||
return exec_run_handler('generic', reload_return)
|
|
||||||
# api call: container_post - post_action: exec - cmd: sieve - task: list
|
|
||||||
def container_post__exec__sieve__list(self, container_id, request_json):
|
|
||||||
if 'username' in request_json:
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"])
|
|
||||||
return exec_run_handler('utf8_text_only', sieve_return)
|
|
||||||
# api call: container_post - post_action: exec - cmd: sieve - task: print
|
|
||||||
def container_post__exec__sieve__print(self, container_id, request_json):
|
|
||||||
if 'username' in request.json and 'script_name' in request_json:
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
cmd = ["/bin/bash", "-c", "/usr/bin/doveadm sieve get -u '" + request_json['username'].replace("'", "'\\''") + "' '" + request_json['script_name'].replace("'", "'\\''") + "'"]
|
|
||||||
sieve_return = container.exec_run(cmd)
|
|
||||||
return exec_run_handler('utf8_text_only', sieve_return)
|
|
||||||
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
|
|
||||||
def container_post__exec__maildir__cleanup(self, container_id, request_json):
|
|
||||||
if 'maildir' in request_json:
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
sane_name = re.sub(r'\W+', '', request_json['maildir'])
|
|
||||||
cmd = ["/bin/bash", "-c", "if [[ -d '/var/vmail/" + request_json['maildir'].replace("'", "'\\''") + "' ]]; then /bin/mv '/var/vmail/" + request_json['maildir'].replace("'", "'\\''") + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"]
|
|
||||||
maildir_cleanup = container.exec_run(cmd, user='vmail')
|
|
||||||
return exec_run_handler('generic', maildir_cleanup)
|
|
||||||
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
|
|
||||||
def container_post__exec__rspamd__worker_password(self, container_id, request_json):
|
|
||||||
if 'raw' in request_json:
|
|
||||||
for container in self.docker_client.containers.list(filters={"id": container_id}):
|
|
||||||
cmd = "/usr/bin/rspamadm pw -e -p '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
|
|
||||||
cmd_response = exec_cmd_container(container, cmd, user="_rspamd")
|
|
||||||
|
|
||||||
matched = False
|
|
||||||
for line in cmd_response.split("\n"):
|
|
||||||
if '$2$' in line:
|
|
||||||
hash = line.strip()
|
|
||||||
hash_out = re.search('\$2\$.+$', hash).group(0)
|
|
||||||
rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
|
|
||||||
rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc"
|
|
||||||
cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
|
|
||||||
cmd_response = exec_cmd_container(container, cmd, user="_rspamd")
|
|
||||||
if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response:
|
|
||||||
container.restart()
|
|
||||||
matched = True
|
|
||||||
if matched:
|
|
||||||
res = { 'type': 'success', 'msg': 'command completed successfully' }
|
|
||||||
logger.info('success changing Rspamd password')
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
else:
|
|
||||||
logger.error('failed changing Rspamd password')
|
|
||||||
res = { 'type': 'danger', 'msg': 'command did not complete' }
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
|
|
||||||
|
|
||||||
def exec_cmd_container(container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
|
|
||||||
|
|
||||||
def recv_socket_data(c_socket, timeout):
|
|
||||||
c_socket.setblocking(0)
|
|
||||||
total_data=[]
|
|
||||||
data=''
|
|
||||||
begin=time.time()
|
|
||||||
while True:
|
|
||||||
if total_data and time.time()-begin > timeout:
|
|
||||||
break
|
|
||||||
elif time.time()-begin > timeout*2:
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
data = c_socket.recv(8192)
|
|
||||||
if data:
|
|
||||||
total_data.append(data.decode('utf-8'))
|
|
||||||
#change the beginning time for measurement
|
|
||||||
begin=time.time()
|
|
||||||
else:
|
|
||||||
#sleep for sometime to indicate a gap
|
|
||||||
time.sleep(0.1)
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return ''.join(total_data)
|
|
||||||
|
|
||||||
|
|
||||||
try :
|
|
||||||
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
|
|
||||||
if not cmd.endswith("\n"):
|
|
||||||
cmd = cmd + "\n"
|
|
||||||
socket.send(cmd.encode('utf-8'))
|
|
||||||
data = recv_socket_data(socket, timeout)
|
|
||||||
socket.close()
|
|
||||||
return data
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("error - exec_cmd_container: %s" % str(e))
|
|
||||||
traceback.print_exc(file=sys.stdout)
|
|
||||||
def exec_run_handler(type, output):
|
|
||||||
if type == 'generic':
|
|
||||||
if output.exit_code == 0:
|
|
||||||
res = { 'type': 'success', 'msg': 'command completed successfully' }
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
else:
|
|
||||||
res = { 'type': 'danger', 'msg': 'command failed: ' + output.output.decode('utf-8') }
|
|
||||||
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
|
||||||
if type == 'utf8_text_only':
|
|
||||||
return Response(content=output.output.decode('utf-8'), media_type="text/plain")
|
|
||||||
|
|
||||||
async def get_host_stats(wait=5):
|
|
||||||
global host_stats_isUpdating
|
|
||||||
|
|
||||||
try:
|
|
||||||
system_time = datetime.now()
|
|
||||||
host_stats = {
|
|
||||||
"cpu": {
|
|
||||||
"cores": psutil.cpu_count(),
|
|
||||||
"usage": psutil.cpu_percent()
|
|
||||||
},
|
|
||||||
"memory": {
|
|
||||||
"total": psutil.virtual_memory().total,
|
|
||||||
"usage": psutil.virtual_memory().percent,
|
|
||||||
"swap": psutil.swap_memory()
|
|
||||||
},
|
|
||||||
"uptime": time.time() - psutil.boot_time(),
|
|
||||||
"system_time": system_time.strftime("%d.%m.%Y %H:%M:%S")
|
|
||||||
}
|
|
||||||
|
|
||||||
redis_client.set('host_stats', json.dumps(host_stats), ex=10)
|
|
||||||
except Exception as e:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
await asyncio.sleep(wait)
|
|
||||||
host_stats_isUpdating = False
|
|
||||||
|
|
||||||
async def get_container_stats(container_id, wait=5, stop=False):
|
|
||||||
global containerIds_to_update
|
|
||||||
|
|
||||||
if container_id and container_id.isalnum():
|
|
||||||
try:
|
|
||||||
for container in (await async_docker_client.containers.list()):
|
|
||||||
if container._id == container_id:
|
|
||||||
res = await container.stats(stream=False)
|
|
||||||
|
|
||||||
if redis_client.exists(container_id + '_stats'):
|
|
||||||
stats = json.loads(redis_client.get(container_id + '_stats'))
|
|
||||||
else:
|
|
||||||
stats = []
|
|
||||||
stats.append(res[0])
|
|
||||||
if len(stats) > 3:
|
|
||||||
del stats[0]
|
|
||||||
redis_client.set(container_id + '_stats', json.dumps(stats), ex=60)
|
|
||||||
except Exception as e:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": str(e)
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
res = {
|
|
||||||
"type": "danger",
|
|
||||||
"msg": "no or invalid id defined"
|
|
||||||
}
|
|
||||||
|
|
||||||
await asyncio.sleep(wait)
|
|
||||||
if stop == True:
|
|
||||||
# update task was called second time, stop
|
|
||||||
containerIds_to_update.remove(container_id)
|
|
||||||
else:
|
|
||||||
# call update task a second time
|
|
||||||
await get_container_stats(container_id, wait=0, stop=True)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if os.environ['REDIS_SLAVEOF_IP'] != "":
|
|
||||||
redis_client = redis.Redis(host=os.environ['REDIS_SLAVEOF_IP'], port=os.environ['REDIS_SLAVEOF_PORT'], db=0)
|
|
||||||
else:
|
|
||||||
redis_client = redis.Redis(host='redis-mailcow', port=6379, db=0)
|
|
||||||
|
|
||||||
sync_docker_client = docker.DockerClient(base_url='unix://var/run/docker.sock', version='auto')
|
|
||||||
async_docker_client = aiodocker.Docker(url='unix:///var/run/docker.sock')
|
|
||||||
|
|
||||||
logger.info('DockerApi started')
|
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import uvicorn
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import async_timeout
|
||||||
|
import asyncio
|
||||||
|
import aiodocker
|
||||||
|
import docker
|
||||||
|
import logging
|
||||||
|
from logging.config import dictConfig
|
||||||
|
from fastapi import FastAPI, Response, Request
|
||||||
|
from modules.DockerApi import DockerApi
|
||||||
|
from redis import asyncio as aioredis
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
|
dockerapi = None
|
||||||
|
|
||||||
|
@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
|
||||||
|
@app.get("/host/stats")
|
||||||
|
async def get_host_update_stats():
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
if dockerapi.host_stats_isUpdating == False:
|
||||||
|
asyncio.create_task(dockerapi.get_host_stats())
|
||||||
|
dockerapi.host_stats_isUpdating = True
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if await dockerapi.redis_client.exists('host_stats'):
|
||||||
|
break
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
|
||||||
|
stats = json.loads(await dockerapi.redis_client.get('host_stats'))
|
||||||
|
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
@app.get("/containers/{container_id}/json")
|
||||||
|
async def get_container(container_id : str):
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
if container_id and container_id.isalnum():
|
||||||
|
try:
|
||||||
|
for container in (await dockerapi.async_docker_client.containers.list()):
|
||||||
|
if container._id == container_id:
|
||||||
|
container_info = await container.show()
|
||||||
|
return Response(content=json.dumps(container_info, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": "no container found"
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
except Exception as e:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": str(e)
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": "no or invalid id defined"
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
@app.get("/containers/json")
|
||||||
|
async def get_containers():
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
containers = {}
|
||||||
|
try:
|
||||||
|
for container in (await dockerapi.async_docker_client.containers.list()):
|
||||||
|
container_info = await container.show()
|
||||||
|
containers.update({container_info['Id']: container_info})
|
||||||
|
return Response(content=json.dumps(containers, indent=4), media_type="application/json")
|
||||||
|
except Exception as e:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": str(e)
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
@app.post("/containers/{container_id}/{post_action}")
|
||||||
|
async def post_containers(container_id : str, post_action : str, request: Request):
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
try :
|
||||||
|
request_json = await request.json()
|
||||||
|
except Exception as err:
|
||||||
|
request_json = {}
|
||||||
|
|
||||||
|
if container_id and container_id.isalnum() and post_action:
|
||||||
|
try:
|
||||||
|
"""Dispatch container_post api call"""
|
||||||
|
if post_action == 'exec':
|
||||||
|
if not request_json or not 'cmd' in request_json:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": "cmd is missing"
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
if not request_json or not 'task' in request_json:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": "task is missing"
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
api_call_method_name = '__'.join(['container_post', str(post_action), str(request_json['cmd']), str(request_json['task']) ])
|
||||||
|
else:
|
||||||
|
api_call_method_name = '__'.join(['container_post', str(post_action) ])
|
||||||
|
|
||||||
|
api_call_method = getattr(dockerapi, api_call_method_name, lambda container_id: Response(content=json.dumps({'type': 'danger', 'msg':'container_post - unknown api call' }, indent=4), media_type="application/json"))
|
||||||
|
|
||||||
|
dockerapi.logger.info("api call: %s, container_id: %s" % (api_call_method_name, container_id))
|
||||||
|
return api_call_method(request_json, container_id=container_id)
|
||||||
|
except Exception as e:
|
||||||
|
dockerapi.logger.error("error - container_post: %s" % str(e))
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": str(e)
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
else:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": "invalid container id or missing action"
|
||||||
|
}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
@app.post("/container/{container_id}/stats/update")
|
||||||
|
async def post_container_update_stats(container_id : str):
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
# start update task for container if no task is running
|
||||||
|
if container_id not in dockerapi.containerIds_to_update:
|
||||||
|
asyncio.create_task(dockerapi.get_container_stats(container_id))
|
||||||
|
dockerapi.containerIds_to_update.append(container_id)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if await dockerapi.redis_client.exists(container_id + '_stats'):
|
||||||
|
break
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
|
||||||
|
stats = json.loads(await dockerapi.redis_client.get(container_id + '_stats'))
|
||||||
|
return Response(content=json.dumps(stats, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
|
# PubSub Handler
|
||||||
|
async def handle_pubsub_messages(channel: aioredis.client.PubSub):
|
||||||
|
global dockerapi
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
async with async_timeout.timeout(60):
|
||||||
|
message = await channel.get_message(ignore_subscribe_messages=True, timeout=30)
|
||||||
|
if message is not None:
|
||||||
|
# Parse message
|
||||||
|
data_json = json.loads(message['data'].decode('utf-8'))
|
||||||
|
dockerapi.logger.info(f"PubSub Received - {json.dumps(data_json)}")
|
||||||
|
|
||||||
|
# Handle api_call
|
||||||
|
if 'api_call' in data_json:
|
||||||
|
# api_call: container_post
|
||||||
|
if data_json['api_call'] == "container_post":
|
||||||
|
if 'post_action' in data_json and 'container_name' in data_json:
|
||||||
|
try:
|
||||||
|
"""Dispatch container_post api call"""
|
||||||
|
request_json = {}
|
||||||
|
if data_json['post_action'] == 'exec':
|
||||||
|
if 'request' in data_json:
|
||||||
|
request_json = data_json['request']
|
||||||
|
if 'cmd' in request_json:
|
||||||
|
if 'task' in request_json:
|
||||||
|
api_call_method_name = '__'.join(['container_post', str(data_json['post_action']), str(request_json['cmd']), str(request_json['task']) ])
|
||||||
|
else:
|
||||||
|
dockerapi.logger.error("api call: task missing")
|
||||||
|
else:
|
||||||
|
dockerapi.logger.error("api call: cmd missing")
|
||||||
|
else:
|
||||||
|
dockerapi.logger.error("api call: request missing")
|
||||||
|
else:
|
||||||
|
api_call_method_name = '__'.join(['container_post', str(data_json['post_action'])])
|
||||||
|
|
||||||
|
if api_call_method_name:
|
||||||
|
api_call_method = getattr(dockerapi, api_call_method_name)
|
||||||
|
if api_call_method:
|
||||||
|
dockerapi.logger.info("api call: %s, container_name: %s" % (api_call_method_name, data_json['container_name']))
|
||||||
|
api_call_method(request_json, container_name=data_json['container_name'])
|
||||||
|
else:
|
||||||
|
dockerapi.logger.error("api call not found: %s, container_name: %s" % (api_call_method_name, data_json['container_name']))
|
||||||
|
except Exception as e:
|
||||||
|
dockerapi.logger.error("container_post: %s" % str(e))
|
||||||
|
else:
|
||||||
|
dockerapi.logger.error("api call: missing container_name, post_action or request")
|
||||||
|
else:
|
||||||
|
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
|
||||||
|
else:
|
||||||
|
dockerapi.logger.error("Unknwon PubSub recieved - %s" % json.dumps(data_json))
|
||||||
|
|
||||||
|
await asyncio.sleep(0.0)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
uvicorn.run(
|
||||||
|
app,
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=443,
|
||||||
|
ssl_certfile="/app/dockerapi_cert.pem",
|
||||||
|
ssl_keyfile="/app/dockerapi_key.pem",
|
||||||
|
log_level="info",
|
||||||
|
loop="none"
|
||||||
|
)
|
||||||
@@ -0,0 +1,487 @@
|
|||||||
|
import psutil
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
import platform
|
||||||
|
from datetime import datetime
|
||||||
|
from fastapi import FastAPI, Response, Request
|
||||||
|
|
||||||
|
class DockerApi:
|
||||||
|
def __init__(self, redis_client, sync_docker_client, async_docker_client, logger):
|
||||||
|
self.redis_client = redis_client
|
||||||
|
self.sync_docker_client = sync_docker_client
|
||||||
|
self.async_docker_client = async_docker_client
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
self.host_stats_isUpdating = False
|
||||||
|
self.containerIds_to_update = []
|
||||||
|
|
||||||
|
# api call: container_post - post_action: stop
|
||||||
|
def container_post__stop(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(all=True, filters=filters):
|
||||||
|
container.stop()
|
||||||
|
|
||||||
|
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: start
|
||||||
|
def container_post__start(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(all=True, filters=filters):
|
||||||
|
container.start()
|
||||||
|
|
||||||
|
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: restart
|
||||||
|
def container_post__restart(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(all=True, filters=filters):
|
||||||
|
container.restart()
|
||||||
|
|
||||||
|
res = { 'type': 'success', 'msg': 'command completed successfully'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: top
|
||||||
|
def container_post__top(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(all=True, filters=filters):
|
||||||
|
res = { 'type': 'success', 'msg': container.top()}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: stats
|
||||||
|
def container_post__stats(self, 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(all=True, filters=filters):
|
||||||
|
for stat in container.stats(decode=True, stream=True):
|
||||||
|
res = { 'type': 'success', 'msg': stat}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: delete
|
||||||
|
def container_post__exec__mailq__delete(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 'items' in request_json:
|
||||||
|
r = re.compile("^[0-9a-fA-F]+$")
|
||||||
|
filtered_qids = filter(r.match, request_json['items'])
|
||||||
|
if filtered_qids:
|
||||||
|
flagged_qids = ['-d %s' % i for i in filtered_qids]
|
||||||
|
sanitized_string = str(' '.join(flagged_qids))
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||||
|
return self.exec_run_handler('generic', postsuper_r)
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: hold
|
||||||
|
def container_post__exec__mailq__hold(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 'items' in request_json:
|
||||||
|
r = re.compile("^[0-9a-fA-F]+$")
|
||||||
|
filtered_qids = filter(r.match, request_json['items'])
|
||||||
|
if filtered_qids:
|
||||||
|
flagged_qids = ['-h %s' % i for i in filtered_qids]
|
||||||
|
sanitized_string = str(' '.join(flagged_qids))
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||||
|
return self.exec_run_handler('generic', postsuper_r)
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: cat
|
||||||
|
def container_post__exec__mailq__cat(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 'items' in request_json:
|
||||||
|
r = re.compile("^[0-9a-fA-F]+$")
|
||||||
|
filtered_qids = filter(r.match, request_json['items'])
|
||||||
|
if filtered_qids:
|
||||||
|
sanitized_string = str(' '.join(filtered_qids))
|
||||||
|
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
postcat_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postcat -q " + sanitized_string], user='postfix')
|
||||||
|
if not postcat_return:
|
||||||
|
postcat_return = 'err: invalid'
|
||||||
|
return self.exec_run_handler('utf8_text_only', postcat_return)
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: unhold
|
||||||
|
def container_post__exec__mailq__unhold(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 'items' in request_json:
|
||||||
|
r = re.compile("^[0-9a-fA-F]+$")
|
||||||
|
filtered_qids = filter(r.match, request_json['items'])
|
||||||
|
if filtered_qids:
|
||||||
|
flagged_qids = ['-H %s' % i for i in filtered_qids]
|
||||||
|
sanitized_string = str(' '.join(flagged_qids))
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
postsuper_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postsuper " + sanitized_string])
|
||||||
|
return self.exec_run_handler('generic', postsuper_r)
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: deliver
|
||||||
|
def container_post__exec__mailq__deliver(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 'items' in request_json:
|
||||||
|
r = re.compile("^[0-9a-fA-F]+$")
|
||||||
|
filtered_qids = filter(r.match, request_json['items'])
|
||||||
|
if filtered_qids:
|
||||||
|
flagged_qids = ['-i %s' % i for i in filtered_qids]
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
for i in flagged_qids:
|
||||||
|
postqueue_r = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postqueue " + i], user='postfix')
|
||||||
|
# todo: check each exit code
|
||||||
|
res = { 'type': 'success', 'msg': 'Scheduled immediate delivery'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: list
|
||||||
|
def container_post__exec__mailq__list(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):
|
||||||
|
mailq_return = container.exec_run(["/usr/sbin/postqueue", "-j"], user='postfix')
|
||||||
|
return self.exec_run_handler('utf8_text_only', mailq_return)
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: flush
|
||||||
|
def container_post__exec__mailq__flush(self, 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):
|
||||||
|
postqueue_r = container.exec_run(["/usr/sbin/postqueue", "-f"], user='postfix')
|
||||||
|
return self.exec_run_handler('generic', postqueue_r)
|
||||||
|
# api call: container_post - post_action: exec - cmd: mailq - task: super_delete
|
||||||
|
def container_post__exec__mailq__super_delete(self, 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):
|
||||||
|
postsuper_r = container.exec_run(["/usr/sbin/postsuper", "-d", "ALL"])
|
||||||
|
return self.exec_run_handler('generic', postsuper_r)
|
||||||
|
# api call: container_post - post_action: exec - cmd: system - task: fts_rescan
|
||||||
|
def container_post__exec__system__fts_rescan(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'username' in request_json:
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -u '" + request_json['username'].replace("'", "'\\''") + "'"], user='vmail')
|
||||||
|
if rescan_return.exit_code == 0:
|
||||||
|
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
if 'all' in request_json:
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
rescan_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm fts rescan -A"], user='vmail')
|
||||||
|
if rescan_return.exit_code == 0:
|
||||||
|
res = { 'type': 'success', 'msg': 'fts_rescan: rescan triggered'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
res = { 'type': 'warning', 'msg': 'fts_rescan error'}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: exec - cmd: system - task: df
|
||||||
|
def container_post__exec__system__df(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'dir' in request_json:
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
df_return = container.exec_run(["/bin/bash", "-c", "/bin/df -H '" + request_json['dir'].replace("'", "'\\''") + "' | /usr/bin/tail -n1 | /usr/bin/tr -s [:blank:] | /usr/bin/tr ' ' ','"], user='nobody')
|
||||||
|
if df_return.exit_code == 0:
|
||||||
|
return df_return.output.decode('utf-8').rstrip()
|
||||||
|
else:
|
||||||
|
return "0,0,0,0,0,0"
|
||||||
|
# api call: container_post - post_action: exec - cmd: system - task: mysql_upgrade
|
||||||
|
def container_post__exec__system__mysql_upgrade(self, 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):
|
||||||
|
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_upgrade -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "'\n"], user='mysql')
|
||||||
|
if sql_return.exit_code == 0:
|
||||||
|
matched = False
|
||||||
|
for line in sql_return.output.decode('utf-8').split("\n"):
|
||||||
|
if 'is already upgraded to' in line:
|
||||||
|
matched = True
|
||||||
|
if matched:
|
||||||
|
res = { 'type': 'success', 'msg':'mysql_upgrade: already upgraded', 'text': sql_return.output.decode('utf-8')}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
container.restart()
|
||||||
|
res = { 'type': 'warning', 'msg':'mysql_upgrade: upgrade was applied', 'text': sql_return.output.decode('utf-8')}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
res = { 'type': 'error', 'msg': 'mysql_upgrade: error running command', 'text': sql_return.output.decode('utf-8')}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: exec - cmd: system - task: mysql_tzinfo_to_sql
|
||||||
|
def container_post__exec__system__mysql_tzinfo_to_sql(self, 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):
|
||||||
|
sql_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/mysql_tzinfo_to_sql /usr/share/zoneinfo | /bin/sed 's/Local time zone must be set--see zic manual page/FCTY/' | /usr/bin/mysql -uroot -p'" + os.environ['DBROOT'].replace("'", "'\\''") + "' mysql \n"], user='mysql')
|
||||||
|
if sql_return.exit_code == 0:
|
||||||
|
res = { 'type': 'info', 'msg': 'mysql_tzinfo_to_sql: command completed successfully', 'text': sql_return.output.decode('utf-8')}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
res = { 'type': 'error', 'msg': 'mysql_tzinfo_to_sql: error running command', 'text': sql_return.output.decode('utf-8')}
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
# api call: container_post - post_action: exec - cmd: reload - task: dovecot
|
||||||
|
def container_post__exec__reload__dovecot(self, 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):
|
||||||
|
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/dovecot reload"])
|
||||||
|
return self.exec_run_handler('generic', reload_return)
|
||||||
|
# api call: container_post - post_action: exec - cmd: reload - task: postfix
|
||||||
|
def container_post__exec__reload__postfix(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):
|
||||||
|
reload_return = container.exec_run(["/bin/bash", "-c", "/usr/sbin/postfix reload"])
|
||||||
|
return self.exec_run_handler('generic', reload_return)
|
||||||
|
# api call: container_post - post_action: exec - cmd: reload - task: nginx
|
||||||
|
def container_post__exec__reload__nginx(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):
|
||||||
|
reload_return = container.exec_run(["/bin/sh", "-c", "/usr/sbin/nginx -s reload"])
|
||||||
|
return self.exec_run_handler('generic', reload_return)
|
||||||
|
# api call: container_post - post_action: exec - cmd: sieve - task: list
|
||||||
|
def container_post__exec__sieve__list(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 'username' in request_json:
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
sieve_return = container.exec_run(["/bin/bash", "-c", "/usr/bin/doveadm sieve list -u '" + request_json['username'].replace("'", "'\\''") + "'"])
|
||||||
|
return self.exec_run_handler('utf8_text_only', sieve_return)
|
||||||
|
# api call: container_post - post_action: exec - cmd: sieve - task: print
|
||||||
|
def container_post__exec__sieve__print(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'username' in request_json and 'script_name' in request_json:
|
||||||
|
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("'", "'\\''") + "'"]
|
||||||
|
sieve_return = container.exec_run(cmd)
|
||||||
|
return self.exec_run_handler('utf8_text_only', sieve_return)
|
||||||
|
# api call: container_post - post_action: exec - cmd: maildir - task: cleanup
|
||||||
|
def container_post__exec__maildir__cleanup(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'maildir' in request_json:
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
sane_name = re.sub(r'\W+', '', request_json['maildir'])
|
||||||
|
vmail_name = request_json['maildir'].replace("'", "'\\''")
|
||||||
|
cmd_vmail = "if [[ -d '/var/vmail/" + vmail_name + "' ]]; then /bin/mv '/var/vmail/" + vmail_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "'; fi"
|
||||||
|
index_name = request_json['maildir'].split("/")
|
||||||
|
if len(index_name) > 1:
|
||||||
|
index_name = index_name[1].replace("'", "'\\''") + "@" + index_name[0].replace("'", "'\\''")
|
||||||
|
cmd_vmail_index = "if [[ -d '/var/vmail_index/" + index_name + "' ]]; then /bin/mv '/var/vmail_index/" + index_name + "' '/var/vmail/_garbage/" + str(int(time.time())) + "_" + sane_name + "_index'; fi"
|
||||||
|
cmd = ["/bin/bash", "-c", cmd_vmail + " && " + cmd_vmail_index]
|
||||||
|
else:
|
||||||
|
cmd = ["/bin/bash", "-c", cmd_vmail]
|
||||||
|
maildir_cleanup = container.exec_run(cmd, user='vmail')
|
||||||
|
return self.exec_run_handler('generic', maildir_cleanup)
|
||||||
|
# api call: container_post - post_action: exec - cmd: rspamd - task: worker_password
|
||||||
|
def container_post__exec__rspamd__worker_password(self, request_json, **kwargs):
|
||||||
|
if 'container_id' in kwargs:
|
||||||
|
filters = {"id": kwargs['container_id']}
|
||||||
|
elif 'container_name' in kwargs:
|
||||||
|
filters = {"name": kwargs['container_name']}
|
||||||
|
|
||||||
|
if 'raw' in request_json:
|
||||||
|
for container in self.sync_docker_client.containers.list(filters=filters):
|
||||||
|
cmd = "/usr/bin/rspamadm pw -e -p '" + request_json['raw'].replace("'", "'\\''") + "' 2> /dev/null"
|
||||||
|
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")
|
||||||
|
|
||||||
|
matched = False
|
||||||
|
for line in cmd_response.split("\n"):
|
||||||
|
if '$2$' in line:
|
||||||
|
hash = line.strip()
|
||||||
|
hash_out = re.search('\$2\$.+$', hash).group(0)
|
||||||
|
rspamd_passphrase_hash = re.sub('[^0-9a-zA-Z\$]+', '', hash_out.rstrip())
|
||||||
|
rspamd_password_filename = "/etc/rspamd/override.d/worker-controller-password.inc"
|
||||||
|
cmd = '''/bin/echo 'enable_password = "%s";' > %s && cat %s''' % (rspamd_passphrase_hash, rspamd_password_filename, rspamd_password_filename)
|
||||||
|
cmd_response = self.exec_cmd_container(container, cmd, user="_rspamd")
|
||||||
|
if rspamd_passphrase_hash.startswith("$2$") and rspamd_passphrase_hash in cmd_response:
|
||||||
|
container.restart()
|
||||||
|
matched = True
|
||||||
|
if matched:
|
||||||
|
res = { 'type': 'success', 'msg': 'command completed successfully' }
|
||||||
|
self.logger.info('success changing Rspamd password')
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
self.logger.error('failed changing Rspamd password')
|
||||||
|
res = { 'type': 'danger', 'msg': 'command did not complete' }
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
|
||||||
|
# Collect host stats
|
||||||
|
async def get_host_stats(self, wait=5):
|
||||||
|
try:
|
||||||
|
system_time = datetime.now()
|
||||||
|
host_stats = {
|
||||||
|
"cpu": {
|
||||||
|
"cores": psutil.cpu_count(),
|
||||||
|
"usage": psutil.cpu_percent()
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"total": psutil.virtual_memory().total,
|
||||||
|
"usage": psutil.virtual_memory().percent,
|
||||||
|
"swap": psutil.swap_memory()
|
||||||
|
},
|
||||||
|
"uptime": time.time() - psutil.boot_time(),
|
||||||
|
"system_time": system_time.strftime("%d.%m.%Y %H:%M:%S"),
|
||||||
|
"architecture": platform.machine()
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.redis_client.set('host_stats', json.dumps(host_stats), ex=10)
|
||||||
|
except Exception as e:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
await asyncio.sleep(wait)
|
||||||
|
self.host_stats_isUpdating = False
|
||||||
|
# Collect container stats
|
||||||
|
async def get_container_stats(self, container_id, wait=5, stop=False):
|
||||||
|
if container_id and container_id.isalnum():
|
||||||
|
try:
|
||||||
|
for container in (await self.async_docker_client.containers.list()):
|
||||||
|
if container._id == container_id:
|
||||||
|
res = await container.stats(stream=False)
|
||||||
|
|
||||||
|
if await self.redis_client.exists(container_id + '_stats'):
|
||||||
|
stats = json.loads(await self.redis_client.get(container_id + '_stats'))
|
||||||
|
else:
|
||||||
|
stats = []
|
||||||
|
stats.append(res[0])
|
||||||
|
if len(stats) > 3:
|
||||||
|
del stats[0]
|
||||||
|
await self.redis_client.set(container_id + '_stats', json.dumps(stats), ex=60)
|
||||||
|
except Exception as e:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": str(e)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
res = {
|
||||||
|
"type": "danger",
|
||||||
|
"msg": "no or invalid id defined"
|
||||||
|
}
|
||||||
|
|
||||||
|
await asyncio.sleep(wait)
|
||||||
|
if stop == True:
|
||||||
|
# update task was called second time, stop
|
||||||
|
self.containerIds_to_update.remove(container_id)
|
||||||
|
else:
|
||||||
|
# call update task a second time
|
||||||
|
await self.get_container_stats(container_id, wait=0, stop=True)
|
||||||
|
|
||||||
|
def exec_cmd_container(self, container, cmd, user, timeout=2, shell_cmd="/bin/bash"):
|
||||||
|
def recv_socket_data(c_socket, timeout):
|
||||||
|
c_socket.setblocking(0)
|
||||||
|
total_data=[]
|
||||||
|
data=''
|
||||||
|
begin=time.time()
|
||||||
|
while True:
|
||||||
|
if total_data and time.time()-begin > timeout:
|
||||||
|
break
|
||||||
|
elif time.time()-begin > timeout*2:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
data = c_socket.recv(8192)
|
||||||
|
if data:
|
||||||
|
total_data.append(data.decode('utf-8'))
|
||||||
|
#change the beginning time for measurement
|
||||||
|
begin=time.time()
|
||||||
|
else:
|
||||||
|
#sleep for sometime to indicate a gap
|
||||||
|
time.sleep(0.1)
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return ''.join(total_data)
|
||||||
|
|
||||||
|
try :
|
||||||
|
socket = container.exec_run([shell_cmd], stdin=True, socket=True, user=user).output._sock
|
||||||
|
if not cmd.endswith("\n"):
|
||||||
|
cmd = cmd + "\n"
|
||||||
|
socket.send(cmd.encode('utf-8'))
|
||||||
|
data = recv_socket_data(socket, timeout)
|
||||||
|
socket.close()
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error("error - exec_cmd_container: %s" % str(e))
|
||||||
|
traceback.print_exc(file=sys.stdout)
|
||||||
|
|
||||||
|
def exec_run_handler(self, type, output):
|
||||||
|
if type == 'generic':
|
||||||
|
if output.exit_code == 0:
|
||||||
|
res = { 'type': 'success', 'msg': 'command completed successfully' }
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
else:
|
||||||
|
res = { 'type': 'danger', 'msg': 'command failed: ' + output.output.decode('utf-8') }
|
||||||
|
return Response(content=json.dumps(res, indent=4), media_type="application/json")
|
||||||
|
if type == 'utf8_text_only':
|
||||||
|
return Response(content=output.output.decode('utf-8'), media_type="text/plain")
|
||||||
@@ -1,115 +1,115 @@
|
|||||||
FROM debian:bullseye-slim
|
FROM alpine:3.19
|
||||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
|
||||||
# renovate: datasource=github-tags depName=dovecot/core versioning=semver-coerced
|
|
||||||
ARG DOVECOT=2.3.20
|
|
||||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
|
|
||||||
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 \
|
||||||
apt-transport-https \
|
bind-tools \
|
||||||
|
findutils \
|
||||||
|
envsubst \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
cpanminus \
|
|
||||||
curl \
|
curl \
|
||||||
dnsutils \
|
|
||||||
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 \
|
|
||||||
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 --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community/ \
|
||||||
|
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 \
|
&& arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
|
||||||
&& pip3 install mysql-connector-python html2text jinja2 redis \
|
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$arch" \
|
||||||
&& apt-get autoremove --purge -y \
|
&& chmod +x /usr/local/bin/gosu \
|
||||||
&& apt-get autoclean \
|
&& gosu nobody true
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
|
||||||
&& rm -rf /tmp/* /var/tmp/* /root/.cache/
|
# RUN cpan LockFile::Simple
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ function auth_password_verify(req, pass)
|
|||||||
VALUES ("%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip)))
|
VALUES ("%s", 0, "%s", "%s")]], con:escape(req.service), con:escape(req.user), con:escape(req.real_rip)))
|
||||||
cur:close()
|
cur:close()
|
||||||
con:close()
|
con:close()
|
||||||
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
|
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||||
end
|
end
|
||||||
row = cur:fetch (row, "a")
|
row = cur:fetch (row, "a")
|
||||||
end
|
end
|
||||||
@@ -180,13 +180,13 @@ function auth_password_verify(req, pass)
|
|||||||
if tostring(req.real_rip) == "__IPV4_SOGO__" then
|
if tostring(req.real_rip) == "__IPV4_SOGO__" then
|
||||||
cur:close()
|
cur:close()
|
||||||
con:close()
|
con:close()
|
||||||
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
|
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||||
elseif row.has_prot_access == "1" then
|
elseif row.has_prot_access == "1" then
|
||||||
con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
|
con:execute(string.format([[REPLACE INTO sasl_log (service, app_password, username, real_rip)
|
||||||
VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
|
VALUES ("%s", %d, "%s", "%s")]], con:escape(req.service), row.id, con:escape(req.user), con:escape(req.real_rip)))
|
||||||
cur:close()
|
cur:close()
|
||||||
con:close()
|
con:close()
|
||||||
return dovecot.auth.PASSDB_RESULT_OK, "password=" .. pass
|
return dovecot.auth.PASSDB_RESULT_OK, ""
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
row = cur:fetch (row, "a")
|
row = cur:fetch (row, "a")
|
||||||
@@ -432,4 +432,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 "$@"
|
||||||
|
|||||||
@@ -8492,6 +8492,7 @@ sub xoauth2
|
|||||||
require HTML::Entities ;
|
require HTML::Entities ;
|
||||||
require JSON ;
|
require JSON ;
|
||||||
require JSON::WebToken::Crypt::RSA ;
|
require JSON::WebToken::Crypt::RSA ;
|
||||||
|
require Crypt::OpenSSL::PKCS12;
|
||||||
require Crypt::OpenSSL::RSA ;
|
require Crypt::OpenSSL::RSA ;
|
||||||
require Encode::Byte ;
|
require Encode::Byte ;
|
||||||
require IO::Socket::SSL ;
|
require IO::Socket::SSL ;
|
||||||
@@ -8532,8 +8533,9 @@ sub xoauth2
|
|||||||
|
|
||||||
$sync->{ debug } and myprint( "Service account: $iss\nKey file: $keyfile\nKey password: $keypass\n");
|
$sync->{ debug } and myprint( "Service account: $iss\nKey file: $keyfile\nKey password: $keypass\n");
|
||||||
|
|
||||||
# Get private key from p12 file (would be better in perl...)
|
# Get private key from p12 file
|
||||||
$key = `openssl pkcs12 -in "$keyfile" -nodes -nocerts -passin pass:$keypass -nomacver`;
|
my $pkcs12 = Crypt::OpenSSL::PKCS12->new_from_file($keyfile);
|
||||||
|
$key = $pkcs12->private_key($keypass);
|
||||||
|
|
||||||
$sync->{ debug } and myprint( "Private key:\n$key\n");
|
$sync->{ debug } and myprint( "Private key:\n$key\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ my $sth = $dbh->prepare("SELECT id,
|
|||||||
custom_params,
|
custom_params,
|
||||||
subscribeall,
|
subscribeall,
|
||||||
timeout1,
|
timeout1,
|
||||||
timeout2
|
timeout2,
|
||||||
|
dry
|
||||||
FROM imapsync
|
FROM imapsync
|
||||||
WHERE active = 1
|
WHERE active = 1
|
||||||
AND is_running = 0
|
AND is_running = 0
|
||||||
@@ -111,13 +112,16 @@ while ($row = $sth->fetchrow_arrayref()) {
|
|||||||
$subscribeall = @$row[18];
|
$subscribeall = @$row[18];
|
||||||
$timeout1 = @$row[19];
|
$timeout1 = @$row[19];
|
||||||
$timeout2 = @$row[20];
|
$timeout2 = @$row[20];
|
||||||
|
$dry = @$row[21];
|
||||||
|
|
||||||
if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
|
if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
|
||||||
|
|
||||||
my $template = $run_dir . '/imapsync.XXXXXXX';
|
my $template = $run_dir . '/imapsync.XXXXXXX';
|
||||||
my $passfile1 = File::Temp->new(TEMPLATE => $template);
|
my $passfile1 = File::Temp->new(TEMPLATE => $template);
|
||||||
my $passfile2 = File::Temp->new(TEMPLATE => $template);
|
my $passfile2 = File::Temp->new(TEMPLATE => $template);
|
||||||
|
|
||||||
|
binmode( $passfile1, ":utf8" );
|
||||||
|
|
||||||
print $passfile1 "$password1\n";
|
print $passfile1 "$password1\n";
|
||||||
print $passfile2 trim($master_pass) . "\n";
|
print $passfile2 trim($master_pass) . "\n";
|
||||||
|
|
||||||
@@ -148,6 +152,7 @@ while ($row = $sth->fetchrow_arrayref()) {
|
|||||||
"--host2", "localhost",
|
"--host2", "localhost",
|
||||||
"--user2", $user2 . '*' . trim($master_user),
|
"--user2", $user2 . '*' . trim($master_user),
|
||||||
"--passfile2", $passfile2->filename,
|
"--passfile2", $passfile2->filename,
|
||||||
|
($dry eq "1" ? ('--dry') : ()),
|
||||||
'--no-modulesversion',
|
'--no-modulesversion',
|
||||||
'--noreleasecheck'];
|
'--noreleasecheck'];
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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,11 @@ 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));
|
||||||
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 +36,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);
|
||||||
|
|||||||
@@ -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,11 @@ 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));
|
||||||
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 +36,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);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
FROM alpine:3.17
|
FROM alpine:3.19
|
||||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
|
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
|
||||||
@@ -12,12 +15,16 @@ 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 \
|
||||||
tzdata \
|
tzdata \
|
||||||
py3-pip \
|
py3-pip \
|
||||||
|
py3-nftables \
|
||||||
musl-dev \
|
musl-dev \
|
||||||
&& pip3 install --ignore-installed --upgrade pip \
|
&& pip3 install --ignore-installed --upgrade pip \
|
||||||
|
jsonschema \
|
||||||
python-iptables \
|
python-iptables \
|
||||||
redis \
|
redis \
|
||||||
ipaddress \
|
ipaddress \
|
||||||
@@ -26,5 +33,10 @@ RUN apk add --virtual .build-deps \
|
|||||||
|
|
||||||
# && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \
|
# && pip3 install --upgrade pip python-iptables==0.13.0 redis ipaddress dnspython \
|
||||||
|
|
||||||
COPY server.py /
|
COPY modules /app/modules
|
||||||
CMD ["python3", "-u", "/server.py"]
|
COPY main.py /app/
|
||||||
|
COPY ./docker-entrypoint.sh /app/
|
||||||
|
|
||||||
|
RUN chmod +x /app/docker-entrypoint.sh
|
||||||
|
|
||||||
|
CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"]
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
backend=iptables
|
||||||
|
|
||||||
|
nft list table ip filter &>/dev/null
|
||||||
|
nftables_found=$?
|
||||||
|
|
||||||
|
iptables -L -n &>/dev/null
|
||||||
|
iptables_found=$?
|
||||||
|
|
||||||
|
if [ $nftables_found -lt $iptables_found ]; then
|
||||||
|
backend=nftables
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $nftables_found -gt $iptables_found ]; then
|
||||||
|
backend=iptables
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $nftables_found -eq 0 ] && [ $nftables_found -eq $iptables_found ]; then
|
||||||
|
nftables_lines=$(nft list ruleset | wc -l)
|
||||||
|
iptables_lines=$(iptables-save | wc -l)
|
||||||
|
if [ $nftables_lines -gt $iptables_lines ]; then
|
||||||
|
backend=nftables
|
||||||
|
else
|
||||||
|
backend=iptables
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec python -u /app/main.py $backend
|
||||||
@@ -0,0 +1,469 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import atexit
|
||||||
|
import signal
|
||||||
|
import ipaddress
|
||||||
|
from collections import Counter
|
||||||
|
from random import randint
|
||||||
|
from threading import Thread
|
||||||
|
from threading import Lock
|
||||||
|
import redis
|
||||||
|
import json
|
||||||
|
import dns.resolver
|
||||||
|
import dns.exception
|
||||||
|
import uuid
|
||||||
|
from modules.Logger import Logger
|
||||||
|
from modules.IPTables import IPTables
|
||||||
|
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
|
||||||
|
WHITELIST = []
|
||||||
|
BLACKLIST= []
|
||||||
|
bans = {}
|
||||||
|
quit_now = False
|
||||||
|
exit_code = 0
|
||||||
|
lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
|
# init Logger
|
||||||
|
logger = Logger(r)
|
||||||
|
# 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():
|
||||||
|
global f2boptions
|
||||||
|
global quit_now
|
||||||
|
global exit_code
|
||||||
|
|
||||||
|
f2boptions = {}
|
||||||
|
|
||||||
|
if not r.get('F2B_OPTIONS'):
|
||||||
|
f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
|
||||||
|
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
|
||||||
|
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
|
||||||
|
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
|
||||||
|
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
|
||||||
|
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
|
||||||
|
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
f2boptions = json.loads(r.get('F2B_OPTIONS'))
|
||||||
|
except ValueError:
|
||||||
|
logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json')
|
||||||
|
quit_now = True
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
verifyF2boptions(f2boptions)
|
||||||
|
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
||||||
|
|
||||||
|
def verifyF2boptions(f2boptions):
|
||||||
|
verifyF2boption(f2boptions,'ban_time', 1800)
|
||||||
|
verifyF2boption(f2boptions,'max_ban_time', 10000)
|
||||||
|
verifyF2boption(f2boptions,'ban_time_increment', True)
|
||||||
|
verifyF2boption(f2boptions,'max_attempts', 10)
|
||||||
|
verifyF2boption(f2boptions,'retry_window', 600)
|
||||||
|
verifyF2boption(f2boptions,'netban_ipv4', 32)
|
||||||
|
verifyF2boption(f2boptions,'netban_ipv6', 128)
|
||||||
|
verifyF2boption(f2boptions,'banlist_id', str(uuid.uuid4()))
|
||||||
|
verifyF2boption(f2boptions,'manage_external', 0)
|
||||||
|
|
||||||
|
def verifyF2boption(f2boptions, f2boption, f2bdefault):
|
||||||
|
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
|
||||||
|
|
||||||
|
def refreshF2bregex():
|
||||||
|
global f2bregex
|
||||||
|
global quit_now
|
||||||
|
global exit_code
|
||||||
|
if not r.get('F2B_REGEX'):
|
||||||
|
f2bregex = {}
|
||||||
|
f2bregex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
|
||||||
|
f2bregex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
|
||||||
|
f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
|
||||||
|
f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
|
||||||
|
f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
|
||||||
|
f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
|
||||||
|
f2bregex[7] = '-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[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
|
||||||
|
f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
|
||||||
|
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
f2bregex = {}
|
||||||
|
f2bregex = json.loads(r.get('F2B_REGEX'))
|
||||||
|
except ValueError:
|
||||||
|
logger.logCrit('Error loading F2B options: F2B_REGEX is not json')
|
||||||
|
quit_now = True
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
def get_ip(address):
|
||||||
|
ip = ipaddress.ip_address(address)
|
||||||
|
if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
|
||||||
|
ip = ip.ipv4_mapped
|
||||||
|
if ip.is_private or ip.is_loopback:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return ip
|
||||||
|
|
||||||
|
def ban(address):
|
||||||
|
global f2boptions
|
||||||
|
global lock
|
||||||
|
|
||||||
|
refreshF2boptions()
|
||||||
|
BAN_TIME = int(f2boptions['ban_time'])
|
||||||
|
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
|
||||||
|
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
||||||
|
RETRY_WINDOW = int(f2boptions['retry_window'])
|
||||||
|
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
|
||||||
|
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
|
||||||
|
|
||||||
|
ip = get_ip(address)
|
||||||
|
if not ip: return
|
||||||
|
address = str(ip)
|
||||||
|
self_network = ipaddress.ip_network(address)
|
||||||
|
|
||||||
|
with lock:
|
||||||
|
temp_whitelist = set(WHITELIST)
|
||||||
|
if temp_whitelist:
|
||||||
|
for wl_key in temp_whitelist:
|
||||||
|
wl_net = ipaddress.ip_network(wl_key, False)
|
||||||
|
if wl_net.overlaps(self_network):
|
||||||
|
logger.logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
|
||||||
|
return
|
||||||
|
|
||||||
|
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
|
||||||
|
net = str(net)
|
||||||
|
|
||||||
|
if not net in bans:
|
||||||
|
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
|
||||||
|
|
||||||
|
current_attempt = time.time()
|
||||||
|
if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW:
|
||||||
|
bans[net]['attempts'] = 0
|
||||||
|
|
||||||
|
bans[net]['attempts'] += 1
|
||||||
|
bans[net]['last_attempt'] = current_attempt
|
||||||
|
|
||||||
|
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||||
|
cur_time = int(round(time.time()))
|
||||||
|
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
|
||||||
|
logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
|
||||||
|
if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1:
|
||||||
|
with lock:
|
||||||
|
tables.banIPv4(net)
|
||||||
|
elif int(f2boptions['manage_external']) != 1:
|
||||||
|
with lock:
|
||||||
|
tables.banIPv6(net)
|
||||||
|
|
||||||
|
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
|
||||||
|
else:
|
||||||
|
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
||||||
|
|
||||||
|
def unban(net):
|
||||||
|
global lock
|
||||||
|
|
||||||
|
if not net in bans:
|
||||||
|
logger.logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
|
||||||
|
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.logInfo('Unbanning %s' % net)
|
||||||
|
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
|
||||||
|
with lock:
|
||||||
|
tables.unbanIPv4(net)
|
||||||
|
else:
|
||||||
|
with lock:
|
||||||
|
tables.unbanIPv6(net)
|
||||||
|
|
||||||
|
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
|
||||||
|
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
||||||
|
if net in bans:
|
||||||
|
bans[net]['attempts'] = 0
|
||||||
|
bans[net]['ban_counter'] += 1
|
||||||
|
|
||||||
|
def permBan(net, unban=False):
|
||||||
|
global f2boptions
|
||||||
|
global lock
|
||||||
|
|
||||||
|
is_unbanned = False
|
||||||
|
is_banned = False
|
||||||
|
if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
|
||||||
|
with lock:
|
||||||
|
if unban:
|
||||||
|
is_unbanned = tables.unbanIPv4(net)
|
||||||
|
elif int(f2boptions['manage_external']) != 1:
|
||||||
|
is_banned = tables.banIPv4(net)
|
||||||
|
else:
|
||||||
|
with lock:
|
||||||
|
if unban:
|
||||||
|
is_unbanned = tables.unbanIPv6(net)
|
||||||
|
elif int(f2boptions['manage_external']) != 1:
|
||||||
|
is_banned = tables.banIPv6(net)
|
||||||
|
|
||||||
|
|
||||||
|
if is_unbanned:
|
||||||
|
r.hdel('F2B_PERM_BANS', '%s' % net)
|
||||||
|
logger.logCrit('Removed host/network %s from blacklist' % net)
|
||||||
|
elif is_banned:
|
||||||
|
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
||||||
|
logger.logCrit('Added host/network %s to blacklist' % net)
|
||||||
|
|
||||||
|
def clear():
|
||||||
|
global lock
|
||||||
|
logger.logInfo('Clearing all bans')
|
||||||
|
for net in bans.copy():
|
||||||
|
unban(net)
|
||||||
|
with lock:
|
||||||
|
tables.clearIPv4Table()
|
||||||
|
tables.clearIPv6Table()
|
||||||
|
r.delete('F2B_ACTIVE_BANS')
|
||||||
|
r.delete('F2B_PERM_BANS')
|
||||||
|
pubsub.unsubscribe()
|
||||||
|
|
||||||
|
def watch():
|
||||||
|
logger.logInfo('Watching Redis channel F2B_CHANNEL')
|
||||||
|
pubsub.subscribe('F2B_CHANNEL')
|
||||||
|
|
||||||
|
global quit_now
|
||||||
|
global exit_code
|
||||||
|
|
||||||
|
while not quit_now:
|
||||||
|
try:
|
||||||
|
for item in pubsub.listen():
|
||||||
|
refreshF2bregex()
|
||||||
|
for rule_id, rule_regex in f2bregex.items():
|
||||||
|
if item['data'] and item['type'] == 'message':
|
||||||
|
try:
|
||||||
|
result = re.search(rule_regex, item['data'])
|
||||||
|
except re.error:
|
||||||
|
result = False
|
||||||
|
if result:
|
||||||
|
addr = result.group(1)
|
||||||
|
ip = ipaddress.ip_address(addr)
|
||||||
|
if ip.is_private or ip.is_loopback:
|
||||||
|
continue
|
||||||
|
logger.logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
|
||||||
|
ban(addr)
|
||||||
|
except Exception as ex:
|
||||||
|
logger.logWarn('Error reading log line from pubsub: %s' % ex)
|
||||||
|
quit_now = True
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
def snat4(snat_target):
|
||||||
|
global lock
|
||||||
|
global quit_now
|
||||||
|
|
||||||
|
while not quit_now:
|
||||||
|
time.sleep(10)
|
||||||
|
with lock:
|
||||||
|
tables.snat4(snat_target, os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24')
|
||||||
|
|
||||||
|
def snat6(snat_target):
|
||||||
|
global lock
|
||||||
|
global quit_now
|
||||||
|
|
||||||
|
while not quit_now:
|
||||||
|
time.sleep(10)
|
||||||
|
with lock:
|
||||||
|
tables.snat6(snat_target, os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64'))
|
||||||
|
|
||||||
|
def autopurge():
|
||||||
|
while not quit_now:
|
||||||
|
time.sleep(10)
|
||||||
|
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'])
|
||||||
|
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
|
||||||
|
if QUEUE_UNBAN:
|
||||||
|
for net in QUEUE_UNBAN:
|
||||||
|
unban(str(net))
|
||||||
|
for net in bans.copy():
|
||||||
|
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
||||||
|
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** bans[net]['ban_counter']
|
||||||
|
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:
|
||||||
|
unban(net)
|
||||||
|
|
||||||
|
def mailcowChainOrder():
|
||||||
|
global lock
|
||||||
|
global quit_now
|
||||||
|
global exit_code
|
||||||
|
while not quit_now:
|
||||||
|
time.sleep(10)
|
||||||
|
with lock:
|
||||||
|
quit_now, exit_code = tables.checkIPv4ChainOrder()
|
||||||
|
if quit_now: return
|
||||||
|
quit_now, exit_code = tables.checkIPv6ChainOrder()
|
||||||
|
|
||||||
|
def isIpNetwork(address):
|
||||||
|
try:
|
||||||
|
ipaddress.ip_network(address, False)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def genNetworkList(list):
|
||||||
|
resolver = dns.resolver.Resolver()
|
||||||
|
hostnames = []
|
||||||
|
networks = []
|
||||||
|
for key in list:
|
||||||
|
if isIpNetwork(key):
|
||||||
|
networks.append(key)
|
||||||
|
else:
|
||||||
|
hostnames.append(key)
|
||||||
|
for hostname in hostnames:
|
||||||
|
hostname_ips = []
|
||||||
|
for rdtype in ['A', 'AAAA']:
|
||||||
|
try:
|
||||||
|
answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
|
||||||
|
except dns.exception.Timeout:
|
||||||
|
logger.logInfo('Hostname %s timedout on resolve' % hostname)
|
||||||
|
break
|
||||||
|
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||||
|
continue
|
||||||
|
except dns.exception.DNSException as dnsexception:
|
||||||
|
logger.logInfo('%s' % dnsexception)
|
||||||
|
continue
|
||||||
|
for rdata in answer:
|
||||||
|
hostname_ips.append(rdata.to_text())
|
||||||
|
networks.extend(hostname_ips)
|
||||||
|
return set(networks)
|
||||||
|
|
||||||
|
def whitelistUpdate():
|
||||||
|
global lock
|
||||||
|
global quit_now
|
||||||
|
global WHITELIST
|
||||||
|
while not quit_now:
|
||||||
|
start_time = time.time()
|
||||||
|
list = r.hgetall('F2B_WHITELIST')
|
||||||
|
new_whitelist = []
|
||||||
|
if list:
|
||||||
|
new_whitelist = genNetworkList(list)
|
||||||
|
with lock:
|
||||||
|
if Counter(new_whitelist) != Counter(WHITELIST):
|
||||||
|
WHITELIST = new_whitelist
|
||||||
|
logger.logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
|
||||||
|
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||||
|
|
||||||
|
def blacklistUpdate():
|
||||||
|
global quit_now
|
||||||
|
global BLACKLIST
|
||||||
|
while not quit_now:
|
||||||
|
start_time = time.time()
|
||||||
|
list = r.hgetall('F2B_BLACKLIST')
|
||||||
|
new_blacklist = []
|
||||||
|
if list:
|
||||||
|
new_blacklist = genNetworkList(list)
|
||||||
|
if Counter(new_blacklist) != Counter(BLACKLIST):
|
||||||
|
addban = set(new_blacklist).difference(BLACKLIST)
|
||||||
|
delban = set(BLACKLIST).difference(new_blacklist)
|
||||||
|
BLACKLIST = new_blacklist
|
||||||
|
logger.logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
|
||||||
|
if addban:
|
||||||
|
for net in addban:
|
||||||
|
permBan(net=net)
|
||||||
|
if delban:
|
||||||
|
for net in delban:
|
||||||
|
permBan(net=net, unban=True)
|
||||||
|
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
||||||
|
|
||||||
|
def quit(signum, frame):
|
||||||
|
global quit_now
|
||||||
|
quit_now = True
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
refreshF2boptions()
|
||||||
|
# In case a previous session was killed without cleanup
|
||||||
|
clear()
|
||||||
|
# Reinit MAILCOW chain
|
||||||
|
# Is called before threads start, no locking
|
||||||
|
logger.logInfo("Initializing mailcow netfilter chain")
|
||||||
|
tables.initChainIPv4()
|
||||||
|
tables.initChainIPv6()
|
||||||
|
|
||||||
|
watch_thread = Thread(target=watch)
|
||||||
|
watch_thread.daemon = True
|
||||||
|
watch_thread.start()
|
||||||
|
|
||||||
|
if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':
|
||||||
|
try:
|
||||||
|
snat_ip = os.getenv('SNAT_TO_SOURCE')
|
||||||
|
snat_ipo = ipaddress.ip_address(snat_ip)
|
||||||
|
if type(snat_ipo) is ipaddress.IPv4Address:
|
||||||
|
snat4_thread = Thread(target=snat4,args=(snat_ip,))
|
||||||
|
snat4_thread.daemon = True
|
||||||
|
snat4_thread.start()
|
||||||
|
except ValueError:
|
||||||
|
print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
|
||||||
|
|
||||||
|
if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':
|
||||||
|
try:
|
||||||
|
snat_ip = os.getenv('SNAT6_TO_SOURCE')
|
||||||
|
snat_ipo = ipaddress.ip_address(snat_ip)
|
||||||
|
if type(snat_ipo) is ipaddress.IPv6Address:
|
||||||
|
snat6_thread = Thread(target=snat6,args=(snat_ip,))
|
||||||
|
snat6_thread.daemon = True
|
||||||
|
snat6_thread.start()
|
||||||
|
except ValueError:
|
||||||
|
print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')
|
||||||
|
|
||||||
|
autopurge_thread = Thread(target=autopurge)
|
||||||
|
autopurge_thread.daemon = True
|
||||||
|
autopurge_thread.start()
|
||||||
|
|
||||||
|
mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
|
||||||
|
mailcowchainwatch_thread.daemon = True
|
||||||
|
mailcowchainwatch_thread.start()
|
||||||
|
|
||||||
|
blacklistupdate_thread = Thread(target=blacklistUpdate)
|
||||||
|
blacklistupdate_thread.daemon = True
|
||||||
|
blacklistupdate_thread.start()
|
||||||
|
|
||||||
|
whitelistupdate_thread = Thread(target=whitelistUpdate)
|
||||||
|
whitelistupdate_thread.daemon = True
|
||||||
|
whitelistupdate_thread.start()
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, quit)
|
||||||
|
atexit.register(clear)
|
||||||
|
|
||||||
|
while not quit_now:
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
sys.exit(exit_code)
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
import iptc
|
||||||
|
import time
|
||||||
|
|
||||||
|
class IPTables:
|
||||||
|
def __init__(self, chain_name, logger):
|
||||||
|
self.chain_name = chain_name
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
def initChainIPv4(self):
|
||||||
|
if not iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name) in iptc.Table(iptc.Table.FILTER).chains:
|
||||||
|
iptc.Table(iptc.Table.FILTER).create_chain(self.chain_name)
|
||||||
|
for c in ['FORWARD', 'INPUT']:
|
||||||
|
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
|
||||||
|
rule = iptc.Rule()
|
||||||
|
rule.src = '0.0.0.0/0'
|
||||||
|
rule.dst = '0.0.0.0/0'
|
||||||
|
target = iptc.Target(rule, self.chain_name)
|
||||||
|
rule.target = target
|
||||||
|
if rule not in chain.rules:
|
||||||
|
chain.insert_rule(rule)
|
||||||
|
|
||||||
|
def initChainIPv6(self):
|
||||||
|
if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name) in iptc.Table6(iptc.Table6.FILTER).chains:
|
||||||
|
iptc.Table6(iptc.Table6.FILTER).create_chain(self.chain_name)
|
||||||
|
for c in ['FORWARD', 'INPUT']:
|
||||||
|
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
|
||||||
|
rule = iptc.Rule6()
|
||||||
|
rule.src = '::/0'
|
||||||
|
rule.dst = '::/0'
|
||||||
|
target = iptc.Target(rule, self.chain_name)
|
||||||
|
rule.target = target
|
||||||
|
if rule not in chain.rules:
|
||||||
|
chain.insert_rule(rule)
|
||||||
|
|
||||||
|
def checkIPv4ChainOrder(self):
|
||||||
|
filter_table = iptc.Table(iptc.Table.FILTER)
|
||||||
|
filter_table.refresh()
|
||||||
|
return self.checkChainOrder(filter_table)
|
||||||
|
|
||||||
|
def checkIPv6ChainOrder(self):
|
||||||
|
filter_table = iptc.Table6(iptc.Table6.FILTER)
|
||||||
|
filter_table.refresh()
|
||||||
|
return self.checkChainOrder(filter_table)
|
||||||
|
|
||||||
|
def checkChainOrder(self, filter_table):
|
||||||
|
err = False
|
||||||
|
exit_code = None
|
||||||
|
|
||||||
|
forward_chain = iptc.Chain(filter_table, 'FORWARD')
|
||||||
|
input_chain = iptc.Chain(filter_table, 'INPUT')
|
||||||
|
for chain in [forward_chain, input_chain]:
|
||||||
|
target_found = False
|
||||||
|
for position, item in enumerate(chain.rules):
|
||||||
|
if item.target.name == self.chain_name:
|
||||||
|
target_found = True
|
||||||
|
if position > 2:
|
||||||
|
self.logger.logCrit('Error in %s chain: %s target not found, restarting container' % (chain.name, self.chain_name))
|
||||||
|
err = True
|
||||||
|
exit_code = 2
|
||||||
|
if not target_found:
|
||||||
|
self.logger.logCrit('Error in %s chain: %s target not found, restarting container' % (chain.name, self.chain_name))
|
||||||
|
err = True
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
return err, exit_code
|
||||||
|
|
||||||
|
def clearIPv4Table(self):
|
||||||
|
self.clearTable(iptc.Table(iptc.Table.FILTER))
|
||||||
|
|
||||||
|
def clearIPv6Table(self):
|
||||||
|
self.clearTable(iptc.Table6(iptc.Table6.FILTER))
|
||||||
|
|
||||||
|
def clearTable(self, filter_table):
|
||||||
|
filter_table.autocommit = False
|
||||||
|
forward_chain = iptc.Chain(filter_table, "FORWARD")
|
||||||
|
input_chain = iptc.Chain(filter_table, "INPUT")
|
||||||
|
mailcow_chain = iptc.Chain(filter_table, self.chain_name)
|
||||||
|
if mailcow_chain in filter_table.chains:
|
||||||
|
for rule in mailcow_chain.rules:
|
||||||
|
mailcow_chain.delete_rule(rule)
|
||||||
|
for rule in forward_chain.rules:
|
||||||
|
if rule.target.name == self.chain_name:
|
||||||
|
forward_chain.delete_rule(rule)
|
||||||
|
for rule in input_chain.rules:
|
||||||
|
if rule.target.name == self.chain_name:
|
||||||
|
input_chain.delete_rule(rule)
|
||||||
|
filter_table.delete_chain(self.chain_name)
|
||||||
|
filter_table.commit()
|
||||||
|
filter_table.refresh()
|
||||||
|
filter_table.autocommit = True
|
||||||
|
|
||||||
|
def banIPv4(self, source):
|
||||||
|
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name)
|
||||||
|
rule = iptc.Rule()
|
||||||
|
rule.src = source
|
||||||
|
target = iptc.Target(rule, "REJECT")
|
||||||
|
rule.target = target
|
||||||
|
if rule in chain.rules:
|
||||||
|
return False
|
||||||
|
chain.insert_rule(rule)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def banIPv6(self, source):
|
||||||
|
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name)
|
||||||
|
rule = iptc.Rule6()
|
||||||
|
rule.src = source
|
||||||
|
target = iptc.Target(rule, "REJECT")
|
||||||
|
rule.target = target
|
||||||
|
if rule in chain.rules:
|
||||||
|
return False
|
||||||
|
chain.insert_rule(rule)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def unbanIPv4(self, source):
|
||||||
|
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name)
|
||||||
|
rule = iptc.Rule()
|
||||||
|
rule.src = source
|
||||||
|
target = iptc.Target(rule, "REJECT")
|
||||||
|
rule.target = target
|
||||||
|
if rule not in chain.rules:
|
||||||
|
return False
|
||||||
|
chain.delete_rule(rule)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def unbanIPv6(self, source):
|
||||||
|
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name)
|
||||||
|
rule = iptc.Rule6()
|
||||||
|
rule.src = source
|
||||||
|
target = iptc.Target(rule, "REJECT")
|
||||||
|
rule.target = target
|
||||||
|
if rule not in chain.rules:
|
||||||
|
return False
|
||||||
|
chain.delete_rule(rule)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def snat4(self, snat_target, source):
|
||||||
|
try:
|
||||||
|
table = iptc.Table('nat')
|
||||||
|
table.refresh()
|
||||||
|
chain = iptc.Chain(table, 'POSTROUTING')
|
||||||
|
table.autocommit = False
|
||||||
|
new_rule = self.getSnat4Rule(snat_target, source)
|
||||||
|
|
||||||
|
if not chain.rules:
|
||||||
|
# if there are no rules in the chain, insert the new rule directly
|
||||||
|
self.logger.logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
|
||||||
|
chain.insert_rule(new_rule)
|
||||||
|
else:
|
||||||
|
for position, rule in enumerate(chain.rules):
|
||||||
|
if not hasattr(rule.target, 'parameter'):
|
||||||
|
continue
|
||||||
|
match = all((
|
||||||
|
new_rule.get_src() == rule.get_src(),
|
||||||
|
new_rule.get_dst() == rule.get_dst(),
|
||||||
|
new_rule.target.parameters == rule.target.parameters,
|
||||||
|
new_rule.target.name == rule.target.name
|
||||||
|
))
|
||||||
|
if position == 0:
|
||||||
|
if not match:
|
||||||
|
self.logger.logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
|
||||||
|
chain.insert_rule(new_rule)
|
||||||
|
else:
|
||||||
|
if match:
|
||||||
|
self.logger.logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
|
||||||
|
chain.delete_rule(rule)
|
||||||
|
|
||||||
|
table.commit()
|
||||||
|
table.autocommit = True
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
self.logger.logCrit('Error running SNAT4, retrying...')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def snat6(self, snat_target, source):
|
||||||
|
try:
|
||||||
|
table = iptc.Table6('nat')
|
||||||
|
table.refresh()
|
||||||
|
chain = iptc.Chain(table, 'POSTROUTING')
|
||||||
|
table.autocommit = False
|
||||||
|
new_rule = self.getSnat6Rule(snat_target, source)
|
||||||
|
|
||||||
|
if new_rule not in chain.rules:
|
||||||
|
self.logger.logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (new_rule.src, snat_target))
|
||||||
|
chain.insert_rule(new_rule)
|
||||||
|
else:
|
||||||
|
for position, item in enumerate(chain.rules):
|
||||||
|
if item == new_rule:
|
||||||
|
if position != 0:
|
||||||
|
chain.delete_rule(new_rule)
|
||||||
|
|
||||||
|
table.commit()
|
||||||
|
table.autocommit = True
|
||||||
|
except:
|
||||||
|
self.logger.logCrit('Error running SNAT6, retrying...')
|
||||||
|
|
||||||
|
|
||||||
|
def getSnat4Rule(self, snat_target, source):
|
||||||
|
rule = iptc.Rule()
|
||||||
|
rule.src = source
|
||||||
|
rule.dst = '!' + rule.src
|
||||||
|
target = rule.create_target("SNAT")
|
||||||
|
target.to_source = snat_target
|
||||||
|
match = rule.create_match("comment")
|
||||||
|
match.comment = f'{int(round(time.time()))}'
|
||||||
|
return rule
|
||||||
|
|
||||||
|
def getSnat6Rule(self, snat_target, source):
|
||||||
|
rule = iptc.Rule6()
|
||||||
|
rule.src = source
|
||||||
|
rule.dst = '!' + rule.src
|
||||||
|
target = rule.create_target("SNAT")
|
||||||
|
target.to_source = snat_target
|
||||||
|
return rule
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
class Logger:
|
||||||
|
def __init__(self, redis):
|
||||||
|
self.r = redis
|
||||||
|
|
||||||
|
def log(self, priority, message):
|
||||||
|
tolog = {}
|
||||||
|
tolog['time'] = int(round(time.time()))
|
||||||
|
tolog['priority'] = priority
|
||||||
|
tolog['message'] = message
|
||||||
|
self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
|
||||||
|
print(message)
|
||||||
|
|
||||||
|
def logWarn(self, message):
|
||||||
|
self.log('warn', message)
|
||||||
|
|
||||||
|
def logCrit(self, message):
|
||||||
|
self.log('crit', message)
|
||||||
|
|
||||||
|
def logInfo(self, message):
|
||||||
|
self.log('info', message)
|
||||||
@@ -0,0 +1,495 @@
|
|||||||
|
import nftables
|
||||||
|
import ipaddress
|
||||||
|
|
||||||
|
class NFTables:
|
||||||
|
def __init__(self, chain_name, logger):
|
||||||
|
self.chain_name = chain_name
|
||||||
|
self.logger = logger
|
||||||
|
|
||||||
|
self.nft = nftables.Nftables()
|
||||||
|
self.nft.set_json_output(True)
|
||||||
|
self.nft.set_handle_output(True)
|
||||||
|
self.nft_chain_names = {'ip': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} },
|
||||||
|
'ip6': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} } }
|
||||||
|
|
||||||
|
self.search_current_chains()
|
||||||
|
|
||||||
|
def initChainIPv4(self):
|
||||||
|
self.insert_mailcow_chains("ip")
|
||||||
|
|
||||||
|
def initChainIPv6(self):
|
||||||
|
self.insert_mailcow_chains("ip6")
|
||||||
|
|
||||||
|
def checkIPv4ChainOrder(self):
|
||||||
|
return self.checkChainOrder("ip")
|
||||||
|
|
||||||
|
def checkIPv6ChainOrder(self):
|
||||||
|
return self.checkChainOrder("ip6")
|
||||||
|
|
||||||
|
def checkChainOrder(self, filter_table):
|
||||||
|
err = False
|
||||||
|
exit_code = None
|
||||||
|
|
||||||
|
for chain in ['input', 'forward']:
|
||||||
|
chain_position = self.check_mailcow_chains(filter_table, chain)
|
||||||
|
if chain_position is None: continue
|
||||||
|
|
||||||
|
if chain_position is False:
|
||||||
|
self.logger.logCrit(f'MAILCOW target not found in {filter_table} {chain} table, restarting container to fix it...')
|
||||||
|
err = True
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
if chain_position > 0:
|
||||||
|
self.logger.logCrit(f'MAILCOW target is in position {chain_position} in the {filter_table} {chain} table, restarting container to fix it...')
|
||||||
|
err = True
|
||||||
|
exit_code = 2
|
||||||
|
|
||||||
|
return err, exit_code
|
||||||
|
|
||||||
|
def clearIPv4Table(self):
|
||||||
|
self.clearTable("ip")
|
||||||
|
|
||||||
|
def clearIPv6Table(self):
|
||||||
|
self.clearTable("ip6")
|
||||||
|
|
||||||
|
def clearTable(self, _family):
|
||||||
|
is_empty_dict = True
|
||||||
|
json_command = self.get_base_dict()
|
||||||
|
chain_handle = self.get_chain_handle(_family, "filter", self.chain_name)
|
||||||
|
# if no handle, the chain doesn't exists
|
||||||
|
if chain_handle is not None:
|
||||||
|
is_empty_dict = False
|
||||||
|
# flush chain
|
||||||
|
mailcow_chain = {'family': _family, 'table': 'filter', 'name': self.chain_name}
|
||||||
|
flush_chain = {'flush': {'chain': mailcow_chain}}
|
||||||
|
json_command["nftables"].append(flush_chain)
|
||||||
|
|
||||||
|
# remove rule in forward chain
|
||||||
|
# remove rule in input chain
|
||||||
|
chains_family = [self.nft_chain_names[_family]['filter']['input'],
|
||||||
|
self.nft_chain_names[_family]['filter']['forward'] ]
|
||||||
|
|
||||||
|
for chain_base in chains_family:
|
||||||
|
if not chain_base: continue
|
||||||
|
|
||||||
|
rules_handle = self.get_rules_handle(_family, "filter", chain_base)
|
||||||
|
if rules_handle is not None:
|
||||||
|
for r_handle in rules_handle:
|
||||||
|
is_empty_dict = False
|
||||||
|
mailcow_rule = {'family':_family,
|
||||||
|
'table': 'filter',
|
||||||
|
'chain': chain_base,
|
||||||
|
'handle': r_handle }
|
||||||
|
delete_rules = {'delete': {'rule': mailcow_rule} }
|
||||||
|
json_command["nftables"].append(delete_rules)
|
||||||
|
|
||||||
|
# remove chain
|
||||||
|
# after delete all rules referencing this chain
|
||||||
|
if chain_handle is not None:
|
||||||
|
mc_chain_handle = {'family':_family,
|
||||||
|
'table': 'filter',
|
||||||
|
'name': self.chain_name,
|
||||||
|
'handle': chain_handle }
|
||||||
|
delete_chain = {'delete': {'chain': mc_chain_handle} }
|
||||||
|
json_command["nftables"].append(delete_chain)
|
||||||
|
|
||||||
|
if is_empty_dict == False:
|
||||||
|
if self.nft_exec_dict(json_command):
|
||||||
|
self.logger.logInfo(f"Clear completed: {_family}")
|
||||||
|
|
||||||
|
def banIPv4(self, source):
|
||||||
|
ban_dict = self.get_ban_ip_dict(source, "ip")
|
||||||
|
return self.nft_exec_dict(ban_dict)
|
||||||
|
|
||||||
|
def banIPv6(self, source):
|
||||||
|
ban_dict = self.get_ban_ip_dict(source, "ip6")
|
||||||
|
return self.nft_exec_dict(ban_dict)
|
||||||
|
|
||||||
|
def unbanIPv4(self, source):
|
||||||
|
unban_dict = self.get_unban_ip_dict(source, "ip")
|
||||||
|
if not unban_dict:
|
||||||
|
return False
|
||||||
|
return self.nft_exec_dict(unban_dict)
|
||||||
|
|
||||||
|
def unbanIPv6(self, source):
|
||||||
|
unban_dict = self.get_unban_ip_dict(source, "ip6")
|
||||||
|
if not unban_dict:
|
||||||
|
return False
|
||||||
|
return self.nft_exec_dict(unban_dict)
|
||||||
|
|
||||||
|
def snat4(self, snat_target, source):
|
||||||
|
self.snat_rule("ip", snat_target, source)
|
||||||
|
|
||||||
|
def snat6(self, snat_target, source):
|
||||||
|
self.snat_rule("ip6", snat_target, source)
|
||||||
|
|
||||||
|
|
||||||
|
def nft_exec_dict(self, query: dict):
|
||||||
|
if not query: return False
|
||||||
|
|
||||||
|
rc, output, error = self.nft.json_cmd(query)
|
||||||
|
if rc != 0:
|
||||||
|
#self.logger.logCrit(f"Nftables Error: {error}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Prevent returning False or empty string on commands that do not produce output
|
||||||
|
if rc == 0 and len(output) == 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
def get_base_dict(self):
|
||||||
|
return {'nftables': [{ 'metainfo': { 'json_schema_version': 1} } ] }
|
||||||
|
|
||||||
|
def search_current_chains(self):
|
||||||
|
nft_chain_priority = {'ip': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} },
|
||||||
|
'ip6': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} } }
|
||||||
|
|
||||||
|
# Command: 'nft list chains'
|
||||||
|
_list = {'list' : {'chains': 'null'} }
|
||||||
|
command = self.get_base_dict()
|
||||||
|
command['nftables'].append(_list)
|
||||||
|
kernel_ruleset = self.nft_exec_dict(command)
|
||||||
|
if kernel_ruleset:
|
||||||
|
for _object in kernel_ruleset['nftables']:
|
||||||
|
chain = _object.get("chain")
|
||||||
|
if not chain: continue
|
||||||
|
|
||||||
|
_family = chain['family']
|
||||||
|
_table = chain['table']
|
||||||
|
_hook = chain.get("hook")
|
||||||
|
_priority = chain.get("prio")
|
||||||
|
_name = chain['name']
|
||||||
|
|
||||||
|
if _family not in self.nft_chain_names: continue
|
||||||
|
if _table not in self.nft_chain_names[_family]: continue
|
||||||
|
if _hook not in self.nft_chain_names[_family][_table]: continue
|
||||||
|
if _priority is None: continue
|
||||||
|
|
||||||
|
_saved_priority = nft_chain_priority[_family][_table][_hook]
|
||||||
|
if _saved_priority is None or _priority < _saved_priority:
|
||||||
|
# at this point, we know the chain has:
|
||||||
|
# hook and priority set
|
||||||
|
# and it has the lowest priority
|
||||||
|
nft_chain_priority[_family][_table][_hook] = _priority
|
||||||
|
self.nft_chain_names[_family][_table][_hook] = _name
|
||||||
|
|
||||||
|
def search_for_chain(self, kernel_ruleset: dict, chain_name: str):
|
||||||
|
found = False
|
||||||
|
for _object in kernel_ruleset["nftables"]:
|
||||||
|
chain = _object.get("chain")
|
||||||
|
if not chain:
|
||||||
|
continue
|
||||||
|
ch_name = chain.get("name")
|
||||||
|
if ch_name == chain_name:
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
return found
|
||||||
|
|
||||||
|
def get_chain_dict(self, _family: str, _name: str):
|
||||||
|
# nft (add | create) chain [<family>] <table> <name>
|
||||||
|
_chain_opts = {'family': _family, 'table': 'filter', 'name': _name }
|
||||||
|
_add = {'add': {'chain': _chain_opts} }
|
||||||
|
final_chain = self.get_base_dict()
|
||||||
|
final_chain["nftables"].append(_add)
|
||||||
|
return final_chain
|
||||||
|
|
||||||
|
def get_mailcow_jump_rule_dict(self, _family: str, _chain: str):
|
||||||
|
_jump_rule = self.get_base_dict()
|
||||||
|
_expr_opt=[]
|
||||||
|
_expr_counter = {'family': _family, 'table': 'filter', 'packets': 0, 'bytes': 0}
|
||||||
|
_counter_dict = {'counter': _expr_counter}
|
||||||
|
_expr_opt.append(_counter_dict)
|
||||||
|
|
||||||
|
_jump_opts = {'jump': {'target': self.chain_name} }
|
||||||
|
|
||||||
|
_expr_opt.append(_jump_opts)
|
||||||
|
|
||||||
|
_rule_params = {'family': _family,
|
||||||
|
'table': 'filter',
|
||||||
|
'chain': _chain,
|
||||||
|
'expr': _expr_opt,
|
||||||
|
'comment': "mailcow" }
|
||||||
|
|
||||||
|
_add_rule = {'insert': {'rule': _rule_params} }
|
||||||
|
|
||||||
|
_jump_rule["nftables"].append(_add_rule)
|
||||||
|
|
||||||
|
return _jump_rule
|
||||||
|
|
||||||
|
def insert_mailcow_chains(self, _family: str):
|
||||||
|
nft_input_chain = self.nft_chain_names[_family]['filter']['input']
|
||||||
|
nft_forward_chain = self.nft_chain_names[_family]['filter']['forward']
|
||||||
|
# Command: 'nft list table <family> filter'
|
||||||
|
_table_opts = {'family': _family, 'name': 'filter'}
|
||||||
|
_list = {'list': {'table': _table_opts} }
|
||||||
|
command = self.get_base_dict()
|
||||||
|
command['nftables'].append(_list)
|
||||||
|
kernel_ruleset = self.nft_exec_dict(command)
|
||||||
|
if kernel_ruleset:
|
||||||
|
# chain
|
||||||
|
if not self.search_for_chain(kernel_ruleset, self.chain_name):
|
||||||
|
cadena = self.get_chain_dict(_family, self.chain_name)
|
||||||
|
if self.nft_exec_dict(cadena):
|
||||||
|
self.logger.logInfo(f"MAILCOW {_family} chain created successfully.")
|
||||||
|
|
||||||
|
input_jump_found, forward_jump_found = False, False
|
||||||
|
|
||||||
|
for _object in kernel_ruleset["nftables"]:
|
||||||
|
if not _object.get("rule"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
rule = _object["rule"]
|
||||||
|
if nft_input_chain and rule["chain"] == nft_input_chain:
|
||||||
|
if rule.get("comment") and rule["comment"] == "mailcow":
|
||||||
|
input_jump_found = True
|
||||||
|
if nft_forward_chain and rule["chain"] == nft_forward_chain:
|
||||||
|
if rule.get("comment") and rule["comment"] == "mailcow":
|
||||||
|
forward_jump_found = True
|
||||||
|
|
||||||
|
if not input_jump_found:
|
||||||
|
command = self.get_mailcow_jump_rule_dict(_family, nft_input_chain)
|
||||||
|
self.nft_exec_dict(command)
|
||||||
|
|
||||||
|
if not forward_jump_found:
|
||||||
|
command = self.get_mailcow_jump_rule_dict(_family, nft_forward_chain)
|
||||||
|
self.nft_exec_dict(command)
|
||||||
|
|
||||||
|
def delete_nat_rule(self, _family:str, _chain: str, _handle:str):
|
||||||
|
delete_command = self.get_base_dict()
|
||||||
|
_rule_opts = {'family': _family,
|
||||||
|
'table': 'nat',
|
||||||
|
'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):
|
||||||
|
chain_name = self.nft_chain_names[_family]['nat']['postrouting']
|
||||||
|
|
||||||
|
# no postrouting chain, may occur if docker has ipv6 disabled.
|
||||||
|
if not chain_name: return
|
||||||
|
|
||||||
|
# Command: nft list chain <family> nat <chain_name>
|
||||||
|
_chain_opts = {'family': _family, 'table': 'nat', 'name': chain_name}
|
||||||
|
_list = {'list':{'chain': _chain_opts} }
|
||||||
|
command = self.get_base_dict()
|
||||||
|
command['nftables'].append(_list)
|
||||||
|
kernel_ruleset = self.nft_exec_dict(command)
|
||||||
|
if not kernel_ruleset:
|
||||||
|
return
|
||||||
|
|
||||||
|
rule_position = 0
|
||||||
|
rule_handle = None
|
||||||
|
rule_found = False
|
||||||
|
for _object in kernel_ruleset["nftables"]:
|
||||||
|
if not _object.get("rule"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
rule = _object["rule"]
|
||||||
|
if not rule.get("comment") or not rule["comment"] == "mailcow":
|
||||||
|
rule_position +=1
|
||||||
|
continue
|
||||||
|
|
||||||
|
rule_found = True
|
||||||
|
rule_handle = rule["handle"]
|
||||||
|
break
|
||||||
|
|
||||||
|
dest_net = ipaddress.ip_network(source_address)
|
||||||
|
target_net = ipaddress.ip_network(snat_target)
|
||||||
|
|
||||||
|
if rule_found:
|
||||||
|
saddr_ip = rule["expr"][0]["match"]["right"]["prefix"]["addr"]
|
||||||
|
saddr_len = int(rule["expr"][0]["match"]["right"]["prefix"]["len"])
|
||||||
|
|
||||||
|
daddr_ip = rule["expr"][1]["match"]["right"]["prefix"]["addr"]
|
||||||
|
daddr_len = int(rule["expr"][1]["match"]["right"]["prefix"]["len"])
|
||||||
|
|
||||||
|
target_ip = rule["expr"][3]["snat"]["addr"]
|
||||||
|
|
||||||
|
saddr_net = ipaddress.ip_network(saddr_ip + '/' + str(saddr_len))
|
||||||
|
daddr_net = ipaddress.ip_network(daddr_ip + '/' + str(daddr_len))
|
||||||
|
current_target_net = ipaddress.ip_network(target_ip)
|
||||||
|
|
||||||
|
match = all((
|
||||||
|
dest_net == saddr_net,
|
||||||
|
dest_net == daddr_net,
|
||||||
|
target_net == current_target_net
|
||||||
|
))
|
||||||
|
try:
|
||||||
|
if rule_position == 0:
|
||||||
|
if not match:
|
||||||
|
# Position 0 , it is a mailcow rule , but it does not have the same parameters
|
||||||
|
if self.delete_nat_rule(_family, chain_name, rule_handle):
|
||||||
|
self.logger.logInfo(f'Remove rule for source network {saddr_net} to SNAT target {target_net} from {_family} nat {chain_name} chain, rule does not match configured parameters')
|
||||||
|
else:
|
||||||
|
# Position > 0 and is mailcow rule
|
||||||
|
if self.delete_nat_rule(_family, chain_name, rule_handle):
|
||||||
|
self.logger.logInfo(f'Remove rule for source network {saddr_net} to SNAT target {target_net} from {_family} nat {chain_name} chain, rule is at position {rule_position}')
|
||||||
|
except:
|
||||||
|
self.logger.logCrit(f"Error running SNAT on {_family}, retrying..." )
|
||||||
|
else:
|
||||||
|
# rule not found
|
||||||
|
json_command = self.get_base_dict()
|
||||||
|
try:
|
||||||
|
snat_dict = {'snat': {'addr': str(target_net.network_address)} }
|
||||||
|
|
||||||
|
expr_counter = {'family': _family, 'table': 'nat', 'packets': 0, 'bytes': 0}
|
||||||
|
counter_dict = {'counter': expr_counter}
|
||||||
|
|
||||||
|
prefix_dict = {'prefix': {'addr': str(dest_net.network_address), 'len': int(dest_net.prefixlen)} }
|
||||||
|
payload_dict = {'payload': {'protocol': _family, 'field': "saddr"} }
|
||||||
|
match_dict1 = {'match': {'op': '==', 'left': payload_dict, 'right': prefix_dict} }
|
||||||
|
|
||||||
|
payload_dict2 = {'payload': {'protocol': _family, 'field': "daddr"} }
|
||||||
|
match_dict2 = {'match': {'op': '!=', 'left': payload_dict2, 'right': prefix_dict } }
|
||||||
|
expr_list = [
|
||||||
|
match_dict1,
|
||||||
|
match_dict2,
|
||||||
|
counter_dict,
|
||||||
|
snat_dict
|
||||||
|
]
|
||||||
|
rule_fields = {'family': _family,
|
||||||
|
'table': 'nat',
|
||||||
|
'chain': chain_name,
|
||||||
|
'comment': "mailcow",
|
||||||
|
'expr': expr_list }
|
||||||
|
|
||||||
|
insert_dict = {'insert': {'rule': rule_fields} }
|
||||||
|
json_command["nftables"].append(insert_dict)
|
||||||
|
if self.nft_exec_dict(json_command):
|
||||||
|
self.logger.logInfo(f'Added {_family} nat {chain_name} rule for source network {dest_net} to {target_net}')
|
||||||
|
except:
|
||||||
|
self.logger.logCrit(f"Error running SNAT on {_family}, retrying...")
|
||||||
|
|
||||||
|
def get_chain_handle(self, _family: str, _table: str, chain_name: str):
|
||||||
|
chain_handle = None
|
||||||
|
# Command: 'nft list chains {family}'
|
||||||
|
_list = {'list': {'chains': {'family': _family} } }
|
||||||
|
command = self.get_base_dict()
|
||||||
|
command['nftables'].append(_list)
|
||||||
|
kernel_ruleset = self.nft_exec_dict(command)
|
||||||
|
if kernel_ruleset:
|
||||||
|
for _object in kernel_ruleset["nftables"]:
|
||||||
|
if not _object.get("chain"):
|
||||||
|
continue
|
||||||
|
chain = _object["chain"]
|
||||||
|
if chain["family"] == _family and chain["table"] == _table and chain["name"] == chain_name:
|
||||||
|
chain_handle = chain["handle"]
|
||||||
|
break
|
||||||
|
return chain_handle
|
||||||
|
|
||||||
|
def get_rules_handle(self, _family: str, _table: str, chain_name: str):
|
||||||
|
rule_handle = []
|
||||||
|
# Command: 'nft list chain {family} {table} {chain_name}'
|
||||||
|
_chain_opts = {'family': _family, 'table': _table, 'name': chain_name}
|
||||||
|
_list = {'list': {'chain': _chain_opts} }
|
||||||
|
command = self.get_base_dict()
|
||||||
|
command['nftables'].append(_list)
|
||||||
|
|
||||||
|
kernel_ruleset = self.nft_exec_dict(command)
|
||||||
|
if kernel_ruleset:
|
||||||
|
for _object in kernel_ruleset["nftables"]:
|
||||||
|
if not _object.get("rule"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
rule = _object["rule"]
|
||||||
|
if rule["family"] == _family and rule["table"] == _table and rule["chain"] == chain_name:
|
||||||
|
if rule.get("comment") and rule["comment"] == "mailcow":
|
||||||
|
rule_handle.append(rule["handle"])
|
||||||
|
return rule_handle
|
||||||
|
|
||||||
|
def get_ban_ip_dict(self, ipaddr: str, _family: str):
|
||||||
|
json_command = self.get_base_dict()
|
||||||
|
|
||||||
|
expr_opt = []
|
||||||
|
ipaddr_net = ipaddress.ip_network(ipaddr)
|
||||||
|
right_dict = {'prefix': {'addr': str(ipaddr_net.network_address), 'len': int(ipaddr_net.prefixlen) } }
|
||||||
|
|
||||||
|
left_dict = {'payload': {'protocol': _family, 'field': 'saddr'} }
|
||||||
|
match_dict = {'op': '==', 'left': left_dict, 'right': right_dict }
|
||||||
|
expr_opt.append({'match': match_dict})
|
||||||
|
|
||||||
|
counter_dict = {'counter': {'family': _family, 'table': "filter", 'packets': 0, 'bytes': 0} }
|
||||||
|
expr_opt.append(counter_dict)
|
||||||
|
|
||||||
|
expr_opt.append({'drop': "null"})
|
||||||
|
|
||||||
|
rule_dict = {'family': _family, 'table': "filter", 'chain': self.chain_name, 'expr': expr_opt}
|
||||||
|
|
||||||
|
base_dict = {'insert': {'rule': rule_dict} }
|
||||||
|
json_command["nftables"].append(base_dict)
|
||||||
|
|
||||||
|
return json_command
|
||||||
|
|
||||||
|
def get_unban_ip_dict(self, ipaddr:str, _family: str):
|
||||||
|
json_command = self.get_base_dict()
|
||||||
|
# Command: 'nft list chain {s_family} filter MAILCOW'
|
||||||
|
_chain_opts = {'family': _family, 'table': 'filter', 'name': self.chain_name}
|
||||||
|
_list = {'list': {'chain': _chain_opts} }
|
||||||
|
command = self.get_base_dict()
|
||||||
|
command['nftables'].append(_list)
|
||||||
|
kernel_ruleset = self.nft_exec_dict(command)
|
||||||
|
rule_handle = None
|
||||||
|
if kernel_ruleset:
|
||||||
|
for _object in kernel_ruleset["nftables"]:
|
||||||
|
if not _object.get("rule"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
rule = _object["rule"]["expr"][0]["match"]
|
||||||
|
left_opt = rule["left"]["payload"]
|
||||||
|
if not left_opt["protocol"] == _family:
|
||||||
|
continue
|
||||||
|
if not left_opt["field"] =="saddr":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ip currently banned
|
||||||
|
rule_right = rule["right"]
|
||||||
|
if isinstance(rule_right, dict):
|
||||||
|
current_rule_ip = rule_right["prefix"]["addr"] + '/' + str(rule_right["prefix"]["len"])
|
||||||
|
else:
|
||||||
|
current_rule_ip = rule_right
|
||||||
|
current_rule_net = ipaddress.ip_network(current_rule_ip)
|
||||||
|
|
||||||
|
# ip to ban
|
||||||
|
candidate_net = ipaddress.ip_network(ipaddr)
|
||||||
|
|
||||||
|
if current_rule_net == candidate_net:
|
||||||
|
rule_handle = _object["rule"]["handle"]
|
||||||
|
break
|
||||||
|
|
||||||
|
if rule_handle is not None:
|
||||||
|
mailcow_rule = {'family': _family, 'table': 'filter', 'chain': self.chain_name, 'handle': rule_handle}
|
||||||
|
delete_rule = {'delete': {'rule': mailcow_rule} }
|
||||||
|
json_command["nftables"].append(delete_rule)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return json_command
|
||||||
|
|
||||||
|
def check_mailcow_chains(self, family: str, chain: str):
|
||||||
|
position = 0
|
||||||
|
rule_found = False
|
||||||
|
chain_name = self.nft_chain_names[family]['filter'][chain]
|
||||||
|
|
||||||
|
if not chain_name: return None
|
||||||
|
|
||||||
|
_chain_opts = {'family': family, 'table': 'filter', 'name': chain_name}
|
||||||
|
_list = {'list': {'chain': _chain_opts}}
|
||||||
|
command = self.get_base_dict()
|
||||||
|
command['nftables'].append(_list)
|
||||||
|
kernel_ruleset = self.nft_exec_dict(command)
|
||||||
|
if kernel_ruleset:
|
||||||
|
for _object in kernel_ruleset["nftables"]:
|
||||||
|
if not _object.get("rule"):
|
||||||
|
continue
|
||||||
|
rule = _object["rule"]
|
||||||
|
if rule.get("comment") and rule["comment"] == "mailcow":
|
||||||
|
rule_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
position+=1
|
||||||
|
|
||||||
|
return position if rule_found else False
|
||||||
@@ -1,587 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import atexit
|
|
||||||
import signal
|
|
||||||
import ipaddress
|
|
||||||
from collections import Counter
|
|
||||||
from random import randint
|
|
||||||
from threading import Thread
|
|
||||||
from threading import Lock
|
|
||||||
import redis
|
|
||||||
import json
|
|
||||||
import iptc
|
|
||||||
import dns.resolver
|
|
||||||
import dns.exception
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
WHITELIST = []
|
|
||||||
BLACKLIST= []
|
|
||||||
|
|
||||||
bans = {}
|
|
||||||
|
|
||||||
quit_now = False
|
|
||||||
exit_code = 0
|
|
||||||
lock = Lock()
|
|
||||||
|
|
||||||
def log(priority, message):
|
|
||||||
tolog = {}
|
|
||||||
tolog['time'] = int(round(time.time()))
|
|
||||||
tolog['priority'] = priority
|
|
||||||
tolog['message'] = message
|
|
||||||
r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False))
|
|
||||||
print(message)
|
|
||||||
|
|
||||||
def logWarn(message):
|
|
||||||
log('warn', message)
|
|
||||||
|
|
||||||
def logCrit(message):
|
|
||||||
log('crit', message)
|
|
||||||
|
|
||||||
def logInfo(message):
|
|
||||||
log('info', message)
|
|
||||||
|
|
||||||
def refreshF2boptions():
|
|
||||||
global f2boptions
|
|
||||||
global quit_now
|
|
||||||
global exit_code
|
|
||||||
if not r.get('F2B_OPTIONS'):
|
|
||||||
f2boptions = {}
|
|
||||||
f2boptions['ban_time'] = int
|
|
||||||
f2boptions['max_attempts'] = int
|
|
||||||
f2boptions['retry_window'] = int
|
|
||||||
f2boptions['netban_ipv4'] = int
|
|
||||||
f2boptions['netban_ipv6'] = int
|
|
||||||
f2boptions['ban_time'] = r.get('F2B_BAN_TIME') or 1800
|
|
||||||
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS') or 10
|
|
||||||
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW') or 600
|
|
||||||
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4') or 32
|
|
||||||
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6') or 128
|
|
||||||
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
f2boptions = {}
|
|
||||||
f2boptions = json.loads(r.get('F2B_OPTIONS'))
|
|
||||||
except ValueError:
|
|
||||||
print('Error loading F2B options: F2B_OPTIONS is not json')
|
|
||||||
quit_now = True
|
|
||||||
exit_code = 2
|
|
||||||
|
|
||||||
def refreshF2bregex():
|
|
||||||
global f2bregex
|
|
||||||
global quit_now
|
|
||||||
global exit_code
|
|
||||||
if not r.get('F2B_REGEX'):
|
|
||||||
f2bregex = {}
|
|
||||||
f2bregex[1] = 'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
|
|
||||||
f2bregex[2] = 'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
|
|
||||||
f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
|
|
||||||
f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
|
|
||||||
f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
|
|
||||||
f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),'
|
|
||||||
f2bregex[7] = '-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[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
|
|
||||||
f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
|
|
||||||
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
f2bregex = {}
|
|
||||||
f2bregex = json.loads(r.get('F2B_REGEX'))
|
|
||||||
except ValueError:
|
|
||||||
print('Error loading F2B options: F2B_REGEX is not json')
|
|
||||||
quit_now = True
|
|
||||||
exit_code = 2
|
|
||||||
|
|
||||||
if r.exists('F2B_LOG'):
|
|
||||||
r.rename('F2B_LOG', 'NETFILTER_LOG')
|
|
||||||
|
|
||||||
def mailcowChainOrder():
|
|
||||||
global lock
|
|
||||||
global quit_now
|
|
||||||
global exit_code
|
|
||||||
while not quit_now:
|
|
||||||
time.sleep(10)
|
|
||||||
with lock:
|
|
||||||
filter4_table = iptc.Table(iptc.Table.FILTER)
|
|
||||||
filter6_table = iptc.Table6(iptc.Table6.FILTER)
|
|
||||||
filter4_table.refresh()
|
|
||||||
filter6_table.refresh()
|
|
||||||
for f in [filter4_table, filter6_table]:
|
|
||||||
forward_chain = iptc.Chain(f, 'FORWARD')
|
|
||||||
input_chain = iptc.Chain(f, 'INPUT')
|
|
||||||
for chain in [forward_chain, input_chain]:
|
|
||||||
target_found = False
|
|
||||||
for position, item in enumerate(chain.rules):
|
|
||||||
if item.target.name == 'MAILCOW':
|
|
||||||
target_found = True
|
|
||||||
if position > 2:
|
|
||||||
logCrit('Error in %s chain order: MAILCOW on position %d, restarting container' % (chain.name, position))
|
|
||||||
quit_now = True
|
|
||||||
exit_code = 2
|
|
||||||
if not target_found:
|
|
||||||
logCrit('Error in %s chain: MAILCOW target not found, restarting container' % (chain.name))
|
|
||||||
quit_now = True
|
|
||||||
exit_code = 2
|
|
||||||
|
|
||||||
def ban(address):
|
|
||||||
global lock
|
|
||||||
refreshF2boptions()
|
|
||||||
BAN_TIME = int(f2boptions['ban_time'])
|
|
||||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
|
||||||
RETRY_WINDOW = int(f2boptions['retry_window'])
|
|
||||||
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
|
|
||||||
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
|
|
||||||
|
|
||||||
ip = ipaddress.ip_address(address)
|
|
||||||
if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
|
|
||||||
ip = ip.ipv4_mapped
|
|
||||||
address = str(ip)
|
|
||||||
if ip.is_private or ip.is_loopback:
|
|
||||||
return
|
|
||||||
|
|
||||||
self_network = ipaddress.ip_network(address)
|
|
||||||
|
|
||||||
with lock:
|
|
||||||
temp_whitelist = set(WHITELIST)
|
|
||||||
|
|
||||||
if temp_whitelist:
|
|
||||||
for wl_key in temp_whitelist:
|
|
||||||
wl_net = ipaddress.ip_network(wl_key, False)
|
|
||||||
if wl_net.overlaps(self_network):
|
|
||||||
logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net))
|
|
||||||
return
|
|
||||||
|
|
||||||
net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
|
|
||||||
net = str(net)
|
|
||||||
|
|
||||||
if not net in bans or time.time() - bans[net]['last_attempt'] > RETRY_WINDOW:
|
|
||||||
bans[net] = { 'attempts': 0 }
|
|
||||||
active_window = RETRY_WINDOW
|
|
||||||
else:
|
|
||||||
active_window = time.time() - bans[net]['last_attempt']
|
|
||||||
|
|
||||||
bans[net]['attempts'] += 1
|
|
||||||
bans[net]['last_attempt'] = time.time()
|
|
||||||
|
|
||||||
active_window = time.time() - bans[net]['last_attempt']
|
|
||||||
|
|
||||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
|
||||||
cur_time = int(round(time.time()))
|
|
||||||
logCrit('Banning %s for %d minutes' % (net, BAN_TIME / 60))
|
|
||||||
if type(ip) is ipaddress.IPv4Address:
|
|
||||||
with lock:
|
|
||||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
|
|
||||||
rule = iptc.Rule()
|
|
||||||
rule.src = net
|
|
||||||
target = iptc.Target(rule, "REJECT")
|
|
||||||
rule.target = target
|
|
||||||
if rule not in chain.rules:
|
|
||||||
chain.insert_rule(rule)
|
|
||||||
else:
|
|
||||||
with lock:
|
|
||||||
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
|
|
||||||
rule = iptc.Rule6()
|
|
||||||
rule.src = net
|
|
||||||
target = iptc.Target(rule, "REJECT")
|
|
||||||
rule.target = target
|
|
||||||
if rule not in chain.rules:
|
|
||||||
chain.insert_rule(rule)
|
|
||||||
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + BAN_TIME)
|
|
||||||
else:
|
|
||||||
logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
|
|
||||||
|
|
||||||
def unban(net):
|
|
||||||
global lock
|
|
||||||
if not net in bans:
|
|
||||||
logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net)
|
|
||||||
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
|
||||||
return
|
|
||||||
logInfo('Unbanning %s' % net)
|
|
||||||
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
|
|
||||||
with lock:
|
|
||||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
|
|
||||||
rule = iptc.Rule()
|
|
||||||
rule.src = net
|
|
||||||
target = iptc.Target(rule, "REJECT")
|
|
||||||
rule.target = target
|
|
||||||
if rule in chain.rules:
|
|
||||||
chain.delete_rule(rule)
|
|
||||||
else:
|
|
||||||
with lock:
|
|
||||||
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
|
|
||||||
rule = iptc.Rule6()
|
|
||||||
rule.src = net
|
|
||||||
target = iptc.Target(rule, "REJECT")
|
|
||||||
rule.target = target
|
|
||||||
if rule in chain.rules:
|
|
||||||
chain.delete_rule(rule)
|
|
||||||
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
|
|
||||||
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
|
|
||||||
if net in bans:
|
|
||||||
del bans[net]
|
|
||||||
|
|
||||||
def permBan(net, unban=False):
|
|
||||||
global lock
|
|
||||||
if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
|
|
||||||
with lock:
|
|
||||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), 'MAILCOW')
|
|
||||||
rule = iptc.Rule()
|
|
||||||
rule.src = net
|
|
||||||
target = iptc.Target(rule, "REJECT")
|
|
||||||
rule.target = target
|
|
||||||
if rule not in chain.rules and not unban:
|
|
||||||
logCrit('Add host/network %s to blacklist' % net)
|
|
||||||
chain.insert_rule(rule)
|
|
||||||
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
|
||||||
elif rule in chain.rules and unban:
|
|
||||||
logCrit('Remove host/network %s from blacklist' % net)
|
|
||||||
chain.delete_rule(rule)
|
|
||||||
r.hdel('F2B_PERM_BANS', '%s' % net)
|
|
||||||
else:
|
|
||||||
with lock:
|
|
||||||
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), 'MAILCOW')
|
|
||||||
rule = iptc.Rule6()
|
|
||||||
rule.src = net
|
|
||||||
target = iptc.Target(rule, "REJECT")
|
|
||||||
rule.target = target
|
|
||||||
if rule not in chain.rules and not unban:
|
|
||||||
logCrit('Add host/network %s to blacklist' % net)
|
|
||||||
chain.insert_rule(rule)
|
|
||||||
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
|
|
||||||
elif rule in chain.rules and unban:
|
|
||||||
logCrit('Remove host/network %s from blacklist' % net)
|
|
||||||
chain.delete_rule(rule)
|
|
||||||
r.hdel('F2B_PERM_BANS', '%s' % net)
|
|
||||||
|
|
||||||
def quit(signum, frame):
|
|
||||||
global quit_now
|
|
||||||
quit_now = True
|
|
||||||
|
|
||||||
def clear():
|
|
||||||
global lock
|
|
||||||
logInfo('Clearing all bans')
|
|
||||||
for net in bans.copy():
|
|
||||||
unban(net)
|
|
||||||
with lock:
|
|
||||||
filter4_table = iptc.Table(iptc.Table.FILTER)
|
|
||||||
filter6_table = iptc.Table6(iptc.Table6.FILTER)
|
|
||||||
for filter_table in [filter4_table, filter6_table]:
|
|
||||||
filter_table.autocommit = False
|
|
||||||
forward_chain = iptc.Chain(filter_table, "FORWARD")
|
|
||||||
input_chain = iptc.Chain(filter_table, "INPUT")
|
|
||||||
mailcow_chain = iptc.Chain(filter_table, "MAILCOW")
|
|
||||||
if mailcow_chain in filter_table.chains:
|
|
||||||
for rule in mailcow_chain.rules:
|
|
||||||
mailcow_chain.delete_rule(rule)
|
|
||||||
for rule in forward_chain.rules:
|
|
||||||
if rule.target.name == 'MAILCOW':
|
|
||||||
forward_chain.delete_rule(rule)
|
|
||||||
for rule in input_chain.rules:
|
|
||||||
if rule.target.name == 'MAILCOW':
|
|
||||||
input_chain.delete_rule(rule)
|
|
||||||
filter_table.delete_chain("MAILCOW")
|
|
||||||
filter_table.commit()
|
|
||||||
filter_table.refresh()
|
|
||||||
filter_table.autocommit = True
|
|
||||||
r.delete('F2B_ACTIVE_BANS')
|
|
||||||
r.delete('F2B_PERM_BANS')
|
|
||||||
pubsub.unsubscribe()
|
|
||||||
|
|
||||||
def watch():
|
|
||||||
logInfo('Watching Redis channel F2B_CHANNEL')
|
|
||||||
pubsub.subscribe('F2B_CHANNEL')
|
|
||||||
|
|
||||||
global quit_now
|
|
||||||
global exit_code
|
|
||||||
|
|
||||||
while not quit_now:
|
|
||||||
try:
|
|
||||||
for item in pubsub.listen():
|
|
||||||
refreshF2bregex()
|
|
||||||
for rule_id, rule_regex in f2bregex.items():
|
|
||||||
if item['data'] and item['type'] == 'message':
|
|
||||||
try:
|
|
||||||
result = re.search(rule_regex, item['data'])
|
|
||||||
except re.error:
|
|
||||||
result = False
|
|
||||||
if result:
|
|
||||||
addr = result.group(1)
|
|
||||||
ip = ipaddress.ip_address(addr)
|
|
||||||
if ip.is_private or ip.is_loopback:
|
|
||||||
continue
|
|
||||||
logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
|
|
||||||
ban(addr)
|
|
||||||
except Exception as ex:
|
|
||||||
logWarn('Error reading log line from pubsub')
|
|
||||||
quit_now = True
|
|
||||||
exit_code = 2
|
|
||||||
|
|
||||||
def snat4(snat_target):
|
|
||||||
global lock
|
|
||||||
global quit_now
|
|
||||||
|
|
||||||
def get_snat4_rule():
|
|
||||||
rule = iptc.Rule()
|
|
||||||
rule.src = os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24'
|
|
||||||
rule.dst = '!' + rule.src
|
|
||||||
target = rule.create_target("SNAT")
|
|
||||||
target.to_source = snat_target
|
|
||||||
match = rule.create_match("comment")
|
|
||||||
match.comment = f'{int(round(time.time()))}'
|
|
||||||
return rule
|
|
||||||
|
|
||||||
while not quit_now:
|
|
||||||
time.sleep(10)
|
|
||||||
with lock:
|
|
||||||
try:
|
|
||||||
table = iptc.Table('nat')
|
|
||||||
table.refresh()
|
|
||||||
chain = iptc.Chain(table, 'POSTROUTING')
|
|
||||||
table.autocommit = False
|
|
||||||
new_rule = get_snat4_rule()
|
|
||||||
for position, rule in enumerate(chain.rules):
|
|
||||||
match = all((
|
|
||||||
new_rule.get_src() == rule.get_src(),
|
|
||||||
new_rule.get_dst() == rule.get_dst(),
|
|
||||||
new_rule.target.parameters == rule.target.parameters,
|
|
||||||
new_rule.target.name == rule.target.name
|
|
||||||
))
|
|
||||||
if position == 0:
|
|
||||||
if not match:
|
|
||||||
logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}')
|
|
||||||
chain.insert_rule(new_rule)
|
|
||||||
else:
|
|
||||||
if match:
|
|
||||||
logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}')
|
|
||||||
chain.delete_rule(rule)
|
|
||||||
table.commit()
|
|
||||||
table.autocommit = True
|
|
||||||
except:
|
|
||||||
print('Error running SNAT4, retrying...')
|
|
||||||
|
|
||||||
def snat6(snat_target):
|
|
||||||
global lock
|
|
||||||
global quit_now
|
|
||||||
|
|
||||||
def get_snat6_rule():
|
|
||||||
rule = iptc.Rule6()
|
|
||||||
rule.src = os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64')
|
|
||||||
rule.dst = '!' + rule.src
|
|
||||||
target = rule.create_target("SNAT")
|
|
||||||
target.to_source = snat_target
|
|
||||||
return rule
|
|
||||||
|
|
||||||
while not quit_now:
|
|
||||||
time.sleep(10)
|
|
||||||
with lock:
|
|
||||||
try:
|
|
||||||
table = iptc.Table6('nat')
|
|
||||||
table.refresh()
|
|
||||||
chain = iptc.Chain(table, 'POSTROUTING')
|
|
||||||
table.autocommit = False
|
|
||||||
if get_snat6_rule() not in chain.rules:
|
|
||||||
logInfo('Added POSTROUTING rule for source network %s to SNAT target %s' % (get_snat6_rule().src, snat_target))
|
|
||||||
chain.insert_rule(get_snat6_rule())
|
|
||||||
table.commit()
|
|
||||||
else:
|
|
||||||
for position, item in enumerate(chain.rules):
|
|
||||||
if item == get_snat6_rule():
|
|
||||||
if position != 0:
|
|
||||||
chain.delete_rule(get_snat6_rule())
|
|
||||||
table.commit()
|
|
||||||
table.autocommit = True
|
|
||||||
except:
|
|
||||||
print('Error running SNAT6, retrying...')
|
|
||||||
|
|
||||||
def autopurge():
|
|
||||||
while not quit_now:
|
|
||||||
time.sleep(10)
|
|
||||||
refreshF2boptions()
|
|
||||||
BAN_TIME = int(f2boptions['ban_time'])
|
|
||||||
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
|
|
||||||
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
|
|
||||||
if QUEUE_UNBAN:
|
|
||||||
for net in QUEUE_UNBAN:
|
|
||||||
unban(str(net))
|
|
||||||
for net in bans.copy():
|
|
||||||
if bans[net]['attempts'] >= MAX_ATTEMPTS:
|
|
||||||
if time.time() - bans[net]['last_attempt'] > BAN_TIME:
|
|
||||||
unban(net)
|
|
||||||
|
|
||||||
def isIpNetwork(address):
|
|
||||||
try:
|
|
||||||
ipaddress.ip_network(address, False)
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def genNetworkList(list):
|
|
||||||
resolver = dns.resolver.Resolver()
|
|
||||||
hostnames = []
|
|
||||||
networks = []
|
|
||||||
for key in list:
|
|
||||||
if isIpNetwork(key):
|
|
||||||
networks.append(key)
|
|
||||||
else:
|
|
||||||
hostnames.append(key)
|
|
||||||
for hostname in hostnames:
|
|
||||||
hostname_ips = []
|
|
||||||
for rdtype in ['A', 'AAAA']:
|
|
||||||
try:
|
|
||||||
answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
|
|
||||||
except dns.exception.Timeout:
|
|
||||||
logInfo('Hostname %s timedout on resolve' % hostname)
|
|
||||||
break
|
|
||||||
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
|
||||||
continue
|
|
||||||
except dns.exception.DNSException as dnsexception:
|
|
||||||
logInfo('%s' % dnsexception)
|
|
||||||
continue
|
|
||||||
for rdata in answer:
|
|
||||||
hostname_ips.append(rdata.to_text())
|
|
||||||
networks.extend(hostname_ips)
|
|
||||||
return set(networks)
|
|
||||||
|
|
||||||
def whitelistUpdate():
|
|
||||||
global lock
|
|
||||||
global quit_now
|
|
||||||
global WHITELIST
|
|
||||||
while not quit_now:
|
|
||||||
start_time = time.time()
|
|
||||||
list = r.hgetall('F2B_WHITELIST')
|
|
||||||
new_whitelist = []
|
|
||||||
if list:
|
|
||||||
new_whitelist = genNetworkList(list)
|
|
||||||
with lock:
|
|
||||||
if Counter(new_whitelist) != Counter(WHITELIST):
|
|
||||||
WHITELIST = new_whitelist
|
|
||||||
logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST))
|
|
||||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
|
||||||
|
|
||||||
def blacklistUpdate():
|
|
||||||
global quit_now
|
|
||||||
global BLACKLIST
|
|
||||||
while not quit_now:
|
|
||||||
start_time = time.time()
|
|
||||||
list = r.hgetall('F2B_BLACKLIST')
|
|
||||||
new_blacklist = []
|
|
||||||
if list:
|
|
||||||
new_blacklist = genNetworkList(list)
|
|
||||||
if Counter(new_blacklist) != Counter(BLACKLIST):
|
|
||||||
addban = set(new_blacklist).difference(BLACKLIST)
|
|
||||||
delban = set(BLACKLIST).difference(new_blacklist)
|
|
||||||
BLACKLIST = new_blacklist
|
|
||||||
logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST))
|
|
||||||
if addban:
|
|
||||||
for net in addban:
|
|
||||||
permBan(net=net)
|
|
||||||
if delban:
|
|
||||||
for net in delban:
|
|
||||||
permBan(net=net, unban=True)
|
|
||||||
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
|
|
||||||
|
|
||||||
def initChain():
|
|
||||||
# Is called before threads start, no locking
|
|
||||||
print("Initializing mailcow netfilter chain")
|
|
||||||
# IPv4
|
|
||||||
if not iptc.Chain(iptc.Table(iptc.Table.FILTER), "MAILCOW") in iptc.Table(iptc.Table.FILTER).chains:
|
|
||||||
iptc.Table(iptc.Table.FILTER).create_chain("MAILCOW")
|
|
||||||
for c in ['FORWARD', 'INPUT']:
|
|
||||||
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
|
|
||||||
rule = iptc.Rule()
|
|
||||||
rule.src = '0.0.0.0/0'
|
|
||||||
rule.dst = '0.0.0.0/0'
|
|
||||||
target = iptc.Target(rule, "MAILCOW")
|
|
||||||
rule.target = target
|
|
||||||
if rule not in chain.rules:
|
|
||||||
chain.insert_rule(rule)
|
|
||||||
# IPv6
|
|
||||||
if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), "MAILCOW") in iptc.Table6(iptc.Table6.FILTER).chains:
|
|
||||||
iptc.Table6(iptc.Table6.FILTER).create_chain("MAILCOW")
|
|
||||||
for c in ['FORWARD', 'INPUT']:
|
|
||||||
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
|
|
||||||
rule = iptc.Rule6()
|
|
||||||
rule.src = '::/0'
|
|
||||||
rule.dst = '::/0'
|
|
||||||
target = iptc.Target(rule, "MAILCOW")
|
|
||||||
rule.target = target
|
|
||||||
if rule not in chain.rules:
|
|
||||||
chain.insert_rule(rule)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
|
||||||
# In case a previous session was killed without cleanup
|
|
||||||
clear()
|
|
||||||
# Reinit MAILCOW chain
|
|
||||||
initChain()
|
|
||||||
|
|
||||||
watch_thread = Thread(target=watch)
|
|
||||||
watch_thread.daemon = True
|
|
||||||
watch_thread.start()
|
|
||||||
|
|
||||||
if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':
|
|
||||||
try:
|
|
||||||
snat_ip = os.getenv('SNAT_TO_SOURCE')
|
|
||||||
snat_ipo = ipaddress.ip_address(snat_ip)
|
|
||||||
if type(snat_ipo) is ipaddress.IPv4Address:
|
|
||||||
snat4_thread = Thread(target=snat4,args=(snat_ip,))
|
|
||||||
snat4_thread.daemon = True
|
|
||||||
snat4_thread.start()
|
|
||||||
except ValueError:
|
|
||||||
print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
|
|
||||||
|
|
||||||
if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':
|
|
||||||
try:
|
|
||||||
snat_ip = os.getenv('SNAT6_TO_SOURCE')
|
|
||||||
snat_ipo = ipaddress.ip_address(snat_ip)
|
|
||||||
if type(snat_ipo) is ipaddress.IPv6Address:
|
|
||||||
snat6_thread = Thread(target=snat6,args=(snat_ip,))
|
|
||||||
snat6_thread.daemon = True
|
|
||||||
snat6_thread.start()
|
|
||||||
except ValueError:
|
|
||||||
print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')
|
|
||||||
|
|
||||||
autopurge_thread = Thread(target=autopurge)
|
|
||||||
autopurge_thread.daemon = True
|
|
||||||
autopurge_thread.start()
|
|
||||||
|
|
||||||
mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
|
|
||||||
mailcowchainwatch_thread.daemon = True
|
|
||||||
mailcowchainwatch_thread.start()
|
|
||||||
|
|
||||||
blacklistupdate_thread = Thread(target=blacklistUpdate)
|
|
||||||
blacklistupdate_thread.daemon = True
|
|
||||||
blacklistupdate_thread.start()
|
|
||||||
|
|
||||||
whitelistupdate_thread = Thread(target=whitelistUpdate)
|
|
||||||
whitelistupdate_thread.daemon = True
|
|
||||||
whitelistupdate_thread.start()
|
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM, quit)
|
|
||||||
atexit.register(clear)
|
|
||||||
|
|
||||||
while not quit_now:
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
FROM alpine:3.17
|
FROM alpine:3.19
|
||||||
LABEL maintainer "Andre Peters <andre.peters@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 \
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
FROM php:8.1-fpm-alpine3.17
|
FROM php:8.2-fpm-alpine3.19
|
||||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
|
||||||
|
|
||||||
# renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced
|
# 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.23
|
||||||
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced
|
# 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
|
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
|
||||||
ARG MAILPARSE_PECL_VERSION=3.1.4
|
ARG MAILPARSE_PECL_VERSION=3.1.6
|
||||||
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced
|
# 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
|
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||||
ARG REDIS_PECL_VERSION=5.3.7
|
ARG REDIS_PECL_VERSION=6.0.2
|
||||||
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced
|
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
|
||||||
ARG COMPOSER_VERSION=2.5.1
|
ARG COMPOSER_VERSION=2.6.6
|
||||||
|
|
||||||
RUN apk add -U --no-cache autoconf \
|
RUN apk add -U --no-cache autoconf \
|
||||||
aspell-dev \
|
aspell-dev \
|
||||||
@@ -52,6 +52,7 @@ RUN apk add -U --no-cache autoconf \
|
|||||||
libxpm-dev \
|
libxpm-dev \
|
||||||
libzip \
|
libzip \
|
||||||
libzip-dev \
|
libzip-dev \
|
||||||
|
linux-headers \
|
||||||
make \
|
make \
|
||||||
mysql-client \
|
mysql-client \
|
||||||
openldap-dev \
|
openldap-dev \
|
||||||
@@ -75,7 +76,7 @@ RUN apk add -U --no-cache autoconf \
|
|||||||
--with-webp \
|
--with-webp \
|
||||||
--with-xpm \
|
--with-xpm \
|
||||||
--with-avif \
|
--with-avif \
|
||||||
&& docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql pspell soap sockets zip bcmath gmp \
|
&& docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql pspell soap sockets sysvsem zip bcmath gmp \
|
||||||
&& docker-php-ext-configure imap --with-imap --with-imap-ssl \
|
&& docker-php-ext-configure imap --with-imap --with-imap-ssl \
|
||||||
&& docker-php-ext-install -j 4 imap \
|
&& docker-php-ext-install -j 4 imap \
|
||||||
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \
|
&& curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \
|
||||||
@@ -99,6 +100,7 @@ RUN apk add -U --no-cache autoconf \
|
|||||||
libxml2-dev \
|
libxml2-dev \
|
||||||
libxpm-dev \
|
libxpm-dev \
|
||||||
libzip-dev \
|
libzip-dev \
|
||||||
|
linux-headers \
|
||||||
make \
|
make \
|
||||||
openldap-dev \
|
openldap-dev \
|
||||||
pcre-dev \
|
pcre-dev \
|
||||||
@@ -108,4 +110,4 @@ COPY ./docker-entrypoint.sh /
|
|||||||
|
|
||||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
|
|
||||||
CMD ["php-fpm"]
|
CMD ["php-fpm"]
|
||||||
|
|||||||
@@ -172,6 +172,24 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
//
|
//
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
DROP EVENT IF EXISTS clean_sasl_log;
|
||||||
|
DELIMITER //
|
||||||
|
CREATE EVENT clean_sasl_log
|
||||||
|
ON SCHEDULE EVERY 1 DAY DO
|
||||||
|
BEGIN
|
||||||
|
DELETE sasl_log.* FROM sasl_log
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT username, service, MAX(datetime) AS lastdate
|
||||||
|
FROM sasl_log
|
||||||
|
GROUP BY username, service
|
||||||
|
) AS last ON sasl_log.username = last.username AND sasl_log.service = last.service
|
||||||
|
WHERE datetime < DATE_SUB(NOW(), INTERVAL 31 DAY) AND datetime < lastdate;
|
||||||
|
DELETE FROM sasl_log
|
||||||
|
WHERE username NOT IN (SELECT username FROM mailbox) AND
|
||||||
|
datetime < DATE_SUB(NOW(), INTERVAL 31 DAY);
|
||||||
|
END;
|
||||||
|
//
|
||||||
|
DELIMITER ;
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
FROM debian:bullseye-slim
|
FROM debian:bullseye-slim
|
||||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ENV LC_ALL C
|
ENV LC_ALL C
|
||||||
@@ -17,10 +17,10 @@ RUN groupadd -g 102 postfix \
|
|||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
dirmngr \
|
dirmngr \
|
||||||
dnsutils \
|
dnsutils \
|
||||||
gnupg \
|
gnupg \
|
||||||
libsasl2-modules \
|
libsasl2-modules \
|
||||||
mariadb-client \
|
mariadb-client \
|
||||||
perl \
|
perl \
|
||||||
postfix \
|
postfix \
|
||||||
postfix-mysql \
|
postfix-mysql \
|
||||||
@@ -32,7 +32,7 @@ RUN groupadd -g 102 postfix \
|
|||||||
syslog-ng \
|
syslog-ng \
|
||||||
syslog-ng-core \
|
syslog-ng-core \
|
||||||
syslog-ng-mod-redis \
|
syslog-ng-mod-redis \
|
||||||
tzdata \
|
tzdata \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& touch /etc/default/locale \
|
&& touch /etc/default/locale \
|
||||||
&& printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \
|
&& printf '#!/bin/bash\n/usr/sbin/postconf -c /opt/postfix/conf "$@"' > /usr/local/sbin/postconf \
|
||||||
|
|||||||
@@ -393,12 +393,101 @@ query = SELECT goto FROM spamalias
|
|||||||
AND validity >= UNIX_TIMESTAMP()
|
AND validity >= UNIX_TIMESTAMP()
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
sed -i '/User overrides/q' /opt/postfix/conf/main.cf
|
if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then
|
||||||
echo >> /opt/postfix/conf/main.cf
|
cat <<EOF > /opt/postfix/conf/dns_blocklists.cf
|
||||||
touch /opt/postfix/conf/extra.cf
|
# This file can be edited.
|
||||||
sed -i '/myhostname/d' /opt/postfix/conf/extra.cf
|
# Delete this file and restart postfix container to revert any changes.
|
||||||
echo -e "myhostname = ${MAILCOW_HOSTNAME}\n$(cat /opt/postfix/conf/extra.cf)" > /opt/postfix/conf/extra.cf
|
postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
|
||||||
|
hostkarma.junkemailfilter.com=127.0.0.1*-2
|
||||||
|
list.dnswl.org=127.0.[0..255].0*-2
|
||||||
|
list.dnswl.org=127.0.[0..255].1*-4
|
||||||
|
list.dnswl.org=127.0.[0..255].2*-6
|
||||||
|
list.dnswl.org=127.0.[0..255].3*-8
|
||||||
|
ix.dnsbl.manitu.net*2
|
||||||
|
bl.spamcop.net*2
|
||||||
|
bl.suomispam.net*2
|
||||||
|
hostkarma.junkemailfilter.com=127.0.0.2*3
|
||||||
|
hostkarma.junkemailfilter.com=127.0.0.4*2
|
||||||
|
hostkarma.junkemailfilter.com=127.0.1.2*1
|
||||||
|
backscatter.spameatingmonkey.net*2
|
||||||
|
bl.ipv6.spameatingmonkey.net*2
|
||||||
|
bl.spameatingmonkey.net*2
|
||||||
|
b.barracudacentral.org=127.0.0.2*7
|
||||||
|
bl.mailspike.net=127.0.0.2*5
|
||||||
|
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
|
||||||
|
fi
|
||||||
|
DNSBL_CONFIG=$(grep -v '^#' /opt/postfix/conf/dns_blocklists.cf | grep '\S')
|
||||||
|
|
||||||
|
if [ ! -z "$DNSBL_CONFIG" ]; then
|
||||||
|
echo -e "\e[33mChecking if ASN for your IP is listed for Spamhaus Bad ASN List...\e[0m"
|
||||||
|
if [ -n "$SPAMHAUS_DQS_KEY" ]; then
|
||||||
|
echo -e "\e[32mDetected SPAMHAUS_DQS_KEY variable from mailcow.conf...\e[0m"
|
||||||
|
echo -e "\e[33mUsing DQS Blocklists from Spamhaus!\e[0m"
|
||||||
|
SPAMHAUS_DNSBL_CONFIG=$(cat <<EOF
|
||||||
|
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[4..7]*6
|
||||||
|
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.[10;11]*8
|
||||||
|
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.3*4
|
||||||
|
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net=127.0.0.2*3
|
||||||
|
postscreen_dnsbl_reply_map = texthash:/opt/postfix/conf/dnsbl_reply.map
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat <<EOF > /opt/postfix/conf/dnsbl_reply.map
|
||||||
|
# Autogenerated by mailcow, using Spamhaus DQS reply domains
|
||||||
|
${SPAMHAUS_DQS_KEY}.sbl.dq.spamhaus.net sbl.spamhaus.org
|
||||||
|
${SPAMHAUS_DQS_KEY}.xbl.dq.spamhaus.net xbl.spamhaus.org
|
||||||
|
${SPAMHAUS_DQS_KEY}.pbl.dq.spamhaus.net pbl.spamhaus.org
|
||||||
|
${SPAMHAUS_DQS_KEY}.zen.dq.spamhaus.net zen.spamhaus.org
|
||||||
|
${SPAMHAUS_DQS_KEY}.dbl.dq.spamhaus.net dbl.spamhaus.org
|
||||||
|
${SPAMHAUS_DQS_KEY}.zrd.dq.spamhaus.net zrd.spamhaus.org
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
else
|
||||||
|
if [ -f "/opt/postfix/conf/dnsbl_reply.map" ]; then
|
||||||
|
rm /opt/postfix/conf/dnsbl_reply.map
|
||||||
|
fi
|
||||||
|
response=$(curl --connect-timeout 15 --max-time 30 -s -o /dev/null -w "%{http_code}" "https://asn-check.mailcow.email")
|
||||||
|
if [ "$response" -eq 503 ]; then
|
||||||
|
echo -e "\e[31mThe AS of your IP is listed as a banned AS from Spamhaus!\e[0m"
|
||||||
|
echo -e "\e[33mNo SPAMHAUS_DQS_KEY found... Skipping Spamhaus blocklists entirely!\e[0m"
|
||||||
|
SPAMHAUS_DNSBL_CONFIG=""
|
||||||
|
elif [ "$response" -eq 200 ]; then
|
||||||
|
echo -e "\e[32mThe AS of your IP is NOT listed as a banned AS from Spamhaus!\e[0m"
|
||||||
|
echo -e "\e[33mUsing the open Spamhaus blocklists.\e[0m"
|
||||||
|
SPAMHAUS_DNSBL_CONFIG=$(cat <<EOF
|
||||||
|
zen.spamhaus.org=127.0.0.[10;11]*8
|
||||||
|
zen.spamhaus.org=127.0.0.[4..7]*6
|
||||||
|
zen.spamhaus.org=127.0.0.3*4
|
||||||
|
zen.spamhaus.org=127.0.0.2*3
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
else
|
||||||
|
echo -e "\e[31mWe couldn't determine your AS... (maybe DNS/Network issue?) Response Code: $response\e[0m"
|
||||||
|
echo -e "\e[33mDeactivating Spamhaus DNS Blocklists to be on the safe site!\e[0m"
|
||||||
|
SPAMHAUS_DNSBL_CONFIG=""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reset main.cf
|
||||||
|
sed -i '/Overrides/q' /opt/postfix/conf/main.cf
|
||||||
|
echo >> /opt/postfix/conf/main.cf
|
||||||
|
# Append postscreen dnsbl sites to main.cf
|
||||||
|
if [ ! -z "$DNSBL_CONFIG" ]; then
|
||||||
|
echo -e "${DNSBL_CONFIG}\n${SPAMHAUS_DNSBL_CONFIG}" >> /opt/postfix/conf/main.cf
|
||||||
|
fi
|
||||||
|
# Append user overrides
|
||||||
|
echo -e "\n# User Overrides" >> /opt/postfix/conf/main.cf
|
||||||
|
touch /opt/postfix/conf/extra.cf
|
||||||
|
sed -i '/\$myhostname/! { /myhostname/d }' /opt/postfix/conf/extra.cf
|
||||||
|
echo -e "myhostname = ${MAILCOW_HOSTNAME}\n$(cat /opt/postfix/conf/extra.cf)" > /opt/postfix/conf/extra.cf
|
||||||
cat /opt/postfix/conf/extra.cf >> /opt/postfix/conf/main.cf
|
cat /opt/postfix/conf/extra.cf >> /opt/postfix/conf/main.cf
|
||||||
|
|
||||||
if [ ! -f /opt/postfix/conf/custom_transport.pcre ]; then
|
if [ ! -f /opt/postfix/conf/custom_transport.pcre ]; then
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
FROM debian:bullseye-slim
|
FROM debian:bullseye-slim
|
||||||
LABEL maintainer "Andre Peters <andre.peters@tinc.gmbh>"
|
LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ARG CODENAME=bullseye
|
ARG CODENAME=bullseye
|
||||||
@@ -13,7 +13,7 @@ RUN apt-get update && apt-get install -y \
|
|||||||
dnsutils \
|
dnsutils \
|
||||||
netcat \
|
netcat \
|
||||||
&& apt-key adv --fetch-keys https://rspamd.com/apt-stable/gpg.key \
|
&& apt-key adv --fetch-keys https://rspamd.com/apt-stable/gpg.key \
|
||||||
&& echo "deb [arch=amd64] https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \
|
&& echo "deb https://rspamd.com/apt-stable/ $CODENAME main" > /etc/apt/sources.list.d/rspamd.list \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get --no-install-recommends -y install rspamd redis-tools procps nano \
|
&& apt-get --no-install-recommends -y install rspamd redis-tools procps nano \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ EOF
|
|||||||
redis-cli -h redis-mailcow SLAVEOF NO ONE
|
redis-cli -h redis-mailcow SLAVEOF NO ONE
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Provide additional lua modules
|
||||||
|
ln -s /usr/lib/$(uname -m)-linux-gnu/liblua5.1-cjson.so.0.0.0 /usr/lib/rspamd/cjson.so
|
||||||
|
|
||||||
chown -R _rspamd:_rspamd /var/lib/rspamd \
|
chown -R _rspamd:_rspamd /var/lib/rspamd \
|
||||||
/etc/rspamd/local.d \
|
/etc/rspamd/local.d \
|
||||||
/etc/rspamd/override.d \
|
/etc/rspamd/override.d \
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
FROM debian:bullseye-slim
|
FROM debian:bullseye-slim
|
||||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
ARG SOGO_DEBIAN_REPOSITORY=http://packages.sogo.nu/nightly/5/debian/
|
ARG SOGO_DEBIAN_REPOSITORY=http://www.axis.cz/linux/debian
|
||||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
|
# 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
|
||||||
@@ -32,7 +32,7 @@ RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \
|
|||||||
&& 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 \
|
&& apt-key adv --keyserver keys.openpgp.org --recv-key 74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 \
|
||||||
&& echo "deb ${SOGO_DEBIAN_REPOSITORY} bullseye bullseye" > /etc/apt/sources.list.d/sogo.list \
|
&& echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} bullseye 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 \
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ FROM solr:7.7-slim
|
|||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced
|
# 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 /
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
FROM alpine:3.17
|
FROM alpine:3.19
|
||||||
|
|
||||||
LABEL maintainer "Andre Peters <andre.peters@servercow.de>"
|
LABEL maintainer "The Infrastructure Company GmbH GmbH <info@servercow.de>"
|
||||||
|
|
||||||
RUN apk add --update --no-cache \
|
RUN apk add --update --no-cache \
|
||||||
curl \
|
curl \
|
||||||
|
bind-tools \
|
||||||
|
netcat-openbsd \
|
||||||
unbound \
|
unbound \
|
||||||
bash \
|
bash \
|
||||||
openssl \
|
openssl \
|
||||||
@@ -18,6 +20,11 @@ EXPOSE 53/udp 53/tcp
|
|||||||
|
|
||||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||||
|
|
||||||
|
# healthcheck (nslookup)
|
||||||
|
COPY healthcheck.sh /healthcheck.sh
|
||||||
|
RUN chmod +x /healthcheck.sh
|
||||||
|
HEALTHCHECK --interval=5s --timeout=30s CMD [ "/healthcheck.sh" ]
|
||||||
|
|
||||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||||
|
|
||||||
CMD ["/usr/sbin/unbound"]
|
CMD ["/usr/sbin/unbound"]
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Declare log function for logfile inside container
|
||||||
|
function log_to_file() {
|
||||||
|
echo "$(date +"%Y-%m-%d %H:%M:%S"): $1" > /var/log/healthcheck.log
|
||||||
|
}
|
||||||
|
|
||||||
|
# General Ping function to check general pingability
|
||||||
|
function check_ping() {
|
||||||
|
declare -a ipstoping=("1.1.1.1" "8.8.8.8" "9.9.9.9")
|
||||||
|
|
||||||
|
for ip in "${ipstoping[@]}" ; do
|
||||||
|
ping -q -c 3 -w 5 "$ip"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
log_to_file "Healthcheck: Couldn't ping $ip for 5 seconds... Gave up!"
|
||||||
|
log_to_file "Please check your internet connection or firewall rules to fix this error, because a simple ping test should always go through from the unbound container!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
log_to_file "Healthcheck: Ping Checks WORKING properly!"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# General DNS Resolve Check against Unbound Resolver himself
|
||||||
|
function check_dns() {
|
||||||
|
declare -a domains=("mailcow.email" "github.com" "hub.docker.com")
|
||||||
|
|
||||||
|
for domain in "${domains[@]}" ; do
|
||||||
|
for ((i=1; i<=3; i++)); do
|
||||||
|
dig +short +timeout=2 +tries=1 "$domain" @127.0.0.1 > /dev/null
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
log_to_file "Healthcheck: DNS Resolution Failed on $i attempt! Trying again..."
|
||||||
|
if [ $i -eq 3 ]; then
|
||||||
|
log_to_file "Healthcheck: DNS Resolution not possible after $i attempts... Gave up!"
|
||||||
|
log_to_file "Maybe check your outbound firewall, as it needs to resolve DNS over TCP AND UDP!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
log_to_file "Healthcheck: DNS Resolver WORKING properly!"
|
||||||
|
return 0
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# Simple Netcat Check to connect to common webports
|
||||||
|
function check_netcat() {
|
||||||
|
declare -a domains=("mailcow.email" "github.com" "hub.docker.com")
|
||||||
|
declare -a ports=("80" "443")
|
||||||
|
|
||||||
|
for domain in "${domains[@]}" ; do
|
||||||
|
for port in "${ports[@]}" ; do
|
||||||
|
nc -z -w 2 $domain $port
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
log_to_file "Healthcheck: Could not reach $domain on Port $port... Gave up!"
|
||||||
|
log_to_file "Please check your internet connection or firewall rules to fix this error."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
log_to_file "Healthcheck: Netcat Checks WORKING properly!"
|
||||||
|
return 0
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
check_dns
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
check_netcat
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_to_file "Healthcheck: ALL CHECKS WERE SUCCESSFUL! Unbound is healthy!"
|
||||||
|
exit 0
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
FROM alpine:3.17
|
FROM alpine:3.19
|
||||||
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 \
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ fi
|
|||||||
|
|
||||||
if [[ "${WATCHDOG_VERBOSE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
if [[ "${WATCHDOG_VERBOSE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
SMTP_VERBOSE="--verbose"
|
SMTP_VERBOSE="--verbose"
|
||||||
|
CURL_VERBOSE="--verbose"
|
||||||
set -xv
|
set -xv
|
||||||
else
|
else
|
||||||
SMTP_VERBOSE=""
|
SMTP_VERBOSE=""
|
||||||
|
CURL_VERBOSE=""
|
||||||
exec 2>/dev/null
|
exec 2>/dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -97,7 +99,9 @@ log_msg() {
|
|||||||
echo $(date) $(printf '%s\n' "${1}")
|
echo $(date) $(printf '%s\n' "${1}")
|
||||||
}
|
}
|
||||||
|
|
||||||
function mail_error() {
|
function notify_error() {
|
||||||
|
# Check if one of the notification options is enabled
|
||||||
|
[[ -z ${WATCHDOG_NOTIFY_EMAIL} ]] && [[ -z ${WATCHDOG_NOTIFY_WEBHOOK} ]] && return 0
|
||||||
THROTTLE=
|
THROTTLE=
|
||||||
[[ -z ${1} ]] && return 1
|
[[ -z ${1} ]] && return 1
|
||||||
# If exists, body will be the content of "/tmp/${1}", even if ${2} is set
|
# If exists, body will be the content of "/tmp/${1}", even if ${2} is set
|
||||||
@@ -122,37 +126,57 @@ function mail_error() {
|
|||||||
else
|
else
|
||||||
SUBJECT="${WATCHDOG_SUBJECT}: ${1}"
|
SUBJECT="${WATCHDOG_SUBJECT}: ${1}"
|
||||||
fi
|
fi
|
||||||
IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}"
|
|
||||||
for rcpt in "${MAIL_RCPTS[@]}"; do
|
# Send mail notification if enabled
|
||||||
RCPT_DOMAIN=
|
if [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]]; then
|
||||||
RCPT_MX=
|
IFS=',' read -r -a MAIL_RCPTS <<< "${WATCHDOG_NOTIFY_EMAIL}"
|
||||||
RCPT_DOMAIN=$(echo ${rcpt} | awk -F @ {'print $NF'})
|
for rcpt in "${MAIL_RCPTS[@]}"; do
|
||||||
CHECK_FOR_VALID_MX=$(dig +short ${RCPT_DOMAIN} mx)
|
RCPT_DOMAIN=
|
||||||
if [[ -z ${CHECK_FOR_VALID_MX} ]]; then
|
RCPT_MX=
|
||||||
log_msg "Cannot determine MX for ${rcpt}, skipping email notification..."
|
RCPT_DOMAIN=$(echo ${rcpt} | awk -F @ {'print $NF'})
|
||||||
|
CHECK_FOR_VALID_MX=$(dig +short ${RCPT_DOMAIN} mx)
|
||||||
|
if [[ -z ${CHECK_FOR_VALID_MX} ]]; then
|
||||||
|
log_msg "Cannot determine MX for ${rcpt}, skipping email notification..."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
[ -f "/tmp/${1}" ] && BODY="/tmp/${1}"
|
||||||
|
timeout 10s ./smtp-cli --missing-modules-ok \
|
||||||
|
"${SMTP_VERBOSE}" \
|
||||||
|
--charset=UTF-8 \
|
||||||
|
--subject="${SUBJECT}" \
|
||||||
|
--body-plain="${BODY}" \
|
||||||
|
--add-header="X-Priority: 1" \
|
||||||
|
--to=${rcpt} \
|
||||||
|
--from="watchdog@${MAILCOW_HOSTNAME}" \
|
||||||
|
--hello-host=${MAILCOW_HOSTNAME} \
|
||||||
|
--ipv4
|
||||||
|
if [[ $? -eq 1 ]]; then # exit code 1 is fine
|
||||||
|
log_msg "Sent notification email to ${rcpt}"
|
||||||
|
else
|
||||||
|
if [[ "${SMTP_VERBOSE}" == "" ]]; then
|
||||||
|
log_msg "Error while sending notification email to ${rcpt}. You can enable verbose logging by setting 'WATCHDOG_VERBOSE=y' in mailcow.conf."
|
||||||
|
else
|
||||||
|
log_msg "Error while sending notification email to ${rcpt}."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Send webhook notification if enabled
|
||||||
|
if [[ ! -z ${WATCHDOG_NOTIFY_WEBHOOK} ]]; then
|
||||||
|
if [[ -z ${WATCHDOG_NOTIFY_WEBHOOK_BODY} ]]; then
|
||||||
|
log_msg "No webhook body set, skipping webhook notification..."
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
[ -f "/tmp/${1}" ] && BODY="/tmp/${1}"
|
|
||||||
timeout 10s ./smtp-cli --missing-modules-ok \
|
# Replace subject and body placeholders
|
||||||
"${SMTP_VERBOSE}" \
|
WEBHOOK_BODY=$(echo ${WATCHDOG_NOTIFY_WEBHOOK_BODY} | sed "s|\$SUBJECT\|\${SUBJECT}|$SUBJECT|g" | sed "s|\$BODY\|\${BODY}|$BODY|")
|
||||||
--charset=UTF-8 \
|
|
||||||
--subject="${SUBJECT}" \
|
# POST to webhook
|
||||||
--body-plain="${BODY}" \
|
curl -X POST -H "Content-Type: application/json" ${CURL_VERBOSE} -d "${WEBHOOK_BODY}" ${WATCHDOG_NOTIFY_WEBHOOK}
|
||||||
--add-header="X-Priority: 1" \
|
|
||||||
--to=${rcpt} \
|
log_msg "Sent notification using webhook"
|
||||||
--from="watchdog@${MAILCOW_HOSTNAME}" \
|
fi
|
||||||
--hello-host=${MAILCOW_HOSTNAME} \
|
|
||||||
--ipv4
|
|
||||||
if [[ $? -eq 1 ]]; then # exit code 1 is fine
|
|
||||||
log_msg "Sent notification email to ${rcpt}"
|
|
||||||
else
|
|
||||||
if [[ "${SMTP_VERBOSE}" == "" ]]; then
|
|
||||||
log_msg "Error while sending notification email to ${rcpt}. You can enable verbose logging by setting 'WATCHDOG_VERBOSE=y' in mailcow.conf."
|
|
||||||
else
|
|
||||||
log_msg "Error while sending notification email to ${rcpt}."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get_container_ip() {
|
get_container_ip() {
|
||||||
@@ -197,7 +221,7 @@ get_container_ip() {
|
|||||||
# One-time check
|
# One-time check
|
||||||
if grep -qi "$(echo ${IPV6_NETWORK} | cut -d: -f1-3)" <<< "$(ip a s)"; then
|
if grep -qi "$(echo ${IPV6_NETWORK} | cut -d: -f1-3)" <<< "$(ip a s)"; then
|
||||||
if [[ -z "$(get_ipv6)" ]]; then
|
if [[ -z "$(get_ipv6)" ]]; then
|
||||||
mail_error "ipv6-config" "enable_ipv6 is true in docker-compose.yml, but an IPv6 link could not be established. Please verify your IPv6 connection."
|
notify_error "ipv6-config" "enable_ipv6 is true in docker-compose.yml, but an IPv6 link could not be established. Please verify your IPv6 connection."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -692,8 +716,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/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
|
||||||
@@ -746,8 +770,8 @@ olefy_checks() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Notify about start
|
# Notify about start
|
||||||
if [[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]]; then
|
if [[ ${WATCHDOG_NOTIFY_START} =~ ^([yY][eE][sS]|[yY])+$ ]]; then
|
||||||
mail_error "watchdog-mailcow" "Watchdog started monitoring mailcow."
|
notify_error "watchdog-mailcow" "Watchdog started monitoring mailcow."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create watchdog agents
|
# Create watchdog agents
|
||||||
@@ -1029,33 +1053,33 @@ while true; do
|
|||||||
fi
|
fi
|
||||||
if [[ ${com_pipe_answer} == "ratelimit" ]]; then
|
if [[ ${com_pipe_answer} == "ratelimit" ]]; then
|
||||||
log_msg "At least one ratelimit was applied"
|
log_msg "At least one ratelimit was applied"
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}"
|
notify_error "${com_pipe_answer}"
|
||||||
elif [[ ${com_pipe_answer} == "mail_queue_status" ]]; then
|
elif [[ ${com_pipe_answer} == "mail_queue_status" ]]; then
|
||||||
log_msg "Mail queue status is critical"
|
log_msg "Mail queue status is critical"
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}"
|
notify_error "${com_pipe_answer}"
|
||||||
elif [[ ${com_pipe_answer} == "external_checks" ]]; then
|
elif [[ ${com_pipe_answer} == "external_checks" ]]; then
|
||||||
log_msg "Your mailcow is an open relay!"
|
log_msg "Your mailcow is an open relay!"
|
||||||
# Define $2 to override message text, else print service was restarted at ...
|
# Define $2 to override message text, else print service was restarted at ...
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please stop mailcow now and check your network configuration!"
|
notify_error "${com_pipe_answer}" "Please stop mailcow now and check your network configuration!"
|
||||||
elif [[ ${com_pipe_answer} == "mysql_repl_checks" ]]; then
|
elif [[ ${com_pipe_answer} == "mysql_repl_checks" ]]; then
|
||||||
log_msg "MySQL replication is not working properly"
|
log_msg "MySQL replication is not working properly"
|
||||||
# Define $2 to override message text, else print service was restarted at ...
|
# Define $2 to override message text, else print service was restarted at ...
|
||||||
# Once mail per 10 minutes
|
# Once mail per 10 minutes
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check the SQL replication status" 600
|
notify_error "${com_pipe_answer}" "Please check the SQL replication status" 600
|
||||||
elif [[ ${com_pipe_answer} == "dovecot_repl_checks" ]]; then
|
elif [[ ${com_pipe_answer} == "dovecot_repl_checks" ]]; then
|
||||||
log_msg "Dovecot replication is not working properly"
|
log_msg "Dovecot replication is not working properly"
|
||||||
# Define $2 to override message text, else print service was restarted at ...
|
# Define $2 to override message text, else print service was restarted at ...
|
||||||
# Once mail per 10 minutes
|
# Once mail per 10 minutes
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check the Dovecot replicator status" 600
|
notify_error "${com_pipe_answer}" "Please check the Dovecot replicator status" 600
|
||||||
elif [[ ${com_pipe_answer} == "certcheck" ]]; then
|
elif [[ ${com_pipe_answer} == "certcheck" ]]; then
|
||||||
log_msg "Certificates are about to expire"
|
log_msg "Certificates are about to expire"
|
||||||
# Define $2 to override message text, else print service was restarted at ...
|
# Define $2 to override message text, else print service was restarted at ...
|
||||||
# Only mail once a day
|
# Only mail once a day
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please renew your certificate" 86400
|
notify_error "${com_pipe_answer}" "Please renew your certificate" 86400
|
||||||
elif [[ ${com_pipe_answer} == "acme-mailcow" ]]; then
|
elif [[ ${com_pipe_answer} == "acme-mailcow" ]]; then
|
||||||
log_msg "acme-mailcow did not complete successfully"
|
log_msg "acme-mailcow did not complete successfully"
|
||||||
# Define $2 to override message text, else print service was restarted at ...
|
# Define $2 to override message text, else print service was restarted at ...
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_error "${com_pipe_answer}" "Please check acme-mailcow for further information."
|
notify_error "${com_pipe_answer}" "Please check acme-mailcow for further information."
|
||||||
elif [[ ${com_pipe_answer} == "fail2ban" ]]; then
|
elif [[ ${com_pipe_answer} == "fail2ban" ]]; then
|
||||||
F2B_RES=($(timeout 4s ${REDIS_CMDLINE} --raw GET F2B_RES 2> /dev/null))
|
F2B_RES=($(timeout 4s ${REDIS_CMDLINE} --raw GET F2B_RES 2> /dev/null))
|
||||||
if [[ ! -z "${F2B_RES}" ]]; then
|
if [[ ! -z "${F2B_RES}" ]]; then
|
||||||
@@ -1065,7 +1089,7 @@ while true; do
|
|||||||
log_msg "Banned ${host}"
|
log_msg "Banned ${host}"
|
||||||
rm /tmp/fail2ban 2> /dev/null
|
rm /tmp/fail2ban 2> /dev/null
|
||||||
timeout 2s whois "${host}" > /tmp/fail2ban
|
timeout 2s whois "${host}" > /tmp/fail2ban
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && [[ ${WATCHDOG_NOTIFY_BAN} =~ ^([yY][eE][sS]|[yY])+$ ]] && mail_error "${com_pipe_answer}" "IP ban: ${host}"
|
[[ ${WATCHDOG_NOTIFY_BAN} =~ ^([yY][eE][sS]|[yY])+$ ]] && notify_error "${com_pipe_answer}" "IP ban: ${host}"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
|
elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then
|
||||||
@@ -1085,7 +1109,7 @@ while true; do
|
|||||||
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/containers/${CONTAINER_ID}/restart
|
||||||
[[ ! -z ${WATCHDOG_NOTIFY_EMAIL} ]] && mail_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
|
||||||
fi
|
fi
|
||||||
@@ -1095,3 +1119,4 @@ while true; do
|
|||||||
kill -USR1 ${BACKGROUND_TASKS[*]}
|
kill -USR1 ${BACKGROUND_TASKS[*]}
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ server {
|
|||||||
add_header X-Download-Options "noopen" always;
|
add_header X-Download-Options "noopen" always;
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
add_header X-Permitted-Cross-Domain-Policies "none" always;
|
add_header X-Permitted-Cross-Domain-Policies "none" always;
|
||||||
add_header X-Robots-Tag "none" always;
|
add_header X-Robots-Tag "noindex, nofollow" always;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
fastcgi_hide_header X-Powered-By;
|
fastcgi_hide_header X-Powered-By;
|
||||||
@@ -86,7 +86,7 @@ server {
|
|||||||
deny all;
|
deny all;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+)\.php(?:$|\/) {
|
location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|ocs-provider\/.+)\.php(?:$|\/) {
|
||||||
fastcgi_split_path_info ^(.+?\.php)(\/.*|)$;
|
fastcgi_split_path_info ^(.+?\.php)(\/.*|)$;
|
||||||
set $path_info $fastcgi_path_info;
|
set $path_info $fastcgi_path_info;
|
||||||
try_files $fastcgi_script_name =404;
|
try_files $fastcgi_script_name =404;
|
||||||
@@ -105,7 +105,7 @@ server {
|
|||||||
fastcgi_read_timeout 1200;
|
fastcgi_read_timeout 1200;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ ^\/(?:updater|oc[ms]-provider)(?:$|\/) {
|
location ~ ^\/(?:updater|ocs-provider)(?:$|\/) {
|
||||||
try_files $uri/ =404;
|
try_files $uri/ =404;
|
||||||
index index.php;
|
index index.php;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ mail_plugins = </etc/dovecot/mail_plugins
|
|||||||
mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
|
mail_attachment_fs = crypt:set_prefix=mail_crypt_global:posix:
|
||||||
mail_attachment_dir = /var/attachments
|
mail_attachment_dir = /var/attachments
|
||||||
mail_attachment_min_size = 128k
|
mail_attachment_min_size = 128k
|
||||||
|
# Significantly speeds up very large mailboxes, but is only safe to enable if
|
||||||
|
# you do not manually modify the files in the `cur` directories in
|
||||||
|
# mailcowdockerized_vmail-vol-1.
|
||||||
|
# https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-performance/
|
||||||
|
maildir_very_dirty_syncs = yes
|
||||||
|
|
||||||
# Dovecot 2.2
|
# Dovecot 2.2
|
||||||
#ssl_protocols = !SSLv3
|
#ssl_protocols = !SSLv3
|
||||||
|
|||||||
@@ -114,7 +114,7 @@
|
|||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
error_page 403 /_rspamderror.php;
|
error_page 401 /_rspamderror.php;
|
||||||
}
|
}
|
||||||
proxy_pass http://rspamd:11334/;
|
proxy_pass http://rspamd:11334/;
|
||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
proxy_cache_path /tmp levels=1:2 keys_zone=sogo:10m inactive=24h max_size=1g;
|
proxy_cache_path /tmp levels=1:2 keys_zone=sogo:10m inactive=24h max_size=1g;
|
||||||
server_names_hash_bucket_size 64;
|
server_names_hash_max_size 512;
|
||||||
|
server_names_hash_bucket_size 128;
|
||||||
|
|
||||||
map $http_x_forwarded_proto $client_req_scheme {
|
map $http_x_forwarded_proto $client_req_scheme {
|
||||||
default $scheme;
|
default $scheme;
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
listen ${HTTPS_PORT} ssl http2;
|
listen ${HTTPS_PORT} ssl;
|
||||||
listen [::]:${HTTPS_PORT} ssl http2;
|
listen [::]:${HTTPS_PORT} ssl;
|
||||||
|
http2 on;
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ if /^\s*Received: from.* \(.*rspamd-mailcow.*mailcow-network.*\).*\(Postcow\)/
|
|||||||
REPLACE Received: from rspamd (rspamd $3) by $4 (Postcow) with $5
|
REPLACE Received: from rspamd (rspamd $3) by $4 (Postcow) with $5
|
||||||
endif
|
endif
|
||||||
/^\s*X-Enigmail/ IGNORE
|
/^\s*X-Enigmail/ IGNORE
|
||||||
/^\s*X-Mailer/ IGNORE
|
# Not removing Mailer by default, might be signed
|
||||||
|
#/^\s*X-Mailer/ IGNORE
|
||||||
/^\s*X-Originating-IP/ IGNORE
|
/^\s*X-Originating-IP/ IGNORE
|
||||||
/^\s*X-Forward/ IGNORE
|
/^\s*X-Forward/ IGNORE
|
||||||
# Not removing UA by default, might be signed
|
# Not removing UA by default, might be signed
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
|
|||||||
smtpd_relay_restrictions = permit_mynetworks,
|
smtpd_relay_restrictions = permit_mynetworks,
|
||||||
permit_sasl_authenticated,
|
permit_sasl_authenticated,
|
||||||
defer_unauth_destination
|
defer_unauth_destination
|
||||||
|
smtpd_forbid_bare_newline = yes
|
||||||
# alias maps are auto-generated in postfix.sh on startup
|
# alias maps are auto-generated in postfix.sh on startup
|
||||||
alias_maps = hash:/etc/aliases
|
alias_maps = hash:/etc/aliases
|
||||||
alias_database = hash:/etc/aliases
|
alias_database = hash:/etc/aliases
|
||||||
@@ -40,34 +41,6 @@ postscreen_blacklist_action = drop
|
|||||||
postscreen_cache_cleanup_interval = 24h
|
postscreen_cache_cleanup_interval = 24h
|
||||||
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
|
postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache
|
||||||
postscreen_dnsbl_action = enforce
|
postscreen_dnsbl_action = enforce
|
||||||
postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2
|
|
||||||
hostkarma.junkemailfilter.com=127.0.0.1*-2
|
|
||||||
list.dnswl.org=127.0.[0..255].0*-2
|
|
||||||
list.dnswl.org=127.0.[0..255].1*-4
|
|
||||||
list.dnswl.org=127.0.[0..255].2*-6
|
|
||||||
list.dnswl.org=127.0.[0..255].3*-8
|
|
||||||
ix.dnsbl.manitu.net*2
|
|
||||||
bl.spamcop.net*2
|
|
||||||
bl.suomispam.net*2
|
|
||||||
hostkarma.junkemailfilter.com=127.0.0.2*3
|
|
||||||
hostkarma.junkemailfilter.com=127.0.0.4*2
|
|
||||||
hostkarma.junkemailfilter.com=127.0.1.2*1
|
|
||||||
backscatter.spameatingmonkey.net*2
|
|
||||||
bl.ipv6.spameatingmonkey.net*2
|
|
||||||
bl.spameatingmonkey.net*2
|
|
||||||
b.barracudacentral.org=127.0.0.2*7
|
|
||||||
bl.mailspike.net=127.0.0.2*5
|
|
||||||
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
|
|
||||||
zen.spamhaus.org=127.0.0.[10;11]*8
|
|
||||||
zen.spamhaus.org=127.0.0.[4..7]*6
|
|
||||||
zen.spamhaus.org=127.0.0.3*4
|
|
||||||
zen.spamhaus.org=127.0.0.2*3
|
|
||||||
postscreen_dnsbl_threshold = 6
|
postscreen_dnsbl_threshold = 6
|
||||||
postscreen_dnsbl_ttl = 5m
|
postscreen_dnsbl_ttl = 5m
|
||||||
postscreen_greet_action = enforce
|
postscreen_greet_action = enforce
|
||||||
@@ -112,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
|
||||||
@@ -188,7 +162,8 @@ 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
|
||||||
|
smtpd_discard_ehlo_keywords = chunking
|
||||||
compatibility_level = 2
|
compatibility_level = 2
|
||||||
smtputf8_enable = no
|
smtputf8_enable = no
|
||||||
# Define protocols for SMTPS and submission service
|
# Define protocols for SMTPS and submission service
|
||||||
@@ -197,4 +172,4 @@ smtps_smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
|
|||||||
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
|
||||||
|
|
||||||
# DO NOT EDIT ANYTHING BELOW #
|
# DO NOT EDIT ANYTHING BELOW #
|
||||||
# User overrides #
|
# Overrides #
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
# Whitelist generated by Postwhite v3.4 on Mon 21 Mar 2022 06:50:26 PM CET
|
# Whitelist generated by Postwhite v3.4 on Mon Jan 1 00:15:22 UTC 2024
|
||||||
# https://github.com/stevejenkins/postwhite/
|
# https://github.com/stevejenkins/postwhite/
|
||||||
# 1898 total rules
|
# 2052 total rules
|
||||||
2a00:1450:4000::/36 permit
|
2a00:1450:4000::/36 permit
|
||||||
2a01:111:f400::/48 permit
|
2a01:111:f400::/48 permit
|
||||||
2a01:111:f403::/48 permit
|
2a01:111:f403:8000::/50 permit
|
||||||
2a01:4180:4050:0400::/64 permit
|
2a01:111:f403::/49 permit
|
||||||
2a01:4180:4050:0800::/64 permit
|
2a01:111:f403:c000::/51 permit
|
||||||
2a01:4180:4051:0400::/64 permit
|
2a01:111:f403:f000::/52 permit
|
||||||
2a01:4180:4051:0800::/64 permit
|
|
||||||
2a02:a60:0:5::/64 permit
|
2a02:a60:0:5::/64 permit
|
||||||
2c0f:fb50:4000::/36 permit
|
2c0f:fb50:4000::/36 permit
|
||||||
|
2.207.151.53 permit
|
||||||
|
3.70.123.177 permit
|
||||||
|
3.93.157.0/24 permit
|
||||||
|
3.129.120.190 permit
|
||||||
|
3.137.16.58 permit
|
||||||
|
3.210.190.0/24 permit
|
||||||
8.20.114.31 permit
|
8.20.114.31 permit
|
||||||
8.25.194.0/23 permit
|
8.25.194.0/23 permit
|
||||||
8.25.196.0/23 permit
|
8.25.196.0/23 permit
|
||||||
@@ -19,41 +24,53 @@
|
|||||||
13.70.32.43 permit
|
13.70.32.43 permit
|
||||||
13.72.50.45 permit
|
13.72.50.45 permit
|
||||||
13.74.143.28 permit
|
13.74.143.28 permit
|
||||||
13.77.161.179 permit
|
|
||||||
13.78.233.182 permit
|
13.78.233.182 permit
|
||||||
13.92.31.129 permit
|
13.92.31.129 permit
|
||||||
13.110.208.0/21 permit
|
13.110.208.0/21 permit
|
||||||
|
13.110.209.0/24 permit
|
||||||
13.110.216.0/22 permit
|
13.110.216.0/22 permit
|
||||||
13.110.224.0/20 permit
|
13.110.224.0/20 permit
|
||||||
13.111.0.0/16 permit
|
13.111.0.0/16 permit
|
||||||
17.41.0.0/16 permit
|
15.200.21.50 permit
|
||||||
|
15.200.44.248 permit
|
||||||
|
15.200.201.185 permit
|
||||||
17.57.155.0/24 permit
|
17.57.155.0/24 permit
|
||||||
17.57.156.0/24 permit
|
17.57.156.0/24 permit
|
||||||
17.58.0.0/16 permit
|
17.58.0.0/16 permit
|
||||||
17.110.0.0/15 permit
|
18.156.89.250 permit
|
||||||
17.142.0.0/15 permit
|
18.157.243.190 permit
|
||||||
17.162.0.0/15 permit
|
|
||||||
17.164.0.0/16 permit
|
|
||||||
17.171.37.0/24 permit
|
|
||||||
17.172.0.0/16 permit
|
|
||||||
17.179.168.0/23 permit
|
|
||||||
18.194.95.56 permit
|
18.194.95.56 permit
|
||||||
18.198.96.88 permit
|
18.198.96.88 permit
|
||||||
20.47.149.138 permit
|
18.208.124.128/25 permit
|
||||||
20.48.0.0/12 permit
|
18.216.232.154 permit
|
||||||
|
18.234.1.244 permit
|
||||||
|
18.236.40.242 permit
|
||||||
|
20.51.6.32/30 permit
|
||||||
20.52.52.2 permit
|
20.52.52.2 permit
|
||||||
20.52.128.133 permit
|
20.52.128.133 permit
|
||||||
|
20.59.80.4/30 permit
|
||||||
20.63.210.192/28 permit
|
20.63.210.192/28 permit
|
||||||
20.64.0.0/10 permit
|
20.69.8.108/30 permit
|
||||||
|
20.70.246.20 permit
|
||||||
|
20.76.201.171 permit
|
||||||
|
20.83.222.104/30 permit
|
||||||
|
20.88.157.184/30 permit
|
||||||
20.94.180.64/28 permit
|
20.94.180.64/28 permit
|
||||||
|
20.97.34.220/30 permit
|
||||||
|
20.98.148.156/30 permit
|
||||||
|
20.98.194.68/30 permit
|
||||||
|
20.105.209.76/30 permit
|
||||||
|
20.107.239.64/30 permit
|
||||||
|
20.112.250.133 permit
|
||||||
|
20.118.139.208/30 permit
|
||||||
20.185.213.160/27 permit
|
20.185.213.160/27 permit
|
||||||
20.185.213.224/27 permit
|
20.185.213.224/27 permit
|
||||||
20.185.214.0/27 permit
|
20.185.214.0/27 permit
|
||||||
20.185.214.2 permit
|
20.185.214.2 permit
|
||||||
20.185.214.32/27 permit
|
20.185.214.32/27 permit
|
||||||
20.185.214.64/27 permit
|
20.185.214.64/27 permit
|
||||||
20.192.0.0/10 permit
|
20.231.239.246 permit
|
||||||
23.100.85.1 permit
|
20.236.44.162 permit
|
||||||
23.103.224.0/19 permit
|
23.103.224.0/19 permit
|
||||||
23.249.208.0/20 permit
|
23.249.208.0/20 permit
|
||||||
23.251.224.0/19 permit
|
23.251.224.0/19 permit
|
||||||
@@ -78,46 +95,34 @@
|
|||||||
27.123.206.56/29 permit
|
27.123.206.56/29 permit
|
||||||
27.123.206.76/30 permit
|
27.123.206.76/30 permit
|
||||||
27.123.206.80/28 permit
|
27.123.206.80/28 permit
|
||||||
34.194.25.167 permit
|
31.25.48.222 permit
|
||||||
34.194.144.120 permit
|
34.195.217.107 permit
|
||||||
|
34.202.239.6 permit
|
||||||
34.212.163.75 permit
|
34.212.163.75 permit
|
||||||
|
34.215.104.144 permit
|
||||||
34.225.212.172 permit
|
34.225.212.172 permit
|
||||||
34.247.168.44 permit
|
34.247.168.44 permit
|
||||||
|
35.161.32.253 permit
|
||||||
|
35.167.93.243 permit
|
||||||
35.176.132.251 permit
|
35.176.132.251 permit
|
||||||
35.190.247.0/24 permit
|
35.190.247.0/24 permit
|
||||||
35.191.0.0/16 permit
|
35.191.0.0/16 permit
|
||||||
37.188.97.188 permit
|
|
||||||
37.218.248.47 permit
|
37.218.248.47 permit
|
||||||
37.218.249.47 permit
|
37.218.249.47 permit
|
||||||
37.218.251.62 permit
|
37.218.251.62 permit
|
||||||
39.156.163.64/29 permit
|
39.156.163.64/29 permit
|
||||||
40.71.187.0/24 permit
|
40.71.187.0/24 permit
|
||||||
40.76.4.15 permit
|
|
||||||
40.77.102.222 permit
|
|
||||||
40.92.0.0/15 permit
|
40.92.0.0/15 permit
|
||||||
40.97.116.82 permit
|
40.92.0.0/16 permit
|
||||||
40.97.128.194 permit
|
|
||||||
40.97.148.226 permit
|
|
||||||
40.97.153.146 permit
|
|
||||||
40.97.156.114 permit
|
|
||||||
40.97.160.2 permit
|
|
||||||
40.97.161.50 permit
|
|
||||||
40.97.164.146 permit
|
|
||||||
40.107.0.0/16 permit
|
40.107.0.0/16 permit
|
||||||
40.112.65.63 permit
|
40.112.65.63 permit
|
||||||
40.112.72.205 permit
|
|
||||||
40.113.200.201 permit
|
|
||||||
40.117.80.0/24 permit
|
40.117.80.0/24 permit
|
||||||
40.121.71.46 permit
|
|
||||||
41.74.192.0/22 permit
|
|
||||||
41.74.196.0/22 permit
|
|
||||||
41.74.200.0/23 permit
|
|
||||||
41.74.204.0/23 permit
|
|
||||||
41.74.206.0/24 permit
|
|
||||||
42.159.163.81 permit
|
|
||||||
42.159.163.82 permit
|
|
||||||
42.159.163.83 permit
|
|
||||||
43.228.184.0/22 permit
|
43.228.184.0/22 permit
|
||||||
|
44.206.138.57 permit
|
||||||
|
44.209.42.157 permit
|
||||||
|
44.236.56.93 permit
|
||||||
|
44.238.220.251 permit
|
||||||
|
46.19.168.0/23 permit
|
||||||
46.226.48.0/21 permit
|
46.226.48.0/21 permit
|
||||||
46.228.36.37 permit
|
46.228.36.37 permit
|
||||||
46.228.36.38/31 permit
|
46.228.36.38/31 permit
|
||||||
@@ -167,6 +172,8 @@
|
|||||||
46.243.88.175 permit
|
46.243.88.175 permit
|
||||||
46.243.88.176 permit
|
46.243.88.176 permit
|
||||||
46.243.88.177 permit
|
46.243.88.177 permit
|
||||||
|
46.243.95.179 permit
|
||||||
|
46.243.95.180 permit
|
||||||
50.18.45.249 permit
|
50.18.45.249 permit
|
||||||
50.18.121.236 permit
|
50.18.121.236 permit
|
||||||
50.18.121.248 permit
|
50.18.121.248 permit
|
||||||
@@ -176,33 +183,38 @@
|
|||||||
50.18.125.237 permit
|
50.18.125.237 permit
|
||||||
50.18.126.162 permit
|
50.18.126.162 permit
|
||||||
50.31.32.0/19 permit
|
50.31.32.0/19 permit
|
||||||
50.31.156.96/27 permit
|
50.56.130.220 permit
|
||||||
50.31.205.0/24 permit
|
50.56.130.221 permit
|
||||||
51.4.71.62 permit
|
|
||||||
51.4.72.0/24 permit
|
|
||||||
51.4.80.0/27 permit
|
|
||||||
51.5.72.0/24 permit
|
|
||||||
51.5.80.0/27 permit
|
|
||||||
51.137.58.21 permit
|
51.137.58.21 permit
|
||||||
51.140.75.55 permit
|
51.140.75.55 permit
|
||||||
51.144.100.179 permit
|
51.144.100.179 permit
|
||||||
51.163.158.0/24 permit
|
|
||||||
51.163.159.21 permit
|
|
||||||
52.5.230.59 permit
|
52.5.230.59 permit
|
||||||
52.27.5.72 permit
|
52.27.5.72 permit
|
||||||
52.27.28.47 permit
|
52.27.28.47 permit
|
||||||
52.33.191.91 permit
|
52.28.63.81 permit
|
||||||
52.36.138.31 permit
|
52.36.138.31 permit
|
||||||
52.37.142.146 permit
|
52.37.142.146 permit
|
||||||
52.38.191.253 permit
|
52.58.216.183 permit
|
||||||
52.41.64.145 permit
|
52.59.143.3 permit
|
||||||
52.60.41.5 permit
|
52.60.41.5 permit
|
||||||
52.60.115.116 permit
|
52.60.115.116 permit
|
||||||
|
52.61.91.9 permit
|
||||||
|
52.71.0.205 permit
|
||||||
52.82.172.0/22 permit
|
52.82.172.0/22 permit
|
||||||
52.94.124.0/28 permit
|
52.94.124.0/28 permit
|
||||||
52.95.48.152/29 permit
|
52.95.48.152/29 permit
|
||||||
52.95.49.88/29 permit
|
52.95.49.88/29 permit
|
||||||
|
52.96.91.34 permit
|
||||||
|
52.96.111.82 permit
|
||||||
|
52.96.172.98 permit
|
||||||
|
52.96.214.50 permit
|
||||||
|
52.96.222.194 permit
|
||||||
|
52.96.222.226 permit
|
||||||
|
52.96.223.2 permit
|
||||||
|
52.96.228.130 permit
|
||||||
|
52.96.229.242 permit
|
||||||
52.100.0.0/14 permit
|
52.100.0.0/14 permit
|
||||||
|
52.103.0.0/17 permit
|
||||||
52.119.213.144/28 permit
|
52.119.213.144/28 permit
|
||||||
52.160.39.140 permit
|
52.160.39.140 permit
|
||||||
52.165.175.144 permit
|
52.165.175.144 permit
|
||||||
@@ -214,23 +226,29 @@
|
|||||||
52.222.73.83 permit
|
52.222.73.83 permit
|
||||||
52.222.73.120 permit
|
52.222.73.120 permit
|
||||||
52.222.75.85 permit
|
52.222.75.85 permit
|
||||||
|
52.222.89.228 permit
|
||||||
52.234.172.96/28 permit
|
52.234.172.96/28 permit
|
||||||
52.236.28.240/28 permit
|
52.236.28.240/28 permit
|
||||||
52.237.141.173 permit
|
|
||||||
52.244.206.214 permit
|
52.244.206.214 permit
|
||||||
52.247.53.144 permit
|
52.247.53.144 permit
|
||||||
52.250.107.196 permit
|
52.250.107.196 permit
|
||||||
52.250.126.174 permit
|
52.250.126.174 permit
|
||||||
52.251.55.143 permit
|
|
||||||
54.90.148.255 permit
|
54.90.148.255 permit
|
||||||
54.156.255.69 permit
|
|
||||||
54.172.97.247 permit
|
54.172.97.247 permit
|
||||||
|
54.174.52.0/24 permit
|
||||||
|
54.174.53.128/30 permit
|
||||||
|
54.174.57.0/24 permit
|
||||||
|
54.174.59.0/24 permit
|
||||||
|
54.174.60.0/23 permit
|
||||||
|
54.174.63.0/24 permit
|
||||||
54.186.193.102 permit
|
54.186.193.102 permit
|
||||||
54.191.223.5 permit
|
54.191.223.56 permit
|
||||||
54.194.61.95 permit
|
54.194.61.95 permit
|
||||||
54.195.113.45 permit
|
54.195.113.45 permit
|
||||||
|
54.213.20.246 permit
|
||||||
54.214.39.184 permit
|
54.214.39.184 permit
|
||||||
54.216.77.168 permit
|
54.216.77.168 permit
|
||||||
|
54.221.227.204 permit
|
||||||
54.240.0.0/18 permit
|
54.240.0.0/18 permit
|
||||||
54.240.64.0/19 permit
|
54.240.64.0/19 permit
|
||||||
54.240.96.0/19 permit
|
54.240.96.0/19 permit
|
||||||
@@ -238,7 +256,9 @@
|
|||||||
54.244.54.130 permit
|
54.244.54.130 permit
|
||||||
54.244.242.0/24 permit
|
54.244.242.0/24 permit
|
||||||
54.246.232.180 permit
|
54.246.232.180 permit
|
||||||
|
54.255.61.23 permit
|
||||||
62.13.128.0/24 permit
|
62.13.128.0/24 permit
|
||||||
|
62.13.128.150 permit
|
||||||
62.13.129.128/25 permit
|
62.13.129.128/25 permit
|
||||||
62.13.136.0/22 permit
|
62.13.136.0/22 permit
|
||||||
62.13.140.0/22 permit
|
62.13.140.0/22 permit
|
||||||
@@ -247,29 +267,32 @@
|
|||||||
62.13.150.0/23 permit
|
62.13.150.0/23 permit
|
||||||
62.13.152.0/23 permit
|
62.13.152.0/23 permit
|
||||||
62.17.146.128/26 permit
|
62.17.146.128/26 permit
|
||||||
62.140.7.0/24 permit
|
62.179.121.0/24 permit
|
||||||
62.140.10.21 permit
|
62.201.172.0/27 permit
|
||||||
|
62.201.172.32/27 permit
|
||||||
|
62.253.227.114 permit
|
||||||
63.32.13.159 permit
|
63.32.13.159 permit
|
||||||
63.80.14.0/23 permit
|
63.80.14.0/23 permit
|
||||||
|
63.111.28.137 permit
|
||||||
63.128.21.0/24 permit
|
63.128.21.0/24 permit
|
||||||
63.143.57.128/25 permit
|
63.143.57.128/25 permit
|
||||||
63.143.59.128/25 permit
|
63.143.59.128/25 permit
|
||||||
64.18.0.0/20 permit
|
64.18.0.0/20 permit
|
||||||
64.20.241.45 permit
|
64.20.241.45 permit
|
||||||
64.34.47.128/27 permit
|
64.69.212.0/24 permit
|
||||||
64.34.57.192/26 permit
|
|
||||||
64.71.149.160/28 permit
|
64.71.149.160/28 permit
|
||||||
64.79.155.0/24 permit
|
64.79.155.0/24 permit
|
||||||
|
64.79.155.192 permit
|
||||||
|
64.79.155.193 permit
|
||||||
|
64.79.155.205 permit
|
||||||
|
64.79.155.206 permit
|
||||||
64.89.44.85 permit
|
64.89.44.85 permit
|
||||||
64.89.45.80 permit
|
64.89.45.80 permit
|
||||||
64.89.45.194 permit
|
64.89.45.194 permit
|
||||||
64.89.45.196 permit
|
64.89.45.196 permit
|
||||||
64.95.144.196 permit
|
|
||||||
64.127.115.252 permit
|
64.127.115.252 permit
|
||||||
64.132.88.0/23 permit
|
64.132.88.0/23 permit
|
||||||
64.132.92.0/24 permit
|
64.132.92.0/24 permit
|
||||||
64.135.77.0/24 permit
|
|
||||||
64.135.83.0/24 permit
|
|
||||||
64.147.123.17 permit
|
64.147.123.17 permit
|
||||||
64.147.123.18 permit
|
64.147.123.18 permit
|
||||||
64.147.123.19 permit
|
64.147.123.19 permit
|
||||||
@@ -281,28 +304,35 @@
|
|||||||
64.147.123.27 permit
|
64.147.123.27 permit
|
||||||
64.147.123.28 permit
|
64.147.123.28 permit
|
||||||
64.147.123.29 permit
|
64.147.123.29 permit
|
||||||
|
64.147.123.128/27 permit
|
||||||
64.207.219.7 permit
|
64.207.219.7 permit
|
||||||
64.207.219.8 permit
|
64.207.219.8 permit
|
||||||
64.207.219.9 permit
|
64.207.219.9 permit
|
||||||
|
64.207.219.10 permit
|
||||||
|
64.207.219.11 permit
|
||||||
|
64.207.219.12 permit
|
||||||
64.207.219.13 permit
|
64.207.219.13 permit
|
||||||
64.207.219.14 permit
|
64.207.219.14 permit
|
||||||
64.207.219.15 permit
|
64.207.219.15 permit
|
||||||
64.207.219.71 permit
|
64.207.219.71 permit
|
||||||
64.207.219.72 permit
|
64.207.219.72 permit
|
||||||
64.207.219.73 permit
|
64.207.219.73 permit
|
||||||
|
64.207.219.74 permit
|
||||||
|
64.207.219.75 permit
|
||||||
|
64.207.219.76 permit
|
||||||
64.207.219.77 permit
|
64.207.219.77 permit
|
||||||
64.207.219.78 permit
|
64.207.219.78 permit
|
||||||
64.207.219.79 permit
|
64.207.219.79 permit
|
||||||
64.207.219.135 permit
|
64.207.219.135 permit
|
||||||
64.207.219.136 permit
|
64.207.219.136 permit
|
||||||
64.207.219.137 permit
|
64.207.219.137 permit
|
||||||
|
64.207.219.138 permit
|
||||||
|
64.207.219.139 permit
|
||||||
|
64.207.219.140 permit
|
||||||
64.207.219.141 permit
|
64.207.219.141 permit
|
||||||
64.207.219.142 permit
|
64.207.219.142 permit
|
||||||
64.207.219.143 permit
|
64.207.219.143 permit
|
||||||
64.233.160.0/19 permit
|
64.233.160.0/19 permit
|
||||||
65.38.115.76 permit
|
|
||||||
65.38.115.84 permit
|
|
||||||
65.39.215.0/24 permit
|
|
||||||
65.52.80.137 permit
|
65.52.80.137 permit
|
||||||
65.54.51.64/26 permit
|
65.54.51.64/26 permit
|
||||||
65.54.61.64/26 permit
|
65.54.61.64/26 permit
|
||||||
@@ -342,6 +372,10 @@
|
|||||||
66.111.4.225 permit
|
66.111.4.225 permit
|
||||||
66.111.4.229 permit
|
66.111.4.229 permit
|
||||||
66.111.4.230 permit
|
66.111.4.230 permit
|
||||||
|
66.119.150.192/26 permit
|
||||||
|
66.135.202.0/27 permit
|
||||||
|
66.135.215.0/24 permit
|
||||||
|
66.135.222.1 permit
|
||||||
66.162.193.226/31 permit
|
66.162.193.226/31 permit
|
||||||
66.163.184.0/21 permit
|
66.163.184.0/21 permit
|
||||||
66.163.184.0/24 permit
|
66.163.184.0/24 permit
|
||||||
@@ -372,8 +406,8 @@
|
|||||||
66.196.81.232/31 permit
|
66.196.81.232/31 permit
|
||||||
66.196.81.234 permit
|
66.196.81.234 permit
|
||||||
66.211.168.230/31 permit
|
66.211.168.230/31 permit
|
||||||
66.211.170.86/31 permit
|
66.211.170.88/29 permit
|
||||||
66.211.170.88/30 permit
|
66.211.184.0/23 permit
|
||||||
66.218.74.64/30 permit
|
66.218.74.64/30 permit
|
||||||
66.218.74.68/31 permit
|
66.218.74.68/31 permit
|
||||||
66.218.75.112/30 permit
|
66.218.75.112/30 permit
|
||||||
@@ -445,6 +479,8 @@
|
|||||||
68.142.230.72/30 permit
|
68.142.230.72/30 permit
|
||||||
68.142.230.76/31 permit
|
68.142.230.76/31 permit
|
||||||
68.142.230.78 permit
|
68.142.230.78 permit
|
||||||
|
68.232.140.138 permit
|
||||||
|
68.232.157.143 permit
|
||||||
68.232.192.0/20 permit
|
68.232.192.0/20 permit
|
||||||
69.63.178.128/25 permit
|
69.63.178.128/25 permit
|
||||||
69.63.181.0/24 permit
|
69.63.181.0/24 permit
|
||||||
@@ -452,6 +488,10 @@
|
|||||||
69.65.42.195 permit
|
69.65.42.195 permit
|
||||||
69.65.49.192/29 permit
|
69.65.49.192/29 permit
|
||||||
69.72.32.0/20 permit
|
69.72.32.0/20 permit
|
||||||
|
69.72.40.93 permit
|
||||||
|
69.72.40.94/31 permit
|
||||||
|
69.72.40.96/30 permit
|
||||||
|
69.72.47.205 permit
|
||||||
69.147.84.227 permit
|
69.147.84.227 permit
|
||||||
69.162.98.0/24 permit
|
69.162.98.0/24 permit
|
||||||
69.169.224.0/20 permit
|
69.169.224.0/20 permit
|
||||||
@@ -460,7 +500,7 @@
|
|||||||
70.37.151.128/25 permit
|
70.37.151.128/25 permit
|
||||||
70.42.149.0/24 permit
|
70.42.149.0/24 permit
|
||||||
70.42.149.35 permit
|
70.42.149.35 permit
|
||||||
72.3.185.0/24 permit
|
72.3.237.64/28 permit
|
||||||
72.14.192.0/18 permit
|
72.14.192.0/18 permit
|
||||||
72.21.192.0/19 permit
|
72.21.192.0/19 permit
|
||||||
72.21.217.142 permit
|
72.21.217.142 permit
|
||||||
@@ -522,15 +562,11 @@
|
|||||||
72.30.239.228/31 permit
|
72.30.239.228/31 permit
|
||||||
72.30.239.244/30 permit
|
72.30.239.244/30 permit
|
||||||
72.30.239.248/31 permit
|
72.30.239.248/31 permit
|
||||||
72.32.154.0/24 permit
|
|
||||||
72.32.217.0/24 permit
|
|
||||||
72.32.243.0/24 permit
|
|
||||||
72.34.168.76 permit
|
72.34.168.76 permit
|
||||||
72.34.168.80 permit
|
72.34.168.80 permit
|
||||||
72.34.168.85 permit
|
72.34.168.85 permit
|
||||||
72.34.168.86 permit
|
72.34.168.86 permit
|
||||||
72.52.72.32/28 permit
|
72.52.72.32/28 permit
|
||||||
72.52.72.36 permit
|
|
||||||
74.6.128.0/21 permit
|
74.6.128.0/21 permit
|
||||||
74.6.128.0/24 permit
|
74.6.128.0/24 permit
|
||||||
74.6.129.0/24 permit
|
74.6.129.0/24 permit
|
||||||
@@ -558,8 +594,12 @@
|
|||||||
74.112.67.243 permit
|
74.112.67.243 permit
|
||||||
74.125.0.0/16 permit
|
74.125.0.0/16 permit
|
||||||
74.202.227.40 permit
|
74.202.227.40 permit
|
||||||
|
74.208.4.192/26 permit
|
||||||
|
74.208.5.64/26 permit
|
||||||
|
74.208.122.0/26 permit
|
||||||
74.209.250.0/24 permit
|
74.209.250.0/24 permit
|
||||||
74.209.250.12 permit
|
75.2.70.75 permit
|
||||||
|
76.223.128.0/19 permit
|
||||||
76.223.176.0/20 permit
|
76.223.176.0/20 permit
|
||||||
77.238.176.0/22 permit
|
77.238.176.0/22 permit
|
||||||
77.238.176.0/24 permit
|
77.238.176.0/24 permit
|
||||||
@@ -582,8 +622,17 @@
|
|||||||
77.238.189.142 permit
|
77.238.189.142 permit
|
||||||
77.238.189.146/31 permit
|
77.238.189.146/31 permit
|
||||||
77.238.189.148/30 permit
|
77.238.189.148/30 permit
|
||||||
|
81.7.169.128/25 permit
|
||||||
81.223.46.0/27 permit
|
81.223.46.0/27 permit
|
||||||
84.16.77.1 permit
|
82.165.159.0/24 permit
|
||||||
|
82.165.159.0/26 permit
|
||||||
|
82.165.229.31 permit
|
||||||
|
82.165.229.130 permit
|
||||||
|
82.165.230.21 permit
|
||||||
|
82.165.230.22 permit
|
||||||
|
84.116.6.0/23 permit
|
||||||
|
84.116.36.0/24 permit
|
||||||
|
84.116.50.0/23 permit
|
||||||
85.158.136.0/21 permit
|
85.158.136.0/21 permit
|
||||||
86.61.88.25 permit
|
86.61.88.25 permit
|
||||||
87.198.219.130 permit
|
87.198.219.130 permit
|
||||||
@@ -624,11 +673,9 @@
|
|||||||
87.248.117.201 permit
|
87.248.117.201 permit
|
||||||
87.248.117.202 permit
|
87.248.117.202 permit
|
||||||
87.248.117.205 permit
|
87.248.117.205 permit
|
||||||
87.252.219.254 permit
|
|
||||||
87.253.232.0/21 permit
|
87.253.232.0/21 permit
|
||||||
89.22.108.0/24 permit
|
89.22.108.0/24 permit
|
||||||
91.220.42.0/24 permit
|
91.211.240.0/22 permit
|
||||||
94.236.119.0/26 permit
|
|
||||||
94.245.112.0/27 permit
|
94.245.112.0/27 permit
|
||||||
94.245.112.10/31 permit
|
94.245.112.10/31 permit
|
||||||
95.131.104.0/21 permit
|
95.131.104.0/21 permit
|
||||||
@@ -638,6 +685,7 @@
|
|||||||
96.43.148.64/28 permit
|
96.43.148.64/28 permit
|
||||||
96.43.148.64/31 permit
|
96.43.148.64/31 permit
|
||||||
96.43.151.64/28 permit
|
96.43.151.64/28 permit
|
||||||
|
98.97.248.0/21 permit
|
||||||
98.136.44.181 permit
|
98.136.44.181 permit
|
||||||
98.136.44.182/31 permit
|
98.136.44.182/31 permit
|
||||||
98.136.44.184 permit
|
98.136.44.184 permit
|
||||||
@@ -1141,25 +1189,22 @@
|
|||||||
98.139.245.208/30 permit
|
98.139.245.208/30 permit
|
||||||
98.139.245.212/31 permit
|
98.139.245.212/31 permit
|
||||||
99.78.197.208/28 permit
|
99.78.197.208/28 permit
|
||||||
|
99.83.190.102 permit
|
||||||
103.2.140.0/22 permit
|
103.2.140.0/22 permit
|
||||||
103.9.8.121 permit
|
|
||||||
103.9.8.122 permit
|
|
||||||
103.9.8.123 permit
|
|
||||||
103.9.96.0/22 permit
|
103.9.96.0/22 permit
|
||||||
103.13.69.0/24 permit
|
103.28.42.0/24 permit
|
||||||
103.47.204.0/22 permit
|
103.47.204.0/22 permit
|
||||||
103.96.21.0/24 permit
|
|
||||||
103.96.23.0/24 permit
|
|
||||||
103.151.192.0/23 permit
|
103.151.192.0/23 permit
|
||||||
103.237.104.0/22 permit
|
103.168.172.128/27 permit
|
||||||
104.43.243.237 permit
|
104.43.243.237 permit
|
||||||
|
104.44.112.128/25 permit
|
||||||
104.47.0.0/17 permit
|
104.47.0.0/17 permit
|
||||||
|
104.47.20.0/23 permit
|
||||||
|
104.47.75.0/24 permit
|
||||||
|
104.47.108.0/23 permit
|
||||||
104.130.96.0/28 permit
|
104.130.96.0/28 permit
|
||||||
104.130.122.0/23 permit
|
104.130.122.0/23 permit
|
||||||
104.214.25.77 permit
|
104.214.25.77 permit
|
||||||
104.215.148.63 permit
|
|
||||||
104.215.186.3 permit
|
|
||||||
104.245.209.192/26 permit
|
|
||||||
106.10.144.64/27 permit
|
106.10.144.64/27 permit
|
||||||
106.10.144.100/31 permit
|
106.10.144.100/31 permit
|
||||||
106.10.144.103 permit
|
106.10.144.103 permit
|
||||||
@@ -1320,9 +1365,9 @@
|
|||||||
117.120.16.0/21 permit
|
117.120.16.0/21 permit
|
||||||
119.42.242.52/31 permit
|
119.42.242.52/31 permit
|
||||||
119.42.242.156 permit
|
119.42.242.156 permit
|
||||||
|
121.244.91.48 permit
|
||||||
|
122.15.156.182 permit
|
||||||
123.126.78.64/29 permit
|
123.126.78.64/29 permit
|
||||||
124.47.150.0/24 permit
|
|
||||||
124.47.189.0/24 permit
|
|
||||||
124.108.96.0/24 permit
|
124.108.96.0/24 permit
|
||||||
124.108.96.24/31 permit
|
124.108.96.24/31 permit
|
||||||
124.108.96.28/31 permit
|
124.108.96.28/31 permit
|
||||||
@@ -1335,20 +1380,40 @@
|
|||||||
128.127.70.0/26 permit
|
128.127.70.0/26 permit
|
||||||
128.245.0.0/20 permit
|
128.245.0.0/20 permit
|
||||||
128.245.64.0/20 permit
|
128.245.64.0/20 permit
|
||||||
|
128.245.176.0/20 permit
|
||||||
|
128.245.240.0/24 permit
|
||||||
|
128.245.241.0/24 permit
|
||||||
|
128.245.242.0/24 permit
|
||||||
|
128.245.242.16 permit
|
||||||
|
128.245.242.17 permit
|
||||||
|
128.245.242.18 permit
|
||||||
|
128.245.243.0/24 permit
|
||||||
|
128.245.244.0/24 permit
|
||||||
|
128.245.245.0/24 permit
|
||||||
|
128.245.246.0/24 permit
|
||||||
|
128.245.247.0/24 permit
|
||||||
|
128.245.248.0/21 permit
|
||||||
129.41.77.70 permit
|
129.41.77.70 permit
|
||||||
129.41.169.249 permit
|
129.41.169.249 permit
|
||||||
|
129.80.5.164 permit
|
||||||
|
129.80.67.121 permit
|
||||||
|
129.145.74.12 permit
|
||||||
|
129.146.88.28 permit
|
||||||
|
129.146.147.105 permit
|
||||||
129.146.236.58 permit
|
129.146.236.58 permit
|
||||||
|
129.151.67.221 permit
|
||||||
|
129.153.62.216 permit
|
||||||
|
129.153.104.71 permit
|
||||||
|
129.153.168.146 permit
|
||||||
|
129.153.190.200 permit
|
||||||
129.153.194.228 permit
|
129.153.194.228 permit
|
||||||
129.159.87.137 permit
|
129.159.87.137 permit
|
||||||
|
129.213.195.191 permit
|
||||||
130.61.9.72 permit
|
130.61.9.72 permit
|
||||||
|
130.162.39.83 permit
|
||||||
130.211.0.0/22 permit
|
130.211.0.0/22 permit
|
||||||
130.248.172.0/24 permit
|
|
||||||
130.248.173.0/24 permit
|
|
||||||
131.107.0.0/16 permit
|
|
||||||
131.253.30.0/24 permit
|
131.253.30.0/24 permit
|
||||||
131.253.121.0/26 permit
|
131.253.121.0/26 permit
|
||||||
131.253.121.20 permit
|
|
||||||
131.253.121.52 permit
|
|
||||||
132.145.13.209 permit
|
132.145.13.209 permit
|
||||||
132.226.26.225 permit
|
132.226.26.225 permit
|
||||||
132.226.49.32 permit
|
132.226.49.32 permit
|
||||||
@@ -1358,45 +1423,69 @@
|
|||||||
134.170.141.64/26 permit
|
134.170.141.64/26 permit
|
||||||
134.170.143.0/24 permit
|
134.170.143.0/24 permit
|
||||||
134.170.174.0/24 permit
|
134.170.174.0/24 permit
|
||||||
135.84.80.192/26 permit
|
135.84.80.0/24 permit
|
||||||
|
135.84.81.0/24 permit
|
||||||
135.84.82.0/24 permit
|
135.84.82.0/24 permit
|
||||||
|
135.84.83.0/24 permit
|
||||||
135.84.216.0/22 permit
|
135.84.216.0/22 permit
|
||||||
|
136.143.160.0/24 permit
|
||||||
|
136.143.161.0/24 permit
|
||||||
136.143.182.0/23 permit
|
136.143.182.0/23 permit
|
||||||
136.143.184.0/24 permit
|
136.143.184.0/24 permit
|
||||||
136.143.188.0/24 permit
|
136.143.188.0/24 permit
|
||||||
|
136.143.190.0/23 permit
|
||||||
136.147.128.0/20 permit
|
136.147.128.0/20 permit
|
||||||
136.147.135.0/24 permit
|
136.147.135.0/24 permit
|
||||||
136.147.176.0/20 permit
|
136.147.176.0/20 permit
|
||||||
136.147.176.0/24 permit
|
136.147.176.0/24 permit
|
||||||
136.147.182.0/24 permit
|
136.147.182.0/24 permit
|
||||||
|
136.179.50.206 permit
|
||||||
138.91.172.26 permit
|
138.91.172.26 permit
|
||||||
139.60.152.0/22 permit
|
139.60.152.0/22 permit
|
||||||
139.178.64.159 permit
|
139.138.35.44 permit
|
||||||
139.178.64.195 permit
|
139.138.46.121 permit
|
||||||
|
139.138.46.176 permit
|
||||||
|
139.138.46.219 permit
|
||||||
|
139.138.57.55 permit
|
||||||
|
139.138.58.119 permit
|
||||||
|
139.180.17.0/24 permit
|
||||||
|
141.148.159.229 permit
|
||||||
141.193.32.0/23 permit
|
141.193.32.0/23 permit
|
||||||
143.55.224.0/21 permit
|
143.55.224.0/21 permit
|
||||||
143.55.232.0/22 permit
|
143.55.232.0/22 permit
|
||||||
143.55.236.0/22 permit
|
143.55.236.0/22 permit
|
||||||
|
143.244.80.0/20 permit
|
||||||
|
144.24.6.140 permit
|
||||||
|
144.34.8.247 permit
|
||||||
|
144.34.9.247 permit
|
||||||
|
144.34.32.247 permit
|
||||||
|
144.34.33.247 permit
|
||||||
144.178.36.0/24 permit
|
144.178.36.0/24 permit
|
||||||
144.178.38.0/24 permit
|
144.178.38.0/24 permit
|
||||||
|
145.253.228.160/29 permit
|
||||||
|
145.253.239.128/29 permit
|
||||||
|
146.20.14.105 permit
|
||||||
|
146.20.14.107 permit
|
||||||
146.20.112.0/26 permit
|
146.20.112.0/26 permit
|
||||||
146.20.113.0/24 permit
|
146.20.113.0/24 permit
|
||||||
146.20.191.0/24 permit
|
146.20.191.0/24 permit
|
||||||
146.20.215.0/24 permit
|
146.20.215.0/24 permit
|
||||||
146.101.78.0/24 permit
|
146.20.215.182 permit
|
||||||
147.75.65.173 permit
|
146.88.28.0/24 permit
|
||||||
147.75.65.174 permit
|
|
||||||
147.75.98.190 permit
|
|
||||||
147.160.158.0/24 permit
|
|
||||||
147.243.1.47 permit
|
147.243.1.47 permit
|
||||||
147.243.1.48 permit
|
147.243.1.48 permit
|
||||||
147.243.1.153 permit
|
147.243.1.153 permit
|
||||||
147.243.128.24 permit
|
147.243.128.24 permit
|
||||||
147.243.128.26 permit
|
147.243.128.26 permit
|
||||||
148.105.0.14 permit
|
148.105.0.0/16 permit
|
||||||
148.105.8.0/21 permit
|
148.105.8.0/21 permit
|
||||||
149.72.0.0/16 permit
|
149.72.0.0/16 permit
|
||||||
|
149.72.248.236 permit
|
||||||
|
149.97.173.180 permit
|
||||||
|
150.230.98.160 permit
|
||||||
152.67.105.195 permit
|
152.67.105.195 permit
|
||||||
|
152.69.200.236 permit
|
||||||
|
155.248.208.51 permit
|
||||||
157.55.0.192/26 permit
|
157.55.0.192/26 permit
|
||||||
157.55.1.128/26 permit
|
157.55.1.128/26 permit
|
||||||
157.55.2.0/25 permit
|
157.55.2.0/25 permit
|
||||||
@@ -1412,37 +1501,58 @@
|
|||||||
157.56.232.0/21 permit
|
157.56.232.0/21 permit
|
||||||
157.56.240.0/20 permit
|
157.56.240.0/20 permit
|
||||||
157.56.248.0/21 permit
|
157.56.248.0/21 permit
|
||||||
|
157.58.30.128/25 permit
|
||||||
157.58.196.96/29 permit
|
157.58.196.96/29 permit
|
||||||
157.58.249.3 permit
|
157.58.249.3 permit
|
||||||
157.151.208.65 permit
|
157.151.208.65 permit
|
||||||
157.255.1.64/29 permit
|
157.255.1.64/29 permit
|
||||||
|
158.101.211.207 permit
|
||||||
|
158.120.80.0/21 permit
|
||||||
|
158.247.16.0/20 permit
|
||||||
|
159.92.154.0/24 permit
|
||||||
|
159.92.155.0/24 permit
|
||||||
159.92.157.0/24 permit
|
159.92.157.0/24 permit
|
||||||
|
159.92.157.16 permit
|
||||||
|
159.92.157.17 permit
|
||||||
|
159.92.157.18 permit
|
||||||
159.92.158.0/24 permit
|
159.92.158.0/24 permit
|
||||||
159.92.159.0/24 permit
|
159.92.159.0/24 permit
|
||||||
159.92.160.0/24 permit
|
159.92.160.0/24 permit
|
||||||
159.92.161.0/24 permit
|
159.92.161.0/24 permit
|
||||||
159.92.162.0/24 permit
|
159.92.162.0/24 permit
|
||||||
|
159.92.163.0/24 permit
|
||||||
|
159.92.164.0/22 permit
|
||||||
|
159.92.168.0/21 permit
|
||||||
|
159.112.240.0/20 permit
|
||||||
|
159.112.242.162 permit
|
||||||
159.135.132.128/25 permit
|
159.135.132.128/25 permit
|
||||||
159.135.140.80/29 permit
|
159.135.140.80/29 permit
|
||||||
159.135.224.0/20 permit
|
159.135.224.0/20 permit
|
||||||
|
159.135.228.10 permit
|
||||||
159.183.0.0/16 permit
|
159.183.0.0/16 permit
|
||||||
|
160.1.62.192 permit
|
||||||
161.38.192.0/20 permit
|
161.38.192.0/20 permit
|
||||||
161.38.204.0/22 permit
|
161.38.204.0/22 permit
|
||||||
161.71.32.0/19 permit
|
161.71.32.0/19 permit
|
||||||
161.71.64.0/20 permit
|
161.71.64.0/20 permit
|
||||||
162.208.119.181 permit
|
|
||||||
162.247.216.0/22 permit
|
162.247.216.0/22 permit
|
||||||
|
163.47.180.0/22 permit
|
||||||
163.47.180.0/23 permit
|
163.47.180.0/23 permit
|
||||||
163.114.130.16 permit
|
163.114.130.16 permit
|
||||||
163.114.132.120 permit
|
163.114.132.120 permit
|
||||||
|
164.177.132.168 permit
|
||||||
|
164.177.132.169 permit
|
||||||
|
164.177.132.170 permit
|
||||||
|
164.177.132.171 permit
|
||||||
|
165.173.128.0/24 permit
|
||||||
166.78.68.0/22 permit
|
166.78.68.0/22 permit
|
||||||
166.78.68.221 permit
|
166.78.68.221 permit
|
||||||
166.78.69.146 permit
|
|
||||||
166.78.69.169 permit
|
166.78.69.169 permit
|
||||||
166.78.69.170 permit
|
166.78.69.170 permit
|
||||||
166.78.71.131 permit
|
166.78.71.131 permit
|
||||||
167.89.0.0/17 permit
|
167.89.0.0/17 permit
|
||||||
167.89.46.159 permit
|
167.89.46.159 permit
|
||||||
|
167.89.54.103 permit
|
||||||
167.89.64.9 permit
|
167.89.64.9 permit
|
||||||
167.89.65.0 permit
|
167.89.65.0 permit
|
||||||
167.89.65.53 permit
|
167.89.65.53 permit
|
||||||
@@ -1457,10 +1567,18 @@
|
|||||||
167.216.129.210 permit
|
167.216.129.210 permit
|
||||||
167.216.131.180 permit
|
167.216.131.180 permit
|
||||||
167.220.67.232/29 permit
|
167.220.67.232/29 permit
|
||||||
167.220.67.238 permit
|
|
||||||
168.138.5.36 permit
|
168.138.5.36 permit
|
||||||
|
168.138.73.51 permit
|
||||||
168.245.0.0/17 permit
|
168.245.0.0/17 permit
|
||||||
|
168.245.12.252 permit
|
||||||
|
168.245.46.9 permit
|
||||||
|
168.245.127.231 permit
|
||||||
|
169.148.129.0/24 permit
|
||||||
|
169.148.131.0/24 permit
|
||||||
|
169.148.142.10 permit
|
||||||
|
169.148.144.0/25 permit
|
||||||
170.10.68.0/22 permit
|
170.10.68.0/22 permit
|
||||||
|
170.10.128.0/24 permit
|
||||||
170.10.129.0/24 permit
|
170.10.129.0/24 permit
|
||||||
170.10.133.0/24 permit
|
170.10.133.0/24 permit
|
||||||
172.217.0.0/19 permit
|
172.217.0.0/19 permit
|
||||||
@@ -1475,10 +1593,8 @@
|
|||||||
173.194.0.0/16 permit
|
173.194.0.0/16 permit
|
||||||
173.203.79.182 permit
|
173.203.79.182 permit
|
||||||
173.203.81.39 permit
|
173.203.81.39 permit
|
||||||
173.224.160.128/25 permit
|
|
||||||
173.224.160.188 permit
|
|
||||||
173.224.161.128/25 permit
|
173.224.161.128/25 permit
|
||||||
173.228.155.0/24 permit
|
173.224.165.0/26 permit
|
||||||
174.36.84.8/29 permit
|
174.36.84.8/29 permit
|
||||||
174.36.84.16/29 permit
|
174.36.84.16/29 permit
|
||||||
174.36.84.32/29 permit
|
174.36.84.32/29 permit
|
||||||
@@ -1491,27 +1607,27 @@
|
|||||||
174.36.114.152/29 permit
|
174.36.114.152/29 permit
|
||||||
174.37.67.28/30 permit
|
174.37.67.28/30 permit
|
||||||
174.129.203.189 permit
|
174.129.203.189 permit
|
||||||
|
175.41.215.51 permit
|
||||||
176.32.105.0/24 permit
|
176.32.105.0/24 permit
|
||||||
176.32.127.0/24 permit
|
176.32.127.0/24 permit
|
||||||
178.236.10.128/26 permit
|
178.236.10.128/26 permit
|
||||||
180.189.28.0/24 permit
|
|
||||||
182.50.76.0/22 permit
|
182.50.76.0/22 permit
|
||||||
182.50.78.64/28 permit
|
182.50.78.64/28 permit
|
||||||
183.240.219.64/29 permit
|
183.240.219.64/29 permit
|
||||||
|
185.4.120.0/23 permit
|
||||||
|
185.4.122.0/24 permit
|
||||||
185.12.80.0/22 permit
|
185.12.80.0/22 permit
|
||||||
185.28.196.0/22 permit
|
|
||||||
185.58.84.93 permit
|
185.58.84.93 permit
|
||||||
185.58.85.0/24 permit
|
|
||||||
185.58.86.0/24 permit
|
|
||||||
185.72.128.75 permit
|
|
||||||
185.72.128.76 permit
|
|
||||||
185.72.128.80 permit
|
|
||||||
185.80.93.204 permit
|
185.80.93.204 permit
|
||||||
185.80.93.227 permit
|
185.80.93.227 permit
|
||||||
185.80.95.31 permit
|
185.80.95.31 permit
|
||||||
|
185.90.20.0/22 permit
|
||||||
185.189.236.0/22 permit
|
185.189.236.0/22 permit
|
||||||
185.211.120.0/22 permit
|
185.211.120.0/22 permit
|
||||||
185.250.236.0/22 permit
|
185.250.236.0/22 permit
|
||||||
|
185.250.239.148 permit
|
||||||
|
185.250.239.168 permit
|
||||||
|
185.250.239.190 permit
|
||||||
188.125.68.132 permit
|
188.125.68.132 permit
|
||||||
188.125.68.152/31 permit
|
188.125.68.152/31 permit
|
||||||
188.125.68.156 permit
|
188.125.68.156 permit
|
||||||
@@ -1563,7 +1679,7 @@
|
|||||||
188.125.85.238 permit
|
188.125.85.238 permit
|
||||||
188.172.128.0/20 permit
|
188.172.128.0/20 permit
|
||||||
192.0.64.0/18 permit
|
192.0.64.0/18 permit
|
||||||
192.28.128.0/18 permit
|
192.18.139.154 permit
|
||||||
192.30.252.0/22 permit
|
192.30.252.0/22 permit
|
||||||
192.64.236.0/24 permit
|
192.64.236.0/24 permit
|
||||||
192.64.237.0/24 permit
|
192.64.237.0/24 permit
|
||||||
@@ -1579,17 +1695,17 @@
|
|||||||
192.254.113.10 permit
|
192.254.113.10 permit
|
||||||
192.254.113.101 permit
|
192.254.113.101 permit
|
||||||
192.254.114.176 permit
|
192.254.114.176 permit
|
||||||
192.254.118.63 permit
|
|
||||||
193.7.206.0/25 permit
|
|
||||||
193.7.207.0/25 permit
|
|
||||||
193.109.254.0/23 permit
|
193.109.254.0/23 permit
|
||||||
193.122.128.100 permit
|
193.122.128.100 permit
|
||||||
|
193.123.56.63 permit
|
||||||
|
194.19.134.0/25 permit
|
||||||
|
194.64.234.128/27 permit
|
||||||
194.64.234.129 permit
|
194.64.234.129 permit
|
||||||
194.104.109.0/24 permit
|
|
||||||
194.104.111.0/24 permit
|
|
||||||
194.106.220.0/23 permit
|
194.106.220.0/23 permit
|
||||||
|
194.113.24.0/22 permit
|
||||||
194.154.193.192/27 permit
|
194.154.193.192/27 permit
|
||||||
195.130.217.0/24 permit
|
195.4.92.0/23 permit
|
||||||
|
195.54.172.0/23 permit
|
||||||
195.234.109.226 permit
|
195.234.109.226 permit
|
||||||
195.245.230.0/23 permit
|
195.245.230.0/23 permit
|
||||||
198.2.128.0/18 permit
|
198.2.128.0/18 permit
|
||||||
@@ -1605,19 +1721,25 @@
|
|||||||
198.37.144.0/20 permit
|
198.37.144.0/20 permit
|
||||||
198.37.152.186 permit
|
198.37.152.186 permit
|
||||||
198.61.254.0/23 permit
|
198.61.254.0/23 permit
|
||||||
|
198.61.254.21 permit
|
||||||
198.61.254.231 permit
|
198.61.254.231 permit
|
||||||
198.74.56.28 permit
|
|
||||||
198.178.234.57 permit
|
198.178.234.57 permit
|
||||||
|
198.244.48.0/20 permit
|
||||||
|
198.244.60.0/22 permit
|
||||||
198.245.80.0/20 permit
|
198.245.80.0/20 permit
|
||||||
198.245.81.0/24 permit
|
198.245.81.0/24 permit
|
||||||
199.15.176.173 permit
|
|
||||||
199.15.212.0/22 permit
|
|
||||||
199.15.213.187 permit
|
199.15.213.187 permit
|
||||||
199.15.226.37 permit
|
199.15.226.37 permit
|
||||||
199.16.156.0/22 permit
|
199.16.156.0/22 permit
|
||||||
199.33.145.1 permit
|
199.33.145.1 permit
|
||||||
199.33.145.32 permit
|
199.33.145.32 permit
|
||||||
|
199.34.22.36 permit
|
||||||
199.59.148.0/22 permit
|
199.59.148.0/22 permit
|
||||||
|
199.67.80.2 permit
|
||||||
|
199.67.82.2 permit
|
||||||
|
199.67.84.0/24 permit
|
||||||
|
199.67.86.0/24 permit
|
||||||
|
199.67.88.0/24 permit
|
||||||
199.101.161.130 permit
|
199.101.161.130 permit
|
||||||
199.101.162.0/25 permit
|
199.101.162.0/25 permit
|
||||||
199.122.120.0/21 permit
|
199.122.120.0/21 permit
|
||||||
@@ -1630,8 +1752,10 @@
|
|||||||
202.177.148.110 permit
|
202.177.148.110 permit
|
||||||
203.31.36.0/22 permit
|
203.31.36.0/22 permit
|
||||||
203.32.4.25 permit
|
203.32.4.25 permit
|
||||||
|
203.55.21.0/24 permit
|
||||||
203.81.17.0/24 permit
|
203.81.17.0/24 permit
|
||||||
203.122.32.250 permit
|
203.122.32.250 permit
|
||||||
|
203.145.57.160/27 permit
|
||||||
203.188.194.32 permit
|
203.188.194.32 permit
|
||||||
203.188.194.151 permit
|
203.188.194.151 permit
|
||||||
203.188.194.203 permit
|
203.188.194.203 permit
|
||||||
@@ -1666,28 +1790,33 @@
|
|||||||
203.209.230.76/31 permit
|
203.209.230.76/31 permit
|
||||||
204.11.168.0/21 permit
|
204.11.168.0/21 permit
|
||||||
204.13.11.48/29 permit
|
204.13.11.48/29 permit
|
||||||
|
204.13.11.48/30 permit
|
||||||
204.14.232.0/21 permit
|
204.14.232.0/21 permit
|
||||||
204.14.232.64/28 permit
|
204.14.232.64/28 permit
|
||||||
204.14.234.64/28 permit
|
204.14.234.64/28 permit
|
||||||
204.29.186.0/23 permit
|
204.29.186.0/23 permit
|
||||||
|
204.75.142.0/24 permit
|
||||||
204.79.197.212 permit
|
204.79.197.212 permit
|
||||||
204.92.114.187 permit
|
204.92.114.187 permit
|
||||||
204.92.114.203 permit
|
204.92.114.203 permit
|
||||||
204.92.114.204/31 permit
|
204.92.114.204/31 permit
|
||||||
|
204.132.224.66 permit
|
||||||
204.141.32.0/23 permit
|
204.141.32.0/23 permit
|
||||||
204.141.42.0/23 permit
|
204.141.42.0/23 permit
|
||||||
204.153.121.0/24 permit
|
204.220.160.0/20 permit
|
||||||
204.232.168.0/24 permit
|
204.232.168.0/24 permit
|
||||||
205.139.110.0/24 permit
|
205.139.110.0/24 permit
|
||||||
205.201.128.0/20 permit
|
205.201.128.0/20 permit
|
||||||
205.201.131.128/25 permit
|
205.201.131.128/25 permit
|
||||||
205.201.134.128/25 permit
|
205.201.134.128/25 permit
|
||||||
205.201.136.0/23 permit
|
205.201.136.0/23 permit
|
||||||
|
205.201.137.229 permit
|
||||||
205.201.139.0/24 permit
|
205.201.139.0/24 permit
|
||||||
205.207.104.0/22 permit
|
205.207.104.0/22 permit
|
||||||
205.207.104.108 permit
|
|
||||||
205.220.167.17 permit
|
205.220.167.17 permit
|
||||||
|
205.220.167.98 permit
|
||||||
205.220.179.17 permit
|
205.220.179.17 permit
|
||||||
|
205.220.179.98 permit
|
||||||
205.251.233.32 permit
|
205.251.233.32 permit
|
||||||
205.251.233.36 permit
|
205.251.233.36 permit
|
||||||
206.25.247.143 permit
|
206.25.247.143 permit
|
||||||
@@ -1715,7 +1844,8 @@
|
|||||||
207.67.98.192/27 permit
|
207.67.98.192/27 permit
|
||||||
207.68.176.0/26 permit
|
207.68.176.0/26 permit
|
||||||
207.68.176.96/27 permit
|
207.68.176.96/27 permit
|
||||||
207.82.80.0/24 permit
|
207.97.204.96 permit
|
||||||
|
207.97.204.97 permit
|
||||||
207.126.144.0/20 permit
|
207.126.144.0/20 permit
|
||||||
207.171.160.0/19 permit
|
207.171.160.0/19 permit
|
||||||
207.211.30.64/26 permit
|
207.211.30.64/26 permit
|
||||||
@@ -1723,6 +1853,7 @@
|
|||||||
207.211.31.0/25 permit
|
207.211.31.0/25 permit
|
||||||
207.211.41.113 permit
|
207.211.41.113 permit
|
||||||
207.218.90.0/24 permit
|
207.218.90.0/24 permit
|
||||||
|
207.218.90.122 permit
|
||||||
207.250.68.0/24 permit
|
207.250.68.0/24 permit
|
||||||
208.40.232.70 permit
|
208.40.232.70 permit
|
||||||
208.43.21.28/30 permit
|
208.43.21.28/30 permit
|
||||||
@@ -1758,8 +1889,10 @@
|
|||||||
208.71.42.212/31 permit
|
208.71.42.212/31 permit
|
||||||
208.71.42.214 permit
|
208.71.42.214 permit
|
||||||
208.72.249.240/29 permit
|
208.72.249.240/29 permit
|
||||||
|
208.74.204.0/22 permit
|
||||||
208.74.204.9 permit
|
208.74.204.9 permit
|
||||||
208.75.120.0/22 permit
|
208.75.120.0/22 permit
|
||||||
|
208.75.121.246 permit
|
||||||
208.75.122.246 permit
|
208.75.122.246 permit
|
||||||
208.82.237.96/29 permit
|
208.82.237.96/29 permit
|
||||||
208.82.237.104/31 permit
|
208.82.237.104/31 permit
|
||||||
@@ -1773,14 +1906,12 @@
|
|||||||
209.46.117.168 permit
|
209.46.117.168 permit
|
||||||
209.46.117.179 permit
|
209.46.117.179 permit
|
||||||
209.61.151.0/24 permit
|
209.61.151.0/24 permit
|
||||||
|
209.61.151.236 permit
|
||||||
|
209.61.151.249 permit
|
||||||
|
209.61.151.251 permit
|
||||||
209.67.98.46 permit
|
209.67.98.46 permit
|
||||||
209.67.98.59 permit
|
209.67.98.59 permit
|
||||||
209.85.128.0/17 permit
|
209.85.128.0/17 permit
|
||||||
212.4.136.0/26 permit
|
|
||||||
212.25.240.80 permit
|
|
||||||
212.25.240.83 permit
|
|
||||||
212.25.240.84/31 permit
|
|
||||||
212.25.240.88 permit
|
|
||||||
212.82.96.0/24 permit
|
212.82.96.0/24 permit
|
||||||
212.82.96.32/27 permit
|
212.82.96.32/27 permit
|
||||||
212.82.96.64/29 permit
|
212.82.96.64/29 permit
|
||||||
@@ -1821,8 +1952,12 @@
|
|||||||
212.82.111.228/31 permit
|
212.82.111.228/31 permit
|
||||||
212.82.111.230 permit
|
212.82.111.230 permit
|
||||||
212.123.28.40 permit
|
212.123.28.40 permit
|
||||||
213.167.75.0/25 permit
|
212.227.15.0/24 permit
|
||||||
213.167.81.0/25 permit
|
212.227.15.0/25 permit
|
||||||
|
212.227.17.0/27 permit
|
||||||
|
212.227.126.128/25 permit
|
||||||
|
213.46.255.0/24 permit
|
||||||
|
213.165.64.0/23 permit
|
||||||
213.199.128.139 permit
|
213.199.128.139 permit
|
||||||
213.199.128.145 permit
|
213.199.128.145 permit
|
||||||
213.199.138.181 permit
|
213.199.138.181 permit
|
||||||
@@ -1861,6 +1996,10 @@
|
|||||||
216.46.168.0/24 permit
|
216.46.168.0/24 permit
|
||||||
216.58.192.0/19 permit
|
216.58.192.0/19 permit
|
||||||
216.66.217.240/29 permit
|
216.66.217.240/29 permit
|
||||||
|
216.71.138.33 permit
|
||||||
|
216.71.152.207 permit
|
||||||
|
216.71.154.29 permit
|
||||||
|
216.71.155.89 permit
|
||||||
216.74.162.13 permit
|
216.74.162.13 permit
|
||||||
216.74.162.14 permit
|
216.74.162.14 permit
|
||||||
216.82.240.0/20 permit
|
216.82.240.0/20 permit
|
||||||
@@ -1870,33 +2009,48 @@
|
|||||||
216.109.114.0/24 permit
|
216.109.114.0/24 permit
|
||||||
216.109.114.32/27 permit
|
216.109.114.32/27 permit
|
||||||
216.109.114.64/29 permit
|
216.109.114.64/29 permit
|
||||||
|
216.113.160.0/24 permit
|
||||||
|
216.113.172.0/25 permit
|
||||||
|
216.113.175.0/24 permit
|
||||||
216.128.126.97 permit
|
216.128.126.97 permit
|
||||||
216.136.162.65 permit
|
216.136.162.65 permit
|
||||||
216.136.162.120/29 permit
|
216.136.162.120/29 permit
|
||||||
216.136.168.80/28 permit
|
216.136.168.80/28 permit
|
||||||
|
216.145.221.0/24 permit
|
||||||
216.198.0.0/18 permit
|
216.198.0.0/18 permit
|
||||||
216.203.30.55 permit
|
216.203.30.55 permit
|
||||||
216.203.33.178/31 permit
|
216.203.33.178/31 permit
|
||||||
216.205.24.0/24 permit
|
216.205.24.0/24 permit
|
||||||
216.239.32.0/19 permit
|
216.239.32.0/19 permit
|
||||||
|
217.72.192.64/26 permit
|
||||||
|
217.72.192.248/29 permit
|
||||||
|
217.72.207.0/27 permit
|
||||||
217.77.141.52 permit
|
217.77.141.52 permit
|
||||||
217.77.141.59 permit
|
217.77.141.59 permit
|
||||||
|
217.175.194.0/24 permit
|
||||||
222.73.195.64/29 permit
|
222.73.195.64/29 permit
|
||||||
223.165.113.0/24 permit
|
223.165.113.0/24 permit
|
||||||
223.165.115.0/24 permit
|
223.165.115.0/24 permit
|
||||||
223.165.118.0/23 permit
|
223.165.118.0/23 permit
|
||||||
223.165.120.0/23 permit
|
223.165.120.0/23 permit
|
||||||
|
2001:0868:0100:0600::/64 permit
|
||||||
2001:4860:4000::/36 permit
|
2001:4860:4000::/36 permit
|
||||||
|
2001:748:100:40::2:0/112 permit
|
||||||
2404:6800:4000::/36 permit
|
2404:6800:4000::/36 permit
|
||||||
|
2603:1010:3:3::5b permit
|
||||||
|
2603:1020:201:10::10f permit
|
||||||
|
2603:1030:20e:3::23c permit
|
||||||
|
2603:1030:b:3::152 permit
|
||||||
|
2603:1030:c02:8::14 permit
|
||||||
2607:f8b0:4000::/36 permit
|
2607:f8b0:4000::/36 permit
|
||||||
2620:109:c003:104::215 permit
|
|
||||||
2620:109:c003:104::/64 permit
|
2620:109:c003:104::/64 permit
|
||||||
2620:109:c006:104::215 permit
|
2620:109:c003:104::215 permit
|
||||||
2620:109:c006:104::/64 permit
|
2620:109:c006:104::/64 permit
|
||||||
|
2620:109:c006:104::215 permit
|
||||||
2620:109:c00d:104::/64 permit
|
2620:109:c00d:104::/64 permit
|
||||||
2620:10d:c090:450::120 permit
|
2620:10d:c090:400::8:1 permit
|
||||||
2620:10d:c091:450::16 permit
|
2620:10d:c091:400::8:1 permit
|
||||||
2620:119:50c0:207::215 permit
|
|
||||||
2620:119:50c0:207::/64 permit
|
2620:119:50c0:207::/64 permit
|
||||||
|
2620:119:50c0:207::215 permit
|
||||||
2800:3f0:4000::/36 permit
|
2800:3f0:4000::/36 permit
|
||||||
194.25.134.0/24 permit # t-online.de
|
194.25.134.0/24 permit # t-online.de
|
||||||
|
|||||||
@@ -27,4 +27,5 @@
|
|||||||
#197518 2 #Rackmarkt SL, Spain
|
#197518 2 #Rackmarkt SL, Spain
|
||||||
#197695 2 #Domain names registrar REG.RU Ltd, Russia
|
#197695 2 #Domain names registrar REG.RU Ltd, Russia
|
||||||
#198068 2 #P.A.G.M. OU, Estonia
|
#198068 2 #P.A.G.M. OU, Estonia
|
||||||
#201942 5 #Soltia Consulting SL, Spain
|
#201942 5 #Soltia Consulting SL, Spain
|
||||||
|
#213373 4 #IP Connect Inc
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
// File size is limited by Nginx site to 10M
|
||||||
|
// To speed things up, we do not include prerequisites
|
||||||
|
header('Content-Type: text/plain');
|
||||||
|
require_once "vars.inc.php";
|
||||||
|
// Do not show errors, we log to using error_log
|
||||||
|
ini_set('error_reporting', 0);
|
||||||
|
// Init database
|
||||||
|
//$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name;
|
||||||
|
$dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name;
|
||||||
|
$opt = [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => false,
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
||||||
|
}
|
||||||
|
catch (PDOException $e) {
|
||||||
|
error_log("FOOTER: " . $e . PHP_EOL);
|
||||||
|
http_response_code(501);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('getallheaders')) {
|
||||||
|
function getallheaders() {
|
||||||
|
if (!is_array($_SERVER)) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
$headers = array();
|
||||||
|
foreach ($_SERVER as $name => $value) {
|
||||||
|
if (substr($name, 0, 5) == 'HTTP_') {
|
||||||
|
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read headers
|
||||||
|
$headers = getallheaders();
|
||||||
|
// Get Domain
|
||||||
|
$domain = $headers['Domain'];
|
||||||
|
// Get Username
|
||||||
|
$username = $headers['Username'];
|
||||||
|
// Get From
|
||||||
|
$from = $headers['From'];
|
||||||
|
// define empty footer
|
||||||
|
$empty_footer = json_encode(array(
|
||||||
|
'html' => '',
|
||||||
|
'plain' => '',
|
||||||
|
'skip_replies' => 0,
|
||||||
|
'vars' => array()
|
||||||
|
));
|
||||||
|
|
||||||
|
error_log("FOOTER: checking for domain " . $domain . ", user " . $username . " and address " . $from . PHP_EOL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("SELECT `plain`, `html`, `mbox_exclude`, `skip_replies` FROM `domain_wide_footer`
|
||||||
|
WHERE `domain` = :domain");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':domain' => $domain
|
||||||
|
));
|
||||||
|
$footer = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if (in_array($from, json_decode($footer['mbox_exclude']))){
|
||||||
|
$footer = false;
|
||||||
|
}
|
||||||
|
if (empty($footer)){
|
||||||
|
echo $empty_footer;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
error_log("FOOTER: " . json_encode($footer) . PHP_EOL);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT `custom_attributes` FROM `mailbox` WHERE `username` = :username");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':username' => $username
|
||||||
|
));
|
||||||
|
$custom_attributes = $stmt->fetch(PDO::FETCH_ASSOC)['custom_attributes'];
|
||||||
|
if (empty($custom_attributes)){
|
||||||
|
$custom_attributes = (object)array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
error_log("FOOTER: " . $e->getMessage() . PHP_EOL);
|
||||||
|
http_response_code(502);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// return footer
|
||||||
|
$footer["vars"] = $custom_attributes;
|
||||||
|
echo json_encode($footer);
|
||||||
@@ -8,7 +8,7 @@ VIRUS_FOUND {
|
|||||||
}
|
}
|
||||||
# Bad policy from free mail providers
|
# Bad policy from free mail providers
|
||||||
FREEMAIL_POLICY_FAILURE {
|
FREEMAIL_POLICY_FAILURE {
|
||||||
expression = "-g+:policies & !DMARC_POLICY_ALLOW & !MAILLIST & ( FREEMAIL_ENVFROM | FREEMAIL_FROM ) & !WHITELISTED_FWD_HOST";
|
expression = "FREEMAIL_FROM & !DMARC_POLICY_ALLOW & !MAILLIST& !WHITELISTED_FWD_HOST & -g+:policies";
|
||||||
score = 16.0;
|
score = 16.0;
|
||||||
}
|
}
|
||||||
# Applies to freemail with undisclosed recipients
|
# Applies to freemail with undisclosed recipients
|
||||||
@@ -68,3 +68,39 @@ WL_FWD_HOST {
|
|||||||
ENCRYPTED_CHAT {
|
ENCRYPTED_CHAT {
|
||||||
expression = "CHAT_VERSION_HEADER & ENCRYPTED_PGP";
|
expression = "CHAT_VERSION_HEADER & ENCRYPTED_PGP";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CLAMD_SPAM_FOUND {
|
||||||
|
expression = "CLAM_SECI_SPAM & !MAILCOW_WHITE";
|
||||||
|
description = "Probably Spam, Securite Spam Flag set through ClamAV";
|
||||||
|
score = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAMD_BAD_PDF {
|
||||||
|
expression = "CLAM_SECI_PDF & !MAILCOW_WHITE";
|
||||||
|
description = "Bad PDF Found, Securite bad PDF Flag set through ClamAV";
|
||||||
|
score = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAMD_BAD_JPG {
|
||||||
|
expression = "CLAM_SECI_JPG & !MAILCOW_WHITE";
|
||||||
|
description = "Bad JPG Found, Securite bad JPG Flag set through ClamAV";
|
||||||
|
score = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAMD_ASCII_MALWARE {
|
||||||
|
expression = "CLAM_SECI_ASCII & !MAILCOW_WHITE";
|
||||||
|
description = "ASCII malware found, Securite ASCII malware Flag set through ClamAV";
|
||||||
|
score = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAMD_HTML_MALWARE {
|
||||||
|
expression = "CLAM_SECI_HTML & !MAILCOW_WHITE";
|
||||||
|
description = "HTML malware found, Securite HTML malware Flag set through ClamAV";
|
||||||
|
score = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAMD_JS_MALWARE {
|
||||||
|
expression = "CLAM_SECI_JS & !MAILCOW_WHITE";
|
||||||
|
description = "JS malware found, Securite JS malware Flag set through ClamAV";
|
||||||
|
score = 8;
|
||||||
|
}
|
||||||
@@ -159,8 +159,8 @@ BAZAAR_ABUSE_CH {
|
|||||||
}
|
}
|
||||||
|
|
||||||
URLHAUS_ABUSE_CH {
|
URLHAUS_ABUSE_CH {
|
||||||
type = "url";
|
type = "selector";
|
||||||
filter = "full";
|
selector = "urls";
|
||||||
map = "https://urlhaus.abuse.ch/downloads/text_online/";
|
map = "https://urlhaus.abuse.ch/downloads/text_online/";
|
||||||
score = 10.0;
|
score = 10.0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Uncomment below to apply the ratelimits globally. Use Ratelimits inside mailcow UI to overwrite them for a specific domain/mailbox.
|
||||||
|
# rates {
|
||||||
|
# # Format: "1 / 1h" or "20 / 1m" etc.
|
||||||
|
# to = "100 / 1s";
|
||||||
|
# to_ip = "100 / 1s";
|
||||||
|
# to_ip_from = "100 / 1s";
|
||||||
|
# bounce_to = "100 / 1h";
|
||||||
|
# bounce_to_ip = "7 / 1m";
|
||||||
|
# }
|
||||||
@@ -221,6 +221,16 @@ rspamd_config:register_symbol({
|
|||||||
local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
|
local tagged_rcpt = task:get_symbol("TAGGED_RCPT")
|
||||||
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
|
local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
|
||||||
|
|
||||||
|
local function remove_moo_tag()
|
||||||
|
local moo_tag_header = task:get_header('X-Moo-Tag', false)
|
||||||
|
if moo_tag_header then
|
||||||
|
task:set_milter_reply({
|
||||||
|
remove_headers = {['X-Moo-Tag'] = 0},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then
|
if tagged_rcpt and tagged_rcpt[1].options and mailcow_domain then
|
||||||
local tag = tagged_rcpt[1].options[1]
|
local tag = tagged_rcpt[1].options[1]
|
||||||
rspamd_logger.infox("found tag: %s", tag)
|
rspamd_logger.infox("found tag: %s", tag)
|
||||||
@@ -229,6 +239,7 @@ rspamd_config:register_symbol({
|
|||||||
|
|
||||||
if action ~= 'no action' and action ~= 'greylist' then
|
if action ~= 'no action' and action ~= 'greylist' then
|
||||||
rspamd_logger.infox("skipping tag handler for action: %s", action)
|
rspamd_logger.infox("skipping tag handler for action: %s", action)
|
||||||
|
remove_moo_tag()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -243,6 +254,7 @@ rspamd_config:register_symbol({
|
|||||||
local function tag_callback_subfolder(err, data)
|
local function tag_callback_subfolder(err, data)
|
||||||
if err or type(data) ~= 'string' then
|
if err or type(data) ~= 'string' then
|
||||||
rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
|
rspamd_logger.infox(rspamd_config, "subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
|
||||||
|
remove_moo_tag()
|
||||||
else
|
else
|
||||||
rspamd_logger.infox("Add X-Moo-Tag header")
|
rspamd_logger.infox("Add X-Moo-Tag header")
|
||||||
task:set_milter_reply({
|
task:set_milter_reply({
|
||||||
@@ -261,6 +273,7 @@ rspamd_config:register_symbol({
|
|||||||
)
|
)
|
||||||
if not redis_ret_subfolder then
|
if not redis_ret_subfolder then
|
||||||
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
|
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
|
||||||
|
remove_moo_tag()
|
||||||
end
|
end
|
||||||
|
|
||||||
else
|
else
|
||||||
@@ -268,7 +281,10 @@ rspamd_config:register_symbol({
|
|||||||
local sbj = task:get_header('Subject')
|
local sbj = task:get_header('Subject')
|
||||||
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
|
new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
|
||||||
task:set_milter_reply({
|
task:set_milter_reply({
|
||||||
remove_headers = {['Subject'] = 1},
|
remove_headers = {
|
||||||
|
['Subject'] = 1,
|
||||||
|
['X-Moo-Tag'] = 0
|
||||||
|
},
|
||||||
add_headers = {['Subject'] = new_sbj}
|
add_headers = {['Subject'] = new_sbj}
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
@@ -284,6 +300,7 @@ rspamd_config:register_symbol({
|
|||||||
)
|
)
|
||||||
if not redis_ret_subject then
|
if not redis_ret_subject then
|
||||||
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
|
rspamd_logger.infox(rspamd_config, "cannot make request to load tag handler for rcpt")
|
||||||
|
remove_moo_tag()
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
@@ -295,6 +312,7 @@ rspamd_config:register_symbol({
|
|||||||
if #rcpt_split == 2 then
|
if #rcpt_split == 2 then
|
||||||
if rcpt_split[1] == 'postmaster' then
|
if rcpt_split[1] == 'postmaster' then
|
||||||
rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
|
rspamd_logger.infox(rspamd_config, "not expanding postmaster alias")
|
||||||
|
remove_moo_tag()
|
||||||
else
|
else
|
||||||
rspamd_http.request({
|
rspamd_http.request({
|
||||||
task=task,
|
task=task,
|
||||||
@@ -307,7 +325,8 @@ rspamd_config:register_symbol({
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
remove_moo_tag()
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
priority = 19
|
priority = 19
|
||||||
@@ -340,6 +359,10 @@ rspamd_config:register_symbol({
|
|||||||
if not bcc_dest then
|
if not bcc_dest then
|
||||||
return -- stop
|
return -- stop
|
||||||
end
|
end
|
||||||
|
-- dot stuff content before sending
|
||||||
|
local email_content = tostring(task:get_content())
|
||||||
|
email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
|
||||||
|
-- send mail
|
||||||
lua_smtp.sendmail({
|
lua_smtp.sendmail({
|
||||||
task = task,
|
task = task,
|
||||||
host = os.getenv("IPV4_NETWORK") .. '.253',
|
host = os.getenv("IPV4_NETWORK") .. '.253',
|
||||||
@@ -347,8 +370,8 @@ rspamd_config:register_symbol({
|
|||||||
from = task:get_from(stp)[1].addr,
|
from = task:get_from(stp)[1].addr,
|
||||||
recipients = bcc_dest,
|
recipients = bcc_dest,
|
||||||
helo = 'bcc',
|
helo = 'bcc',
|
||||||
timeout = 10,
|
timeout = 20,
|
||||||
}, task:get_content(), sendmail_cb)
|
}, email_content, sendmail_cb)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- determine from
|
-- determine from
|
||||||
@@ -499,3 +522,166 @@ rspamd_config:register_symbol({
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
})
|
})
|
||||||
|
|
||||||
|
rspamd_config:register_symbol({
|
||||||
|
name = 'MOO_FOOTER',
|
||||||
|
type = 'prefilter',
|
||||||
|
callback = function(task)
|
||||||
|
local cjson = require "cjson"
|
||||||
|
local lua_mime = require "lua_mime"
|
||||||
|
local lua_util = require "lua_util"
|
||||||
|
local rspamd_logger = require "rspamd_logger"
|
||||||
|
local rspamd_http = require "rspamd_http"
|
||||||
|
local envfrom = task:get_from(1)
|
||||||
|
local uname = task:get_user()
|
||||||
|
if not envfrom or not uname then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local uname = uname:lower()
|
||||||
|
local env_from_domain = envfrom[1].domain:lower()
|
||||||
|
local env_from_addr = envfrom[1].addr:lower()
|
||||||
|
|
||||||
|
-- determine newline type
|
||||||
|
local function newline(task)
|
||||||
|
local t = task:get_newlines_type()
|
||||||
|
|
||||||
|
if t == 'cr' then
|
||||||
|
return '\r'
|
||||||
|
elseif t == 'lf' then
|
||||||
|
return '\n'
|
||||||
|
end
|
||||||
|
|
||||||
|
return '\r\n'
|
||||||
|
end
|
||||||
|
-- retrieve footer
|
||||||
|
local function footer_cb(err_message, code, data, headers)
|
||||||
|
if err or type(data) ~= 'string' then
|
||||||
|
rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
|
||||||
|
else
|
||||||
|
|
||||||
|
-- parse json string
|
||||||
|
local footer = cjson.decode(data)
|
||||||
|
if not footer then
|
||||||
|
rspamd_logger.infox(rspamd_config, "parsing domain wide footer for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
|
||||||
|
else
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 from_name = ""
|
||||||
|
if envfrom_mime and envfrom_mime[1].name then
|
||||||
|
from_name = envfrom_mime[1].name
|
||||||
|
elseif envfrom and envfrom[1].name then
|
||||||
|
from_name = envfrom[1].name
|
||||||
|
end
|
||||||
|
|
||||||
|
-- default replacements
|
||||||
|
local replacements = {
|
||||||
|
auth_user = uname,
|
||||||
|
from_user = envfrom[1].user,
|
||||||
|
from_name = from_name,
|
||||||
|
from_addr = envfrom[1].addr,
|
||||||
|
from_domain = envfrom[1].domain:lower()
|
||||||
|
}
|
||||||
|
-- add custom mailbox attributes
|
||||||
|
if footer.vars and type(footer.vars) == "string" then
|
||||||
|
local footer_vars = cjson.decode(footer.vars)
|
||||||
|
|
||||||
|
if type(footer_vars) == "table" then
|
||||||
|
for key, value in pairs(footer_vars) do
|
||||||
|
replacements[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if footer.html and footer.html ~= "" then
|
||||||
|
footer.html = lua_util.jinja_template(footer.html, replacements, true)
|
||||||
|
end
|
||||||
|
if footer.plain and footer.plain ~= "" then
|
||||||
|
footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- add footer
|
||||||
|
local out = {}
|
||||||
|
local rewrite = lua_mime.add_text_footer(task, footer.html, footer.plain) or {}
|
||||||
|
|
||||||
|
local seen_cte
|
||||||
|
local newline_s = newline(task)
|
||||||
|
|
||||||
|
local function rewrite_ct_cb(name, hdr)
|
||||||
|
if rewrite.need_rewrite_ct then
|
||||||
|
if name:lower() == 'content-type' then
|
||||||
|
local nct = string.format('%s: %s/%s; charset=utf-8',
|
||||||
|
'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype)
|
||||||
|
out[#out + 1] = nct
|
||||||
|
return
|
||||||
|
elseif name:lower() == 'content-transfer-encoding' then
|
||||||
|
out[#out + 1] = string.format('%s: %s',
|
||||||
|
'Content-Transfer-Encoding', 'quoted-printable')
|
||||||
|
seen_cte = true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
out[#out + 1] = hdr.raw:gsub('\r?\n?$', '')
|
||||||
|
end
|
||||||
|
|
||||||
|
task:headers_foreach(rewrite_ct_cb, {full = true})
|
||||||
|
|
||||||
|
if not seen_cte and rewrite.need_rewrite_ct then
|
||||||
|
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- End of headers
|
||||||
|
out[#out + 1] = newline_s
|
||||||
|
|
||||||
|
if rewrite.out then
|
||||||
|
for _,o in ipairs(rewrite.out) do
|
||||||
|
out[#out + 1] = o
|
||||||
|
end
|
||||||
|
else
|
||||||
|
out[#out + 1] = task:get_rawbody()
|
||||||
|
end
|
||||||
|
local out_parts = {}
|
||||||
|
for _,o in ipairs(out) do
|
||||||
|
if type(o) ~= 'table' then
|
||||||
|
out_parts[#out_parts + 1] = o
|
||||||
|
out_parts[#out_parts + 1] = newline_s
|
||||||
|
else
|
||||||
|
local removePrefix = "--\x0D\x0AContent-Type"
|
||||||
|
if string.lower(string.sub(tostring(o[1]), 1, string.len(removePrefix))) == string.lower(removePrefix) then
|
||||||
|
o[1] = string.sub(tostring(o[1]), string.len("--\x0D\x0A") + 1)
|
||||||
|
end
|
||||||
|
out_parts[#out_parts + 1] = o[1]
|
||||||
|
if o[2] then
|
||||||
|
out_parts[#out_parts + 1] = newline_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
task:set_message(out_parts)
|
||||||
|
else
|
||||||
|
rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\")", uname, data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- fetch footer
|
||||||
|
rspamd_http.request({
|
||||||
|
task=task,
|
||||||
|
url='http://nginx:8081/footer.php',
|
||||||
|
body='',
|
||||||
|
callback=footer_cb,
|
||||||
|
headers={Domain=env_from_domain,Username=uname,From=env_from_addr},
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
priority = 1
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
rates {
|
|
||||||
# Format: "1 / 1h" or "20 / 1m" etc. - global ratelimits are disabled by default
|
|
||||||
to = "100 / 1s";
|
|
||||||
to_ip = "100 / 1s";
|
|
||||||
to_ip_from = "100 / 1s";
|
|
||||||
bounce_to = "100 / 1h";
|
|
||||||
bounce_to_ip = "7 / 1m";
|
|
||||||
}
|
|
||||||
whitelisted_rcpts = "postmaster,mailer-daemon";
|
whitelisted_rcpts = "postmaster,mailer-daemon";
|
||||||
max_rcpt = 25;
|
max_rcpt = 25;
|
||||||
custom_keywords = "/etc/rspamd/lua/ratelimit.lua";
|
custom_keywords = "/etc/rspamd/lua/ratelimit.lua";
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
SOGoJunkFolderName= "Junk";
|
SOGoJunkFolderName= "Junk";
|
||||||
SOGoMailDomain = "sogo.local";
|
SOGoMailDomain = "sogo.local";
|
||||||
SOGoEnableEMailAlarms = YES;
|
SOGoEnableEMailAlarms = YES;
|
||||||
|
SOGoMailHideInlineAttachments = YES;
|
||||||
SOGoFoldersSendEMailNotifications = YES;
|
SOGoFoldersSendEMailNotifications = YES;
|
||||||
SOGoForwardEnabled = YES;
|
SOGoForwardEnabled = YES;
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@
|
|||||||
SOGoFirstDayOfWeek = "1";
|
SOGoFirstDayOfWeek = "1";
|
||||||
|
|
||||||
SOGoSieveFolderEncoding = "UTF-8";
|
SOGoSieveFolderEncoding = "UTF-8";
|
||||||
SOGoPasswordChangeEnabled = YES;
|
SOGoPasswordChangeEnabled = NO;
|
||||||
SOGoSentFolderName = "Sent";
|
SOGoSentFolderName = "Sent";
|
||||||
SOGoMailShowSubscribedFoldersOnly = NO;
|
SOGoMailShowSubscribedFoldersOnly = NO;
|
||||||
NGImap4ConnectionStringSeparator = "/";
|
NGImap4ConnectionStringSeparator = "/";
|
||||||
@@ -83,6 +84,7 @@
|
|||||||
//SoDebugBaseURL = YES;
|
//SoDebugBaseURL = YES;
|
||||||
//ImapDebugEnabled = YES;
|
//ImapDebugEnabled = YES;
|
||||||
//SOGoEASDebugEnabled = YES;
|
//SOGoEASDebugEnabled = YES;
|
||||||
|
SOGoEASSearchInBody = YES; // Experimental. Enabled since 2023-10
|
||||||
//LDAPDebugEnabled = YES;
|
//LDAPDebugEnabled = YES;
|
||||||
//PGDebugEnabled = YES;
|
//PGDebugEnabled = YES;
|
||||||
//MySQL4DebugEnabled = YES;
|
//MySQL4DebugEnabled = YES;
|
||||||
|
|||||||
@@ -20,6 +20,6 @@
|
|||||||
<pre>BACKUP_LOCATION=/tmp/ ./helper-scripts/backup_and_restore.sh backup all</pre>
|
<pre>BACKUP_LOCATION=/tmp/ ./helper-scripts/backup_and_restore.sh backup all</pre>
|
||||||
<pre>docker compose down --volumes ; docker compose up -d</pre>
|
<pre>docker compose down --volumes ; docker compose up -d</pre>
|
||||||
<p>Make sure your timezone is correct. Use "America/New_York" for example, do not use spaces. Check <a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">here</a> for a list.</p>
|
<p>Make sure your timezone is correct. Use "America/New_York" for example, do not use spaces. Check <a href="https://en.wikipedia.org/wiki/List_of_tz_database_time_zones">here</a> for a list.</p>
|
||||||
<br>Click to learn more about <a style="color:red;text-decoration:none;" href="https://mailcow.github.io/mailcow-dockerized-docs/#get-support" target="_blank">getting support.</a>
|
<br>Click to learn more about <a style="color:red;text-decoration:none;" href="https://docs.mailcow.email/#get-support" target="_blank">getting support.</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+12
-1
@@ -80,6 +80,13 @@ foreach ($RSPAMD_MAPS['regex'] as $rspamd_regex_desc => $rspamd_regex_map) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cors settings
|
||||||
|
$cors_settings = cors('get');
|
||||||
|
$cors_settings['allowed_origins'] = str_replace(", ", "\n", $cors_settings['allowed_origins']);
|
||||||
|
$cors_settings['allowed_methods'] = explode(", ", $cors_settings['allowed_methods']);
|
||||||
|
|
||||||
|
$f2b_data = fail2ban('get');
|
||||||
|
|
||||||
$template = 'admin.twig';
|
$template = 'admin.twig';
|
||||||
$template_data = [
|
$template_data = [
|
||||||
'tfa_data' => $tfa_data,
|
'tfa_data' => $tfa_data,
|
||||||
@@ -96,16 +103,20 @@ $template_data = [
|
|||||||
'domains' => $domains,
|
'domains' => $domains,
|
||||||
'all_domains' => $all_domains,
|
'all_domains' => $all_domains,
|
||||||
'mailboxes' => $mailboxes,
|
'mailboxes' => $mailboxes,
|
||||||
'f2b_data' => fail2ban('get'),
|
'f2b_data' => $f2b_data,
|
||||||
|
'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'),
|
||||||
'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,
|
||||||
'logo_specs' => customize('get', 'main_logo_specs'),
|
'logo_specs' => customize('get', 'main_logo_specs'),
|
||||||
|
'logo_dark_specs' => customize('get', 'main_logo_dark_specs'),
|
||||||
'ip_check' => customize('get', 'ip_check'),
|
'ip_check' => customize('get', 'ip_check'),
|
||||||
'password_complexity' => password_complexity('get'),
|
'password_complexity' => password_complexity('get'),
|
||||||
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
|
||||||
|
'cors_settings' => $cors_settings,
|
||||||
|
'is_https' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on',
|
||||||
'lang_admin' => json_encode($lang['admin']),
|
'lang_admin' => json_encode($lang['admin']),
|
||||||
'lang_datatables' => json_encode($lang['datatables'])
|
'lang_datatables' => json_encode($lang['datatables'])
|
||||||
];
|
];
|
||||||
|
|||||||
+289
-4
@@ -1,4 +1,4 @@
|
|||||||
openapi: 3.0.0
|
openapi: 3.1.0
|
||||||
info:
|
info:
|
||||||
description: >-
|
description: >-
|
||||||
mailcow is complete e-mailing solution with advanced antispam, antivirus,
|
mailcow is complete e-mailing solution with advanced antispam, antivirus,
|
||||||
@@ -699,6 +699,38 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
summary: Create Domain Admin user
|
summary: Create Domain Admin user
|
||||||
|
/api/v1/add/sso/domain-admin:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
examples:
|
||||||
|
response:
|
||||||
|
value:
|
||||||
|
token: "591F6D-5C3DD2-7455CD-DAF1C1-AA4FCC"
|
||||||
|
description: OK
|
||||||
|
headers: { }
|
||||||
|
tags:
|
||||||
|
- Single Sign-On
|
||||||
|
description: >-
|
||||||
|
Using this endpoint you can issue a token for Domain Admin user. This token can be used for
|
||||||
|
autologin Domain Admin user by using query_string var sso_token={token}. Token expiration time is 30s
|
||||||
|
operationId: Issue Domain Admin SSO token
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
example:
|
||||||
|
username: testadmin
|
||||||
|
properties:
|
||||||
|
username:
|
||||||
|
description: the username for the admin user
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
summary: Issue Domain Admin SSO token
|
||||||
/api/v1/edit/da-acl:
|
/api/v1/edit/da-acl:
|
||||||
post:
|
post:
|
||||||
responses:
|
responses:
|
||||||
@@ -1999,7 +2031,7 @@ paths:
|
|||||||
- domain.tld
|
- domain.tld
|
||||||
- domain2.tld
|
- domain2.tld
|
||||||
properties:
|
properties:
|
||||||
items:
|
items:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
@@ -2993,7 +3025,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
log:
|
log:
|
||||||
@@ -3105,6 +3137,86 @@ paths:
|
|||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
summary: Update domain
|
summary: Update domain
|
||||||
|
/api/v1/edit/domain/footer:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
examples:
|
||||||
|
response:
|
||||||
|
value:
|
||||||
|
- log:
|
||||||
|
- mailbox
|
||||||
|
- edit
|
||||||
|
- domain_wide_footer
|
||||||
|
- domains:
|
||||||
|
- mailcow.tld
|
||||||
|
html: "<br>foo {= foo =}"
|
||||||
|
plain: "<foo {= foo =}"
|
||||||
|
mbox_exclude:
|
||||||
|
- moo@mailcow.tld
|
||||||
|
- null
|
||||||
|
msg:
|
||||||
|
- domain_footer_modified
|
||||||
|
- mailcow.tld
|
||||||
|
type: success
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
log:
|
||||||
|
description: contains request object
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
msg:
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
type:
|
||||||
|
enum:
|
||||||
|
- success
|
||||||
|
- danger
|
||||||
|
- error
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
description: OK
|
||||||
|
headers: {}
|
||||||
|
tags:
|
||||||
|
- Domains
|
||||||
|
description: >-
|
||||||
|
You can update the footer of one or more domains per request.
|
||||||
|
operationId: Update domain wide footer
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
example:
|
||||||
|
attr:
|
||||||
|
html: "<br>foo {= foo =}"
|
||||||
|
plain: "foo {= foo =}"
|
||||||
|
mbox_exclude:
|
||||||
|
- moo@mailcow.tld
|
||||||
|
items: mailcow.tld
|
||||||
|
properties:
|
||||||
|
attr:
|
||||||
|
properties:
|
||||||
|
html:
|
||||||
|
description: Footer text in HTML format
|
||||||
|
type: string
|
||||||
|
plain:
|
||||||
|
description: Footer text in PLAIN text format
|
||||||
|
type: string
|
||||||
|
mbox_exclude:
|
||||||
|
description: Array of mailboxes to exclude from domain wide footer
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
items:
|
||||||
|
description: contains a list of domain names where you want to update the footer
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
summary: Update domain wide footer
|
||||||
/api/v1/edit/fail2ban:
|
/api/v1/edit/fail2ban:
|
||||||
post:
|
post:
|
||||||
responses:
|
responses:
|
||||||
@@ -3144,8 +3256,10 @@ paths:
|
|||||||
example:
|
example:
|
||||||
attr:
|
attr:
|
||||||
ban_time: "86400"
|
ban_time: "86400"
|
||||||
|
ban_time_increment: "1"
|
||||||
blacklist: "10.100.6.5/32,10.100.8.4/32"
|
blacklist: "10.100.6.5/32,10.100.8.4/32"
|
||||||
max_attempts: "5"
|
max_attempts: "5"
|
||||||
|
max_ban_time: "86400"
|
||||||
netban_ipv4: "24"
|
netban_ipv4: "24"
|
||||||
netban_ipv6: "64"
|
netban_ipv6: "64"
|
||||||
retry_window: "600"
|
retry_window: "600"
|
||||||
@@ -3159,11 +3273,17 @@ paths:
|
|||||||
description: the backlisted ips or hostnames separated by comma
|
description: the backlisted ips or hostnames separated by comma
|
||||||
type: string
|
type: string
|
||||||
ban_time:
|
ban_time:
|
||||||
description: the time a ip should be banned
|
description: the time an ip should be banned
|
||||||
type: number
|
type: number
|
||||||
|
ban_time_increment:
|
||||||
|
description: if the time of the ban should increase each time
|
||||||
|
type: boolean
|
||||||
max_attempts:
|
max_attempts:
|
||||||
description: the maximum numbe of wrong logins before a ip is banned
|
description: the maximum numbe of wrong logins before a ip is banned
|
||||||
type: number
|
type: number
|
||||||
|
max_ban_time:
|
||||||
|
description: the maximum time an ip should be banned
|
||||||
|
type: number
|
||||||
netban_ipv4:
|
netban_ipv4:
|
||||||
description: the networks mask to ban for ipv4
|
description: the networks mask to ban for ipv4
|
||||||
type: number
|
type: number
|
||||||
@@ -3296,6 +3416,86 @@ paths:
|
|||||||
type: object
|
type: object
|
||||||
type: object
|
type: object
|
||||||
summary: Update mailbox
|
summary: Update mailbox
|
||||||
|
/api/v1/edit/mailbox/custom-attribute:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
examples:
|
||||||
|
response:
|
||||||
|
value:
|
||||||
|
- log:
|
||||||
|
- mailbox
|
||||||
|
- edit
|
||||||
|
- mailbox_custom_attribute
|
||||||
|
- mailboxes:
|
||||||
|
- moo@mailcow.tld
|
||||||
|
attribute:
|
||||||
|
- role
|
||||||
|
- foo
|
||||||
|
value:
|
||||||
|
- cow
|
||||||
|
- bar
|
||||||
|
- null
|
||||||
|
msg:
|
||||||
|
- mailbox_modified
|
||||||
|
- moo@mailcow.tld
|
||||||
|
type: success
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
log:
|
||||||
|
description: contains request object
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
msg:
|
||||||
|
items: {}
|
||||||
|
type: array
|
||||||
|
type:
|
||||||
|
enum:
|
||||||
|
- success
|
||||||
|
- danger
|
||||||
|
- error
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
description: OK
|
||||||
|
headers: {}
|
||||||
|
tags:
|
||||||
|
- Mailboxes
|
||||||
|
description: >-
|
||||||
|
You can update custom attributes of one or more mailboxes per request.
|
||||||
|
operationId: Update mailbox custom attributes
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
example:
|
||||||
|
attr:
|
||||||
|
attribute:
|
||||||
|
- role
|
||||||
|
- foo
|
||||||
|
value:
|
||||||
|
- cow
|
||||||
|
- bar
|
||||||
|
items:
|
||||||
|
- moo@mailcow.tld
|
||||||
|
properties:
|
||||||
|
attr:
|
||||||
|
properties:
|
||||||
|
attribute:
|
||||||
|
description: Array of attribute keys
|
||||||
|
type: object
|
||||||
|
value:
|
||||||
|
description: Array of attribute values
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
items:
|
||||||
|
description: contains list of mailboxes you want update
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
summary: Update mailbox custom attributes
|
||||||
/api/v1/edit/mailq:
|
/api/v1/edit/mailq:
|
||||||
post:
|
post:
|
||||||
responses:
|
responses:
|
||||||
@@ -4081,10 +4281,12 @@ paths:
|
|||||||
response:
|
response:
|
||||||
value:
|
value:
|
||||||
ban_time: 604800
|
ban_time: 604800
|
||||||
|
ban_time_increment: 1
|
||||||
blacklist: |-
|
blacklist: |-
|
||||||
45.82.153.37/32
|
45.82.153.37/32
|
||||||
92.118.38.52/32
|
92.118.38.52/32
|
||||||
max_attempts: 1
|
max_attempts: 1
|
||||||
|
max_ban_time: 604800
|
||||||
netban_ipv4: 32
|
netban_ipv4: 32
|
||||||
netban_ipv6: 128
|
netban_ipv6: 128
|
||||||
perm_bans:
|
perm_bans:
|
||||||
@@ -5539,6 +5741,7 @@ paths:
|
|||||||
sogo_access: "1"
|
sogo_access: "1"
|
||||||
tls_enforce_in: "0"
|
tls_enforce_in: "0"
|
||||||
tls_enforce_out: "0"
|
tls_enforce_out: "0"
|
||||||
|
custom_attributes: {}
|
||||||
domain: domain3.tld
|
domain: domain3.tld
|
||||||
is_relayed: 0
|
is_relayed: 0
|
||||||
local_part: info
|
local_part: info
|
||||||
@@ -5560,6 +5763,84 @@ paths:
|
|||||||
description: You can list all mailboxes existing in system for a specific domain.
|
description: You can list all mailboxes existing in system for a specific domain.
|
||||||
operationId: Get mailboxes of a domain
|
operationId: Get mailboxes of a domain
|
||||||
summary: Get mailboxes of a domain
|
summary: Get mailboxes of a domain
|
||||||
|
/api/v1/edit/cors:
|
||||||
|
post:
|
||||||
|
responses:
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
examples:
|
||||||
|
response:
|
||||||
|
value:
|
||||||
|
- type: "success"
|
||||||
|
log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
|
||||||
|
msg: "cors_headers_edited"
|
||||||
|
description: OK
|
||||||
|
headers: { }
|
||||||
|
tags:
|
||||||
|
- Cross-Origin Resource Sharing (CORS)
|
||||||
|
description: >-
|
||||||
|
This endpoint allows you to manage Cross-Origin Resource Sharing (CORS) settings for the API.
|
||||||
|
CORS is a security feature implemented by web browsers to prevent unauthorized cross-origin requests.
|
||||||
|
By editing the CORS settings, you can specify which domains and which methods are permitted to access the API resources from outside the mailcow domain.
|
||||||
|
operationId: Edit Cross-Origin Resource Sharing (CORS) settings
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
example:
|
||||||
|
attr:
|
||||||
|
allowed_origins: ["*", "mail.mailcow.tld"]
|
||||||
|
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
|
||||||
|
properties:
|
||||||
|
attr:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
allowed_origins:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
allowed_methods:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
summary: Edit Cross-Origin Resource Sharing (CORS) settings
|
||||||
|
"/api/v1/get/spam-score/{mailbox}":
|
||||||
|
get:
|
||||||
|
parameters:
|
||||||
|
- description: name of mailbox or empty for current user - admin user will retrieve the global spam filter score
|
||||||
|
in: path
|
||||||
|
name: mailbox
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- description: e.g. api-key-string
|
||||||
|
example: api-key-string
|
||||||
|
in: header
|
||||||
|
name: X-API-Key
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"401":
|
||||||
|
$ref: "#/components/responses/Unauthorized"
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
examples:
|
||||||
|
response:
|
||||||
|
value:
|
||||||
|
spam_score: "8,15"
|
||||||
|
description: OK
|
||||||
|
headers: {}
|
||||||
|
tags:
|
||||||
|
- Mailboxes
|
||||||
|
description: >-
|
||||||
|
Using this endpoint you can get the global spam filter score or the spam filter score of a certain mailbox.
|
||||||
|
operationId: Get mailbox or global spam filter score
|
||||||
|
summary: Get mailbox or global spam filter score
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
- name: Domains
|
- name: Domains
|
||||||
@@ -5586,6 +5867,8 @@ tags:
|
|||||||
description: Manage DKIM keys
|
description: Manage DKIM keys
|
||||||
- name: Domain admin
|
- name: Domain admin
|
||||||
description: Create or udpdate domain admin users
|
description: Create or udpdate domain admin users
|
||||||
|
- name: Single Sign-On
|
||||||
|
description: Issue tokens for users
|
||||||
- name: Address Rewriting
|
- name: Address Rewriting
|
||||||
description: Create BCC maps or recipient maps
|
description: Create BCC maps or recipient maps
|
||||||
- name: Outgoing TLS Policy Map Overrides
|
- name: Outgoing TLS Policy Map Overrides
|
||||||
@@ -5602,3 +5885,5 @@ tags:
|
|||||||
description: Get the status of your cow
|
description: Get the status of your cow
|
||||||
- name: Ratelimits
|
- name: Ratelimits
|
||||||
description: Edit domain ratelimits
|
description: Edit domain ratelimits
|
||||||
|
- name: Cross-Origin Resource Sharing (CORS)
|
||||||
|
description: Manage Cross-Origin Resource Sharing (CORS) settings
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
// Begin Swagger UI call region
|
// Begin Swagger UI call region
|
||||||
const ui = SwaggerUIBundle({
|
window.ui = SwaggerUIBundle({
|
||||||
urls: [{url: "/api/openapi.yaml", name: "mailcow API"}],
|
urls: [{url: "/api/openapi.yaml", name: "mailcow API"}],
|
||||||
dom_id: '#swagger-ui',
|
dom_id: '#swagger-ui',
|
||||||
deepLinking: true,
|
deepLinking: true,
|
||||||
@@ -15,5 +15,4 @@ window.onload = function() {
|
|||||||
});
|
});
|
||||||
// End Swagger UI call region
|
// End Swagger UI call region
|
||||||
|
|
||||||
window.ui = ui;
|
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -342,6 +342,10 @@ div.dataTables_wrapper div.dt-row {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.dataTables_wrapper span.sorting-value {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
div.dataTables_scrollHead table.dataTable {
|
div.dataTables_scrollHead table.dataTable {
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,11 +42,6 @@ table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before,
|
|||||||
table.dataTable td.dt-control:before {
|
table.dataTable td.dt-control:before {
|
||||||
background-color: #979797 !important;
|
background-color: #979797 !important;
|
||||||
}
|
}
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
|
|
||||||
background-color: #fbfbfb;
|
|
||||||
}
|
|
||||||
table.dataTable.table-striped>tbody>tr>td {
|
table.dataTable.table-striped>tbody>tr>td {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
@@ -77,4 +72,22 @@ li .dtr-data {
|
|||||||
table.dataTable>tbody>tr.child span.dtr-title {
|
table.dataTable>tbody>tr.child span.dtr-title {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
div.dataTables_wrapper div.dataTables_filter {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
div.dataTables_wrapper div.dataTables_length {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.dataTables_paginate, .dataTables_length, .dataTables_filter {
|
||||||
|
margin: 10px 0!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.dt-text-right {
|
||||||
|
text-align: end !important;
|
||||||
|
}
|
||||||
|
th.dt-text-right {
|
||||||
|
text-align: end !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -357,6 +357,7 @@ button[aria-expanded='true'] > .caret {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.progress {
|
.progress {
|
||||||
|
height: 16px;
|
||||||
background-color: #d5d5d5;
|
background-color: #d5d5d5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,14 +371,22 @@ button[aria-expanded='true'] > .caret {
|
|||||||
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
|
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
|
||||||
background-color: #f0f0f0 !important;
|
background-color: #f0f0f0 !important;
|
||||||
}
|
}
|
||||||
|
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
|
||||||
|
color: #fff;
|
||||||
div.dataTables_wrapper div.dataTables_filter {
|
background-color: #555;
|
||||||
text-align: left;
|
background-image: none;
|
||||||
|
border-color: #4d4d4d;
|
||||||
}
|
}
|
||||||
div.dataTables_wrapper div.dataTables_length {
|
.btn-check:checked+.btn-light:focus, .btn-check:active+.btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, .show>.btn-light.dropdown-toggle:focus,
|
||||||
text-align: right;
|
.btn-check:focus+.btn-light, .btn-light:focus {
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.dataTables_paginate, .dataTables_length, .dataTables_filter {
|
.btn-group>.btn:not(:last-of-type) {
|
||||||
margin: 10px 0!important;
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
.badge.bg-info > a,
|
||||||
|
.badge.bg-danger > a {
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.responsive-tabs .tab-pane {
|
.responsive-tabs .tab-pane:not(.rsettings) {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@@ -203,6 +203,22 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.senders-mw220 {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
|
||||||
|
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before,
|
||||||
|
table.dataTable td.dt-control:before {
|
||||||
|
height: 2rem;
|
||||||
|
width: 2rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
margin-top: -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li .dtr-data {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 350px) {
|
@media (max-width: 350px) {
|
||||||
|
|||||||
@@ -66,4 +66,6 @@ table tbody tr td input[type="checkbox"] {
|
|||||||
padding: .2em .4em .3em !important;
|
padding: .2em .4em .3em !important;
|
||||||
background-color: #ececec!important;
|
background-color: #ececec!important;
|
||||||
}
|
}
|
||||||
|
.badge.bg-info .bi {
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|||||||
+104
-102
@@ -1,102 +1,104 @@
|
|||||||
.pagination a {
|
.pagination a {
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel.panel-default {
|
.panel.panel-default {
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-responsive {
|
.table-responsive {
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-responsive {
|
.table-responsive {
|
||||||
overflow-x: scroll !important;
|
overflow-x: scroll !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-add-item {
|
.footer-add-item {
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: #F5F5F5;
|
background: #F5F5F5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: 1920px) {
|
@media (min-width: 1920px) {
|
||||||
.container {
|
.container {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mass-actions-quarantine {
|
.mass-actions-quarantine {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputMissingAttr {
|
.inputMissingAttr {
|
||||||
border-color: #FF4136;
|
border-color: #FF4136;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal#qidDetailModal p {
|
.modal#qidDetailModal p {
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
span#qid_detail_score {
|
span#qid_detail_score {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.rspamd-symbol {
|
span.rspamd-symbol {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 2px 6px 2px 0;
|
margin: 2px 6px 2px 0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0 7px;
|
padding: 0 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.rspamd-symbol.positive {
|
span.rspamd-symbol.positive {
|
||||||
background: #4CAF50;
|
background: #4CAF50;
|
||||||
border: 1px solid #4CAF50;
|
border: 1px solid #4CAF50;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.rspamd-symbol.negative {
|
span.rspamd-symbol.negative {
|
||||||
background: #ff4136;
|
background: #ff4136;
|
||||||
border: 1px solid #ff4136;
|
border: 1px solid #ff4136;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.rspamd-symbol.neutral {
|
span.rspamd-symbol.neutral {
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
color: #333;
|
color: #333;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.rspamd-symbol span.score {
|
span.rspamd-symbol span.score {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.mail-address-item {
|
span.mail-address-item {
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
padding: 2px 7px;
|
padding: 2px 7px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 2px 6px 2px 0;
|
margin: 2px 6px 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
table tbody tr {
|
table tbody tr {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
table tbody tr td input[type="checkbox"] {
|
table tbody tr td input[type="checkbox"] {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.label-rspamd-action {
|
.label-rspamd-action {
|
||||||
font-size:110%;
|
font-size:110%;
|
||||||
margin:20px;
|
margin:20px;
|
||||||
}
|
}
|
||||||
|
.senders-mw220 {
|
||||||
|
max-width: 220px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,85 +1,128 @@
|
|||||||
body {
|
body {
|
||||||
background-color: #414141;
|
background-color: #1c1c1e;
|
||||||
color: #e0e0e0;
|
color: #f2f2f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
border: 1px solid #1c1c1c;
|
border: 1px solid #2c2c2e;
|
||||||
background-color: #3a3a3a;
|
background-color: #2c2c2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
legend {
|
legend {
|
||||||
color: #f5f5f5;
|
color: #f2f2f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
color: #bbb;
|
color: #8e8e93;
|
||||||
background-color: #2c2c2c;
|
background-color: #1c1c1e;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
--bs-card-color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-secondary, .paginate_button, .page-link, .btn-light {
|
.btn-secondary, .paginate_button, .page-link, .btn-light {
|
||||||
color: #fff !important;
|
color: #f2f2f7 !important;
|
||||||
background-color: #7a7a7a !important;
|
background-color: #5e5e5e !important;
|
||||||
border-color: #5c5c5c !important;
|
border-color: #4c4c4e !important;
|
||||||
}
|
}
|
||||||
.btn-check:checked+.btn-secondary, .btn-check:active+.btn-secondary, .btn-secondary:active, .btn-secondary.active, .show>.btn-secondary.dropdown-toggle {
|
|
||||||
border-color: #7a7a7a !important;
|
.btn-dark {
|
||||||
}
|
color: #f2f2f7 !important;
|
||||||
.alert-secondary {
|
background-color: #242424 !important;
|
||||||
color: #fff !important;
|
border-color: #1c1c1e !important;
|
||||||
background-color: #7a7a7a !important;
|
|
||||||
border-color: #5c5c5c !important;
|
|
||||||
}
|
|
||||||
.bg-secondary {
|
|
||||||
color: #fff !important;
|
|
||||||
background-color: #7a7a7a !important;
|
|
||||||
}
|
|
||||||
.alert-secondary, .alert-secondary a, .alert-secondary .alert-link {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.page-item.active .page-link {
|
|
||||||
background-color: #158cba !important;
|
|
||||||
border-color: #127ba3 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary:focus, .btn-secondary:hover, .btn-group.open .dropdown-toggle.btn-secondary {
|
.btn-secondary:focus, .btn-secondary:hover, .btn-group.open .dropdown-toggle.btn-secondary {
|
||||||
background-color: #7a7a7a;
|
background-color: #444444;
|
||||||
border-color: #5c5c5c !important;
|
border-color: #4c4c4e !important;
|
||||||
color: #fff;
|
color: #f2f2f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-check:checked+.btn-secondary, .btn-check:active+.btn-secondary, .btn-secondary:active, .btn-secondary.active, .show>.btn-secondary.dropdown-toggle {
|
||||||
|
border-color: #5e5e5e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-secondary {
|
||||||
|
color: #f2f2f7 !important;
|
||||||
|
background-color: #5e5e5e !important;
|
||||||
|
border-color: #4c4c4e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-secondary {
|
||||||
|
color: #f2f2f7 !important;
|
||||||
|
background-color: #5e5e5e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-secondary, .alert-secondary a, .alert-secondary .alert-link {
|
||||||
|
color: #f2f2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-item.active .page-link {
|
||||||
|
background-color: #3e3e3e !important;
|
||||||
|
border-color: #3e3e3e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:focus, .btn-secondary:hover, .btn-group.open .dropdown-toggle.btn-secondary {
|
||||||
|
background-color: #5e5e5e;
|
||||||
|
border-color: #4c4c4e !important;
|
||||||
|
color: #f2f2f7;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-secondary:disabled, .btn-secondary.disabled {
|
.btn-secondary:disabled, .btn-secondary.disabled {
|
||||||
border-color: #7a7a7a !important;
|
border-color: #5e5e5e !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background-color: #414141;
|
--bs-modal-color: #bbb;
|
||||||
|
background-color: #2c2c2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
border-bottom: 1px solid #161616;
|
border-bottom: 1px solid #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
color: white;
|
color: #bbb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal .btn-close {
|
.modal .btn-close {
|
||||||
filter: invert(1) grayscale(100%) brightness(200%);
|
filter: invert(1) grayscale(100%) brightness(200%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar.bg-light {
|
.navbar.bg-light {
|
||||||
background-color: #222222 !important;
|
background-color: #1c1c1e !important;
|
||||||
border-color: #181818;
|
border-color: #2c2c2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
color: #ccc !important;
|
color: #8e8e93 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
|
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-tabs, .nav-tabs .nav-link {
|
||||||
|
border-color: #444444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-tabs .nav-link:not(.disabled):hover, .nav-tabs .nav-link:not(.disabled):focus, .nav-tabs .nav-link.active {
|
.nav-tabs .nav-link:not(.disabled):hover, .nav-tabs .nav-link:not(.disabled):focus, .nav-tabs .nav-link.active {
|
||||||
border-bottom-color: #414141;
|
border-bottom-color: #1c1c1e !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card .nav-tabs .nav-link:not(.disabled):hover, .card .nav-tabs .nav-link:not(.disabled):focus, .card .nav-tabs .nav-link.active {
|
||||||
|
border-bottom-color: #2c2c2e !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
|
.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
|
||||||
color: #ccc !important;
|
color: #f2f2f7 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
background-color: #585858;
|
background-color: #424242;
|
||||||
border: 1px solid #333;
|
border: 1px solid #282828;
|
||||||
}
|
}
|
||||||
.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover {
|
.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover {
|
||||||
color: #fafafa;
|
color: #fafafa;
|
||||||
@@ -92,7 +135,7 @@ legend {
|
|||||||
color: #d4d4d4 !important;
|
color: #d4d4d4 !important;
|
||||||
}
|
}
|
||||||
tbody tr {
|
tbody tr {
|
||||||
color: #555;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
.navbar-default .navbar-nav>.open>a, .navbar-default .navbar-nav>.open>a:focus, .navbar-default .navbar-nav>.open>a:hover {
|
.navbar-default .navbar-nav>.open>a, .navbar-default .navbar-nav>.open>a:focus, .navbar-default .navbar-nav>.open>a:hover {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
@@ -101,18 +144,15 @@ tbody tr {
|
|||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
.list-group-item {
|
.list-group-item {
|
||||||
background-color: #333;
|
background-color: #282828;
|
||||||
border: 1px solid #555;
|
border: 1px solid #555;
|
||||||
}
|
}
|
||||||
.table-striped>tbody>tr:nth-of-type(odd) {
|
.table-striped>tbody>tr:nth-of-type(odd) {
|
||||||
background-color: #333;
|
background-color: #424242;
|
||||||
}
|
}
|
||||||
table.dataTable>tbody>tr.child ul.dtr-details>li {
|
table.dataTable>tbody>tr.child ul.dtr-details>li {
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.13);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.13);
|
||||||
}
|
}
|
||||||
tbody tr {
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
.label.label-last-login {
|
.label.label-last-login {
|
||||||
color: #ccc !important;
|
color: #ccc !important;
|
||||||
background-color: #555 !important;
|
background-color: #555 !important;
|
||||||
@@ -128,20 +168,23 @@ div.numberedtextarea-number {
|
|||||||
}
|
}
|
||||||
.well {
|
.well {
|
||||||
border: 1px solid #555;
|
border: 1px solid #555;
|
||||||
background-color: #333;
|
background-color: #282828;
|
||||||
}
|
}
|
||||||
pre {
|
pre {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
background-color: #333;
|
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: #555 !important;
|
background-color: #424242 !important;
|
||||||
border: 1px solid #999;
|
border: 1px solid #999;
|
||||||
}
|
}
|
||||||
input.form-control:focus, textarea.form-control {
|
input.form-control:focus, textarea.form-control {
|
||||||
background-color: #555 !important;
|
background-color: #424242 !important;
|
||||||
}
|
}
|
||||||
input.form-control:disabled, textarea.form-disabled {
|
input.form-control:disabled, textarea.form-disabled {
|
||||||
color: #a8a8a8 !important;
|
color: #a8a8a8 !important;
|
||||||
@@ -149,16 +192,14 @@ input.form-control:disabled, textarea.form-disabled {
|
|||||||
}
|
}
|
||||||
.input-group-addon {
|
.input-group-addon {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
background-color: #555 !important;
|
background-color: #424242 !important;
|
||||||
border: 1px solid #999;
|
border: 1px solid #999;
|
||||||
}
|
}
|
||||||
.input-group-text {
|
.input-group-text {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
background-color: #242424;
|
background-color: #1c1c1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.list-group-item {
|
.list-group-item {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
@@ -170,11 +211,11 @@ input.form-control:disabled, textarea.form-disabled {
|
|||||||
}
|
}
|
||||||
.dropdown-item.active:hover {
|
.dropdown-item.active:hover {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
background-color: #31b1e4;
|
background-color: #007aff;
|
||||||
}
|
}
|
||||||
.form-select {
|
.form-select {
|
||||||
color: #e2e2e2!important;
|
color: #e2e2e2!important;
|
||||||
background-color: #555!important;
|
background-color: #424242!important;
|
||||||
border: 1px solid #999;
|
border: 1px solid #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,31 +227,6 @@ input.form-control:disabled, textarea.form-disabled {
|
|||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.table-secondary {
|
|
||||||
--bs-table-bg: #7a7a7a;
|
|
||||||
--bs-table-striped-bg: #e4e4e4;
|
|
||||||
--bs-table-striped-color: #000;
|
|
||||||
--bs-table-active-bg: #d8d8d8;
|
|
||||||
--bs-table-active-color: #000;
|
|
||||||
--bs-table-hover-bg: #dedede;
|
|
||||||
--bs-table-hover-color: #000;
|
|
||||||
color: #000;
|
|
||||||
border-color: #d8d8d8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-light {
|
|
||||||
--bs-table-bg: #f6f6f6;
|
|
||||||
--bs-table-striped-bg: #eaeaea;
|
|
||||||
--bs-table-striped-color: #000;
|
|
||||||
--bs-table-active-bg: #dddddd;
|
|
||||||
--bs-table-active-color: #000;
|
|
||||||
--bs-table-hover-bg: #e4e4e4;
|
|
||||||
--bs-table-hover-color: #000;
|
|
||||||
color: #000;
|
|
||||||
border-color: #dddddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control-plaintext {
|
.form-control-plaintext {
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
}
|
}
|
||||||
@@ -284,12 +300,12 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag-box {
|
.tag-box {
|
||||||
background-color: #555;
|
background-color: #282828;
|
||||||
border: 1px solid #999;
|
border: 1px solid #555;
|
||||||
}
|
}
|
||||||
.tag-input {
|
.tag-input {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #555;
|
background-color: #282828;
|
||||||
}
|
}
|
||||||
.tag-add {
|
.tag-add {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
@@ -298,43 +314,24 @@ a:hover {
|
|||||||
color: #d1d1d1;
|
color: #d1d1d1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
|
|
||||||
background-color: #7a7a7a !important;
|
|
||||||
}
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before {
|
|
||||||
background-color: #7a7a7a !important;
|
|
||||||
border: 1.5px solid #5c5c5c !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before {
|
|
||||||
background-color: #949494;
|
|
||||||
}
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,
|
|
||||||
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
|
|
||||||
background-color: #444444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-check-label {
|
.btn-check-label {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.btn-outline-secondary:hover {
|
.btn-outline-secondary:hover {
|
||||||
background-color: #c3c3c3;
|
background-color: #5c5c5c;
|
||||||
}
|
}
|
||||||
.btn.btn-outline-secondary {
|
.btn.btn-outline-secondary {
|
||||||
color: #fff !important;
|
color: #e0e0e0 !important;
|
||||||
border-color: #7a7a7a !important;
|
border-color: #7a7a7a !important;
|
||||||
}
|
}
|
||||||
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
|
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
|
||||||
background-color: #9b9b9b !important;
|
background-color: #7a7a7a !important;
|
||||||
|
}
|
||||||
|
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
|
||||||
|
color: #f2f2f7 !important;
|
||||||
|
background-color: #242424 !important;
|
||||||
|
border-color: #1c1c1e !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.btn-input-missing,
|
.btn-input-missing,
|
||||||
.btn-input-missing:hover,
|
.btn-input-missing:hover,
|
||||||
.btn-input-missing:active,
|
.btn-input-missing:active,
|
||||||
@@ -342,27 +339,119 @@ table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
|
|||||||
.btn-input-missing:active:hover,
|
.btn-input-missing:active:hover,
|
||||||
.btn-input-missing:active:focus {
|
.btn-input-missing:active:focus {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
background-color: #ff2f24 !important;
|
background-color: #ff3b30 !important;
|
||||||
border-color: #e21207 !important;
|
border-color: #ff3b30 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputMissingAttr {
|
.inputMissingAttr {
|
||||||
border-color: #FF4136 !important;
|
border-color: #ff4136 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.list-group-details {
|
.list-group-details {
|
||||||
background: #444444;
|
background: #555;
|
||||||
}
|
}
|
||||||
.list-group-header {
|
.list-group-header {
|
||||||
background: #333;
|
background: #444;
|
||||||
}
|
}
|
||||||
|
|
||||||
span.mail-address-item {
|
span.mail-address-item {
|
||||||
background-color: #333;
|
background-color: #444;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #555;
|
border: 1px solid #555;
|
||||||
padding: 2px 7px;
|
padding: 2px 7px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 2px 6px 2px 0;
|
margin: 2px 6px 2px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
|
||||||
|
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
|
||||||
|
background-color: #7a7a7a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
|
||||||
|
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before {
|
||||||
|
background-color: #7a7a7a !important;
|
||||||
|
border: 1.5px solid #5c5c5c !important;
|
||||||
|
color: #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
|
||||||
|
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before {
|
||||||
|
background-color: #949494;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,
|
||||||
|
table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,
|
||||||
|
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
|
||||||
|
background-color: #414141;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
|
||||||
|
color: #ccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-secondary {
|
||||||
|
--bs-table-bg: #282828;
|
||||||
|
--bs-table-striped-bg: #343434;
|
||||||
|
--bs-table-striped-color: #f2f2f7;
|
||||||
|
--bs-table-active-bg: #4c4c4c;
|
||||||
|
--bs-table-active-color: #f2f2f7;
|
||||||
|
--bs-table-hover-bg: #3a3a3a;
|
||||||
|
--bs-table-hover-color: #f2f2f7;
|
||||||
|
color: #ccc;
|
||||||
|
border-color: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-light {
|
||||||
|
--bs-table-bg: #3a3a3a;
|
||||||
|
--bs-table-striped-bg: #444444;
|
||||||
|
--bs-table-striped-color: #f2f2f7;
|
||||||
|
--bs-table-active-bg: #5c5c5c;
|
||||||
|
--bs-table-active-color: #f2f2f7;
|
||||||
|
--bs-table-hover-bg: #4c4c4c;
|
||||||
|
--bs-table-hover-color: #f2f2f7;
|
||||||
|
color: #ccc;
|
||||||
|
border-color: #4c4c4c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-bordered {
|
||||||
|
border-color: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-bordered th,
|
||||||
|
.table-bordered td {
|
||||||
|
border-color: #3a3a3a !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-bordered thead th,
|
||||||
|
.table-bordered thead td {
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-striped>tbody>tr:nth-of-type(odd)>td,
|
||||||
|
.table-striped>tbody>tr:nth-of-type(odd)>th {
|
||||||
|
background-color: #282828;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover>tbody>tr:hover {
|
||||||
|
background-color: #343434;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table>:not(caption)>*>* {
|
||||||
|
border-color: #5c5c5c;
|
||||||
|
--bs-table-color-state:#bbb;
|
||||||
|
--bs-table-bg: #3a3a3a;
|
||||||
|
}
|
||||||
|
.text-muted {
|
||||||
|
--bs-secondary-color: #8e8e93;
|
||||||
|
}
|
||||||
|
input::placeholder {
|
||||||
|
color: #8e8e93 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%238e8e93' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
.btn-light, .btn-light:hover {
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
+12
-5
@@ -47,6 +47,7 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
|||||||
$quota_notification_bcc = quota_notification_bcc('get', $domain);
|
$quota_notification_bcc = quota_notification_bcc('get', $domain);
|
||||||
$rl = ratelimit('get', 'domain', $domain);
|
$rl = ratelimit('get', 'domain', $domain);
|
||||||
$rlyhosts = relayhost('get');
|
$rlyhosts = relayhost('get');
|
||||||
|
$domain_footer = mailbox('get', 'domain_wide_footer', $domain);
|
||||||
$template = 'edit/domain.twig';
|
$template = 'edit/domain.twig';
|
||||||
$template_data = [
|
$template_data = [
|
||||||
'acl' => $_SESSION['acl'],
|
'acl' => $_SESSION['acl'],
|
||||||
@@ -56,23 +57,28 @@ if (isset($_SESSION['mailcow_cc_role'])) {
|
|||||||
'rlyhosts' => $rlyhosts,
|
'rlyhosts' => $rlyhosts,
|
||||||
'dkim' => dkim('details', $domain),
|
'dkim' => dkim('details', $domain),
|
||||||
'domain_details' => $result,
|
'domain_details' => $result,
|
||||||
|
'domain_footer' => $domain_footer,
|
||||||
|
'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]),
|
||||||
|
'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address')
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elseif (isset($_GET["template"])){
|
elseif (isset($_GET['template'])){
|
||||||
$domain_template = mailbox('get', 'domain_templates', $_GET["template"]);
|
$domain_template = mailbox('get', 'domain_templates', $_GET['template']);
|
||||||
if ($domain_template){
|
if ($domain_template){
|
||||||
$template_data = [
|
$template_data = [
|
||||||
'template' => $domain_template
|
'template' => $domain_template,
|
||||||
|
'rl' => ['frame' => $domain_template['attributes']['rl_frame']],
|
||||||
];
|
];
|
||||||
$template = 'edit/domain-templates.twig';
|
$template = 'edit/domain-templates.twig';
|
||||||
$result = true;
|
$result = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$mailbox_template = mailbox('get', 'mailbox_templates', $_GET["template"]);
|
$mailbox_template = mailbox('get', 'mailbox_templates', $_GET['template']);
|
||||||
if ($mailbox_template){
|
if ($mailbox_template){
|
||||||
$template_data = [
|
$template_data = [
|
||||||
'template' => $mailbox_template
|
'template' => $mailbox_template,
|
||||||
|
'rl' => ['frame' => $mailbox_template['attributes']['rl_frame']],
|
||||||
];
|
];
|
||||||
$template = 'edit/mailbox-templates.twig';
|
$template = 'edit/mailbox-templates.twig';
|
||||||
$result = true;
|
$result = true;
|
||||||
@@ -214,6 +220,7 @@ $js_minifier->add('/web/js/site/pwgen.js');
|
|||||||
$template_data['result'] = $result;
|
$template_data['result'] = $result;
|
||||||
$template_data['return_to'] = $_SESSION['return_to'];
|
$template_data['return_to'] = $_SESSION['return_to'];
|
||||||
$template_data['lang_user'] = json_encode($lang['user']);
|
$template_data['lang_user'] = json_encode($lang['user']);
|
||||||
|
$template_data['lang_admin'] = json_encode($lang['admin']);
|
||||||
$template_data['lang_datatables'] = json_encode($lang['datatables']);
|
$template_data['lang_datatables'] = json_encode($lang['datatables']);
|
||||||
|
|
||||||
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/footer.inc.php';
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ function bcc($_action, $_data = null, $_attr = null) {
|
|||||||
}
|
}
|
||||||
elseif (filter_var($local_dest, FILTER_VALIDATE_EMAIL)) {
|
elseif (filter_var($local_dest, FILTER_VALIDATE_EMAIL)) {
|
||||||
$mailbox = mailbox('get', 'mailbox_details', $local_dest);
|
$mailbox = mailbox('get', 'mailbox_details', $local_dest);
|
||||||
if ($mailbox === false && array_key_exists($local_dest, array_merge($direct_aliases, $shared_aliases)) === false) {
|
$shared_aliases = mailbox('get', 'shared_aliases');
|
||||||
|
$direct_aliases = mailbox('get', 'direct_aliases');
|
||||||
|
if ($mailbox === false && in_array($local_dest, array_merge($direct_aliases, $shared_aliases)) === false) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
'log' => array(__FUNCTION__, $_action, $_data, $_attr),
|
||||||
|
|||||||
@@ -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':
|
||||||
@@ -24,9 +25,10 @@ function customize($_action, $_item, $_data = null) {
|
|||||||
}
|
}
|
||||||
switch ($_item) {
|
switch ($_item) {
|
||||||
case 'main_logo':
|
case 'main_logo':
|
||||||
if (in_array($_data['main_logo']['type'], array('image/gif', 'image/jpeg', 'image/pjpeg', 'image/x-png', 'image/png', 'image/svg+xml'))) {
|
case 'main_logo_dark':
|
||||||
|
if (in_array($_data[$_item]['type'], array('image/gif', 'image/jpeg', 'image/pjpeg', 'image/x-png', 'image/png', 'image/svg+xml'))) {
|
||||||
try {
|
try {
|
||||||
if (file_exists($_data['main_logo']['tmp_name']) !== true) {
|
if (file_exists($_data[$_item]['tmp_name']) !== true) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||||
@@ -34,7 +36,24 @@ function customize($_action, $_item, $_data = null) {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$image = new Imagick($_data['main_logo']['tmp_name']);
|
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']);
|
||||||
if ($image->valid() !== true) {
|
if ($image->valid() !== true) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
@@ -63,7 +82,7 @@ function customize($_action, $_item, $_data = null) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
$redis->Set('MAIN_LOGO', 'data:' . $_data['main_logo']['type'] . ';base64,' . base64_encode(file_get_contents($_data['main_logo']['tmp_name'])));
|
$redis->Set(strtoupper($_item), 'data:' . $_data[$_item]['type'] . ';base64,' . base64_encode(file_get_contents($_data[$_item]['tmp_name'])));
|
||||||
}
|
}
|
||||||
catch (RedisException $e) {
|
catch (RedisException $e) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@@ -201,8 +220,9 @@ function customize($_action, $_item, $_data = null) {
|
|||||||
}
|
}
|
||||||
switch ($_item) {
|
switch ($_item) {
|
||||||
case 'main_logo':
|
case 'main_logo':
|
||||||
|
case 'main_logo_dark':
|
||||||
try {
|
try {
|
||||||
if ($redis->del('MAIN_LOGO')) {
|
if ($redis->del(strtoupper($_item))) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
'log' => array(__FUNCTION__, $_action, $_item, $_data),
|
||||||
@@ -239,8 +259,9 @@ function customize($_action, $_item, $_data = null) {
|
|||||||
return ($app_links) ? $app_links : false;
|
return ($app_links) ? $app_links : false;
|
||||||
break;
|
break;
|
||||||
case 'main_logo':
|
case 'main_logo':
|
||||||
|
case 'main_logo_dark':
|
||||||
try {
|
try {
|
||||||
return $redis->get('MAIN_LOGO');
|
return $redis->get(strtoupper($_item));
|
||||||
}
|
}
|
||||||
catch (RedisException $e) {
|
catch (RedisException $e) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@@ -277,9 +298,14 @@ function customize($_action, $_item, $_data = null) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'main_logo_specs':
|
case 'main_logo_specs':
|
||||||
|
case 'main_logo_dark_specs':
|
||||||
try {
|
try {
|
||||||
$image = new Imagick();
|
$image = new Imagick();
|
||||||
$img_data = explode('base64,', customize('get', 'main_logo'));
|
if($_item == 'main_logo_specs') {
|
||||||
|
$img_data = explode('base64,', customize('get', 'main_logo'));
|
||||||
|
} else {
|
||||||
|
$img_data = explode('base64,', customize('get', 'main_logo_dark'));
|
||||||
|
}
|
||||||
if ($img_data[1]) {
|
if ($img_data[1]) {
|
||||||
$image->readImageBlob(base64_decode($img_data[1]));
|
$image->readImageBlob(base64_decode($img_data[1]));
|
||||||
return $image->identifyImage();
|
return $image->identifyImage();
|
||||||
|
|||||||
@@ -192,5 +192,16 @@ function docker($action, $service_name = null, $attr1 = null, $attr2 = null, $ex
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
break;
|
break;
|
||||||
|
case 'broadcast':
|
||||||
|
$request = array(
|
||||||
|
"api_call" => "container_post",
|
||||||
|
"container_name" => $service_name,
|
||||||
|
"post_action" => $attr1,
|
||||||
|
"request" => $attr2
|
||||||
|
);
|
||||||
|
|
||||||
|
$redis->publish("MC_CHANNEL", json_encode($request));
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,407 +1,468 @@
|
|||||||
<?php
|
<?php
|
||||||
function domain_admin($_action, $_data = null) {
|
function domain_admin($_action, $_data = null) {
|
||||||
global $pdo;
|
global $pdo;
|
||||||
global $lang;
|
global $lang;
|
||||||
$_data_log = $_data;
|
$_data_log = $_data;
|
||||||
!isset($_data_log['password']) ?: $_data_log['password'] = '*';
|
!isset($_data_log['password']) ?: $_data_log['password'] = '*';
|
||||||
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
|
!isset($_data_log['password2']) ?: $_data_log['password2'] = '*';
|
||||||
!isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
|
!isset($_data_log['user_old_pass']) ?: $_data_log['user_old_pass'] = '*';
|
||||||
!isset($_data_log['user_new_pass']) ?: $_data_log['user_new_pass'] = '*';
|
!isset($_data_log['user_new_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'] = '*';
|
||||||
switch ($_action) {
|
switch ($_action) {
|
||||||
case 'add':
|
case 'add':
|
||||||
$username = strtolower(trim($_data['username']));
|
$username = strtolower(trim($_data['username']));
|
||||||
$password = $_data['password'];
|
$password = $_data['password'];
|
||||||
$password2 = $_data['password2'];
|
$password2 = $_data['password2'];
|
||||||
$domains = (array)$_data['domains'];
|
$domains = (array)$_data['domains'];
|
||||||
$active = intval($_data['active']);
|
$active = intval($_data['active']);
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => 'access_denied'
|
'msg' => 'access_denied'
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (empty($domains)) {
|
if (empty($domains)) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => 'domain_invalid'
|
'msg' => 'domain_invalid'
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username) || $username == 'API') {
|
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username) || $username == 'API') {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => array('username_invalid', $username)
|
'msg' => array('username_invalid', $username)
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
|
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox`
|
||||||
WHERE `username` = :username");
|
WHERE `username` = :username");
|
||||||
$stmt->execute(array(':username' => $username));
|
$stmt->execute(array(':username' => $username));
|
||||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT `username` FROM `admin`
|
$stmt = $pdo->prepare("SELECT `username` FROM `admin`
|
||||||
WHERE `username` = :username");
|
WHERE `username` = :username");
|
||||||
$stmt->execute(array(':username' => $username));
|
$stmt->execute(array(':username' => $username));
|
||||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
|
$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
|
||||||
WHERE `username` = :username");
|
WHERE `username` = :username");
|
||||||
$stmt->execute(array(':username' => $username));
|
$stmt->execute(array(':username' => $username));
|
||||||
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
$num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
|
||||||
foreach ($num_results as $num_results_each) {
|
foreach ($num_results as $num_results_each) {
|
||||||
if ($num_results_each != 0) {
|
if ($num_results_each != 0) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => array('object_exists', htmlspecialchars($username))
|
'msg' => array('object_exists', htmlspecialchars($username))
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (password_check($password, $password2) !== true) {
|
if (password_check($password, $password2) !== true) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$password_hashed = hash_password($password);
|
$password_hashed = hash_password($password);
|
||||||
$valid_domains = 0;
|
$valid_domains = 0;
|
||||||
foreach ($domains as $domain) {
|
foreach ($domains as $domain) {
|
||||||
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
|
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => array('domain_invalid', htmlspecialchars($domain))
|
'msg' => array('domain_invalid', htmlspecialchars($domain))
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$valid_domains++;
|
$valid_domains++;
|
||||||
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
|
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
|
||||||
VALUES (:username, :domain, :created, :active)");
|
VALUES (:username, :domain, :created, :active)");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
':domain' => $domain,
|
':domain' => $domain,
|
||||||
':created' => date('Y-m-d H:i:s'),
|
':created' => date('Y-m-d H:i:s'),
|
||||||
':active' => $active
|
':active' => $active
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if ($valid_domains != 0) {
|
if ($valid_domains != 0) {
|
||||||
$stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
|
$stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`)
|
||||||
VALUES (:username, :password_hashed, '0', :active)");
|
VALUES (:username, :password_hashed, '0', :active)");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
':password_hashed' => $password_hashed,
|
':password_hashed' => $password_hashed,
|
||||||
':active' => $active
|
':active' => $active
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)");
|
$stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username
|
':username' => $username
|
||||||
));
|
));
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => array('domain_admin_added', htmlspecialchars($username))
|
'msg' => array('domain_admin_added', htmlspecialchars($username))
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'edit':
|
case 'edit':
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => 'access_denied'
|
'msg' => 'access_denied'
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Administrator
|
// Administrator
|
||||||
if ($_SESSION['mailcow_cc_role'] == "admin") {
|
if ($_SESSION['mailcow_cc_role'] == "admin") {
|
||||||
if (!is_array($_data['username'])) {
|
if (!is_array($_data['username'])) {
|
||||||
$usernames = array();
|
$usernames = array();
|
||||||
$usernames[] = $_data['username'];
|
$usernames[] = $_data['username'];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$usernames = $_data['username'];
|
$usernames = $_data['username'];
|
||||||
}
|
}
|
||||||
foreach ($usernames as $username) {
|
foreach ($usernames as $username) {
|
||||||
$is_now = domain_admin('details', $username);
|
$is_now = domain_admin('details', $username);
|
||||||
$domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null;
|
$domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null;
|
||||||
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'];
|
||||||
$domains = (!empty($domains)) ? $domains : $is_now['selected_domains'];
|
$domains = (!empty($domains)) ? $domains : $is_now['selected_domains'];
|
||||||
$username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username'];
|
$username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username'];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => 'access_denied'
|
'msg' => 'access_denied'
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$password = $_data['password'];
|
$password = $_data['password'];
|
||||||
$password2 = $_data['password2'];
|
$password2 = $_data['password2'];
|
||||||
if (!empty($domains)) {
|
if (!empty($domains)) {
|
||||||
foreach ($domains as $domain) {
|
foreach ($domains as $domain) {
|
||||||
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
|
if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => array('domain_invalid', htmlspecialchars($domain))
|
'msg' => array('domain_invalid', htmlspecialchars($domain))
|
||||||
);
|
);
|
||||||
continue 2;
|
continue 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) {
|
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => array('username_invalid', $username_new)
|
'msg' => array('username_invalid', $username_new)
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ($username_new != $username) {
|
if ($username_new != $username) {
|
||||||
if (!empty(domain_admin('details', $username_new)['username'])) {
|
if (!empty(domain_admin('details', $username_new)['username'])) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => array('username_invalid', $username_new)
|
'msg' => array('username_invalid', $username_new)
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
));
|
));
|
||||||
$stmt = $pdo->prepare("UPDATE `da_acl` SET `username` = :username_new WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `da_acl` SET `username` = :username_new WHERE `username` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username_new' => $username_new,
|
':username_new' => $username_new,
|
||||||
':username' => $username
|
':username' => $username
|
||||||
));
|
));
|
||||||
if (!empty($domains)) {
|
if (!empty($domains)) {
|
||||||
foreach ($domains as $domain) {
|
foreach ($domains as $domain) {
|
||||||
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
|
$stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`)
|
||||||
VALUES (:username_new, :domain, :created, :active)");
|
VALUES (:username_new, :domain, :created, :active)");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username_new' => $username_new,
|
':username_new' => $username_new,
|
||||||
':domain' => $domain,
|
':domain' => $domain,
|
||||||
':created' => date('Y-m-d H:i:s'),
|
':created' => date('Y-m-d H:i:s'),
|
||||||
':active' => $active
|
':active' => $active
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!empty($password)) {
|
if (!empty($password)) {
|
||||||
if (password_check($password, $password2) !== true) {
|
if (password_check($password, $password2) !== true) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$password_hashed = hash_password($password);
|
$password_hashed = hash_password($password);
|
||||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':password_hashed' => $password_hashed,
|
':password_hashed' => $password_hashed,
|
||||||
':username_new' => $username_new,
|
':username_new' => $username_new,
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
':active' => $active
|
':active' => $active
|
||||||
));
|
));
|
||||||
if (isset($_data['disable_tfa'])) {
|
if (isset($_data['disable_tfa'])) {
|
||||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||||
$stmt->execute(array(':username' => $username));
|
$stmt->execute(array(':username' => $username));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
||||||
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username_new' => $username_new,
|
':username_new' => $username_new,
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
':active' => $active
|
':active' => $active
|
||||||
));
|
));
|
||||||
if (isset($_data['disable_tfa'])) {
|
if (isset($_data['disable_tfa'])) {
|
||||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username");
|
||||||
$stmt->execute(array(':username' => $username));
|
$stmt->execute(array(':username' => $username));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username");
|
||||||
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
$stmt->execute(array(':username_new' => $username_new, ':username' => $username));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => array('domain_admin_modified', htmlspecialchars($username))
|
'msg' => array('domain_admin_modified', htmlspecialchars($username))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Domain administrator
|
// Domain administrator
|
||||||
// Can only edit itself
|
// Can only edit itself
|
||||||
elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
|
elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") {
|
||||||
$username = $_SESSION['mailcow_cc_username'];
|
$username = $_SESSION['mailcow_cc_username'];
|
||||||
$password_old = $_data['user_old_pass'];
|
$password_old = $_data['user_old_pass'];
|
||||||
$password_new = $_data['user_new_pass'];
|
$password_new = $_data['user_new_pass'];
|
||||||
$password_new2 = $_data['user_new_pass2'];
|
$password_new2 = $_data['user_new_pass2'];
|
||||||
|
|
||||||
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
$stmt = $pdo->prepare("SELECT `password` FROM `admin`
|
||||||
WHERE `username` = :user");
|
WHERE `username` = :user");
|
||||||
$stmt->execute(array(':user' => $username));
|
$stmt->execute(array(':user' => $username));
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
if (!verify_hash($row['password'], $password_old)) {
|
if (!verify_hash($row['password'], $password_old)) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => 'access_denied'
|
'msg' => 'access_denied'
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (password_check($password_new, $password_new2) !== true) {
|
if (password_check($password_new, $password_new2) !== true) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$password_hashed = hash_password($password_new);
|
$password_hashed = hash_password($password_new);
|
||||||
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
|
$stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':password_hashed' => $password_hashed,
|
':password_hashed' => $password_hashed,
|
||||||
':username' => $username
|
':username' => $username
|
||||||
));
|
));
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => array('domain_admin_modified', htmlspecialchars($username))
|
'msg' => array('domain_admin_modified', htmlspecialchars($username))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => 'access_denied'
|
'msg' => 'access_denied'
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$usernames = (array)$_data['username'];
|
$usernames = (array)$_data['username'];
|
||||||
foreach ($usernames as $username) {
|
foreach ($usernames as $username) {
|
||||||
if (empty(domain_admin('details', $username))) {
|
if (empty(domain_admin('details', $username))) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => array('username_invalid', $username)
|
'msg' => array('username_invalid', $username)
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
$stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
));
|
));
|
||||||
$stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
|
$stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
));
|
));
|
||||||
$stmt = $pdo->prepare("DELETE FROM `da_acl` WHERE `username` = :username");
|
$stmt = $pdo->prepare("DELETE FROM `da_acl` WHERE `username` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
));
|
));
|
||||||
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
|
$stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
));
|
));
|
||||||
$stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username");
|
$stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
));
|
));
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => array('domain_admin_removed', htmlspecialchars($username))
|
'msg' => array('domain_admin_removed', htmlspecialchars($username))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'get':
|
case 'get':
|
||||||
$domainadmins = array();
|
$domainadmins = array();
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $_action, $_data_log),
|
'log' => array(__FUNCTION__, $_action, $_data_log),
|
||||||
'msg' => 'access_denied'
|
'msg' => 'access_denied'
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->query("SELECT DISTINCT
|
$stmt = $pdo->query("SELECT DISTINCT
|
||||||
`username`
|
`username`
|
||||||
FROM `domain_admins`
|
FROM `domain_admins`
|
||||||
WHERE `username` IN (
|
WHERE `username` IN (
|
||||||
SELECT `username` FROM `admin`
|
SELECT `username` FROM `admin`
|
||||||
WHERE `superadmin`!='1'
|
WHERE `superadmin`!='1'
|
||||||
)");
|
)");
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
while ($row = array_shift($rows)) {
|
while ($row = array_shift($rows)) {
|
||||||
$domainadmins[] = $row['username'];
|
$domainadmins[] = $row['username'];
|
||||||
}
|
}
|
||||||
return $domainadmins;
|
return $domainadmins;
|
||||||
break;
|
break;
|
||||||
case 'details':
|
case 'details':
|
||||||
$domainadmindata = array();
|
$domainadmindata = array();
|
||||||
if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) {
|
if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) {
|
elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) {
|
if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("SELECT
|
$stmt = $pdo->prepare("SELECT
|
||||||
`tfa`.`active` AS `tfa_active`,
|
`tfa`.`active` AS `tfa_active`,
|
||||||
`domain_admins`.`username`,
|
`domain_admins`.`username`,
|
||||||
`domain_admins`.`created`,
|
`domain_admins`.`created`,
|
||||||
`domain_admins`.`active` AS `active`
|
`domain_admins`.`active` AS `active`
|
||||||
FROM `domain_admins`
|
FROM `domain_admins`
|
||||||
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
|
LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username`
|
||||||
WHERE `domain_admins`.`username`= :domain_admin");
|
WHERE `domain_admins`.`username`= :domain_admin");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':domain_admin' => $_data
|
':domain_admin' => $_data
|
||||||
));
|
));
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
if (empty($row)) {
|
if (empty($row)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$domainadmindata['username'] = $row['username'];
|
$domainadmindata['username'] = $row['username'];
|
||||||
$domainadmindata['tfa_active'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
|
$domainadmindata['tfa_active'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
|
||||||
$domainadmindata['tfa_active_int'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
|
$domainadmindata['tfa_active_int'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active'];
|
||||||
$domainadmindata['active'] = $row['active'];
|
$domainadmindata['active'] = $row['active'];
|
||||||
$domainadmindata['active_int'] = $row['active'];
|
$domainadmindata['active_int'] = $row['active'];
|
||||||
$domainadmindata['created'] = $row['created'];
|
$domainadmindata['created'] = $row['created'];
|
||||||
// GET SELECTED
|
// GET SELECTED
|
||||||
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
||||||
WHERE `domain` IN (
|
WHERE `domain` IN (
|
||||||
SELECT `domain` FROM `domain_admins`
|
SELECT `domain` FROM `domain_admins`
|
||||||
WHERE `username`= :domain_admin)");
|
WHERE `username`= :domain_admin)");
|
||||||
$stmt->execute(array(':domain_admin' => $_data));
|
$stmt->execute(array(':domain_admin' => $_data));
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
while($row = array_shift($rows)) {
|
while($row = array_shift($rows)) {
|
||||||
$domainadmindata['selected_domains'][] = $row['domain'];
|
$domainadmindata['selected_domains'][] = $row['domain'];
|
||||||
}
|
}
|
||||||
// GET UNSELECTED
|
// GET UNSELECTED
|
||||||
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
$stmt = $pdo->prepare("SELECT `domain` FROM `domain`
|
||||||
WHERE `domain` NOT IN (
|
WHERE `domain` NOT IN (
|
||||||
SELECT `domain` FROM `domain_admins`
|
SELECT `domain` FROM `domain_admins`
|
||||||
WHERE `username`= :domain_admin)");
|
WHERE `username`= :domain_admin)");
|
||||||
$stmt->execute(array(':domain_admin' => $_data));
|
$stmt->execute(array(':domain_admin' => $_data));
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
while($row = array_shift($rows)) {
|
while($row = array_shift($rows)) {
|
||||||
$domainadmindata['unselected_domains'][] = $row['domain'];
|
$domainadmindata['unselected_domains'][] = $row['domain'];
|
||||||
}
|
}
|
||||||
if (!isset($domainadmindata['unselected_domains'])) {
|
if (!isset($domainadmindata['unselected_domains'])) {
|
||||||
$domainadmindata['unselected_domains'] = "";
|
$domainadmindata['unselected_domains'] = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return $domainadmindata;
|
return $domainadmindata;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function domain_admin_sso($_action, $_data) {
|
||||||
|
global $pdo;
|
||||||
|
|
||||||
|
switch ($_action) {
|
||||||
|
case 'check':
|
||||||
|
$token = $_data;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT `t1`.`username` FROM `da_sso` AS `t1` JOIN `admin` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL '30' SECOND) AND `t2`.`active` = 1 AND `t2`.`superadmin` = 0;");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token)
|
||||||
|
));
|
||||||
|
$return = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
return empty($return['username']) ? false : $return['username'];
|
||||||
|
case 'issue':
|
||||||
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_data),
|
||||||
|
'msg' => 'access_denied'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = $_data['username'];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT `username` FROM `domain_admins`
|
||||||
|
WHERE `username` = :username");
|
||||||
|
$stmt->execute(array(':username' => $username));
|
||||||
|
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
|
||||||
|
if ($num_results < 1) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_data),
|
||||||
|
'msg' => array('object_doesnt_exist', htmlspecialchars($username))
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = implode('-', array(
|
||||||
|
strtoupper(bin2hex(random_bytes(3))),
|
||||||
|
strtoupper(bin2hex(random_bytes(3))),
|
||||||
|
strtoupper(bin2hex(random_bytes(3))),
|
||||||
|
strtoupper(bin2hex(random_bytes(3))),
|
||||||
|
strtoupper(bin2hex(random_bytes(3)))
|
||||||
|
));
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO `da_sso` (`username`, `token`)
|
||||||
|
VALUES (:username, :token)");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':username' => $username,
|
||||||
|
':token' => $token
|
||||||
|
));
|
||||||
|
|
||||||
|
// perform cleanup
|
||||||
|
$pdo->query("DELETE FROM `da_sso` WHERE created < DATE_SUB(NOW(), INTERVAL '30' SECOND);");
|
||||||
|
|
||||||
|
return ['token' => $token];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
function fail2ban($_action, $_data = null) {
|
function fail2ban($_action, $_data = null, $_extra = null) {
|
||||||
global $redis;
|
global $redis;
|
||||||
$_data_log = $_data;
|
$_data_log = $_data;
|
||||||
switch ($_action) {
|
switch ($_action) {
|
||||||
@@ -239,12 +239,15 @@ function fail2ban($_action, $_data = null) {
|
|||||||
$is_now = fail2ban('get');
|
$is_now = fail2ban('get');
|
||||||
if (!empty($is_now)) {
|
if (!empty($is_now)) {
|
||||||
$ban_time = intval((isset($_data['ban_time'])) ? $_data['ban_time'] : $is_now['ban_time']);
|
$ban_time = intval((isset($_data['ban_time'])) ? $_data['ban_time'] : $is_now['ban_time']);
|
||||||
|
$ban_time_increment = (isset($_data['ban_time_increment']) && $_data['ban_time_increment'] == "1") ? 1 : 0;
|
||||||
$max_attempts = intval((isset($_data['max_attempts'])) ? $_data['max_attempts'] : $is_now['max_attempts']);
|
$max_attempts = intval((isset($_data['max_attempts'])) ? $_data['max_attempts'] : $is_now['max_attempts']);
|
||||||
|
$max_ban_time = intval((isset($_data['max_ban_time'])) ? $_data['max_ban_time'] : $is_now['max_ban_time']);
|
||||||
$retry_window = intval((isset($_data['retry_window'])) ? $_data['retry_window'] : $is_now['retry_window']);
|
$retry_window = intval((isset($_data['retry_window'])) ? $_data['retry_window'] : $is_now['retry_window']);
|
||||||
$netban_ipv4 = intval((isset($_data['netban_ipv4'])) ? $_data['netban_ipv4'] : $is_now['netban_ipv4']);
|
$netban_ipv4 = intval((isset($_data['netban_ipv4'])) ? $_data['netban_ipv4'] : $is_now['netban_ipv4']);
|
||||||
$netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
|
$netban_ipv6 = intval((isset($_data['netban_ipv6'])) ? $_data['netban_ipv6'] : $is_now['netban_ipv6']);
|
||||||
$wl = (isset($_data['whitelist'])) ? $_data['whitelist'] : $is_now['whitelist'];
|
$wl = (isset($_data['whitelist'])) ? $_data['whitelist'] : $is_now['whitelist'];
|
||||||
$bl = (isset($_data['blacklist'])) ? $_data['blacklist'] : $is_now['blacklist'];
|
$bl = (isset($_data['blacklist'])) ? $_data['blacklist'] : $is_now['blacklist'];
|
||||||
|
$manage_external = (isset($_data['manage_external'])) ? intval($_data['manage_external']) : 0;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@@ -256,12 +259,16 @@ function fail2ban($_action, $_data = null) {
|
|||||||
}
|
}
|
||||||
$f2b_options = array();
|
$f2b_options = array();
|
||||||
$f2b_options['ban_time'] = ($ban_time < 60) ? 60 : $ban_time;
|
$f2b_options['ban_time'] = ($ban_time < 60) ? 60 : $ban_time;
|
||||||
|
$f2b_options['ban_time_increment'] = ($ban_time_increment == 1) ? true : false;
|
||||||
|
$f2b_options['max_ban_time'] = ($max_ban_time < 60) ? 60 : $max_ban_time;
|
||||||
$f2b_options['netban_ipv4'] = ($netban_ipv4 < 8) ? 8 : $netban_ipv4;
|
$f2b_options['netban_ipv4'] = ($netban_ipv4 < 8) ? 8 : $netban_ipv4;
|
||||||
$f2b_options['netban_ipv6'] = ($netban_ipv6 < 8) ? 8 : $netban_ipv6;
|
$f2b_options['netban_ipv6'] = ($netban_ipv6 < 8) ? 8 : $netban_ipv6;
|
||||||
$f2b_options['netban_ipv4'] = ($netban_ipv4 > 32) ? 32 : $netban_ipv4;
|
$f2b_options['netban_ipv4'] = ($netban_ipv4 > 32) ? 32 : $netban_ipv4;
|
||||||
$f2b_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
|
$f2b_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
|
||||||
$f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
|
$f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
|
||||||
$f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
|
$f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
|
||||||
|
$f2b_options['banlist_id'] = $is_now['banlist_id'];
|
||||||
|
$f2b_options['manage_external'] = ($manage_external > 0) ? 1 : 0;
|
||||||
try {
|
try {
|
||||||
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
|
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
|
||||||
$redis->Del('F2B_WHITELIST');
|
$redis->Del('F2B_WHITELIST');
|
||||||
@@ -325,5 +332,71 @@ function fail2ban($_action, $_data = null) {
|
|||||||
'msg' => 'f2b_modified'
|
'msg' => 'f2b_modified'
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case 'banlist':
|
||||||
|
try {
|
||||||
|
$f2b_options = json_decode($redis->Get('F2B_OPTIONS'), true);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
http_response_code(500);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (is_array($_extra)) {
|
||||||
|
$_extra = $_extra[0];
|
||||||
|
}
|
||||||
|
if ($_extra != $f2b_options['banlist_id']){
|
||||||
|
http_response_code(404);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($_data) {
|
||||||
|
case 'get':
|
||||||
|
try {
|
||||||
|
$bl = $redis->hKeys('F2B_BLACKLIST');
|
||||||
|
$active_bans = $redis->hKeys('F2B_ACTIVE_BANS');
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
http_response_code(500);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$banlist = implode("\n", array_merge($bl, $active_bans));
|
||||||
|
return $banlist;
|
||||||
|
break;
|
||||||
|
case 'refresh':
|
||||||
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$f2b_options['banlist_id'] = uuid4();
|
||||||
|
try {
|
||||||
|
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'success',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_data_log, $_extra),
|
||||||
|
'msg' => 'f2b_banlist_refreshed'
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+190
-22
@@ -526,8 +526,9 @@ function logger($_data = false) {
|
|||||||
':remote' => get_remote_ip()
|
':remote' => get_remote_ip()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
catch (Exception $e) {
|
catch (PDOException $e) {
|
||||||
// Do nothing
|
# handle the exception here, as the exception handler function results in a white page
|
||||||
|
error_log($e->getMessage(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1015,20 +1016,58 @@ function formatBytes($size, $precision = 2) {
|
|||||||
}
|
}
|
||||||
return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
|
return round(pow(1024, $base - floor($base)), $precision) . $suffixes[floor($base)];
|
||||||
}
|
}
|
||||||
function update_sogo_static_view() {
|
function update_sogo_static_view($mailbox = null) {
|
||||||
if (getenv('SKIP_SOGO') == "y") {
|
if (getenv('SKIP_SOGO') == "y") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
global $pdo;
|
global $pdo;
|
||||||
global $lang;
|
global $lang;
|
||||||
$stmt = $pdo->query("SELECT 'OK' FROM INFORMATION_SCHEMA.TABLES
|
|
||||||
WHERE TABLE_NAME = 'sogo_view'");
|
$mailbox_exists = false;
|
||||||
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
|
if ($mailbox !== null) {
|
||||||
if ($num_results != 0) {
|
// Check if the mailbox exists
|
||||||
$stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`)
|
$stmt = $pdo->prepare("SELECT username FROM mailbox WHERE username = :mailbox AND active = '1'");
|
||||||
SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view");
|
$stmt->execute(array(':mailbox' => $mailbox));
|
||||||
$stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');");
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
if ($row){
|
||||||
|
$mailbox_exists = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$query = "REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`)
|
||||||
|
SELECT
|
||||||
|
mailbox.username,
|
||||||
|
mailbox.domain,
|
||||||
|
mailbox.username,
|
||||||
|
IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.force_pw_update')) = '0',
|
||||||
|
IF(JSON_UNQUOTE(JSON_VALUE(attributes, '$.sogo_access')) = 1, password, '{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'),
|
||||||
|
'{SSHA256}A123A123A321A321A321B321B321B123B123B321B432F123E321123123321321'),
|
||||||
|
mailbox.name,
|
||||||
|
mailbox.username,
|
||||||
|
IFNULL(GROUP_CONCAT(ga.aliases ORDER BY ga.aliases SEPARATOR ' '), ''),
|
||||||
|
IFNULL(gda.ad_alias, ''),
|
||||||
|
IFNULL(external_acl.send_as_acl, ''),
|
||||||
|
mailbox.kind,
|
||||||
|
mailbox.multiple_bookings
|
||||||
|
FROM
|
||||||
|
mailbox
|
||||||
|
LEFT OUTER JOIN grouped_mail_aliases ga ON ga.username REGEXP CONCAT('(^|,)', mailbox.username, '($|,)')
|
||||||
|
LEFT OUTER JOIN grouped_domain_alias_address gda ON gda.username = mailbox.username
|
||||||
|
LEFT OUTER JOIN grouped_sender_acl_external external_acl ON external_acl.username = mailbox.username
|
||||||
|
WHERE
|
||||||
|
mailbox.active = '1'";
|
||||||
|
|
||||||
|
if ($mailbox_exists) {
|
||||||
|
$query .= " AND mailbox.username = :mailbox";
|
||||||
|
$stmt = $pdo->prepare($query);
|
||||||
|
$stmt->execute(array(':mailbox' => $mailbox));
|
||||||
|
} else {
|
||||||
|
$query .= " GROUP BY mailbox.username";
|
||||||
|
$stmt = $pdo->query($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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) {
|
||||||
@@ -1739,7 +1778,7 @@ function verify_tfa_login($username, $_data) {
|
|||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $username, '*'),
|
'log' => array(__FUNCTION__, $username, '*'),
|
||||||
'msg' => array('webauthn_verification_failed', 'authenticator not found')
|
'msg' => array('webauthn_authenticator_failed')
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1748,11 +1787,20 @@ function verify_tfa_login($username, $_data) {
|
|||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $username, '*'),
|
'log' => array(__FUNCTION__, $username, '*'),
|
||||||
'msg' => array('webauthn_verification_failed', 'publicKey not found')
|
'msg' => array('webauthn_publickey_failed')
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $username, '*'),
|
||||||
|
'msg' => array('webauthn_username_failed')
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
|
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']);
|
||||||
}
|
}
|
||||||
@@ -1784,21 +1832,12 @@ function verify_tfa_login($username, $_data) {
|
|||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'danger',
|
'type' => 'danger',
|
||||||
'log' => array(__FUNCTION__, $username, '*'),
|
'log' => array(__FUNCTION__, $username, '*'),
|
||||||
'msg' => array('webauthn_verification_failed', 'could not determine user role')
|
'msg' => array('webauthn_role_failed')
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){
|
|
||||||
$_SESSION['return'][] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'log' => array(__FUNCTION__, $username, '*'),
|
|
||||||
'msg' => array('webauthn_verification_failed', 'user who requests does not match with sql entry')
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
|
$_SESSION["mailcow_cc_username"] = $process_webauthn['username'];
|
||||||
$_SESSION['tfa_id'] = $process_webauthn['id'];
|
$_SESSION['tfa_id'] = $process_webauthn['id'];
|
||||||
$_SESSION['authReq'] = null;
|
$_SESSION['authReq'] = null;
|
||||||
@@ -2093,6 +2132,135 @@ function rspamd_ui($action, $data = null) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function cors($action, $data = null) {
|
||||||
|
global $redis;
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case "edit":
|
||||||
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $action, $data),
|
||||||
|
'msg' => 'access_denied'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
foreach ($allowed_origins as $origin) {
|
||||||
|
if (!filter_var($origin, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) && $origin != '*') {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $action, $data),
|
||||||
|
'msg' => 'cors_invalid_origin'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed_methods = isset($data['allowed_methods']) ? $data['allowed_methods'] : array('GET', 'POST', 'PUT', 'DELETE');
|
||||||
|
$allowed_methods = !is_array($allowed_methods) ? array_map('trim', preg_split( "/( |,|;|\n)/", $allowed_methods)) : $allowed_methods;
|
||||||
|
$available_methods = array('GET', 'POST', 'PUT', 'DELETE');
|
||||||
|
foreach ($allowed_methods as $method) {
|
||||||
|
if (!in_array($method, $available_methods)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $action, $data),
|
||||||
|
'msg' => 'cors_invalid_method'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$redis->hMSet('CORS_SETTINGS', array(
|
||||||
|
'allowed_origins' => implode(', ', $allowed_origins),
|
||||||
|
'allowed_methods' => implode(', ', $allowed_methods)
|
||||||
|
));
|
||||||
|
} catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $action, $data),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'success',
|
||||||
|
'log' => array(__FUNCTION__, $action, $data),
|
||||||
|
'msg' => 'cors_headers_edited'
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
|
case "get":
|
||||||
|
try {
|
||||||
|
$cors_settings = $redis->hMGet('CORS_SETTINGS', array('allowed_origins', 'allowed_methods'));
|
||||||
|
} catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $action, $data),
|
||||||
|
'msg' => array('redis_error', $e)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cors_settings = !$cors_settings ? array('allowed_origins' => $_SERVER['SERVER_NAME'], 'allowed_methods' => 'GET, POST, PUT, DELETE') : $cors_settings;
|
||||||
|
$cors_settings['allowed_origins'] = empty($cors_settings['allowed_origins']) ? $_SERVER['SERVER_NAME'] : $cors_settings['allowed_origins'];
|
||||||
|
$cors_settings['allowed_methods'] = empty($cors_settings['allowed_methods']) ? 'GET, POST, PUT, DELETE, OPTION' : $cors_settings['allowed_methods'];
|
||||||
|
|
||||||
|
return $cors_settings;
|
||||||
|
break;
|
||||||
|
case "set_headers":
|
||||||
|
$cors_settings = cors('get');
|
||||||
|
// check if requested origin is in allowed origins
|
||||||
|
$allowed_origins = explode(', ', $cors_settings['allowed_origins']);
|
||||||
|
$cors_settings['allowed_origins'] = $allowed_origins[0];
|
||||||
|
if (in_array('*', $allowed_origins)){
|
||||||
|
$cors_settings['allowed_origins'] = '*';
|
||||||
|
} else if (in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
|
||||||
|
$cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN'];
|
||||||
|
}
|
||||||
|
// always allow OPTIONS for preflight request
|
||||||
|
$cors_settings["allowed_methods"] = empty($cors_settings["allowed_methods"]) ? 'OPTIONS' : $cors_settings["allowed_methods"] . ', ' . 'OPTIONS';
|
||||||
|
|
||||||
|
header('Access-Control-Allow-Origin: ' . $cors_settings['allowed_origins']);
|
||||||
|
header('Access-Control-Allow-Methods: '. $cors_settings['allowed_methods']);
|
||||||
|
header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin');
|
||||||
|
|
||||||
|
// Access-Control settings requested, this is just a preflight request
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS' &&
|
||||||
|
isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) &&
|
||||||
|
isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
|
||||||
|
|
||||||
|
$allowed_methods = explode(', ', $cors_settings["allowed_methods"]);
|
||||||
|
if (in_array($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'], $allowed_methods, true))
|
||||||
|
// method allowed send 200 OK
|
||||||
|
http_response_code(200);
|
||||||
|
else
|
||||||
|
// method not allowed send 405 METHOD NOT ALLOWED
|
||||||
|
http_response_code(405);
|
||||||
|
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function getBaseURL() {
|
||||||
|
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||||
|
$host = $_SERVER['HTTP_HOST'];
|
||||||
|
$base_url = $protocol . '://' . $host;
|
||||||
|
|
||||||
|
return $base_url;
|
||||||
|
}
|
||||||
|
function uuid4() {
|
||||||
|
$data = openssl_random_pseudo_bytes(16);
|
||||||
|
|
||||||
|
$data[6] = chr(ord($data[6]) & 0x0f | 0x40);
|
||||||
|
$data[8] = chr(ord($data[8]) & 0x3f | 0x80);
|
||||||
|
|
||||||
|
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
|
||||||
|
}
|
||||||
|
|
||||||
function get_logs($application, $lines = false) {
|
function get_logs($application, $lines = false) {
|
||||||
if ($lines === false) {
|
if ($lines === false) {
|
||||||
|
|||||||
@@ -325,6 +325,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$timeout2 = intval($_data['timeout2']);
|
$timeout2 = intval($_data['timeout2']);
|
||||||
$skipcrossduplicates = intval($_data['skipcrossduplicates']);
|
$skipcrossduplicates = intval($_data['skipcrossduplicates']);
|
||||||
$automap = intval($_data['automap']);
|
$automap = intval($_data['automap']);
|
||||||
|
$dry = intval($_data['dry']);
|
||||||
$port1 = $_data['port1'];
|
$port1 = $_data['port1'];
|
||||||
$host1 = strtolower($_data['host1']);
|
$host1 = strtolower($_data['host1']);
|
||||||
$password1 = $_data['password1'];
|
$password1 = $_data['password1'];
|
||||||
@@ -435,8 +436,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `timeout1`, `timeout2`, `automap`, `skipcrossduplicates`, `maxbytespersecond`, `subscribeall`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `custom_params`, `active`)
|
$stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `timeout1`, `timeout2`, `automap`, `skipcrossduplicates`, `maxbytespersecond`, `subscribeall`, `dry`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `custom_params`, `active`)
|
||||||
VALUES (:user2, :exclude, :delete1, :delete2, :timeout1, :timeout2, :automap, :skipcrossduplicates, :maxbytespersecond, :subscribeall, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :custom_params, :active)");
|
VALUES (:user2, :exclude, :delete1, :delete2, :timeout1, :timeout2, :automap, :skipcrossduplicates, :maxbytespersecond, :subscribeall, :dry, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :custom_params, :active)");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':user2' => $username,
|
':user2' => $username,
|
||||||
':custom_params' => $custom_params,
|
':custom_params' => $custom_params,
|
||||||
@@ -450,6 +451,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
':skipcrossduplicates' => $skipcrossduplicates,
|
':skipcrossduplicates' => $skipcrossduplicates,
|
||||||
':maxbytespersecond' => $maxbytespersecond,
|
':maxbytespersecond' => $maxbytespersecond,
|
||||||
':subscribeall' => $subscribeall,
|
':subscribeall' => $subscribeall,
|
||||||
|
':dry' => $dry,
|
||||||
':subfolder2' => $subfolder2,
|
':subfolder2' => $subfolder2,
|
||||||
':host1' => $host1,
|
':host1' => $host1,
|
||||||
':authmech1' => 'PLAIN',
|
':authmech1' => 'PLAIN',
|
||||||
@@ -476,16 +478,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',
|
||||||
@@ -518,11 +528,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';
|
||||||
}
|
}
|
||||||
@@ -623,9 +633,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(
|
||||||
@@ -1004,11 +1018,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',
|
||||||
@@ -1017,9 +1043,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;
|
||||||
@@ -1027,7 +1051,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']);
|
||||||
@@ -1225,12 +1249,29 @@ 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;
|
||||||
|
} 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']);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
$stmt = $pdo->prepare("INSERT INTO `user_acl`
|
$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`)
|
||||||
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) ");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username,
|
':username' => $username,
|
||||||
':spam_alias' => $_data['spam_alias'],
|
':spam_alias' => $_data['spam_alias'],
|
||||||
@@ -1249,13 +1290,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
':app_passwds' => $_data['app_passwds']
|
':app_passwds' => $_data['app_passwds']
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
else {
|
catch (PDOException $e) {
|
||||||
$stmt = $pdo->prepare("INSERT INTO `user_acl` (`username`) VALUES (:username)");
|
$_SESSION['return'][] = array(
|
||||||
$stmt->execute(array(
|
'type' => 'danger',
|
||||||
':username' => $username
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
));
|
'msg' => $e->getMessage()
|
||||||
|
);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$_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,
|
||||||
@@ -1264,11 +1309,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update_sogo_static_view($username);
|
||||||
$_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_added', htmlspecialchars($username))
|
'msg' => array('mailbox_added', htmlspecialchars($username))
|
||||||
);
|
);
|
||||||
|
return true;
|
||||||
break;
|
break;
|
||||||
case 'resource':
|
case 'resource':
|
||||||
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
|
$domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46);
|
||||||
@@ -1502,17 +1549,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;
|
||||||
@@ -1531,20 +1578,20 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$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 {
|
} else {
|
||||||
$_data['acl'] = (array)$_data['acl'];
|
$_data['acl'] = (array)$_data['acl'];
|
||||||
$attr['acl_spam_alias'] = 1;
|
$attr['acl_spam_alias'] = 0;
|
||||||
$attr['acl_tls_policy'] = 1;
|
$attr['acl_tls_policy'] = 0;
|
||||||
$attr['acl_spam_score'] = 1;
|
$attr['acl_spam_score'] = 0;
|
||||||
$attr['acl_spam_policy'] = 1;
|
$attr['acl_spam_policy'] = 0;
|
||||||
$attr['acl_delimiter_action'] = 1;
|
$attr['acl_delimiter_action'] = 0;
|
||||||
$attr['acl_syncjobs'] = 0;
|
$attr['acl_syncjobs'] = 0;
|
||||||
$attr['acl_eas_reset'] = 1;
|
$attr['acl_eas_reset'] = 0;
|
||||||
$attr['acl_sogo_profile_reset'] = 0;
|
$attr['acl_sogo_profile_reset'] = 0;
|
||||||
$attr['acl_pushover'] = 1;
|
$attr['acl_pushover'] = 0;
|
||||||
$attr['acl_quarantine'] = 1;
|
$attr['acl_quarantine'] = 0;
|
||||||
$attr['acl_quarantine_attachments'] = 1;
|
$attr['acl_quarantine_attachments'] = 0;
|
||||||
$attr['acl_quarantine_notification'] = 1;
|
$attr['acl_quarantine_notification'] = 0;
|
||||||
$attr['acl_quarantine_category'] = 1;
|
$attr['acl_quarantine_category'] = 0;
|
||||||
$attr['acl_app_passwds'] = 1;
|
$attr['acl_app_passwds'] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2011,6 +2058,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$success = (isset($_data['success'])) ? NULL : $is_now['success'];
|
$success = (isset($_data['success'])) ? NULL : $is_now['success'];
|
||||||
$delete2duplicates = (isset($_data['delete2duplicates'])) ? intval($_data['delete2duplicates']) : $is_now['delete2duplicates'];
|
$delete2duplicates = (isset($_data['delete2duplicates'])) ? intval($_data['delete2duplicates']) : $is_now['delete2duplicates'];
|
||||||
$subscribeall = (isset($_data['subscribeall'])) ? intval($_data['subscribeall']) : $is_now['subscribeall'];
|
$subscribeall = (isset($_data['subscribeall'])) ? intval($_data['subscribeall']) : $is_now['subscribeall'];
|
||||||
|
$dry = (isset($_data['dry'])) ? intval($_data['dry']) : $is_now['dry'];
|
||||||
$delete1 = (isset($_data['delete1'])) ? intval($_data['delete1']) : $is_now['delete1'];
|
$delete1 = (isset($_data['delete1'])) ? intval($_data['delete1']) : $is_now['delete1'];
|
||||||
$delete2 = (isset($_data['delete2'])) ? intval($_data['delete2']) : $is_now['delete2'];
|
$delete2 = (isset($_data['delete2'])) ? intval($_data['delete2']) : $is_now['delete2'];
|
||||||
$automap = (isset($_data['automap'])) ? intval($_data['automap']) : $is_now['automap'];
|
$automap = (isset($_data['automap'])) ? intval($_data['automap']) : $is_now['automap'];
|
||||||
@@ -2144,6 +2192,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
`timeout1` = :timeout1,
|
`timeout1` = :timeout1,
|
||||||
`timeout2` = :timeout2,
|
`timeout2` = :timeout2,
|
||||||
`subscribeall` = :subscribeall,
|
`subscribeall` = :subscribeall,
|
||||||
|
`dry` = :dry,
|
||||||
`active` = :active
|
`active` = :active
|
||||||
WHERE `id` = :id");
|
WHERE `id` = :id");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
@@ -2169,6 +2218,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
':timeout1' => $timeout1,
|
':timeout1' => $timeout1,
|
||||||
':timeout2' => $timeout2,
|
':timeout2' => $timeout2,
|
||||||
':subscribeall' => $subscribeall,
|
':subscribeall' => $subscribeall,
|
||||||
|
':dry' => $dry,
|
||||||
':active' => $active,
|
':active' => $active,
|
||||||
));
|
));
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@@ -3130,7 +3180,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
'msg' => array('mailbox_modified', $username)
|
'msg' => array('mailbox_modified', $username)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
update_sogo_static_view($username);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
break;
|
break;
|
||||||
case 'mailbox_templates':
|
case 'mailbox_templates':
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
@@ -3236,6 +3289,62 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
break;
|
break;
|
||||||
|
case 'mailbox_custom_attribute':
|
||||||
|
$_data['attribute'] = isset($_data['attribute']) ? $_data['attribute'] : array();
|
||||||
|
$_data['attribute'] = is_array($_data['attribute']) ? $_data['attribute'] : array($_data['attribute']);
|
||||||
|
$_data['attribute'] = array_map(function($value) { return str_replace(' ', '', $value); }, $_data['attribute']);
|
||||||
|
$_data['value'] = isset($_data['value']) ? $_data['value'] : array();
|
||||||
|
$_data['value'] = is_array($_data['value']) ? $_data['value'] : array($_data['value']);
|
||||||
|
$attributes = (object)array_combine($_data['attribute'], $_data['value']);
|
||||||
|
$mailboxes = is_array($_data['mailboxes']) ? $_data['mailboxes'] : array($_data['mailboxes']);
|
||||||
|
|
||||||
|
foreach ($mailboxes as $mailbox) {
|
||||||
|
if (!filter_var($mailbox, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => array('username_invalid', $mailbox)
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$is_now = mailbox('get', 'mailbox_details', $mailbox);
|
||||||
|
if(!empty($is_now)){
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => 'access_denied'
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE `mailbox`
|
||||||
|
SET `custom_attributes` = :custom_attributes
|
||||||
|
WHERE username = :username");
|
||||||
|
$stmt->execute(array(
|
||||||
|
":username" => $mailbox,
|
||||||
|
":custom_attributes" => json_encode($attributes)
|
||||||
|
));
|
||||||
|
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'success',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => array('mailbox_modified', $mailbox)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
break;
|
||||||
case 'resource':
|
case 'resource':
|
||||||
if (!is_array($_data['name'])) {
|
if (!is_array($_data['name'])) {
|
||||||
$names = array();
|
$names = array();
|
||||||
@@ -3315,6 +3424,92 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'domain_wide_footer':
|
||||||
|
if (!is_array($_data['domains'])) {
|
||||||
|
$domains = array();
|
||||||
|
$domains[] = $_data['domains'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$domains = $_data['domains'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$footers = array();
|
||||||
|
$footers['html'] = isset($_data['html']) ? $_data['html'] : '';
|
||||||
|
$footers['plain'] = isset($_data['plain']) ? $_data['plain'] : '';
|
||||||
|
$footers['skip_replies'] = isset($_data['skip_replies']) ? (int)$_data['skip_replies'] : 0;
|
||||||
|
$footers['mbox_exclude'] = array();
|
||||||
|
if (isset($_data["mbox_exclude"])){
|
||||||
|
if (!is_array($_data["mbox_exclude"])) {
|
||||||
|
$_data["mbox_exclude"] = array($_data["mbox_exclude"]);
|
||||||
|
}
|
||||||
|
foreach ($_data["mbox_exclude"] as $mailbox) {
|
||||||
|
if (!filter_var($mailbox, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => array('username_invalid', $mailbox)
|
||||||
|
);
|
||||||
|
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) {
|
||||||
|
$domain = idn_to_ascii(strtolower(trim($domain)), 0, INTL_IDNA_VARIANT_UTS46);
|
||||||
|
if (!is_valid_domain_name($domain)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => 'domain_invalid'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => 'access_denied'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("DELETE FROM `domain_wide_footer` WHERE `domain`= :domain");
|
||||||
|
$stmt->execute(array(':domain' => $domain));
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO `domain_wide_footer` (`domain`, `html`, `plain`, `mbox_exclude`, `skip_replies`) VALUES (:domain, :html, :plain, :mbox_exclude, :skip_replies)");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':domain' => $domain,
|
||||||
|
':html' => $footers['html'],
|
||||||
|
':plain' => $footers['plain'],
|
||||||
|
':mbox_exclude' => json_encode($footers['mbox_exclude']),
|
||||||
|
':skip_replies' => $footers['skip_replies'],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
catch (PDOException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => $e->getMessage()
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'success',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => array('domain_footer_modified', htmlspecialchars($domain))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'get':
|
case 'get':
|
||||||
@@ -3867,13 +4062,17 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
|
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$stmt = $pdo->prepare("SELECT `id` FROM `alias` WHERE `address` != `goto` AND `domain` = :domain");
|
$stmt = $pdo->prepare("SELECT `id`, `address` FROM `alias` WHERE `address` != `goto` AND `domain` = :domain");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':domain' => $_data,
|
':domain' => $_data,
|
||||||
));
|
));
|
||||||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
while($row = array_shift($rows)) {
|
while($row = array_shift($rows)) {
|
||||||
$aliases[] = $row['id'];
|
if ($_extra == "address"){
|
||||||
|
$aliases[] = $row['address'];
|
||||||
|
} else {
|
||||||
|
$aliases[] = $row['id'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return $aliases;
|
return $aliases;
|
||||||
break;
|
break;
|
||||||
@@ -3960,6 +4159,39 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
}
|
}
|
||||||
return $aliasdomaindata;
|
return $aliasdomaindata;
|
||||||
break;
|
break;
|
||||||
|
case 'shared_aliases':
|
||||||
|
$shared_aliases = array();
|
||||||
|
$stmt = $pdo->query("SELECT `address` FROM `alias`
|
||||||
|
WHERE `goto` REGEXP ','
|
||||||
|
AND `address` NOT LIKE '@%'
|
||||||
|
AND `goto` != `address`");
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
while($row = array_shift($rows)) {
|
||||||
|
$domain = explode("@", $row['address'])[1];
|
||||||
|
if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
||||||
|
$shared_aliases[] = $row['address'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $shared_aliases;
|
||||||
|
break;
|
||||||
|
case 'direct_aliases':
|
||||||
|
$direct_aliases = array();
|
||||||
|
$stmt = $pdo->query("SELECT `address` FROM `alias`
|
||||||
|
WHERE `goto` NOT LIKE '%,%'
|
||||||
|
AND `address` NOT LIKE '@%'
|
||||||
|
AND `goto` != `address`");
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
while($row = array_shift($rows)) {
|
||||||
|
$domain = explode("@", $row['address'])[1];
|
||||||
|
if (hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) {
|
||||||
|
$direct_aliases[] = $row['address'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $direct_aliases;
|
||||||
|
break;
|
||||||
case 'domains':
|
case 'domains':
|
||||||
$domains = array();
|
$domains = array();
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") {
|
||||||
@@ -4192,6 +4424,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
`mailbox`.`modified`,
|
`mailbox`.`modified`,
|
||||||
`quota2`.`bytes`,
|
`quota2`.`bytes`,
|
||||||
`attributes`,
|
`attributes`,
|
||||||
|
`custom_attributes`,
|
||||||
`quota2`.`messages`
|
`quota2`.`messages`
|
||||||
FROM `mailbox`, `quota2`, `domain`
|
FROM `mailbox`, `quota2`, `domain`
|
||||||
WHERE (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)
|
WHERE (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)
|
||||||
@@ -4212,6 +4445,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
`mailbox`.`modified`,
|
`mailbox`.`modified`,
|
||||||
`quota2replica`.`bytes`,
|
`quota2replica`.`bytes`,
|
||||||
`attributes`,
|
`attributes`,
|
||||||
|
`custom_attributes`,
|
||||||
`quota2replica`.`messages`
|
`quota2replica`.`messages`
|
||||||
FROM `mailbox`, `quota2replica`, `domain`
|
FROM `mailbox`, `quota2replica`, `domain`
|
||||||
WHERE (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)
|
WHERE (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)
|
||||||
@@ -4228,12 +4462,12 @@ 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'];
|
||||||
$mailboxdata['messages'] = $row['messages'];
|
$mailboxdata['messages'] = $row['messages'];
|
||||||
$mailboxdata['attributes'] = json_decode($row['attributes'], true);
|
$mailboxdata['attributes'] = json_decode($row['attributes'], true);
|
||||||
|
$mailboxdata['custom_attributes'] = json_decode($row['custom_attributes'], true);
|
||||||
$mailboxdata['quota_used'] = intval($row['bytes']);
|
$mailboxdata['quota_used'] = intval($row['bytes']);
|
||||||
$mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
|
$mailboxdata['percent_in_use'] = ($row['quota'] == 0) ? '- ' : round((intval($row['bytes']) / intval($row['quota'])) * 100);
|
||||||
$mailboxdata['created'] = $row['created'];
|
$mailboxdata['created'] = $row['created'];
|
||||||
@@ -4394,6 +4628,44 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
}
|
}
|
||||||
return $resourcedata;
|
return $resourcedata;
|
||||||
break;
|
break;
|
||||||
|
case 'domain_wide_footer':
|
||||||
|
$domain = idn_to_ascii(strtolower(trim($_data)), 0, INTL_IDNA_VARIANT_UTS46);
|
||||||
|
if (!is_valid_domain_name($domain)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => 'domain_invalid'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => 'access_denied'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("SELECT `html`, `plain`, `mbox_exclude`, `skip_replies` FROM `domain_wide_footer`
|
||||||
|
WHERE `domain` = :domain");
|
||||||
|
$stmt->execute(array(
|
||||||
|
':domain' => $domain
|
||||||
|
));
|
||||||
|
$footer = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
catch (PDOException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => $e->getMessage()
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $footer;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
@@ -4892,13 +5164,19 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
if (!empty($mailbox_details['domain']) && !empty($mailbox_details['local_part'])) {
|
if (!empty($mailbox_details['domain']) && !empty($mailbox_details['local_part'])) {
|
||||||
$maildir = $mailbox_details['domain'] . '/' . $mailbox_details['local_part'];
|
$maildir = $mailbox_details['domain'] . '/' . $mailbox_details['local_part'];
|
||||||
$exec_fields = array('cmd' => 'maildir', 'task' => 'cleanup', 'maildir' => $maildir);
|
$exec_fields = array('cmd' => 'maildir', 'task' => 'cleanup', 'maildir' => $maildir);
|
||||||
$maildir_gc = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true);
|
|
||||||
if ($maildir_gc['type'] != 'success') {
|
if (getenv("CLUSTERMODE") == "replication") {
|
||||||
$_SESSION['return'][] = array(
|
// broadcast to each dovecot container
|
||||||
'type' => 'warning',
|
docker('broadcast', 'dovecot-mailcow', 'exec', $exec_fields);
|
||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
} else {
|
||||||
'msg' => 'Could not move maildir to garbage collector: ' . $maildir_gc['msg']
|
$maildir_gc = json_decode(docker('post', 'dovecot-mailcow', 'exec', $exec_fields), true);
|
||||||
);
|
if ($maildir_gc['type'] != 'success') {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'warning',
|
||||||
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
|
'msg' => 'Could not move maildir to garbage collector: ' . $maildir_gc['msg']
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -4951,9 +5229,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username
|
':username' => $username
|
||||||
));
|
));
|
||||||
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :username");
|
$stmt = $pdo->prepare("DELETE FROM `sender_acl` WHERE `logged_in_as` = :logged_in_as OR `send_as` = :send_as");
|
||||||
$stmt->execute(array(
|
$stmt->execute(array(
|
||||||
':username' => $username
|
':logged_in_as' => $username,
|
||||||
|
':send_as' => $username
|
||||||
));
|
));
|
||||||
// fk, better safe than sorry
|
// fk, better safe than sorry
|
||||||
$stmt = $pdo->prepare("DELETE FROM `user_acl` WHERE `username` = :username");
|
$stmt = $pdo->prepare("DELETE FROM `user_acl` WHERE `username` = :username");
|
||||||
@@ -5053,12 +5332,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update_sogo_static_view($username);
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
'type' => 'success',
|
'type' => 'success',
|
||||||
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
|
||||||
'msg' => array('mailbox_removed', htmlspecialchars($username))
|
'msg' => array('mailbox_removed', htmlspecialchars($username))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
break;
|
break;
|
||||||
case 'mailbox_templates':
|
case 'mailbox_templates':
|
||||||
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
if ($_SESSION['mailcow_cc_role'] != "admin") {
|
||||||
@@ -5264,7 +5546,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource'))) {
|
if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'resource')) && getenv('SKIP_SOGO') != "y") {
|
||||||
update_sogo_static_view();
|
update_sogo_static_view();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ $globalVariables = [
|
|||||||
'ui_texts' => $UI_TEXTS,
|
'ui_texts' => $UI_TEXTS,
|
||||||
'css_path' => '/cache/'.basename($CSSPath),
|
'css_path' => '/cache/'.basename($CSSPath),
|
||||||
'logo' => customize('get', 'main_logo'),
|
'logo' => customize('get', 'main_logo'),
|
||||||
|
'logo_dark' => customize('get', 'main_logo_dark'),
|
||||||
'available_languages' => $AVAILABLE_LANGUAGES,
|
'available_languages' => $AVAILABLE_LANGUAGES,
|
||||||
'lang' => $lang,
|
'lang' => $lang,
|
||||||
'skip_sogo' => (getenv('SKIP_SOGO') == 'y'),
|
'skip_sogo' => (getenv('SKIP_SOGO') == 'y'),
|
||||||
|
|||||||
+1495
-1465
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user